53

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.

enter image description here

I have been playing with it and I know I am very close. The secret really lies on the method OnChildDraw.

I would like to Draw a Rect that fits the text Delete then draw the Edit text besides it with their respective background color. The remaining white space when clicked should restore the row to its initial position.

I have managed to paint the background while the user is swiping to the sides but I don't know how to add the listeners and once it is swiped to the side, the dragging function begins to misbehave.

I am working on Xamarin but pure java solutions also are accepted as I can easily port them to 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);
        }
    }
}

This is what I currently have.

If you know how to add listeners or any suggestions please leave a comment!

UPDATE:

I just realized that on double tap on the white remaining space of the row already restore the row to its initial state. Not a single tap though :(

4

11 回答 11

124

我在同样的问题上苦苦挣扎,并试图在网上找到解决方案。大多数解决方案使用两层方法(一层视图项,另一层按钮),但我只想坚持使用 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);
    }
}

用法:

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
                    }
                }
        ));
    }
};

注意:这个助手类是为左滑动设计的。您可以在SwipeHelper 的构造函数中更改滑动方向,并在onChildDraw方法中根据 dX 进行相应的更改。

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

有一个已知的错误,当您将一个项目从一个项目对角滑动到另一个项目时,第一个触摸的项目会闪烁一点。这可以通过减小getSwipeVelocityThreshold的值来解决,但这会使用户更难滑动项目。您还可以通过更改getSwipeThresholdgetSwipeEscapeVelocity中的其他两个值来调整滑动感觉。查看 ItemTouchHelper 源代码,评论很有帮助。

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

在此处输入图像描述

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

于 2017-07-12T16:06:00.717 回答
28

这是基于公认答案方法的 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 }
}

用法:

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")
                }
            })
    }
于 2020-06-08T14:59:59.550 回答
5

对于所有想要为此使用库的人,请查看:

https://github.com/chthai64/SwipeRevealLayout

而且,对于这个库的精简版本,签出:

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

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

于 2020-06-03T20:21:38.973 回答
3

我做了以下能够绘制一个drawable而不是文本:

  1. SwipeHelper,我改变了

    UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener)
    

    UnderlayButton(String text, Bitmap bitmap, int color, UnderlayButtonClickListener clickListener)
    

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

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

    c.drawBitmap(bitmap, rect.left, rect.top, p);
    

    Wherecp你的 Canvas 和 Paint 变量分别。

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

    int idDrawable = R.drawable.ic_delete_white;
    Bitmap bitmap = getBitmapFromVectorDrawable(getContext(), idDrawable);
    

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

以文本和位图为中心的完整 onDraw 方法:

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(24);


            float spaceHeight = 10; // change to whatever you deem looks better
            float textWidth = p.measureText(text);
            Rect bounds = new Rect();
            p.getTextBounds(text, 0, text.length(), bounds);
            float combinedHeight = bitmap.getHeight() + spaceHeight + bounds.height();
            c.drawBitmap(bitmap, rect.centerX() - (bitmap.getWidth() / 2), rect.centerY() - (combinedHeight / 2), null);
            //If you want text as well with bitmap
            c.drawText(text, rect.centerX() - (textWidth / 2), rect.centerY() + (combinedHeight / 2), p);

            clickRegion = rect;
            this.pos = pos;
        }
于 2019-11-08T09:11:58.770 回答
2

我迟到了,但如果有人在寻找 UIKit UITableView 删除按钮行为,那么你可以在 Xamarin.Android 中使用这样的东西和 RecyclerView:

public class SwipeDeleteHelper : ItemTouchHelper.Callback
{
    private int _startingWidth = 0;
    private bool? _rightAlignedText = null;
    private bool _alreadyClicked = false;

    private static float _previousDx = float.NegativeInfinity;
    private static float _viewWidth = float.NegativeInfinity;
    private static float _permanentlyDeleteThreshold = float.NegativeInfinity;

    private static RecyclerView.ViewHolder _currentViewHolder;
    private RecyclerView.ViewHolder CurrentViewHolder
    {
        get => _currentViewHolder;

        set
        {
            _startingWidth = 0;
            _rightAlignedText = null;
            _alreadyClicked = false;

            _previousDx = float.NegativeInfinity;

            _currentViewHolder = value;
        }
    }
    /*
    You can create a method in a utility class for the buttonwidth conversion like this:
    public static float GetPxFromDp(float dp)
    {
        return dp * Application.Context.ApplicationContext.Resources.DisplayMetrics.Density;
    }
    Also you can use text width measurement to determine the optimal width of the button for your delete text.
    */
    public static int buttonWidth = 60 * Application.Context.ApplicationContext.Resources.DisplayMetrics.Density;

    public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
    {
        if (viewHolder is EntryCell)
        {
            return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.Left | ItemTouchHelper.Start | ItemTouchHelper.Right | ItemTouchHelper.End);
        }

        return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.ActionStateIdle);
    }

    public override void OnSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
    {
        if (float.IsNegativeInfinity(_permanentlyDeleteThreshold))
        {
            _viewWidth = viewHolder.ItemView.Width;
            _permanentlyDeleteThreshold = (viewHolder.ItemView.Width * 3f / 4f);
        }

        if (viewHolder != CurrentViewHolder)
        {
            if (viewHolder != null) // This is a new selection and the button of the previous viewHolder should get hidden.
            {
                (CurrentViewHolder as EntryCell)?.ResetView(CurrentViewHolder);

                CurrentViewHolder = viewHolder;
            }
            else if (CurrentViewHolder != null) // This is the end of the previous selection
            {
                var hidden = CurrentViewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

                _previousDx = float.NegativeInfinity;

                if (hidden.LayoutParameters.Width > _permanentlyDeleteThreshold && !_alreadyClicked) // released in permanent delete area
                {
                    _alreadyClicked = true;

                    hidden.LayoutParameters.Width = CurrentViewHolder.ItemView.Width;

                    hidden.CallOnClick();

                    CurrentViewHolder = null;
                }
                else
                {
                    _startingWidth = hidden.LayoutParameters.Width >= buttonWidth ? buttonWidth : 0;

                    hidden.LayoutParameters.Width = _startingWidth;
                }

                AlignDeleteButtonText(hidden);

                hidden.RequestLayout();
            }
        }

        base.OnSelectedChanged(viewHolder, actionState);
    }

    public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
    {
        if (actionState == ItemTouchHelper.ActionStateSwipe && !_alreadyClicked)
        {
            var hidden = viewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

            if (isCurrentlyActive) // swiping
            {
                if (float.IsNegativeInfinity(_previousDx)) // This is a new swipe
                {
                    _previousDx = dX;
                }

                if (Math.Abs(dX - _previousDx) > 0.1f && Math.Abs(dX - (-_viewWidth)) > 0.1f)
                {
                    hidden.LayoutParameters.Width = Math.Max(0, (int)Math.Round(hidden.LayoutParameters.Width - (dX >= _previousDx ? 1 : -1) * (Math.Abs(dX - _previousDx))));

                    _previousDx = dX;

                    AlignDeleteButtonText(hidden);

                    hidden.RequestLayout();
                }
            }
        }
    }

    private void AlignDeleteButtonText(Button hidden)
    {
        if (_rightAlignedText != false && hidden.LayoutParameters.Width >= _permanentlyDeleteThreshold) // pulled into permanent delete area
        {
            hidden.Gravity = GravityFlags.AxisSpecified | GravityFlags.AxisPullBefore | GravityFlags.CenterVertical;
            _rightAlignedText = false;
        }
        else if (_rightAlignedText != null && hidden.LayoutParameters.Width <= buttonWidth)
        {
            hidden.Gravity = GravityFlags.Center;
            _rightAlignedText = null;
        }
        else if (_rightAlignedText != true && hidden.LayoutParameters.Width > buttonWidth && hidden.LayoutParameters.Width < _permanentlyDeleteThreshold) // pulled back from permanent delete area
        {
            hidden.Gravity = GravityFlags.AxisSpecified | GravityFlags.AxisPullAfter | GravityFlags.CenterVertical;
            _rightAlignedText = true;
        }
    }

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

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

EntryCell 是 MvxRecyclerViewHolder 的后代,它应该包含如下内容:

public class EntryCell : MvxRecyclerViewHolder
{
    public EntryCell(View itemView, IMvxAndroidBindingContext context) : base(itemView, context)
    {
        Button _delButton = itemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

        _delButton.Text = "Delete";
    }

    public void ResetView(RecyclerView.ViewHolder currentViewHolder)
    {
        var hidden = currentViewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

        hidden.LayoutParameters.Width = 0;

        hidden.RequestLayout();
    }
}

您的视图中应该有一个按钮(在 EntryCell 中作为 Resource.Id.fileListDeleteButton 引用,因此按钮的 ID 是 fileListDeleteButton)。我使用 XML 作为视图,它看起来像这样:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:orientation="vertical">

<!-- The rest of your code... -->

<Button
    android:id="@+id/fileListDeleteButton"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_alignParentRight="true"
    android:paddingHorizontal="@dimen/abc_button_padding_horizontal_material"
    android:background="#f00"
    android:textColor="@android:color/white"
    android:textAllCaps="false"
    android:singleLine="true"
    android:ellipsize="none"
    android:text="dummy" />
</RelativeLayout>

在 RecyclerView 所在的代码中,像这样使用它:

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeDeleteHelper());
itemTouchHelper.AttachToRecyclerView(yourRecyclerView);

我希望这可以帮助别人。

于 2019-09-04T09:06:39.683 回答
2

If you want button(s) on left side as well when swipe in the other direction, just try to add this simple lines in the existing answer:

  1. In the drawButtons method:

    private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX) {
        float right = itemView.getRight();
        float left = itemView.getLeft();
        float dButtonWidth = (-1) * dX / buffer.size();
    
        for (UnderlayButton button : buffer) {
            if (dX < 0) {
                left = right - dButtonWidth;
                button.onDraw(
                        c,
                        new RectF(
                                left,
                                itemView.getTop(),
                                right,
                                itemView.getBottom()
                        ),
                        pos, dX //(to draw button on right)
                );
    
                right = left;
            } else if (dX > 0) {
                right = left - dButtonWidth;
                button.onDraw(c,
                        new RectF(
                                right,
                                itemView.getTop(),
                                left,
                                itemView.getBottom()
                        ), pos, dX //(to draw button on left)
                );
    
    
            }
        }
    }
    
  2. In the onDraw method check the value of dX and set the text and the colour of the buttons:

    public void onDraw(Canvas c, RectF rect, int pos, float dX) {
            Paint p = new Paint();
    
            // Draw background
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
    
                if (dX > 0)
                    p.setColor(Color.parseColor("#23d2c5"));
                else if (dX < 0)
                    p.setColor(Color.parseColor("#23d2c5"));
    
                c.drawRect(rect, p);
    
    
                // Draw Text
                p.setColor(Color.WHITE);
                p.setTextSize(36);
                //  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;
                if (dX > 0) {
                    p.setColor(Color.parseColor("#23d2c5"));
                    c.drawText("Reject", rect.left + x, rect.top + y, p);
                } else if (dX < 0) {
    
                    c.drawText(text, rect.left + x, rect.top + y, p);
                }
                clickRegion = rect;
                this.pos = pos;
            }
    
        }
    
于 2018-07-10T06:27:28.930 回答
2

按照 Wenxi Zeng's answer here,如果您想让按钮中的文本多行,请将 UnderlayButton 的 onDraw 方法替换为:

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

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

        // Draw Text
        TextPaint textPaint = new TextPaint();
        textPaint.setTextSize(UtilitiesOperations.convertDpToPx(getContext(), 15));
        textPaint.setColor(Color.WHITE);
        StaticLayout sl = new StaticLayout(text, textPaint, (int)rect.width(),
                Layout.Alignment.ALIGN_CENTER, 1, 1, false);

        canvas.save();
        Rect r = new Rect();
        float y = (rect.height() / 2f) + (r.height() / 2f) - r.bottom - (sl.getHeight() /2);
        canvas.translate(rect.left, rect.top + y);
        sl.draw(canvas);
        canvas.restore();

        clickRegion = rect;
        this.pos = pos;
    }
于 2017-10-19T10:01:08.140 回答
1

由于我还没有看到任何地方如何实现它并且我确实设法让它工作,我将发布一个解决这个问题的解决方案,但是它在 c# Xamarin Android 中有效。

如果你需要它原生的android,你将不得不将它转换成android native,这应该不难。如果非常需要,我可能会在以后这样做。

这是我的 ItemHelper 基类:

 internal abstract class ItemTouchHelperBase : ItemTouchHelper.Callback
    {
        protected RecyclerViewAdapterBase adapter;
        public int currentPosition = -1;
        public Rect ItemRect = new Rect();

        private Paint backgroundPaint = new Paint();
        private Rect backgroundBounds = new Rect();
        private TextPaint textPaint = new TextPaint();
        private string deleteText;
        private readonly float textWidth;
        private readonly float textHeight;

        public ItemTouchHelperBase()
        {
            backgroundPaint.Color = new Color(ContextCompat.GetColor(Application.Context, Resource.Color.delete_red));
            textPaint.Color = Color.White;
            textPaint.AntiAlias = true;
            textPaint.TextSize = FontHelper.GetFontSize(Application.Context, Resource.Dimension.font_size_button);
            deleteText = "  " + StringResource.delete + "  ";

            Rect textBounds = new Rect();
            textPaint.GetTextBounds(deleteText, 0, deleteText.Length, textBounds);

            textHeight = textBounds.Height();
            textWidth = textPaint.MeasureText(deleteText);
        }

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

        public override void ClearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
        {
            if (adapter != null)
            {
                ItemRect = new Rect();
            }
            base.ClearView(recyclerView, viewHolder);
        }

        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)
        {
            // Note: Don't create variables inside OnDraw due to performance issues
            try
            {
                if (actionState == ItemTouchHelper.ActionStateSwipe)
                {
                    if (dX <= 0) // Left swipe
                    {
                        // Swipe up to text width accordingly to ratio
                        dX /= viewHolder.ItemView.Right / textWidth;

                        //Draw background
                        backgroundBounds = new Rect(
                            viewHolder.ItemView.Right + (int) dX,
                            viewHolder.ItemView.Top,
                            viewHolder.ItemView.Right,
                            viewHolder.ItemView.Bottom);
                        c.DrawRect(backgroundBounds, backgroundPaint);

                        if (adapter != null)
                        {
                            ItemRect = backgroundBounds;
                        }

                        //Draw text
                        c.DrawText(
                            deleteText,
                            (float) viewHolder.ItemView.Right - textWidth, viewHolder.ItemView.Top + (viewHolder.ItemView.Height / 2) + (textHeight / 2),
                            textPaint);
                    }

                    base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
                }
            }
            catch (Exception)
            {
            }
        }

        internal void AttachToRecyclerview(RecyclerView recycleview)
        {
            new ItemTouchHelper(this).AttachToRecyclerView(recycleview);
        }

        public void ClickOutsideDeleteButton()
        {
            try
            {
                if (currentPosition != -1)
                {
                    PutRowBackToDefault();
                }
            }
            catch (Exception)
            {
            }
        }

        protected void PutRowBackToDefault()
        {
            adapter.NotifyItemChanged(currentPosition);
            currentPosition = -1;
        }
    }

然后在您的项目助手类上,您有:

 internal class MyItemsTouchHelperCallback : ItemTouchHelperBase
    {
        public MyItemsTouchHelperCallback (MyAdapter adapter)
        {
            this.adapter = adapter;
        }

        public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
        {
            try
            {
                if (currentPosition != -1 && currentPosition != viewHolder.AdapterPosition)
                {
                    PutRowBackToDefault();
                }
                currentPosition = viewHolder.AdapterPosition;
            }
            catch (Exception)
            {
            }

            int swipeFlags = viewHolder is MyViewHolder ? ItemTouchHelper.Start : ItemTouchHelper.ActionStateIdle;
            return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, swipeFlags);
        }
    }

然后在您的活动中,您有:

把这个 OnCreate

        recycleViewLayoutManager = new LinearLayoutManager(this);
        recycler_view_main.SetLayoutManager(recycleViewLayoutManager);
        recyclerAdapter = new MyAdapter(this, this);
        recycler_view_main.SetAdapter(recyclerAdapter);
        myItemsTouchHelperCallback = new MyItemsTouchHelperCallback (recyclerAdapter);
        myItemsTouchHelperCallback .AttachToRecyclerview(recycler_view_main);

然后在活动中覆盖此方法:

 public override bool DispatchTouchEvent(MotionEvent e)
            {
                int[] recyclerviewLocationOnScreen = new int[2];
                recycler_view_main.GetLocationOnScreen(recyclerviewLocationOnScreen);

                TouchEventsHelper.TouchUpEvent(
                    e.Action,
                    e.GetX() - recyclerviewLocationOnScreen[0],
                    e.GetY() - recyclerviewLocationOnScreen[1],
                    myItemsTouchHelperCallback .ItemRect,
                    delegate
                    {
                        // Delete your row
                    },
                    delegate
                    { myItemsTouchHelperCallback .ClickOutsideDeleteButton(); });


                return base.DispatchTouchEvent(e);
            }

这是我创建的供调度事件使用的辅助方法:

internal static void TouchUpEvent(MotionEventActions eventActions, float x, float y, Rect rectangle, Action ActionDeleteClick, Action NormalClick)
        {
            try
            {
                if (rectangle.Contains((int) x, (int) y))
                {
                    //inside delete button
                    if (eventActions == MotionEventActions.Down)
                    {
                        isClick = true;
                    }
                    else if (eventActions == MotionEventActions.Up || eventActions == MotionEventActions.Cancel)
                    {
                        if (isClick)
                        {
                            ActionDeleteClick.Invoke();
                        }
                    }
                }
                else if (eventActions == MotionEventActions.Up ||
                         eventActions == MotionEventActions.Cancel ||
                         eventActions == MotionEventActions.Down)
                {
                    //click anywhere outside delete button
                    isClick = false;
                    if (eventActions == MotionEventActions.Down)
                    {
                        NormalClick.Invoke();
                    }
                }
            }
            catch (Exception)
            {
            }
        }

它有点复杂,但效果很好。我已经在很多方面对此进行了测试。如果您在执行此操作时遇到任何问题,请告诉我

于 2019-06-21T12:07:19.480 回答
0

我有一个更简单的解决方案:

  1. 向您的行 XML 添加一个按钮,宽度为 0,浮动在右侧:
> <Button
>     android:id="@+id/hidden"
>     android:layout_width="0dp"
>     android:layout_height="match_parent"
>     android:layout_alignParentRight = "true">
  1. 在 onChildDraw() 中,只需将其宽度增加 dX 值。

     int position = viewHolder.getAdapterPosition();
     View v = recyclerView.getLayoutManager().findViewByPosition(position);
     Button hidden = v.findViewById(R.id.hidden);
     hidden.setLayoutParams(new LinearLayout.LayoutParams((int)-dX, -1));
    

确保不要调用默认的 super.onChildDraw()

于 2018-10-11T06:56:39.730 回答
0

我也想在我的应用程序中使用这种触摸手势,在使用 Itemtouchhelper 工作太多之后,我决定编写自己的触摸处理程序:

    private class TouchHelper : Java.Lang.Object, View.IOnTouchListener
    {
        ViewHolder vh;
        public TouchHelper(ViewHolder vh)
        { this.vh = vh;  }

        float DownX, DownY; bool isSliding;
        TimeSpan tsDown;
        public bool OnTouch(View v, MotionEvent e)
        {
            switch (e.Action)
            {
                case MotionEventActions.Down:
                    DownX = e.GetX(); DownY = e.GetY();
                    tsDown = TimeSpan.Now;
                    break;
                case MotionEventActions.Move:
                    float deltaX = e.GetX() - DownX, deltaY = e.GetX() - DownY;
                    if (Math.Abs(deltaX) >= Values.ScreenWidth / 20 || Math.Abs(deltaY) >= Values.ScreenWidth / 20)
                        isSliding = Math.Abs(deltaX) > Math.Abs(deltaY);

                    //TextsPlace is the layout that moves with touch
                    if(isSliding)
                        vh.TextsPlace.TranslationX = deltaX / 2;

                    break;
                case MotionEventActions.Cancel:
                case MotionEventActions.Up:
                    //handle if touch was for clicking
                    if (Math.Abs(deltaX) <= 50 && (TimeSpan.Now - tsDown).TotalMilliseconds <= 400)
                        vh.OnTextsPlaceClick(vh.TextsPlace, null);
                    break;
            }
            return true;
        }
    }

注意:在创建视图时将其设置为视图内容的 ontouchlistener。您可以添加动画以将项目返回到第一个位置。

您还可以编写自定义布局管理器以在项目滑动时阻止垂直滚动。

于 2017-07-07T08:23:35.937 回答
0

如果您使用 a RecyclerView,请尝试使用OnScrollListener. 做这样的事情。

 private class YourOnScrollListener extends RecyclerView.OnScrollListener {

    private boolean directionLeft;

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            if (directionLeft) drawButtons();
            //Draw buttons here if you want them to be drawn after scroll is finished
            //here you can play with states, to draw buttons or erase it whenever you want
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dx < 0) directionLeft = true;
        else directionLeft = false;
    }

}
于 2017-07-07T07:53:51.407 回答