1818
1919package org.oxycblt.auxio.home
2020
21- import android.animation.Animator
22- import android.animation.AnimatorListenerAdapter
23- import android.animation.AnimatorSet
24- import android.animation.ObjectAnimator
21+ import android.animation.ArgbEvaluator
22+ import android.graphics.Canvas
23+ import android.graphics.ColorFilter
24+ import android.graphics.PixelFormat
25+ import android.graphics.PorterDuff
26+ import android.graphics.Rect
27+ import android.graphics.drawable.Drawable
2528import android.content.Context
2629import android.content.res.ColorStateList
27- import android.graphics.drawable.Drawable
28- import android.graphics.drawable.RotateDrawable
2930import android.os.Bundle
3031import android.os.Parcelable
3132import android.util.AttributeSet
32- import android.util.Property
33- import android.view.View
34- import android.widget.ImageView
3533import android.widget.TextView
3634import androidx.annotation.AttrRes
3735import androidx.annotation.FloatRange
3836import androidx.core.os.BundleCompat
3937import androidx.core.view.setMargins
4038import androidx.core.view.updateLayoutParams
4139import androidx.core.widget.TextViewCompat
40+ import androidx.dynamicanimation.animation.FloatValueHolder
41+ import androidx.dynamicanimation.animation.SpringAnimation
42+ import androidx.dynamicanimation.animation.SpringForce
43+ import com.google.android.material.R as MR
44+ import com.google.android.material.motion.MotionUtils
4245import com.google.android.material.shape.MaterialShapeDrawable
4346import com.leinardi.android.speeddial.FabWithLabelView
4447import com.leinardi.android.speeddial.SpeedDialActionItem
4548import com.leinardi.android.speeddial.SpeedDialView
4649import kotlin.math.roundToInt
4750import kotlinx.parcelize.Parcelize
4851import org.oxycblt.auxio.R
49- import org.oxycblt.auxio.ui.AnimConfig
5052import org.oxycblt.auxio.util.getAttrColorCompat
5153import org.oxycblt.auxio.util.getAttrResourceId
5254import org.oxycblt.auxio.util.getDimen
@@ -65,9 +67,23 @@ import org.oxycblt.auxio.util.getDimenPixels
6567 * @author Hai Zhang, Alexander Capehart (OxygenCobalt)
6668 */
6769class ThemedSpeedDialView : SpeedDialView {
68- private var mainFabAnimator : Animator ? = null
70+ private var mainFabAnimation : MainFabAnimation ? = null
6971 private val spacingSmall = context.getDimenPixels(R .dimen.spacing_small)
7072 private var innerChangeListener: ((Boolean ) -> Unit )? = null
73+ private val mainFabDrawable = RotatingDrawable (checkNotNull(mainFab.drawable).mutate())
74+ private val mainFabSpatialSpring =
75+ MotionUtils .resolveThemeSpringForce(
76+ context,
77+ MR .attr.motionSpringFastSpatial,
78+ MR .style.Motion_Material3_Spring_Standard_Fast_Spatial ,
79+ )
80+ private val mainFabEffectsSpring =
81+ MotionUtils .resolveThemeSpringForce(
82+ context,
83+ MR .attr.motionSpringFastEffects,
84+ MR .style.Motion_Material3_Spring_Standard_Fast_Effects ,
85+ )
86+ private val argbEvaluator = ArgbEvaluator ()
7187
7288 constructor (context: Context ) : super (context)
7389
@@ -79,8 +95,6 @@ class ThemedSpeedDialView : SpeedDialView {
7995 @AttrRes defStyleAttr: Int ,
8096 ) : super (context, attrs, defStyleAttr)
8197
82- private val stationaryConfig = AnimConfig .of(context, AnimConfig .STANDARD , AnimConfig .MEDIUM2 )
83-
8498 init {
8599 // Work around ripple bug on Android 12 when useCompatPadding = true.
86100 // @see https://github.com/material-components/material-components-android/issues/2617
@@ -107,18 +121,15 @@ class ThemedSpeedDialView : SpeedDialView {
107121 .defaultColor
108122
109123 // Always use our own animation to fix the library issue that ripple is rotated as well.
110- val mainFabDrawable =
111- RotateDrawable ().apply {
112- drawable = mainFab.drawable
113- toDegrees = 45f + 90f
114- }
115124 mainFabAnimationRotateAngle = 0f
116125 setMainFabClosedDrawable(mainFabDrawable)
126+ setMainFabOpenedDrawable(null )
117127 setOnChangeListener(
118128 object : OnChangeListener {
119129 override fun onMainActionSelected (): Boolean = false
120130
121131 override fun onToggleChanged (isOpen : Boolean ) {
132+ mainFabAnimation?.cancel()
122133 mainFab.backgroundTintList =
123134 ColorStateList .valueOf(
124135 if (isOpen) mainFabClosedBackgroundColor
@@ -128,67 +139,83 @@ class ThemedSpeedDialView : SpeedDialView {
128139 ColorStateList .valueOf(
129140 if (isOpen) mainFabClosedIconColor else mainFabOpenedIconColor
130141 )
131- mainFabAnimator?.cancel()
132- mainFabAnimator =
133- createMainFabAnimator(isOpen).apply {
134- addListener(
135- object : AnimatorListenerAdapter () {
136- override fun onAnimationEnd (animation : Animator ) {
137- mainFabAnimator = null
138- }
139- }
140- )
141- start()
142- }
142+ mainFabAnimation = createMainFabAnimation(isOpen).apply { start() }
143143 innerChangeListener?.invoke(isOpen)
144144 }
145145 }
146146 )
147147 }
148148
149- private fun createMainFabAnimator (isOpen : Boolean ): Animator {
150- val totalDuration = stationaryConfig.duration
151- val partialDuration = totalDuration / 2 // This is half of the total duration
152- val delay = totalDuration / 4 // This is one fourth of the total duration
149+ private fun createMainFabAnimation (isOpen : Boolean ): MainFabAnimation {
150+ val targetBackgroundTint =
151+ if (isOpen) mainFabOpenedBackgroundColor else mainFabClosedBackgroundColor
152+ val targetImageTint = if (isOpen) mainFabOpenedIconColor else mainFabClosedIconColor
153153
154- val backgroundTintAnimator =
155- ObjectAnimator .ofArgb(
156- mainFab,
157- VIEW_PROPERTY_BACKGROUND_TINT ,
158- if (isOpen) mainFabOpenedBackgroundColor else mainFabClosedBackgroundColor,
159- )
160- .apply {
161- startDelay = delay
162- duration = partialDuration
163- }
154+ val backgroundTintAnimation =
155+ createColorSpringAnimation(
156+ startColor =
157+ if (isOpen) mainFabClosedBackgroundColor else mainFabOpenedBackgroundColor,
158+ endColor = targetBackgroundTint,
159+ ) { mainFab.backgroundTintList = ColorStateList .valueOf(it) }
160+ val imageTintAnimation =
161+ createColorSpringAnimation(
162+ startColor = if (isOpen) mainFabClosedIconColor else mainFabOpenedIconColor,
163+ endColor = targetImageTint,
164+ ) { mainFab.imageTintList = ColorStateList .valueOf(it) }
165+ val rotationAnimation =
166+ createSpringAnimation(
167+ startValue = mainFabDrawable.rotationDegrees,
168+ finalValue = if (isOpen) MAIN_FAB_OPEN_ROTATION_DEGREES else 0f ,
169+ springTemplate = mainFabSpatialSpring,
170+ minimumVisibleChange = MAIN_FAB_ROTATION_MIN_VISIBLE_CHANGE ,
171+ dampingRatioOverride = MAIN_FAB_ROTATION_DAMPING_RATIO_OVERRIDE ,
172+ ) { mainFabDrawable.rotationDegrees = it }
164173
165- val imageTintAnimator =
166- ObjectAnimator .ofArgb(
167- mainFab,
168- IMAGE_VIEW_PROPERTY_IMAGE_TINT ,
169- if (isOpen) mainFabOpenedIconColor else mainFabClosedIconColor,
170- )
171- .apply {
172- startDelay = delay
173- duration = partialDuration
174- }
174+ return MainFabAnimation (
175+ listOf (backgroundTintAnimation, imageTintAnimation, rotationAnimation)
176+ ) {
177+ mainFabAnimation = null
178+ }
179+ }
175180
176- val levelAnimator =
177- ObjectAnimator .ofInt(
178- mainFab.drawable,
179- DRAWABLE_PROPERTY_LEVEL ,
180- if (isOpen) 10000 else 0 ,
181- )
182- .apply { duration = totalDuration }
181+ private fun createColorSpringAnimation (
182+ startColor : Int ,
183+ endColor : Int ,
184+ update : (Int ) -> Unit ,
185+ ): SpringAnimation =
186+ createSpringAnimation(
187+ startValue = 0f ,
188+ finalValue = 1f ,
189+ springTemplate = mainFabEffectsSpring,
190+ minimumVisibleChange = MAIN_FAB_COLOR_PROGRESS_MIN_VISIBLE_CHANGE ,
191+ ) { progress ->
192+ val color =
193+ argbEvaluator.evaluate(progress.coerceIn(0f , 1f ), startColor, endColor) as Int
194+ update(color)
195+ }
183196
184- val animatorSet =
185- AnimatorSet ().apply {
186- playTogether(backgroundTintAnimator, imageTintAnimator, levelAnimator)
187- interpolator = stationaryConfig.interpolator
197+ private fun createSpringAnimation (
198+ startValue : Float ,
199+ finalValue : Float ,
200+ springTemplate : SpringForce ,
201+ minimumVisibleChange : Float ,
202+ dampingRatioOverride : Float? = null,
203+ update : (Float ) -> Unit ,
204+ ): SpringAnimation =
205+ SpringAnimation (FloatValueHolder (startValue)).apply {
206+ spring =
207+ SpringForce ().apply {
208+ dampingRatio = dampingRatioOverride ? : springTemplate.dampingRatio
209+ stiffness = springTemplate.stiffness
210+ finalPosition = finalValue
211+ }
212+ setStartValue(startValue)
213+ setMinimumVisibleChange(minimumVisibleChange)
214+ addUpdateListener { _, value, _ -> update(value) }
215+ addEndListener { _, canceled, value, _ ->
216+ update(if (canceled) value else finalValue)
188217 }
189- animatorSet.start()
190- return animatorSet
191- }
218+ }
192219
193220 override fun onAttachedToWindow () {
194221 super .onAttachedToWindow()
@@ -304,34 +331,140 @@ class ThemedSpeedDialView : SpeedDialView {
304331 innerChangeListener = listener
305332 }
306333
307- companion object {
308- private val VIEW_PROPERTY_BACKGROUND_TINT =
309- object : Property <View , Int >(Int ::class .java, " backgroundTint" ) {
310- override fun get (view : View ): Int = view.backgroundTintList!! .defaultColor
334+ @Parcelize
335+ private class State (val superState : Parcelable ? , val isOpen : Boolean ) : Parcelable
336+
337+ private class MainFabAnimation (
338+ private val animations : List <SpringAnimation >,
339+ private val onEnd : () -> Unit ,
340+ ) {
341+ private var remainingAnimations = animations.size
342+ private var finished = false
311343
312- override fun set (view : View , value : Int? ) {
313- view.backgroundTintList = ColorStateList .valueOf(value!! )
344+ init {
345+ animations.forEach { animation ->
346+ animation.addEndListener { _, _, _, _ ->
347+ if (finished) {
348+ return @addEndListener
349+ }
350+ remainingAnimations - = 1
351+ if (remainingAnimations == 0 ) {
352+ finished = true
353+ onEnd()
354+ }
314355 }
315356 }
357+ }
316358
317- private val IMAGE_VIEW_PROPERTY_IMAGE_TINT =
318- object : Property <ImageView , Int >(Int ::class .java, " imageTint" ) {
319- override fun get (view : ImageView ): Int = view.imageTintList!! .defaultColor
359+ fun start () {
360+ animations.forEach { animation ->
361+ animation.animateToFinalPosition(animation.spring.finalPosition)
362+ }
363+ }
320364
321- override fun set ( view : ImageView , value : Int? ) {
322- view.imageTintList = ColorStateList .valueOf(value !! )
323- }
365+ fun cancel ( ) {
366+ if (finished) {
367+ return
324368 }
369+ finished = true
370+ animations.forEach { it.cancel() }
371+ onEnd()
372+ }
373+ }
374+ }
325375
326- private val DRAWABLE_PROPERTY_LEVEL =
327- object : Property <Drawable , Int >(Int ::class .java, " level" ) {
328- override fun get (drawable : Drawable ): Int = drawable.level
376+ private class RotatingDrawable (
377+ drawable : Drawable ,
378+ ) : Drawable(), Drawable.Callback {
379+ private var wrappedDrawable = drawable
329380
330- override fun set (drawable : Drawable , value : Int? ) {
331- drawable.level = value!!
332- }
381+ var rotationDegrees = 0f
382+ set(value) {
383+ if (field == value) {
384+ return
333385 }
386+ field = value
387+ invalidateSelf()
388+ }
389+
390+ init {
391+ wrappedDrawable.callback = this
392+ }
393+
394+ override fun draw (canvas : Canvas ) {
395+ if (bounds.isEmpty) {
396+ return
397+ }
398+
399+ val saveCount = canvas.save()
400+ canvas.rotate(rotationDegrees, bounds.exactCenterX(), bounds.exactCenterY())
401+ wrappedDrawable.draw(canvas)
402+ canvas.restoreToCount(saveCount)
403+ }
404+
405+ override fun onBoundsChange (bounds : Rect ) {
406+ wrappedDrawable.bounds = bounds
407+ }
408+
409+ override fun onStateChange (state : IntArray ): Boolean {
410+ val changed = wrappedDrawable.setState(state)
411+ if (changed) {
412+ invalidateSelf()
413+ }
414+ return changed
415+ }
416+
417+ override fun isStateful (): Boolean = wrappedDrawable.isStateful
418+
419+ override fun getPadding (padding : Rect ): Boolean = wrappedDrawable.getPadding(padding)
420+
421+ override fun getIntrinsicWidth (): Int = wrappedDrawable.intrinsicWidth
422+
423+ override fun getIntrinsicHeight (): Int = wrappedDrawable.intrinsicHeight
424+
425+ override fun setAlpha (alpha : Int ) {
426+ wrappedDrawable.alpha = alpha
427+ }
428+
429+ override fun getAlpha (): Int = wrappedDrawable.alpha
430+
431+ override fun setColorFilter (colorFilter : ColorFilter ? ) {
432+ wrappedDrawable.colorFilter = colorFilter
433+ }
434+
435+ override fun getOpacity (): Int = PixelFormat .TRANSLUCENT
436+
437+ override fun setTint (tintColor : Int ) {
438+ wrappedDrawable.setTint(tintColor)
439+ }
440+
441+ override fun setTintList (tint : ColorStateList ? ) {
442+ wrappedDrawable.setTintList(tint)
443+ }
444+
445+ override fun setTintMode (tintMode : PorterDuff .Mode ? ) {
446+ wrappedDrawable.setTintMode(tintMode)
334447 }
335448
336- @Parcelize private class State (val superState : Parcelable ? , val isOpen : Boolean ) : Parcelable
449+ override fun setVisible (visible : Boolean , restart : Boolean ): Boolean =
450+ super .setVisible(visible, restart) || wrappedDrawable.setVisible(visible, restart)
451+
452+ override fun onLevelChange (level : Int ): Boolean = wrappedDrawable.setLevel(level)
453+
454+ override fun invalidateDrawable (who : Drawable ) {
455+ invalidateSelf()
456+ }
457+
458+ override fun scheduleDrawable (who : Drawable , what : Runnable , `when `: Long ) {
459+ scheduleSelf(what, `when `)
460+ }
461+
462+ override fun unscheduleDrawable (who : Drawable , what : Runnable ) {
463+ unscheduleSelf(what)
464+ }
337465}
466+
467+ private const val MAIN_FAB_OPEN_ROTATION_DEGREES = 135f
468+ private const val MAIN_FAB_ROTATION_MIN_VISIBLE_CHANGE = 0.1f
469+ private const val MAIN_FAB_ROTATION_DAMPING_RATIO_OVERRIDE = 0.6f
470+ private const val MAIN_FAB_COLOR_PROGRESS_MIN_VISIBLE_CHANGE = 1f / 255f
0 commit comments