Updated on 2026-08-14

This commit is contained in:
Tangem 2022-01-26 20:25:32 +03:00
parent 102f4cf536
commit a370de2bcb
19 changed files with 110 additions and 648 deletions

View file

@ -105,6 +105,7 @@ object TangemSdk {
is TangemSdkError.WrongInteractionMode -> TangemSdkError.WrongInteractionMode()
is TangemSdkError.IssuerSignatureLoadingFailed -> TangemSdkError.IssuerSignatureLoadingFailed()
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
data class ByteArrayKey(val bytes: ByteArray) {
override fun equals(other: Any?): Boolean {
return this === other || other is ByteArrayKey && this.bytes.contentEquals(other.bytes)
}
override fun hashCode(): Int = bytes.contentHashCode()
}
fun ByteArray.toMapKey(): ByteArrayKey = ByteArrayKey(this)
fun ByteArrayKey.bytes(): ByteArray = this.bytes

View file

@ -1,147 +0,0 @@
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 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.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: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply {
playSequentially(translate)
}
}
fun View.leapAnimation(animDuration: Long, properties: LeapViewProperties, overLift: Float): AnimatorSet {
val halfDuration = animDuration / 2
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.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
playTogether(scaleX, scaleY)
}
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)
}
}
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, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
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
interpolator = LinearInterpolator()
playTogether(scaleX, scaleY)
}
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)
}
}
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.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
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translateUp, scaleX, scaleY, elevation)
}
}
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.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
duration = pullDuration
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

@ -1,144 +0,0 @@
package com.tangem.tap.common.leapfrogWidget
import android.view.View
/**
[REDACTED_AUTHOR]
*/
class LeapView(
val view: View,
val index: Int,
position: Int,
private val maximalPosition: Int,
private val calculator: PropertyCalculator,
) {
var state: LeapViewState
private set
val initialState: LeapViewState
init {
val initialProperties = createInitialProperty(position)
state = LeapViewState(index, position, position, initialProperties)
initialState = state.copy()
}
fun initView() {
applyState(initialState)
}
fun leap(): LeapFrogAnimation {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
0 -> maximalPosition
else -> state.currentPosition - 1
}
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == 0 && currentPosition == maximalPosition -> LeapFrogAnimation.LEAP
else -> LeapFrogAnimation.PULL
}
}
fun leapBack(): LeapFrogAnimation {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
maximalPosition -> 0
else -> state.currentPosition + 1
}
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == maximalPosition && currentPosition == 0 -> LeapFrogAnimation.LEAP
else -> LeapFrogAnimation.PULL
}
}
fun fold() {
state = state.copy(properties = state.properties.toFold()
)
}
fun unfold() {
state = state.copy(properties = state.properties.toUnfold(calculator))
}
fun applyState(state: LeapViewState) {
this.state = state
setProperties(this.state.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): 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): 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 LeapViewProperties.toUnfold(calculator: PropertyCalculator): LeapViewProperties {
return this.copy(yTranslation = calculator.yTranslation(this.positionEnd))
}
private fun LeapViewProperties.toFold(): LeapViewProperties {
return this.copy(yTranslation = 0f)
}
override fun toString(): String = "index: $index, position: ${state.currentPosition}, code: ${view.hashCode()}"
}
data class LeapViewState(
val index: Int,
val currentPosition: Int,
val previousPosition: Int,
val properties: LeapViewProperties
)
enum class LeapFrogAnimation {
LEAP, PULL
}
data class LeapViewProperties(
val positionStart: Int,
val positionEnd: Int,
val scale: Float,
val elevationStart: Float,
val elevationEnd: Float,
val yTranslation: Float,
val hasForeground: Boolean,
)

View file

@ -1,249 +0,0 @@
package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.children
import com.tangem.common.extensions.VoidCallback
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
[REDACTED_AUTHOR]
*/
class LeapfrogWidget(
private val parentContainer: FrameLayout,
private val calculator: PropertyCalculator = PropertyCalculator(),
) {
private val foldAnimationDuration = 300L
private val leapAnimationDuration = 500L
private val leapBackAnimationDuration = 500L
private val leapViews = mutableListOf<LeapView>()
private var foldUnfoldProgress: Int = 100
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
init {
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
if (viewIsFullFledged()) {
val maxPosition = parentContainer.childCount - 1
parentContainer.children.forEachIndexed { index, view ->
val initialPosition = maxPosition - index
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
}
}
}
/**
* Apply initial properties to the views
*/
fun initViews() {
leapViews.forEach { it.initView() }
}
fun fold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = foldAnimator(animate)
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun unfold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = unfoldAnimator(animate)
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun foldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(true, animate, foldAnimationDuration)
}
fun unfoldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(false, animate, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, animate: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
val animateDuration = if (animate) duration else 0
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(animateDuration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(animateDuration, it.state.properties))
}
}
animatorsList.add(createProgressListener(animateDuration) { foldUnfoldProgress = it })
val animator = AnimatorSet()
animator.interpolator = AccelerateDecelerateInterpolator()
animator.playTogether(animatorsList.toList())
return animator
}
fun leap(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = if (animate) leapAnimationDuration else 0
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leap()) {
LeapFrogAnimation.LEAP -> {
val overLift = calculator.overLift(leapViews.size)
animatorsList.add(it.view.leapAnimation(duration, it.state.properties, overLift))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullUpAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapProgress = it })
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun leapBack(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = if (animate) leapBackAnimationDuration else 0
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leapBack()) {
LeapFrogAnimation.LEAP -> {
animatorsList.add(it.view.leapBackAnimation(duration, it.state.properties, calculator))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullDownAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapBackProgress = it })
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun getViewsCount(): Int = parentContainer.childCount
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(leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
state.leapViewStates.forEach { viewState ->
leapViews.firstOrNull { it.index == viewState.index }?.applyState(viewState)
}
}
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
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
}
}
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> 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
}
data class LeapfrogWidgetState(
val leapViewStates: List<LeapViewState>
)
class PropertyCalculator(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.15f,
val yTranslationFactor: Float = 35f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 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) {
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

@ -5,6 +5,8 @@ import android.view.View
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.test_leapfrog_fragment.*