Android RecyclerView 拖曳item复制到其他界面上

简述

在Android 开发中,可能有些小伙伴会遇到类似这种功能:
1.在画板上添加一些贴图,从图片列表中拖曳到画板上
2.将某个新闻条目拖动到收藏夹包里保存
对此写了关于RecyclerView 简单拖曳复制View的文章 ,给小伙伴们提供一下灵感和思路,也算是抛砖引玉,有更好的文章或者想法也希望能在评论区留言一下,共同进步。
老规矩,先上图:


录制效果图.gif
解决思路

首先我们把gif图所展示整体功能拆分成几个步骤
1.item长按点击时生成一个新的View
2.View 的滑动处理事件效果编辑
3.解决RecyclerView 与生成的View 在触摸事件上的冲突
这边会通过代码和文字把以上这几个问题如何一一解决呈现给小伙伴们,先提出总提方向是让大家有个大致思路,再往下就比较好理解。

页面布局(只截图不给代码,相信小伙伴们能看懂)

适配器布局:


item_pic.png

主界面布局:


activity_main.png
代码部分

PicAdapter的实现非常简单,也不是本次讨论重点,这边主要就是就把长按点击通过接口回调处理
代码如下:

public class PicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context context;
    private List<DataEntity> dataEntityList;
    private OnItemLongClickListener onItemLongClickListener;


    public PicAdapter(Context context, List<DataEntity> dataEntityList, OnItemLongClickListener onItemLongClickListener) {
        this.context=context;
        this.dataEntityList = dataEntityList;
        this.onItemLongClickListener=onItemLongClickListener;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view= LayoutInflater.from(context).inflate(R.layout.item_pic,null);
        ViewHolder viewHolder=new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        ((ViewHolder) holder).lyItem.setBackgroundColor(Color.parseColor(dataEntityList.get(position).getColor()));
        ((ViewHolder) holder).tvView.setText(dataEntityList.get(position).getName());
        ((ViewHolder) holder).lyItem.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if(onItemLongClickListener!=null)onItemLongClickListener.onItemClickEvent(v,position);
                return true;
            }
        });
    }

    @Override
    public int getItemCount() {
        return dataEntityList==null? 0: dataEntityList.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder{
         LinearLayout lyItem;
         TextView tvView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            lyItem=itemView.findViewById(R.id.lyItem);
            tvView=itemView.findViewById(R.id.tvView);
        }
    }

    public interface OnItemLongClickListener {
        void onItemClickEvent(View view, int selectPosition);
    }
}

DataEntity 是一个拥有名字和颜色属性的简单类,用在主界面添加循环生成随机颜色的集合对象,代码如下:

public class DataEntity {
    private String name;
    private String color;

    public DataEntity(String name, String color) {
        this.name = name;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

MainActivity的所有代码(有的小伙伴看不懂就可以往下看我简单的解释)

public class MainActivity extends AppCompatActivity {
    private Button btnTool;
    private RecyclerView ryTool;
    private LinearLayout lyTool,deleteView;
    private RelativeLayout rlView;
    private boolean showToolView ,itemPress;
    private PicAdapter picAdapter;
    private List<DataEntity>dataEntityList=new ArrayList<>();
    private List<View> copyView=new ArrayList<>();
    private View.OnTouchListener onTouchListener;
    private int startY;
    private int startX;
    private boolean ryCanScroll=true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initAdapter();
        initListener();
    }

    private void initListener() {
        btnTool.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showRy(showToolView=!showToolView);
            }
        });
        onTouchListener=new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                moveViewEvent( v,  event);
                return true;
            }
        };

        ryTool.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
                //当生成复制View 的时候,禁止RecyclerView 滑动
                return !ryCanScroll;
            }

            @Override
            public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
                //触发长按之后,item的触摸事件来到这里了.MotionEvent返回手指移动的位置.以及up事件
                if(copyView.size()>0 && itemPress){
                    ryMoveEvent(copyView.get(copyView.size()-1),e);
                }
            }

            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

            }
        });
    }

    private void ryMoveEvent(View v, MotionEvent event) {
        if(v==null)return;
        v.setScaleX(1.3f);
        v.setScaleY(1.3f);
        int[] location = new int[2];
        v.getLocationOnScreen(location);
        //手指按下后View 产生位移偏差,好看得出复制了
        startX = location[0]+dp2px(this,30);
        startY = location[1]+dp2px(this,60);;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;
            case MotionEvent.ACTION_MOVE:

                //获取移动后的坐标
                int moveX = (int) event.getRawX();
                int moveY = (int) event.getRawY();
                //拿到手指移动距离的大小
                int move_bigX = moveX - startX;
                int move_bigY = moveY - startY;

                //拿到当前控件未移动的坐标:只需要计算该控件离左边和上边的距离即可
                int left = v.getLeft();
                int top = v.getTop();
                left += move_bigX;
                top += move_bigY;
                RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
                params.setMargins(left, top, 0, 0);
                v.setLayoutParams(params);
                startX = moveX;
                startY = moveY;
                break;
            case MotionEvent.ACTION_CANCEL://手指抬起来的同时判断是否红色范围内,是则回收该View
            case MotionEvent.ACTION_UP:
                v.setScaleX(1.0f);
                v.setScaleY(1.0f);
                itemPress=false;
                ryCanScroll=true;
                if(isInChangeImageZone(deleteView,(int)event.getRawX(),(int)event.getRawY())){
                    copyView.remove(v);
                    rlView.removeView(v);
                }
                break;
        }
    }

    private void moveViewEvent(View v, MotionEvent event) {
        if(v==null)return;
        v.setScaleX(1.3f);
        v.setScaleY(1.3f);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //获取当前按下的坐标
                startX = (int) event.getRawX();
                startY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                //获取移动后的坐标
                int moveX = (int) event.getRawX();
                int moveY = (int) event.getRawY();
                //拿到手指移动距离的大小
                int move_bigX = moveX - startX;
                int move_bigY = moveY - startY;
                //拿到当前控件未移动的坐标:只需要计算该控件离左边和上边的距离即可
                int left = v.getLeft();
                int top = v.getTop();
                left += move_bigX;
                top += move_bigY;
                RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
                params.setMargins(left, top, 0, 0);
                v.setLayoutParams(params);
                startX = moveX;
                startY = moveY;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                v.setScaleX(1.0f);
                v.setScaleY(1.0f);
                if(isInChangeImageZone(deleteView,(int)event.getRawX(),(int)event.getRawY())){
                    copyView.remove(v);
                    rlView.removeView(v);
                }
                break;
        }
    }

    private void initAdapter() {
        for (int i=0;i<18;i++){
            DataEntity dataEntity=new DataEntity("图片"+i,getRandColor());
            dataEntityList.add(dataEntity);
        }
        picAdapter=new PicAdapter(this, dataEntityList, new PicAdapter.OnItemLongClickListener() {
            @Override
            public void onItemClickEvent(View view, int selectPosition) {
                //长按
                copyItem(view ,selectPosition);
                itemPress=true;
                ryCanScroll=false;

            }
        });

        ryTool.setLayoutManager(new GridLayoutManager(this,3));
        ryTool.setAdapter(picAdapter);

    }

    private void copyItem(View view,int selectPosition) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        LinearLayout linearLayout=new LinearLayout(this);
        linearLayout.setGravity(Gravity.CENTER);
        LinearLayout.LayoutParams layoutParams=new LinearLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
        linearLayout.setLayoutParams(layoutParams);
        layoutParams.setMargins(location[0],location[1],0,0);
        linearLayout.setOnTouchListener(onTouchListener);
        linearLayout.setBackgroundColor(Color.parseColor(dataEntityList.get(selectPosition).getColor()));

        TextView textView=new TextView(this);
        textView.setText("复制:"+dataEntityList.get(selectPosition).getName());
        ViewGroup.LayoutParams params=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,dp2px(this,80));
        textView.setLayoutParams(params);
        textView.setTextColor(Color.WHITE);
        textView.setGravity(Gravity.CENTER);
        linearLayout.addView(textView);

        copyView.add(linearLayout);
        rlView.addView(linearLayout);

    }


    private void showRy(boolean b) {
        lyTool.setVisibility(b? View.VISIBLE:View.GONE);
    }

    private void initView() {
        btnTool=findViewById(R.id.btnTool);
        deleteView=findViewById(R.id.deleteView);
        ryTool =findViewById(R.id.ry);
        lyTool=findViewById(R.id.lyTool);
        rlView=findViewById(R.id.rlView);
    }



    /**
     * 获取十六进制的颜色代码.例如  "#5A6677"
     * 分别取R、G、B的随机值,然后加起来即可
     * 通过Color.parseColor()转为color值即可使用
     * @return String
     */
    public static String getRandColor() {
        String R, G, B;
        Random random = new Random();
        R = Integer.toHexString(random.nextInt(256)).toUpperCase();
        G = Integer.toHexString(random.nextInt(256)).toUpperCase();
        B = Integer.toHexString(random.nextInt(256)).toUpperCase();
        R = R.length() == 1 ? "0" + R : R;
        G = G.length() == 1 ? "0" + G : G;
        B = B.length() == 1 ? "0" + B : B;
        return "#" + R + G + B;
    }


    /**
     * dp 转 px
     */
    public static int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 用于判断某个坐标是否在View 范围内
     *
     * */
    private Rect mChangeImageBackgroundRect = null;
    private boolean isInChangeImageZone(View view, int x, int y) {
        if (null == mChangeImageBackgroundRect) {
            mChangeImageBackgroundRect = new Rect();
        }
        view.getDrawingRect(mChangeImageBackgroundRect);
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        mChangeImageBackgroundRect.left = location[0];
        mChangeImageBackgroundRect.top = location[1];
        mChangeImageBackgroundRect.right = mChangeImageBackgroundRect.right + location[0];
        mChangeImageBackgroundRect.bottom = mChangeImageBackgroundRect.bottom + location[1];
        return mChangeImageBackgroundRect.contains(x, y);
    }
}

item长按点击时生成一个新的View (看此函数 copyItem(View view,int selectPosition) )
,是通过根布局rlView添加到界面中去,同时为了方便管理移除,这边会把添加copyView 集合list中去,实际上这样写很繁琐,可以通过继承线性布局,将其封装成一个类这样比较直观。

View 的滑动处理事件效果编辑则是通过onTouchListener,在里面实现缩放,移动,与手指抬起判断。
注意:这里边view的移动不可通过view.layout(l,t,r,b) 来实现,因为当父组件rlView.addView(View v)时,都会将重置子View位置

        onTouchListener=new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                moveViewEvent( v,  event);
                return true;
            }
        };


    private void moveViewEvent(View v, MotionEvent event) {
        if(v==null)return;
        v.setScaleX(1.3f);
        v.setScaleY(1.3f);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //获取当前按下的坐标
                startX = (int) event.getRawX();
                startY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                //获取移动后的坐标
                int moveX = (int) event.getRawX();
                int moveY = (int) event.getRawY();
                //拿到手指移动距离的大小
                int move_bigX = moveX - startX;
                int move_bigY = moveY - startY;
                //拿到当前控件未移动的坐标:只需要计算该控件离左边和上边的距离即可
                int left = v.getLeft();
                int top = v.getTop();
                left += move_bigX;
                top += move_bigY;
                RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
                params.setMargins(left, top, 0, 0);
                v.setLayoutParams(params);
                startX = moveX;
                startY = moveY;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                v.setScaleX(1.0f);
                v.setScaleY(1.0f);
                if(isInChangeImageZone(deleteView,(int)event.getRawX(),(int)event.getRawY())){
                    copyView.remove(v);
                    rlView.removeView(v);
                }
                break;
        }
    }

关于RecyclerView 与生成的View 在触摸事件上的冲突,这边着重要解决的问题就是长按时(没有松开),实际上事件分发到我们的ryTool,但是滑动的时候却是移动产生在ryTool 上面的view, 这里我们要做的两点就是
1.长按时,将禁止 RecyclerView 的滚动
这边通过设置 ryCanScroll ,在长按时的回调设置为false


image.png

2.将RecyclerView上产生的任何触摸事件传给最新生成的View 移动,且当手指抬起时,则允许RecyclerView重新滚动。


image.png

至此,整篇文章讲解完毕,这次写的也算比较粗糙,讲解的也是一些很表面上的东西,深层次的事件分发也没有展开讲,只算给小伙伴们提供一些思路,若是哪里有误,欢迎留言提出,共同进步。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,277评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,777评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,946评论 0 245
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,271评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,636评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,767评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,989评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,733评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,457评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,674评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,155评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,518评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,160评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,114评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,898评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,822评论 2 280
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,705评论 2 273

推荐阅读更多精彩内容