From a4e7c2d6270f8ed20bc0562d494d6781774f12bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Oct 2021 16:00:46 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/common/leapfrogWidget/Animations.kt | 154 +++++++++++++ .../tap/common/leapfrogWidget/LeapView.kt | 59 +++++ .../common/leapfrogWidget/LeapfrogWidget.kt | 204 ++++++++++++++++++ app/src/main/res/layout/leapfrog_widget.xml | 26 +++ 4 files changed, 443 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/leapfrogWidget/Animations.kt create mode 100644 app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapView.kt create mode 100644 app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapfrogWidget.kt create mode 100644 app/src/main/res/layout/leapfrog_widget.xml diff --git a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/Animations.kt b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/Animations.kt new file mode 100644 index 0000000000..b89618ee5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/Animations.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapView.kt b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapView.kt new file mode 100644 index 0000000000..b3a115f964 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapView.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapfrogWidget.kt b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapfrogWidget.kt new file mode 100644 index 0000000000..e88e1cf49a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/LeapfrogWidget.kt @@ -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() + + 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() + 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() + 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() + 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 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/leapfrog_widget.xml b/app/src/main/res/layout/leapfrog_widget.xml new file mode 100644 index 0000000000..13a8e61d88 --- /dev/null +++ b/app/src/main/res/layout/leapfrog_widget.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file