在轻扫上的RecyclerView ItemTouchHelper按钮

Gus*_*sta 29 android android-recyclerview itemtouchhelper

我正在尝试将一些iOS功能移植到Android.

我打算创建一个表格,向左滑动显示2按钮:编辑和删除.

在此输入图像描述

我一直在玩它,我知道我非常接近.秘密真正在于OnChildDraw方法.

我想绘制一个适合文本删除的Rect,然后用它们各自的背景颜色绘制除此之外的Edit文本.单击时剩余的空白区域应将行恢复到其初始位置.

我设法在用户向侧面滑动时绘制背景,但我不知道如何添加侦听器,一旦将其滑到一边,拖动功能就开始行为不端.

我正在研究Xamarin,但纯java解决方案也被接受,因为我可以轻松地将它们移植到c#.

    public class SavedPlacesItemTouchHelper : ItemTouchHelper.SimpleCallback
    {
        private SavedPlacesRecyclerviewAdapter adapter;
        private Paint paint = new Paint();
        private Context context;

        public SavedPlacesItemTouchHelper(Context context, SavedPlacesRecyclerviewAdapter adapter) : base(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.Left)
        {
            this.context = context;
            this.adapter = adapter;
        }

        public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
        {
            return false;
        }

        public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
        {
        }

        public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
        {
            float translationX = dX;
            View itemView = viewHolder.ItemView;
            float height = (float)itemView.Bottom - (float)itemView.Top;

            if (actionState == ItemTouchHelper.ActionStateSwipe && dX <= 0) // Swiping Left
            {
                 translationX = -Math.Min(-dX, height * 2);
                paint.Color = Color.Red;
                RectF background = new RectF((float)itemView.Right + translationX, (float)itemView.Top, (float)itemView.Right, (float)itemView.Bottom);
                c.DrawRect(background, paint);

                //viewHolder.ItemView.TranslationX = translationX;
            }
            else if (actionState == ItemTouchHelper.ActionStateSwipe && dX > 0) // Swiping Right
            {
                translationX = Math.Min(dX, height * 2);
                paint.Color = Color.Red;
                RectF background = new RectF((float)itemView.Right + translationX, (float)itemView.Top, (float)itemView.Right, (float)itemView.Bottom);
                c.DrawRect(background, paint);
            }

            base.OnChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是我现在拥有的.

如果您知道如何添加听众或任何建议,请发表评论!

更新:

我刚刚意识到,在该行的白色剩余空间上双击已经将行恢复到其初始状态.虽然没有一个水龙头:(

Wen*_*eng 74

我在同样的问题上挣扎,并试图在线找到解决方案.大多数解决方案使用两层方法(一层视图项,另一层图层按钮),但我想坚持使用ItemTouchHelper.最后,我提出了一个有效的解决方案.请检查下面.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

public abstract class SwipeHelper extends ItemTouchHelper.SimpleCallback {

    public static final int BUTTON_WIDTH = YOUR_WIDTH_IN_PIXEL_PER_BUTTON
    private RecyclerView recyclerView;
    private List<UnderlayButton> buttons;
    private GestureDetector gestureDetector;
    private int swipedPos = -1;
    private float swipeThreshold = 0.5f;
    private Map<Integer, List<UnderlayButton>> buttonsBuffer;
    private Queue<Integer> recoverQueue;

    private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            for (UnderlayButton button : buttons){
                if(button.onClick(e.getX(), e.getY()))
                    break;
            }

            return true;
        }
    };

    private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent e) {
            if (swipedPos < 0) return false;
            Point point = new Point((int) e.getRawX(), (int) e.getRawY());

            RecyclerView.ViewHolder swipedViewHolder = recyclerView.findViewHolderForAdapterPosition(swipedPos);
            View swipedItem = swipedViewHolder.itemView;
            Rect rect = new Rect();
            swipedItem.getGlobalVisibleRect(rect);

            if (e.getAction() == MotionEvent.ACTION_DOWN || e.getAction() == MotionEvent.ACTION_UP ||e.getAction() == MotionEvent.ACTION_MOVE) {
                if (rect.top < point.y && rect.bottom > point.y)
                    gestureDetector.onTouchEvent(e);
                else {
                    recoverQueue.add(swipedPos);
                    swipedPos = -1;
                    recoverSwipedItem();
                }
            }
            return false;
        }
    };

    public SwipeHelper(Context context, RecyclerView recyclerView) {
        super(0, ItemTouchHelper.LEFT);
        this.recyclerView = recyclerView;
        this.buttons = new ArrayList<>();
        this.gestureDetector = new GestureDetector(context, gestureListener);
        this.recyclerView.setOnTouchListener(onTouchListener);
        buttonsBuffer = new HashMap<>();
        recoverQueue = new LinkedList<Integer>(){
            @Override
            public boolean add(Integer o) {
                if (contains(o))
                    return false;
                else
                    return super.add(o);
            }
        };

        attachSwipe();
    }


    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        int pos = viewHolder.getAdapterPosition();

        if (swipedPos != pos)
            recoverQueue.add(swipedPos);

        swipedPos = pos;

        if (buttonsBuffer.containsKey(swipedPos))
            buttons = buttonsBuffer.get(swipedPos);
        else
            buttons.clear();

        buttonsBuffer.clear();
        swipeThreshold = 0.5f * buttons.size() * BUTTON_WIDTH;
        recoverSwipedItem();
    }

    @Override
    public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
        return swipeThreshold;
    }

    @Override
    public float getSwipeEscapeVelocity(float defaultValue) {
        return 0.1f * defaultValue;
    }

    @Override
    public float getSwipeVelocityThreshold(float defaultValue) {
        return 5.0f * defaultValue;
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        int pos = viewHolder.getAdapterPosition();
        float translationX = dX;
        View itemView = viewHolder.itemView;

        if (pos < 0){
            swipedPos = pos;
            return;
        }

        if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
            if(dX < 0) {
                List<UnderlayButton> buffer = new ArrayList<>();

                if (!buttonsBuffer.containsKey(pos)){
                    instantiateUnderlayButton(viewHolder, buffer);
                    buttonsBuffer.put(pos, buffer);
                }
                else {
                    buffer = buttonsBuffer.get(pos);
                }

                translationX = dX * buffer.size() * BUTTON_WIDTH / itemView.getWidth();
                drawButtons(c, itemView, buffer, pos, translationX);
            }
        }

        super.onChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
    }

    private synchronized void recoverSwipedItem(){
        while (!recoverQueue.isEmpty()){
            int pos = recoverQueue.poll();
            if (pos > -1) {
                recyclerView.getAdapter().notifyItemChanged(pos);
            }
        }
    }

    private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX){
        float right = itemView.getRight();
        float dButtonWidth = (-1) * dX / buffer.size();

        for (UnderlayButton button : buffer) {
            float left = right - dButtonWidth;
            button.onDraw(
                    c,
                    new RectF(
                            left,
                            itemView.getTop(),
                            right,
                            itemView.getBottom()
                    ),
                    pos
            );

            right = left;
        }
    }

    public void attachSwipe(){
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this);
        itemTouchHelper.attachToRecyclerView(recyclerView);
    }

    public abstract void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons);

    public static class UnderlayButton {
        private String text;
        private int imageResId;
        private int color;
        private int pos;
        private RectF clickRegion;
        private UnderlayButtonClickListener clickListener;

        public UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener) {
            this.text = text;
            this.imageResId = imageResId;
            this.color = color;
            this.clickListener = clickListener;
        }

        public boolean onClick(float x, float y){
            if (clickRegion != null && clickRegion.contains(x, y)){
                clickListener.onClick(pos);
                return true;
            }

            return false;
        }

        public void onDraw(Canvas c, RectF rect, int pos){
            Paint p = new Paint();

            // Draw background
            p.setColor(color);
            c.drawRect(rect, p);

            // Draw Text
            p.setColor(Color.WHITE);
            p.setTextSize(LayoutHelper.getPx(MyApplication.getAppContext(), 12));

            Rect r = new Rect();
            float cHeight = rect.height();
            float cWidth = rect.width();
            p.setTextAlign(Paint.Align.LEFT);
            p.getTextBounds(text, 0, text.length(), r);
            float x = cWidth / 2f - r.width() / 2f - r.left;
            float y = cHeight / 2f + r.height() / 2f - r.bottom;
            c.drawText(text, rect.left + x, rect.top + y, p);

            clickRegion = rect;
            this.pos = pos;
        }
    }

    public interface UnderlayButtonClickListener {
        void onClick(int pos);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

SwipeHelper swipeHelper = new SwipeHelper(this, recyclerView) {
    @Override
    public void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons) {
        underlayButtons.add(new SwipeHelper.UnderlayButton(
                "Delete",
                0,
                Color.parseColor("#FF3C30"),
                new SwipeHelper.UnderlayButtonClickListener() {
                    @Override
                    public void onClick(int pos) {
                        // TODO: onDelete
                    }
                }
        ));

        underlayButtons.add(new SwipeHelper.UnderlayButton(
                "Transfer",
                0,
                Color.parseColor("#FF9502"),
                new SwipeHelper.UnderlayButtonClickListener() {
                    @Override
                    public void onClick(int pos) {
                        // TODO: OnTransfer
                    }
                }
        ));
        underlayButtons.add(new SwipeHelper.UnderlayButton(
                "Unshare",
                0,
                Color.parseColor("#C7C7CB"),
                new SwipeHelper.UnderlayButtonClickListener() {
                    @Override
                    public void onClick(int pos) {
                        // TODO: OnUnshare
                    }
                }
        ));
    }
};
Run Code Online (Sandbox Code Playgroud)

注意:此助手类专为左滑动而设计.您可以在SwipeHelper的构造函数中更改滑动方向,并相应地在onChildDraw方法中基于dX进行更改.

如果要在按钮中显示图像,只需在UnderlayButton中使用imageResId,然后重新实现onDraw方法.

有一个已知的错误,当您从一个项目沿对角线方向滑动项目时,第一个触摸的项目会闪烁一点.这可以通过降低getSwipeVelocityThreshold的值来解决,但这使得用户更难以滑动项目.您还可以通过更改getSwipeThresholdgetSwipeEscapeVelocity中的其他两个值来调整滑动感.检查ItemTouchHelper源代码,注释非常有用.

我相信有很多优化的地方.如果你想坚持使用ItemTouchHelper,这个解决方案只是给出了一个想法.如果您使用它有问题,请告诉我.下面是截图.

在此输入图像描述

致谢:这个解决方案主要来自AdamWei在这篇文章中的回答

  • 您可能想在“UnderlayButton”的“onDraw()”中使用“Paint.setAntiAlias(true)”和“Paint.setSubpixelText(true)”来使按钮文本更平滑(不像素化) (3认同)
  • 我不得不放弃这个解决方案。最终在向左滑动时删除并在向右滑动时进行编辑。但我想使用按钮。您的解决方案似乎是合法的,我下周会尝试,如果我有问题,我会告诉您,如果有效,我会将其标记为已接受的答案:D (2认同)
  • @WenxiZeng谢谢!通过维护变量 swipedBack 并在 dx==0 时将其设置在 onChildDraw 中来解决我遇到的问题。另外,我需要问的另一件事是我可以同时向右和向左滑动吗?我想我需要一个新的左按钮缓冲区? (2认同)

Nho*_*yen 26

这是基于公认答案方法的 Kotlin 版本。通过一些小的更改,我设法根据文本的内在大小而不是使用固定宽度来呈现按钮宽度。

演示项目:https : //github.com/ntnhon/RecyclerViewRowOptionsDemo

在此处输入图片说明

实施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<Int, List<UnderlayButton>> = mutableMapOf()
    private val recoverQueue = object : LinkedList<Int>() {
        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<UnderlayButton>,
        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<UnderlayButton>

    //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<SwipeHelper.UnderlayButton>.intrinsicWidth(): Float {
    if (isEmpty()) return 0.0f
    return map { it.intrinsicWidth }.reduce { acc, fl -> acc + fl }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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<UnderlayButton> {
                var buttons = listOf<UnderlayButton>()
                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")
                }
            })
    }
Run Code Online (Sandbox Code Playgroud)

  • 我下载了示例项目并将所有库更新到最新版本。不知怎的,拖动不能正常工作。 (2认同)

Geo*_*rge 6

我执行了以下操作以便能够绘制可绘制对象而不是文本:

  1. SwipeHelper,我改变了

    UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener)
    
    Run Code Online (Sandbox Code Playgroud)

    UnderlayButton(String text, Bitmap bitmap, int color, UnderlayButtonClickListener clickListener)
    
    Run Code Online (Sandbox Code Playgroud)

    当然,我删除imageResId并创建了一个Bitmap bitmap并将构造函数变量传递给它,用作this.bitmap = bitmap;其余的变量。

  2. 然后SwipeHelper.onDaw()您可以调用drawBitmap()将位图应用到画布。例如:

    c.drawBitmap(bitmap, rect.left, rect.top, p);
    
    Run Code Online (Sandbox Code Playgroud)

    其中cp分别是您的 Canvas 和 Paint 变量。

  3. 在我调用的活动中,我使用以下方法UnderlayButton将可绘制对象(在我的例子中是 a VectorDrawable)转换为位图:

    int idDrawable = R.drawable.ic_delete_white;
    Bitmap bitmap = getBitmapFromVectorDrawable(getContext(), idDrawable);
    
    Run Code Online (Sandbox Code Playgroud)

剩下要做的就是图标的居中。

文本和位图均居中的完整 onDraw 方法:

UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener)
Run Code Online (Sandbox Code Playgroud)


h8p*_*hak 6

对于所有想使用库来实现此目的的人,请查看以下内容:

https://github.com/chthai64/SwipeRevealLayout

并且,对于该库的精简版本,请查看:

https://android.jlelse.eu/android-recyclerview-swipeable-items-46a3c763498d

PS 您可以使用这些创建任何自定义布局(甚至使用Image Buttons)作为隐藏布局。