一个流畅的拖动排序DragSortGridView,自动滚屏

先上效果

一个流畅的拖动排序DragSortGridView,自动滚屏_第1张图片

流畅效果超越了网易新闻和UC浏览器的栏目收藏.gif图和实际效果有差距

1.拖拽可以移动item,并且其他item会立即自动补位,快速拖拽也非常流畅
2.item太多时,拖拽到边缘时会自动滚屏.
3.可以自定义被拖拽的的View放大,添加阴影等效果
4.长按启动删除模式,需要自己实现item的删除按钮展示,自己管理删除模式的切换
5.可以放在ScrollView中拖动排序,需要ScrollView继承ListenScrollView,不影响外面控件的大部分事件.
6.可以长按启动item拖拽,也可以触摸直接开始拖动

简单使用方法

        dragSortGridView = (DragSortGridView) findViewById(R.id.dragSort1);
        //长按item响应该item的拖动排序,默认是触摸就开始拖动
        dragSortGridView.setDragModel(DragSortGridView.DRAG_BY_LONG_CLICK);
        dragAdapter = new MyAdapter();
        dragSortGridView.setAdapter(dragAdapter);

dragAdapter 需要多实现onDataModelMove一个方法,界面排序改变需要提供真实数据排序改变.示例

class MyAdapter extends DragAdapter {
        @Override
        public void onDataModelMove(int from, int to) {
            String s = list.remove(from);
            list.add(to, s);
        }

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

        @Override
        public String getItem(int position) {
            return list.get(position);
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            TextView textView;
            if (convertView == null) {
                FrameLayout frameLayout = new FrameLayout(SecondActivity.this);
                convertView = frameLayout;
                textView = new TextView(SecondActivity.this);
                frameLayout.setPadding(20, 20, 20, 20);
                textView.setPadding(20, 100, 20, 100);
                frameLayout.addView(textView);
                textView.setBackgroundColor(0x33ff00ff);
                textView.setGravity(Gravity.CENTER);
            } else {
                textView = (TextView) ((FrameLayout) convertView).getChildAt(0);
            }
            textView.setText(getItem(position));
            return convertView;
        }
    }

扩展可以改变的功能

            //设置每行个数
        dragSortGridView.setNumColumns(4);

            /*改变拖动item所在动画层,例如frameLayout是位于最上册的全屏透明层,
            则item拖拽可以在全屏范围内,超出dragSortGridView本身范围,
            这个一般用来配合外层是ListenScrollView用*/
        dragSortGridView.setAnimFrame(frameLayout);

            //设置前面多少个位置固定,不能拖动
        dragSortGridView.setNoPositionChangeItemCount(2);
            //设置尾部多少个位置固定,不能拖动
        dragSortGridView.setFootNoPositionChangeItemCount(1);
            //修改item响应拖动时的效果,默认是放大到120%
        dragSortGridView.setOnDragSelectListener(new DragSortGridView.OnDragSelectListener() {
            @Override
            public void onDragSelect(View mirror) {
                //当item开始拖动时调用该方法
            }

            @Override
            public void onPutDown(View itemView) {
                //当item被放时是调用该方法
            }
        });
        //修改长按拖动的响应时间
        dragSortGridView.setDragLongPressTime(1500);

        dragSortGridView.setOnItemClickListener(...);
        dragSortGridView.setOnLongClickListener(...);

注意

  1. 不能给DragSortGridView设置padding,但是可以用margin,不能使用横竖间隙,要item之间的间距只能在adapter生成item里面设置padding
  2. 必须做contentView的复用,不做则不流畅.

下面是源码
有一个R.id.first,需要在values的ids.xml里面添加

"first" type="id" />


/**
 * Copyright (C), 2008-2015, Huawei Tech. Co., Ltd.
 * 

* Description : 拖动排序布局 * * @version V100R001 * @since V100R001 */ @SuppressLint({ "NewApi", "Override" }) public class DragSortGridView extends FrameLayout { protected NoScrollGridView mGridView; private ScrollView mScrollView; private int headDragPosition = 0; private int footDragPosition = 0; private FrameLayout mDragFrame; private View mCopyView, hideView; private GestureDetector detector; /** 动画时间 */ private static final long ANIM_DURING = 250; protected int mNumColumns = 3, mColHeight = 0, mColWidth = 0, mChildCount = 0, mMaxHeight = 0; private int currentDragPosition = -1; private DragAdapter adapter; /** 持有子view */ private List mChilds = new ArrayList(); private static final int TAG_KEY = R.id.first; // private static final int TAG_KEY = R.id.tag_key; private int mCurrentY = 0; /** * 触摸区域,0不滚动区域,1可向上滚动的区域,-1可向下滚动的区域 */ private int mTouchArea = 0; /** * gridview能否滚动,是否内容太多 */ private boolean canScroll = true; /** * 是否可以拖动,点击拖动策略下直接开启,长按拖动需要长按以后开启 */ private boolean isDragable = true; /** * 自动滚屏的动画 */ private ValueAnimator animator; /** * view是否加载完成,如果未加载完成,没有宽高,无法接受事件 */ private boolean isViewInitDone = false; /** 是否有位置发生改变,否则不用重绘 */ private boolean hasPositionChange = false; /** 适配器的观察者,观察适配器的数据改变 */ private DataSetObserver observer = new DataSetObserver() { @Override public void onChanged() { mChildCount = adapter.getCount(); // 下列属性状态清除,才会在被调用notifyDataSetChange时,在gridview测量布局完成后重新获取 mChilds.clear(); mColHeight = mColWidth = mMaxHeight = 0; isViewInitDone = false; } @Override public void onInvalidated() { mChildCount = adapter.getCount(); } }; private float[] lastLocation = null; /** * 手势监听器,滚动和单击 */ private SimpleOnGestureListener simpleOnGestureListener = new SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (hasSendDragMsg) { hasSendDragMsg = false; handler.removeMessages(0x123); } if (isDragable && mCopyView != null) {// 可以拖动,实现跟随手指的拖动效果 // /// 2015/11/27补充修正跟随手指移动方法,适用于当本控件在drag时同时滚动的情况 if (lastLocation == null) { lastLocation = new float[] { e1.getRawX(), e1.getRawY() }; } distanceX = lastLocation[0] - e2.getRawX(); distanceY = lastLocation[1] - e2.getRawY(); lastLocation[0] = e2.getRawX(); lastLocation[1] = e2.getRawY(); // //////// mCopyView.setX(mCopyView.getX() - distanceX); mCopyView.setY(mCopyView.getY() - distanceY); mCopyView.invalidate(); int to = eventToPosition(e2); if (to != currentDragPosition && to >= headDragPosition && to < mChildCount - footDragPosition) { onDragPositionChange(currentDragPosition, to); } } return true; } @Override public void onShowPress(MotionEvent e) { /** 响应长按拖拽 */ if (mDragMode == DRAG_BY_LONG_CLICK) { // 启动拖拽模式 // isDragable = true; // 通知父控件不拦截我的事件 getParent().requestDisallowInterceptTouchEvent(true); // 根据点击的位置生成该位置上的view镜像 int position = eventToPosition(e); if (position >= headDragPosition && position < mChildCount - footDragPosition) { // copyView(currentDragPosition = position); Message msg = handler.obtainMessage(0x123, position, 0); // showpress本身大概需要170毫秒 handler.sendMessageDelayed(msg, dragLongPressTime - 170); hasSendDragMsg = true; } } }; }; private boolean hasSendDragMsg = false; private Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { switch (msg.what) { case 0x123: // 启动拖拽模式 isDragable = true; // 根据点击的位置生成该位置上的view镜像 copyView(currentDragPosition = msg.arg1); hasSendDragMsg = false; break; default: break; } return false; } }); public DragSortGridView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public DragSortGridView(Context context) { super(context); init(); } private void init() { Context context = getContext(); mGridView = new NoScrollGridView(context); mGridView.setVerticalScrollBarEnabled(false); mGridView.setStretchMode(GridView.STRETCH_COLUMN_WIDTH); mGridView.setSelector(new ColorDrawable()); // View的宽高之类必须在测量,布局,绘制一系列过程之后才能获取到 mGridView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (mChilds.isEmpty()) { for (int i = 0; i < mGridView.getChildCount(); i++) { View view = mGridView.getChildAt(i); view.setTag(TAG_KEY, new int[] { 0, 0 }); view.clearAnimation(); mChilds.add(view); } } if (!mChilds.isEmpty()) { mColHeight = mChilds.get(0).getHeight(); } mColWidth = mGridView.getColumnWidth(); if (mChildCount % mNumColumns == 0) { mMaxHeight = mColHeight * mChildCount / mNumColumns; } else { mMaxHeight = mColHeight * (mChildCount / mNumColumns + 1); } canScroll = mMaxHeight - getHeight() > 0; // 告知事件处理,完成View加载,许多属性也已经初始化了 isViewInitDone = true; } }); mScrollView = new ListenScrollView(context); mDragFrame = new FrameLayout(context); addView(mScrollView, -1, -1); mScrollView.addView(mGridView, -1, -1); addView(mDragFrame, new LayoutParams(-1, -1)); detector = new GestureDetector(context, simpleOnGestureListener); detector.setIsLongpressEnabled(false); mGridView.setNumColumns(mNumColumns); } @Override public boolean onTouchEvent(MotionEvent ev) { if (l != null) { l.onTouch(this, ev); } if (!isViewInitDone) { return false; } if (isDragable) { handleScrollAndCreMirror(ev); } else { // 交给子控件自己处理 if (canScroll) mScrollView.dispatchTouchEvent(ev); else mGridView.dispatchTouchEvent(ev); } // 处理拖动 detector.onTouchEvent(ev); if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) { lastLocation = null; if (hasSendDragMsg) { hasSendDragMsg = false; handler.removeMessages(0x123); } } return true; } /** * Author :[pWX273343] 2015年7月22日 *

* Description :拦截所有事件 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } /** * 处理自动滚屏,和单击生成镜像 */ private void handleScrollAndCreMirror(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // 通知父控件不拦截我的事件 getParent().requestDisallowInterceptTouchEvent(true); // 根据点击的位置生成该位置上的view镜像 int position = eventToPosition(ev); if (position >= headDragPosition && position < mChildCount - footDragPosition) { copyView(currentDragPosition = position); } break; case MotionEvent.ACTION_MOVE: getParent().requestDisallowInterceptTouchEvent(true);// 通知父控件不拦截我的事件 // 内容太多时,移动到边缘会自动滚动 if (canScroll) { int touchArea = decodeTouchArea(ev); if (touchArea != mTouchArea) { onTouchAreaChange(touchArea); mTouchArea = touchArea; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (hideView != null) { hideView.setVisibility(View.VISIBLE); if (onDragSelectListener != null) { onDragSelectListener.onPutDown(hideView); } } mDragFrame.removeAllViews(); // mDragFrame.scrollTo(0, 0); // isNotifyByDragSort = true; if (hasPositionChange) { hasPositionChange = false; adapter.notifyDataSetChanged(); } else if (mDragMode == DRAG_BY_LONG_CLICK && itemLongClickListener != null) { itemLongClickListener.onItemLongClick(mGridView, childAt(currentDragPosition), currentDragPosition, 0); } // 停止滚动 if (canScroll) { int scrollStates2 = decodeTouchArea(ev); if (scrollStates2 != 0) { onTouchAreaChange(0); mTouchArea = 0; } } // 放手时取消拖动排序模式 if (mDragMode == DRAG_BY_LONG_CLICK) { isDragable = false; } break; default: break; } } /** * @param ev * 事件 * @return 0中间区域, 1底部,-1顶部 * @描述: 检查当前触摸事件位于哪个区域, 顶部1/5可能触发下滚,底部1/5可能触发上滚 * @作者 [pWX273343] 2015年6月30日 */ private int decodeTouchArea(MotionEvent ev) { if (ev.getY() > getHeight() * 4 / (double) 5) { return 1; } else if (ev.getY() < getHeight() / (double) 5) { return -1; } else { return 0; } } /** * @param ev * @return * @描述 得到事件触发点,摸到的是哪一个item * @作者 [pWX273343] 2015年7月6日 */ public int eventToPosition(MotionEvent ev) { if (ev != null) { int m = (int) ev.getX() / mColWidth; int n = (int) (ev.getY() + mCurrentY) / mColHeight; int position = n * mNumColumns + m; if (position >= mChildCount) { return mChildCount - 1; } else { return position; } } return 0; } // 这里把控件作为假的横向ListView,所以返回position跟高度无关,暂时这样 // public int eventToPosition(MotionEvent ev) { // // if (ev != null) { // int m = (int) ev.getX() / mColWidth; // if (m >= mChildCount) { // return mChildCount - 1; // } else { // return m; // } // } // return 0; // } /** * @param dragPosition * @描述:复制一个镜像,并添加到透明层 * @作者 [pWX273343] 2015年7月6日 */ private void copyView(int dragPosition) { hideView = mChilds.get(dragPosition); int realPosition = mGridView.indexOfChild(hideView); if (!adapter.isUseCopyView()) { mCopyView = adapter.getView(realPosition, mCopyView, mDragFrame); } else { mCopyView = adapter.copyView(realPosition, mCopyView, mDragFrame); } hideView.setVisibility(View.INVISIBLE); mDragFrame.addView(mCopyView, mColWidth, mColHeight); int[] l1 = new int[2]; int[] l2 = new int[2]; hideView.getLocationOnScreen(l1); mDragFrame.getLocationOnScreen(l2); // mCopyView.setX(hideView.getLeft()); // mCopyView.setY(hideView.getTop() - mCurrentY); mCopyView.setX(l1[0] - l2[0]); mCopyView.setY(l1[1] - l2[1]); if (onDragSelectListener == null) { mCopyView.setScaleX(1.2f); mCopyView.setScaleY(1.2f); } else { onDragSelectListener.onDragSelect(mCopyView); } } /** * @param from * @param to * @描述:动画效果移动View * @作者 [pWX273343] 2015年6月24日 */ private void translateView(int from, int to) { View view = mChilds.get(from); int fromXValue = ((int[]) view.getTag(TAG_KEY))[0]; int fromYValue = ((int[]) view.getTag(TAG_KEY))[1]; int toXValue = to % mNumColumns - from % mNumColumns + fromXValue; int toYValue = to / mNumColumns - from / mNumColumns + fromYValue; Animation animation = new TranslateAnimation(1, fromXValue, 1, toXValue, 1, fromYValue, 1, toYValue); animation.setDuration(ANIM_DURING); animation.setFillAfter(true); view.setTag(TAG_KEY, new int[] { toXValue, toYValue }); view.startAnimation(animation); } /** * @param from * @param to * @描述:拖动View使位置发生改变时 * @作者 [pWX273343] 2015年7月6日 */ private void onDragPositionChange(int from, int to) { if (from > to) { for (int i = to; i < from; i++) { translateView(i, i + 1); } } else { for (int i = to; i > from; i--) { translateView(i, i - 1); } } if (!hasPositionChange) { hasPositionChange = true; } adapter.onDataModelMove(from, to); View view = mChilds.remove(from); mChilds.add(to, view); currentDragPosition = to; } /** * Function :setAdapter *

* Author :[pWX273343] 2015年6月24日 *

* Description :设置适配器.该适配器必须实现一个方法,当view的位置发生变动时,对实际数据的改动 * * @param adapter * @see GridView#setAdapter(android.widget.ListAdapter) */ public void setAdapter(DragAdapter adapter) { if (this.adapter != null && observer != null) { this.adapter.unregisterDataSetObserver(observer); } this.adapter = adapter; mGridView.setAdapter(adapter); adapter.registerDataSetObserver(observer); mChildCount = adapter.getCount(); } public int getNumColumns() { return mNumColumns; } /** * 每行几个 */ public void setNumColumns(int numColumns) { this.mNumColumns = numColumns; mGridView.setNumColumns(numColumns); } /** * 设置前几个item不可以改变位置 */ public void setNoPositionChangeItemCount(int count) { headDragPosition = count; } /** * 设置后几个item不可以改变位置 */ public void setFootNoPositionChangeItemCount(int count) { footDragPosition = count; } /** * 控制自动滚屏的动画监听器. */ private AnimatorUpdateListener animUpdateListener = new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int targetY = Math.round((Float) animation.getAnimatedValue()); if (targetY < 0) { targetY = 0; } else if (targetY > mMaxHeight - getHeight()) { targetY = mMaxHeight - getHeight(); } // mGridView.scrollTo(0, targetY); mScrollView.smoothScrollTo(0, targetY); // mCurrentY = targetY; } }; /** * @param scrollStates * @描述:触摸区域改变,做相应处理,开始滚动或停止滚动 * @作者 [pWX273343] 2015年6月29日 */ protected void onTouchAreaChange(int scrollStates) { if (!canScroll) { return; } if (animator != null) { animator.removeUpdateListener(animUpdateListener); } if (scrollStates == 1) {// 从普通区域进入触发向上滚动的区域 int instance = mMaxHeight - getHeight() - mCurrentY; animator = ValueAnimator.ofFloat(mCurrentY, mMaxHeight - getHeight()); animator.setDuration((long) (instance / 0.5f)); animator.setTarget(mGridView); animator.addUpdateListener(animUpdateListener); animator.start(); } else if (scrollStates == -1) {// 进入触发向下滚动的区域 animator = ValueAnimator.ofFloat(mCurrentY, 0); animator.setDuration((long) (mCurrentY / 0.5f)); animator.setTarget(mGridView); animator.addUpdateListener(animUpdateListener); animator.start(); } } private OnDragSelectListener onDragSelectListener; /** * @描述:一个item view刚被拖拽和放下时起来生成镜像时调用. * @作者 [pWX273343] 2015年6月30日 */ public void setOnDragSelectListener(OnDragSelectListener onDragSelectListener) { this.onDragSelectListener = onDragSelectListener; } public interface OnDragSelectListener { /** * @param mirror * 所拖拽起来的view生成的镜像 ,并不是实际的view.可对这个镜像实施变换效果,但是并不改变放下后的效果 * @描述:拖拽起一个view时调用 * @作者 [pWX273343] 2015年6月30日 */ void onDragSelect(View mirror); /** * @param itemView * @描述:拖拽的View放下时调用 * @作者 [pWX273343] 2015年7月3日 */ void onPutDown(View itemView); } class NoScrollGridView extends GridView { public NoScrollGridView(Context context) { super(context); } /** * @return * @描述:兼容老版本的getColumWidth * @作者 [pWX273343] 2015年7月1日 */ public int getColumnWidth() { return getWidth() / getNumColumns(); } public NoScrollGridView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int mExpandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, mExpandSpec); } } /** * * Copyright (C), 2008-2015, Huawei Tech. Co., Ltd. *

* Description : 监听滚动的scrollview,我们需要实时知道他已滚动的距离 * * @author [pWX273343] 2015年7月22日 * @version V100R001 * @since V100R001 * */ class ListenScrollView extends ScrollView { public ListenScrollView(Context context) { super(context); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); mCurrentY = getScrollY(); } } public View getChildViewAtIndex(int index) { if (index < mChilds.size()) { return mChilds.get(index); } return null; } // 转交给gridview一些常用监听器 private OnItemLongClickListener itemLongClickListener; /** * * @描述:item 转交给gridview一些常用监听器 * * @param itemClickListener * @作者 [pWX273343] 2015年7月27日 */ public void setOnItemClickListener(OnItemClickListener itemClickListener) { mGridView.setOnItemClickListener(itemClickListener); } /** * 长按监听器自己触发,点击拖动模式不存在长按 * * @param */ public void setOnItemLongClickListener(OnItemLongClickListener itemLongClickListener) { this.itemLongClickListener = itemLongClickListener; } /** 点击拖动 */ public static final int DRAG_WHEN_TOUCH = 0; /** 长按拖动 */ public static final int DRAG_BY_LONG_CLICK = 1; private int mDragMode = DRAG_WHEN_TOUCH; /** * @param mode * int类型 * @描述:设置拖动的策略是点击还是长按 * @作者 [pWX273343] 2015年7月20日 参考 DRAG_WHEN_TOUCH,DRAG_BY_LONG_CLICK */ public void setDragModel(int mode) { this.mDragMode = mode; isDragable = mode == DRAG_WHEN_TOUCH; } public View childAt(int index) { return mGridView.getChildAt(index); } public int childCount() { return mGridView.getChildCount(); } public void setAnimFrame(FrameLayout mDragFrame) { this.mDragFrame = mDragFrame; } private OnTouchListener l; @Override public void setOnTouchListener(OnTouchListener l) { this.l = l; } private long dragLongPressTime = 600; /** * 设置长按需要用时 * * @param time */ public void setDragLongPressTime(long time) { dragLongPressTime = time; } }

DragAdapter

public abstract class DragAdapter extends BaseAdapter {

    /**
     * 
     * @描述:当从from排序被拖到to排序时的处理方式,请对相应的数据做处理。
     * 
     * @param from
     * @param to
     * @作者 [pWX273343] 2015年6月24日
     */
    public abstract void onDataModelMove(int from, int to);

    /**
     * 复制View使用的方法,默认直接使用getView方法获取
     * @param position
     * @param convertView
     * @param parent
     * @return
     */
    public View copyView(int position, View convertView, ViewGroup parent) {
        return null;
    }

    /**
     * 是否启用copyView方法
     * @return true 使用copyView复制 false 使用getView直接获取镜像
     */
    public boolean isUseCopyView() {
        return false;
    }
}

ps:
其实相同功能的控件已经很多了,但是当时初学android写这个控件花了一些心血.或许也只能安慰地说一句,写这个控件的过程中了解了更多android的机制,也是一种成长经历吧.

你可能感兴趣的:(android)