图片拖拽排序自定义控件实现(gallery、custom ViewGroup)

一、实现效果

 

二、应用到的主要技术点

  1. android:animateLayoutChanges="true"设置这个属性有效。主要的作用是当childView 显示位置发生变化时,系统自动播放动画。
  2. 自定义ViewGroup要求我们自己实现onMeasure和onLayout控制childView的大小和显示位置。
  3. 重写onInterruptTouchEvent方法判断自定义ViewGroup是否需要截断事件,判断依据是是否有有效滑动距离产生。
  4. 重写onTouchEvent方法实现childViewd的拖拽显示(拖拽的是childView的截图)。
  5. 重写draw方法用于绘制被拖拽的childView截图。(注意:在自定义ViewGroup初始化阶段要调用setWillNotDraw(false);关闭系统的绘制优化,否则ViewGroup接到invalidate事件后,draw方法不会被调用)
  6. 调用childView.getDrawingCache()创建childView截图。

三、实现细节

1.通过重写onMeasure和onLayout方法控制childView显示大小和位置。


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //由于需要支持RTL布局,所以开始布局的横坐标根据padding而不同。
        int statLeft = Build.VERSION.SDK_INT < 17 ? getPaddingLeft() : getPaddingStart();
        int starTop = getPaddingTop();
        //布局第一个大图
        if (childCount > 0) {
            View childView = getChildAt(0);
            LayoutParameter layoutParameter = (LayoutParameter) childView.getLayoutParams();
            //view的宽度是等于容器的宽度减掉容器的padding,高度等于跨度
            int childWidth = width - getPaddingLeft() - getPaddingRight();
            childView.measure(MeasureSpec.makeMeasureSpec(childWidth, EXACTLY), MeasureSpec.makeMeasureSpec(childWidth, EXACTLY));
            //在自定义的LayoutParameter中保存view显示的位置,在onLayout阶段可以直接使用这个数据进行布局。
            layoutParameter.left = statLeft;
            layoutParameter.top = starTop;
            layoutParameter.right = layoutParameter.left + childView.getMeasuredWidth();
            layoutParameter.bottom = layoutParameter.top + childView.getMeasuredHeight();
            starTop = layoutParameter.bottom;
        }
        starTop += divideSpace;
        //计算第二行小图的宽度,排除容器的padding和小图间的空隙进行4等分
        int secondLineChildWidth = width - getPaddingLeft() - getPaddingRight() - secondLineLeftSpace - secondLineRightSpace - (secondLineDivideSpace * 3);
        secondLineChildWidth /= 4;

        bitmapRect.set(0, 0, secondLineChildWidth, secondLineChildWidth);

        statLeft = Build.VERSION.SDK_INT >= 17 ? getPaddingStart() : getPaddingLeft();
        statLeft += secondLineLeftSpace;
        //RTL布局排列第二行的小图
        if (Build.VERSION.SDK_INT >= 17 && getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
            for (int i = childCount - 1; i > 0; i--) {
                View childView = getChildAt(i);
                LayoutParameter layoutParameter = (LayoutParameter) childView.getLayoutParams();
                //小图的宽高也是相等的。
                childView.measure(MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY), MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY));
                layoutParameter.left = statLeft;
                layoutParameter.top = starTop;
                layoutParameter.right = layoutParameter.left + childView.getMeasuredWidth();
                layoutParameter.bottom = layoutParameter.top + childView.getMeasuredHeight();
                statLeft = layoutParameter.right;
                statLeft += secondLineDivideSpace;
            }
        } else {
            //LTR布局排列第二行
            for (int i = 1; i < childCount; i++) {
                View childView = getChildAt(i);
                LayoutParameter layoutParameter = (LayoutParameter) childView.getLayoutParams();
                childView.measure(MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY), MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY));
                layoutParameter.left = statLeft;
                layoutParameter.top = starTop;
                layoutParameter.right = layoutParameter.left + childView.getMeasuredWidth();
                layoutParameter.bottom = layoutParameter.top + childView.getMeasuredHeight();
                statLeft = layoutParameter.right;
                statLeft += secondLineDivideSpace;
            }
        }
        //设置容器的大小
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        View childView;
        LayoutParameter layoutParameter;
        for (int i = 0; i < childCount; i++) {
            childView = getChildAt(i);
            layoutParameter = (LayoutParameter) childView.getLayoutParams();
            //根据onMeasure阶段保存的位置数据进行布局
            childView.layout(layoutParameter.left, layoutParameter.top, layoutParameter.right, layoutParameter.bottom);
        }
    }

2.重写onInterruptTouchEvent和onTouchEvent方法实现childView的拖拽。


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                startX = 0;
                startY = 0;
                int x = (int) ev.getX();
                int y = (int) ev.getY();
                //根据坐标点找到对应的childView
                dragView = findViewByPosition(x, y);
                Log.d(TAG, "onInterceptTouchEvent ACTION_DOWN dragView " + dragView);
                if (dragView != null) {
                    Object object = dragView.getTag();
                    boolean canMove = false;
                    if (object != null && object instanceof Holder) {
                        canMove = ((Holder) object).canMove();
                    }
                    Log.d(TAG, "onInterceptTouchEvent ACTION_DOWN canMove " + canMove);
                    //如果找到的childView是可以移动的,那么保存当前的坐标。
                    if (canMove) {
                        Log.d(TAG, "onInterceptTouchEvent ACTION_DOWN find a view");
                        startX = x;
                        startY = y;
                    }
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                //其中有一个变量不为空,说明找到的childView是可移动的
                if (startX != 0 || startY != 0) {
                    int x = (int) ev.getX();
                    int y = (int) ev.getY();
                    //如果是长按或是移动距离发生溢出,我们启动childView拖拽功能。
                    if (ev.getEventTime() - ev.getDownTime() >= ViewConfiguration.getLongPressTimeout() || Math.abs(startX - x) > touchSlop || Math.abs(startY - y) > touchSlop) {
                        Log.d(TAG, "onInterceptTouchEvent ACTION_MOVE start move");
                        touchMode = TouchMode.DRAG;
                        //不允许其他控件进行事件截断。
                        requestDisallowInterceptTouchEvent(true);
                        lastX = x;
                        lastY = y;
                        //启动拖拽功能。
                        buildDragBitmap(x, y);
                    }
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //重置拖拽参数、
                resetDragBitmap();
                break;
        }

        return touchMode == TouchMode.DRAG;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                return true;
            case MotionEvent.ACTION_MOVE: {
                int x = (int) event.getX();
                int y = (int) event.getY();

                if (touchMode == TouchMode.DRAG && (Math.abs(x - lastX) > 5 || Math.abs(y - lastY) > 5)) {
                    Log.d(TAG, "onTouchEvent ACTION_MOVE ===========");
                    //移动childView的截图
                    moveDragBitmap(x - lastX, y - lastY);
                    lastX = x;
                    lastY = y;
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                touchMode = TouchMode.NORMAL;
                //重置拖拽参数。
                resetDragBitmap();
                break;
        }

        return touchMode == TouchMode.DRAG;
    }

3.在开始拖拽的时候通过调用getDrawingCache创建childView截图并播放动画。


    private void buildDragBitmap(float x, float y) {
        dragBitmap = null;
        if (dragView == null) {
            Log.d(TAG, "buildDragBitmap dragView == null!!!");
            return;
        }
        Object obj = dragView.getTag();
        if (obj != null && obj instanceof Holder) {
            ((Holder) obj).beforeHideView();
        }
        //销毁老的drawing cache
        dragView.destroyDrawingCache();
        dragView.setDrawingCacheEnabled(true);
        //获取新的截图
        Bitmap bitmap = dragView.getDrawingCache();
        int offsetX = 0;
        int offsetY = 0;
        if (bitmap != null) {
            int width = bitmap.getWidth();
            int height = bitmap.getHeight();
            Matrix matrix = new Matrix();
            matrix.setScale(bitmapRect.width() * 1.0f / width, bitmapRect.height() * 1.0f / height);
            //根据要求的大小生成截图
            dragBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
            offsetX = (width - dragBitmap.getWidth()) / 2;
            offsetY = (height - dragBitmap.getHeight()) / 2;
        }
        if (dragBitmap == null) {
            Log.d(TAG, "buildDragBitmap dragBitmap == null!!!");
            return;
        }
        if (obj != null && obj instanceof Holder) {
            ((Holder) obj).hideView(indexOfChild(dragView));
        }
        dragBitmapWidth = dragBitmap.getWidth();
        dragBitmapHeight = dragBitmap.getHeight();
        //因为截图大小发生了变化,所以需要计算截图显示位置。
        dragBitmapX = dragView.getLeft() + offsetX;
        dragBitmapY = dragView.getTop() + offsetY;

        dragOffsetX = dragBitmapX;
        dragOffsetY = dragBitmapY;

        animationOffsetX = 0;
        animationOffsetY = 0;
        //播放动画显示childView到截图大小和位置的变化
        startAnimation(dragView.getLeft() + ((dragView.getWidth() - dragBitmapWidth) / 2.0f), x - (dragBitmapWidth / 2.0f), dragView.getTop() + ((dragView.getHeight() - dragBitmapHeight) / 2.0f),
                y - (dragBitmapHeight / 2.0f), dragView.getWidth() * 1.0f / dragBitmapWidth, 1.0f, null, startAnimationDuration);
    }

4.接收ACTION_MOVE事件更新childView的截图显示位置,拖动过程中判断是否更改childView的顺序。


    private void moveDragBitmap(float dx, float dy) {
        //如果正在播放动画则不处理移动事件
        if (animationIsRunning) {
            animationOffsetX += dx;
            animationOffsetY += dy;
            return;
        }
        //更新childView截图显示问题
        dragBitmapX += dx;
        dragBitmapY += dy;
        dragOffsetX += dx;
        dragOffsetY += dy;
        invalidate();
        if (dragView == null) {
            Log.d(TAG, "moveDragBitmap dragView == null");
            return;
        }
        currentTime = System.currentTimeMillis();
        //进行childView的重排序,判断是否重排序的时间间隔是300ms
        if (currentTime - lastFindIntersectTime < 300) {
            Log.d(TAG, "moveDragBitmap currentTime - lastFindIntersectTime < 300");
            return;
        }
        lastFindIntersectTime = currentTime;
        //找到截图下方重合的childView
        View childView = findViewByPosition((int) dragOffsetX + (dragBitmapWidth / 2), (int) dragOffsetY + (dragBitmapHeight / 2));
        if (childView == null) {
            Log.d(TAG, "moveDragBitmap childView == null");
            return;
        }
        //判断是否是上次获取的childView
        if (intersectView != null && intersectView == childView) {
            Log.d(TAG, "moveDragBitmap intersectView == childView");
            return;
        }
        Object object = childView.getTag();
        if (object != null && object instanceof Holder && !((Holder) object).canMove()) {
            Log.d(TAG, "moveDragBitmap child can not move");
            return;
        }
        intersectView = childView;
        //获取相交childView的index
        int intersectIndex = indexOfChild(intersectView);
        //获取被拖拽的childView的index
        int dragIndex = indexOfChild(dragView);
        if (intersectIndex < 0 || dragIndex < 0 || intersectIndex == dragIndex) {
            Log.d(TAG, "moveDragBitmap intersectIndex < 0 || dragIndex < 0 || intersectIndex == dragIndex");
            return;
        }
        //进行childView排序,同时系统的layout change animation开始播放。
        removeView(dragView);
        addView(dragView, intersectIndex);
    }

5.拖拽childView截图的描绘


    private void drawDragBitmap(Canvas canvas) {
        if (dragBitmap != null) {
            canvas.save();
            dragMatrix.reset();
            //如果正在播放拖拽开始动画,要将位移动画的数值计算在内
            if (animationIsRunning) {
                if (animationRequestFinished) {
                    dragOffsetX += animationOffsetX;
                    dragOffsetY += animationOffsetY;
                    dragMatrix.setTranslate(dragOffsetX, dragOffsetY);
                    animationIsRunning = false;
                    animationRequestFinished = false;
                    animationOffsetX = 0;
                    animationOffsetY = 0;
                } else {
                    dragMatrix.setTranslate(dragOffsetX + animationOffsetX, dragOffsetY + animationOffsetY);
                }
            } else {//没有播放动画时直接根据拖拽的位置描绘
                dragMatrix.setTranslate(dragOffsetX, dragOffsetY);
            }
            //设置缩放比例
            dragMatrix.preScale(dragScale, dragScale, dragBitmapWidth / 2.0f, dragBitmapHeight / 2.0f);
            //应用变换矩阵并描绘截图
            canvas.concat(dragMatrix);
            canvas.drawBitmap(dragBitmap, 0, 0, paint);
            canvas.restore();
        }
    }

6.拖拽结束播放结束动画


    private void resetDragBitmap() {
        if (dragView == null) {
            return;
        }
        startAnimation(dragOffsetX, dragView.getLeft() + (dragView.getWidth() / 2.0f) - (dragBitmapWidth / 2.0f), dragOffsetY,
                dragView.getTop() + (dragView.getHeight() / 2.0f) - (dragBitmapHeight / 2.0f), dragScale, dragView.getWidth() * 1.0f / dragBitmapWidth, new Runnable() {
                    @Override
                    public void run() {
                        if (dragView != null) {
                            Object obj = dragView.getTag();
                            if (obj != null && obj instanceof Holder) {
                                ((Holder) obj).showView(indexOfChild(dragView));
                            }
                        }
                        dragBitmap = null;
                        dragView = null;
                        intersectView = null;
                        notifyAllHolderChange();
                        invalidate();
                    }
                }, endAnimationDuration);
    }

四、代码

https://github.com/mjlong123123/SortLayout

我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号

​​​​​​​

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mjlong123123

你的鼓励时我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值