android 自定义View开发实战(六) 可拖动的GridView
1前言
由于项目需求,需要把项目的主界面采用GridView显示,并且需要根据模块优先级支持拖动图标(砍死产品狗)。为此,自定义了一个支持拖拽图标的GridView。效果如下: 
具体效果如上图
2 可拖拽的GridView实现
要实现上面的效果有两个难点,第一就是如何创造一个可拖动的View在我们的Activity界面上。第二个就是如何实现两个View的交换
关于第一个:我们可以用WindowManager 来往我们的界面上添加View,这样我们再重写GridView的onTouchEvent()方法,根据移动的距离来更新的我们View的位置即可 
关于第二个:可以这样实现,当我们拖动时,创建一个透明度低一点的镜像item
View。把要拖动的item对应的View先隐藏起来,此时Adapter的item先不交换,当我们把拖动的item移动到另一个item对应的范围内,我们再进行交换,先把这个item隐藏掉,然后在原来的位置显示出这个item。最后镜像item对应的view
 再消失。
其实关于第二点,也有其他的交换策略,比如判断拖到镜像view到另一个item之上一段时间再进行交换等。
1 实现思路
好了,下面我们仔细总结了一下思路,有了思路我们就很好办了:
1.根据手指按下的X,Y坐标来获取我们在GridView上面点击的item,再获取对应的View
2.手指按下的时候使用Handler和Runnable来实现一个定时器,假如定时时间为1000毫秒,在1000毫秒内,如果手指抬起了就移除定时器,没有抬起并且手指点击在GridView的item所在的区域,则表示我们长按了GridView的item
3. 如果我们长按了item则隐藏item,然后使用WindowManager来添加一个item的镜像在屏幕用来代替刚刚隐藏的item
4.当我们手指在屏幕移动的时候,更新item镜像的位置,然后在根据我们移动的X,Y的坐标来获取移动到GridView的哪一个位置
5. 到GridView的item过多的时候,可能一屏幕显示不完,我们手指拖动item镜像到屏幕下方,要触发GridView想上滚动,同理,当我们手指拖动item镜像到屏幕上面,触发GridView向下滚动
6.GridView交换数据,刷新界面,移除item的镜像
2 实现代码
这里定义一个XGridView继承GridView来实现 
代码如下,加了详细的注释,应该容易看懂
package com.qiyei.javatest;
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.GridView;
import android.widget.ImageView;
/**
 * Created by qiyei2015 on 2017/1/5.
 */
public class XGridView extends GridView {
    //拖拽响应的时间 默认为1s
    private long mDragResponseMs = 1000;
    //是否支持拖拽,默认不支持
    private boolean isDrag = false;
    //振动器,用于提示替换
    private Vibrator mVibrator;
    //拖拽的item的position
    private int mDragPosition;
    //拖拽的item对应的View
    private View mDragView;
    //窗口管理器,用于为Activity上添加拖拽的View
    private 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 int mStatusHeight;
    //XGridView向下滚动的边界值
    private int mDownScrollBorder;
    //XGridView向上滚动的边界值
    private int mUpScrollBorder;
    //滚动的速度
    private int mSpeed = 20;
    //item发生变化的回调接口
    private OnItemChangeListener changeListener;
    private Handler mHandler;
    /**
     * 长按的Runnable
     */
    private Runnable mLongClickRunable = new Runnable() {
        @Override
        public void run() {
            isDrag = true;
            mVibrator.vibrate(200);
            //隐藏该item
            mDragView.setVisibility(INVISIBLE);
            //在点击的地方创建并显示item镜像
            createDragView(mDragBitmap,mDownX,mDownY);
        }
    };
    /**
     * 当moveY的值大于向上滚动的边界值,触发GridView自动向上滚动
     * 当moveY的值小于向下滚动的边界值,触犯GridView自动向下滚动
     * 否则不进行滚动
     */
    private Runnable mScrollRunbale = new Runnable() {
        @Override
        public void run() {
            int scrollY = 0;
            if (mMoveY > mUpScrollBorder){
                scrollY = mSpeed;
                mHandler.postDelayed(mScrollRunbale,25);
            }else if (mMoveY < mDownScrollBorder){
                scrollY = -mSpeed;
                mHandler.postDelayed(mScrollRunbale,25);
            }else {
                scrollY = 0;
                mHandler.removeCallbacks(mScrollRunbale);
            }
            smoothScrollBy(scrollY,10);
        }
    };
    public XGridView(Context context) {
        this(context,null);
    }
    public XGridView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public XGridView(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();
                //获取按下的position
                mDragPosition = pointToPosition(mDownX,mDownY);
                if (mDragPosition == INVALID_POSITION){     //无效就返回
                    return super.dispatchTouchEvent(ev);
                }
                //延时长按执行mLongClickRunable
                mHandler.postDelayed(mLongClickRunable,mDragResponseMs);
                //获取按下的item对应的View 由于存在复用机制,所以需要 处理FirstVisiblePosition
                mDragView = getChildAt(mDragPosition - getFirstVisiblePosition());
                if (mDragView == null){
                    return super.dispatchTouchEvent(ev);
                }
                //计算按下的点到所在item的left top 距离
                mPoint2ItemLeft = mDownX - mDragView.getLeft();
                mPoint2ItemTop = mDownY - mDragView.getTop();
                //计算GridView的left top 偏移量:原始距离 - 相对距离就是偏移量
                mOffset2Left = (int)ev.getRawX() - mDownX;
                mOffset2Top = (int)ev.getRawY() - mDownY;
                //获取GridView自动向下滚动的偏移量,小于这个值,DragGridView向下滚动
                mDownScrollBorder = getHeight() /4;
                //获取GridView自动向上滚动的偏移量,大于这个值,DragGridView向上滚动
                mUpScrollBorder = getHeight() * 3/4;
                //开启视图缓存
                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(mLongClickRunable);
                }
                break;
            case MotionEvent.ACTION_UP:
                mHandler.removeCallbacks(mLongClickRunable);
                mHandler.removeCallbacks(mScrollRunbale);
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (isDrag && mDragMirrorView != null){
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    break;
                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 setOnItemChangeListener(OnItemChangeListener changeListener) {
        this.changeListener = changeListener;
    }
    /******************************************************************************/
    /**
     * 点是否在该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
     */
    private static int getStatusHeight(Context context){
        int statusHeight = 0;
        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(localClass.getField("status_bar_height").get(localObject).toString());
                statusHeight = context.getResources().getDimensionPixelSize(height);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return statusHeight;
    }
    /**
     * 停止拖动
     */
    private void onStopDrag() {
        View view = getChildAt(mDragPosition - getFirstVisiblePosition());
        if (view != null){
            view.setVisibility(VISIBLE);
        }
        removeDragImage();
    }
    /**
     * 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);
        onSwapItem(x,y);
        mHandler.post(mScrollRunbale);
    }
    /**
     * 交换 item 并且控制 item之间的显示与隐藏
     * @param x
     * @param y
     */
    private void onSwapItem(int x, int y) {
        //获取我们手指移动到那个item
        int tmpPosition = pointToPosition(x,y);
        if (tmpPosition != INVALID_POSITION && tmpPosition != mDragPosition){
            if (changeListener != null){
                changeListener.onChange(mDragPosition,tmpPosition);
            }
            //隐藏tmpPosition
            getChildAt(tmpPosition - getFirstVisiblePosition()).setVisibility(INVISIBLE);
            //显示之前的item
            getChildAt(mDragPosition - getFirstVisiblePosition()).setVisibility(VISIBLE);
            mDragPosition = tmpPosition;
        }
    }
    /**
     * 创建拖动的镜像
     * @param bitmap
     * @param downX
     * @param downY
     */
    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.4f;
        //指定标志 不能获取焦点和触摸
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        mDragMirrorView = new ImageView(getContext());
        mDragMirrorView.setImageBitmap(bitmap);
        //添加View到窗口中
        mWindowManager.addView(mDragMirrorView,mLayoutParams);
    }
    /**
     * item 交换时的回调接口
     */
    public interface OnItemChangeListener{
        void onChange(int from,int to);
    }android 自定义View开发实战(六) 可拖动的GridView的更多相关文章
- Android自定义View实战(SlideTab-可滑动的选择器)
		转载请标明出处: http://blog.csdn.net/xmxkf/article/details/52178553 本文出自:[openXu的博客] 目录: 初步分析重写onDraw绘制 重写o ... 
- 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象
		前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ... 
- android 自定义view 前的基础知识
		本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了 ... 
- 【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介
		前言 在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指.但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了.多点触控是在Android2 ... 
- Android 自定义View合集
		自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ... 
- Android 自定义View (五)——实践
		前言: 前面已经介绍了<Android 自定义 view(四)-- onMeasure 方法理解>,那么这次我们就来小实践下吧 任务: 公司现有两个任务需要我完成 (1)监测液化天然气液压 ... 
- Android 自定义 view(四)—— onMeasure 方法理解
		前言: 前面我们已经学过<Android 自定义 view(三)-- onDraw 方法理解>,那么接下我们还需要继续去理解自定义view里面的onMeasure 方法 推荐文章: htt ... 
- Android 自定义View及其在布局文件中的使用示例(二)
		转载请注明出处 http://www.cnblogs.com/crashmaker/p/3530213.html From crash_coder linguowu linguowu0622@gami ... 
- Android自定义View
		转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View ... 
随机推荐
- cherrypy & gevent patch
			给cherrypy 打gevent WSGIServer的patch 1. patch Serving 类 2. 关闭python的原生WSGIServer 具体使用例子参考 我的开源项目 http ... 
- winform中ComboBox控件的简单使用
			在开发winform中用到了ComboBox,但是发现和asp.net中的DropDownList差别比我想象中的大. 给ComboBox添加数据总结的有两种方法(绑定数据库在这里不说): 第一种方法 ... 
- python mock模块使用(二)
			本篇继续介绍mock里面另一种实现方式,patch装饰器的使用,patch() 作为函数装饰器,为您创建模拟并将其传递到装饰函数 官方文档地址 patch简介 1.unittest.mock.patc ... 
- Personal Recommendation Using Deep Recurrent Neural Networks in NetEase读书笔记
			一.文章综述 1.研究目的:实现网易考拉电商平台的商品高效实时个性化推荐.缩短用户与目标商品的距离,让用户点击最少的页面就可以得到想要的商品 2.研究背景:基于用户和基于物品的协同过滤(Collabo ... 
- 使用jemalloc优化nginx和mysql内存管理
			预先安装autoconf 和 make yum -y install autoconf make jemalloc的安装jiemalloc 开源项目网站 http://www.canonware.co ... 
- [luoguP3258] [JLOI2014]松鼠的新家(lca + 树上差分)
			传送门 需要把一条路径上除了终点外的所有数都 + 1, 比如,给路径 s - t 上的权值 + 1,可以先求 x = lca(s,t) 类似数列上差分的思路,可以给 s 和 f[t] 的权值 + 1, ... 
- hiho一下 第四十五周  博弈游戏·Nim游戏·二 [ 博弈 ]
			传送门 题目1 : 博弈游戏·Nim游戏·二 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 Alice和Bob这一次准备玩一个关于硬币的游戏:N枚硬币排成一列,有的正面 ... 
- eclipse 修改Java代码 不用重新启动tomcat
			例子: 1.在tomcat server.xml文件配置加上这句话: <Context debug="0" docBase="C:\Users\admin\Desk ... 
- ztr loves lucky numbers--hdu5676(DFS)
			http://acm.hdu.edu.cn/showproblem.php?pid=5676 题目大意: 给你一个数 让你找比这数大并且只含4和7 并且4和7的个数一样 枚举从0到10的18次方之 ... 
- Multiply Strings(字符串乘法模拟,包含了加法模拟)
			Given two numbers represented as strings, return multiplication of the numbers as a string. Note: Th ... 
