Android 可长按拖拽的RecyclerView

近期项目遇到需要拖拽的RecyclerView来实现需求,首先考虑了ItemTouchHelper 这个类,但是后续使用发现无法把item视图拖出RecyclerView的视图范围,于是打算自定义RecyclerView来实现长按item可在屏幕内随意拖拽,在此简单记录一下。

实现效果

主要思路

  1. 继承RecylerView,重写dispatchTouchEvent
  2. 根据findChildViewUnder和getChildAdapterPosition方法获取到手指所在的View和索引
  3. 利用WindowManager添加获取到的视图到屏幕窗口
  4. 根据手指滑动的位置索引改变添加的视图位置,实现滑动

实现代码

package com.pgc.dragrecyclerviewdemo;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;

import androidx.recyclerview.widget.RecyclerView;

import java.util.Objects;

/**
 * @explain 长按移动的recyclerView
 * @author Created by PengGuiChu on 2022/4/11 11:58
 */
public class DragRecyclerView extends RecyclerView {

    //拖拽响应的时间 默认为1s
    private long mDragResponseMs = 1000;
    //是否支持拖拽,默认不支持
    private boolean isDrag = false;
    //振动器,用于提示替换
    private final Vibrator mVibrator;
    //拖拽的item的position
    private int mDragPosition;
    //拖拽的item对应的View
    private View mDragView;

    //窗口管理器,用于为Activity上添加拖拽的View
    private final WindowManager mWindowManager;
    //item镜像的布局参数
    private WindowManager.LayoutParams mLayoutParams;

    //item镜像的 显示镜像,这里用ImageView显示
    private ImageView mDragMirrorView;
    //item镜像的bitmap
    private Bitmap mDragBitmap;

    //按下的点到所在item的左边缘距离
    private int mPoint2ItemLeft;
    private int mPoint2ItemTop;

    //DragView到上边缘的距离
    private int mOffset2Top;
    private int mOffset2Left;

    //按下时x,y
    private int mDownX;
    private int mDownY;
    //移动的时x.y
    private int mMoveX;
    private int mMoveY;

    //状态栏高度
    private final int mStatusHeight;

    //item发生变化的回调接口
    private OnItemMoveListener itemMoveListener;
    private final Handler mHandler;

    /**
     * 长按的Runnable
     */
    private final Runnable mLongClickRunnable = new Runnable() {
        @Override
        public void run() {

            isDrag = true;
            mVibrator.vibrate(200);
            //隐藏该item
            mDragView.setVisibility(INVISIBLE);
            //在点击的地方创建并显示item镜像
            createDragView(mDragBitmap, mDownX, mDownY);
        }
    };

    public DragRecyclerView(Context context) {
        this(context, null);
    }

    public DragRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mHandler = new Handler();
        mStatusHeight = getStatusHeight(context);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = (int) ev.getX();
                mDownY = (int) ev.getY();

                mDragView=findChildViewUnder(mDownX,mDownY);
                if (mDragView == null) {
                    return super.dispatchTouchEvent(ev);
                }
                //获取按下的position
                mDragPosition =getChildAdapterPosition(mDragView);
                if (mDragPosition == NO_POSITION) {     //无效就返回
                    return super.dispatchTouchEvent(ev);
                }
                //延时长按执行mLongClickRunable
                mHandler.postDelayed(mLongClickRunnable, mDragResponseMs);
                //获取按下的item对应的View 由于存在复用机制,所以需要 处理FirstVisiblePosition
                //计算按下的点到所在item的left top 距离
                mPoint2ItemLeft = mDownX - mDragView.getLeft();
                mPoint2ItemTop = mDownY - mDragView.getTop();
                //计算RecyclerView的left top 偏移量:原始距离 - 相对距离就是偏移量
                mOffset2Left = (int) ev.getRawX() - mDownX;
                mOffset2Top = (int) ev.getRawY() - mDownY;
                //开启视图缓存
                mDragView.setDrawingCacheEnabled(true);
                //获取缓存的中的bitmap镜像 包含了item中的ImageView和TextView
                mDragBitmap = Bitmap.createBitmap(mDragView.getDrawingCache());
                //释放视图缓存 避免出现重复的镜像
                mDragView.destroyDrawingCache();

                break;
            case MotionEvent.ACTION_MOVE:

                mMoveX = (int) ev.getX();
                mMoveY = (int) ev.getY();
                //如果只在按下的item上移动,未超过边界,就不移除mLongClickRunable
                if (!isTouchInItem(mDragView, mMoveX, mMoveY)) {
                    mHandler.removeCallbacks(mLongClickRunnable);
                }
                break;
            case MotionEvent.ACTION_UP:
                mHandler.removeCallbacks(mLongClickRunnable);
                //判断是否是点击
                if (!isDrag&&mDragMirrorView==null&&itemMoveListener!=null&&Math.abs(mMoveX-mDownX)<20&&Math.abs(mMoveY-mDownY)<20){
                    itemMoveListener.onItemClick(mDragPosition);
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }


    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (isDrag && mDragMirrorView != null) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    mMoveX = (int) ev.getX();
                    mMoveY = (int) ev.getY();
                    onDragItem(mMoveX, mMoveY);
                    break;
                case MotionEvent.ACTION_UP:
                    onStopDrag();
                    isDrag = false;
                    break;
                default:
                    break;
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }


    /************************对外提供的接口***************************************/

    public boolean isDrag() {
        return isDrag;
    }

    public void setDrag(boolean drag) {
        isDrag = drag;
    }

    public long getDragResponseMs() {
        return mDragResponseMs;
    }

    public void setDragResponseMs(long mDragResponseMs) {
        this.mDragResponseMs = mDragResponseMs;
    }

    public void setOnItemMoveListener(OnItemMoveListener itemMoveListener) {
        this.itemMoveListener = itemMoveListener;
    }
    /******************************************************************************/


    /**
     * 点是否在该View上面
     *
     * @param view
     * @param x
     * @param y
     * @return
     */
    private boolean isTouchInItem(View view, int x, int y) {
        if (view == null) {
            return false;
        }
        if (view.getLeft() < x && x < view.getRight()
                && view.getTop() < y && y < view.getBottom()) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * 获取状态栏的高度
     *
     * @param context
     * @return
     */
    @SuppressLint("PrivateApi")
    private static int getStatusHeight(Context context) {
        int statusHeight;
        Rect localRect = new Rect();
        ((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect);
        statusHeight = localRect.top;
        if (0 == statusHeight) {
            Class<?> localClass;
            try {
                localClass = Class.forName("com.android.internal.R$dimen");
                Object localObject = localClass.newInstance();
                int height = Integer.parseInt(Objects.requireNonNull(localClass.getField("status_bar_height").get(localObject)).toString());
                statusHeight = context.getResources().getDimensionPixelSize(height);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return statusHeight;
    }

    /**
     * 停止拖动
     */
    private void onStopDrag() {
        if (mDragView != null) {
            mDragView.setVisibility(VISIBLE);
        }
        removeDragImage();
        if (itemMoveListener!=null){
            itemMoveListener.onUp(mDragPosition);
        }
    }

    /**
     * WindowManager 移除镜像
     */
    private void removeDragImage() {
        if (mDragMirrorView != null) {
            mWindowManager.removeView(mDragMirrorView);
            mDragMirrorView = null;
        }
    }

    /**
     * 拖动item到指定位置
     *
     * @param x
     * @param y
     */
    private void onDragItem(int x, int y) {
        mLayoutParams.x = x - mPoint2ItemLeft + mOffset2Left;
        mLayoutParams.y = y - mPoint2ItemTop + mOffset2Top - mStatusHeight;
        //更新镜像位置
        mWindowManager.updateViewLayout(mDragMirrorView, mLayoutParams);
        int[] location = new int[2];
        getLocationOnScreen(location);
        int pY = location[1];
        if (itemMoveListener!=null){
            itemMoveListener.onMove(x,y+pY);
        }
    }

    /**
     * 创建拖动的镜像
     *
     * @param bitmap
     * @param downX
     * @param downY
     */
    @SuppressLint("RtlHardcoded")
    private void createDragView(Bitmap bitmap, int downX, int downY) {
        mLayoutParams = new WindowManager.LayoutParams();
        mLayoutParams.format = PixelFormat.TRANSLUCENT; //图片之外其他地方透明
        mLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; //左 上
        //指定位置 其实就是 该 item 对应的 rawX rawY 因为Window 添加View是需要知道 raw x ,y的
        mLayoutParams.x = mOffset2Left + (downX - mPoint2ItemLeft);
        mLayoutParams.y = mOffset2Top + (downY - mPoint2ItemTop) + mStatusHeight;
        //指定布局大小
        mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        //透明度
        mLayoutParams.alpha = 0.5f;
        //指定标志 不能获取焦点和触摸,允许拖动到窗口外
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;

        mDragMirrorView = new ImageView(getContext());
        mDragMirrorView.setImageBitmap(bitmap);
        //添加View到窗口中
        mWindowManager.addView(mDragMirrorView, mLayoutParams);
    }

    /**
     * item 交换时的回调接口
     */
    public interface OnItemMoveListener {
        void onMove(int x, int y);
        void onUp(int position);
        void onItemClick(int position);
    }
}

使用

<com.pgc.dragrecyclerviewdemo.DragRecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        app:layout_constraintBottom_toTopOf="@id/view_5"
        />
recyclerView.setOnItemMoveListener(new DragRecyclerView.OnItemMoveListener() {
            @Override
            public void onMove(int x, int y) {
                if (x > viewX1 && x < viewX1 + viewWidth1 && y > viewY1 && y < viewY1 + viewHeight1) {
                    moveType = 1;
                } else if (x > viewX2 && x < viewX2 + viewWidth2 && y > viewY2 && y < viewY2 + viewHeight2) {
                    moveType = 2;
                } else if (x > viewX3 && x < viewX3 + viewWidth3 && y > viewY3 && y < viewY3 + viewHeight3) {
                    moveType = 3;
                } else if (x > viewX4 && x < viewX4 + viewWidth4 && y > viewY4 && y < viewY4 + viewHeight4) {
                    moveType = 4;
                } else if (x > viewX5 && x < viewX5 + viewWidth5 && y > viewY5 && y < viewY5 + viewHeight5) {
                    moveType = 5;
                } else {
                    moveType = -1;
                }
                if (moveType != -1)
                    Toast.makeText(getApplicationContext(), "移动到VIEW" + moveType, Toast.LENGTH_LONG).show();
            }

            @Override
            public void onUp(int position) {
                Log.d("哦豁",moveType+"----"+position);
            }

            @Override
            public void onItemClick(int position) {
                Log.d("进入","点击了="+position);
            }
        });

下载链接

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
您好,针对您的问题,我理解您想实现在 Android 应用中,用户可以通过按某个元素并拖动,实现多张图片的拖拽和拖放。我可以为您提供以下思路和代码示例: 首先,您需要为每一张图片设置一个触摸事件,以便用户可以通过按并拖动图片。例如,在您的布局文件中,可以为每个图片添加如下代码: ```xml <ImageView android:id="@+id/image1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/image1" android:tag="draggable" android:longClickable="true" android:onLongClick="onLongClickImage" /> ``` 其中,`android:tag="draggable"` 表示该元素可以被拖拽,`android:longClickable="true"` 表示该元素可以被按,`android:onLongClick="onLongClickImage"` 表示当用户按该元素时,会调用 `onLongClickImage` 方法。 接下来,您需要实现 `onLongClickImage` 方法,以便能够在用户按图片时启动拖拽事件。例如,可以使用 `View.OnLongClickListener` 接口来实现该方法: ```java public class MainActivity extends AppCompatActivity implements View.OnLongClickListener { private ImageView mSelectedImage; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 为每个图片添加按事件 findViewById(R.id.image1).setOnLongClickListener(this); findViewById(R.id.image2).setOnLongClickListener(this); findViewById(R.id.image3).setOnLongClickListener(this); } @Override public boolean onLongClick(View v) { // 记录选中的图片 mSelectedImage = (ImageView) v; // 启动拖拽事件 View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v); v.startDragAndDrop(null, shadowBuilder, null, 0); return true; } } ``` 在这个例子中,当用户按某个图片时,会记录选中的图片并启动拖拽事件。拖拽事件使用 `View.startDragAndDrop` 方法来启动,并使用 `View.DragShadowBuilder` 创建一个拖拽的阴影。 最后,您需要实现 `View.OnDragListener` 接口来处理拖拽事件。例如,可以使用以下代码来实现: ```java public class MainActivity extends AppCompatActivity implements View.OnLongClickListener, View.OnDragListener { private ImageView mSelectedImage; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 为每个图片添加按事件 findViewById(R.id.image1).setOnLongClickListener(this); findViewById(R.id.image2).setOnLongClickListener(this); findViewById(R.id.image3).setOnLongClickListener(this); // 设置拖拽事件 findViewById(R.id.container).setOnDragListener(this); } @Override public boolean onLongClick(View v) { // 记录选中的图片 mSelectedImage = (ImageView) v; // 启动拖拽事件 View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v); v.startDragAndDrop(null, shadowBuilder, null, 0); return true; } @Override public boolean onDrag(View v, DragEvent event) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: // 拖拽事件开始,返回 true 表示可以接收拖拽事件 return true; case DragEvent.ACTION_DRAG_ENTERED: // 拖拽事件进入容器,改变容器的背景色 v.setBackgroundColor(Color.YELLOW); break; case DragEvent.ACTION_DRAG_EXITED: // 拖拽事件离开容器,恢复容器的背景色 v.setBackgroundColor(Color.TRANSPARENT); break; case DragEvent.ACTION_DROP: // 拖拽事件释放,将图片从原位置移动到容器中 ViewGroup viewGroup = (ViewGroup) mSelectedImage.getParent(); viewGroup.removeView(mSelectedImage); ((ViewGroup) v).addView(mSelectedImage); break; case DragEvent.ACTION_DRAG_ENDED: // 拖拽事件结束,恢复容器的背景色 v.setBackgroundColor(Color.TRANSPARENT); break; } return true; } } ``` 在这个例子中,当用户将图片拖拽到容器中时,会将图片从原位置移动到容器中。您可以根据需要修改代码,实现更复杂的拖拽和拖放功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值