Updated on 2026-08-14
This commit is contained in:
parent
75a66a1b5b
commit
8ed876086d
3 changed files with 191 additions and 78 deletions
|
|
@ -25,24 +25,29 @@ import android.view.animation.LinearInterpolator
|
|||
|
||||
typealias ProgressListener = (Int) -> Unit
|
||||
|
||||
fun AnimatorSet.createProgressListener(duration: Long, listener: ProgressListener?): Animator {
|
||||
fun createProgressListener(duration: Long, listener: ProgressListener?): Animator {
|
||||
val valueAnimator = ValueAnimator.ofInt(0, 100)
|
||||
valueAnimator.duration = duration
|
||||
valueAnimator.addUpdateListener { listener?.invoke((it.animatedValue as? Int) ?: 0) }
|
||||
return valueAnimator
|
||||
}
|
||||
|
||||
fun View.initAnimation(animDuration: Long, properties: Properties): AnimatorSet {
|
||||
elevation = properties.elevationEnd
|
||||
scaleX = properties.scaleEnd
|
||||
scaleY = properties.scaleEnd
|
||||
|
||||
fun View.unfoldAnimation(animDuration: Long, properties: AnimationProperties): AnimatorSet {
|
||||
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, properties.yTranslationEnd)
|
||||
translate.duration = animDuration
|
||||
return AnimatorSet().apply { playTogether(translate) }
|
||||
}
|
||||
|
||||
fun View.leapAnimation(animDuration: Long, properties: Properties, overLift: Float): AnimatorSet {
|
||||
fun View.foldAnimation(animDuration: Long, properties: AnimationProperties): AnimatorSet {
|
||||
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, properties.yTranslationEnd)
|
||||
translate.duration = animDuration
|
||||
|
||||
return AnimatorSet().apply {
|
||||
playSequentially(translate)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.leapAnimation(animDuration: Long, properties: AnimationProperties, overLift: Float): AnimatorSet {
|
||||
val halfDuration = animDuration / 2
|
||||
|
||||
val upTo = (height.toFloat() + properties.yTranslationStart + overLift) * -1
|
||||
|
|
@ -80,7 +85,7 @@ fun View.leapAnimation(animDuration: Long, properties: Properties, overLift: Flo
|
|||
}
|
||||
}
|
||||
|
||||
fun View.leapBackAnimation(animDuration: Long, properties: Properties, calculator: PropertyCalculator): AnimatorSet {
|
||||
fun View.leapBackAnimation(animDuration: Long, properties: AnimationProperties, calculator: PropertyCalculator): AnimatorSet {
|
||||
val halfDuration = animDuration / 2
|
||||
|
||||
val upTo = ((height).toFloat() - calculator.yTranslationFactor) * -1
|
||||
|
|
@ -117,7 +122,7 @@ fun View.leapBackAnimation(animDuration: Long, properties: Properties, calculato
|
|||
}
|
||||
}
|
||||
|
||||
fun View.pullUpAnimation(leapDuration: Long, properties: Properties): AnimatorSet {
|
||||
fun View.pullUpAnimation(leapDuration: Long, properties: AnimationProperties): AnimatorSet {
|
||||
val pullDuration = leapDuration / 2
|
||||
val delayDuration = leapDuration / 2
|
||||
|
||||
|
|
@ -135,7 +140,7 @@ fun View.pullUpAnimation(leapDuration: Long, properties: Properties): AnimatorSe
|
|||
}
|
||||
}
|
||||
|
||||
fun View.pullDownAnimation(leapDuration: Long, properties: Properties): AnimatorSet {
|
||||
fun View.pullDownAnimation(leapDuration: Long, properties: AnimationProperties): AnimatorSet {
|
||||
val pullDuration = leapDuration / 2
|
||||
val delayDuration = leapDuration / 4
|
||||
|
||||
|
|
|
|||
|
|
@ -9,36 +9,124 @@ class LeapView(
|
|||
val view: View,
|
||||
val index: Int,
|
||||
position: Int,
|
||||
val maximalPosition: Int,
|
||||
val initialProperties: Properties
|
||||
private val maximalPosition: Int,
|
||||
private val calculator: PropertyCalculator,
|
||||
) {
|
||||
var currentProperties: AnimationProperties
|
||||
private set
|
||||
|
||||
var currentPosition: Int = position
|
||||
private set
|
||||
var previousPosition: Int = position
|
||||
private set
|
||||
|
||||
fun changePositionByLeap() {
|
||||
init {
|
||||
val initialProperties = createInitialProperty(position)
|
||||
currentProperties = AnimationProperties.from(initialProperties, initialProperties)
|
||||
|
||||
view.elevation = initialProperties.elevation
|
||||
view.translationY = initialProperties.yTranslation
|
||||
view.scaleX = initialProperties.scale
|
||||
view.scaleY = initialProperties.scale
|
||||
// initialProperties.hasForeground
|
||||
}
|
||||
|
||||
fun leap(): LeapFrogAnimation {
|
||||
previousPosition = currentPosition
|
||||
currentPosition = when (previousPosition) {
|
||||
0 -> maximalPosition
|
||||
else -> currentPosition - 1
|
||||
}
|
||||
updateProperties(createLeapAnimationProperty(previousPosition, currentPosition))
|
||||
|
||||
return when {
|
||||
previousPosition == 0 && currentPosition == maximalPosition -> LeapFrogAnimation.LEAP
|
||||
else -> LeapFrogAnimation.PULL
|
||||
}
|
||||
}
|
||||
|
||||
fun changePositionByLeapBack() {
|
||||
fun leapBack(): LeapFrogAnimation {
|
||||
previousPosition = currentPosition
|
||||
currentPosition = when (previousPosition) {
|
||||
maximalPosition -> 0
|
||||
else -> currentPosition + 1
|
||||
}
|
||||
updateProperties(createLeapAnimationProperty(previousPosition, currentPosition))
|
||||
|
||||
return when {
|
||||
previousPosition == maximalPosition && currentPosition == 0 -> LeapFrogAnimation.LEAP
|
||||
else -> LeapFrogAnimation.PULL
|
||||
}
|
||||
}
|
||||
|
||||
fun fold() {
|
||||
updateProperties(currentProperties.toFold())
|
||||
}
|
||||
|
||||
fun unfold() {
|
||||
updateProperties(currentProperties.toUnfold(calculator))
|
||||
}
|
||||
|
||||
private fun updateProperties(properties: AnimationProperties) {
|
||||
currentProperties = properties
|
||||
}
|
||||
|
||||
private fun createInitialProperty(endPosition: Int): Properties {
|
||||
return Properties(
|
||||
endPosition,
|
||||
calculator.scale(endPosition),
|
||||
calculator.elevation(endPosition, maximalPosition + 1),
|
||||
calculator.yTranslation(0),
|
||||
calculator.hasForeground(endPosition),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createLeapAnimationProperty(startPosition: Int, endPosition: Int): AnimationProperties {
|
||||
return AnimationProperties(
|
||||
startPosition,
|
||||
endPosition,
|
||||
calculator.scale(startPosition),
|
||||
calculator.scale(endPosition),
|
||||
calculator.elevation(startPosition, maximalPosition + 1),
|
||||
calculator.elevation(endPosition, maximalPosition + 1),
|
||||
calculator.yTranslation(startPosition),
|
||||
calculator.yTranslation(endPosition),
|
||||
calculator.hasForeground(startPosition),
|
||||
calculator.hasForeground(endPosition),
|
||||
)
|
||||
}
|
||||
|
||||
private fun AnimationProperties.toUnfold(calculator: PropertyCalculator): AnimationProperties {
|
||||
val start = this.yTranslationEnd
|
||||
val end = calculator.yTranslation(this.positionEnd)
|
||||
return this.copy(yTranslationStart = start, yTranslationEnd = end)
|
||||
}
|
||||
|
||||
private fun AnimationProperties.toFold(): AnimationProperties {
|
||||
val start = this.yTranslationEnd
|
||||
val end = 0f
|
||||
return this.copy(yTranslationStart = start, yTranslationEnd = end)
|
||||
}
|
||||
|
||||
override fun toString(): String = "index: $index, position: $currentPosition, code: ${view.hashCode()}"
|
||||
|
||||
}
|
||||
|
||||
enum class LeapFrogAnimation {
|
||||
LEAP, PULL
|
||||
}
|
||||
|
||||
data class Properties(
|
||||
val startPosition: Int,
|
||||
val endPosition: Int,
|
||||
val position: Int,
|
||||
val scale: Float,
|
||||
val elevation: Float,
|
||||
val yTranslation: Float,
|
||||
val hasForeground: Boolean,
|
||||
)
|
||||
|
||||
data class AnimationProperties(
|
||||
val positionStart: Int,
|
||||
val positionEnd: Int,
|
||||
val scaleStart: Float,
|
||||
val scaleEnd: Float,
|
||||
val elevationStart: Float,
|
||||
|
|
@ -48,6 +136,7 @@ data class Properties(
|
|||
val hasForegroundStart: Boolean,
|
||||
val hasForegroundEnd: Boolean,
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
return """
|
||||
scaleStart: $scaleStart, scaleEnd: $scaleEnd,
|
||||
|
|
@ -56,4 +145,16 @@ data class Properties(
|
|||
hasForegroundStart : $hasForegroundStart, hasForegroundEnd: $hasForegroundEnd,
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(start: Properties, end: Properties): AnimationProperties {
|
||||
return AnimationProperties(
|
||||
start.position, end.position,
|
||||
start.scale, end.scale,
|
||||
start.elevation, end.elevation,
|
||||
start.yTranslation, end.yTranslation,
|
||||
start.hasForeground, end.hasForeground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,15 +15,18 @@ class LeapfrogWidget(
|
|||
private val parentContainer: FrameLayout,
|
||||
private val calculator: PropertyCalculator = PropertyCalculator(),
|
||||
) {
|
||||
private val initAnimationDuration = 500L
|
||||
private val foldAnimationDuration = 500L
|
||||
private val leapAnimationDuration = 800L
|
||||
private val leapBackAnimationDuration = 800L
|
||||
|
||||
private val leapViews = mutableListOf<LeapView>()
|
||||
|
||||
private var foldUnfoldProgress: Int = 100
|
||||
private var leapProgress: Int = 100
|
||||
private var leapBackProgress: Int = 100
|
||||
|
||||
private var isFolded: Boolean = true
|
||||
|
||||
init {
|
||||
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
|
||||
init()
|
||||
|
|
@ -35,54 +38,65 @@ class LeapfrogWidget(
|
|||
val maxPosition = parentContainer.childCount - 1
|
||||
parentContainer.children.forEachIndexed { index, view ->
|
||||
val initialPosition = maxPosition - index
|
||||
val initialProperties = createViewProperty(0, initialPosition)
|
||||
leapViews.add(LeapView(view, index, initialPosition, maxPosition, initialProperties))
|
||||
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
|
||||
}
|
||||
resetToInitialState()
|
||||
}
|
||||
|
||||
private fun resetToInitialState(withAnimation: Boolean = true, duration: Long = initAnimationDuration) {
|
||||
if (!viewIsFullFledged()) return
|
||||
fun fold(listener: ProgressListener? = null) {
|
||||
if (!canFoldUnfold() || isFolded) return
|
||||
|
||||
if (withAnimation) {
|
||||
val animatorsList = mutableListOf<AnimatorSet>()
|
||||
leapViews.forEach { animatorsList.add(it.view.initAnimation(duration, it.initialProperties)) }
|
||||
val animator = AnimatorSet()
|
||||
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||
animator.playTogether(animatorsList.toList())
|
||||
animator.start()
|
||||
} else {
|
||||
leapViews.forEach {
|
||||
it.view.scaleX = it.initialProperties.scaleEnd
|
||||
it.view.scaleY = it.initialProperties.scaleEnd
|
||||
it.view.translationY = it.initialProperties.yTranslationEnd
|
||||
it.view.elevation = it.initialProperties.elevationEnd
|
||||
createFoldUnfoldAnimation(true, foldAnimationDuration, listener)
|
||||
}
|
||||
|
||||
fun unfold(listener: ProgressListener? = null) {
|
||||
if (!canFoldUnfold() || !isFolded) return
|
||||
|
||||
createFoldUnfoldAnimation(false, foldAnimationDuration, listener)
|
||||
}
|
||||
|
||||
private fun createFoldUnfoldAnimation(isFoldAnimation: Boolean, duration: Long, listener: ProgressListener?) {
|
||||
val animatorsList = mutableListOf<Animator>()
|
||||
leapViews.forEach {
|
||||
if (isFoldAnimation) {
|
||||
it.fold()
|
||||
animatorsList.add(it.view.foldAnimation(duration, it.currentProperties))
|
||||
} else {
|
||||
it.unfold()
|
||||
animatorsList.add(it.view.unfoldAnimation(duration, it.currentProperties))
|
||||
}
|
||||
}
|
||||
animatorsList.add(createProgressListener(duration) {
|
||||
foldUnfoldProgress = it
|
||||
if (it == 100) isFolded = isFoldAnimation
|
||||
})
|
||||
animatorsList.add(createProgressListener(duration, listener))
|
||||
|
||||
val animator = AnimatorSet()
|
||||
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||
animator.playTogether(animatorsList.toList())
|
||||
animator.start()
|
||||
}
|
||||
|
||||
fun leap(listener: ProgressListener? = null) {
|
||||
if (!canLeap()) return
|
||||
|
||||
// if not - then it's ready for pullUp
|
||||
fun isReadyForLeap(leapView: LeapView): Boolean {
|
||||
return leapView.previousPosition == 0 && leapView.currentPosition == leapView.maximalPosition
|
||||
}
|
||||
|
||||
val duration = leapAnimationDuration
|
||||
val animatorsList = mutableListOf<Animator>()
|
||||
leapViews.forEach {
|
||||
it.changePositionByLeap()
|
||||
val properties = createViewProperty(it.previousPosition, it.currentPosition)
|
||||
if (isReadyForLeap(it)) {
|
||||
val overLift = calculator.overLift(leapViews.size)
|
||||
animatorsList.add(it.view.leapAnimation(leapAnimationDuration, properties, overLift))
|
||||
} else {
|
||||
animatorsList.add(it.view.pullUpAnimation(leapAnimationDuration, properties))
|
||||
when (it.leap()) {
|
||||
LeapFrogAnimation.LEAP -> {
|
||||
val overLift = calculator.overLift(leapViews.size)
|
||||
animatorsList.add(it.view.leapAnimation(duration, it.currentProperties, overLift))
|
||||
}
|
||||
LeapFrogAnimation.PULL -> {
|
||||
animatorsList.add(it.view.pullUpAnimation(duration, it.currentProperties))
|
||||
}
|
||||
}
|
||||
}
|
||||
animatorsList.add(createProgressListener(duration) { leapProgress = it })
|
||||
animatorsList.add(createProgressListener(duration, listener))
|
||||
|
||||
val animator = AnimatorSet()
|
||||
animatorsList.add(animator.createProgressListener(leapAnimationDuration) { leapProgress = it })
|
||||
animatorsList.add(animator.createProgressListener(leapAnimationDuration, listener))
|
||||
animator.playTogether(animatorsList.toList())
|
||||
animator.start()
|
||||
}
|
||||
|
|
@ -90,25 +104,21 @@ class LeapfrogWidget(
|
|||
fun leapBack(listener: ProgressListener? = null) {
|
||||
if (!canLeapBack()) return
|
||||
|
||||
// if not - then it's ready for pullDown
|
||||
fun isReadyForLeapBack(leapView: LeapView): Boolean {
|
||||
return leapView.previousPosition == leapView.maximalPosition && leapView.currentPosition == 0
|
||||
}
|
||||
|
||||
val duration = leapBackAnimationDuration
|
||||
val animatorsList = mutableListOf<Animator>()
|
||||
leapViews.forEach {
|
||||
it.changePositionByLeapBack()
|
||||
val properties = createViewProperty(it.previousPosition, it.currentPosition)
|
||||
if (isReadyForLeapBack(it)) {
|
||||
val overLift = calculator.overLift(leapViews.size)
|
||||
animatorsList.add(it.view.leapBackAnimation(leapBackAnimationDuration, properties, calculator))
|
||||
} else {
|
||||
animatorsList.add(it.view.pullDownAnimation(leapBackAnimationDuration, properties))
|
||||
when (it.leapBack()) {
|
||||
LeapFrogAnimation.LEAP -> {
|
||||
animatorsList.add(it.view.leapBackAnimation(duration, it.currentProperties, calculator))
|
||||
}
|
||||
LeapFrogAnimation.PULL -> {
|
||||
animatorsList.add(it.view.pullDownAnimation(duration, it.currentProperties))
|
||||
}
|
||||
}
|
||||
}
|
||||
animatorsList.add(createProgressListener(duration) { leapBackProgress = it })
|
||||
animatorsList.add(createProgressListener(duration, listener))
|
||||
val animator = AnimatorSet()
|
||||
animatorsList.add(animator.createProgressListener(leapBackAnimationDuration) { leapBackProgress = it })
|
||||
animatorsList.add(animator.createProgressListener(leapBackAnimationDuration, listener))
|
||||
animator.playTogether(animatorsList.toList())
|
||||
animator.start()
|
||||
}
|
||||
|
|
@ -119,9 +129,19 @@ class LeapfrogWidget(
|
|||
|
||||
private fun viewIsFullFledged(): Boolean = parentContainer.childCount > 1
|
||||
|
||||
private fun canFoldUnfold(): Boolean {
|
||||
return when {
|
||||
!viewIsFullFledged() -> false
|
||||
foldUnfoldInProgress() -> false
|
||||
leapInProgress() || leapBackInProgress() -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun canLeap(): Boolean {
|
||||
return when {
|
||||
!viewIsFullFledged() -> false
|
||||
isFolded -> false
|
||||
leapBackInProgress() -> false
|
||||
leapProgress < 70 -> false
|
||||
else -> true
|
||||
|
|
@ -131,29 +151,16 @@ class LeapfrogWidget(
|
|||
private fun canLeapBack(): Boolean {
|
||||
return when {
|
||||
!viewIsFullFledged() -> false
|
||||
isFolded -> false
|
||||
leapInProgress() -> false
|
||||
leapBackProgress < 70 -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun foldUnfoldInProgress(): Boolean = foldUnfoldProgress != 100
|
||||
private fun leapInProgress(): Boolean = leapProgress != 100
|
||||
private fun leapBackInProgress(): Boolean = leapBackProgress != 100
|
||||
|
||||
private fun createViewProperty(startPosition: Int, endPosition: Int): Properties {
|
||||
return Properties(
|
||||
startPosition,
|
||||
endPosition,
|
||||
calculator.scale(startPosition),
|
||||
calculator.scale(endPosition),
|
||||
calculator.elevation(startPosition, parentContainer.childCount),
|
||||
calculator.elevation(endPosition, parentContainer.childCount),
|
||||
calculator.yTranslation(startPosition),
|
||||
calculator.yTranslation(endPosition),
|
||||
calculator.hasForeground(startPosition),
|
||||
calculator.hasForeground(endPosition),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyCalculator(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue