diff --git a/stripe/res/drawable/ic_trash.xml b/stripe/res/drawable/ic_trash.xml index eb3494ac2b1..40e175221ba 100644 --- a/stripe/res/drawable/ic_trash.xml +++ b/stripe/res/drawable/ic_trash.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/stripe/res/values/colors.xml b/stripe/res/values/colors.xml index 6ce331e1f59..de1cb03e493 100644 --- a/stripe/res/values/colors.xml +++ b/stripe/res/values/colors.xml @@ -13,4 +13,7 @@ @android:color/white @android:color/secondary_text_light + + #DFDEDF + #D5473F diff --git a/stripe/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt b/stripe/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt index 57ef3660535..b8adc0f2bcb 100644 --- a/stripe/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt +++ b/stripe/src/main/java/com/stripe/android/view/PaymentMethodSwipeCallback.kt @@ -2,10 +2,13 @@ package com.stripe.android.view import android.content.Context import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.View +import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import com.stripe.android.R import com.stripe.android.model.PaymentMethod /** @@ -19,7 +22,13 @@ internal class PaymentMethodSwipeCallback( ) : ItemTouchHelper.SimpleCallback( 0, ItemTouchHelper.RIGHT ) { - private val trashIcon = ContextCompat.getDrawable(context, R.drawable.ic_trash) + private val trashIcon = + ContextCompat.getDrawable(context, com.stripe.android.R.drawable.ic_trash)!! + private val swipeStartColor = + ContextCompat.getColor(context, com.stripe.android.R.color.swipe_start_payment_method) + private val swipeThresholdColor = + ContextCompat.getColor(context, com.stripe.android.R.color.swipe_threshold_payment_method) + private val background = ColorDrawable(swipeStartColor) override fun onMove( recyclerView: RecyclerView, @@ -34,8 +43,20 @@ internal class PaymentMethodSwipeCallback( listener.onSwiped(paymentMethod) } + override fun getSwipeDirs( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return if (viewHolder is PaymentMethodsAdapter.PaymentMethodViewHolder) { + // only allow swiping on Payment Method items + super.getSwipeDirs(recyclerView, viewHolder) + } else { + 0 + } + } + override fun onChildDraw( - c: Canvas, + canvas: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, @@ -43,20 +64,115 @@ internal class PaymentMethodSwipeCallback( actionState: Int, isCurrentlyActive: Boolean ) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) - viewHolder.itemView + super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + if (viewHolder is PaymentMethodsAdapter.PaymentMethodViewHolder) { + val itemView = viewHolder.itemView + + val startTransition = itemView.width * START_TRANSITION_THRESHOLD + val endTransition = itemView.width * END_TRANSITION_THRESHOLD + + // calculate the transition fraction to animate the background color of the swipe + val transitionFraction: Float = + when { + dX < startTransition -> + 0F + dX >= endTransition -> + 1F + else -> + ((dX - startTransition) / (endTransition - startTransition)) + } + + updateSwipedPaymentMethod( + itemView, + dX.toInt(), + transitionFraction, + canvas + ) + } } - override fun getSwipeDirs( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ): Int { - return if (viewHolder is PaymentMethodsAdapter.PaymentMethodViewHolder) { - // only allow swiping on Payment Method items - super.getSwipeDirs(recyclerView, viewHolder) - } else { - 0 + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { + return END_TRANSITION_THRESHOLD + } + + private fun updateSwipedPaymentMethod( + itemView: View, + dX: Int, + transitionFraction: Float, + canvas: Canvas + ) { + val backgroundCornerOffset = trashIcon.intrinsicWidth / 2 + + val iconMargin = (itemView.height - trashIcon.intrinsicHeight) / 2 + val iconTop = itemView.top + (itemView.height - trashIcon.intrinsicHeight) / 2 + val iconBottom = iconTop + trashIcon.intrinsicHeight + + when { + // swipe right + dX > 0 -> { + val iconLeft = itemView.left + iconMargin + val iconRight = iconLeft + trashIcon.intrinsicWidth + + // hide the icon until the swipe distance is enough that it won't clash + // with the view + if (dX > iconRight) { + trashIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom) + } else { + trashIcon.setBounds(0, 0, 0, 0) + } + + background.setBounds(itemView.left, itemView.top, + itemView.left + dX + backgroundCornerOffset, + itemView.bottom) + background.color = when { + transitionFraction <= 0.0F -> + swipeStartColor + transitionFraction >= 1.0F -> + swipeThresholdColor + else -> + calculateTransitionColor( + transitionFraction, + swipeStartColor, + swipeThresholdColor + ) + } + } + else -> { + // reset when done swiping + trashIcon.setBounds(0, 0, 0, 0) + background.setBounds(0, 0, 0, 0) + } } + + background.draw(canvas) + trashIcon.draw(canvas) + } + + companion object { + // calculate the background color while transitioning from start to end threshold + internal fun calculateTransitionColor( + fraction: Float, + @ColorInt startValue: Int, + @ColorInt endValue: Int + ): Int { + val startAlpha = Color.alpha(startValue) + val startRed = Color.red(startValue) + val startGreen = Color.green(startValue) + val startBlue = Color.blue(startValue) + val deltaAlpha = (Color.alpha(endValue) - startAlpha) * fraction + val deltaRed = (Color.red(endValue) - startRed) * fraction + val deltaGreen = (Color.green(endValue) - startGreen) * fraction + val deltaBlue = (Color.blue(endValue) - startBlue) * fraction + return Color.argb( + (startAlpha + deltaAlpha).toInt(), + (startRed + deltaRed).toInt(), + (startGreen + deltaGreen).toInt(), + (startBlue + deltaBlue).toInt() + ) + } + + private const val START_TRANSITION_THRESHOLD = 0.25F + private const val END_TRANSITION_THRESHOLD = 0.5F } interface Listener { diff --git a/stripe/src/main/java/com/stripe/android/view/PaymentMethodsActivity.java b/stripe/src/main/java/com/stripe/android/view/PaymentMethodsActivity.java index 7f20751ad1b..c6c23eef75a 100644 --- a/stripe/src/main/java/com/stripe/android/view/PaymentMethodsActivity.java +++ b/stripe/src/main/java/com/stripe/android/view/PaymentMethodsActivity.java @@ -47,9 +47,6 @@ public class PaymentMethodsActivity extends AppCompatActivity { public static final String TOKEN_PAYMENT_METHODS_ACTIVITY = "PaymentMethodsActivity"; - // TODO(mshafrir-stripe): enable when ready - private static final boolean SHOULD_ENABLE_PAYMENT_METHOD_SWIPING = false; - private PaymentMethodsAdapter mAdapter; private ProgressBar mProgressBar; private boolean mStartedFromPaymentSession; @@ -110,9 +107,7 @@ public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) { new PaymentMethodSwipeCallback(this, mAdapter, new SwipeToDeleteCallbackListener(this)) ); - if (SHOULD_ENABLE_PAYMENT_METHOD_SWIPING) { - itemTouchHelper.attachToRecyclerView(recyclerView); - } + itemTouchHelper.attachToRecyclerView(recyclerView); mCustomerSession = CustomerSession.getInstance(); mStartedFromPaymentSession = args.isPaymentSessionActive; diff --git a/stripe/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt b/stripe/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt new file mode 100644 index 00000000000..3b0bf0b9ad8 --- /dev/null +++ b/stripe/src/test/java/com/stripe/android/view/PaymentMethodSwipeCallbackTest.kt @@ -0,0 +1,25 @@ +package com.stripe.android.view + +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.test.core.app.ApplicationProvider +import com.stripe.android.R +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PaymentMethodSwipeCallbackTest { + + @Test + fun testCalculateTransitionColor() { + val context: Context = ApplicationProvider.getApplicationContext() + val calculatedColor = PaymentMethodSwipeCallback.calculateTransitionColor( + 0.25F, + ContextCompat.getColor(context, R.color.swipe_start_payment_method), + ContextCompat.getColor(context, R.color.swipe_threshold_payment_method) + ) + assertEquals(-2312009, calculatedColor) + } +}