Instantly share code, notes, and snippets.
Last active
December 17, 2022 14:42
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save huhbxxd/fb2c98a428b57efadc40a210942f9087 to your computer and use it in GitHub Desktop.
Swipe to menu item simple item touch helper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SwipeButton private constructor( | |
private val context: Context, | |
private val title: String?, | |
@DimenRes private val textSize: Int?, | |
@ColorRes private val textColor: Int?, | |
@ColorRes private val backgroundColor: Int?, | |
@DrawableRes private val drawRes: Int?, | |
@DimenRes private val horizontalPadding: Int?, | |
private val clickListener: UnderlayButtonClickListener? | |
) { | |
private var clickableRegion: RectF? = null | |
private val textSizeDimension: Float = textSize | |
?.let { context.resources.getDimension(it) } ?: ZERO_SIZE | |
private val horizontalPaddingDimension: Float = | |
horizontalPadding?.let { context.resources.getDimension(it) } ?: ZERO_SIZE | |
private val verticalPaddingDimension: Float = horizontalPaddingDimension | |
val intrinsicWidth: Float | |
val instrinsicHeight: Float | |
init { | |
intrinsicWidth = initBoundsWidth() | |
instrinsicHeight = initBoundsHeight() | |
} | |
private fun initBoundsWidth(): Float { | |
return if (title != null) { // to safe let | |
initTextBoundWidth() | |
} else initDrawableBoundWidth() | |
} | |
private fun initBoundsHeight(): Float { | |
return if (title != null) { // to safe let | |
initTextBoundHeight() | |
} else initDrawableBoundHeight() | |
} | |
private fun initTextBoundWidth(): Float { | |
val paint = Paint() | |
paint.textSize = textSizeDimension | |
paint.typeface = Typeface.DEFAULT_BOLD | |
paint.textAlign = Paint.Align.LEFT | |
val titleBounds = Rect() | |
paint.getTextBounds(title, 0, title?.length ?: throw NullPointerException(), titleBounds) | |
return titleBounds.width() + 2 * horizontalPaddingDimension | |
} | |
private fun initDrawableBoundWidth(): Float { | |
return if (drawRes != null) { | |
val drawable = ContextCompat.getDrawable(context, drawRes) | |
(drawable?.intrinsicWidth?.toFloat() | |
?: throw NullPointerException("No drawable by id")) + 2 * horizontalPaddingDimension | |
} else throw NullPointerException("No define title or drawable") | |
} | |
private fun initTextBoundHeight(): Float { | |
val paint = Paint() | |
paint.textSize = textSizeDimension | |
paint.typeface = Typeface.DEFAULT_BOLD | |
paint.textAlign = Paint.Align.LEFT | |
val titleBounds = Rect() | |
paint.getTextBounds(title, 0, title?.length ?: throw NullPointerException(), titleBounds) | |
return titleBounds.height() + 2 * horizontalPaddingDimension | |
} | |
private fun initDrawableBoundHeight(): Float { | |
return if (drawRes != null) { | |
val drawable = ContextCompat.getDrawable(context, drawRes) | |
(drawable?.intrinsicHeight?.toFloat() | |
?: throw NullPointerException("No drawable by id")) + 2 * horizontalPaddingDimension | |
} else throw NullPointerException("No define title or drawable") | |
} | |
private fun drawTitleWithIcon(canvas: Canvas, rect: RectF, paint: Paint) { | |
val drawableBitmap = | |
ContextCompat.getDrawable( | |
context, | |
drawRes ?: throw NullPointerException("Require drawable!") | |
)?.toBitmap() | |
?: throw NullPointerException("No drawable by id") | |
// Draw title | |
paint.color = ContextCompat.getColor(context, textColor ?: R.color.black) | |
paint.textSize = textSizeDimension | |
paint.typeface = Typeface.DEFAULT_BOLD | |
paint.textAlign = Paint.Align.LEFT | |
val titleBounds = Rect() | |
paint.getTextBounds( | |
title, | |
0, | |
title?.length ?: throw NullPointerException("Require title!"), | |
titleBounds | |
) | |
val bitmapWidth = drawableBitmap.width | |
val bitmapHeight = drawableBitmap.height | |
val yImage = | |
rect.top + rect.height() / 2 - (bitmapHeight / 2) - titleBounds.height() / 2 - context.resources.getDimension( | |
R.dimen.low_space | |
) | |
val xImage = | |
rect.right - (titleBounds.width() / 2) - horizontalPaddingDimension - (bitmapWidth / 2) | |
val yText = | |
(rect.height() / 2 + titleBounds.height() / 2 - titleBounds.bottom) + rect.top + bitmapHeight / 2 | |
val xText = rect.right - titleBounds.width() - horizontalPaddingDimension | |
canvas.drawText(title, xText, yText, paint) | |
// draw icon | |
canvas.drawBitmap(drawableBitmap, xImage, yImage, null) | |
} | |
private fun drawIcon(canvas: Canvas, rect: RectF, paint: Paint) { | |
val drawableBitmap = | |
ContextCompat.getDrawable( | |
context, | |
drawRes ?: throw NullPointerException("Require drawable!") | |
)?.toBitmap() ?: throw NullPointerException("No drawable by id") | |
val bitmapWidth = drawableBitmap.width | |
val bitmapHeight = drawableBitmap.height | |
val yImage = rect.top + rect.height() / 2 - (bitmapHeight / 2) | |
val xImage = rect.right - horizontalPaddingDimension - bitmapWidth | |
canvas.drawBitmap(drawableBitmap, xImage, yImage, null) | |
} | |
private fun drawTitle(canvas: Canvas, rect: RectF, paint: Paint) { | |
// Draw title | |
paint.color = ContextCompat.getColor(context, textColor ?: R.color.black) | |
paint.textSize = textSizeDimension | |
paint.typeface = Typeface.DEFAULT_BOLD | |
paint.textAlign = Paint.Align.LEFT | |
val titleBounds = Rect() | |
paint.getTextBounds( | |
title, | |
0, | |
title?.length ?: throw NullPointerException("Require title!"), | |
titleBounds | |
) | |
val yText = | |
(rect.height() / 2 + titleBounds.height() / 2 - titleBounds.bottom) + rect.top | |
val xText = rect.right - titleBounds.width() - horizontalPaddingDimension | |
canvas.drawText(title, xText, yText, paint) | |
} | |
fun draw(canvas: Canvas, rect: RectF) { | |
val paint = Paint() | |
// Draw background | |
paint.color = ContextCompat.getColor(context, backgroundColor ?: R.color.purple_200) | |
canvas.drawRect(rect, paint) | |
when { | |
title != null && drawRes != null -> drawTitleWithIcon(canvas, rect, paint) | |
drawRes != null -> drawIcon(canvas, rect, paint) | |
title != null -> drawTitle(canvas, rect, paint) | |
} | |
clickableRegion = rect | |
} | |
fun handle(event: MotionEvent) { | |
clickableRegion?.let { | |
if (it.contains(event.x, event.y)) { | |
clickListener?.onClick() | |
} | |
} | |
} | |
companion object { | |
private const val ZERO_SIZE = 0.0f | |
} | |
class Builder { | |
private var title: String? = null | |
@DimenRes | |
private var textSize: Int? = null | |
@ColorRes | |
private var textColor: Int? = null | |
@ColorRes | |
private var backgroundColor: Int? = null | |
@DrawableRes | |
private var drawRes: Int? = null | |
@DimenRes | |
private var horizontalPadding: Int? = null | |
private var clickListener: UnderlayButtonClickListener? = null | |
fun setTitle(title: String) = apply { this.title = title } | |
fun setTextSize(@DimenRes textSize: Int) = apply { this.textSize = textSize } | |
fun setBackgroundColor(@ColorRes colorRes: Int) = apply { backgroundColor = colorRes } | |
fun setTextColor(@ColorRes colorRes: Int) = apply { textColor = colorRes } | |
fun setDrawRes(@DrawableRes drawRes: Int) = apply { this.drawRes = drawRes } | |
fun setClickListener(listener: UnderlayButtonClickListener) = | |
apply { clickListener = listener } | |
fun setHorizontalPadding(@DimenRes horizontalPadding: Int) = | |
apply { this.horizontalPadding = horizontalPadding } | |
fun build(context: Context) = | |
SwipeButton( | |
context, | |
title, | |
textSize, | |
textColor, | |
backgroundColor, | |
drawRes, | |
horizontalPadding, | |
clickListener | |
) | |
} | |
} | |
interface UnderlayButtonClickListener { | |
fun onClick(position: Int) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@SuppressLint("ClickableViewAccessibility") | |
abstract class SwipeToMenu( | |
private val recyclerView: RecyclerView, | |
private val orientation: Orientation = Orientation.VERTICAL | |
) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.LEFT) { | |
enum class Orientation { HORIZONTAL, VERTICAL } | |
private var swipedPosition = -1 | |
private val buttonsBuffer: MutableMap<Int, List<SwipeButton>> = mutableMapOf() | |
private val recoverQueue = object : LinkedList<Int>() { | |
override fun add(element: Int): Boolean { | |
if (contains(element)) return false | |
return super.add(element) | |
} | |
} | |
private val touchListener = View.OnTouchListener { _, event -> | |
if (swipedPosition < 0) return@OnTouchListener false | |
buttonsBuffer[swipedPosition]?.forEach { it.handle(event, swipePosition) } | |
recoverQueue.add(swipedPosition) | |
swipedPosition = -1 | |
recoverSwipedItem() | |
true | |
} | |
init { | |
recyclerView.setOnTouchListener(touchListener) | |
} | |
private fun recoverSwipedItem() { | |
while (!recoverQueue.isEmpty()) { | |
val position = recoverQueue.poll() ?: return | |
recyclerView.adapter?.notifyItemChanged(position) | |
} | |
} | |
private fun drawButtons( | |
canvas: Canvas, | |
buttons: List<SwipeButton>, | |
itemView: View, | |
dX: Float, | |
maxHeight: Float | |
) { | |
var right = itemView.right | |
var bottom = itemView.bottom | |
when (orientation) { | |
Orientation.VERTICAL -> { | |
buttons.forEach { button -> | |
val width = | |
button.intrinsicWidth / buttons.intrinsicWidth() * kotlin.math.abs(dX) | |
val left = right - width | |
button.draw( | |
canvas, | |
RectF( | |
left, | |
itemView.top.toFloat(), | |
right.toFloat(), | |
itemView.bottom.toFloat() | |
) | |
) | |
right = left.toInt() | |
} | |
} | |
Orientation.HORIZONTAL -> { | |
buttons.forEach { button -> | |
val height = maxHeight / buttons.size | |
val top = bottom - height | |
val width = kotlin.math.abs(dX) | |
val left = right - width | |
button.draw( | |
canvas, | |
RectF(left, top, right.toFloat(), bottom.toFloat()) | |
) | |
bottom = top.toInt() | |
} | |
} | |
} | |
} | |
override fun onChildDraw( | |
c: Canvas, | |
recyclerView: RecyclerView, | |
viewHolder: RecyclerView.ViewHolder, | |
dX: Float, | |
dY: Float, | |
actionState: Int, | |
isCurrentlyActive: Boolean | |
) { | |
val position = viewHolder.adapterPosition | |
var maxDX = dX | |
val itemView = viewHolder.itemView | |
val isCanceled = dX == 0f && !isCurrentlyActive | |
if (isCanceled) { | |
itemView.setLayerType(View.LAYER_TYPE_SOFTWARE, null) | |
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) | |
return | |
} | |
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { | |
if (dX < 0) { | |
if (!buttonsBuffer.containsKey(position)) { | |
buttonsBuffer[position] = instantiateUnderlayButton(position) | |
} | |
val buttons = buttonsBuffer[position] ?: return | |
if (buttons.isEmpty()) return | |
maxDX = when (orientation) { | |
Orientation.VERTICAL -> max(-buttons.intrinsicWidth(), dX) | |
Orientation.HORIZONTAL -> max( | |
-buttons.maxBy { it.intrinsicWidth }.intrinsicWidth, | |
dX | |
) | |
} | |
val maxHeight = max(buttons.intrinsicHeight(), itemView.height.toFloat()) | |
drawButtons(c, buttons, itemView, maxDX, maxHeight) | |
} | |
} | |
super.onChildDraw( | |
c, | |
recyclerView, | |
viewHolder, | |
maxDX, | |
dY, | |
actionState, | |
isCurrentlyActive | |
) | |
} | |
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.adapterPosition | |
if (swipedPosition != position) recoverQueue.add(swipedPosition) | |
swipedPosition = position | |
recoverSwipedItem() | |
} | |
abstract fun instantiateUnderlayButton(position: Int): List<SwipeButton> | |
} | |
private fun List<SwipeButton>.intrinsicWidth(): Float { | |
if (isEmpty()) return 0.0f | |
return map { it.intrinsicWidth }.reduce { acc, fl -> acc + fl } | |
} | |
private fun List<SwipeButton>.intrinsicHeight(): Float { | |
if (isEmpty()) return 0.0f | |
return map { it.instrinsicHeight }.reduce { acc, fl -> acc + fl } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment