diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 675f5c198..355c9961e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -115,8 +115,8 @@ abstract class DetailFragment

: val outRatio = min(ratio * 2, 1f) val detailHeader = binding.detailHeader - detailHeader.scaleX = 1 - 0.05f * outRatio - detailHeader.scaleY = 1 - 0.05f * outRatio + detailHeader.scaleX = 1 - 0.2f * outRatio / (5f / 3f) + detailHeader.scaleY = 1 - 0.2f * outRatio / (5f / 3f) detailHeader.alpha = 1 - outRatio val inRatio = max(ratio - 0.5f, 0f) * 2 diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt index 2637fb331..d111899e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt @@ -46,7 +46,7 @@ import com.leinardi.android.speeddial.SpeedDialView import kotlin.math.roundToInt import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.R -import org.oxycblt.auxio.ui.StationaryAnim +import org.oxycblt.auxio.ui.AnimConfig import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimenPixels @@ -78,7 +78,7 @@ class ThemedSpeedDialView : SpeedDialView { @AttrRes defStyleAttr: Int ) : super(context, attrs, defStyleAttr) - private val inAnim = StationaryAnim.forMediumComponent(context) + private val stationaryConfig = AnimConfig.of(context, AnimConfig.STANDARD, AnimConfig.MEDIUM2) init { // Work around ripple bug on Android 12 when useCompatPadding = true. @@ -142,7 +142,7 @@ class ThemedSpeedDialView : SpeedDialView { } private fun createMainFabAnimator(isOpen: Boolean): Animator { - val totalDuration = inAnim.duration + val totalDuration = stationaryConfig.duration val partialDuration = totalDuration / 2 // This is half of the total duration val delay = totalDuration / 4 // This is one fourth of the total duration @@ -174,7 +174,7 @@ class ThemedSpeedDialView : SpeedDialView { val animatorSet = AnimatorSet().apply { playTogether(backgroundTintAnimator, imageTintAnimator, levelAnimator) - interpolator = inAnim.interpolator + interpolator = stationaryConfig.interpolator } animatorSet.start() return animatorSet diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 4631750e1..535d47fb8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.home.fastscroll +import android.animation.Animator import android.content.Context import android.graphics.Canvas import android.graphics.Rect @@ -37,8 +38,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.AuxioRecyclerView -import org.oxycblt.auxio.ui.InAnim -import org.oxycblt.auxio.ui.OutAnim +import org.oxycblt.auxio.ui.MaterialFader import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.isRtl @@ -84,9 +84,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr background = context.getDrawableCompat(R.drawable.ui_scroll_thumb) } - private val thumbEnter = InAnim.forSmallComponent(context) - private val thumbExit = OutAnim.forSmallComponent(context) - private val thumbWidth = thumbView.background.intrinsicWidth private val thumbHeight = thumbView.background.intrinsicHeight private val thumbPadding = Rect(0, 0, 0, 0) @@ -114,8 +111,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - private val popupEnter = InAnim.forSmallComponent(context) - private val popupExit = OutAnim.forSmallComponent(context) + private val fader = MaterialFader.forSmallComponent(context) + private var thumbAnimator: Animator? = null + private var popupAnimator: Animator? = null private var showingPopup = false @@ -426,12 +424,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingThumb = true - thumbView - .animate() - .scaleX(1f) - .setInterpolator(thumbEnter.interpolator) - .setDuration(thumbEnter.duration) - .start() + thumbAnimator?.cancel() + thumbAnimator = fader.fadeIn(thumbView).also { it.start() } } private fun hideScrollbar() { @@ -440,12 +434,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingThumb = false - thumbView - .animate() - .scaleX(0f) - .setInterpolator(thumbExit.interpolator) - .setDuration(thumbExit.duration) - .start() + thumbAnimator?.cancel() + thumbAnimator = fader.fadeOut(thumbView).also { it.start() } } private fun showPopup() { @@ -458,13 +448,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr popupView.alpha = 1f showingPopup = true - popupView - .animate() - .scaleX(1f) - .scaleY(1f) - .setInterpolator(popupEnter.interpolator) - .setDuration(popupEnter.duration) - .start() + popupAnimator?.cancel() + popupAnimator = fader.fadeIn(popupView).also { it.start() } } private fun hidePopup() { @@ -473,14 +458,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingPopup = false - popupView - .animate() - .alpha(0f) - .scaleX(0.75f) - .scaleY(0.75f) - .setInterpolator(popupExit.interpolator) - .setDuration(popupExit.duration) - .start() + popupAnimator?.cancel() + popupAnimator = fader.fadeOut(popupView).also { it.start() } } // --- LAYOUT STATE --- diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index b20d6c47b..29accb6f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -18,12 +18,12 @@ package org.oxycblt.auxio.playback.ui -import android.animation.ValueAnimator +import android.animation.Animator import android.content.Context import android.util.AttributeSet import com.google.android.material.button.MaterialButton +import org.oxycblt.auxio.ui.MaterialCornerAnim import org.oxycblt.auxio.ui.RippleFixMaterialButton -import org.oxycblt.auxio.ui.StationaryAnim import timber.log.Timber as L /** @@ -43,9 +43,8 @@ class AnimatedMaterialButton : RippleFixMaterialButton { defStyleAttr: Int ) : super(context, attrs, defStyleAttr) - private var currentCornerRadiusRatio = 0f - private var animator: ValueAnimator? = null - private val anim = StationaryAnim.forMediumComponent(context) + private var animator: Animator? = null + private val anim = MaterialCornerAnim(context) override fun setActivated(activated: Boolean) { super.setActivated(activated) @@ -55,22 +54,12 @@ class AnimatedMaterialButton : RippleFixMaterialButton { if (!isLaidOut) { // Not laid out, initialize it without animation before drawing. L.d("Not laid out, immediately updating corner radius") - updateCornerRadiusRatio(targetRadius) + shapeAppearanceModel = shapeAppearanceModel.withCornerSize { it.width() * targetRadius } return } L.d("Starting corner radius animation") animator?.cancel() - animator = - anim - .genericFloat(currentCornerRadiusRatio, targetRadius, 0, ::updateCornerRadiusRatio) - .also { it.start() } - } - - private fun updateCornerRadiusRatio(ratio: Float) { - currentCornerRadiusRatio = ratio - // Can't reproduce the intrinsic ratio corner radius, just manually implement it with - // a dimension value. - shapeAppearanceModel = shapeAppearanceModel.withCornerSize { it.width() * ratio } + animator = anim.animate(this, width * targetRadius).also { it.start() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index 20f6befe5..db54ba9eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -18,14 +18,58 @@ package org.oxycblt.auxio.ui +import android.animation.Animator +import android.animation.AnimatorSet import android.animation.TimeInterpolator import android.animation.ValueAnimator import android.content.Context +import android.graphics.Rect +import android.view.View +import androidx.annotation.AttrRes +import androidx.core.graphics.toRectF +import androidx.core.view.isInvisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.google.android.material.R as MR +import com.google.android.material.button.MaterialButton import com.google.android.material.motion.MotionUtils -data class Anim(val interpolator: TimeInterpolator, val duration: Long) { +class AnimConfig( + context: Context, + @AttrRes interpolatorRes: Int, + @AttrRes durationRes: Int, + defaultDuration: Int +) { + val interpolator: TimeInterpolator = + MotionUtils.resolveThemeInterpolator(context, interpolatorRes, FastOutSlowInInterpolator()) + val duration: Long = + MotionUtils.resolveThemeDuration(context, durationRes, defaultDuration).toLong() + + companion object { + val STANDARD = MR.attr.motionEasingStandardInterpolator + val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator + val EMPHASIZED_ACCELERATE = MR.attr.motionEasingEmphasizedAccelerateInterpolator + val EMPHASIZED_DECELERATE = MR.attr.motionEasingEmphasizedDecelerateInterpolator + val SHORT1 = MR.attr.motionDurationShort1 to 50 + val SHORT2 = MR.attr.motionDurationShort2 to 100 + val SHORT3 = MR.attr.motionDurationShort3 to 150 + val SHORT4 = MR.attr.motionDurationShort4 to 200 + val MEDIUM1 = MR.attr.motionDurationMedium1 to 250 + val MEDIUM2 = MR.attr.motionDurationMedium2 to 300 + val MEDIUM3 = MR.attr.motionDurationMedium3 to 350 + val MEDIUM4 = MR.attr.motionDurationMedium4 to 400 + val LONG1 = MR.attr.motionDurationLong1 to 450 + val LONG2 = MR.attr.motionDurationLong2 to 500 + val LONG3 = MR.attr.motionDurationLong3 to 550 + val LONG4 = MR.attr.motionDurationLong4 to 600 + val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700 + val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800 + val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900 + val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000 + + fun of(context: Context, @AttrRes interpolator: Int, duration: Pair) = + AnimConfig(context, interpolator, duration.first, duration.second) + } + inline fun genericFloat( from: Float, to: Float, @@ -34,52 +78,94 @@ data class Anim(val interpolator: TimeInterpolator, val duration: Long) { ): ValueAnimator = ValueAnimator.ofFloat(from, to).apply { startDelay = delayMs - duration = this@Anim.duration - interpolator = this@Anim.interpolator + duration = this@AnimConfig.duration + interpolator = this@AnimConfig.interpolator addUpdateListener { update(animatedValue as Float) } } } -object StationaryAnim { - fun forMediumComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) +class MaterialCornerAnim(context: Context) { + private val config = AnimConfig.of(context, AnimConfig.STANDARD, AnimConfig.MEDIUM2) + + fun animate(button: MaterialButton, sizeDp: Float): Animator { + val shapeModel = button.shapeAppearanceModel + val bounds = Rect(0, 0, button.width, button.height) + val start = shapeModel.topRightCornerSize.getCornerSize(bounds.toRectF()) + return config.genericFloat(start, sizeDp) { size -> + button.shapeAppearanceModel = shapeModel.withCornerSize { size } + } + } } -object InAnim { - fun forSmallComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingStandardDecelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300).toLong()) +class MaterialFader private constructor(context: Context, private val scale: Float) { + private val alphaOutConfig = + AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3) + private val scaleOutConfig = + AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.MEDIUM1) + private val inConfig = AnimConfig.of(context, AnimConfig.EMPHASIZED, AnimConfig.LONG2) - fun forMediumComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedDecelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) + fun jumpToFadeOut(view: View) { + view.apply { + alpha = 0f + scaleX = scale + scaleY = scale + isInvisible = true + } + } + + fun jumpToFadeIn(view: View) { + view.apply { + alpha = 1f + scaleX = 1.0f + scaleY = 1.0f + isInvisible = false + } + } + + fun fadeOut(view: View): Animator { + if (!view.isLaidOut) { + jumpToFadeOut(view) + return AnimatorSet() + } + + val alphaAnimator = alphaOutConfig.genericFloat(view.alpha, 0f) { view.alpha = it } + val scaleXAnimator = scaleOutConfig.genericFloat(view.scaleX, scale) { view.scaleX = it } + val scaleYAnimator = scaleOutConfig.genericFloat(view.scaleY, scale) { view.scaleY = it } + return AnimatorSet().apply { playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator) } + } + + fun fadeIn(view: View): Animator { + if (!view.isLaidOut) { + jumpToFadeIn(view) + return AnimatorSet() + } + val alphaAnimator = + inConfig.genericFloat(view.alpha, 1f) { + view.alpha = it + view.isInvisible = view.alpha == 0f + } + val scaleXAnimator = inConfig.genericFloat(view.scaleX, 1.0f) { view.scaleX = it } + val scaleYAnimator = inConfig.genericFloat(view.scaleY, 1.0f) { view.scaleY = it } + return AnimatorSet().apply { playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator) } + } + + companion object { + fun forSmallComponent(context: Context) = MaterialFader(context, 0.4f) + + fun forLargeComponent(context: Context) = MaterialFader(context, 0.9f) + } } -object OutAnim { - fun forSmallComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingStandardAccelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100).toLong()) +class MaterialFlipper(context: Context) { + private val fader = MaterialFader.forLargeComponent(context) - fun forMediumComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedAccelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).toLong()) + fun jump(from: View) { + fader.jumpToFadeOut(from) + } + + fun flip(from: View, to: View): Animator { + val outAnimator = fader.fadeOut(from) + val inAnimator = fader.fadeIn(to).apply { startDelay = outAnimator.totalDuration } + return AnimatorSet().apply { playTogether(outAnimator, inAnimator) } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index ee0609bf1..f24872842 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -18,33 +18,27 @@ package org.oxycblt.auxio.ui -import android.animation.AnimatorSet +import android.animation.Animator import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.annotation.IdRes -import androidx.appcompat.widget.Toolbar import androidx.core.view.children -import androidx.core.view.isInvisible import timber.log.Timber as L class MultiToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { - private var animator: AnimatorSet? = null + private var animator: Animator? = null private var currentlyVisible = 0 - private val outAnim = OutAnim.forMediumComponent(context) - private val inAnim = InAnim.forMediumComponent(context) + private val flipper = MaterialFlipper(context) override fun onFinishInflate() { super.onFinishInflate() for (i in 1 until childCount) { - getChildAt(i).apply { - alpha = 0f - isInvisible = true - } + getChildAt(i).apply { flipper.jump(this) } } } @@ -59,56 +53,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // TODO: Animate nicer Material Fade transitions using animators (Normal transitions // don't work due to translation) // Set up the target transitions for both the inner and selection toolbars. - val targetFromAlpha = 0f - val targetToAlpha = 1f - val fromView = getChildAt(from) as Toolbar - val toView = getChildAt(to) as Toolbar - - if (fromView.alpha == targetFromAlpha && toView.alpha == targetToAlpha) { - // Nothing to do. - return false - } - - if (!isLaidOut) { - // Not laid out, just change it immediately while are not shown to the user. - // This is an initialization, so we return false despite changing. - L.d("Not laid out, immediately updating visibility") - fromView.apply { - alpha = 0f - isInvisible = true - } - toView.apply { - alpha = 1f - isInvisible = false - } - return false - } - L.d("Changing toolbar visibility $from -> 0f, $to -> 1f") animator?.cancel() - val outAnimator = - outAnim.genericFloat(fromView.alpha, 0f) { - fromView.apply { - scaleX = 1 - 0.05f * (1 - it) - scaleY = 1 - 0.05f * (1 - it) - alpha = it - isInvisible = alpha == 0f - } - } - val inAnimator = - inAnim.genericFloat(toView.alpha, 1f, outAnim.duration) { - toView.apply { - scaleX = 1 - 0.05f * (1 - it) - scaleY = 1 - 0.05f * (1 - it) - alpha = it - isInvisible = alpha == 0f - } - } - animator = - AnimatorSet().apply { - playTogether(outAnimator, inAnimator) - start() - } + animator = flipper.flip(getChildAt(from), getChildAt(to)).also { it.start() } return true }