Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-14 22:11:44 +03:00
parent 72e282956c
commit b327109ca8
3 changed files with 175 additions and 201 deletions

View file

@ -32,14 +32,14 @@ fun createProgressListener(duration: Long, listener: ProgressListener?): Animato
return valueAnimator
}
fun View.unfoldAnimation(animDuration: Long, properties: AnimationProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, properties.yTranslationEnd)
fun View.unfoldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply { playTogether(translate) }
}
fun View.foldAnimation(animDuration: Long, properties: AnimationProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, properties.yTranslationEnd)
fun View.foldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply {
@ -47,17 +47,17 @@ fun View.foldAnimation(animDuration: Long, properties: AnimationProperties): Ani
}
}
fun View.leapAnimation(animDuration: Long, properties: AnimationProperties, overLift: Float): AnimatorSet {
fun View.leapAnimation(animDuration: Long, properties: LeapViewProperties, overLift: Float): AnimatorSet {
val halfDuration = animDuration / 2
val upTo = (height.toFloat() + properties.yTranslationStart + overLift) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, upTo)
val upTo = (height.toFloat() + overLift) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scaleStart, properties.scaleEnd)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scaleStart, properties.scaleEnd)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
@ -65,37 +65,28 @@ fun View.leapAnimation(animDuration: Long, properties: AnimationProperties, over
playTogether(scaleX, scaleY)
}
val elevation = ValueAnimator.ofFloat(properties.elevationStart, properties.elevationEnd)
elevation.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: properties.elevationStart }
elevation.startDelay = halfDuration
elevation.duration = 100
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo, properties.yTranslationEnd)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
translateDown.interpolator = LinearInterpolator()
return AnimatorSet().apply {
playTogether(
translateUp,
scale,
elevation,
translateDown
)
playTogether(translateUp, scale, elevation, translateDown)
}
}
fun View.leapBackAnimation(animDuration: Long, properties: AnimationProperties, calculator: PropertyCalculator): AnimatorSet {
fun View.leapBackAnimation(animDuration: Long, properties: LeapViewProperties, calculator: PropertyCalculator): AnimatorSet {
val halfDuration = animDuration / 2
val upTo = ((height).toFloat() - calculator.yTranslationFactor) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, upTo)
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scaleStart, properties.scaleEnd)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scaleStart, properties.scaleEnd)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
@ -103,34 +94,24 @@ fun View.leapBackAnimation(animDuration: Long, properties: AnimationProperties,
playTogether(scaleX, scaleY)
}
val elevation = ValueAnimator.ofFloat(properties.elevationStart, properties.elevationEnd)
elevation.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: properties.elevationStart }
elevation.startDelay = halfDuration
elevation.duration = 100
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo, properties.yTranslationEnd)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
return AnimatorSet().apply {
playTogether(
translateUp,
scale,
elevation,
translateDown,
)
playTogether(translateUp, scale, elevation, translateDown)
}
}
fun View.pullUpAnimation(leapDuration: Long, properties: AnimationProperties): AnimatorSet {
fun View.pullUpAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
val pullDuration = leapDuration / 2
val delayDuration = leapDuration / 2
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, properties.yTranslationEnd)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scaleStart, properties.scaleEnd)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scaleStart, properties.scaleEnd)
val elevation = ValueAnimator.ofFloat(properties.elevationStart, properties.elevationEnd)
elevation.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: properties.elevationStart }
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
return AnimatorSet().apply {
startDelay = delayDuration
@ -140,15 +121,14 @@ fun View.pullUpAnimation(leapDuration: Long, properties: AnimationProperties): A
}
}
fun View.pullDownAnimation(leapDuration: Long, properties: AnimationProperties): AnimatorSet {
fun View.pullDownAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
val pullDuration = leapDuration / 2
val delayDuration = leapDuration / 4
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, properties.yTranslationEnd)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scaleStart, properties.scaleEnd)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scaleStart, properties.scaleEnd)
val elevation = ValueAnimator.ofFloat(properties.elevationStart, properties.elevationEnd)
elevation.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: properties.elevationStart }
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
return AnimatorSet().apply {
startDelay = delayDuration
@ -156,4 +136,12 @@ fun View.pullDownAnimation(leapDuration: Long, properties: AnimationProperties):
interpolator = DecelerateInterpolator()
playTogether(translate, scaleX, scaleY, elevation)
}
}
fun View.elevationAnimator(start: Float, end: Float, startDelay: Long? = null, duration: Long? = null): Animator {
val elevationAnimator = ValueAnimator.ofFloat(start, end)
elevationAnimator.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: start }
startDelay?.let { elevationAnimator.startDelay = it }
duration?.let { elevationAnimator.duration = it }
return elevationAnimator
}

View file

@ -12,35 +12,33 @@ class LeapView(
private val maximalPosition: Int,
private val calculator: PropertyCalculator,
) {
var currentProperties: AnimationProperties
var state: LeapViewState
private set
var currentPosition: Int = position
private set
var previousPosition: Int = position
private set
val initialState: LeapViewState
init {
val initialProperties = createInitialProperty(position)
currentProperties = AnimationProperties.from(initialProperties, initialProperties)
initView(initialProperties)
state = LeapViewState(index, position, position, initialProperties)
initialState = state.copy()
}
private fun initView(properties: Properties) {
view.elevation = properties.elevation
view.translationY = properties.yTranslation
view.scaleX = properties.scale
view.scaleY = properties.scale
// initialProperties.hasForeground
fun initView() {
applyState(initialState)
}
fun leap(): LeapFrogAnimation {
previousPosition = currentPosition
currentPosition = when (previousPosition) {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
0 -> maximalPosition
else -> currentPosition - 1
else -> state.currentPosition - 1
}
updateProperties(createLeapAnimationProperty(previousPosition, currentPosition))
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == 0 && currentPosition == maximalPosition -> LeapFrogAnimation.LEAP
@ -49,12 +47,16 @@ class LeapView(
}
fun leapBack(): LeapFrogAnimation {
previousPosition = currentPosition
currentPosition = when (previousPosition) {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
maximalPosition -> 0
else -> currentPosition + 1
else -> state.currentPosition + 1
}
updateProperties(createLeapAnimationProperty(previousPosition, currentPosition))
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == maximalPosition && currentPosition == 0 -> LeapFrogAnimation.LEAP
@ -63,66 +65,60 @@ class LeapView(
}
fun fold() {
updateProperties(currentProperties.toFold())
state = state.copy(properties = state.properties.toFold()
)
}
fun unfold() {
updateProperties(currentProperties.toUnfold(calculator))
}
fun getState(): LeapViewState {
return LeapViewState(index, currentPosition, previousPosition, currentProperties)
state = state.copy(properties = state.properties.toUnfold(calculator))
}
fun applyState(state: LeapViewState) {
previousPosition = state.previousPosition
currentPosition = state.currentPosition
updateProperties(state.properties)
initView(state.properties.endProperties())
this.state = state
setProperties(this.state.properties)
}
private fun updateProperties(properties: AnimationProperties) {
currentProperties = properties
private fun setProperties(properties: LeapViewProperties) {
view.elevation = properties.elevationEnd
view.translationY = properties.yTranslation
view.scaleX = properties.scale
view.scaleY = properties.scale
// initialProperties.hasForeground
}
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 createInitialProperty(endPosition: Int): LeapViewProperties {
return LeapViewProperties(
endPosition,
endPosition,
calculator.scale(endPosition),
calculator.elevation(endPosition, maximalPosition + 1),
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 createLeapAnimationProperty(startPosition: Int, endPosition: Int): LeapViewProperties {
return LeapViewProperties(
startPosition,
endPosition,
calculator.scale(endPosition),
calculator.elevation(startPosition, maximalPosition + 1),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.yTranslation(endPosition),
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 LeapViewProperties.toUnfold(calculator: PropertyCalculator): LeapViewProperties {
return this.copy(yTranslation = calculator.yTranslation(this.positionEnd))
}
private fun AnimationProperties.toFold(): AnimationProperties {
val start = this.yTranslationEnd
val end = 0f
return this.copy(yTranslationStart = start, yTranslationEnd = end)
private fun LeapViewProperties.toFold(): LeapViewProperties {
return this.copy(yTranslation = 0f)
}
override fun toString(): String = "index: $index, position: $currentPosition, code: ${view.hashCode()}"
override fun toString(): String = "index: $index, position: ${state.currentPosition}, code: ${view.hashCode()}"
}
@ -130,62 +126,19 @@ data class LeapViewState(
val index: Int,
val currentPosition: Int,
val previousPosition: Int,
val properties: AnimationProperties
val properties: LeapViewProperties
)
enum class LeapFrogAnimation {
LEAP, PULL
}
data class Properties(
val position: Int,
val scale: Float,
val elevation: Float,
val yTranslation: Float,
val hasForeground: Boolean,
)
data class AnimationProperties(
data class LeapViewProperties(
val positionStart: Int,
val positionEnd: Int,
val scaleStart: Float,
val scaleEnd: Float,
val scale: Float,
val elevationStart: Float,
val elevationEnd: Float,
val yTranslationStart: Float,
val yTranslationEnd: Float,
val hasForegroundStart: Boolean,
val hasForegroundEnd: Boolean,
) {
fun endProperties(): Properties {
return Properties(
positionEnd,
scaleEnd,
elevationEnd,
yTranslationEnd,
hasForegroundEnd,
)
}
override fun toString(): String {
return """
scaleStart: $scaleStart, scaleEnd: $scaleEnd,
elevationStart: $elevationStart, elevationEnd: $elevationEnd,
yTranslationStart: $yTranslationStart, yTranslationEnd: $yTranslationEnd,
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
)
}
}
}
val yTranslation: Float,
val hasForeground: Boolean,
)

View file

@ -2,9 +2,9 @@ package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.children
import com.tangem.tangem_sdk_new.extensions.dpToPx
@ -25,60 +25,87 @@ class LeapfrogWidget(
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
private var isFolded: Boolean = true
var isFolded: Boolean = true
private set
init {
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
init()
}
private fun init() {
if (!viewIsFullFledged()) return
val maxPosition = parentContainer.childCount - 1
parentContainer.children.forEachIndexed { index, view ->
val initialPosition = maxPosition - index
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
if (viewIsFullFledged()) {
val maxPosition = parentContainer.childCount - 1
parentContainer.children.forEachIndexed { index, view ->
val initialPosition = maxPosition - index
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
}
}
}
fun fold(listener: ProgressListener? = null) {
if (!canFoldUnfold() || isFolded) return
createFoldUnfoldAnimation(true, foldAnimationDuration, listener)
/**
* Apply initial properties to the views
*/
fun initViews() {
isFolded = true
leapViews.forEach { it.initView() }
}
fun unfold(listener: ProgressListener? = null) {
if (!canFoldUnfold() || !isFolded) return
createFoldUnfoldAnimation(false, foldAnimationDuration, listener)
fun fold(onEnd: () -> Unit = {}) {
val animator = foldAnimator()
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
private fun createFoldUnfoldAnimation(isFoldAnimation: Boolean, duration: Long, listener: ProgressListener?) {
fun unfold(onEnd: () -> Unit = {}) {
val animator = unfoldAnimator()
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun foldAnimator(): Animator? {
if (!canFoldUnfold() || isFolded) return null
return createFoldUnfoldAnimator(true, foldAnimationDuration)
}
fun unfoldAnimator(): Animator? {
if (!canFoldUnfold() || !isFolded) return null
return createFoldUnfoldAnimator(false, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(duration, it.currentProperties))
animatorsList.add(it.view.foldAnimation(duration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(duration, it.currentProperties))
animatorsList.add(it.view.unfoldAnimation(duration, it.state.properties))
}
}
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()
return animator
}
fun leap(listener: ProgressListener? = null) {
if (!canLeap()) return
fun leap(onEnd: () -> Unit = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = leapAnimationDuration
val animatorsList = mutableListOf<Animator>()
@ -86,48 +113,54 @@ class LeapfrogWidget(
when (it.leap()) {
LeapFrogAnimation.LEAP -> {
val overLift = calculator.overLift(leapViews.size)
animatorsList.add(it.view.leapAnimation(duration, it.currentProperties, overLift))
animatorsList.add(it.view.leapAnimation(duration, it.state.properties, overLift))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullUpAnimation(duration, it.currentProperties))
animatorsList.add(it.view.pullUpAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapProgress = it })
animatorsList.add(createProgressListener(duration, listener))
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun leapBack(listener: ProgressListener? = null) {
if (!canLeapBack()) return
fun leapBack(onEnd: () -> Unit = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = leapBackAnimationDuration
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leapBack()) {
LeapFrogAnimation.LEAP -> {
animatorsList.add(it.view.leapBackAnimation(duration, it.currentProperties, calculator))
animatorsList.add(it.view.leapBackAnimation(duration, it.state.properties, calculator))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullDownAnimation(duration, it.currentProperties))
animatorsList.add(it.view.pullDownAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapBackProgress = it })
animatorsList.add(createProgressListener(duration, listener))
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun getViewByPosition(position: Int): View? {
return leapViews.firstOrNull { it.currentPosition == position }?.view
}
fun getViewsCount(): Int = parentContainer.childCount
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(isFolded, leapViews.map { it.getState() })
fun getViewPositionByIndex(index: Int): Int = leapViews.first { it.index == index }.state.currentPosition
fun getViewByPosition(position: Int): LeapView = leapViews.first { it.state.currentPosition == position }
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(isFolded, leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
isFolded = state.isFolded
@ -179,11 +212,11 @@ data class LeapfrogWidgetState(
class PropertyCalculator(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.1f,
val yTranslationFactor: Float = 20f,
val decreaseScaleFactor: Float = 0.2f,
val yTranslationFactor: Float = -40f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 1st view it is view on the top of stack, but it is last in parent.children
// 0 view it is view on the top of stack, but it is last in parent.children
private var translationValueConverter: ((Float) -> Float)? = null
fun setTranslationConverter(converter: (Float) -> Float) {