前言
在图片操作中裁剪最为常见,安卓中常用的裁剪方式是通过调用 Bitmap.createBitmap(@NonNull Bitmap source, int x, int y, int width, int height)
等实现的,本文所展示的View便是以此为核心设计。
设计思路
在一个图片裁剪的过程中,我们可以看到其主要由以下两部分组成:
- 裁剪区域(裁剪框)
- 图片区域(裁剪目标)
因此,我们可以将其抽象为两个矩形,裁剪结果即两个矩形取交集,即:
- 代表裁剪区域的矩形(下称cropRectF)
- 代表图片区域的矩形(下称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的left
、top
值在picRectF上的比例,然后等比例换算为裁剪目标bitmap上的裁剪起始位的x
、y
,然后再将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();
}
}
才疏学浅,不足之处烦请多多指教。