可拖拽排序的GridView(高仿今日头条编辑频道效果)

来源:互联网 发布:tower软件 编辑:程序博客网 时间:2024/06/06 11:37

最近在一直在用今日头条,发现在我的频道编辑时的拖拽排序体验非常有意思,这种拖拽功能其实在支付宝等app上也频繁使用,于是打算自己研究一下,网上虽然有很多类似于此类功能的博客,但实现的都不是特别完美,效果总有瑕疵,今天我分享一个完美体验版,大家用了就知道!

老规矩,先上效果:
这里写图片描述

准备工作

要想实现这个效果,首先你要了解这几个方面的知识,有欠缺的同学赶紧先补一下:
* 使用WindowManager添加悬浮窗口
* onTouchEvent触摸事件的处理
* 简单的TranslateAnimation平移动画
* GridView api的熟练使用

原理分析

首先它是一个gridview,里面放了很多item,至于item你可以自己随意布局,例子中我只用了一个textview。

1、触发gridview长按事件,用windowmanager添加一个悬浮view,并占位隐藏原来的item,这个悬浮窗就是我们长按的item的一个图片副本,并且将悬浮view定位到自己的手指触摸点,将这个悬浮view设置放大倍数及透明度;
2、监听手指的移动,实时改变悬浮view的位置;
3、当移动距离超过自己的position时,用TranslateAnimation动画平移从起始位置到目标位置之间的item;
4、当手机抬起时,改变adapter中数据的位置,刷新gridview,并释放悬浮view;

下面我们一步一步的来撸代码

//首先展示一下需要的成员变量    private WindowManager.LayoutParams mWindowParams;    private WindowManager mWindowManager;    //被拖拽的item图片副本    private ImageView mDragImageView;    //按下时手指的坐标    private float downX, downY;    //是否正在拖拽    private boolean isDraging = false;    //是否正在进行移动item动画,防止高频率触发动画而发生抖动    private boolean isMoving = false;    //被拖拽和未被拖拽的标记    private static final int NOT_DRAG_ITEM = 0x0;    private static final int SHOW_DRAG_ITEM = 0x1;    //拖动时副本放大倍数    private static final float DRAG_SCALE = 1.2F;    //记录拖拽的item位置    private int mDragItemPosition;    //记录最后一个item动画toString格式    private String lastAnim;   

在构造方法中初始化必要的一些变量:

 public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView() {        //设置item长按事件        setOnItemLongClickListener(this);        mWindowParams = new WindowManager.LayoutParams();        mDragImageView = new ImageView(getContext());         //标记未被拖拽        mDragImageView.setTag(NOT_DRAG_ITEM);        //获取窗口管理对象,用于后面向窗口中添加dragImageView        mWindowManager = (WindowManager)   getContext().getSystemService(Context.WINDOW_SERVICE);    }

在长按事件中创建副本,隐藏拖拽item

  @Override    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {        mDragItemPosition = position;        //清空item之前的图片缓存        view.destroyDrawingCache();        //开启图片缓存        view.setDrawingCacheEnabled(true);        //创建一个item的图片副本        Bitmap dragBitmap = Bitmap.createBitmap(view.getDrawingCache());        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;        //定义副本的长和宽        mWindowParams.width = (int) (DRAG_SCALE * dragBitmap.getWidth());        mWindowParams.height = (int) (DRAG_SCALE * dragBitmap.getHeight());        //定义副本的位置        mWindowParams.x = (int) (downX - dragBitmap.getWidth() / 2);        mWindowParams.y = (int) (downY - dragBitmap.getHeight() / 2);        //定义副本的附加参数:不能点击、不能获取焦点、悬浮窗的形式        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;        //定义副本支持透明格式        mWindowParams.format = PixelFormat.TRANSLUCENT;        mWindowParams.windowAnimations = 0;        //如果之前有这个副本先移除        if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) {            mWindowManager.removeView(mDragImageView);            mDragImageView.setTag(NOT_DRAG_ITEM);        }        //将图片副本放入imageview        mDragImageView.setImageBitmap(dragBitmap);        //设置已有副本标记        mDragImageView.setTag(SHOW_DRAG_ITEM);        //设置imageView透明度        mDragImageView.setAlpha(0.7f);        //添加这个imageview到悬浮窗        mWindowManager.addView(mDragImageView, mWindowParams);        //此时状态变为可拖拽        isDraging = true;        //通知adapter隐藏拖拽的item        ((DragAdapter) getAdapter()).hideItem(position);        //将拖拽item的图片缓存功能关闭,释放内存        view.setDrawingCacheEnabled(false);        return true;    }

监听手指的移动来移动item副本,并执行动画:

@Override    public boolean onTouchEvent(MotionEvent ev) {        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                //记录按下x、y轴位置                downX = ev.getRawX();                downY = ev.getRawY();                break;            case MotionEvent.ACTION_MOVE:                if (isDraging) {                    //移动时实时刷新副本的坐标位置                    mWindowParams.x = (int) (ev.getRawX() - mDragImageView.getWidth() / 2);                    mWindowParams.y = (int) (ev.getRawY() - mDragImageView.getHeight() / 2);                    //改变副本的位置                    mWindowManager.updateViewLayout(mDragImageView, mWindowParams);                    //移动中间的item                    moveItem((int) ev.getX(), (int) ev.getY());                }                break;                }

使用平移动画移动中间item的位置

 /**     * 移动item的位置     */    private void moveItem(int x, int y) {        //得到移动到的点x、y在gridview中的所在位置        final int currentPosition = pointToPosition(x, y);        //如果移动位置移除自己所在的位置,则支持位置移动        if (currentPosition != mDragItemPosition && currentPosition > -1) {            //如果已经在执行移动动画,则不再执行,防止高频率触发动画而发生抖动            if (isMoving) return;            //向后拖拽,则中间的item向前移动            if (currentPosition > mDragItemPosition) {                for (int i = mDragItemPosition+1; i <= currentPosition; i++) {                    View before = getChildAt(i);                    int toPosition = i;                    int toX = -before.getWidth();                    int toY = 0;                    if(toPosition % getNumColumns() == 0){                        toX = before.getWidth() * (getNumColumns()-1);                        toY = -before.getHeight();                    }                    startAnim(before, toX , toY , i ,currentPosition);                }            } else {     //向前拖拽,则中间的item向后移动                for (int i = mDragItemPosition - 1 ; i >= currentPosition ; i--) {                    View before = getChildAt(i);                    int toPosition = i+1;                    int toX = before.getWidth();                    int toY = 0;                    if(toPosition % getNumColumns() == 0){                        toX = - before.getWidth() * (getNumColumns()-1);                        toY = before.getHeight();                    }                    startAnim(before, toX , toY , i ,currentPosition);                }            }        }    }    /**     * 启动平移动画     * @param view              将要移动的view     * @param toX               x轴上移动值     * @param toY               y轴上的移动值     * @param positon           当前item的位置     * @param currentPosition   拖拽中的view的最终位置     */    private void startAnim(View view, float toX , float toY , int positon , final int currentPosition) {        Animation anim = getMoveAnim( toX , toY );        if (positon == currentPosition) {            lastAnim = anim.toString();        }        view.startAnimation(anim);        anim.setAnimationListener(new Animation.AnimationListener() {            @Override            public void onAnimationStart(Animation animation) {                //动画开始将移动状态设为true                isMoving = true;            }            @Override            public void onAnimationEnd(Animation animation) {                //最后一个item动画结束,将移动状态设为false                if (!StringUtils.isEmpty(lastAnim) && lastAnim.equals(animation.toString())) {                    //通知adapter刷新数据的位置,刷新界面                    ((DragAdapter) getAdapter()).changeItemPosition(currentPosition, mDragItemPosition);                    mDragItemPosition = currentPosition;                    isMoving = false;                }            }            @Override            public void onAnimationRepeat(Animation animation) {            }        });    }    /**     * 生成item移动动画     */    private TranslateAnimation getMoveAnim(float toX, float toY) {        TranslateAnimation anim = new TranslateAnimation(0.0F, toX , 0.0F , toY);        anim.setDuration(300);        anim.setFillAfter(true);        return anim;    }

手指抬起时,将所有状态还原,释放窗口悬浮view:

 case MotionEvent.ACTION_UP:                if (isDraging) {                    ((DragAdapter) getAdapter()).cancelDrag();                    if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) {                        mWindowManager.removeView(mDragImageView);                        mDragImageView.setTag(NOT_DRAG_ITEM);                    }                    isDraging = false;                    isMoving = false;                }                break;

在来看看拖拽GridView专用的adapter基类,主要是提供了一些工具方法:

/**     * 拖拽adapter基类,如要使用拖拽gridview必须继承     * 继承时不能复用convertView,否则会出现一些奇怪现象     */    public static abstract class DragAdapter<T> extends BaseAdapter {        protected List<T> datas = new ArrayList<T>();        private int hidePosition = AdapterView.INVALID_POSITION;        /**         * 是否隐藏标记         */        public static final int ITEM_TYPE_NORMAL = 0x0;        public static final int ITEM_TYPE_HIDE = 0x1;        public void setDatas(List<T> list) {            this.datas.clear();            this.datas.addAll(list);            notifyDataSetChanged();        }        @Override        public int getCount() {            return datas.size();        }        @Override        public T getItem(int position) {            return datas.get(position);        }        @Override        public long getItemId(int position) {            return 0;        }        @Override        public View getView(int position, View convertView, ViewGroup parent) {            return getItemView(position, convertView, parent);        }        protected abstract View getItemView(int position, View convertView, ViewGroup parent);        /**         * 返回要隐藏的item位置,在继承这个adapter基类时只要通过这个方法判断type = ITEM_TYPE_NORMAL时将该item隐藏即可         */        @Override        public int getItemViewType(int position) {            if (hidePosition == position) {                return ITEM_TYPE_HIDE;            }            return ITEM_TYPE_NORMAL;        }        /**         * 取消拖拽         */        public void cancelDrag() {            hidePosition = AdapterView.INVALID_POSITION;            notifyDataSetChanged();        }        /**         * 改变拖拽item位置         */        public void changeItemPosition(int currentPosition, int dragPosition) {            //从后往前移            if (currentPosition < dragPosition) {                datas.add(currentPosition, getItem(dragPosition));                datas.remove(dragPosition + 1);            } else {    //从前往后移                datas.add(currentPosition + 1, getItem(dragPosition));                datas.remove(dragPosition);            }            hidePosition = currentPosition;            notifyDataSetChanged();        }        /**         * 隐藏item         */        public void hideItem(int positon) {            hidePosition = positon;            notifyDataSetChanged();        }    }

好了,最后贴上GragGridView源码:

/** * Created by caoyujie on 17/1/13. * 可拖拽的gridView */public class DragGridView<T extends DragGridView.DragAdapter> extends GridView implements AdapterView.OnItemLongClickListener {    private WindowManager.LayoutParams mWindowParams;    private WindowManager mWindowManager;    private float downX, downY;    /**     * 被拖拽的item图片副本     */    private ImageView mDragImageView;    /**     * 是否正在拖拽     */    private boolean isDraging = false;    /**     * 是否正在进行移动item动画,防止高频率触发动画而发生抖动     */    private boolean isMoving = false;    /**     * 被拖拽和未被拖拽的标记     */    private static final int NOT_DRAG_ITEM = 0x0;    private static final int SHOW_DRAG_ITEM = 0x1;    /**     * 拖动时放大倍数     */    private static final float DRAG_SCALE = 1.2F;    /**     * 记录拖拽的item位置     */    private int mDragItemPosition;    /**     * 记录最后一个item动画toString格式     */    private String lastAnim;    /**     * 行间距     */    private int verticalSpacing;    public DragGridView(Context context) {        this(context, null);    }    public DragGridView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView() {        setOnItemLongClickListener(this);        mWindowParams = new WindowManager.LayoutParams();        mDragImageView = new ImageView(getContext());        //标记未被拖拽        mDragImageView.setTag(NOT_DRAG_ITEM);        //获取窗口管理对象,用于后面向窗口中添加dragImageView        mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);        verticalSpacing = getVerticalSpacing();    }    @Override    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {        mDragItemPosition = position;        //清空item之前的图片缓存        view.destroyDrawingCache();        //开启图片缓存        view.setDrawingCacheEnabled(true);        //创建一个item的图片副本        Bitmap dragBitmap = Bitmap.createBitmap(view.getDrawingCache());        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;        //定义副本的长和宽        mWindowParams.width = (int) (DRAG_SCALE * dragBitmap.getWidth());        mWindowParams.height = (int) (DRAG_SCALE * dragBitmap.getHeight());        //定义副本的位置        mWindowParams.x = (int) (downX - dragBitmap.getWidth() / 2);        mWindowParams.y = (int) (downY - dragBitmap.getHeight() / 2);        //定义副本的附加参数:不能点击、不能获取焦点、悬浮窗的形式        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;        //定义副本支持透明格式        mWindowParams.format = PixelFormat.TRANSLUCENT;        mWindowParams.windowAnimations = 0;        //如果之前有这个副本先移除        if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) {            mWindowManager.removeView(mDragImageView);            mDragImageView.setTag(NOT_DRAG_ITEM);        }        //将图片副本放入imageview        mDragImageView.setImageBitmap(dragBitmap);        //设置已有副本标记        mDragImageView.setTag(SHOW_DRAG_ITEM);        //设置imageView透明度        mDragImageView.setAlpha(0.7f);        //添加这个imageview到悬浮窗        mWindowManager.addView(mDragImageView, mWindowParams);        //此时状态变为可拖拽        isDraging = true;        //通知adapter隐藏拖拽的item        ((DragAdapter) getAdapter()).hideItem(position);        //将拖拽item的图片缓存功能关闭,释放内存        view.setDrawingCacheEnabled(false);        return true;    }    @Override    public boolean onTouchEvent(MotionEvent ev) {        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                //记录按下x、y轴位置                downX = ev.getRawX();                downY = ev.getRawY();                break;            case MotionEvent.ACTION_MOVE:                if (isDraging) {                    //移动时实时刷新副本的坐标位置                    mWindowParams.x = (int) (ev.getRawX() - mDragImageView.getWidth() / 2);                    mWindowParams.y = (int) (ev.getRawY() - mDragImageView.getHeight() / 2);                    //改变副本的位置                    mWindowManager.updateViewLayout(mDragImageView, mWindowParams);                    //移动中间的item                    moveItem((int) ev.getX(), (int) ev.getY());                }                break;            case MotionEvent.ACTION_UP:                if (isDraging) {                    ((DragAdapter) getAdapter()).cancelDrag();                    if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) {                        mWindowManager.removeView(mDragImageView);                        mDragImageView.setTag(NOT_DRAG_ITEM);                    }                    isDraging = false;                    isMoving = false;                }                break;        }        return super.onTouchEvent(ev);    }    /**     * 移动item的位置     */    private void moveItem(int x, int y) {        //得到移动到的点x、y在gridview中的所在位置        final int currentPosition = pointToPosition(x, y);        //如果移动位置移除自己所在的位置,则支持位置移动        if (currentPosition != mDragItemPosition && currentPosition > -1) {            //如果已经在执行移动动画,则不再执行,防止高频率触发动画而发生抖动            if (isMoving) return;            //向后拖拽,则中间的item向前移动            if (currentPosition > mDragItemPosition) {                for (int i = mDragItemPosition+1; i <= currentPosition; i++) {                    View before = getChildAt(i);                    int toPosition = i;                    int toX = -before.getWidth();                    int toY = 0;                    if(toPosition % getNumColumns() == 0){                        toX = before.getWidth() * (getNumColumns()-1);                        toY = -before.getHeight() - verticalSpacing;                    }                    startAnim(before, toX , toY , i ,currentPosition);                }            } else {     //向前拖拽,则中间的item向后移动                for (int i = mDragItemPosition - 1 ; i >= currentPosition ; i--) {                    View before = getChildAt(i);                    int toPosition = i+1;                    int toX = before.getWidth();                    int toY = 0;                    if(toPosition % getNumColumns() == 0){                        toX = - before.getWidth() * (getNumColumns()-1);                        toY = before.getHeight() + verticalSpacing;                    }                    startAnim(before, toX , toY , i ,currentPosition);                }            }        }    }    /**     * 启动平移动画     * @param view              将要移动的view     * @param toX               x轴上移动值     * @param toY               y轴上的移动值     * @param positon           当前item的位置     * @param currentPosition   拖拽中的view的最终位置     */    private void startAnim(View view, float toX , float toY , int positon , final int currentPosition) {        Animation anim = getMoveAnim( toX , toY );        if (positon == currentPosition) {            lastAnim = anim.toString();        }        view.startAnimation(anim);        anim.setAnimationListener(new Animation.AnimationListener() {            @Override            public void onAnimationStart(Animation animation) {                //动画开始将移动状态设为true                isMoving = true;            }            @Override            public void onAnimationEnd(Animation animation) {                //最后一个item动画结束,将移动状态设为false                if (!StringUtils.isEmpty(lastAnim) && lastAnim.equals(animation.toString())) {                    //通知adapter刷新数据的位置,刷新界面                    ((DragAdapter) getAdapter()).changeItemPosition(currentPosition, mDragItemPosition);                    mDragItemPosition = currentPosition;                    isMoving = false;                }            }            @Override            public void onAnimationRepeat(Animation animation) {            }        });    }    /**     * 生成item移动动画     */    private TranslateAnimation getMoveAnim(float toX, float toY) {        TranslateAnimation anim = new TranslateAnimation(0.0F, toX , 0.0F , toY);        anim.setDuration(300);        anim.setFillAfter(true);        return anim;    }    /**     * 拖拽adapter基类,如要使用拖拽gridview必须继承     * 继承时不能复用convertView,否则会出现一些奇怪现象     */    public static abstract class DragAdapter<T> extends BaseAdapter {        protected List<T> datas = new ArrayList<T>();        private int hidePosition = AdapterView.INVALID_POSITION;        /**         * 是否隐藏标记         */        public static final int ITEM_TYPE_NORMAL = 0x0;        public static final int ITEM_TYPE_HIDE = 0x1;        public void setDatas(List<T> list) {            this.datas.clear();            this.datas.addAll(list);            notifyDataSetChanged();        }        @Override        public int getCount() {            return datas.size();        }        @Override        public T getItem(int position) {            return datas.get(position);        }        @Override        public long getItemId(int position) {            return 0;        }        @Override        public View getView(int position, View convertView, ViewGroup parent) {            return getItemView(position, convertView, parent);        }        protected abstract View getItemView(int position, View convertView, ViewGroup parent);        /**         * 返回要隐藏的item位置,在继承这个adapter基类时只要通过这个方法判断type = ITEM_TYPE_NORMAL时将该item隐藏即可         */        @Override        public int getItemViewType(int position) {            if (hidePosition == position) {                return ITEM_TYPE_HIDE;            }            return ITEM_TYPE_NORMAL;        }        /**         * 取消拖拽         */        public void cancelDrag() {            hidePosition = AdapterView.INVALID_POSITION;            notifyDataSetChanged();        }        /**         * 改变拖拽item位置         */        public void changeItemPosition(int currentPosition, int dragPosition) {            //从后往前移            if (currentPosition < dragPosition) {                datas.add(currentPosition, getItem(dragPosition));                datas.remove(dragPosition + 1);            } else {    //从前往后移                datas.add(currentPosition + 1, getItem(dragPosition));                datas.remove(dragPosition);            }            hidePosition = currentPosition;            notifyDataSetChanged();        }        /**         * 隐藏item         */        public void hideItem(int positon) {            hidePosition = positon;            notifyDataSetChanged();        }    }}/** * Created by caoyujie on 17/1/13. * 拖拽adapter的实现类 * 我们实际使用的adapter */public class MenuDragAdapter extends DragGridView.DragAdapter {    private LayoutInflater mLayoutInflater;    public MenuDragAdapter(Context context) {        mLayoutInflater = LayoutInflater.from(context);    }    @Override    protected View getItemView(int position, View convertView, ViewGroup parent) {        convertView = mLayoutInflater.inflate(R.layout.list_menu_drag, parent, false);        TextView label = (TextView) convertView.findViewById(R.id.tv_label);        label.setText((String) datas.get(position));        return convertView;    }}

通过这个view我们可以学到很多的知识,值得我们去探索,想看源码的可以参考我的开源项目:https://github.com/18973809797/BaseStorehouse。
全局搜索 DragGridView即可,效果在主页点击标题进入:
里面还有很多实用的app框架实现及实用view的学习哦~
这里写图片描述

0 0
原创粉丝点击