ItemTouchHelper : Limit swipe width of ItemTouchHelper.SimpleCallBack on RecyclerView
Your onChildDraw()
method at the end calls its super super method like so:
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
To limit the maximum width of your swipe you can invoke super.onChildDraw()
with a reduced dX
:
super.onChildDraw(c, recyclerView, viewHolder, dX / 4, dY, actionState, isCurrentlyActive);
Following the response from Alexander Thiel I created a class that manages Left and Right Item Swipe:
/** * Adds [RecyclerView] items decoration shown when the user swipes an item horizontally (left or right). * When the item is being swiped to left/right, customizable views are shown (check MEMBERS section). * * USAGE: * val callback = ItemTouchSwipeHelperCallback(recyclerView) * callback.onItemSwipeListener = this * callback.rightBackgroundColor = R.color.red * // ... Any other type of configuration * */class ItemTouchSwipeHelperCallback( private val recyclerView: RecyclerView) : ItemTouchHelper.SimpleCallback( ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { private val context: Context get() = recyclerView.context /* ------------------- VIEW DEFAULTS ------------------- */ private val defaultIconSizeDp = 28 private val defaultIconHorizontalMarginDp = 30 @ColorRes private val defaultRightIconTint = R.color.white_FFFFFF @ColorRes private val defaultLeftIconTint = R.color.white_FFFFFF @ColorRes private val defaultLeftBackgroundColor = R.color.red_FF7F8F @ColorRes private val defaultRightBackgroundColor = R.color.green_64ECA8 @ColorRes private val defaultTextsColor = R.color.white_FFFFFF private val defaultLeftText = context.getString(R.string.action_left_swipe) private val defaultRightText = context.getString(R.string.action_right_swipe) private val defaultTextsTypeface = ResourcesCompat.getFont(context, R.font.roboto_bold) private val defaultTextsSize = context.resources.getDimensionPixelSize(R.dimen.text_size_12px) /* ------------------- MEMBERS ------------------- */ /** Left and Right icons horizontal margin (left and right respectively) */ var iconsHorizontalMargin: Int = 0 set(value) { field = context.applyDimension(value).toInt() } /** Left and Right icons size. The size is applied to the drawables is width == height (square) */ var iconsSize: Int = 0 set(value) { field = context.applyDimension(value).toInt() } /** Helper member */ private val halfIconSize: Float get() = iconsSize / 2f /** Left icon Drawable to draw */ var leftIcon: Drawable? = ResourcesCompat.getDrawable( context.resources, R.drawable.ic_left_swipe, context.theme )?.mutate() /** Right icon Drawable to draw */ var rightIcon: Drawable? = ResourcesCompat.getDrawable( context.resources, R.drawable.ic_right_swipe, context.theme )?.mutate() /** Left text String to draw. Positioned below leftIcon and centered horizontally. */ var leftText: String = "" set(value) { field = value.toUpperCase() } /** Right text String to draw. Positioned below rightIcon and centered horizontally. */ var rightText: String = "" set(value) { field = value.toUpperCase() } /** Right and Left texts typeface (font) */ var textsTypeface: Typeface? = null set(value) { field = value textPaint.typeface = value } /** Right and Left texts sizes */ var textsSize: Float = 0f set(value) { field = value textPaint.textSize = value } /** Left and Right texts colors */ @ColorRes var textsColor: Int = 0 set(value) { field = value textPaint.color = ContextCompat.getColor(context, value) } /** Right icon [Drawable] tint color */ @ColorRes var rightIconTint: Int = 0 set(value) { field = ContextCompat.getColor(context, value) rightIcon?.let { DrawableCompat.setTint(it, field) } } /** Left icon [Drawable] tint color */ @ColorRes var leftIconTint: Int = 0 set(value) { field = ContextCompat.getColor(context, value) leftIcon?.let { DrawableCompat.setTint(it, field) } } /** Left View Background Color shown when the user is swiping */ @ColorRes var leftBackgroundColor: Int = 0 set(value) { field = value leftPaint.color = ContextCompat.getColor(context, value) } /** Right View Background Color shown when the user is swiping */ @ColorRes var rightBackgroundColor: Int = 0 set(value) { field = value rightPaint.color = ContextCompat.getColor(context, value) } /** false for swipe to left direction disabled */ var swipeToLeftEnabled: Boolean = true set(value) { field = value if (!value) { setDefaultSwipeDirs(ItemTouchHelper.RIGHT) } } /** false for swipe to right direction disabled */ var swipeToRightEnabled: Boolean = true set(value) { field = value if (!value) { setDefaultSwipeDirs(ItemTouchHelper.LEFT) } } /** Max Item Width for drawing when is being swiped [0 - 1] **/ var swipeableWidthPercentage: Float = 0.25f /** * Item swipe listener called when the user swipes the item to the left/right. * Notified with the item position and direction. * * Values of direction: [ItemTouchHelper.LEFT] or [ItemTouchHelper.RIGHT] */ var onItemSwipeListener: OnItemSwipeListener? = null /* ------------------- DRAWING ------------------- */ /** Paint used to draw the left background */ private val leftPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) /** Paint used to draw the right background */ private val rightPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) /** Paint used to draw the left and right texts */ private val textPaint: Paint = Paint() private var bgAlphaAnimation: ValueAnimator private val recyclerViewAdapterDataObserver: RecyclerView.AdapterDataObserver private var pendingItemsAnimations: Int = 0 init { iconsSize = defaultIconSizeDp iconsHorizontalMargin = defaultIconHorizontalMarginDp leftText = defaultLeftText rightText = defaultRightText leftBackgroundColor = defaultLeftBackgroundColor rightBackgroundColor = defaultRightBackgroundColor textsTypeface = defaultTextsTypeface textsSize = defaultTextsSize.toFloat() textsColor = defaultTextsColor rightIconTint = defaultRightIconTint leftIconTint = defaultLeftIconTint bgAlphaAnimation = ValueAnimator.ofInt(255, 255 / 2).apply { interpolator = AccelerateDecelerateInterpolator() repeatCount = ValueAnimator.INFINITE repeatMode = ValueAnimator.REVERSE duration = 500 } ItemTouchHelper(this).attachToRecyclerView(recyclerView) recyclerViewAdapterDataObserver = object : RecyclerView.AdapterDataObserver() { override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { pendingItemsAnimations -= 1 if (pendingItemsAnimations == 0) { bgAlphaAnimation.cancel() bgAlphaAnimation.removeAllUpdateListeners() leftPaint.alpha = 255 rightPaint.alpha = 255 } } } recyclerView.adapter?.registerAdapterDataObserver(recyclerViewAdapterDataObserver) } override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val position = viewHolder.absoluteAdapterPosition pendingItemsAnimations += 1 if (!bgAlphaAnimation.isRunning) { bgAlphaAnimation.start() } onItemSwipeListener?.onItemSwiped(recyclerView, position, direction)// recyclerView.adapter?.notifyItemChanged(position) } override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { super.onChildDraw(c, recyclerView, viewHolder, dX * swipeableWidthPercentage, dY, actionState, isCurrentlyActive) val view = viewHolder.itemView val yCenter = (view.top + view.bottom) / 2 when { swipeToRightEnabled && dX > 0 -> { // Swipe to the right c.drawRect( view.left.toFloat(), view.top.toFloat(), view.left.toFloat() + (dX * swipeableWidthPercentage), view.bottom.toFloat(), leftPaint ) val leftTxtBounds = Rect() textPaint.getTextBounds(leftText, 0, leftText.length, leftTxtBounds) leftIcon?.let { it.setBounds( view.left + iconsHorizontalMargin, yCenter - halfIconSize.toInt() - leftTxtBounds.height() / 2, view.left + iconsHorizontalMargin + iconsSize, yCenter + halfIconSize.toInt() - leftTxtBounds.height() / 2 ) it.draw(c) } val y = yCenter + halfIconSize + leftTxtBounds.height() / 2 c.drawText( leftText, iconsHorizontalMargin + halfIconSize - leftTxtBounds.width() / 2, y, textPaint ) if (!isCurrentlyActive && abs(dX.toInt()) == view.width) { bgAlphaAnimation.addUpdateListener { leftPaint.alpha = it.animatedValue as Int view.requestLayout() } } } swipeToLeftEnabled && dX < 0 -> { // Swipe to the left c.drawRect( view.right.toFloat() + (dX * swipeableWidthPercentage), view.top.toFloat(), view.right.toFloat(), view.bottom.toFloat(), rightPaint ) val rightTxtBounds = Rect() textPaint.getTextBounds(rightText, 0, rightText.length, rightTxtBounds) rightIcon?.let { it.setBounds( view.right - iconsHorizontalMargin - iconsSize, yCenter - halfIconSize.toInt() - rightTxtBounds.height() / 2, view.right - iconsHorizontalMargin, yCenter + halfIconSize.toInt() - rightTxtBounds.height() / 2 ) it.draw(c) } val y = yCenter + halfIconSize + rightTxtBounds.height() / 2 c.drawText( rightText, view.right - iconsHorizontalMargin - halfIconSize - rightTxtBounds.width() / 2, y, textPaint ) if (!isCurrentlyActive && abs(dX.toInt()) == view.width) { bgAlphaAnimation.addUpdateListener { rightPaint.alpha = it.animatedValue as Int recyclerView.invalidate() } } } else -> { // view unSwiped } } } interface OnItemSwipeListener { fun onItemSwiped(recyclerView: RecyclerView, position: Int, direction: Int) }}fun Context.applyDimension(dimen: Int, unit: Int = TypedValue.COMPLEX_UNIT_DIP) = TypedValue.applyDimension( unit, dimen.toFloat(), resources.displayMetrics )
The class is ready to use with customizable left/right icons and texts.
- When the user swipes an item to left/right, the background is animated with an alpha animation and the
OnItemSwipeListener
is called. - The
swipeableWidthPercentage
param is used to determine how much width we wan't to use for drawing (0.25 by default). This answers the question.
Example usage of the listener:
override fun onItemSwiped(recyclerView: RecyclerView, position: Int, direction: Int) { val door = adapter.peek(position) when (direction) { ItemTouchHelper.LEFT -> { // TODO perform swipe to left action recyclerView.postDelayed( { Toast.makeText( requireContext(), getString(R.string.swipe_left_action), Toast.LENGTH_SHORT ).show() // Reset the item to stop the animation and return to original state adapter.notifyItemChanged(position) }, 2000 ) } ItemTouchHelper.RIGHT -> { // TODO perform swipe to right action recyclerView.postDelayed( { Toast.makeText( requireContext(), getString(R.string.swipe_right_action), Toast.LENGTH_SHORT ).show() // Reset the item to stop the animation and return to original state adapter.notifyItemChanged(position) }, 2000 ) } } }