forked from thijooree/android
tap-to-pay part 1
This commit is contained in:
@@ -24,12 +24,30 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.Gravity
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlTapToPayClient
|
||||
import sh.sar.basedbank.nfc.BmlHostCardEmulatorService
|
||||
import sh.sar.basedbank.nfc.HceTokenStore
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentCardsBinding
|
||||
import sh.sar.basedbank.util.CardsCache
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.Totp
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
import kotlin.math.abs
|
||||
@@ -45,6 +63,8 @@ class CardsFragment : Fragment() {
|
||||
private var cardWidth: Int = 0
|
||||
private var pendingQrAccountNumber: String? = null
|
||||
private var isManageMode: Boolean = false
|
||||
private var isTapMode: Boolean = false
|
||||
private var tapAnimView: NfcTapAnimationView? = null
|
||||
|
||||
// Carousel snapshot captured on enter, used to reverse the exit animation
|
||||
private var carouselCardLayoutTop = 0f // card layout top relative to contentLayout
|
||||
@@ -136,7 +156,7 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
|
||||
// Swipe-down on the manage card to dismiss manage mode
|
||||
binding.manageCardView.root.setOnTouchListener { _, event ->
|
||||
if (!isManageMode) return@setOnTouchListener false
|
||||
if (!isManageMode && !isTapMode) return@setOnTouchListener false
|
||||
val mgr = binding.manageCardView.root
|
||||
when (event.action) {
|
||||
android.view.MotionEvent.ACTION_DOWN -> {
|
||||
@@ -163,7 +183,7 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
||||
swipeIsDragging = false
|
||||
if (dy > 130f) {
|
||||
setManageMode(false)
|
||||
if (isTapMode) setTapMode(false) else setManageMode(false)
|
||||
} else {
|
||||
// Snap back
|
||||
mgr.animate().translationY(0f).scaleX(1f).scaleY(1f)
|
||||
@@ -192,8 +212,17 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
binding.btnTapToPay.isEnabled = nfcAvailable
|
||||
binding.btnTapToPay.setOnClickListener {
|
||||
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
||||
val msg = if (item is CardItem.Mib) R.string.mib_qr_nfc_not_supported else R.string.work_in_progress
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
|
||||
if (item is CardItem.Mib) {
|
||||
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
val bmlItem = item as CardItem.Bml
|
||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
if (prefs.getBoolean("biometrics_transfer_confirm", false)) {
|
||||
showBiometricPromptForTap(bmlItem)
|
||||
} else {
|
||||
setTapMode(true, bmlItem)
|
||||
}
|
||||
}
|
||||
|
||||
val wip = View.OnClickListener {
|
||||
@@ -378,6 +407,250 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
.start()
|
||||
}
|
||||
|
||||
// ── Tap-to-pay mode ────────────────────────────────────────────────────────
|
||||
|
||||
private fun setTapMode(enabled: Boolean, item: CardItem.Bml? = null) {
|
||||
isTapMode = enabled
|
||||
requireActivity().title = getString(if (enabled) R.string.card_pay_nfc else R.string.nav_pay_with_card)
|
||||
if (enabled) enterTapMode(item!!) else exitTapMode()
|
||||
}
|
||||
|
||||
private fun showBiometricPromptForTap(item: CardItem.Bml) {
|
||||
val bmgr = BiometricManager.from(requireContext())
|
||||
if (bmgr.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) != BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
setTapMode(true, item)
|
||||
return
|
||||
}
|
||||
val prompt = BiometricPrompt(this, ContextCompat.getMainExecutor(requireContext()),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
setTapMode(true, item)
|
||||
}
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { }
|
||||
})
|
||||
prompt.authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.card_pay_nfc))
|
||||
.setSubtitle(item.account.accountBriefName)
|
||||
.setNegativeButtonText(getString(R.string.cancel))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun enterTapMode(item: CardItem.Bml) {
|
||||
// Bind card data to the shared manage card view
|
||||
val cv = binding.manageCardView
|
||||
cv.tvCardOwner.text = item.account.accountBriefName
|
||||
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
|
||||
|
||||
// Snapshot carousel card position before layout changes (for animation)
|
||||
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()
|
||||
|
||||
carouselCardLayoutTop = srcScreenTop
|
||||
carouselCardCenterX = srcCenterX
|
||||
carouselTextLayoutTop = textSrcScreenTop
|
||||
|
||||
// Apply layout changes
|
||||
binding.btnManageCard.visibility = View.GONE
|
||||
binding.topSpacer.visibility = View.GONE
|
||||
binding.rvCards.visibility = View.GONE
|
||||
binding.pageIndicator.visibility = View.GONE
|
||||
binding.divider.visibility = View.GONE
|
||||
binding.llPayButtons.visibility = View.GONE
|
||||
binding.llManageButtons.visibility = View.GONE
|
||||
binding.llDefaultCardRow.visibility = View.GONE
|
||||
binding.manageCardView.root.visibility = View.VISIBLE
|
||||
binding.flTapMode.visibility = View.VISIBLE
|
||||
|
||||
// Build tap mode content: animation view + cancel button
|
||||
binding.flTapMode.removeAllViews()
|
||||
val animView = NfcTapAnimationView(requireContext())
|
||||
tapAnimView = animView
|
||||
|
||||
val dp = resources.displayMetrics.density
|
||||
val cancelBtn = MaterialButton(requireContext(), null,
|
||||
com.google.android.material.R.attr.materialButtonOutlinedStyle
|
||||
).apply { setText(R.string.cancel); setOnClickListener { setTapMode(false) } }
|
||||
|
||||
val cancelWrapper = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
setPadding(0, 0, 0, (24 * dp).toInt())
|
||||
addView(cancelBtn)
|
||||
}
|
||||
val container = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
addView(View(requireContext()).apply { // spacer pushes content below card
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||
})
|
||||
addView(animView.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 3f)
|
||||
})
|
||||
addView(cancelWrapper.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
}
|
||||
binding.flTapMode.addView(container)
|
||||
|
||||
// Animate card up from carousel position (same as manage mode)
|
||||
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
|
||||
|
||||
mgr.pivotX = mgr.width / 2f
|
||||
mgr.pivotY = 0f
|
||||
mgr.scaleX = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
||||
mgr.scaleY = mgr.scaleX
|
||||
mgr.translationX = srcCenterX - dstCenterX
|
||||
mgr.translationY = srcScreenTop - dstTop
|
||||
|
||||
mgr.animate()
|
||||
.scaleX(1f).scaleY(1f)
|
||||
.translationX(0f).translationY(0f)
|
||||
.setDuration(380).setInterpolator(DecelerateInterpolator()).start()
|
||||
|
||||
val textDstLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||
binding.tvSelectedCardType.translationY = textSrcScreenTop - (textDstLoc[1] - contentLoc[1]).toFloat()
|
||||
binding.tvSelectedCardType.animate()
|
||||
.translationY(0f)
|
||||
.setDuration(380).setInterpolator(DecelerateInterpolator()).start()
|
||||
}
|
||||
|
||||
fetchAndArmToken(item)
|
||||
}
|
||||
|
||||
private fun exitTapMode() {
|
||||
tapAnimView?.stopAnimation()
|
||||
tapAnimView = null
|
||||
BmlHostCardEmulatorService.clearToken()
|
||||
BmlHostCardEmulatorService.onTransactionComplete = null
|
||||
|
||||
binding.manageCardView.root.animate().cancel()
|
||||
binding.tvSelectedCardType.animate().cancel()
|
||||
|
||||
val mgr = binding.manageCardView.root
|
||||
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
||||
val mgrLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
||||
val mgrLayoutTop = (mgrLoc[1] - contentLoc[1]).toFloat() - mgr.translationY
|
||||
val mgrLayoutCenterX = (mgrLoc[0] - contentLoc[0]).toFloat() - mgr.translationX + mgr.width / 2f
|
||||
|
||||
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||
val textLayoutTop = (textLoc[1] - contentLoc[1]).toFloat() - binding.tvSelectedCardType.translationY
|
||||
|
||||
mgr.pivotX = mgr.width / 2f
|
||||
mgr.pivotY = 0f
|
||||
|
||||
mgr.animate()
|
||||
.scaleX(if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f)
|
||||
.scaleY(if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f)
|
||||
.translationX(carouselCardCenterX - mgrLayoutCenterX)
|
||||
.translationY(carouselCardLayoutTop - mgrLayoutTop)
|
||||
.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.flTapMode.visibility = View.GONE
|
||||
binding.flTapMode.removeAllViews()
|
||||
binding.btnManageCard.visibility = View.VISIBLE
|
||||
binding.topSpacer.visibility = View.VISIBLE
|
||||
binding.rvCards.visibility = View.VISIBLE
|
||||
binding.divider.visibility = View.VISIBLE
|
||||
binding.llPayButtons.visibility = View.VISIBLE
|
||||
buildDots(cards.size, currentCardPosition)
|
||||
}
|
||||
.start()
|
||||
|
||||
binding.tvSelectedCardType.animate()
|
||||
.translationY(carouselTextLayoutTop - textLayoutTop)
|
||||
.setDuration(320)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.withEndAction { binding.tvSelectedCardType.translationY = 0f }
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun fetchAndArmToken(item: CardItem.Bml) {
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
val tokenStore = HceTokenStore(requireContext())
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
var token = withContext(Dispatchers.IO) { tokenStore.popToken() }
|
||||
|
||||
if (token == null) {
|
||||
val loginId = item.account.loginTag.removePrefix("bml_")
|
||||
val session = app.bmlSessionFor(item.account)
|
||||
val otpSeed = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
||||
|
||||
if (session == null || otpSeed == null) {
|
||||
if (isTapMode) {
|
||||
Toast.makeText(requireContext(),
|
||||
if (session == null) getString(R.string.transfer_session_unavailable)
|
||||
else "OTP unavailable",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
setTapMode(false)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val otp = Totp.generate(otpSeed)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
runCatching { BmlTapToPayClient().fetchTokens(session, item.account.internalId, otp) }
|
||||
}
|
||||
val fetched = result.getOrNull()
|
||||
if (fetched.isNullOrEmpty()) {
|
||||
if (isTapMode) {
|
||||
Toast.makeText(requireContext(),
|
||||
result.exceptionOrNull()?.message ?: "Failed to get payment token",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
setTapMode(false)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
tokenStore.saveTokens(fetched)
|
||||
token = tokenStore.popToken()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTapMode) return@launch // user cancelled while we were fetching
|
||||
|
||||
val t = token ?: run {
|
||||
if (isTapMode) {
|
||||
Toast.makeText(requireContext(), "No valid payment tokens", Toast.LENGTH_SHORT).show()
|
||||
setTapMode(false)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
BmlHostCardEmulatorService.setToken(t)
|
||||
BmlHostCardEmulatorService.onTransactionComplete = { success ->
|
||||
view?.post {
|
||||
if (!isTapMode) return@post
|
||||
setTapMode(false)
|
||||
if (success) Toast.makeText(requireContext(), "Payment complete", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rebuildCards() {
|
||||
// Remember which card is currently selected by identity so we can restore position after reorder
|
||||
val currentCard = cards.getOrNull(currentCardPosition)
|
||||
@@ -433,7 +706,7 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
}
|
||||
|
||||
private fun buildDots(count: Int, selected: Int) {
|
||||
if (isManageMode) return
|
||||
if (isManageMode || isTapMode) return
|
||||
binding.pageIndicator.removeAllViews()
|
||||
if (count <= 1) {
|
||||
binding.pageIndicator.visibility = View.GONE
|
||||
@@ -469,6 +742,10 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
if (isTapMode) {
|
||||
setTapMode(false)
|
||||
return true
|
||||
}
|
||||
if (isManageMode) {
|
||||
setManageMode(false)
|
||||
return true
|
||||
@@ -476,12 +753,24 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (isTapMode) {
|
||||
BmlHostCardEmulatorService.clearToken()
|
||||
BmlHostCardEmulatorService.onTransactionComplete = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = getString(R.string.nav_pay_with_card)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
tapAnimView?.stopAnimation()
|
||||
tapAnimView = null
|
||||
BmlHostCardEmulatorService.clearToken()
|
||||
BmlHostCardEmulatorService.onTransactionComplete = null
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
@@ -543,6 +832,104 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
}
|
||||
}
|
||||
|
||||
// ── NFC animation view ─────────────────────────────────────────────────────
|
||||
|
||||
private inner class NfcTapAnimationView(context: Context) : View(context) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 1600
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
repeatMode = ValueAnimator.RESTART
|
||||
addUpdateListener { invalidate() }
|
||||
start()
|
||||
}
|
||||
|
||||
fun stopAnimation() = animator.cancel()
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val w = width.toFloat(); val h = height.toFloat()
|
||||
if (w <= 0f || h <= 0f) return
|
||||
|
||||
val dp = resources.displayMetrics.density
|
||||
val progress = animator.animatedFraction
|
||||
val cx = w / 2f; val cy = h / 2f - 20 * dp
|
||||
|
||||
val colorOnSurface = MaterialColors.getColor(this,
|
||||
com.google.android.material.R.attr.colorOnSurface, android.graphics.Color.BLACK)
|
||||
val colorPrimary = MaterialColors.getColor(this,
|
||||
com.google.android.material.R.attr.colorPrimary, android.graphics.Color.BLUE)
|
||||
val colorSurfaceVariant = MaterialColors.getColor(this,
|
||||
com.google.android.material.R.attr.colorSurfaceVariant, android.graphics.Color.LTGRAY)
|
||||
|
||||
// Phone (left of center)
|
||||
val phoneW = 36 * dp; val phoneH = 62 * dp
|
||||
val phoneX = cx - 72 * dp - phoneW; val phoneY = cy - phoneH / 2f
|
||||
|
||||
// POS terminal (right of center)
|
||||
val posW = 30 * dp; val posH = 50 * dp
|
||||
val posX = cx + 72 * dp; val posY = cy - posH / 2f
|
||||
|
||||
// Phone body
|
||||
paint.style = Paint.Style.FILL; paint.color = colorSurfaceVariant
|
||||
canvas.drawRoundRect(phoneX, phoneY, phoneX + phoneW, phoneY + phoneH, 6 * dp, 6 * dp, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorOnSurface
|
||||
canvas.drawRoundRect(phoneX, phoneY, phoneX + phoneW, phoneY + phoneH, 6 * dp, 6 * dp, paint)
|
||||
|
||||
// Phone screen
|
||||
paint.style = Paint.Style.FILL; paint.color = colorPrimary; paint.alpha = 70
|
||||
canvas.drawRoundRect(phoneX + 3 * dp, phoneY + 8 * dp,
|
||||
phoneX + phoneW - 3 * dp, phoneY + phoneH - 12 * dp, 3 * dp, 3 * dp, paint)
|
||||
paint.alpha = 255
|
||||
|
||||
// Static NFC arcs on the right side of phone
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorPrimary
|
||||
val arcOriginX = phoneX + phoneW
|
||||
for (i in 1..3) {
|
||||
val r = i * 10 * dp
|
||||
paint.alpha = 220 - i * 50
|
||||
canvas.drawArc(RectF(arcOriginX - r, cy - r, arcOriginX + r, cy + r),
|
||||
-70f, 140f, false, paint)
|
||||
}
|
||||
paint.alpha = 255
|
||||
|
||||
// POS terminal body
|
||||
paint.style = Paint.Style.FILL; paint.color = colorSurfaceVariant
|
||||
canvas.drawRoundRect(posX, posY, posX + posW, posY + posH, 5 * dp, 5 * dp, paint)
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2 * dp; paint.color = colorOnSurface
|
||||
canvas.drawRoundRect(posX, posY, posX + posW, posY + posH, 5 * dp, 5 * dp, paint)
|
||||
|
||||
// POS screen
|
||||
paint.style = Paint.Style.FILL; paint.color = colorPrimary; paint.alpha = 70
|
||||
canvas.drawRoundRect(posX + 3 * dp, posY + 4 * dp,
|
||||
posX + posW - 3 * dp, posY + posH * 0.45f, 3 * dp, 3 * dp, paint)
|
||||
paint.alpha = 255
|
||||
|
||||
// POS card slot
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 1.5f * dp; paint.color = colorOnSurface
|
||||
canvas.drawLine(posX + 4 * dp, posY + posH * 0.72f, posX + posW - 4 * dp, posY + posH * 0.72f, paint)
|
||||
|
||||
// Animated NFC rings travelling from phone toward POS
|
||||
val gapStart = arcOriginX + 28 * dp
|
||||
val gapEnd = posX - 4 * dp
|
||||
val midX = (gapStart + gapEnd) / 2f
|
||||
paint.style = Paint.Style.STROKE; paint.strokeWidth = 2.5f * dp
|
||||
for (i in 0..2) {
|
||||
val p = ((progress + i / 3f) % 1f)
|
||||
val r = p * (gapEnd - gapStart) / 2f + 6 * dp
|
||||
paint.color = colorPrimary; paint.alpha = ((1f - p) * 200).toInt().coerceIn(0, 255)
|
||||
canvas.drawArc(RectF(midX - r, cy - r, midX + r, cy + r), -80f, 160f, false, paint)
|
||||
}
|
||||
paint.alpha = 255
|
||||
|
||||
// Label
|
||||
paint.style = Paint.Style.FILL; paint.color = colorOnSurface; paint.alpha = 160
|
||||
paint.textSize = 14 * dp; paint.textAlign = Paint.Align.CENTER
|
||||
canvas.drawText(context.getString(R.string.card_pay_nfc), cx, cy + 60 * dp, paint)
|
||||
paint.alpha = 255; paint.textAlign = Paint.Align.LEFT
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
|
||||
"51" -> "cards/mib/faisa_card.png"
|
||||
|
||||
Reference in New Issue
Block a user