安卓图片裁剪——使用自定义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)即可获得裁剪结果。

源码



<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>
        
        <attr name="widthWeight" format="integer"/>
        
        <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"/>
        
        <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;

    /**
     * 裁剪区域内部将绘制一个内切椭圆.
* 仅在API 21及以上设置有效. */
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; } /** * 获取裁剪区域内部样式的描线宽度.
* 以像素为单位. */
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; } /** * 设置新的裁剪区域宽高比例.
* 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; } /** * 设置阴影层颜色.
* 建议附加透明度. * * @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; } /** * 设置裁剪区域内部样式的绘制颜色.
* 四个范围示意角将使用同样的颜色绘制. * * @param divideLineColor 新的绘制颜色 * @return 当前对象的引用 * @see #refresh() */
public ImageCroppingView setDivideLineColor(@ColorInt int divideLineColor) { this.divideLineColor = divideLineColor; return this; } /** * 设置裁剪区域内部样式的描线宽度.
* 四个范围示意角的描线宽度为其二倍. * * @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(); } }

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

你可能感兴趣的:(安卓View,安卓,android)