一个实现item可手动拖拽的ListView和GridView

 

上篇博客分享了一个实现ListView中item交换动画的控件(戳这里查看),但是有些情况下我们的需求比这种效果要复杂。比如说需要手动拖拽item来完成item交换的交互。像这样:

还有这样:

 

这次分享的控件就实现这样的功能,下面开始正文。

先说实现item拖拽功能的DragListView。上代码:

public class DragListView extends ListView {

    /**
     * 速度模板,影响视图移动时的速度变化
     * 

* MODE_LINEAR // 线性变化模式 * MODE_ACCELERATE // 加速模式 * MODE_DECELERATE // 减速模式 * MODE_ACCELERATE_DECELERATE // 先加速后加速模式 */ public static final int MODE_LINEAR = 0x001; public static final int MODE_ACCELERATE = 0x002; public static final int MODE_DECELERATE = 0x003; public static final int MODE_ACCELERATE_DECELERATE = 0x004; private Context context; // 拖动时的视图 private View dragView; private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private BaseDragAdapter adapter; /** * 可设置选项 */ // 移动动画储持续时间,单位毫秒 private long duration = 300; // 速度模板 private int speedMode = MODE_ACCELERATE_DECELERATE; // 自动滚动的速度 private int scrollSpeed = 50; /** * 运行参数 */ // 拖动块的原始坐标 private int originalPosition = -1; // 拖动块当前所在坐标 private int currentPosition = -1; // 用于记录上次点击事件MotionEvent.getX(); private int lastX; // 用于记录上次点击事件MotionEvent.getY(); private int lastY; // 用于记录上次点击事件MotionEvent.getRawX(); private int lastRawX; // 用于记录上次点击事件MotionEvent.getRawY(); private int lastRawY; // 拖动块中心点x坐标,用于判断拖动块所处的列表位置 private int dragCenterX; // 拖动块中心点y坐标,用于判断拖动块所处的列表位置 private int dragCenterY; // 滑动上边界,拖动块中心超过该边界时列表自动向下滑动 private int upScrollBorder; // 滑动下边界,拖动块中心超过该边界时列表自动向上滑动 private int downScrollBorder; // 状态栏高度 private int statusHeight; // 拖动时的列表刷新标识符 private boolean dragRefresh; // 拖动锁定标记,为false时选中块可被拖动 private boolean dragLock = true; // 动画列表,存放当前屏幕上正在播放的所有滑动动画的动画对象 private ArrayList animatorList; // 视图列表,存放当前屏幕上正在播放的所有滑动动画的视图对象 private ArrayList dragViews; /** * 可监听接口 */ // 拖动块视图对象生成器,可通过设置该接口自定义一个拖动视图的样式,不设置时会有默认实现 private DragViewCreator dragViewCreator; // 拖动监听接口,拖动开始和结束时会在该接口回调 private OnDragingListener dragingListener; // 当前拖动目标位置改变时,每次改变都会在该接口回调 private OnDragTargetChangedListener targetChangedListener; // 内部接口,动画观察者,滑动动画结束是回调 private AnimatorObserver animatorObserver; private Handler handler = new Handler(); // 列表自动滚动线程 private Runnable scrollRunnable = new Runnable() { @Override public void run() { int scrollY; // 滚动到顶或到底时停止滚动 if (getFirstVisiblePosition() == 0 || getLastVisiblePosition() == getCount() - 1) { handler.removeCallbacks(scrollRunnable); } // 触控点y坐标超过上边界时,出发列表自动向下滚动 if (lastY > upScrollBorder) { scrollY = scrollSpeed; handler.postDelayed(scrollRunnable, 25); } // 触控点y坐标超过下边界时,出发列表自动向上滚动 else if (lastY < downScrollBorder) { scrollY = -scrollSpeed; handler.postDelayed(scrollRunnable, 25); } // // 触控点y坐标处于上下边界之间时,停止滚动 else { scrollY = 0; handler.removeCallbacks(scrollRunnable); } smoothScrollBy(scrollY, 10); } }; public DragListView(Context context) { super(context); init(context); } public DragListView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public DragListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } /** * 初始化方法 * * @param context */ private void init(Context context) { this.context = context; statusHeight = getStatusHeight(); windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); animatorList = new ArrayList<>(); dragViews = new ArrayList<>(); // 拖动块视图对象生成器的默认实现,返回一个与被拖动项外观一致的ImageView dragViewCreator = new DragViewCreator() { @Override public View createDragView(int width, int height, Bitmap viewCache) { ImageView imageView = new ImageView(DragListView.this.context); imageView.setImageBitmap(viewCache); return imageView; } }; } @Override public boolean dispatchTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: downScrollBorder = getHeight() / 5; upScrollBorder = getHeight() * 4 / 5; // 手指按下时记录相关坐标 lastX = (int) motionEvent.getX(); lastY = (int) motionEvent.getY(); lastRawX = (int) motionEvent.getRawX(); lastRawY = (int) motionEvent.getRawY(); currentPosition = pointToPosition(lastRawX, lastRawY); if (currentPosition == AdapterView.INVALID_POSITION || !adapter.isDragAvailable(currentPosition)) { return true; } originalPosition = currentPosition; break; } return super.dispatchTouchEvent(motionEvent); } @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: if (!dragLock) { int currentRawX = (int) motionEvent.getRawX(); int currentRawY = (int) motionEvent.getRawY(); if (dragView == null) { createDragImageView(getChildAt(pointToPosition(lastRawX, lastRawY) - getFirstVisiblePosition())); getChildAt(originalPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE); if (dragingListener != null) { dragingListener.onStart(originalPosition); } } drag(currentRawY - lastRawY); if (dragingListener != null) { dragingListener.onDraging((int) motionEvent.getX(), (int) motionEvent.getY(), currentRawX, currentRawY); } int position = pointToPosition(dragCenterX, dragCenterY); // 满足交换条件时让目标位置的原有视图上滑或下滑 if (position != AdapterView.INVALID_POSITION && currentPosition != position && adapter.isDragAvailable(position)) { translation(position, currentPosition); currentPosition = position; if (targetChangedListener != null) { targetChangedListener.onTargetChanged(currentPosition); } } // 更新点击位置 lastX = (int) motionEvent.getX(); lastY = (int) motionEvent.getY(); lastRawX = currentRawX; lastRawY = currentRawY; // 返回true消耗掉这次点击事件,防止ListView本身接收到这次点击事件后触发滚动 return true; } break; case MotionEvent.ACTION_UP: // 手指抬起时,如果所有滑动动画都已播放完毕,则直接执行拖动完成逻辑 if (animatorList.size() == 0) { resetDataAndView(); if (dragingListener != null) { dragingListener.onFinish(currentPosition); } } // 如果还有未播放完成的滑动动画,则注册观察者,延时执行拖动完成逻辑 else { animatorObserver = new AnimatorObserver() { @Override public void onAllAnimatorFinish() { resetDataAndView(); if (dragingListener != null) { dragingListener.onFinish(currentPosition); } } }; } break; } return super.onTouchEvent(motionEvent); } /** * 创建拖动块视图方法 * * @param view 被拖动位置的视图对象 */ private void createDragImageView(View view) { if (view == null) { return; } removeDragImageView(); int[] location = new int[2]; view.getLocationOnScreen(location); view.setDrawingCacheEnabled(true); Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); view.destroyDrawingCache(); windowLayoutParams = new WindowManager.LayoutParams(); windowLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; windowLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; windowLayoutParams.format = PixelFormat.TRANSPARENT; windowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; windowLayoutParams.x = location[0]; windowLayoutParams.y = location[1] - statusHeight; windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; dragCenterX = windowLayoutParams.x + view.getWidth() / 2; dragCenterY = windowLayoutParams.y + view.getHeight() / 2; dragView = dragViewCreator.createDragView(view.getWidth(), view.getHeight(), bitmap); if (dragView == null) { throw new NullPointerException("dragView can not be null"); } else { windowManager.addView(dragView, windowLayoutParams); } } /** * 移除拖动视图方法 */ private void removeDragImageView() { if (dragView != null && windowManager != null) { windowManager.removeView(dragView); dragView = null; windowLayoutParams = null; } } /** * 拖动方法 * * @param dy */ private void drag(int dy) { dragCenterY += dy; windowLayoutParams.y += dy; windowManager.updateViewLayout(dragView, windowLayoutParams); handler.post(scrollRunnable); } /** * 移动指定位置视图方法 * * @param fromPosition 移动起始位置 * @param toPosition 移动目标位置 */ private void translation(int fromPosition, int toPosition) { View fromView = getChildAt(fromPosition - getFirstVisiblePosition()); View toView = getChildAt(toPosition - getFirstVisiblePosition()); if (fromView == null || toView == null) { return; } float distance = (toView.getY() - toView.getTranslationY()) - (fromView.getY() - fromView.getTranslationY()); ObjectAnimator animator = ObjectAnimator.ofFloat(fromView, "translationY", 0, distance); animator.setDuration(duration); animator.setInterpolator(getAnimatorInterpolator()); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animatorList.remove(animation); // 所有滑动动画都播放结束时,执行相关操作 if (animatorList.size() == 0) { // 重置所有滑动过的视图的translateY,避免列表刷新后视图重叠 resetTranslate(dragViews); dragViews.clear(); adapter.exchangeData(originalPosition, currentPosition); addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (dragRefresh) { removeOnLayoutChangeListener(this); resetChildVisibility(); getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE); originalPosition = currentPosition; dragRefresh = false; if (animatorObserver != null) { animatorObserver.onAllAnimatorFinish(); animatorObserver = null; } } } }); dragRefresh = true; adapter.notifyDataSetChanged(); } } }); animatorList.add(animator); dragViews.add(fromView); animator.start(); } /** * 重置列表所有项的可见性方法 */ private void resetChildVisibility() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child != null) { child.setVisibility(VISIBLE); } } } /** * 重置指定视图的translateY属性方法 * * @param list */ private void resetTranslate(ArrayList list) { for (int i = 0; i < list.size(); i++) { if (list.get(i) != null) { list.get(i).setTranslationY(0); } } } /** * 重置数据和视图相关数据方法 */ private void resetDataAndView() { if (currentPosition == -1) { return; } getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE); originalPosition = -1; currentPosition = -1; handler.removeCallbacks(scrollRunnable); removeDragImageView(); } @Override public void setAdapter(ListAdapter adapter) { if (adapter instanceof BaseDragAdapter) { this.adapter = (BaseDragAdapter) adapter; super.setAdapter(adapter); } else { throw new IllegalStateException("the adapter must extends BaseDragAdapter"); } } /** * 根据速度模板创建动画迭代器 * * @return */ private Interpolator getAnimatorInterpolator() { switch (speedMode) { case MODE_LINEAR: return new LinearInterpolator(); case MODE_ACCELERATE: return new AccelerateInterpolator(); case MODE_DECELERATE: return new DecelerateInterpolator(); case MODE_ACCELERATE_DECELERATE: return new AccelerateDecelerateInterpolator(); default: return null; } } /** * 拖动解锁方法,调用者需手动调用该方法后才能开启列表拖动功能 */ public void unlockDrag() { dragLock = false; } /** * 拖动锁定方法,调用者调用该方法后关闭列表拖动功能 */ public void lockDrag() { dragLock = true; } /** * 设置移动动画持续时间 * * @param duration 时间,单位毫秒 */ public void setDuration(long duration) { this.duration = duration; } /** * 设置速度模式,可选项: * MODE_LINEAR 线性变化模式 * MODE_ACCELERATE 加速模式 * MODE_DECELERATE 减速模式 * MODE_ACCELERATE_DECELERATE 先加速后加速模式 * * @param speedMode */ public void setSpeedMode(int speedMode) { this.speedMode = speedMode; } /** * 设置自动滚动速度 * * @param scrollSpeed 速度,单位:dp/10ms */ public void setScrollSpeed(int scrollSpeed) { this.scrollSpeed = scrollSpeed; } /** * 设置拖动块视图对象生成器方法 * * @param creator */ public void setDragViewCreator(DragViewCreator creator) { if (creator == null) { return; } this.dragViewCreator = creator; } /** * 设置拖动监听接口 * * @param dragingListener */ public void setOnDragingListener(OnDragingListener dragingListener) { this.dragingListener = dragingListener; } /** * 设置拖动目标位置改变监听接口 * * @param targetChangedListener */ public void setOnDragTargetChangedListener(OnDragTargetChangedListener targetChangedListener) { this.targetChangedListener = targetChangedListener; } private int getStatusHeight() { int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { return context.getResources().getDimensionPixelSize(resourceId); } return 0; } /** * 动画观察者 */ private interface AnimatorObserver { /** * 滑动动画播放结束时回调 */ void onAllAnimatorFinish(); } /** * 拖动块视图对象生成器 */ public interface DragViewCreator { /** * 创建拖动块视图对象方法,可通过实现该方法自定义拖动块样式 */ View createDragView(int width, int height, Bitmap viewCache); } /** * 拖动监听接口 */ public interface OnDragingListener { /** * 拖动开始时回调 * * @param startPosition 拖动起始坐标 */ void onStart(int startPosition); /** * 拖动过程中回调 * * @param x 触控点相对ListView的x坐标 * @param y 触控点相对ListView的y坐标 * @param rawX 触控点相对屏幕的x坐标 * @param rawY 触控点相对屏幕的y坐标 */ void onDraging(int x, int y, int rawX, int rawY); /** * 拖动结束时回调 * * @param finalPosition 拖动终点坐标 */ void onFinish(int finalPosition); } /** * 拖动目标位置改变监听接口 */ public interface OnDragTargetChangedListener { /** * 拖动过程中,每次目标位置改变,会在该方法回调 * * @param targetPosition 拖动目标位置坐标 */ void onTargetChanged(int targetPosition); } }

简单讲一下实现原理。手指按下时通过ListView的getChildAt方法获得按下位置的item并获取其视图缓存,也就是这句话:

view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.destroyDrawingCache();

然后新建一个View把这个缓存塞进去并置于屏幕之上,并隐藏原来的item,让人看起来就好像是item被“拽”了下来,也就是这句话:

windowManager.addView(dragView, windowLayoutParams);

手指移动时,改变这个View的LayoutParams的y坐标值,让它跟随手指移动,也就是这两句话:

windowLayoutParams.y += dy;
windowManager.updateViewLayout(dragView, windowLayoutParams);

拖拽过程中,当判定交换行为发生时,用一个属性动画不断改变目标item的translationY属性来实现交换效果,也就是这句话:

ObjectAnimator animator = ObjectAnimator.ofFloat(fromView, "translationY", 0, distance);

具体代码大家可以看注释,应该写得比较清楚了。

要特别说明的是,DragListView的setAdapter方法被重写了,只接收BaseDragAdapter的继承类,BaseDragAdapter长这样:
 

public abstract class BaseDragAdapter extends BaseAdapter {

    /**
     * 调用者需实现该方法,返回列表的所有数据集合
     *
     * @return
     */
    public abstract List getDataList();

    /**
     * 调用者可实现该方法自定义某一项是否可被拖动
     *
     * @param position
     * @return
     */
    public abstract boolean isDragAvailable(int position);

    /**
     * 实现数据交换方法
     *
     * @param oldPosition
     * @param newPosition
     */
    public void exchangeData(int oldPosition, int newPosition) {

        List list = getDataList();
        if (list == null) {
            return;
        }
        Object temp = list.get(oldPosition);
        if (oldPosition < newPosition) {
            for (int i = oldPosition; i < newPosition; i++) {
                Collections.swap(list, i, i + 1);
            }
        } else if (oldPosition > newPosition) {
            for (int i = oldPosition; i > newPosition; i--) {
                Collections.swap(list, i, i - 1);
            }
        }
        list.set(newPosition, temp);
    }
}

BaseDragAdapter的目的是替调用者封装一些必要的操作,它给普通的BaseAdapter增加了两个需要实现的抽象方法:getDataList()和isDragAvailable()。getDataList()返回ListView 的数据列表即可,isDragAvailable()用来让调用者决定某个item是否可被拖拽,比如说需求是列表的第一项不可被拖拽,只需要实现isDragAvailable方法,在position=0时返回false即可。

然后就可以使用了。先写一个item的布局:
 




    

    

再简单写一个适配器TestListViewAdapter继承自BaseDragAdapter:

public class TestListViewAdapter extends BaseDragAdapter {

    private Context context;
    private int resourceId;
    private ArrayList list;

    private Vibrator vibrator;

    private OnItemLongClickListener listener;

    public TestListViewAdapter(Context context, int resourceId, ArrayList list) {
        this.context = context;
        this.resourceId = resourceId;
        this.list = list;
        this.vibrator = (Vibrator) context.getSystemService(context.VIBRATOR_SERVICE);
    }

    @Override
    public List getDataList() {
        return list;
    }

    @Override
    public boolean isDragAvailable(int position) {
        return true;
    }

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

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

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

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            viewHolder.itemLayout = view.findViewById(R.id.item_layout);
            viewHolder.contentTextView = view.findViewById(R.id.content_textview);
            viewHolder.dividerLine = view.findViewById(R.id.divider_line);
            view.setTag(viewHolder);
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }

        viewHolder.contentTextView.setText(list.get(position));

        viewHolder.dividerLine.setVisibility(position != list.size() - 1 ? View.VISIBLE : View.INVISIBLE);

        viewHolder.itemLayout.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                vibrator.vibrate(100);

                if (listener != null) {
                    listener.onItemLongClick(position);
                }

                return false;
            }
        });

        return view;
    }

    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
        this.listener = listener;
    }

    public interface OnItemLongClickListener {

        void onItemLongClick(int position);

    }

    class ViewHolder {
        LinearLayout itemLayout;
        TextView contentTextView;
        View dividerLine;
    }
}

代码很简单,就不多说了。

最后就可以使用了,Activity里这样写:
 

private DragListView dragListView;

private TestListViewAdapter adapter;

private ArrayList list;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_drag_listview_test);

    initData();
    initView();
}

private void initData() {
    list = new ArrayList<>();

    for (int i = 1; i <= 40; i++) {
        list.add("我是第" + i + "条数据");
    }
}

private void initView() {
    dragListView = findViewById(R.id.drag_listview);

    dragListView.setOnDragingListener(new DragListView.OnDragingListener() {
        @Override
        public void onStart(int startPosition) {

        }

        @Override
        public void onDraging(int x, int y, int rawX, int rawY) {

        }

        @Override
        public void onFinish(int finalPosition) {
            dragListView.lockDrag();
        }
    });

    adapter = new TestListViewAdapter(this, R.layout.item_test_listview, list);

    adapter.setOnItemLongClickListener(new TestListViewAdapter.OnItemLongClickListener() {
        @Override
        public void onItemLongClick(int position) {
            dragListView.unlockDrag();
        }
    });

    dragListView.setAdapter(adapter);
}

用法和普通的ListView一样,通过调用unlockDrag()来解锁拖动(示例代码中通过长按操作来解锁),通过调用lockDrag()方法来锁定拖动。之后还可以通过设置OnDragingListener来监听拖拽过程。开启和锁定拖动操作的条件视项目需求而定,比如长安开启,或者按编辑按钮开启等等。

最后运行一下就可以看到开头的效果了。

控件支持自定义拖拽View的样式。可以通过setDragViewCreator()方法来实现。比如说我想给拖拽的View加一个高亮效果,就可以这样写:

dragListView.setDragViewCreator(new DragListView.DragViewCreator() {
    @Override
    public View createDragView(int width, int height, Bitmap viewCache) {
        RelativeLayout layout = new RelativeLayout(DragListViewTestActivity.this);

        ImageView imageView = new ImageView(DragListViewTestActivity.this);
        imageView.setImageBitmap(viewCache);
        layout.addView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));

        View view = new View(DragListViewTestActivity.this);
        view.setBackground(getDrawable(R.drawable.edging_red));
        layout.addView(view, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));

        return layout;
    }
});

其中高亮的资源edging_red.xml长这样:




    

    

代码很简单,就是新建一个Layout,里面放一张图片,再在之上加一层高亮遮罩,并将这个layout返回给DragViewCreator接口即可。运行一下看一下效果:

 

同样的原理再写一个支持item拖拽的GridView,上源码:

public class DragGridView extends GridView {

    /**
     * 速度模板,影响视图移动时的速度变化
     * 

* MODE_LINEAR // 线性变化模式 * MODE_ACCELERATE // 加速模式 * MODE_DECELERATE // 减速模式 * MODE_ACCELERATE_DECELERATE // 先加速后加速模式 */ public static final int MODE_LINEAR = 0x001; public static final int MODE_ACCELERATE = 0x002; public static final int MODE_DECELERATE = 0x003; public static final int MODE_ACCELERATE_DECELERATE = 0x004; private Context context; // 拖动时的视图 private View dragView; private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private BaseDragAdapter adapter; /** * 可设置选项 */ // 移动动画储持续时间,单位毫秒 private long duration = 300; // 速度模板 private int speedMode = MODE_ACCELERATE_DECELERATE; // 自动滚动的速度 private int scrollSpeed = 50; /** * 运行参数 */ // 拖动块的原始坐标 private int originalPosition = -1; // 拖动块当前所在坐标 private int currentPosition = -1; // 用于记录上次点击事件MotionEvent.getX(); private int lastX; // 用于记录上次点击事件MotionEvent.getY(); private int lastY; // 用于记录上次点击事件MotionEvent.getRawX(); private int lastRawX; // 用于记录上次点击事件MotionEvent.getRawY(); private int lastRawY; // 拖动块中心点x坐标,用于判断拖动块所处的列表位置 private int dragCenterX; // 拖动块中心点y坐标,用于判断拖动块所处的列表位置 private int dragCenterY; // 滑动上边界,拖动块中心超过该边界时列表自动向下滑动 private int upScrollBorder; // 滑动下边界,拖动块中心超过该边界时列表自动向上滑动 private int downScrollBorder; // 状态栏高度 private int statusHeight; // 拖动时的列表刷新标识符 private boolean dragRefresh; // 拖动锁定标记,为false时选中块可被拖动 private boolean dragLock = true; // 动画列表,存放当前屏幕上正在播放的所有滑动动画的动画对象 private ArrayList animatorList; // 视图列表,存放当前屏幕上正在播放的所有滑动动画的视图对象 private ArrayList dragViews; /** * 可监听接口 */ // 拖动块视图对象生成器,可通过设置该接口自定义一个拖动视图的样式,不设置时会有默认实现 private DragViewCreator dragViewCreator; // 拖动监听接口,拖动开始和结束时会在该接口回调 private OnDragingListener dragingListener; // 当前拖动目标位置改变时,每次改变都会在该接口回调 private OnDragTargetChangedListener targetChangedListener; // 内部接口,动画观察者,滑动动画结束是回调 private AnimatorObserver animatorObserver; private Handler handler = new Handler(); // 列表自动滚动线程 private Runnable scrollRunnable = new Runnable() { @Override public void run() { int scrollY; // 滚动到顶或到底时停止滚动 if (getFirstVisiblePosition() == 0 || getLastVisiblePosition() == getCount() - 1) { handler.removeCallbacks(scrollRunnable); } // 触控点y坐标超过上边界时,出发列表自动向下滚动 if (lastY > upScrollBorder) { scrollY = scrollSpeed; handler.postDelayed(scrollRunnable, 25); } // 触控点y坐标超过下边界时,出发列表自动向上滚动 else if (lastY < downScrollBorder) { scrollY = -scrollSpeed; handler.postDelayed(scrollRunnable, 25); } // // 触控点y坐标处于上下边界之间时,停止滚动 else { scrollY = 0; handler.removeCallbacks(scrollRunnable); } smoothScrollBy(scrollY, 10); } }; public DragGridView(Context context) { super(context); init(context); } public DragGridView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } /** * 初始化方法 * * @param context */ private void init(Context context) { this.context = context; statusHeight = getStatusHeight(); windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); animatorList = new ArrayList<>(); dragViews = new ArrayList<>(); // 拖动块视图对象生成器的默认实现,返回一个与被拖动项外观一致的ImageView dragViewCreator = new DragGridView.DragViewCreator() { @Override public View createDragView(int width, int height, Bitmap viewCache) { ImageView imageView = new ImageView(DragGridView.this.context); imageView.setImageBitmap(viewCache); return imageView; } }; } @Override public boolean dispatchTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: downScrollBorder = getHeight() / 5; upScrollBorder = getHeight() * 4 / 5; // 手指按下时记录相关坐标 lastX = (int) motionEvent.getX(); lastY = (int) motionEvent.getY(); lastRawX = (int) motionEvent.getRawX(); lastRawY = (int) motionEvent.getRawY(); currentPosition = pointToPosition(lastRawX, lastRawY); if (currentPosition == AdapterView.INVALID_POSITION || !adapter.isDragAvailable(currentPosition)) { return true; } originalPosition = currentPosition; break; } return super.dispatchTouchEvent(motionEvent); } @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: if (!dragLock) { int currentRawX = (int) motionEvent.getRawX(); int currentRawY = (int) motionEvent.getRawY(); if (dragView == null) { createDragImageView(getChildAt(pointToPosition(lastRawX, lastRawY) - getFirstVisiblePosition())); getChildAt(originalPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE); if (dragingListener != null) { dragingListener.onStart(originalPosition); } } drag(currentRawX - lastRawX, currentRawY - lastRawY); if (dragingListener != null) { dragingListener.onDraging((int) motionEvent.getX(), (int) motionEvent.getY(), currentRawX, currentRawY); } int position = pointToPosition(dragCenterX, dragCenterY); if (position != AdapterView.INVALID_POSITION && currentPosition != position && adapter.isDragAvailable(position) && animatorList.size() == 0) { translation(position, currentPosition); currentPosition = position; if (targetChangedListener != null) { targetChangedListener.onTargetChanged(currentPosition); } } // 更新点击位置 lastX = (int) motionEvent.getX(); lastY = (int) motionEvent.getY(); lastRawX = currentRawX; lastRawY = currentRawY; // 返回true消耗掉这次点击事件,防止ListView本身接收到这次点击事件后触发滚动 return true; } break; case MotionEvent.ACTION_UP: // 手指抬起时,如果所有滑动动画都已播放完毕,则直接执行拖动完成逻辑 if (animatorList.size() == 0) { resetDataAndView(); if (dragingListener != null) { dragingListener.onFinish(currentPosition); } } // 如果还有未播放完成的滑动动画,则注册观察者,延时执行拖动完成逻辑 else { animatorObserver = new AnimatorObserver() { @Override public void onAllAnimatorFinish() { resetDataAndView(); if (dragingListener != null) { dragingListener.onFinish(currentPosition); } } }; } break; } return super.onTouchEvent(motionEvent); } /** * 创建拖动块视图方法 * * @param view 被拖动位置的视图对象 */ private void createDragImageView(View view) { if (view == null) { return; } removeDragImageView(); int[] location = new int[2]; view.getLocationOnScreen(location); view.setDrawingCacheEnabled(true); Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); view.destroyDrawingCache(); windowLayoutParams = new WindowManager.LayoutParams(); windowLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; windowLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; windowLayoutParams.format = PixelFormat.TRANSPARENT; windowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; windowLayoutParams.x = location[0]; windowLayoutParams.y = location[1] - statusHeight; windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; dragCenterX = windowLayoutParams.x + view.getWidth() / 2; dragCenterY = windowLayoutParams.y + view.getHeight() / 2; dragView = dragViewCreator.createDragView(view.getWidth(), view.getHeight(), bitmap); if (dragView == null) { throw new NullPointerException("dragView can not be null"); } else { windowManager.addView(dragView, windowLayoutParams); } } /** * 移除拖动视图方法 */ private void removeDragImageView() { if (dragView != null && windowManager != null) { windowManager.removeView(dragView); dragView = null; windowLayoutParams = null; } } /** * 拖动方法 * * @param dx * @param dy */ private void drag(int dx, int dy) { dragCenterX += dx; dragCenterY += dy; windowLayoutParams.x += dx; windowLayoutParams.y += dy; windowManager.updateViewLayout(dragView, windowLayoutParams); handler.post(scrollRunnable); } /** * 移动指定位置视图方法 * * @param fromPosition 移动起始位置 * @param toPosition 移动目标位置 */ private void translation(int fromPosition, int toPosition) { ArrayList list = new ArrayList<>(); if (toPosition > fromPosition) { for (int position = fromPosition; position < toPosition; position++) { View view = getChildAt(position - getFirstVisiblePosition()); dragViews.add(view); if ((position + 1) % getNumColumns() == 0) { list.add(createTranslationAnimations(view, 0, -(view.getWidth() + getVerticalSpacing()) * (getNumColumns() - 1), 0, view.getHeight() + getHorizontalSpacing())); } else { list.add(createTranslationAnimations(view, 0, view.getWidth() + getVerticalSpacing(), 0, 0)); } } } else { for (int position = fromPosition; position > toPosition; position--) { View view = getChildAt(position - getFirstVisiblePosition()); dragViews.add(view); if (position % getNumColumns() == 0) { list.add(createTranslationAnimations(view, 0, (view.getWidth() + getVerticalSpacing()) * (getNumColumns() - 1), 0, -(view.getHeight() + getHorizontalSpacing()))); } else { list.add(createTranslationAnimations(view, 0, -(view.getWidth() + getVerticalSpacing()), 0, 0)); } } } AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(list); animatorSet.setDuration(duration); animatorSet.setInterpolator(getAnimatorInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animatorList.remove(animation); // 所有滑动动画都播放结束时,执行相关操作 if (animatorList.size() == 0) { // 重置所有滑动过的视图的translateY,避免列表刷新后视图重叠 resetTranslate(dragViews); dragViews.clear(); adapter.exchangeData(originalPosition, currentPosition); addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (dragRefresh) { removeOnLayoutChangeListener(this); resetChildVisibility(); getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE); originalPosition = currentPosition; dragRefresh = false; if (animatorObserver != null) { animatorObserver.onAllAnimatorFinish(); animatorObserver = null; } } } }); dragRefresh = true; adapter.notifyDataSetChanged(); } } }); animatorList.add(animatorSet); animatorSet.start(); } /** * 生成移动动画方法 * * @param view 需要移动的视图 * @param startX 移动起始x坐标 * @param endX 移动终点x坐标 * @param startY 移动起始y坐标 * @param endY 移动终点y坐标 * @return */ private Animator createTranslationAnimations(View view, float startX, float endX, float startY, float endY) { ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", startX, endX); ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", startY, endY); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(animatorX, animatorY); return animatorSet; } /** * 重置列表所有项的可见性方法 */ private void resetChildVisibility() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child != null) { child.setVisibility(VISIBLE); } } } /** * 重置指定视图的translateY属性方法 * * @param list */ private void resetTranslate(ArrayList list) { for (int i = 0; i < list.size(); i++) { if (list.get(i) != null) { list.get(i).setTranslationX(0); list.get(i).setTranslationY(0); } } } /** * 重置数据和视图相关数据方法 */ private void resetDataAndView() { if (currentPosition == -1) { return; } getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE); originalPosition = -1; currentPosition = -1; dragLock = true; handler.removeCallbacks(scrollRunnable); removeDragImageView(); } @Override public void setAdapter(ListAdapter adapter) { if (adapter instanceof BaseDragAdapter) { this.adapter = (BaseDragAdapter) adapter; super.setAdapter(adapter); } else { throw new IllegalStateException("the adapter must extends BaseDragAdapter"); } } /** * 根据速度模板创建动画迭代器 * * @return */ private Interpolator getAnimatorInterpolator() { switch (speedMode) { case MODE_LINEAR: return new LinearInterpolator(); case MODE_ACCELERATE: return new AccelerateInterpolator(); case MODE_DECELERATE: return new DecelerateInterpolator(); case MODE_ACCELERATE_DECELERATE: return new AccelerateDecelerateInterpolator(); default: return null; } } /** * 拖动解锁方法,调用者需手动调用该方法后才能开启列表拖动功能 */ public void unlockDrag() { dragLock = false; } /** * 拖动锁定方法,调用者调用该方法后关闭列表拖动功能 */ public void lockDrag() { dragLock = true; } /** * 设置移动动画持续时间 * * @param duration 时间,单位毫秒 */ public void setDuration(long duration) { this.duration = duration; } /** * 设置速度模式,可选项: * MODE_LINEAR 线性变化模式 * MODE_ACCELERATE 加速模式 * MODE_DECELERATE 减速模式 * MODE_ACCELERATE_DECELERATE 先加速后加速模式 * * @param speedMode */ public void setSpeedMode(int speedMode) { this.speedMode = speedMode; } /** * 设置自动滚动速度 * * @param scrollSpeed 速度,单位:dp/10ms */ public void setScrollSpeed(int scrollSpeed) { this.scrollSpeed = scrollSpeed; } /** * 设置拖动块视图对象生成器方法 * * @param creator */ public void setDragViewCreator(DragViewCreator creator) { if (creator == null) { return; } this.dragViewCreator = creator; } /** * 设置拖动监听接口 * * @param dragingListener */ public void setOnDragingListener(OnDragingListener dragingListener) { this.dragingListener = dragingListener; } /** * 设置拖动目标位置改变监听接口 * * @param targetChangedListener */ public void setOnDragTargetChangedListener(OnDragTargetChangedListener targetChangedListener) { this.targetChangedListener = targetChangedListener; } private int getStatusHeight() { int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { return context.getResources().getDimensionPixelSize(resourceId); } return 0; } /** * 动画观察者 */ private interface AnimatorObserver { /** * 滑动动画播放结束时回调 */ void onAllAnimatorFinish(); } /** * 拖动块视图对象生成器 */ public interface DragViewCreator { /** * 创建拖动块视图对象方法,可通过实现该方法自定义拖动块样式 */ View createDragView(int width, int height, Bitmap viewCache); } /** * 拖动监听接口 */ public interface OnDragingListener { /** * 拖动开始时回调 * * @param startPosition 拖动起始坐标 */ void onStart(int startPosition); /** * 拖动过程中回调 * * @param x 触控点相对ListView的x坐标 * @param y 触控点相对ListView的y坐标 * @param rawX 触控点相对屏幕的x坐标 * @param rawY 触控点相对屏幕的y坐标 */ void onDraging(int x, int y, int rawX, int rawY); /** * 拖动结束时回调 * * @param finalPosition 拖动终点坐标 */ void onFinish(int finalPosition); } /** * 拖动目标位置改变监听接口 */ public interface OnDragTargetChangedListener { /** * 拖动过程中,每次目标位置改变,会在该方法回调 * * @param targetPosition 拖动目标位置坐标 */ void onTargetChanged(int targetPosition); } }

实现原理和DragListView差不多,就不多做解释了。DragGridView的setAdapter方法同样只接收BaseDragAdapter的继承类,用法和DragListView一样。

简单使用一下,先写一个item布局item_test_gridview.xml:




    

再写一个适配器TestGridViewAdapter:

public class TestGridViewAdapter extends BaseDragAdapter {

    private Context context;
    private int resourceId;
    private ArrayList list;

    private Vibrator vibrator;

    private OnItemLongClickListener listener;

    public TestGridViewAdapter(Context context, int resourceId, ArrayList list) {
        this.context = context;
        this.resourceId = resourceId;
        this.list = list;
        this.vibrator = (Vibrator) context.getSystemService(context.VIBRATOR_SERVICE);
    }

    @Override
    public List getDataList() {
        return list;
    }

    @Override
    public boolean isDragAvailable(int position) {
        return true;
    }

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

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

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

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            viewHolder.itemLayout = view.findViewById(R.id.item_layout);
            viewHolder.contentTextView = view.findViewById(R.id.content_textview);
            view.setTag(viewHolder);
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }

        viewHolder.contentTextView.setText(list.get(position));

        viewHolder.itemLayout.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                vibrator.vibrate(100);

                if (listener != null) {
                    listener.onItemLongClick(position);
                }

                return false;
            }
        });

        return view;
    }

    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
        this.listener = listener;
    }

    public interface OnItemLongClickListener {

        void onItemLongClick(int position);

    }

    class ViewHolder {
        LinearLayout itemLayout;
        TextView contentTextView;
    }
}

最后Activity这样写:

    private DragGridView dragGridView;

    private TestGridViewAdapter adapter;

    private ArrayList list;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_drag_gridview_test);

        initData();
        initView();
    }

    private void initData() {
        list = new ArrayList<>();

        for (int i = 1; i <= 40; i++) {
            list.add("我是第" + i + "条数据");
        }
    }

    private void initView() {
        dragGridView = findViewById(R.id.drag_gridview);
        
        dragGridView.setOnDragingListener(new DragGridView.OnDragingListener() {
            @Override
            public void onStart(int startPosition) {

            }

            @Override
            public void onDraging(int x, int y, int rawX, int rawY) {

            }

            @Override
            public void onFinish(int finalPosition) {
                dragGridView.lockDrag();
            }
        });

        adapter = new TestGridViewAdapter(this, R.layout.item_test_gridview, list);

        adapter.setOnItemLongClickListener(new TestGridViewAdapter.OnItemLongClickListener() {
            @Override
            public void onItemLongClick(int position) {
                dragGridView.unlockDrag();
            }
        });

        dragGridView.setAdapter(adapter);
    }

用法和DragListView一毛一样。运行一下就能看到开头的效果了。

DragGridView同样可以自定义拖拽View的样式,同样通过setDragViewCreator()方法来实现。比如说添加一个高亮效果:

dragGridView.setDragViewCreator(new DragGridView.DragViewCreator() {
    @Override
    public View createDragView(int width, int height, Bitmap viewCache) {
        RelativeLayout layout = new RelativeLayout(DragGridViewTestActivity.this);

        ImageView imageView = new ImageView(DragGridViewTestActivity.this);
        imageView.setImageBitmap(viewCache);
        layout.addView(imageView, new RelativeLayout.LayoutParams(width, height));

        View view = new View(DragGridViewTestActivity.this);
        view.setBackground(getDrawable(R.drawable.edging_red));
        layout.addView(view, new RelativeLayout.LayoutParams(width, height));

        return layout;
    }
});

看看效果:

 

以上就是全部内容了,最后来总结一下。


DragListView和DragGridView分别实现ListView和GridView的item拖拽功能。接收Adapter必须是BaseDragAdapter的继承类,通过unlockDrag()方法和lockDrag()方法来开启和关闭拖动。提供OnDragingListener接口来监听拖动过程,提供DragViewCreator接口来自定义拖拽样式。

 

最后的最后,附上源码地址:https://download.csdn.net/download/Sure_Min/12572918

这次的内容就到这里,我们下次再见。
 

你可能感兴趣的:(自定义控件)