ui: start moving to pre-packaged anims

This commit is contained in:
Alexander Capehart 2024-10-19 12:26:02 -06:00
parent 50829a54d3
commit 22ce9988c8
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 155 additions and 154 deletions

View file

@ -115,8 +115,8 @@ abstract class DetailFragment<P : MusicParent, C : Music> :
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

View file

@ -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

View file

@ -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 ---

View file

@ -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() }
}
}

View file

@ -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<Int, Int>) =
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) }
}
}

View file

@ -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
}