Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-07 16:00:46 +03:00
parent 5036f8a383
commit a4e7c2d627
4 changed files with 443 additions and 0 deletions

View file

@ -0,0 +1,154 @@
package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
/**
[REDACTED_AUTHOR]
*/
//fun View.setForeground(hasForegroundAtEnd: Boolean) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// foreground = getForegroundDrawable()
// }
//}
//fun View.getForegroundDrawable(): Drawable? {
// return AppCompatResources.getDrawable(context, R.drawable.shape_rectangle_rounded_8_op_20)?.also {
// it.alpha = 60
// }
//}
typealias ProgressListener = (Int) -> Unit
fun AnimatorSet.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
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 {
val halfDuration = animDuration / 2
val upTo = (height.toFloat() + properties.yTranslationStart + overLift) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslationStart, 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 scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
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)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
translateDown.interpolator = LinearInterpolator()
return AnimatorSet().apply {
playTogether(
translateUp,
scale,
elevation,
translateDown
)
}
}
fun View.leapBackAnimation(animDuration: Long, properties: Properties, 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)
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 scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
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)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
return AnimatorSet().apply {
playTogether(
translateUp,
scale,
elevation,
translateDown,
)
}
}
fun View.pullUpAnimation(leapDuration: Long, properties: Properties): 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 }
return AnimatorSet().apply {
startDelay = delayDuration
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translateUp, scaleX, scaleY, elevation)
}
}
fun View.pullDownAnimation(leapDuration: Long, properties: Properties): 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 }
return AnimatorSet().apply {
startDelay = delayDuration
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translate, scaleX, scaleY, elevation)
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.common.leapfrogWidget
import android.view.View
/**
[REDACTED_AUTHOR]
*/
class LeapView(
val view: View,
val index: Int,
position: Int,
val maximalPosition: Int,
val initialProperties: Properties
) {
var currentPosition: Int = position
private set
var previousPosition: Int = position
private set
fun changePositionByLeap() {
previousPosition = currentPosition
currentPosition = when (previousPosition) {
0 -> maximalPosition
else -> currentPosition - 1
}
}
fun changePositionByLeapBack() {
previousPosition = currentPosition
currentPosition = when (previousPosition) {
maximalPosition -> 0
else -> currentPosition + 1
}
}
override fun toString(): String = "index: $index, position: $currentPosition, code: ${view.hashCode()}"
}
data class Properties(
val startPosition: Int,
val endPosition: Int,
val scaleStart: Float,
val scaleEnd: Float,
val elevationStart: Float,
val elevationEnd: Float,
val yTranslationStart: Float,
val yTranslationEnd: Float,
val hasForegroundStart: Boolean,
val hasForegroundEnd: Boolean,
) {
override fun toString(): String {
return """
scaleStart: $scaleStart, scaleEnd: $scaleEnd,
elevationStart: $elevationStart, elevationEnd: $elevationEnd,
yTranslationStart: $yTranslationStart, yTranslationEnd: $yTranslationEnd,
hasForegroundStart : $hasForegroundStart, hasForegroundEnd: $hasForegroundEnd,
""".trimIndent()
}
}

View file

@ -0,0 +1,204 @@
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.view.children
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
[REDACTED_AUTHOR]
*/
class LeapfrogWidget(
private val parentContainer: FrameLayout,
private val calculator: PropertyCalculator = PropertyCalculator(),
) {
private val initAnimationDuration = 500L
private val leapAnimationDuration = 800L
private val leapBackAnimationDuration = 800L
private val leapViews = mutableListOf<LeapView>()
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
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
val initialProperties = createViewProperty(0, initialPosition)
leapViews.add(LeapView(view, index, initialPosition, maxPosition, initialProperties))
}
resetToInitialState()
}
private fun resetToInitialState(withAnimation: Boolean = true, duration: Long = initAnimationDuration) {
if (!viewIsFullFledged()) 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
}
}
}
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 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))
}
}
val animator = AnimatorSet()
animatorsList.add(animator.createProgressListener(leapAnimationDuration) { leapProgress = it })
animatorsList.add(animator.createProgressListener(leapAnimationDuration, listener))
animator.playTogether(animatorsList.toList())
animator.start()
}
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 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))
}
}
val animator = AnimatorSet()
animatorsList.add(animator.createProgressListener(leapBackAnimationDuration) { leapBackProgress = it })
animatorsList.add(animator.createProgressListener(leapBackAnimationDuration, listener))
animator.playTogether(animatorsList.toList())
animator.start()
}
fun getViewByPosition(position: Int): View? {
return leapViews.firstOrNull { it.currentPosition == position }?.view
}
private fun viewIsFullFledged(): Boolean = parentContainer.childCount > 1
private fun canLeap(): Boolean {
return when {
!viewIsFullFledged() -> false
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
}
}
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> false
leapInProgress() -> false
leapBackProgress < 70 -> false
else -> true
}
}
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(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.1f,
val yTranslationFactor: Float = 20f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 1st 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) {
translationValueConverter = converter
}
fun scale(position: Int): Float {
return 1f - decreaseScaleFactor * position
}
fun elevation(position: Int, count: Int): Float {
return elevationFactor * (count - 1 - position)
}
fun yTranslation(position: Int): Float {
val yTranslation = yTranslationFactor * position
return convertTranslation(yTranslation)
}
fun hasForeground(position: Int): Boolean {
return position != 0
}
fun overLift(viewCount: Int): Float {
val initialOverLift = when {
yTranslationFactor < 0f -> yTranslationFactor * decreaseOverLiftingFactor * viewCount * -1
else -> 10f
}
val scaleOverLift = when {
decreaseScaleFactor == 0f -> initialOverLift
else -> initialOverLift * decreaseOverLiftingFactor - initialOverLift * decreaseScaleFactor
}
return convertTranslation(scaleOverLift)
}
private fun convertTranslation(value: Float): Float {
return translationValueConverter?.invoke(value) ?: value
}
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/leapfrog_views_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<!-- parent container must include android:clipChildren="false" -->
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_home_front_card" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_home_front_card" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_home_front_card" />
</FrameLayout>