RecyclerView ItemTouchHelper Buttons on Swipe

后端 未结 11 1277
春和景丽
春和景丽 2020-11-27 11:05

I am trying to port some iOS functionality to Android.

I intent to create a table where on swipe to the left shows 2 button: Edit and Delete.

I have

11条回答
  •  暖寄归人
    2020-11-27 11:09

    Here is the Kotlin version based on the accepted answer approach. With some minor changes I managed to render the buttons width based on the intrinsic size of the text instead of using a fixed width.

    Demo project: https://github.com/ntnhon/RecyclerViewRowOptionsDemo

    Implementation of SwipeHelper:

    import android.annotation.SuppressLint
    import android.content.Context
    import android.graphics.*
    import android.view.MotionEvent
    import android.view.View
    import androidx.annotation.ColorRes
    import androidx.core.content.ContextCompat
    import androidx.recyclerview.widget.ItemTouchHelper
    import androidx.recyclerview.widget.RecyclerView
    import java.util.*
    import kotlin.math.abs
    import kotlin.math.max
    
    abstract class SwipeHelper(
        private val recyclerView: RecyclerView
    ) : ItemTouchHelper.SimpleCallback(
        ItemTouchHelper.ACTION_STATE_IDLE,
        ItemTouchHelper.LEFT
    ) {
        private var swipedPosition = -1
        private val buttonsBuffer: MutableMap> = mutableMapOf()
        private val recoverQueue = object : LinkedList() {
            override fun add(element: Int): Boolean {
                if (contains(element)) return false
                return super.add(element)
            }
        }
    
        @SuppressLint("ClickableViewAccessibility")
        private val touchListener = View.OnTouchListener { _, event ->
            if (swipedPosition < 0) return@OnTouchListener false
            buttonsBuffer[swipedPosition]?.forEach { it.handle(event) }
            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,
            itemView: View,
            dX: Float
        ) {
            var right = itemView.right
            buttons.forEach { button ->
                val width = button.intrinsicWidth / buttons.intrinsicWidth() * abs(dX)
                val left = right - width
                button.draw(
                    canvas,
                    RectF(left, itemView.top.toFloat(), right.toFloat(), itemView.bottom.toFloat())
                )
    
                right = left.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
    
            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 = max(-buttons.intrinsicWidth(), dX)
                    drawButtons(c, buttons, itemView, maxDX)
                }
            }
    
            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
    
        //region UnderlayButton
        interface UnderlayButtonClickListener {
            fun onClick()
        }
    
        class UnderlayButton(
            private val context: Context,
            private val title: String,
            textSize: Float,
            @ColorRes private val colorRes: Int,
            private val clickListener: UnderlayButtonClickListener
        ) {
            private var clickableRegion: RectF? = null
            private val textSizeInPixel: Float = textSize * context.resources.displayMetrics.density // dp to px
            private val horizontalPadding = 50.0f
            val intrinsicWidth: Float
    
            init {
                val paint = Paint()
                paint.textSize = textSizeInPixel
                paint.typeface = Typeface.DEFAULT_BOLD
                paint.textAlign = Paint.Align.LEFT
                val titleBounds = Rect()
                paint.getTextBounds(title, 0, title.length, titleBounds)
                intrinsicWidth = titleBounds.width() + 2 * horizontalPadding
            }
    
            fun draw(canvas: Canvas, rect: RectF) {
                val paint = Paint()
    
                // Draw background
                paint.color = ContextCompat.getColor(context, colorRes)
                canvas.drawRect(rect, paint)
    
                // Draw title
                paint.color = ContextCompat.getColor(context, android.R.color.white)
                paint.textSize = textSizeInPixel
                paint.typeface = Typeface.DEFAULT_BOLD
                paint.textAlign = Paint.Align.LEFT
    
                val titleBounds = Rect()
                paint.getTextBounds(title, 0, title.length, titleBounds)
    
                val y = rect.height() / 2 + titleBounds.height() / 2 - titleBounds.bottom
                canvas.drawText(title, rect.left + horizontalPadding, rect.top + y, paint)
    
                clickableRegion = rect
            }
    
            fun handle(event: MotionEvent) {
                clickableRegion?.let {
                    if (it.contains(event.x, event.y)) {
                        clickListener.onClick()
                    }
                }
            }
        }
        //endregion
    }
    
    private fun List.intrinsicWidth(): Float {
        if (isEmpty()) return 0.0f
        return map { it.intrinsicWidth }.reduce { acc, fl -> acc + fl }
    }
    

    Usage:

    private fun setUpRecyclerView() {
            binding.recyclerView.adapter = Adapter(listOf(
                "Item 0: No action",
                "Item 1: Delete",
                "Item 2: Delete & Mark as unread",
                "Item 3: Delete, Mark as unread & Archive"
            ))
            binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
            binding.recyclerView.layoutManager = LinearLayoutManager(this)
    
            val itemTouchHelper = ItemTouchHelper(object : SwipeHelper(binding.recyclerView) {
                override fun instantiateUnderlayButton(position: Int): List {
                    var buttons = listOf()
                    val deleteButton = deleteButton(position)
                    val markAsUnreadButton = markAsUnreadButton(position)
                    val archiveButton = archiveButton(position)
                    when (position) {
                        1 -> buttons = listOf(deleteButton)
                        2 -> buttons = listOf(deleteButton, markAsUnreadButton)
                        3 -> buttons = listOf(deleteButton, markAsUnreadButton, archiveButton)
                        else -> Unit
                    }
                    return buttons
                }
            })
    
            itemTouchHelper.attachToRecyclerView(binding.recyclerView)
        }
    
        private fun toast(text: String) {
            toast?.cancel()
            toast = Toast.makeText(this, text, Toast.LENGTH_SHORT)
            toast?.show()
        }
    
        private fun deleteButton(position: Int) : SwipeHelper.UnderlayButton {
            return SwipeHelper.UnderlayButton(
                this,
                "Delete",
                14.0f,
                android.R.color.holo_red_light,
                object : SwipeHelper.UnderlayButtonClickListener {
                    override fun onClick() {
                        toast("Deleted item $position")
                    }
                })
        }
    
        private fun markAsUnreadButton(position: Int) : SwipeHelper.UnderlayButton {
            return SwipeHelper.UnderlayButton(
                this,
                "Mark as unread",
                14.0f,
                android.R.color.holo_green_light,
                object : SwipeHelper.UnderlayButtonClickListener {
                    override fun onClick() {
                        toast("Marked as unread item $position")
                    }
                })
        }
    
        private fun archiveButton(position: Int) : SwipeHelper.UnderlayButton {
            return SwipeHelper.UnderlayButton(
                this,
                "Archive",
                14.0f,
                android.R.color.holo_blue_light,
                object : SwipeHelper.UnderlayButtonClickListener {
                    override fun onClick() {
                        toast("Archived item $position")
                    }
                })
        }
    

提交回复
热议问题