可拖拽的GridView实现

本文详细介绍了一种在Android项目中实现Gridview长按拖拽换位的方法,包括自定义MyGridView、承载Activity、数据bean类、适配器以及拖拽逻辑处理等内容,解决了不改变拖拽轨迹item顺序及多个bug修复的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

之前项目中有需求,需要对Gridview长按拖拽换位,总体实现难度不大,主要问题是不改变拖拽轨迹item顺序以及好多bug的修复过程,因此在此记录

效果图

大纲

1.自定义MyGridView

1.1成员变量介绍

1.2点击事件处理

1.3拖拽逻辑处理

2.承载Activity:MyGridActivity

3.数据bean类:MyGridBean

4.适配器:MyGridAdapter

5.Bug总结以及改进

6.代码地址

正文

1.1成员变量介绍

    //坐标
    private int mDownX, mDownY; 
    private int mPointTop, mPointLeft;
    private int mOffsetX, mOffsetY;
    private int mUpBorder, mDownBorder;
    private int mTouchX, mTouchY;
    private int mStatusHeight;
    //当前view
    private int mSelectIndex;
    private int mCurrentPosition;
    private int mDragStartPosition;
    private View mSelctView;
    //事件
    private Runnable mLongRunnable;
    private Runnable mScrollRunnable;
    private Handler mHandler;
    private boolean isDrag;
    private onChangeListener mOnChangeListener;
    //镜像
    private WindowManager mWindowManager;
    private WindowManager.LayoutParams mWindowLayoutParams;
    private ImageView mDragImageView;
    private Bitmap mDragView;
    //其他常量
    private static final long LONG_TOUCH_TIME = 800;
    private static final int SCROLL_SPEED = 20;

坐标:

int mDownX, mDownY;

获取的是dispatchTouchEvent中ev.getX/Y,手指点下的相对坐标,具体区别参考:http://blog.youkuaiyun.com/dmk877/article/details/51550031

int mPointTop, mPointLeft;

获取的是手指点下的点和当前view边界距离,为之后计算镜像位置

int mOffsetX, mOffsetY;

获取MyGridview距离屏幕距离,也为之后计算镜像位置

int mUpBorder, mDownBorder;

计算MyGridview开始滚动的区域,因为拖拽过程中Gridview滑动事件失效,需要自定义实现,因此需要获取边界值

int mTouchX, mTouchY;

获取onTouchEvent中获取的ev.getX/Y,为获取手指移动到的itemView

int mStatusHeight

计算当前设备状态栏高度,为之后计算镜像位置

 

当前view

int mSelectIndex

手指点下的item position,后面是交换item时的start position

int mCurrentPosition

手指移动到的item position,同时也是交换item时的end position

int mDragStartPosition

长按事件点下的那个item position,为了实现一次拖拽只交换抬起和最后放下的item,需要保存这个item position

View mSelctView

长按事件点下的那个item view,获取bitmap构建镜像

 

事件

Runnable mLongRunnable;

长按事件,创建镜像等逻辑

Runnable mScrollRunnable;

滚动事件,开始拖动后需要自定义实现滚动

Handler mHandler;

Handler

boolean isDrag;

滚动开始标记

onChangeListener mOnChangeListener;

对外提供交换接口,用于数据源内信息交换

 

镜像

WindowManager mWindowManager;
WindowManager.LayoutParams mWindowLayoutParams;

ImageView mDragImageView;

Bitmap mDragView;

实现镜像创建

 

其他常量

long LONG_TOUCH_TIME = 800

长按事件触发延迟,单位毫秒

int SCROLL_SPEED = 20;

滑动速度

    private void init() {
        mHandler = new Handler();

        mLongRunnable = new Runnable() {
            @Override
            public void run() {
                isDrag = true;
                mSelctView.setVisibility(View.INVISIBLE);
                creatDragImage(mDragView, mDownX, mDownY);
            }
        };

        mScrollRunnable = new Runnable() {
            @Override
            public void run() {
                mHandler.removeCallbacks(mLongRunnable);
                int scrollY;
                if (mTouchY > mUpBorder) {
                    scrollY = SCROLL_SPEED;
                    mHandler.postDelayed(mScrollRunnable, 25);
                } else if (mTouchY < mDownBorder) {
                    scrollY = -SCROLL_SPEED;
                    mHandler.postDelayed(mScrollRunnable, 25);
                } else {
                    scrollY = 0;
                    mHandler.removeCallbacks(mScrollRunnable);
                }

                onSwapItem(mTouchX, mTouchY);

                smoothScrollBy(scrollY, 10);
            }
        };

        mStatusHeight = getStatusHeight(getContext());

        mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);

        //为区别position = 0情况,设置缺省值为-1
        mCurrentPosition = -1;
    }

1.2点击事件处理

主要流程:

dispatchTouchEvent

1.down获取按下点坐标,计算上文的几个坐标,以及获取需要当前view bitmap,同时绑定长按事件

2.move获取移动坐标,判定是否手指移出view区域或者开始滑动,若是则取消长按事件

3.up和cancel取消长按事件和滚动事件

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

                mSelectIndex = pointToPosition(mDownX, mDownY);
                if (mSelectIndex != AdapterView.INVALID_POSITION) {
                    mHandler.postDelayed(mLongRunnable, LONG_TOUCH_TIME);
                    mSelctView = getChildAt(mSelectIndex - getFirstVisiblePosition());
                    //记录一次拖拽流程中真正交换的起始item position
                    mDragStartPosition = mSelectIndex;

                    mPointTop = mDownY - mSelctView.getTop();
                    mPointLeft = mDownX - mSelctView.getLeft();

                    mOffsetX = (int) (ev.getRawX() - mDownX);
                    mOffsetY = (int) (ev.getRawY() - mDownY);

                    mUpBorder = getHeight() * 3 / 4;
                    mDownBorder = getHeight() / 4;

                    mSelctView.setDrawingCacheEnabled(true);
                    mDragView = Bitmap.createBitmap(mSelctView.getDrawingCache());
                    mSelctView.destroyDrawingCache();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int) ev.getX();
                int moveY = (int) ev.getY();

                //若手指移除当前view区域或gridview滚动则取消长按事件
                if (!isTouchInItem(mSelctView, moveX, moveY) || Math.abs(moveY - mDownY) > 100) {
                    mHandler.removeCallbacks(mLongRunnable);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mHandler.removeCallbacks(mLongRunnable);
                mHandler.removeCallbacks(mScrollRunnable);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

onTouchEvent

1.move根据移动坐标获取当前移动到的item,进行交换逻辑

2.up停止拖拽

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (isDrag && mDragImageView != null) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    mTouchX = (int) ev.getX();
                    mTouchY = (int) ev.getY();
                    //拖动item
                    onDragItem(mTouchX, mTouchY);
                    break;
                case MotionEvent.ACTION_UP:
                    onStopDrag();
                    isDrag = false;
                    mCurrentPosition = AdapterView.INVALID_POSITION;
                    break;
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }

1.3拖拽逻辑处理

onDragItem

跟随手指移动更新镜像位置

    /**
     * 拖拽更新
     *
     * @param moveX
     * @param moveY
     */
    private void onDragItem(int moveX, int moveY) {
        mWindowLayoutParams.x = moveX - mPointLeft + mOffsetX;
        mWindowLayoutParams.y = moveY - mPointTop + mOffsetY - mStatusHeight;
        mWindowManager.updateViewLayout(mDragImageView, mWindowLayoutParams); //更新镜像的位置
        onSwapItem(moveX, moveY);

        //拖拽时到边界自动滚动
        mHandler.post(mScrollRunnable);
    }

onSwapItem

调用对外接口实现数据源数据更新

    /**
     * item换位
     *
     * @param moveX
     * @param moveY
     */
    private void onSwapItem(int moveX, int moveY) {
        mCurrentPosition = pointToPosition(moveX, moveY);

        if (mCurrentPosition != mSelectIndex && mCurrentPosition != AdapterView.INVALID_POSITION) {
            if (mOnChangeListener != null) {
                mOnChangeListener.onChange(mSelectIndex, mCurrentPosition, mDragStartPosition);
            }

            mSelectIndex = mCurrentPosition;
        }
    }

onStopDrag

清除镜像

    /**
     * 停止拖拽
     */
    private void onStopDrag() {
        View view = getChildAt(mSelectIndex - getFirstVisiblePosition());
        if (view != null) {
            view.setVisibility(View.VISIBLE);
        }

        if (mDragImageView != null) {
            mWindowManager.removeView(mDragImageView);
            mDragImageView = null;
            
        }
    }

2.承载Activity:MyGridActivity

因为是demo没有特别复杂逻辑,主要生成假数据,然后实现MyGridview提供的交换接口,内嵌的adapter单独拿出后面讲

public class MyGridActivity extends Activity {


    private MyGridView mMyGridView;
    private MyGridAdapter mAdapter;
    private List<MyGridBean> mBeans;

    private static final int MAX_ITEM = 100;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_grid);
        mMyGridView = (MyGridView) findViewById(R.id.mgv);
        mMyGridView.setSelector(new ColorDrawable(Color.TRANSPARENT));
        mBeans = new ArrayList<>();

        //假数据填充
        for (int i = 0; i < MAX_ITEM; i++) {
            MyGridBean bean = new MyGridBean();
            if (i % 4 == 0) {
                bean.setDrableRes(R.mipmap.icon_one);
            } else if (i % 4 == 1) {
                bean.setDrableRes(R.mipmap.icon_two);
            } else if (i % 4 == 2) {
                bean.setDrableRes(R.mipmap.icon_three);
            } else if (i % 4 == 3) {
                bean.setDrableRes(R.mipmap.icon_four);
            }
            bean.setTitle("按钮" + i);
            mBeans.add(bean);
        }

        mAdapter = new MyGridAdapter(this);
        mMyGridView.setAdapter(mAdapter);
        mMyGridView.setOnChangeListenner(new MyGridView.onChangeListener() {
            @Override
            public void onChange(int start, int end, int drag) {
                mAdapter.changeItem(start, end, drag);
            }
        });
    }
}

3.数据bean类:MyGridBean

测试demo仅需要两个属性即可,图片资源和title,实际可以再拓展一个属性,三种type不同的gridview:空白view、添加view、显示的view;具体根据业务实现吧,在此简单略过了

public class MyGridBean {
    private int drableRes;
    private String title;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getDrableRes() {
        return drableRes;
    }

    public void setDrableRes(int drableRes) {
        this.drableRes = drableRes;
    }
}

4.适配器:MyGridAdapter

因为就适配个图片和文字,getview就不详细描述,主要讲下changeItem,最初开始是交换完数据源内容直接notifyDataSetChanged,后面发现输入item过多会有少许卡顿 ,所以优化了下,然后出了个问题,就是如果滑屏换位的话,有可能getChildAt会取到null,因为换位item已经不可见了,所以这时候就不要对不可见view操作,数据源替换之后,再次可见这个view时还会调用getview

class MyGridAdapter extends BaseAdapter {

        Context context;

        public MyGridAdapter(Context context) {
            this.context = context;
        }

        @Override
        public int getCount() {
            return mBeans.size();
        }

        @Override
        public Object getItem(int position) {
            return mBeans.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            convertView = View.inflate(context, R.layout.layout_my_grid, null);
            viewHolder = new ViewHolder();
            viewHolder.iv = (ImageView) convertView.findViewById(R.id.iv_layout_mg);
            viewHolder.tv = (TextView) convertView.findViewById(R.id.tv_layout_mg);
            MyGridBean bean = mBeans.get(position);
            viewHolder.iv.setImageResource(bean.getDrableRes());
            viewHolder.tv.setText(bean.getTitle());
            convertView.setTag(viewHolder);
            return convertView;
        }

        /**
         * 刷新交换item数据,代替notifyDataSetChanged
         * @param start
         * @param end
         * @param drag
         */
        public void changeItem(int start, int end, int drag) {
            int fristVisible = mMyGridView.getFirstVisiblePosition();
            ViewHolder viewHolderStart = null;
            ViewHolder viewHolderEnd = null;


            MyGridBean tempBean = mBeans.get(start);
            mBeans.set(start, mBeans.get(end));
            mBeans.set(end, tempBean);


            MyGridBean beanStart = mBeans.get(start);
            MyGridBean beanEnd = mBeans.get(end);
            View viewStart = mMyGridView.getChildAt(start - fristVisible);
            View viewEnd = mMyGridView.getChildAt(end - fristVisible);
            /**
             * 因为拖拽过程可能伴随着滑屏,导致需要换位的item不可见,则getChildAt获取为null,因此以下有关viewStart、viewEnd处理均需加入非null判断
             * 但只要在数据源mBeans中交换了,再次显示会重新调用getView,显示还是交换后的数据
             */

            if (viewStart != null) {
                viewHolderStart = (ViewHolder) viewStart.getTag();
                viewHolderStart.iv.setImageResource(beanStart.getDrableRes());
                viewHolderStart.tv.setText(beanStart.getTitle());
            }

            if (viewEnd != null) {
                viewHolderEnd = (ViewHolder) viewEnd.getTag();
                viewHolderEnd.iv.setImageResource(beanEnd.getDrableRes());
                viewHolderEnd.tv.setText(beanEnd.getTitle());
            }


            if (mMyGridView.getCurrentPosition() == start) {
                viewStart.setVisibility(View.INVISIBLE);
                if (viewEnd != null) {
                    viewEnd.setVisibility(View.VISIBLE);
                }
            } else if (mMyGridView.getCurrentPosition() == end) {
                if (viewStart != null) {
                    viewStart.setVisibility(View.VISIBLE);
                }
                viewEnd.setVisibility(View.INVISIBLE);
            }

            //二次交换
            if (start == drag || end == drag)
                return;
            tempBean = mBeans.get(start);
            mBeans.set(start, mBeans.get(drag));
            mBeans.set(drag, tempBean);

            beanStart = mBeans.get(start);
            beanEnd = mBeans.get(drag);
            viewStart = mMyGridView.getChildAt(start - fristVisible);
            viewEnd = mMyGridView.getChildAt(drag - fristVisible);

            if (viewStart != null) {
                viewHolderStart = (ViewHolder) viewStart.getTag();
                viewHolderStart.iv.setImageResource(beanStart.getDrableRes());
                viewHolderStart.tv.setText(beanStart.getTitle());
            }

            if (viewEnd != null) {
                viewHolderEnd = (ViewHolder) viewEnd.getTag();
                viewHolderEnd.iv.setImageResource(beanEnd.getDrableRes());
                viewHolderEnd.tv.setText(beanEnd.getTitle());
            }

            if (mMyGridView.getCurrentPosition() == start) {
                if (viewStart != null) {
                    viewStart.setVisibility(View.INVISIBLE);
                }
                if (viewEnd != null) {
                    viewEnd.setVisibility(View.VISIBLE);
                }
            } else if (mMyGridView.getCurrentPosition() == drag) {
                if (viewStart != null) {
                    viewStart.setVisibility(View.VISIBLE);
                }
                if (viewEnd != null) {
                    viewEnd.setVisibility(View.INVISIBLE);
                }
            }
        }

        class ViewHolder {
            ImageView iv;
            TextView tv;
        }
    }

5.Bug总结以及改进

可能还存在其他未发现bug,若有网友朋友发现希望不吝赐教,在此简单罗列目前发现的几个Bug:

1.滑屏过程触发长按事件:手指在任意item滑屏,延时时间过后会触发拖拽事件;在dispatchTouchEvent中move对Y偏移量进行判断,若超过一定值即认定为滑动,随后取消长按事件

2.换位可见与不可见问题:拖拽时应当移动时隐藏手指所在item,本来是使用getChildAt获取换位的两个view写在onDragItem中,但是因为个什么原因出现了异常,因此这部分逻辑换到changeItem中

 

改进:

1.在geiview中没有复用convertView,因为换位没有notifyDataSetChanged,因此效果还是可以的,但是如果增加几种不同type的item,就需要考虑这个问题;

2.换位动画还没有增加,可以让换位更圆润丝滑些

3.当前activity退出后再次进入换位效果不能保存,因此需要数据持久化处理

4.再有什么建议希望大家踊跃提供

6.代码地址

demo:GitHub链接

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值