自定义View之九宫格Item位置*交换

前言

这个是年前就写好的demo,现在是整理后放了上来,至于我为什么要写这个,因为要写别的了:)
标题的描述可能不够准确,所以具体的样式还是看效果图。
关于这个效果其实是可以扩展成我想玩的一个游戏的,类似淘宝里的猫咪咖啡馆,等我周日补上来。

效果图

自定义View之九宫格Item位置*交换

思路 1

  1. 有两个list,一个位置固定的,另一个位置不固定的(即手指滑动哪一个就更改哪一个的坐标)。

  2. 在手势的ACTION_MOVE的时候,位置是及时更新的,但是不允许越过手机屏幕的边界。
    2.1 判断这个item是否处于已经从Square中拖动的状态,如果是,直接更新位置,如果不是则走2.2的逻辑。
    2.2 根据手指实际对应的位置找出不固定位置的列表对应的item,并及时更新位置信息。

  3. 在手势为ACTION_UP的时候,判断是否是靠近了最近的一个Square,如果是,直接让这个item进入到Square里。如果该Square内的item不为空,则交换两个item的位置。

  4. 尺寸相关的适配。

思路 2 —2019/02/18更新

我又思考了一下,可以考虑只做一个list,
但是在拖动item的坐标是一个单独的item对象,这样在拖动item值不为空的情况下,
绘制list的item的坐标的时候,跳过拖动的item,(拖动的item需要单独绘制)
因为最终的item的坐标终归会回到初始值.所以最后只需要在up的时候,替换list中的两个item的坐标即可.

代码

ExchangeCirclesView.java

public class ExchangeCirclesView extends View {
    private final int radiusCircle2 = 150;                                                         //直径
    private Paint paint;
    private float circleX = radiusCircle2, circleY = radiusCircle2;
    private int[] arrays;
    private int width;
    private List<Point> circlePointList = new ArrayList<>();                                      //圆圈的实际PointList,坐标位置可变
    private List<Point> finalSquarePointList = new ArrayList<>();                                  //固定Square区域的Point,坐标位置不可变
    private float heightPadding;
    private int height;
    private Point touchPoint;                                                                       //实际触摸的那个Point(圆圈)
    private int textColor;

    public ExchangeCirclesView(Context context) {
        this(context, null);
    }

    public ExchangeCirclesView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ExchangeCirclesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    private void initPaint() {
        int textSize = radiusCircle2 / 5;
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(1);
        paint.setTextSize(textSize * 3 / 2);
        paint.setTextAlign(Paint.Align.CENTER);
        touchPoint = new Point(-1, -1);
        textColor = Color.WHITE;
        arrays = new int[]{Color.MAGENTA, Color.GREEN, Color.YELLOW, Color.RED, Color.BLUE, Color.GRAY, Color.DKGRAY,
                ContextCompat.getColor(getContext(), R.color.colorPrimaryDark), Color.LTGRAY, Color.CYAN};
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        initList();
    }

    private void initList() {
        int xLines = 4;
        int yLines = 3;
        clear();
        int padding = (width - radiusCircle2 * yLines) / (yLines + 1);
        heightPadding = 0;
        float paceHeight = height - (padding * xLines + radiusCircle2 * 5);
        int index = 0;
        for (int j = 0; j < xLines; j++) {
            int endX = padding + radiusCircle2;
            float endY = radiusCircle2 * (yLines - 1) + j * (radiusCircle2 + padding) + (j > 0 ? paceHeight : 0);
            if (j == (xLines - 1))
                heightPadding = endY;
            for (int i = 0; i < yLines; i++) {
                if (i == 0 && j == 0) {
                    circleX = endX - radiusCircle2 / 2;
                    circleY = endY - radiusCircle2 / 2;
                }
                circlePointList.add(new Point(endX - radiusCircle2 / 2, endY - radiusCircle2 / 2, j > 0 ? index : -1, index, j == 0));
                finalSquarePointList.add(new Point(endX - radiusCircle2 / 2, endY - radiusCircle2 / 2));     //固定坐标,不可移动.
                index++;
                endX += padding + radiusCircle2;
            }
        }
    }

    public void clear() {
        circlePointList.clear();
        finalSquarePointList.clear();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                circleX = event.getRawX();
                circleY = event.getRawY() - radiusCircle2 / 2;
                //不允许越过X,Y的最大边界
                circleX = circleX < radiusCircle2 ? radiusCircle2 : circleX > width - radiusCircle2 ? width - radiusCircle2 : circleX;
                circleY = circleY < radiusCircle2 ? radiusCircle2 : circleY > heightPadding ? heightPadding : circleY;
                updateMovePointPos();
                break;
            case MotionEvent.ACTION_UP:
                updateUpPointPos();
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawFinalSquares(canvas);
        paint.setStyle(Paint.Style.FILL);
        int j = 0;
        for (int i = 0; i < circlePointList.size(); i++) {
            Point point = circlePointList.get(i);
            if (point.isEmpty)     //没有填充的内容
                continue;
            drawCircles(canvas, point, j);
            drawText(canvas, point);
            j++;
        }
    }

    /**
     * 绘制固定的正方形背景(12个)
     *
     * @param canvas
     */
    private void drawFinalSquares(Canvas canvas) {
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        for (Point point : finalSquarePointList) {
            canvas.drawRect(point.endX - radiusCircle2 / 2, point.endY - radiusCircle2 / 2,
                    point.endX + radiusCircle2 / 2, point.endY + radiusCircle2 / 2, paint);
        }
    }

    /**
     * 绘制所有的圆圈(9个)
     *
     * @param canvas
     * @param point
     * @param j
     */
    private void drawCircles(Canvas canvas, Point point, int j) {
        paint.setColor(arrays[j]);
        circleX = point.endX;
        circleY = point.endY;
        canvas.drawCircle(circleX, circleY, radiusCircle2 / 2, paint);
    }

    /**
     * 辅助文字来查看实际以及原来的位置(S:Start Position,R:Real Position)
     *
     * @param canvas
     * @param point
     */
    private void drawText(Canvas canvas, Point point) {
        paint.setColor(textColor);
        canvas.drawText("S: " + String.valueOf(point.startIndex), circleX, circleY - 10, paint);
        canvas.drawText("R: " + String.valueOf(point.realIndex), circleX, circleY + 40, paint);
    }

    /**
     * 手势为ACTION_MOVE的时候,及时更新被Move的Point的位置
     */
    private void updateMovePointPos() {
        if (touchPoint.startIndex >= 0) {                                                           //已经处于拖动中了
            int touchIndex = touchPoint.startIndex;
            Point point = circlePointList.get(touchIndex);
            updatePointListRealItem(point, touchIndex, point.realIndex);
            touchPoint = new Point(point.endX, point.endY, point.realIndex, point.startIndex, true);
            postInvalidate();
            return;
        }
        
        for (int i = 0; i < circlePointList.size(); i++) {
            Point point = circlePointList.get(i);
            if (point.realIndex < 0)                                                                //只有原来状态是有圆圈的时候,才允许交换
                continue;
            if (isCircleRange(point, false)) {
                //只有上一个圆圈结束了非正方形内的Move周期,才允许新的圆圈移动
                if (touchPoint.realIndex >= 0 && touchPoint.realIndex != point.realIndex)
                    continue;
                updatePointListRealItem(point, i, point.realIndex);
                touchPoint = new Point(point.endX, point.endY, point.realIndex, point.startIndex, true);
                postInvalidate();
                break;
            }
        }
    }

    /**
     * 手势为ACTION_UP的时候,更新被UP的Point的位置(需要检查是否是靠近Square的四周,如果是,则直接更新Point位置为固定的Square)
     */
    private void updateUpPointPos() {
        if (!circlePointList.isEmpty() && touchPoint.startIndex >= 0) {
            Point point, point2, point3;
            for (int i = 0; i < finalSquarePointList.size(); i++) {
                point = finalSquarePointList.get(i);
                if (isCircleRange(point, true)) {                                           //已经靠近Rect区域周围
                    circleX = point.endX;
                    circleY = point.endY;
                    int currentIndex = touchPoint.startIndex;
                    for (int j = 0; j < circlePointList.size(); j++) {
                        point2 = circlePointList.get(j);
                        if (point2.realIndex == i && touchPoint.realIndex >= 0) {
                            point3 = finalSquarePointList.get(touchPoint.realIndex);           //这个是point1变更前的list内的index的值
                            point2.endX = point3.endX;
                            point2.endY = point3.endY;
                            point2.realIndex = touchPoint.realIndex;
                            circlePointList.set(j, point2);
                            break;
                        }
                    }
                    updatePointListRealItem(circlePointList.get(currentIndex), currentIndex, i);     //更新实际位于list位置的Point
                    touchPoint.realIndex = -1;
                    postInvalidate();
                    break;
                }
            }
        }
    }

    /**
     *  Point是否处于可被拖动
     * @param point
     * @param isFinal
     * @return
     */
    public boolean isCircleRange(Point point, boolean isFinal) {
        int range = isFinal ? radiusCircle2 : radiusCircle2 / 2;//radiusCircle2 / 2
        return point.endX - range < circleX && circleX < point.endX + range &&
                point.endY - range < circleY && circleY < point.endY + range;
    }

    /**
     * 更新Point的位置
     *
     * @param point     要更新的Point
     * @param index     实际list的位置
     * @param realIndex 在view的显示的位置,在手指抬起的时候可能会有变更
     */
    private void updatePointListRealItem(Point point, int index, int realIndex) {
        point.endX = circleX;
        point.endY = circleY;
        point.realIndex = realIndex;
        circlePointList.set(index, point);
    }
}

Point.java

public class Point {
    public float endY;
    public float endX;
    public int realIndex = -1;    //实际的index位置
    public int startIndex = -1;  //初始的index的位置
    public boolean isEmpty = true;

    public Point(float endX, float endY) {
        this(endX, endY, -1, -1, true);
    }

    public Point(float endX, float endY, int realIndex, int startIndex, boolean isEmpty) {
        this.endY = endY;
        this.endX = endX;
        this.realIndex = realIndex;
        this.startIndex = startIndex;
        this.isEmpty = isEmpty;
    }

    @Override
    public String toString() {
        return "Point{" +
                ", realIndex=" + realIndex +
                ", startIndex=" + startIndex +
                '}';
    }
}

补充

一个扩展效果

实现方式 与上述代码类似
自定义View之九宫格Item位置*交换

最后

Github 项目地址(PS:暂时未传上去,等我夜晚传上去吧。)
有不对的地方欢迎留言指正,不胜感激.。