安卓图片裁剪——使用自定义View

前言

在图片操作中裁剪最为常见,安卓中常用的裁剪方式是通过调用 Bitmap.createBitmap(@NonNull Bitmap source, int x, int y, int width, int height) 等实现的,本文所展示的View便是以此为核心设计。

设计思路

在一个图片裁剪的过程中,我们可以看到其主要由以下两部分组成:

  1. 裁剪区域(裁剪框)
  2. 图片区域(裁剪目标)

因此,我们可以将其抽象为两个矩形,裁剪结果即两个矩形取交集,即:

  1. 代表裁剪区域的矩形(下称cropRectF)
  2. 代表图片区域的矩形(下称picRectF)

下面是对这两个矩形的几种设计思路:

对cropRectF:

  • 使用固定尺寸比例设置cropRectF的大小,简单易行,且方便裁剪出固定比例的图片
  • 通过拖动边界自由变化cropRectF,这可以通过判断触点坐标是否在其边界或边界附近来判断拖动,从而改变cropRectF的大小

最终鉴于简单选择了前者,同时也加入了通过单指触摸拖动裁剪框,以缓解不能修正裁剪位置的缺陷;

对picRectF则添加了常用手势操作,双指平移图片和缩放图片,由此牵扯出两种方案:

  • 可随意移动缩放图片,裁剪时通过取交集的方式获取结果,适用于裁剪结果可以包含透明区域
  • 同样可随意移动缩放图片,但其picRectF应当始终包含cropRectF,则裁剪结果取cropRectF的全集即可,适用于裁剪结果不应包含透明区域

最终采用了第二种方案,同时出于实践的目的也尝试了下在第一种方案中如何获取裁剪结果,部分代码如下:

    public Bitmap getCroppingResult() {
        if (picture != null) {// picture为Bitmap对象,即裁剪目标
            // 构建裁剪框对应的区域
            Region resultRegion = new Region((int) cropRectF.left, (int) cropRectF.top, (int) cropRectF.right, (int) cropRectF.bottom);
            // 与图片区域相交
            resultRegion.op((int) picRectF.left, (int) picRectF.top, (int) picRectF.right, (int) picRectF.bottom, Region.Op.INTERSECT);
            Rect resultRect = resultRegion.getBounds();// 获取取交集后相交区域的矩形
            if (!resultRect.isEmpty()) {// 图片与裁剪框有相交区域
                float cropWidth = cropRectF.width();
                float cropHeight = cropRectF.height();
                float picWidth = picRectF.width();
                float picHeight = picRectF.height();
                int pictureWidth = picture.getWidth();
                int pictureHeight = picture.getHeight();
                // 计算相交区域的左上角坐标分别在裁剪框和图片中的位置比例
                // 因该相交区域必为裁剪区域或图片的一部分,所以下面4个比例值一定属于 [0, 1]
                // 使用PointF仅仅只是为了同时存储两个维度的比例
                PointF scaleAtCrop = new PointF((resultRect.left - cropRectF.left) / cropWidth, (resultRect.top - cropRectF.top) / cropHeight);
                PointF scaleAtPic = new PointF((resultRect.left - picRectF.left) / picWidth, (resultRect.top - picRectF.top) / picHeight);
                float unitWidth = pictureWidth / picWidth;
                float unitHeight = pictureHeight / picHeight;
                // 计算裁剪框的宽高在与picture同密度下的宽高,即裁剪结果的宽高
                int resultWidth = (int) (cropWidth * unitWidth);
                int resultHeight = (int) (cropHeight * unitHeight);
                // 计算相交区域的宽高在与picture同密度下的宽高
                int picPartWidth = (int) (resultRect.width() * unitWidth);
                int picPartHeight = (int) (resultRect.height() * unitHeight);
                Bitmap result = Bitmap.createBitmap(resultWidth, resultHeight, picture.getConfig());
                Canvas canvas = new Canvas(result);// 目的为将相交区域图片画在指定位置
                BitmapDrawable picPart = new BitmapDrawable(context.getResources(), Bitmap.createBitmap(
                        picture,
                        (int) (pictureWidth * scaleAtPic.x),
                        (int) (pictureHeight * scaleAtPic.y),
                        picPartWidth,
                        picPartHeight
                ));
                // 计算相交部分图片绘制在结果中的位置
                int drawLeft = (int) (resultWidth * scaleAtCrop.x);
                int drawTop = (int) (resultHeight * scaleAtCrop.y);
                picPart.setBounds(drawLeft, drawTop, drawLeft + picPartWidth, drawTop + picPartHeight);
                picPart.draw(canvas);// 将相交部分图片绘制在结果中
                return result;
            }
        }
        return null;
    }

回归正文,最终选取的方案确定为:

  • cropRectF使用固定比例
  • cropRectF可通过单指拖动以平移变化
  • picRectF可通过双指操作以平移、缩放
  • picRectF必须始终包含cropRectF

但在继续之前,我们应当先确定两个矩形的大小。
picRectF必须始终包含cropRectF,所以先规划cropRectF的大小。出于视觉上的考量,最终我选择使用View宽或是高的2/3作为cropRectF的宽或是高。但在直接设置之前,有两个问题应当先行解决:

  • View是较宽还是较高?
  • 裁剪区域是较宽还是较高?

不难想到:

  • View较宽 && 裁剪区域较高 ⇒ 使用View的高做基准(即以View高的2/3作为cropRectF的高),cropRectF的宽必定不大于View的宽
  • View较高 && 裁剪区域较宽 ⇒ 使用View的宽做基准(即以View宽的2/3作为cropRectF的宽),cropRectF的高必定不大于View的高

但若View与裁剪区域都较宽或是都较高时,便不能简单的确定cropRectF的宽高了。比如说,如果都较宽,我们可以先以View的宽为基准,在按裁剪区域比例计算出cropRectF的高后,应当先比较它是否要比View的高大,如若确实如此,那我们则应当改使用View的高为基准了,不过这时我们可以确定,计算出的cropRectF的宽必定不大于View的宽。同理,我们可以得出它们都较高时的值了。

确定了cropRectF的值后,便可确定picRectF的值了。实际上,这与上述相似,它们都是包含关系,把cropRectF类比为View,把picRectF类比为cropRectF即可,便不赘述。

然后接下来便是如何通过触摸控制cropRectF和picRectF的位置或大小,此时就轮到View类中的onTouchEvent(MotionEvent event)方法出场了。

顾名思义,这个方法的处理对象正是触摸事件,它是一个触摸事件的消费者,其返回值为布朗值,true表示其已经消费了该触摸事件(MotionEvent event),反之则表示它没有消费该事件。在这里,我所用到的触摸事件有以下三种:

  • ACTION_DOWN:当第一个手指按下屏幕时
  • ACTION_POINTER_DOWN:除第一个手指以外,如果有其它的手指按下屏幕时
  • ACTION_MOVE:当任意手指在屏幕上移动或多个手指同时在屏幕上移动时

为判断单指操控裁剪框及双指操控图片,我用两个boolean变量来确定在ACTION_MOVE事件中操控的对象:isMovingCrop(操控裁剪框)isMovingPic(操控图片)

当ACTION_DOWN事件发生时,其必然不会是要操控图片,此时令isMovingPic = false,是否为操控裁剪框则取决于其触点是否在cropRectF的范围内,因此令isMovingCrop = cropRectF.contains(event.getX(), event.getY()),与此同时,用了一个PointF对象记录下了此时的坐标。

case MotionEvent.ACTION_DOWN:
    isMovingCrop = cropRectF.contains(x0, y0);
    isMovingPic = false;
    fingerPoint0.set(x0, y0);
    break;

当ACTION_POINTER_DOWN事件发生时,其必然不会是要操控裁剪框,此时令isMovingCrop = false,是否为操控图片则取决于此时是否为双指,且若为双指,则其中是否至少有一个触点在picRectF的范围内,因此令isMovingPic = event.getPointerCount() == 2 && (picRectF.contains(fingerPoint0.x, fingerPoint0.y) || picRectF.contains(event.getX(1), event.getY(1))),若确为操控图片,则记录下此时第二个触点的坐标,然后计算双指的中心点坐标及双指的距离。

case MotionEvent.ACTION_POINTER_DOWN:
    isMovingCrop = false;
    if (isMovingPic = event.getPointerCount() == 2 // 双指操作
            // 至少有一点在图片范围内
            && (picRectF.contains(fingerPoint0.x, fingerPoint0.y) || picRectF.contains(x1, y1))) {
        fingerPoint1.set(x1, y1);
        updateCenterPoint();
        lastDistance = computeDistance();
    }
    break;

因此,为避免发生其它触摸事件导致isMovingCrop或isMovingPic标记错误,在默认分支中将其全部归为false。

default:
    isMovingCrop = false;
    isMovingPic = false;
    break;

最后,我们所希望的触摸操控便在ACTION_MOVE事件中执行,在发生ACTION_MOVE事件时:

当操控裁剪框时,我们只需要计算当前事件发生坐标相对于ACTION_DOWN事件发生时的坐标的偏移量,然后平移cropRectF,同时更新ACTION_DOWN事件坐标为当前事件坐标(以便下一个ACTION_MOVE事件的坐标偏移量计算是以相对于本次坐标计算的)即可,但需要注意的是,cropRectF的平移有以下两个限制:

  • 必须含于picRectF(请注意,picRectF并非要含于View)
  • 必须含于View

对此,只需在偏移cropRectF之前,判断它在偏移后是否会超出限制,然后对偏移量进行修正即可。

if (isMovingCrop) {
    float xDiff = x0 - fingerPoint0.x;
    float yDiff = y0 - fingerPoint0.y;
    fingerPoint0.set(x0, y0);
    // 限制不能滑出图片的范围
    if (cropRectF.left + xDiff < picRectF.left) {// 左移
        xDiff = picRectF.left - cropRectF.left;
    } else if (cropRectF.right + xDiff > picRectF.right) {// 右移
        xDiff = picRectF.right - cropRectF.right;
    }
    if (cropRectF.top + yDiff < picRectF.top) {// 上移
        yDiff = picRectF.top - cropRectF.top;
    } else if (cropRectF.bottom + yDiff > picRectF.bottom) {// 下移
        yDiff = picRectF.bottom - cropRectF.bottom;
    }
    // 限制不能滑出整个视图的范围
    if (cropRectF.left + xDiff < 0) {// 左移
        xDiff = - cropRectF.left;
    } else if (cropRectF.right + xDiff > getWidth()) {// 右移
        xDiff = getWidth() - cropRectF.right;
    }
    if (cropRectF.top + yDiff < 0) {// 上移
        yDiff = - cropRectF.top;
    } else if (cropRectF.bottom + yDiff > getHeight()) {// 下移
        yDiff = getHeight() - cropRectF.bottom;
    }
    cropRectF.offset(xDiff, yDiff);
    refresh();
}

当操控图片时,其目的可能为以下三种之一:

  • 平移图片 ⇒ 双指距离基本保持不变
  • 放大图片 ⇒ 双指距离增大
  • 缩小图片 ⇒ 双指距离减小

因此,我们可以通过判断双指距离变化的方式来做出相应的操作:

  • 当双指距离相比上一次变化不大时(注意应当使用绝对值),将其视为无变化,此时即为平行移动,通过对中心点的偏移量计算,从而得出picRectF的偏移量。同样,我们应当注意,picRectF必须始终包含cropRectF,处理方式与平移裁剪框类似。

  • 当双指距离相比上一次增大或减小时,此时即为缩放图片。对图片的缩放我们应当确定缩放的中心点,且其应当在图片上。因此我们需要对双指中心点进行处理(其可能不在图片上)。例如其x轴坐标,若大于picRectF.right,则让它等于picRectF.right即可,若小于picRectF.left,则让它等于picRectF,left即可,对其y轴坐标的处理类似。
    确定缩放中心后,让缩放中心的坐标保持不变,而只变化坐标点各方向两边(x轴方向及y轴方向)的长度,即可达到图片按缩放中心缩放的效果。对此,我们可以先计算缩放中心在图片各方向上的位置比例,然后计算缩放后图片的宽高,让缩放点仍处于图片缩放后同样的位置比例,同时保存坐标不变即可。同样,我们需要注意以下两点:

    • 缩放后图片的宽高应至少比cropRectF的宽高大,否则不可能包含cropRectF
    • picRectF必须始终包含cropRectF

    对于第一条,我们可以在图片的宽高缩放前,先判断缩放后的宽高是否要不小于cropRectF的宽高,然后对缩放比例修正即可。
    对于第二条,则可以对picRectF添加偏移量修正即可。

if (isMovingPic) {
    double distance = computeDistance();
    fingerPoint0.set(x0, y0);
    fingerPoint1.set(x1, y1);
    if (Math.abs(distance - lastDistance) <= 20/*临界值*/) {// 平行移动
        // 考虑到滑动过程中的轻微抖动,因此设定临界值,
        // 两点距离的变动值在该值以内均视为平行移动
        float centerX = centerPoint.x;
        float centerY = centerPoint.y;
        updateCenterPoint();
        float xDiff = centerPoint.x - centerX;
        float yDiff = centerPoint.y - centerY;
        // 限制必须包含裁剪区域
        if (picRectF.left + xDiff > cropRectF.left) {// 右移
            xDiff = cropRectF.left - picRectF.left;
        } else if (picRectF.right + xDiff < cropRectF.right) {// 左移
            xDiff = picRectF.right - cropRectF.right;
        }
        if (picRectF.top + yDiff > cropRectF.top) {// 下移
            yDiff = cropRectF.top - picRectF.top;
        } else if (picRectF.bottom + yDiff < cropRectF.bottom) {// 上移
            yDiff = picRectF.bottom - cropRectF.bottom;
        }
        picRectF.offset(xDiff, yDiff);
    } else {// 缩放
        // 将双指中心点转化为缩放中心点
        float zoomCenterX = Math.max(picRectF.left, Math.min(centerPoint.x, picRectF.right));
        float zoomCenterY = Math.max(picRectF.top, Math.min(centerPoint.y, picRectF.bottom));
        updateCenterPoint();
        float picWidth = picRectF.width();
        float picHeight = picRectF.height();
        // 计算缩放中心点在图片中x、y方向上的位置比例
        float xScale = (zoomCenterX - picRectF.left) / picWidth;// 缩放中心x方向位置比例
        float yScale = (zoomCenterY - picRectF.top) / picHeight;// 缩放中心y方向位置比例
        float zoomScale = (float) (distance / lastDistance);// 图片的缩放比例
        // 限制至少要包含裁剪区域
        float newPicWidth = Math.max(picWidth * zoomScale, cropRectF.width());// 缩放后图片的宽度
        float newPicHeight = newPicWidth * picHeight / picWidth;// 缩放后图片的高度
        if (newPicHeight < cropRectF.height()) {// 需要放大,放大后的图片宽度一定大于裁剪区域的宽度
            newPicHeight *= (cropRectF.height() / newPicHeight);
            newPicWidth = newPicHeight * picWidth / picHeight;
        }
        // 根据缩放中心的位置比例计算图片的矩阵位置
        float newPicLeft = zoomCenterX - newPicWidth * xScale;
        float newPicTop = zoomCenterY - newPicHeight * yScale;
        picRectF.set(newPicLeft, newPicTop, newPicLeft + newPicWidth, newPicTop + newPicHeight);
        // 校正图片位置
        // 此时图片的宽高一定大于裁剪区域的宽高
        float xDiff = 0.0f;
        float yDiff = 0.0f;
        if (picRectF.left > cropRectF.left) {
            xDiff = cropRectF.left - picRectF.left;
        } else if (picRectF.right < cropRectF.right) {
            xDiff = cropRectF.right - picRectF.right;
        }
        if (picRectF.top > cropRectF.top) {
            yDiff = cropRectF.top - picRectF.top;
        } else if (picRectF.bottom < cropRectF.bottom) {
            yDiff = cropRectF.bottom - picRectF.bottom;
        }
            picRectF.offset(xDiff, yDiff);
        }
        lastDistance = distance;
        refresh();
    }
}

最后,便是获取裁剪的结果。
因cropRectF必定含于PicRectF,因此裁剪结果即为cropRectF的全集,所以只需计算cropRectF的lefttop值在picRectF上的比例,然后等比例换算为裁剪目标bitmap上的裁剪起始位的xy,然后再将cropRectF的宽高同样等比例的换算为裁剪目标bitmap上的裁剪宽度和高度,使用Bitmap.createBitmap(@NonNull Bitmap source, int x, int y, int width, int height)即可获得裁剪结果。

源码

<!-- attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ImageCroppingView">
        <!-- 设置裁剪区域宽高比. -->
        <attr name="sizeScale" format="enum">
            <enum name="use_weight" value="0"/>
            <enum name="device_size" value="1"/>
            <enum name="device_size_invert" value="2"/>
        </attr>
        <!-- 设置裁剪区域宽度所占分量,仅在sizeScale设置为use_weight时有效. -->
        <attr name="widthWeight" format="integer"/>
        <!-- 设置裁剪区域高度所占分量,仅在sizeScale设置为use_weight时有效. -->
        <attr name="heightWeight" format="integer"/>
        <!-- 设置背景颜色. -->
        <attr name="backgroundColor" format="color"/>
        <!-- 设置阴影层颜色(建议补充颜色的透明度). -->
        <attr name="shadowColor" format="color"/>
        <!-- 设置是否显示四个范围示意角. -->
        <attr name="showFourAngle" format="boolean"/>
        <!-- 设置裁剪区域内部的填充样式. -->
        <attr name="fillStyle" format="enum">
            <!-- 不使用任何样式. -->
            <enum name="none" value="0"/>
            <!-- 画一个内切椭圆. -->
            <enum name="circle" value="1"/>
            <!-- 用九宫格划分. -->
            <enum name="nineGrid" value="2"/>
        </attr>
        <!-- 设置填充样式是否使用虚线绘制. -->
        <attr name="styleUseDashed" format="boolean"/>
        <!-- 设置裁剪区域内的描线宽度,四个角的描线宽度为其2倍. -->
        <attr name="divideLineWidth" format="dimension"/>
        <!-- 设置裁剪区域内的描线颜色. -->
        <attr name="divideLineColor" format="color"/>
    </declare-styleable>
</resources>
// ImageCroppingView.java
public class ImageCroppingView extends View {
    private final Context context;
    private final DisplayMetrics dm;// 设备显示器信息
    private final Path path;
    private final Paint paint;
    private final DashPathEffect dashPathEffect;
    private final RectF picRectF;// 图片区域矩形
    private final RectF cropRectF;// 裁剪区域矩形
    private final PointF fingerPoint0;// 第一个手指触摸点的坐标
    private final PointF fingerPoint1;// 第二个手指触摸点的坐标
    private final PointF centerPoint;// 两个手指触点的中心点
    private BitmapDrawable picture = null;
    private float scale;// 裁剪区域高度对宽度的比例
    private boolean isMovingCrop = false;// 操作目标为裁剪区域
    private boolean isMovingPic = false;// 操作目标为图片
    private double lastDistance;// 上一次双指操作时两触点的距离

    // *****************属性值*****************
    private int sizeScale;
    private int widthWeight;
    private int heightWeight;
    private int backgroundColor;
    private int shadowColor;
    private boolean showFourAngle;
    private int fillStyle;
    private boolean styleUseDashed;
    private float divideLineWidth;
    private int divideLineColor;

    // ****************枚举常量****************
    /**
     * 使用设置的宽高权重来指定裁剪区域的比例.
     */
    public static final int SCALE_USE_WEIGHT = 0;

    /**
     * 使用当前设备的尺寸来指定裁剪区域的比例.
     */
    public static final int SCALE_DEVICE_SIZE = 1;

    /**
     * 使用当前设备尺寸的反转比例来指定裁剪区域的比例.
     */
    public static final int SCALE_DEVICE_SIZE_INVERT = 2;

    @IntDef({SCALE_USE_WEIGHT, SCALE_DEVICE_SIZE, SCALE_DEVICE_SIZE_INVERT})
    @Retention(RetentionPolicy.SOURCE)
    private @interface SizeScale {}

    /**
     * 裁剪区域内部不填充样式.
     */
    public static final int STYLE_NONE = 0;

    /**
     * 裁剪区域内部将绘制一个内切椭圆.<br>
     * <font color="#F57C00">仅在API 21及以上设置有效</font>.
     */
    public static final int STYLE_CIRCLE = 1;

    /**
     * 裁剪区域内部将绘制九宫格.
     */
    public static final int STYLE_NINE_GRID = 2;

    @IntDef({STYLE_NONE, STYLE_CIRCLE, STYLE_NINE_GRID})
    @Retention(RetentionPolicy.SOURCE)
    private @interface FillStyle {}

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

    public ImageCroppingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        dm = context.getResources().getDisplayMetrics();
        // 1dp转换为像素单位的大小
        float oneDp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.0f, dm);
        path = new Path();
        paint = new Paint();
        dashPathEffect = new DashPathEffect(new float[] {10, 5}, 0);
        picRectF = new RectF();
        cropRectF = new RectF();
        fingerPoint0 = new PointF();
        fingerPoint1 = new PointF();
        centerPoint = new PointF();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ImageCroppingView);
        try {
            sizeScale = typedArray.getInt(R.styleable.ImageCroppingView_sizeScale, SCALE_USE_WEIGHT);
            widthWeight = typedArray.getInt(R.styleable.ImageCroppingView_widthWeight, 1);
            if (widthWeight < 1) {
                widthWeight = 1;
            }
            heightWeight = typedArray.getInt(R.styleable.ImageCroppingView_heightWeight, 1);
            if (heightWeight < 1) {
                heightWeight = 1;
            }
            backgroundColor = typedArray.getColor(R.styleable.ImageCroppingView_backgroundColor, Color.rgb(66, 66, 66));
            shadowColor = typedArray.getColor(R.styleable.ImageCroppingView_shadowColor, Color.argb(127, 0, 0, 0));
            showFourAngle = typedArray.getBoolean(R.styleable.ImageCroppingView_showFourAngle, true);
            fillStyle = typedArray.getInt(R.styleable.ImageCroppingView_fillStyle, STYLE_NONE);
            styleUseDashed = typedArray.getBoolean(R.styleable.ImageCroppingView_styleUseDashed, true);
            divideLineWidth = typedArray.getDimension(R.styleable.ImageCroppingView_divideLineWidth, oneDp);
            divideLineColor = typedArray.getColor(R.styleable.ImageCroppingView_divideLineColor, Color.WHITE);
        } finally {
            typedArray.recycle();
        }
        setSizeScale(sizeScale);
    }

    private void initScale(int wWeight, int hWeight) {
        scale = (float) hWeight / wWeight;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST);
        }
        if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        measureCropArea();
        canvas.drawColor(backgroundColor);// 填充背景色
        // 绘制图片
        if (picture != null) {
            picture.setBounds((int) picRectF.left, (int) picRectF.top, (int) picRectF.right, (int) picRectF.bottom);
            picture.draw(canvas);// 避免因直接使用drawBitmap方法带来的可能的内存不足的问题
        }
        // 绘制裁剪区域
        drawCropArea(canvas);
    }

    /**
     * 通过合理计算得出裁剪区域.
     */
    private void measureCropArea() {
        if (cropRectF.isEmpty()) {
            float width = getWidth();
            float height = getHeight();
            boolean wideView = width > height;// 视图偏宽
            boolean wideCrop = scale < 1;// 裁剪区域偏宽
            float cropWidth;
            float cropHeight;
            if (wideView) {
                if (wideCrop) {
                    // 判断视图的高是否足够容纳的下裁剪区域需要的高度
                    // 若直接使用视图高度为基准可能使得裁剪区域过于细长
                    float cropWidthTemp = width * 2.0f / 3.0f;
                    float cropHeightTemp = cropWidthTemp * scale;
                    if (cropHeightTemp > height) {
                        cropHeight = height * 2.0f / 3.0f;
                        cropWidth = cropHeight / scale;
                    } else {
                        cropWidth = cropWidthTemp;
                        cropHeight = cropHeightTemp;
                    }
                } else {
                    // 以高度的2/3作为裁剪区域高度
                    // 宽度按设定比例得出
                    cropHeight = height * 2.0f / 3.0f;
                    cropWidth = cropHeight / scale;
                }
            } else {
                if (wideCrop) {
                    // 以宽度的2/3作为裁剪区域宽度
                    // 高度按设定比例得出
                    cropWidth = width * 2.0f / 3.0f;
                    cropHeight = cropWidth * scale;
                } else {
                    // 判断视图的宽是否足够容纳的下裁剪区域需要的宽度
                    // 若直接使用视图宽度为基准可能使得裁剪区域过细高
                    float cropHeightTemp = height * 2.0f / 3.0f;
                    float cropWidthTemp = cropHeightTemp / scale;
                    if (cropWidthTemp > width) {
                        cropWidth = width * 2.0f / 3.0f;
                        cropHeight = cropWidth * scale;
                    } else {
                        cropWidth = cropWidthTemp;
                        cropHeight = cropHeightTemp;
                    }
                }
            }
            float cropLeft = (width - cropWidth) / 2.0f;
            float cropTop = (height - cropHeight) / 2.0f;
            cropRectF.set(cropLeft, cropTop, cropLeft + cropWidth, cropTop + cropHeight);
            measurePicture();
        }
    }

    /**
     * 计算图片的矩形区域.
     */
    private void measurePicture() {
        if (picture != null) {
            float picWidth = picture.getIntrinsicWidth();
            float picHeight = picture.getIntrinsicHeight();
            float cropHeight = cropRectF.height();
            float cropWidth = cropRectF.width();
            if (picWidth > picHeight) {// 宽图,按图片高度缩放到裁剪区域高度
                picWidth = picWidth * cropHeight / picHeight;
                if (picWidth < cropWidth) {// 缩放后宽度不够,则放大宽度
                    picHeight = cropHeight * cropWidth / picWidth;
                    picWidth = cropWidth;
                } else {
                    picHeight = cropHeight;
                }
            } else {// 高图,按图片宽度缩放到裁剪区域宽度
                picHeight = picHeight * cropWidth / picWidth;
                if (picHeight < cropHeight) {// 缩放后高度不够,则放大高度
                    picWidth = cropWidth * cropHeight / picHeight;
                    picHeight = cropHeight;
                } else {
                    picWidth = cropWidth;
                }
            }
            // 将图片居中放置
            float picLeft = (getWidth() - picWidth) / 2.0f;
            float picTop = (getHeight() - picHeight) / 2.0f;
            picRectF.set(picLeft, picTop, picLeft + picWidth, picTop + picHeight);
        }
    }

    /**
     * 绘制裁剪区域.
     */
    private void drawCropArea(Canvas canvas) {
        // 绘制阴影层
        canvas.save();
        canvas.clipRect(cropRectF, Region.Op.DIFFERENCE);
        canvas.drawColor(shadowColor);
        canvas.restore();
        // 绘制四个角
        if (showFourAngle) {
            path.reset();
            float lineLength = 0.1f * Math.min(cropRectF.width(), cropRectF.height());
            // 左上角
            path.moveTo(cropRectF.left - divideLineWidth, cropRectF.top + lineLength);
            path.rLineTo(0, - lineLength - divideLineWidth);
            path.rLineTo(divideLineWidth + lineLength, 0);
            // 右上角
            path.moveTo(cropRectF.right - lineLength, cropRectF.top - divideLineWidth);
            path.rLineTo(lineLength + divideLineWidth, 0);
            path.rLineTo(0, divideLineWidth + lineLength);
            // 右下角
            path.moveTo(cropRectF.right + divideLineWidth, cropRectF.bottom - lineLength);
            path.rLineTo(0, lineLength + divideLineWidth);
            path.rLineTo(- divideLineWidth - lineLength, 0);
            // 左下角
            path.moveTo(cropRectF.left + lineLength, cropRectF.bottom + divideLineWidth);
            path.rLineTo(- lineLength - divideLineWidth, 0);
            path.rLineTo(0, - divideLineWidth - lineLength);
            paint.reset();
            paint.setColor(divideLineColor);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(2 * divideLineWidth);
            canvas.drawPath(path, paint);
        }
        // 绘制区域内样式
        if (fillStyle != STYLE_NONE) {
            if (!showFourAngle) {
                paint.reset();
                paint.setColor(divideLineColor);
                paint.setStyle(Paint.Style.STROKE);
            }
            paint.setStrokeWidth(divideLineWidth);
            if (styleUseDashed) {
                paint.setPathEffect(dashPathEffect);
            }
            path.reset();
            float strokeHalf = divideLineWidth / 2.0f;
            if (fillStyle == STYLE_CIRCLE) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    paint.setAntiAlias(true);// 抗锯齿
                    path.addOval(cropRectF.left + strokeHalf, cropRectF.top + strokeHalf,
                            cropRectF.right - strokeHalf, cropRectF.bottom - strokeHalf, Path.Direction.CW);
                }
            } else if (fillStyle == STYLE_NINE_GRID) {
                float cropWidth = cropRectF.width();
                float cropHeight = cropRectF.height();
                // 上横
                path.moveTo(cropRectF.left + strokeHalf, cropRectF.top + cropHeight / 3.0f);
                path.lineTo(cropRectF.right - strokeHalf, cropRectF.top + cropHeight / 3.0f);
                // 下横
                path.moveTo(cropRectF.left + strokeHalf, cropRectF.top + cropHeight * 2.0f / 3.0f);
                path.lineTo(cropRectF.right - strokeHalf, cropRectF.top + cropHeight * 2.0f / 3.0f);
                // 左竖
                path.moveTo(cropRectF.left + cropWidth / 3.0f, cropRectF.top + strokeHalf);
                path.lineTo(cropRectF.left + cropWidth / 3.0f, cropRectF.bottom - strokeHalf);
                // 右竖
                path.moveTo(cropRectF.left + cropWidth * 2.0f / 3.0f, cropRectF.top + strokeHalf);
                path.lineTo(cropRectF.left + cropWidth * 2.0f / 3.0f, cropRectF.bottom - strokeHalf);
            }
            canvas.drawPath(path, paint);
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (picture != null) {
            float x0 = event.getX();
            float y0 = event.getY();
            float x1 = 0.0f;
            float y1 = 0.0f;
            if (event.getPointerCount() == 2) {// 双指
                x1 = event.getX(1);
                y1 = event.getY(1);
            }
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    isMovingCrop = cropRectF.contains(x0, y0);
                    isMovingPic = false;
                    fingerPoint0.set(x0, y0);
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    isMovingCrop = false;
                    if (isMovingPic = event.getPointerCount() == 2 // 双指操作
                            // 至少有一点在图片范围内
                            && (picRectF.contains(fingerPoint0.x, fingerPoint0.y) || picRectF.contains(x1, y1))) {
                        fingerPoint1.set(x1, y1);
                        updateCenterPoint();
                        lastDistance = computeDistance();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (isMovingCrop) {
                        float xDiff = x0 - fingerPoint0.x;
                        float yDiff = y0 - fingerPoint0.y;
                        fingerPoint0.set(x0, y0);
                        // 限制不能滑出图片的范围
                        if (cropRectF.left + xDiff < picRectF.left) {// 左移
                            xDiff = picRectF.left - cropRectF.left;
                        } else if (cropRectF.right + xDiff > picRectF.right) {// 右移
                            xDiff = picRectF.right - cropRectF.right;
                        }
                        if (cropRectF.top + yDiff < picRectF.top) {// 上移
                            yDiff = picRectF.top - cropRectF.top;
                        } else if (cropRectF.bottom + yDiff > picRectF.bottom) {// 下移
                            yDiff = picRectF.bottom - cropRectF.bottom;
                        }
                        // 限制不能滑出整个视图的范围
                        if (cropRectF.left + xDiff < 0) {// 左移
                            xDiff = - cropRectF.left;
                        } else if (cropRectF.right + xDiff > getWidth()) {// 右移
                            xDiff = getWidth() - cropRectF.right;
                        }
                        if (cropRectF.top + yDiff < 0) {// 上移
                            yDiff = - cropRectF.top;
                        } else if (cropRectF.bottom + yDiff > getHeight()) {// 下移
                            yDiff = getHeight() - cropRectF.bottom;
                        }
                        cropRectF.offset(xDiff, yDiff);
                        refresh();
                    }
                    if (isMovingPic) {
                        double distance = computeDistance();
                        fingerPoint0.set(x0, y0);
                        fingerPoint1.set(x1, y1);
                        if (Math.abs(distance - lastDistance) <= 20/*临界值*/) {// 平行移动
                            // 考虑到滑动过程中的轻微抖动,因此设定临界值,
                            // 两点距离的变动值在该值以内均视为平行移动
                            float centerX = centerPoint.x;
                            float centerY = centerPoint.y;
                            updateCenterPoint();
                            float xDiff = centerPoint.x - centerX;
                            float yDiff = centerPoint.y - centerY;
                            // 限制必须包含裁剪区域
                            if (picRectF.left + xDiff > cropRectF.left) {// 右移
                                xDiff = cropRectF.left - picRectF.left;
                            } else if (picRectF.right + xDiff < cropRectF.right) {// 左移
                                xDiff = picRectF.right - cropRectF.right;
                            }
                            if (picRectF.top + yDiff > cropRectF.top) {// 下移
                                yDiff = cropRectF.top - picRectF.top;
                            } else if (picRectF.bottom + yDiff < cropRectF.bottom) {// 上移
                                yDiff = picRectF.bottom - cropRectF.bottom;
                            }
                            picRectF.offset(xDiff, yDiff);
                        } else {// 缩放
                            // 将双指中心点转化为缩放中心点
                            float zoomCenterX = Math.max(picRectF.left, Math.min(centerPoint.x, picRectF.right));
                            float zoomCenterY = Math.max(picRectF.top, Math.min(centerPoint.y, picRectF.bottom));
                            updateCenterPoint();
                            float picWidth = picRectF.width();
                            float picHeight = picRectF.height();
                            // 计算缩放中心点在图片中x、y方向上的位置比例
                            float xScale = (zoomCenterX - picRectF.left) / picWidth;// 缩放中心x方向位置比例
                            float yScale = (zoomCenterY - picRectF.top) / picHeight;// 缩放中心y方向位置比例
                            float zoomScale = (float) (distance / lastDistance);// 图片的缩放比例
                            // 限制至少要包含裁剪区域
                            float newPicWidth = Math.max(picWidth * zoomScale, cropRectF.width());// 缩放后图片的宽度
                            float newPicHeight = newPicWidth * picHeight / picWidth;// 缩放后图片的高度
                            if (newPicHeight < cropRectF.height()) {// 需要放大,放大后的图片宽度一定大于裁剪区域的宽度
                                newPicHeight *= (cropRectF.height() / newPicHeight);
                                newPicWidth = newPicHeight * picWidth / picHeight;
                            }
                            // 根据缩放中心的位置比例计算图片的矩阵位置
                            float newPicLeft = zoomCenterX - newPicWidth * xScale;
                            float newPicTop = zoomCenterY - newPicHeight * yScale;
                            picRectF.set(newPicLeft, newPicTop, newPicLeft + newPicWidth, newPicTop + newPicHeight);
                            // 校正图片位置
                            // 此时图片的宽高一定大于裁剪区域的宽高
                            float xDiff = 0.0f;
                            float yDiff = 0.0f;
                            if (picRectF.left > cropRectF.left) {
                                xDiff = cropRectF.left - picRectF.left;
                            } else if (picRectF.right < cropRectF.right) {
                                xDiff = cropRectF.right - picRectF.right;
                            }
                            if (picRectF.top > cropRectF.top) {
                                yDiff = cropRectF.top - picRectF.top;
                            } else if (picRectF.bottom < cropRectF.bottom) {
                                yDiff = cropRectF.bottom - picRectF.bottom;
                            }
                            picRectF.offset(xDiff, yDiff);
                        }
                        lastDistance = distance;
                        refresh();
                    }
                    break;
                default:
                    isMovingCrop = false;
                    isMovingPic = false;
                    break;
            }
            return true;
        }
        return super.onTouchEvent(event);
    }

    private void updateCenterPoint() {
        centerPoint.set((fingerPoint0.x + fingerPoint1.x) / 2.0f, (fingerPoint0.y + fingerPoint1.y) / 2.0f);
    }

    private double computeDistance() {
        return Math.sqrt(Math.pow(fingerPoint0.x - fingerPoint1.x, 2.0) + Math.pow(fingerPoint0.y - fingerPoint1.y, 2.0));
    }

    // ****************获得数据****************
    /**
     * 获得裁剪结果.
     *
     * @return 裁剪结果,若未设置裁剪目标则返回null
     */
    @Nullable
    public Bitmap getCroppingResult() {
        if (picture != null) {
            Bitmap bitmap = picture.getBitmap();
            int pictureWidth = bitmap.getWidth();
            int pictureHeight = bitmap.getHeight();
            float picWidth = picRectF.width();
            float picHeight = picRectF.height();
            return Bitmap.createBitmap(
                    bitmap,
                    (int) (pictureWidth * (cropRectF.left - picRectF.left) / picWidth),// 截取的起始x
                    (int) (pictureHeight * (cropRectF.top - picRectF.top) / picHeight),// 截取的起始y
                    (int) (pictureWidth * cropRectF.width() / picWidth),// 截取的宽度
                    (int) (pictureHeight * cropRectF.height() / picHeight)// 截取的高度
            );
        }
        return null;
    }

    /**
     * 获得裁剪的原始图片.
     *
     * @return 原始图片,若未设置裁剪目标则返回null
     */
    @Nullable
    public Bitmap getPicture() {
        return picture != null ? picture.getBitmap() : null;
    }

    /**
     * 获取裁剪区域高度对宽度的比例.
     */
    public float getWeightScale() {
        return scale;
    }

    /**
     * 获取背景颜色.
     */
    public int getBackgroundColor() {
        return backgroundColor;
    }

    /**
     * 获取阴影层颜色.
     */
    public int getShadowColor() {
        return shadowColor;
    }

    /**
     * 裁剪区域是否展示四个区域范围示意角.
     *
     * @return 若展示则返回true,否则返回false
     */
    public boolean isShowFourAngle() {
        return showFourAngle;
    }

    /**
     * 裁剪区域内部样式是否使用虚线绘制.
     *
     * @return 若使用虚线绘制则返回true,否则返回false
     */
    public boolean isStyleUseDashed() {
        return styleUseDashed;
    }

    /**
     * 获取绘制裁剪区域内部样式的颜色.
     */
    public int getDivideLineColor() {
        return divideLineColor;
    }

    /**
     * 获取裁剪区域内部样式的描线宽度.<br>
     * 以像素为单位.
     */
    public float getDivideLineWidth() {
        return divideLineWidth;
    }

    /**
     * 获取裁剪区域内部的绘制样式.
     *
     * @see #STYLE_NONE
     * @see #STYLE_CIRCLE
     * @see #STYLE_NINE_GRID
     */
    public int getFillStyle() {
        return fillStyle;
    }

    // ****************更新数据****************
    /**
     * 设置目标裁剪图片.
     *
     * @param picture 目标图片
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setPicture(@NonNull BitmapDrawable picture) {
        this.picture = picture;
        cropRectF.setEmpty();
        return this;
    }

    /**
     * 设置目标裁剪图片.
     *
     * @param picture 目标图片
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setPicture(@NonNull Bitmap picture) {
        return setPicture(new BitmapDrawable(context.getResources(), picture));
    }

    /**
     * 使用图片的URI设置目标裁剪图片.
     *
     * @param pictureUri 目标图片的URI
     * @return 当前对象的引用
     * @throws FileNotFoundException 如果无法打开提供的URI
     * @throws RuntimeException 如果传入的URI文件不是图片
     * @see #refresh()
     */
    public ImageCroppingView setPicture(@NonNull Uri pictureUri) throws FileNotFoundException, RuntimeException {
        Drawable drawable = Drawable.createFromStream(
                context.getContentResolver().openInputStream(pictureUri), null);
        if (!(drawable instanceof BitmapDrawable)) {
            throw new RuntimeException("错误的图片类型");
        }
        return setPicture((BitmapDrawable) drawable);
    }

    /**
     * 设置裁剪区域尺寸比例的类型.
     *
     * @param sizeScale 类型值
     * @return 当前对象的引用
     * @see #SCALE_USE_WEIGHT
     * @see #SCALE_DEVICE_SIZE
     * @see #SCALE_DEVICE_SIZE_INVERT
     * @see #refresh()
     */
    public ImageCroppingView setSizeScale(@SizeScale int sizeScale) {
        this.sizeScale = sizeScale;
        switch (sizeScale) {
            case SCALE_USE_WEIGHT:
                initScale(widthWeight, heightWeight);
                break;
            case SCALE_DEVICE_SIZE:
                initScale(dm.widthPixels, dm.heightPixels);
                break;
            case SCALE_DEVICE_SIZE_INVERT:
                initScale(dm.heightPixels, dm.widthPixels);
                break;
            default:
                scale = 1.0f;
                break;
        }
        cropRectF.setEmpty();
        return this;
    }

    /**
     * 设置新的裁剪区域宽高比例.<br>
     * sizeScale的值将同时设为{@link #SCALE_USE_WEIGHT}.
     *
     * @param widthWeight 宽度所占分量
     * @param heightWeight 高度所占分量
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setWeightScale(@IntRange(from = 1) int widthWeight, @IntRange(from = 0) int heightWeight) {
        this.widthWeight = widthWeight;
        this.heightWeight = heightWeight;
        this.sizeScale = SCALE_USE_WEIGHT;
        initScale(widthWeight, heightWeight);
        cropRectF.setEmpty();
        return this;
    }

    /**
     * 设置背景颜色.
     *
     * @param backgroundColor 新的背景颜色
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setCroppingBackgroundColor(@ColorInt int backgroundColor) {
        this.backgroundColor = backgroundColor;
        return this;
    }

    /**
     * 设置阴影层颜色.<br>
     * 建议附加透明度.
     *
     * @param shadowColor 新的阴影层颜色
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setShadowColor(@ColorInt int shadowColor) {
        this.shadowColor = shadowColor;
        return this;
    }

    /**
     * 设置裁剪区域是否展示四个区域范围示意角.
     *
     * @param showFourAngle true表示展示,false表示不展示
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setShowFourAngle(boolean showFourAngle) {
        this.showFourAngle = showFourAngle;
        return this;
    }

    /**
     * 设置裁剪区域内部样式是否使用虚线绘制.
     *
     * @param styleUseDashed true表示使用虚线,false表示使用直线
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setStyleUseDashed(boolean styleUseDashed) {
        this.styleUseDashed = styleUseDashed;
        return this;
    }

    /**
     * 设置裁剪区域内部样式的绘制颜色.<br>
     * 四个范围示意角将使用同样的颜色绘制.
     *
     * @param divideLineColor 新的绘制颜色
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setDivideLineColor(@ColorInt int divideLineColor) {
        this.divideLineColor = divideLineColor;
        return this;
    }

    /**
     * 设置裁剪区域内部样式的描线宽度.<br>
     * 四个范围示意角的描线宽度为其二倍.
     *
     * @param divideLineWidthDpValue 新的描线宽度(以dp为单位)
     * @return 当前对象的引用
     * @see #refresh()
     */
    public ImageCroppingView setDivideLineWidth(@FloatRange(from = 0.0f) float divideLineWidthDpValue) {
        this.divideLineWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, divideLineWidthDpValue, dm);
        return this;
    }

    /**
     * 设置裁剪区域的内部样式.
     *
     * @param fillStyle 样式值
     * @return 当前对象的引用
     * @see #STYLE_NONE
     * @see #STYLE_CIRCLE
     * @see #STYLE_NINE_GRID
     * @see #refresh()
     */
    public ImageCroppingView setFillStyle(@FillStyle int fillStyle) {
        this.fillStyle = fillStyle;
        return this;
    }

    /**
     * 刷新所有的设置以显示在视图上.
     */
    public void refresh() {
        invalidate();
        requestLayout();
    }
}

才疏学浅,不足之处烦请多多指教。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值