ui: start moving to pre-packaged anims
This commit is contained in:
parent
50829a54d3
commit
22ce9988c8
6 changed files with 155 additions and 154 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ---
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
object OutAnim {
|
||||
fun forSmallComponent(context: Context) =
|
||||
Anim(
|
||||
MotionUtils.resolveThemeInterpolator(
|
||||
context,
|
||||
MR.attr.motionEasingStandardAccelerateInterpolator,
|
||||
FastOutSlowInInterpolator()),
|
||||
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100).toLong())
|
||||
|
||||
fun forMediumComponent(context: Context) =
|
||||
Anim(
|
||||
MotionUtils.resolveThemeInterpolator(
|
||||
context,
|
||||
MR.attr.motionEasingEmphasizedAccelerateInterpolator,
|
||||
FastOutSlowInInterpolator()),
|
||||
MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).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)
|
||||
}
|
||||
}
|
||||
|
||||
class MaterialFlipper(context: Context) {
|
||||
private val fader = MaterialFader.forLargeComponent(context)
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue