forked from shihaam/thijooree
manage card mode improve part 2 - animations
This commit is contained in:
@@ -14,8 +14,11 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.doOnNextLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -41,6 +44,15 @@ class CardsFragment : Fragment() {
|
|||||||
private var pendingQrAccountNumber: String? = null
|
private var pendingQrAccountNumber: String? = null
|
||||||
private var isManageMode: Boolean = false
|
private var isManageMode: Boolean = false
|
||||||
|
|
||||||
|
// Carousel snapshot captured on enter, used to reverse the exit animation
|
||||||
|
private var carouselCardLayoutTop = 0f // card layout top relative to contentLayout
|
||||||
|
private var carouselCardCenterX = 0f // card center X relative to contentLayout
|
||||||
|
private var carouselTextLayoutTop = 0f // tvSelectedCardType layout top relative to contentLayout
|
||||||
|
|
||||||
|
// Swipe-to-dismiss tracking
|
||||||
|
private var swipeDragStartRawY = 0f
|
||||||
|
private var swipeIsDragging = false
|
||||||
|
|
||||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||||
@@ -133,6 +145,50 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
setManageMode(!isManageMode)
|
setManageMode(!isManageMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swipe-down on the manage card to dismiss manage mode
|
||||||
|
binding.manageCardView.root.setOnTouchListener { _, event ->
|
||||||
|
if (!isManageMode) return@setOnTouchListener false
|
||||||
|
val mgr = binding.manageCardView.root
|
||||||
|
when (event.action) {
|
||||||
|
android.view.MotionEvent.ACTION_DOWN -> {
|
||||||
|
mgr.animate().cancel()
|
||||||
|
binding.tvSelectedCardType.animate().cancel()
|
||||||
|
swipeDragStartRawY = event.rawY
|
||||||
|
swipeIsDragging = false
|
||||||
|
true
|
||||||
|
}
|
||||||
|
android.view.MotionEvent.ACTION_MOVE -> {
|
||||||
|
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
||||||
|
if (dy > 12f || swipeIsDragging) {
|
||||||
|
swipeIsDragging = true
|
||||||
|
mgr.translationY = dy
|
||||||
|
binding.tvSelectedCardType.translationY = dy * 0.6f
|
||||||
|
val scale = 1f - (dy / (binding.contentLayout.height * 2.5f)).coerceIn(0f, 0.12f)
|
||||||
|
mgr.scaleX = scale
|
||||||
|
mgr.scaleY = scale
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
||||||
|
if (swipeIsDragging) {
|
||||||
|
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
||||||
|
swipeIsDragging = false
|
||||||
|
if (dy > 130f) {
|
||||||
|
setManageMode(false)
|
||||||
|
} else {
|
||||||
|
// Snap back
|
||||||
|
mgr.animate().translationY(0f).scaleX(1f).scaleY(1f)
|
||||||
|
.setDuration(280).setInterpolator(DecelerateInterpolator()).start()
|
||||||
|
binding.tvSelectedCardType.animate().translationY(0f)
|
||||||
|
.setDuration(280).setInterpolator(DecelerateInterpolator()).start()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.btnScanToPay.setOnClickListener {
|
binding.btnScanToPay.setOnClickListener {
|
||||||
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
||||||
if (item is CardItem.Mib) {
|
if (item is CardItem.Mib) {
|
||||||
@@ -162,39 +218,148 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
private fun setManageMode(enabled: Boolean) {
|
private fun setManageMode(enabled: Boolean) {
|
||||||
isManageMode = enabled
|
isManageMode = enabled
|
||||||
requireActivity().title = getString(if (enabled) R.string.card_manage else R.string.nav_pay_with_card)
|
requireActivity().title = getString(if (enabled) R.string.card_manage else R.string.nav_pay_with_card)
|
||||||
val gone = View.GONE
|
if (enabled) enterManageMode() else exitManageMode()
|
||||||
val visible = View.VISIBLE
|
}
|
||||||
binding.btnManageCard.visibility = if (enabled) gone else visible
|
|
||||||
binding.topSpacer.visibility = if (enabled) gone else visible
|
private fun enterManageMode() {
|
||||||
binding.rvCards.visibility = if (enabled) gone else visible
|
val item = cards.getOrNull(currentCardPosition) ?: return
|
||||||
binding.pageIndicator.visibility = if (enabled) gone else visible
|
|
||||||
binding.llPayButtons.visibility = if (enabled) gone else visible
|
// Bind card data
|
||||||
binding.llManageButtons.visibility = if (enabled) visible else gone
|
val cv = binding.manageCardView
|
||||||
binding.manageCardView.root.visibility = if (enabled) visible else gone
|
when (item) {
|
||||||
if (!enabled) buildDots(cards.size, currentCardPosition)
|
is CardItem.Mib -> {
|
||||||
if (enabled) {
|
cv.tvCardOwner.text = item.card.cardHolderName
|
||||||
val item = cards.getOrNull(currentCardPosition) ?: return
|
cv.tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
||||||
val cv = binding.manageCardView
|
val assetPath = cardImageAsset(item.card)
|
||||||
when (item) {
|
if (assetPath != null) loadCardImage(cv.ivCardImage, assetPath)
|
||||||
is CardItem.Mib -> {
|
else cv.ivCardImage.setImageDrawable(null)
|
||||||
cv.tvCardOwner.text = item.card.cardHolderName
|
bindCardStatus(cv.tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
||||||
cv.tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
cv.root.alpha = 1f
|
||||||
val assetPath = cardImageAsset(item.card)
|
}
|
||||||
if (assetPath != null) loadCardImage(cv.ivCardImage, assetPath)
|
is CardItem.Bml -> {
|
||||||
else cv.ivCardImage.setImageDrawable(null)
|
cv.tvCardOwner.text = item.account.accountBriefName
|
||||||
bindCardStatus(cv.tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
cv.tvCardNumber.text = formatMasked(item.account.accountNumber)
|
||||||
cv.root.alpha = 1f
|
loadCardImage(cv.ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||||
}
|
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
||||||
is CardItem.Bml -> {
|
bindCardStatus(cv.tvCardStatus, item.account.statusDesc.takeUnless { isActive })
|
||||||
cv.tvCardOwner.text = item.account.accountBriefName
|
cv.root.alpha = if (isActive) 1f else 0.45f
|
||||||
cv.tvCardNumber.text = formatMasked(item.account.accountNumber)
|
|
||||||
loadCardImage(cv.ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
|
||||||
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
|
||||||
bindCardStatus(cv.tvCardStatus, item.account.statusDesc.takeUnless { isActive })
|
|
||||||
cv.root.alpha = if (isActive) 1f else 0.45f
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture positions BEFORE layout changes (for enter animation + exit animation later)
|
||||||
|
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
||||||
|
val lm = binding.rvCards.layoutManager as? LinearLayoutManager
|
||||||
|
val srcView = lm?.findViewByPosition(currentCardPosition)
|
||||||
|
val srcLoc = IntArray(2).also { srcView?.getLocationOnScreen(it) ?: run { it[0] = contentLoc[0]; it[1] = contentLoc[1] } }
|
||||||
|
val srcScreenTop = (srcLoc[1] - contentLoc[1]).toFloat()
|
||||||
|
val srcCenterX = (srcLoc[0] - contentLoc[0]).toFloat() + cardWidth / 2f
|
||||||
|
|
||||||
|
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||||
|
val textSrcScreenTop = (textLoc[1] - contentLoc[1]).toFloat()
|
||||||
|
|
||||||
|
// Apply layout changes
|
||||||
|
binding.btnManageCard.visibility = View.GONE
|
||||||
|
binding.topSpacer.visibility = View.GONE
|
||||||
|
binding.rvCards.visibility = View.GONE
|
||||||
|
binding.pageIndicator.visibility = View.GONE
|
||||||
|
binding.llPayButtons.visibility = View.GONE
|
||||||
|
binding.llManageButtons.visibility = View.VISIBLE
|
||||||
|
binding.manageCardView.root.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
// After layout pass, compute offsets, save carousel snapshot, and animate
|
||||||
|
binding.contentLayout.doOnNextLayout {
|
||||||
|
val mgr = binding.manageCardView.root
|
||||||
|
val dstLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
||||||
|
val dstTop = (dstLoc[1] - contentLoc[1]).toFloat()
|
||||||
|
val dstCenterX = (dstLoc[0] - contentLoc[0]).toFloat() + mgr.width / 2f
|
||||||
|
|
||||||
|
val scaleStart = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
||||||
|
val transXStart = srcCenterX - dstCenterX
|
||||||
|
val transYStart = srcScreenTop - dstTop
|
||||||
|
|
||||||
|
// Save the carousel card's position (relative to contentLayout) for the exit animation
|
||||||
|
carouselCardLayoutTop = srcScreenTop
|
||||||
|
carouselCardCenterX = srcCenterX
|
||||||
|
carouselTextLayoutTop = textSrcScreenTop
|
||||||
|
|
||||||
|
val textDstLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||||
|
val textDstTop = (textDstLoc[1] - contentLoc[1]).toFloat()
|
||||||
|
|
||||||
|
mgr.pivotX = mgr.width / 2f
|
||||||
|
mgr.pivotY = 0f
|
||||||
|
mgr.scaleX = scaleStart
|
||||||
|
mgr.scaleY = scaleStart
|
||||||
|
mgr.translationX = transXStart
|
||||||
|
mgr.translationY = transYStart
|
||||||
|
|
||||||
|
mgr.animate()
|
||||||
|
.scaleX(1f).scaleY(1f)
|
||||||
|
.translationX(0f).translationY(0f)
|
||||||
|
.setDuration(380)
|
||||||
|
.setInterpolator(DecelerateInterpolator())
|
||||||
|
.start()
|
||||||
|
|
||||||
|
binding.tvSelectedCardType.translationY = textSrcScreenTop - textDstTop
|
||||||
|
binding.tvSelectedCardType.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(380)
|
||||||
|
.setInterpolator(DecelerateInterpolator())
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exitManageMode() {
|
||||||
|
binding.manageCardView.root.animate().cancel()
|
||||||
|
binding.tvSelectedCardType.animate().cancel()
|
||||||
|
|
||||||
|
val mgr = binding.manageCardView.root
|
||||||
|
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
||||||
|
|
||||||
|
// Compute layout top of manage card (strip current translationY which may be from a swipe drag)
|
||||||
|
val mgrLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
||||||
|
val mgrLayoutTop = (mgrLoc[1] - contentLoc[1]).toFloat() - mgr.translationY
|
||||||
|
|
||||||
|
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||||
|
val textLayoutTop = (textLoc[1] - contentLoc[1]).toFloat() - binding.tvSelectedCardType.translationY
|
||||||
|
|
||||||
|
// Target: animate card back to carousel position
|
||||||
|
val scaleEnd = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
||||||
|
val mgrLayoutCenterX = (mgrLoc[0] - contentLoc[0]).toFloat() - mgr.translationX + mgr.width / 2f
|
||||||
|
val targetTransX = carouselCardCenterX - mgrLayoutCenterX
|
||||||
|
val targetTransY = carouselCardLayoutTop - mgrLayoutTop
|
||||||
|
|
||||||
|
val targetTextTransY = carouselTextLayoutTop - textLayoutTop
|
||||||
|
|
||||||
|
mgr.pivotX = mgr.width / 2f
|
||||||
|
mgr.pivotY = 0f
|
||||||
|
|
||||||
|
mgr.animate()
|
||||||
|
.scaleX(scaleEnd).scaleY(scaleEnd)
|
||||||
|
.translationX(targetTransX)
|
||||||
|
.translationY(targetTransY)
|
||||||
|
.setDuration(320)
|
||||||
|
.setInterpolator(AccelerateInterpolator())
|
||||||
|
.withEndAction {
|
||||||
|
mgr.scaleX = 1f; mgr.scaleY = 1f
|
||||||
|
mgr.translationX = 0f; mgr.translationY = 0f
|
||||||
|
mgr.visibility = View.GONE
|
||||||
|
binding.tvSelectedCardType.translationY = 0f
|
||||||
|
|
||||||
|
binding.btnManageCard.visibility = View.VISIBLE
|
||||||
|
binding.topSpacer.visibility = View.VISIBLE
|
||||||
|
binding.rvCards.visibility = View.VISIBLE
|
||||||
|
binding.llPayButtons.visibility = View.VISIBLE
|
||||||
|
binding.llManageButtons.visibility = View.GONE
|
||||||
|
buildDots(cards.size, currentCardPosition)
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
|
||||||
|
binding.tvSelectedCardType.animate()
|
||||||
|
.translationY(targetTextTransY)
|
||||||
|
.setDuration(320)
|
||||||
|
.setInterpolator(AccelerateInterpolator())
|
||||||
|
.withEndAction { binding.tvSelectedCardType.translationY = 0f }
|
||||||
|
.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyCardScales() {
|
private fun applyCardScales() {
|
||||||
|
|||||||
Reference in New Issue
Block a user