前语

在开发项目过程中,有一个功用是获取算法同事给我们一张图片,以及两个点,让我们在图片上制作矩形,框出算法标示的区域,感觉后边或许会有另一个需求是,给一张图片,用户给图片框出标示的区域,之后将坐标点上传后台。依据这主意测验完成算法标示框。View的运转作用如下:

Android自定义控件之算法标注框(图片上标注)

功用分析

功用能够简略说为:手指按下并划动时分制作标示框
但详细到功用完成有些细节当地需求留意
1.标示框弹性:手指拖拽标示框不同区域,能够完成标示框不同方向弹性,例如左上角能够上下左右弹性,左面则只能左右弹性。
2.标示框拖拽:拖拽框不会超出View边际,例如拖拽标示框超出图片的右下侧,则标准框右侧和下侧不动,左面和上侧移动。
3.标示框点坐标:获取标示框点坐标,一般算法需求框的左上点与右下点坐标。
4.其他功用:依据需求的扩展,例如标示框装修款式,色彩,呼应弹性标示框的区域宽度等。

代码完成

(为了看起来方便,代码完成部分讲解的代码都是截取的,View完好代码能够在文章结尾检查,也能够在文章结尾的Github地址下载源码运转)
标示框制作比较简略,在onTouchEvent()办法内记载下手指按下与移动的坐标点,依据坐标点,设置矩形的坐标值,之后在onDraw()办法内制作矩形即可,代码能够简写为下面方式。

@Override
public boolean onTouchEvent(MotionEvent event) {
    float x = event.getX();
    float y = event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = x;
            downY = y;
            downRectF.set(downX, downY, downX, downY);
            //... 其他功用代码
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            float offsetX = x - lastX;
            float offsetY = y - lastY;
            if (!boxRectConfirm) {//标示框还未承认
                boxRectF.set(Math.min(downX, x), Math.min(downY, y), Math.max(downX, x), Math.max(downY, y));
            }
            //... 其他功用省掉代码
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_UP:
            //... 其他功用省掉代码
            break;
    }
    postInvalidate();
    return true;
}
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (boxRectF != null) {
        canvas.drawRect(boxRectF, boxPaint);
        //... 其他功用省掉代码
    }
}

依据上面代码,一个依据手指移动进行制作算法标示框,就简略的完成了,下面开始弥补功用。

标示框弹性

依据标示框的运用,能够将算法框的拖拽区域分为八个部分,上,下,左,右,左上,右上,右下,左下。详细能够看下图所示:

Android自定义控件之算法标注框(图片上标注)
上图中显现了八个有色彩的矩形,当手指按下点在这八个矩形内的时分,才能够对矩形进行弹性。当手指移动的时分,能够核算xy坐标的偏移量来移动算法框矩形,例如x轴方向偏移量的核算规则为:
float offsetX = x – lastX;
偏移量x = 当前手指的x轴左面 – 手指上一次的x坐标
获取偏移量后,需求判别一下是否将偏移量作用于算法标示框的矩形上,假如满足条件将偏移量作用于算法标示框矩形,代码如下:

case MotionEvent.ACTION_MOVE:
//...其他功用代码
if (changeBoxSide) {//算法框可弹性
    if (leftRectF.contains(downRectF)) {//左区域
        boxRectF.left += offsetX;
    }else if (leftTopRectF.contains(downRectF) && !dragRectF.contains(downRectF)) {//左上区域 特别在,点击方位不坐落上一次的标示框内
        boxRectF.left += offsetX;
        boxRectF.top += offsetY;
    }//... 其他区域
}
//...其他功用代码
break;

上面代码中boxRectF是算法标示框矩形,当算法框可弹性的时分,判别手指按下矩形是否包括在左面区域中,假如包括的话,则将x轴偏移量作用于算法标示框的左面。
当手指按下区域包括在左上区域的时分,则需求将x轴与y轴的偏移量作用于算法标示框的左面和上侧, 手指点击其他方向区域时分处理逻辑与上面类似。 上面代码中changeBoxSide获取则是在手指按下的时分判别,当按下手指区域包括在呼应弹性的区域且算法标示框已经存在的时分,changeBoxSide = true,及算法框可弹性,对应代码如下:

float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        downX = x;
        downY = y;
        downRectF.set(downX, downY, downX, downY);
        if (sideBoxRectF.contains(downRectF) && !dragRectF.contains(downRectF) && boxRectConfirm) {
            changeBoxSide = true;
        }
        lastX = x;
        lastY = y;
        break;
}

上面代码中if中判别条件或许欠好理解,下面给一张图弥补下:

Android自定义控件之算法标注框(图片上标注)
上图左一绿色区域为sideBoxRectF
上图左二灰色区域为dragRectF
上图左三红色箭头指的绿色框区域则是sideBoxRectF.contains(downRectF) && !dragRectF.contains(downRectF)
if判别条件里边boxRectConfirm特点是判别算法标示框是否承认存在了,这个承认存在是指,在算法标示框没有的时分,手指按下并松开形成的算法标示框。

标示框拖拽

标示框拖拽完成相对简略,在手指按下时分判别按下区域是否在拖拽区域且算法标示框存在时将变量dragBox赋为true,之后在ACTION_MOVE事情内获取手指移动的偏移量之后作用于算法标示框。代码如下:

case MotionEvent.ACTION_DOWN:
    //...
    if (dragRectF.contains(downRectF) && boxRectConfirm){
        dragBox = true;
    }
    lastX = x;
    lastY = y;
    break;
case MotionEvent.ACTION_MOVE:
    float offsetX = x - lastX;
    float offsetY = y - lastY;
    //...
    if (dragBox) {
        boxRectF.offset(offsetX, offsetY);
    }
    //...
    lastX = x;
    lastY = y;
    break;

标示框初始化,弹性,拖拽时超出View宽高处理

关于算法标示框超出View时分,将标示框超出的边际设置为View的边际部分代码如下:

case MotionEvent.ACTION_MOVE:
    //...
    //算法标示框超出View处理
    if (boxRectF.left < 0){
        boxRectF.left = 0;
    }else if (boxRectF.left > viewWidth){
        boxRectF.left = viewWidth;
    }
    if (boxRectF.top < 0){
        boxRectF.top = 0;
    }else if (boxRectF.top > viewHeight){
        boxRectF.top = viewHeight;
    }
    if (boxRectF.right < 0){
        boxRectF.right = 0;
    }else if (boxRectF.right > viewWidth){
        boxRectF.right = viewWidth;
    }
    if (boxRectF.bottom < 0){
        boxRectF.bottom = 0;
    }else if (boxRectF.bottom > viewHeight){
        boxRectF.bottom = viewHeight;
    }
    lastX = x;
    lastY = y;
    break;

上面的代码我是放在ACTION_MOVE事情里边处理,放在这儿边目的是手指移动算法标示框的时分能实时看到作用,为了性能更好的话,能够把上面处理超出View的代码放在手指抬起的事情里边,像下面代码:

case MotionEvent.ACTION_UP:
//算法标示框超出View处理
if (boxRectF.left < 0){
    //...
}
//...其他代码

挑选放在ACTION_MOVE作用如下:

Android自定义控件之算法标注框(图片上标注)
挑选放在ACTION_UP的作用如下:
Android自定义控件之算法标注框(图片上标注)

标示框装修类型

标示框装修类型,无装修和圆形是比较好完成的,例如圆形装修,只需求获取八个方向的点,之后制作运用canvas.drawCircle()办法就能够了。例如下面代码

//左上
canvas.drawCircle(boxRectF.left, boxRectF.top, decorationLineWidth / 2, decorationPaint);
//...
//左面
canvas.drawCircle(boxRectF.left, (boxRectF.top + boxRectF.bottom) / 2, decorationLineWidth / 2, decorationPaint);
//...

严格来说上面的左上左面并不精确,例如在拖动左面框没有超越右侧时分,是正确的但一旦有下图的操作时分就不精确了。

Android自定义控件之算法标注框(图片上标注)
能够看到未松手时分,圆形是跟着左面边框移动而移动的,当手指松开时分圆形康复对应的方位,及左上,左面。出现这种状况的原因是,在制作圆形时分运用的坐标值来源于boxRectF算法标示框矩形,当弹性左面边框超越右侧时分,依据ACTION_MOVE事情里边对弹性的处理代码如下;

boxRectF.left += offsetX;

这个操作会让boxRectF.left 小于 boxRectF.right,代码是在onTouchEvent()办法结尾添加剧绘办法,每逢手指移动结束后让view重绘,此时制作左上圆运用的坐标是(boxRectF.left, boxRectF.top),由于手指右移超出本来右侧导致boxRectF.left 小于 boxRectF.right。因此制作的圆形便是右上的。
而松手会导致圆回到左上角,是由于在ACTION_UP事情内,运用下面的代码,从头对boxRectF的坐标赋值,代码如下:

float left = Math.min(boxRectF.left, boxRectF.right);
float right = Math.max(boxRectF.left, boxRectF.right);
float top = Math.min(boxRectF.top, boxRectF.bottom);
float bottom = Math.max(boxRectF.top, boxRectF.bottom);
boxRectF.set(left, top, right, bottom);

上面代码使得,boxRectF(算法标示框)左上角与右下角的点坐标能精确。那么把上面代码移动到ACTION_MOVE事情内是不是就行了呢,例如放在ACTION_MOVE结尾。但假如测验后发现作用如下图所示;

Android自定义控件之算法标注框(图片上标注)
比方从左上移动到右下时分发现,左上点与右下角点重合,且一同向右下角移动。这是由于当在ACTION_MOVE事情内,履行上面代码的时分,每逢手指右下角移动后的boxRectF.left小于boxRectF.right时,都会将两者值交流,假如手指一向向右下角移动时分,会一向交流boxRectF.left和boxRectF.right。这样就导致两者都向右下角移动。因此只能将上面代码放到ACTION_UP事情内处理。
由于canvas.drawCircle运用坐标值来源于boxRectF,而boxRectF的坐标会在手指移动时分改动,这样导致了左面框超出右侧时分圆制作方位出现问题。只有手指松开时分圆形才能康复对应的方位。这样显现不会有问题吗,其实假如将八个圆都制作出来的话,是看不出这个问题的,由于就算left和right换了方位,也不过是将两个点的圆形方位交流算了,并不会对作用有影响,究竟圆形是360度都对称的图画,八个点制作后的运转图如下:

Android自定义控件之算法标注框(图片上标注)
尽管圆形不受影响,但当那些不对称图画在制作时分运用boxRectF坐标时分就会影响了,比方标示框的装修类型为矩形时分,左上角形状制作代码如下。

leftTopPath.reset();
leftTopPath.moveTo(boxRectF.left, boxRectF.top + decorationLineLength);
leftTopPath.lineTo(boxRectF.left, boxRectF.top);
leftTopPath.lineTo(boxRectF.left + decorationLineLength, boxRectF.top);
canvas.drawPath(leftTopPath, decorationPaint);

运转作用如下;

Android自定义控件之算法标注框(图片上标注)
就能很明显看运用boxRectF(算法标示框)坐标制作矩形对弹性作用影响了。对于这种非对称形状的装修就不能直接运用boxRectF(算法标示框)坐标了,需求例外创立变量,在ACTION_MOVE事情内核算左上和右下对应的坐标并记载。例如下面是对非对称形状的装修代码修正(左上角),运用的坐标由本来的boxRectF.left等坐标,换为了moveBoxRectLeft等:

leftTopPath.reset();
leftTopPath.moveTo(moveBoxRectLeft, moveBoxRectTop + decorationLineLength);
leftTopPath.lineTo(moveBoxRectLeft, moveBoxRectTop);
leftTopPath.lineTo(moveBoxRectLeft + decorationLineLength, moveBoxRectTop);
canvas.drawPath(leftTopPath, decorationPaint);

下面是手指移动与手指抬起时对moveBoxRectLeft等四个值获取逻辑相关代码:

private float moveBoxRectLeft, moveBoxRectRight, moveBoxRectTop, moveBoxRectBottom;
@Override
public boolean onTouchEvent(MotionEvent event) {
    float x = event.getX();
    float y = event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //...
            break;
        case MotionEvent.ACTION_MOVE:
            //...
            if (decorationType == RECT || decorationType == OUT_RECT) {
                moveBoxRectLeft = Math.min(boxRectF.left, boxRectF.right);
                moveBoxRectRight = Math.max(boxRectF.left, boxRectF.right);
                moveBoxRectTop = Math.min(boxRectF.top, boxRectF.bottom);
                moveBoxRectBottom = Math.max(boxRectF.top, boxRectF.bottom);
            }
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_UP:
            float left = Math.min(boxRectF.left, boxRectF.right);
            float right = Math.max(boxRectF.left, boxRectF.right);
            float top = Math.min(boxRectF.top, boxRectF.bottom);
            float bottom = Math.max(boxRectF.top, boxRectF.bottom);
            //假如运用矩形款式能够注释下面四行代码
            moveBoxRectLeft = left;
            moveBoxRectRight = right;
            moveBoxRectTop = top;
            moveBoxRectBottom = bottom;
            //...
            break;
    }
    postInvalidate();
    return true;
}

经过上面的修正,能够使非对称的算法标示框装修能正确显现了,作用图如下:上面代码中在ACTION_UP事情内,也对moveBoxRectLeft等特点进行赋值,是由于代码在ACTION_MOVE事情内对moveBoxRectLeft赋值条件是标示框的类型为矩形和外矩形时才能赋值,假如把ACTION_UP内对moveBoxRectLeft特点赋值注释掉的话,当非矩形或外矩形装修的框在制作完首次切换到矩形或外矩形时,是不会显现矩形与外矩形的,由于ACTION_MOVE内当装修类型为非矩形或许非外矩形时不会给moveBoxRectLeft特点赋值,而又在ACTION_UP注释掉给moveBoxRectLeft特点赋值的代码,moveBoxRectLeft就一向是初始值了。
假如不运用矩形或许外矩形标示框装修的话,能够去掉这两个装修款式。代码也更精简些。

标示框点坐标获取

标示框点坐标获取相对简略,程序中运用boxRectF这个矩形保存标示框的坐标,获取标示框的left,top,right,bottom后将这四个特点别离除以对应的viewWidth或viewHeight,就能够获取在水平或高度所占百分比,之后百分比乘以图片的高度,便是坐标值了。简化的代码如下:

private int[] coordinates = new int[4];//保存标示框左上角与右下角点左面,startX,startY,endX,endY
private int imgWidth, imgHeight;
private float viewWidth, viewHeight;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    Drawable drawable = getDrawable();
    if (drawable instanceof BitmapDrawable) {
        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        Bitmap bitmap = bitmapDrawable.getBitmap();
        if (bitmap != null) {
            imgHeight = bitmap.getHeight();
            imgWidth = bitmap.getWidth();
        }
    }
    viewWidth = getWidth();
    viewHeight = getHeight();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //...
        case MotionEvent.ACTION_MOVE:
            //...
        case MotionEvent.ACTION_UP:
            //...
            //手指抬起时,核算点百分比方位
            leftPercent = boxRectF.left / viewWidth;
            topPercent = boxRectF.top / viewHeight;
            rightPercent = boxRectF.right / viewWidth;
            bottomPercent = boxRectF.bottom / viewHeight;
            coordinates[0] = (int) (imgWidth * leftPercent);
            coordinates[1] = (int) (imgHeight * topPercent);
            coordinates[2] = (int) (imgWidth * rightPercent);
            coordinates[3] = (int) (imgHeight * bottomPercent);
            boxRectConfirm = true;
            changeBoxSide = false;
            dragBox = false;
            break;
    }
    postInvalidate();
    return true;
}

这儿留意的点是获取图片宽高的话,最好是在onLayout()或许其他类似等界面加载好后的办法内获取,假如是在其他当地获取图片宽高,获取的不是图片实在的分辨率。就无法算出算法框在图片上的坐标。假如算法能够接纳百分比方式数据的话,也不需求考虑这一点了。

View完好代码

public class BoundingBoxImageView extends androidx.appcompat.widget.AppCompatImageView {
    private final int NONE = 0, CIRCLE = 1, RECT = 2, OUT_RECT = 3;
    private Paint boxPaint, broadsidePaint, cornerPaint, decorationPaint;
    private float downX = -1f, downY = -1f;//手指按下
    private float lastX = -1f, lastY = -1f;//上一个xy
    private RectF boxRectF = new RectF();//算法框
    private RectF lastBoxRect = new RectF();//上次手指抬起时算法框
    private RectF downRectF = new RectF();//手指按下点Rect
    private RectF sideBoxRectF = new RectF();//用于拖拽算法框边际的矩形
    private RectF leftRectF = new RectF(), leftTopRectF = new RectF(), topRectF = new RectF(), rightTopRectF = new RectF(), rightRectF = new RectF(), rightBottomRectF = new RectF(), bottomRectF = new RectF(), leftBottomRectF = new RectF();
    private RectF dragRectF = new RectF();//拖拽区域Rect
    private boolean boxRectConfirm = false;//标示框承认 当第一次手指松开时分该值为true,承认标示框已经有了
    private boolean changeBoxSide = false;//标示框形状改动
    private boolean showDragWidth = false;//展现拖拽区域
    private boolean dragBox = false;
    private float boxLineWidth = 5;//标示框线宽
    private float dragWidth = 30;//拖拽区域宽度
    private float decorationLineLength = 80;//装修线长度
    private float decorationLineWidth = 10;//装修线宽度
    private int imgWidth, imgHeight;
    private float viewWidth, viewHeight;
    private float leftPercent, topPercent, rightPercent, bottomPercent;
    private int[] coordinates = new int[4];//保存标示框左上角与右下角点左面,startX,startY,endX,endY
    private float moveBoxRectLeft, moveBoxRectRight, moveBoxRectTop, moveBoxRectBottom;
    private Point startPoint = new Point(), endPoint = new Point();
    private int boxColor = Color.parseColor("#99129823");
    private int decorationColor = Color.parseColor("#5DA9FF");
    private int decorationType = NONE;
    private Path leftTopPath = new Path(), rightTopPath = new Path(), rightBottomPath = new Path(), leftBottomPath = new Path();
    public BoundingBoxImageView(Context context) {
        super(context);
    }
    public BoundingBoxImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }
    public BoundingBoxImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }
    private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BoundingBoxImageView);
        showDragWidth = typedArray.getBoolean(R.styleable.BoundingBoxImageView_show_drag_width, showDragWidth);
        dragWidth = typedArray.getDimension(R.styleable.BoundingBoxImageView_drag_width, dragWidth);
        boxLineWidth = typedArray.getDimension(R.styleable.BoundingBoxImageView_box_line_width, boxLineWidth);
        boxColor = typedArray.getColor(R.styleable.BoundingBoxImageView_box_color, boxColor);
        decorationLineLength = typedArray.getDimension(R.styleable.BoundingBoxImageView_decoration_line_length, decorationLineLength);
        decorationLineWidth = typedArray.getDimension(R.styleable.BoundingBoxImageView_decoration_line_width, decorationLineWidth);
        decorationColor = typedArray.getColor(R.styleable.BoundingBoxImageView_decoration_color, decorationColor);
        decorationType = typedArray.getInt(R.styleable.BoundingBoxImageView_decoration_type, decorationType);
        typedArray.recycle();//开释,避免内存泄露
        boxPaint = new Paint();
        boxPaint.setColor(boxColor);
        boxPaint.setStyle(Paint.Style.STROKE);
        boxPaint.setStrokeWidth(boxLineWidth);
        decorationPaint = new Paint();
        decorationPaint.setColor(decorationColor);
        decorationPaint.setStyle(Paint.Style.STROKE);
        decorationPaint.setStrokeWidth(decorationLineWidth);
        decorationPaint.setAntiAlias(true);//敞开抗锯齿
        broadsidePaint = new Paint();
        broadsidePaint.setColor(Color.parseColor("#99345212"));
        broadsidePaint.setStrokeWidth(dragWidth);
        cornerPaint = new Paint();
        cornerPaint.setColor(Color.parseColor("#99876534"));
        cornerPaint.setStrokeWidth(dragWidth);
    }
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Drawable drawable = getDrawable();
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            Bitmap bitmap = bitmapDrawable.getBitmap();
            if (bitmap != null) {
                imgHeight = bitmap.getHeight();
                imgWidth = bitmap.getWidth();
            }
        }
        viewWidth = getWidth();
        viewHeight = getHeight();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (boxRectF != null) {
            canvas.drawRect(boxRectF, boxPaint);
            //边框装修
            drawDecoration(canvas, decorationType);
            //开发测试运用
            if (showDragWidth) {
                canvas.drawRect(leftRectF, broadsidePaint);
                canvas.drawRect(topRectF, broadsidePaint);
                canvas.drawRect(rightRectF, broadsidePaint);
                canvas.drawRect(bottomRectF, broadsidePaint);
                canvas.drawRect(leftTopRectF, cornerPaint);
                canvas.drawRect(rightTopRectF, cornerPaint);
                canvas.drawRect(leftBottomRectF, cornerPaint);
                canvas.drawRect(rightBottomRectF, cornerPaint);
            }
        }
    }
    private void drawDecoration(Canvas canvas, int type) {
        //矩形承认后才制作装修,好处是,避免矩形太小时分导致装修挤在一同
        if (boxRectConfirm) {
            if (type == NONE) {
                return;
            } else if (type == RECT) {
                //左上右下
                canvas.drawLine(moveBoxRectLeft, (moveBoxRectTop + moveBoxRectBottom) / 2 - decorationLineLength / 2, moveBoxRectLeft, (moveBoxRectTop + moveBoxRectBottom) / 2 + decorationLineLength / 2, decorationPaint);
                canvas.drawLine((moveBoxRectLeft + moveBoxRectRight) / 2 - decorationLineLength / 2, moveBoxRectTop, (moveBoxRectLeft + moveBoxRectRight) / 2 + decorationLineLength / 2, moveBoxRectTop, decorationPaint);
                canvas.drawLine(moveBoxRectRight, (moveBoxRectTop + moveBoxRectBottom) / 2 - decorationLineLength / 2, moveBoxRectRight, (moveBoxRectTop + moveBoxRectBottom) / 2 + decorationLineLength / 2, decorationPaint);
                canvas.drawLine((moveBoxRectLeft + moveBoxRectRight) / 2 - decorationLineLength / 2, moveBoxRectBottom, (moveBoxRectLeft + moveBoxRectRight) / 2 + decorationLineLength / 2, moveBoxRectBottom, decorationPaint);
                leftTopPath.reset();
                leftTopPath.moveTo(moveBoxRectLeft, moveBoxRectTop + decorationLineLength);
                leftTopPath.lineTo(moveBoxRectLeft, moveBoxRectTop);
                leftTopPath.lineTo(moveBoxRectLeft + decorationLineLength, moveBoxRectTop);
                canvas.drawPath(leftTopPath, decorationPaint);
                rightTopPath.reset();
                rightTopPath.moveTo(moveBoxRectRight - decorationLineLength, moveBoxRectTop);
                rightTopPath.lineTo(moveBoxRectRight, moveBoxRectTop);
                rightTopPath.lineTo(moveBoxRectRight, moveBoxRectTop + decorationLineLength);
                canvas.drawPath(rightTopPath, decorationPaint);
                rightBottomPath.reset();
                rightBottomPath.moveTo(moveBoxRectRight, moveBoxRectBottom - decorationLineLength);
                rightBottomPath.lineTo(moveBoxRectRight, moveBoxRectBottom);
                rightBottomPath.lineTo(moveBoxRectRight - decorationLineLength, moveBoxRectBottom);
                canvas.drawPath(rightBottomPath, decorationPaint);
                leftBottomPath.reset();
                leftBottomPath.moveTo(moveBoxRectLeft + decorationLineLength, moveBoxRectBottom);
                leftBottomPath.lineTo(moveBoxRectLeft, moveBoxRectBottom);
                leftBottomPath.lineTo(moveBoxRectLeft, moveBoxRectBottom - decorationLineLength);
                canvas.drawPath(leftBottomPath, decorationPaint);
            } else if (type == CIRCLE) {
                //角装修 左上 右上 右下 左下
                canvas.drawCircle(boxRectF.left, boxRectF.top, decorationLineWidth / 2, decorationPaint);
                canvas.drawCircle(boxRectF.right, boxRectF.top, decorationLineWidth / 2, decorationPaint);
                canvas.drawCircle(boxRectF.right, boxRectF.bottom, decorationLineWidth / 2, decorationPaint);
                canvas.drawCircle(boxRectF.left, boxRectF.bottom, decorationLineWidth / 2, decorationPaint);
                //边装修 左上右下
                canvas.drawCircle(boxRectF.left, (boxRectF.top + boxRectF.bottom) / 2, decorationLineWidth / 2, decorationPaint);
                canvas.drawCircle((boxRectF.left + boxRectF.right) / 2, boxRectF.top, decorationLineWidth / 2, decorationPaint);
                canvas.drawCircle(boxRectF.right, (boxRectF.top + boxRectF.bottom) / 2, decorationLineWidth / 2, decorationPaint);
                canvas.drawCircle((boxRectF.left + boxRectF.right) / 2, boxRectF.bottom, decorationLineWidth / 2, decorationPaint);
            } else if (type == OUT_RECT) {
                canvas.drawLine(moveBoxRectLeft - decorationLineWidth / 2, (moveBoxRectTop + moveBoxRectBottom) / 2 - decorationLineLength / 2, moveBoxRectLeft - decorationLineWidth / 2, (moveBoxRectTop + moveBoxRectBottom) / 2 + decorationLineLength / 2, decorationPaint);
                canvas.drawLine((moveBoxRectLeft + moveBoxRectRight) / 2 - decorationLineLength / 2, moveBoxRectTop - decorationLineWidth / 2, (moveBoxRectLeft + moveBoxRectRight) / 2 + decorationLineLength / 2, moveBoxRectTop - decorationLineWidth / 2, decorationPaint);
                canvas.drawLine(moveBoxRectRight + decorationLineWidth / 2, (moveBoxRectTop + moveBoxRectBottom) / 2 - decorationLineLength / 2, moveBoxRectRight + decorationLineWidth / 2, (moveBoxRectTop + moveBoxRectBottom) / 2 + decorationLineLength / 2, decorationPaint);
                canvas.drawLine((moveBoxRectLeft + moveBoxRectRight) / 2 - decorationLineLength / 2, moveBoxRectBottom + decorationLineWidth / 2, (moveBoxRectLeft + moveBoxRectRight) / 2 + decorationLineLength / 2, moveBoxRectBottom + decorationLineWidth / 2, decorationPaint);
                leftTopPath.reset();
                leftTopPath.moveTo(moveBoxRectLeft - decorationLineWidth / 2, moveBoxRectTop + decorationLineLength);
                leftTopPath.lineTo(moveBoxRectLeft - decorationLineWidth / 2, moveBoxRectTop - decorationLineWidth / 2);
                leftTopPath.lineTo(moveBoxRectLeft + decorationLineLength, moveBoxRectTop - decorationLineWidth / 2);
                canvas.drawPath(leftTopPath, decorationPaint);
                rightTopPath.reset();
                rightTopPath.moveTo(moveBoxRectRight - decorationLineLength, moveBoxRectTop - decorationLineWidth / 2);
                rightTopPath.lineTo(moveBoxRectRight + decorationLineWidth / 2, moveBoxRectTop - decorationLineWidth / 2);
                rightTopPath.lineTo(moveBoxRectRight + decorationLineWidth / 2, moveBoxRectTop + decorationLineLength);
                canvas.drawPath(rightTopPath, decorationPaint);
                rightBottomPath.reset();
                rightBottomPath.moveTo(moveBoxRectRight + decorationLineWidth / 2, moveBoxRectBottom - decorationLineLength);
                rightBottomPath.lineTo(moveBoxRectRight + decorationLineWidth / 2, moveBoxRectBottom + decorationLineWidth / 2);
                rightBottomPath.lineTo(moveBoxRectRight - decorationLineLength, moveBoxRectBottom + decorationLineWidth / 2);
                canvas.drawPath(rightBottomPath, decorationPaint);
                leftBottomPath.reset();
                leftBottomPath.moveTo(moveBoxRectLeft + decorationLineLength, moveBoxRectBottom + decorationLineWidth / 2);
                leftBottomPath.lineTo(moveBoxRectLeft - decorationLineWidth / 2, moveBoxRectBottom + decorationLineWidth / 2);
                leftBottomPath.lineTo(moveBoxRectLeft - decorationLineWidth / 2, moveBoxRectBottom - decorationLineLength);
                canvas.drawPath(leftBottomPath, decorationPaint);
            }
        }
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //内部阻拦 处理滑动冲突
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
        }
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = x;
                downY = y;
                downRectF.set(downX, downY, downX, downY);
                //表明能够改动矩形形状 得是矩形承认形状后
                if (sideBoxRectF.contains(downRectF) && !dragRectF.contains(downRectF) && boxRectConfirm) {
                    changeBoxSide = true;
                }
                if (dragRectF.contains(downRectF) && boxRectConfirm) {
                    dragBox = true;
                }
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float offsetX = x - lastX;
                float offsetY = y - lastY;
                if (!boxRectConfirm) {//矩形承认 第一次松手后 承认矩形 由于new Rect()的点都是0,必须记载下downX和后边手指移动的X
                    boxRectF.set(Math.min(downX, x), Math.min(downY, y), Math.max(downX, x), Math.max(downY, y));
                }
                //拖拽标示框 范围超出View时处理  改动框时分不触发下面条件
                if (dragBox) {//手指按下点,在上一个lastBoxRectF矩形内,才考虑履行下面的代码   由于boxRectF会变的,有或许导致变着变着boxRectF包括了downRectF
                    boxRectF.offset(offsetX, offsetY);
                }
                //拖动边际改动View巨细和形状
                //当左面框小到必定程度就不呼应了
                if (changeBoxSide) {
                    //感觉需求对 矩形上下左右区域做下区别
                    if (leftRectF.contains(downRectF)) {//左区域
                        boxRectF.left += offsetX;
                    } else if (leftTopRectF.contains(downRectF) && !dragRectF.contains(downRectF)) {//左上区域 特别在,点击方位不坐落上一次的标示框内
                        boxRectF.left += offsetX;
                        boxRectF.top += offsetY;
                    } else if (topRectF.contains(downRectF)) {//上区域
                        boxRectF.top += offsetY;
                    } else if (rightTopRectF.contains(downRectF) && !dragRectF.contains(downRectF)) {
                        boxRectF.top += offsetY;
                        boxRectF.right += offsetX;
                    } else if (rightRectF.contains(downRectF)) {//右区域
                        boxRectF.right += offsetX;
                    } else if (rightBottomRectF.contains(downRectF) && !dragRectF.contains(downRectF)) {
                        boxRectF.right += offsetX;
                        boxRectF.bottom += offsetY;
                    } else if (bottomRectF.contains(downRectF)) {//下区域
                        boxRectF.bottom += offsetY;
                    } else if (leftBottomRectF.contains(downRectF) && !dragRectF.contains(downRectF)) {
                        boxRectF.left += offsetX;
                        boxRectF.bottom += offsetY;
                    }
                }
                //承认矩形 与 改动矩形巨细都需求判别修正后的矩形是否超出View
                if (boxRectF.left < 0) {
                    boxRectF.left = 0;
                } else if (boxRectF.left > viewWidth) {
                    boxRectF.left = viewWidth;
                }
                if (boxRectF.top < 0) {
                    boxRectF.top = 0;
                } else if (boxRectF.top > viewHeight) {
                    boxRectF.top = viewHeight;
                }
                if (boxRectF.right < 0) {
                    boxRectF.right = 0;
                } else if (boxRectF.right > viewWidth) {
                    boxRectF.right = viewWidth;
                }
                if (boxRectF.bottom < 0) {
                    boxRectF.bottom = 0;
                } else if (boxRectF.bottom > viewHeight) {
                    boxRectF.bottom = viewHeight;
                }
                if (decorationType == RECT || decorationType == OUT_RECT) {
                    moveBoxRectLeft = Math.min(boxRectF.left, boxRectF.right);
                    moveBoxRectRight = Math.max(boxRectF.left, boxRectF.right);
                    moveBoxRectTop = Math.min(boxRectF.top, boxRectF.bottom);
                    moveBoxRectBottom = Math.max(boxRectF.top, boxRectF.bottom);
                }
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_UP:
                float left = Math.min(boxRectF.left, boxRectF.right);
                float right = Math.max(boxRectF.left, boxRectF.right);
                float top = Math.min(boxRectF.top, boxRectF.bottom);
                float bottom = Math.max(boxRectF.top, boxRectF.bottom);
                //假如运用矩形款式能够注释下面四行代码
                moveBoxRectLeft = left;
                moveBoxRectRight = right;
                moveBoxRectTop = top;
                moveBoxRectBottom = bottom;
                boxRectF.set(left, top, right, bottom);
                lastBoxRect.set(boxRectF);
                dragRectF.set(boxRectF.left + dragWidth, boxRectF.top + dragWidth, boxRectF.right - dragWidth, boxRectF.bottom - dragWidth);
                sideBoxRectF.set(boxRectF.left - dragWidth, boxRectF.top - dragWidth, boxRectF.right + dragWidth, boxRectF.bottom + dragWidth);
                leftRectF.set(boxRectF.left - dragWidth, boxRectF.top + dragWidth, boxRectF.left + dragWidth, boxRectF.bottom - dragWidth);
                leftTopRectF.set(boxRectF.left - dragWidth, boxRectF.top - dragWidth, boxRectF.left + dragWidth, boxRectF.top + dragWidth);
                topRectF.set(boxRectF.left + dragWidth, boxRectF.top - dragWidth, boxRectF.right - dragWidth, boxRectF.top + dragWidth);
                rightTopRectF.set(boxRectF.right - dragWidth, boxRectF.top - dragWidth, boxRectF.right + dragWidth, boxRectF.top + dragWidth);
                rightRectF.set(boxRectF.right - dragWidth, boxRectF.top + dragWidth, boxRectF.right + dragWidth, boxRectF.bottom - dragWidth);
                rightBottomRectF.set(boxRectF.right - dragWidth, boxRectF.bottom - dragWidth, boxRectF.right + dragWidth, boxRectF.bottom + dragWidth);
                bottomRectF.set(boxRectF.left + dragWidth, boxRectF.bottom - dragWidth, boxRectF.right - dragWidth, boxRectF.bottom + dragWidth);
                leftBottomRectF.set(boxRectF.left - dragWidth, boxRectF.bottom - dragWidth, boxRectF.left + dragWidth, boxRectF.bottom + dragWidth);
                //手指抬起时,核算点百分比方位
                leftPercent = boxRectF.left / viewWidth;
                topPercent = boxRectF.top / viewHeight;
                rightPercent = boxRectF.right / viewWidth;
                bottomPercent = boxRectF.bottom / viewHeight;
                coordinates[0] = (int) (imgWidth * leftPercent);
                coordinates[1] = (int) (imgHeight * topPercent);
                coordinates[2] = (int) (imgWidth * rightPercent);
                coordinates[3] = (int) (imgHeight * bottomPercent);
                boxRectConfirm = true;
                changeBoxSide = false;
                dragBox = false;
                break;
        }
        postInvalidate();
        return true;
    }
    public int[] getCoordinates() {
        return coordinates;
    }
    public void clearBox() {
        boxRectF.set(0, 0, 0, 0);
        Arrays.fill(coordinates, 0);
        boxRectConfirm = false;
        postInvalidate();
    }
    public void setDecorationType(int decorationType) {
        this.decorationType = decorationType;
        postInvalidate();
    }
}

总结

实践开发的时分发现,标示框装修运用圆形是最省劲的,由于标示框弹性是会使左面超越右侧的,运用对称的图画装修,在移动的时分看不出问题。
而框装修为矩形的一般用于截图运用,由于截图一般不会让框左面弹性超越右侧这种状况,而且框的巨细有时还会规定死。就不需求考虑超出后导致装修形状显现错误的问题了。
GitHub地址:github.com/SmallCrispy…