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 outRatio = min(ratio * 2, 1f)
val detailHeader = binding.detailHeader val detailHeader = binding.detailHeader
detailHeader.scaleX = 1 - 0.05f * outRatio detailHeader.scaleX = 1 - 0.2f * outRatio / (5f / 3f)
detailHeader.scaleY = 1 - 0.05f * outRatio detailHeader.scaleY = 1 - 0.2f * outRatio / (5f / 3f)
detailHeader.alpha = 1 - outRatio detailHeader.alpha = 1 - outRatio
val inRatio = max(ratio - 0.5f, 0f) * 2 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 kotlin.math.roundToInt
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.R 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.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDimenPixels
@ -78,7 +78,7 @@ class ThemedSpeedDialView : SpeedDialView {
@AttrRes defStyleAttr: Int @AttrRes defStyleAttr: Int
) : super(context, attrs, defStyleAttr) ) : super(context, attrs, defStyleAttr)
private val inAnim = StationaryAnim.forMediumComponent(context) private val stationaryConfig = AnimConfig.of(context, AnimConfig.STANDARD, AnimConfig.MEDIUM2)
init { init {
// Work around ripple bug on Android 12 when useCompatPadding = true. // Work around ripple bug on Android 12 when useCompatPadding = true.
@ -142,7 +142,7 @@ class ThemedSpeedDialView : SpeedDialView {
} }
private fun createMainFabAnimator(isOpen: Boolean): Animator { 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 partialDuration = totalDuration / 2 // This is half of the total duration
val delay = totalDuration / 4 // This is one fourth 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 = val animatorSet =
AnimatorSet().apply { AnimatorSet().apply {
playTogether(backgroundTintAnimator, imageTintAnimator, levelAnimator) playTogether(backgroundTintAnimator, imageTintAnimator, levelAnimator)
interpolator = inAnim.interpolator interpolator = stationaryConfig.interpolator
} }
animatorSet.start() animatorSet.start()
return animatorSet return animatorSet

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.home.fastscroll package org.oxycblt.auxio.home.fastscroll
import android.animation.Animator
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
@ -37,8 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
import org.oxycblt.auxio.ui.InAnim import org.oxycblt.auxio.ui.MaterialFader
import org.oxycblt.auxio.ui.OutAnim
import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.isRtl 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) 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 thumbWidth = thumbView.background.intrinsicWidth
private val thumbHeight = thumbView.background.intrinsicHeight private val thumbHeight = thumbView.background.intrinsicHeight
private val thumbPadding = Rect(0, 0, 0, 0) 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 fader = MaterialFader.forSmallComponent(context)
private val popupExit = OutAnim.forSmallComponent(context) private var thumbAnimator: Animator? = null
private var popupAnimator: Animator? = null
private var showingPopup = false private var showingPopup = false
@ -426,12 +424,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
showingThumb = true showingThumb = true
thumbView thumbAnimator?.cancel()
.animate() thumbAnimator = fader.fadeIn(thumbView).also { it.start() }
.scaleX(1f)
.setInterpolator(thumbEnter.interpolator)
.setDuration(thumbEnter.duration)
.start()
} }
private fun hideScrollbar() { private fun hideScrollbar() {
@ -440,12 +434,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
showingThumb = false showingThumb = false
thumbView thumbAnimator?.cancel()
.animate() thumbAnimator = fader.fadeOut(thumbView).also { it.start() }
.scaleX(0f)
.setInterpolator(thumbExit.interpolator)
.setDuration(thumbExit.duration)
.start()
} }
private fun showPopup() { private fun showPopup() {
@ -458,13 +448,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
popupView.alpha = 1f popupView.alpha = 1f
showingPopup = true showingPopup = true
popupView popupAnimator?.cancel()
.animate() popupAnimator = fader.fadeIn(popupView).also { it.start() }
.scaleX(1f)
.scaleY(1f)
.setInterpolator(popupEnter.interpolator)
.setDuration(popupEnter.duration)
.start()
} }
private fun hidePopup() { private fun hidePopup() {
@ -473,14 +458,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
showingPopup = false showingPopup = false
popupView popupAnimator?.cancel()
.animate() popupAnimator = fader.fadeOut(popupView).also { it.start() }
.alpha(0f)
.scaleX(0.75f)
.scaleY(0.75f)
.setInterpolator(popupExit.interpolator)
.setDuration(popupExit.duration)
.start()
} }
// --- LAYOUT STATE --- // --- LAYOUT STATE ---

View file

@ -18,12 +18,12 @@
package org.oxycblt.auxio.playback.ui package org.oxycblt.auxio.playback.ui
import android.animation.ValueAnimator import android.animation.Animator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.ui.MaterialCornerAnim
import org.oxycblt.auxio.ui.RippleFixMaterialButton import org.oxycblt.auxio.ui.RippleFixMaterialButton
import org.oxycblt.auxio.ui.StationaryAnim
import timber.log.Timber as L import timber.log.Timber as L
/** /**
@ -43,9 +43,8 @@ class AnimatedMaterialButton : RippleFixMaterialButton {
defStyleAttr: Int defStyleAttr: Int
) : super(context, attrs, defStyleAttr) ) : super(context, attrs, defStyleAttr)
private var currentCornerRadiusRatio = 0f private var animator: Animator? = null
private var animator: ValueAnimator? = null private val anim = MaterialCornerAnim(context)
private val anim = StationaryAnim.forMediumComponent(context)
override fun setActivated(activated: Boolean) { override fun setActivated(activated: Boolean) {
super.setActivated(activated) super.setActivated(activated)
@ -55,22 +54,12 @@ class AnimatedMaterialButton : RippleFixMaterialButton {
if (!isLaidOut) { if (!isLaidOut) {
// Not laid out, initialize it without animation before drawing. // Not laid out, initialize it without animation before drawing.
L.d("Not laid out, immediately updating corner radius") L.d("Not laid out, immediately updating corner radius")
updateCornerRadiusRatio(targetRadius) shapeAppearanceModel = shapeAppearanceModel.withCornerSize { it.width() * targetRadius }
return return
} }
L.d("Starting corner radius animation") L.d("Starting corner radius animation")
animator?.cancel() animator?.cancel()
animator = animator = anim.animate(this, width * targetRadius).also { it.start() }
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 }
} }
} }

View file

@ -18,14 +18,58 @@
package org.oxycblt.auxio.ui package org.oxycblt.auxio.ui
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.TimeInterpolator import android.animation.TimeInterpolator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context 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 androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.R as MR import com.google.android.material.R as MR
import com.google.android.material.button.MaterialButton
import com.google.android.material.motion.MotionUtils 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( inline fun genericFloat(
from: Float, from: Float,
to: Float, to: Float,
@ -34,52 +78,94 @@ data class Anim(val interpolator: TimeInterpolator, val duration: Long) {
): ValueAnimator = ): ValueAnimator =
ValueAnimator.ofFloat(from, to).apply { ValueAnimator.ofFloat(from, to).apply {
startDelay = delayMs startDelay = delayMs
duration = this@Anim.duration duration = this@AnimConfig.duration
interpolator = this@Anim.interpolator interpolator = this@AnimConfig.interpolator
addUpdateListener { update(animatedValue as Float) } addUpdateListener { update(animatedValue as Float) }
} }
} }
object StationaryAnim { class MaterialCornerAnim(context: Context) {
fun forMediumComponent(context: Context) = private val config = AnimConfig.of(context, AnimConfig.STANDARD, AnimConfig.MEDIUM2)
Anim(
MotionUtils.resolveThemeInterpolator( fun animate(button: MaterialButton, sizeDp: Float): Animator {
context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()), val shapeModel = button.shapeAppearanceModel
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) 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 { class MaterialFader private constructor(context: Context, private val scale: Float) {
fun forSmallComponent(context: Context) = private val alphaOutConfig =
Anim( AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3)
MotionUtils.resolveThemeInterpolator( private val scaleOutConfig =
context, AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.MEDIUM1)
MR.attr.motionEasingStandardDecelerateInterpolator, private val inConfig = AnimConfig.of(context, AnimConfig.EMPHASIZED, AnimConfig.LONG2)
FastOutSlowInInterpolator()),
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300).toLong())
fun forMediumComponent(context: Context) = fun jumpToFadeOut(view: View) {
Anim( view.apply {
MotionUtils.resolveThemeInterpolator( alpha = 0f
context, scaleX = scale
MR.attr.motionEasingEmphasizedDecelerateInterpolator, scaleY = scale
FastOutSlowInInterpolator()), isInvisible = true
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) }
}
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 { class MaterialFlipper(context: Context) {
fun forSmallComponent(context: Context) = private val fader = MaterialFader.forLargeComponent(context)
Anim(
MotionUtils.resolveThemeInterpolator(
context,
MR.attr.motionEasingStandardAccelerateInterpolator,
FastOutSlowInInterpolator()),
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100).toLong())
fun forMediumComponent(context: Context) = fun jump(from: View) {
Anim( fader.jumpToFadeOut(from)
MotionUtils.resolveThemeInterpolator( }
context,
MR.attr.motionEasingEmphasizedAccelerateInterpolator, fun flip(from: View, to: View): Animator {
FastOutSlowInInterpolator()), val outAnimator = fader.fadeOut(from)
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).toLong()) 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 package org.oxycblt.auxio.ui
import android.animation.AnimatorSet import android.animation.Animator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isInvisible
import timber.log.Timber as L import timber.log.Timber as L
class MultiToolbar class MultiToolbar
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) { FrameLayout(context, attrs, defStyleAttr) {
private var animator: AnimatorSet? = null private var animator: Animator? = null
private var currentlyVisible = 0 private var currentlyVisible = 0
private val outAnim = OutAnim.forMediumComponent(context) private val flipper = MaterialFlipper(context)
private val inAnim = InAnim.forMediumComponent(context)
override fun onFinishInflate() { override fun onFinishInflate() {
super.onFinishInflate() super.onFinishInflate()
for (i in 1 until childCount) { for (i in 1 until childCount) {
getChildAt(i).apply { getChildAt(i).apply { flipper.jump(this) }
alpha = 0f
isInvisible = true
}
} }
} }
@ -59,56 +53,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// TODO: Animate nicer Material Fade transitions using animators (Normal transitions // TODO: Animate nicer Material Fade transitions using animators (Normal transitions
// don't work due to translation) // don't work due to translation)
// Set up the target transitions for both the inner and selection toolbars. // 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") L.d("Changing toolbar visibility $from -> 0f, $to -> 1f")
animator?.cancel() animator?.cancel()
val outAnimator = animator = flipper.flip(getChildAt(from), getChildAt(to)).also { it.start() }
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()
}
return true return true
} }