CropView从入门到放弃
本篇我会带你去从零设计一款裁剪旋转的View
你需要准备的
裁剪的View最关键的是裁剪框的绘制和手势的调整,另外还有最核心的裁剪功能就是调用方法createBitmap去裁剪得到目标图片。
//图片裁剪的核心功能 |
- 1、裁剪框的绘制无非就是一个透明的方形和灰色透明度的方形之外的区域(如下图红色箭头所指区域)、裁剪框的绘制关键的地方在于裁剪的Rect区域的确定
- 2、手势调整裁剪框的边角、四周边。裁剪控件可以拖动的边界为上图的紫色箭头区域,并且有一个点击所在点区域的周边也可以正常去控制边界的拖动,说明有一个动态的范围
目录
为了能正常的显示图片的缩放比例和旋转、居中等,可以考虑自己控制Bitmap的各项绘制参数。
一、测量
主要是Padding值的处理
二、布局
- 1、Bitmap中心点的确定
- 2、图片比例的确定
- 3、图片Bitmap参数的确定(比如旋转。放大)
- 4、图片显示区域的确定
- 5、裁剪框区域的确定
三、绘制
- 1、画背景
- 2、画Bitmap
- 3、画裁剪框
- 4、画边界线
- 5、画指导线
- 6、画定制四边角的图案
四、添加MotionEvent控制
- 1、按下时计算点击的区域位置
- 2、根据第1步点击的区域去判断需不需要对边界进行Move处理
- 3、抬起时恢复默认的处理状态
五、旋转翻转处理
- 1、左右翻转
- 2、旋转处理
六、裁剪
- 1、对获取的bitmap进行旋转处理
- 2、计算裁剪的原图
- 3、对图片进行翻转处理
- 4、拿到裁剪后的图
七、开放属性Style
八、使用
额外需要首先考虑的
首先思考我们的控件大概需要什么动态的属性,为了以后方便扩展业务或者功能的精确控制
- 1、裁剪框有一个最小的矩形大小,说明要限制裁剪框的长度宽度,一般为正方形(本例中设计为了正方形,当然解析到该块内容时我会顺便说下如何实现长宽不一样的裁剪框)。此处需要属性为裁剪框最小边长
- 2、裁剪框外围除了画Bitmap之外,会存在除了Bitmap之外的空白区域,这里希望能控制他们的颜色。此处需要属性为窗口背景颜色
- 3、由于我们要自定义Bitmap区域的绘制,类似ImageView控件,我们需要src属性来引入图片内容。此处需要属性为src
- 4、上面讲过,手势点击边界或者角时,点击所在点区域的周边也可以正常去控制边界的拖动,说明有一个动态的范围。这个范围也希望是可以控制的。此处需要属性为触摸边界的大小范围
- 5、颜色的比如有:裁剪内框的颜色,裁剪外框的颜色,边界线的颜色,裁剪框指导线的颜色,四周边角自定义点的图案的颜色
- 6、尺寸的比如:边框线的宽度,指导线的宽度,四周边角线宽度、长度(或者圆圈的半径)
- 7、图片初始化的比例
- 8、根据默认打开裁剪控件时的状态确定开关类属性:裁剪框的是否绘制
- 9、如果说有时候指导线是想要点击才拖动裁剪框才显示,那么还要添加指导线的绘制开关
- 10、同理,四边角自定义图案也需要开关
- 11、如果你想的够多,那么这些边角图案有时候可能还会加入类似button的点击之后放大图案的需求,那么这个时候你还需要添加一个开关用来控制要不要开启边角动画(逃)
一、测量
测量View的大小,主要是处理Padding的值,甚至某些情况要专门对wrap_content属性做专门的处理。对Padding进行处理如下
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
final int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(viewWidth, viewHeight);
mViewWidth = viewWidth - getPaddingLeft() - getPaddingRight();
mViewHeight = viewHeight - getPaddingTop() - getPaddingBottom();
}
对左右Padding进行了处理,最终得到的是CropView内容的区域的实际宽高
二、布局
这一部分只要是对Bitmap中心点、图片比例、裁剪框区域的确定,布局代码如下,为了提高性能,这里只有在图片已设置到View之后```
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getDrawable() != null) {
doLayout(mViewWidth, mViewHeight);
}
}
/** |
1、Bitmap中心点的确定
这个没什么好说的,就是View除了Padding区域外x,y取中点
2、图片比例的确定
这里开始会用到一些变量(或者是属性)
// 旋转角度 |
计算比例的代码如下,其实就是图片的宽高之比
private float calcScale(int viewWidth, int viewHeight, float angle) { |
首先拿到Drawable的固有宽宽高,接着对图片的宽高mImgWidth、mImgHeight进行边界控制,然后计算出图片的长宽比例,要是有旋转的情况那么viewRatio的值要拿到旋转后的宽高来判断,这里关键就是怎么样拿到旋转后的宽高,
private float getRotatedWidth(float angle) {
return getRotatedWidth(angle, mImgWidth, mImgHeight);
}
private float getRotatedWidth(float angle, float width, float height) {
return angle % 180 == 0 ? width : height;
}
最后会把放大当前的图片比例保存下来
/**
* 保存比例
*
* @param scale
*/
private void setScale(float scale) {
mCropScale = scale;
}
|
// 图形的矩阵类
private Matrix mMatrix = null;
// 居中中点的矩型区域
private PointF mCenter = new PointF();并且矩阵类我们会在构造方法里进行初始化
mMatrix = new Matrix();一张Bitmap图片加载到View默认情况下一般是这样
![](https://upload-images.jianshu.io/upload_images/3515789-40995be52e7605eb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
那我们需要做的是将它平移到中点,并且比例放大到我们想要的比例
所以,Matrix变化如下,先对其做平移操作,平移距离是Bitmap显示区域中点到屏幕中点的。放大是以屏幕中心点为放大原点,然后X,Y进行整比例的放大,后面考虑到我们要用到旋转操作,也加了旋转的Matrix操作。
/**
* 重设矩阵,平移缩放旋转操作
*/
private void setMatrix() {
mMatrix.reset();
mMatrix.setTranslate(mCenter.x - mImgWidth * 0.5f, mCenter.y - mImgHeight * 0.5f);
mMatrix.postScale(mCropScale, mCropScale, mCenter.x, mCenter.y);
mMatrix.postRotate(mImgAngle, mCenter.x, mCenter.y);
}
##### 4、图片显示区域的确定 |
// 图形的矩阵类
private Matrix mMatrix = null;
// 图片的rectf区域
private RectF mImageRectF;
// 裁剪框的RectF
private RectF mFrameRectF;
RectF rectF = new RectF(0, 0, mImgWidth, mImgHeight);
mImageRectF = calcImageRect(rectF, mMatrix);
/**
* 将Matrix映射到rect
*
* @param rect
* @param matrix
* @return
*/
private RectF calcImageRect(RectF rect, Matrix matrix) {
RectF applied = new RectF();
matrix.mapRect(applied, rect);
return applied;
}
根据Img长宽初始化一个新的Rect区域,然后将变化后的Matrix应用到Rect |
/**
* 裁剪框的比例模式
*
*
*/
public enum CropModeEnum {
FIT_IMAGE(0), RATIO_2_3(1), RATIO_3_2(2), RATIO_4_3(3), RATIO_3_4(4), SQUARE(5), RATIO_16_9(6), RATIO_9_16(7), FREE(
8);
private final int ID;
CropModeEnum(int id) {
ID = id;
}
public int getID() {
return ID;
}
}
|
// 裁剪模式,默认比例是自由模式
private CropModeEnum mCropMode = CropModeEnum.FREE;
计算裁剪框的代码如下:
private RectF calculateFrameRect(RectF imageRect) {
float frameW = getRatioX(imageRect.width());
float frameH = getRatioY(imageRect.height());
float frameRatio = frameW / frameH;
float imgRatio = imageRect.width() / imageRect.height();
float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom;
if (frameRatio >= imgRatio) {
//宽比长比例大于img图宽高比的情况
l = imageRect.left;
r = imageRect.right;
//图的中点
float hy = (imageRect.top + imageRect.bottom) * 0.5f;
//中点到上下顶点坐标的距离
float hh = (imageRect.width() / frameRatio) * 0.5f;
t = hy - hh;
b = hy + hh;
} else if (frameRatio < imgRatio) {
//宽比长比例大于img图宽高比的情况
t = imageRect.top;
b = imageRect.bottom;
float hx = (imageRect.left + imageRect.right) * 0.5f;
float hw = imageRect.height() * frameRatio * 0.5f;
l = hx - hw;
r = hx + hw;
}
//裁剪框宽度
float w = r - l;
//高度
float h = b - t;
//中心点
float cx = l + w / 2;
float cy = t + h / 2;
//放大后的裁剪框的宽高
float sw = w * mInitialFrameScale;
float sh = h * mInitialFrameScale;
return new RectF(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2);
}
|
private float getRatioX(float w) {
switch (mCropMode) {
case FIT_IMAGE:
return mImageRectF.width();
case FREE:
return w;
case RATIO_2_3:
return 2;
case RATIO_3_2:
return 3;
case RATIO_4_3:
return 4;
case RATIO_3_4:
return 3;
case RATIO_16_9:
return 16;
case RATIO_9_16:
return 9;
case SQUARE:
return 1;
default:
return w;
}
}
接着我们通过分别计算获取裁剪框后的比例和原drawable图的比例,拿到他们的比例为了判断裁剪的框要处于图片框中的什么位置
//获取裁剪模式后计算的比例
float frameRatio = frameW / frameH;
//图片原始比例
float imgRatio = imageRect.width() / imageRect.height();
|
float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom;
if (frameRatio >= imgRatio) {
//宽比长比例大于img图宽高比的情况
l = imageRect.left;
r = imageRect.right;
//图的中点
float hy = (imageRect.top + imageRect.bottom) * 0.5f;
//中点到上下顶点坐标的距离
float hh = (imageRect.width() / frameRatio) * 0.5f;
t = hy - hh;
b = hy + hh;
} else if (frameRatio < imgRatio) {
//宽比长比例大于img图宽高比的情况
t = imageRect.top;
b = imageRect.bottom;
float hx = (imageRect.left + imageRect.right) * 0.5f;
float hw = imageRect.height() * frameRatio * 0.5f;
l = hx - hw;
r = hx + hw;
}
首先定义裁剪框的四个边角参数左上角left、top,右下角right、bottom坐标分别为l,t,r,b。 |
t = hy - hh;
b = hy + hh;
|
//裁剪框宽度
float w = r - l;
//高度
float h = b - t;
//中心点
float cx = l + w / 2;
float cy = t + h / 2;
//放大后的裁剪框的宽高
float sw = w * mInitialFrameScale;
float sh = h * mInitialFrameScale;
return new RectF(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2);
|
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(mBackgroundColor);
//只有初始化完毕了才绘制
if (mIsInitialized) {
setMatrix();//这里一开始绘制裁剪框是没有想到的,后面刷新视图的时候,设置了参数之后需要计算bitmap的情况,需要重新对Bitmap进行矩阵处理
Bitmap bitmap = getBitmap();
if (bitmap != null) {
canvas.drawBitmap(bitmap, mMatrix, mBitmapPaint);
// 画裁剪框
drawCropFrame(canvas);
}
}
}
##### 1、画背景
画ImageView外围的背景(图片显示之外会存在的空白部分)
canvas.drawColor(mBackgroundColor);##### 2、画Bitmap
canvas.drawBitmap(bitmap, mMatrix, mBitmapPaint);将bitmap画到已确定位置的mMatrix矩阵中
##### 3、画裁剪框
private void drawCropFrame(Canvas canvas) {
drawOverlay(canvas);
drawFrame(canvas);
drawGuidelines(canvas);
drawHandleLines(canvas);
}接下来裁剪框会有覆盖层,透明层,边界线和指导线的绘制
/**
* 画裁剪框的半透明覆盖层
*
* @param canvas
*/
private void drawOverlay(Canvas canvas) {
mTranslucentPaint.setStyle(Paint.Style.FILL);
mTranslucentPaint.setFilterBitmap(true);
mTranslucentPaint.setColor(mOverlayColor);
Path path = new Path();
RectF overlayRectF = new RectF();
overlayRectF.set(mImageRectF);
path.addRect(overlayRectF, Path.Direction.CW);
path.addRect(mFrameRectF, Path.Direction.CCW);
canvas.drawPath(path, mTranslucentPaint);
}
第二部分拿到了图片的Rect区域,我们这里画半透明覆盖层只要调用路径来处理,这里我们调用了路径了两种不同的方向来添加,以使最终得到的路径是裁剪框之外。(当然这里也可以用路径合成方式的处理) |
4、画边界线
属性变量
// 裁剪外框线框宽度 |
直接拿到FrameRect边界来绘制边界线/**
* 画裁剪框边界线
*
* @param canvas
*/
private void drawFrame(Canvas canvas) {
mFramePaint.setStyle(Paint.Style.STROKE);
mFramePaint.setFilterBitmap(true);
mFramePaint.setColor(mFrameColor);
mFramePaint.setStrokeWidth(mFrameStrokeWeight);
canvas.drawRect(mFrameRectF, mFramePaint);
}
5、画指导线
这里同理,只需要拿到裁剪框边界,计算索要的点的位置的点的坐标即可绘制自己想要的线条
/** |
6、画定制四边角的图案
这里我是在四个角画了线条,由于自由模式有点特殊,自由模式下四条边也可以拉动,所以需要给四个边中间点也绘制图案
变量如下// 四角线的长度值
private int mHandleSize;
// 四角的线宽度
private int mHandleWidth;
主要就是坐标的计算/**
* 画四角的线
*
* @param canvas
*/
private void drawHandleLines(Canvas canvas) {
mFramePaint.setColor(mHandleColor);
mFramePaint.setStyle(Paint.Style.FILL);
// 指导线最边界(最左/最右/最下/最上)的x和y
float handleLineLeftX = mFrameRectF.left - mHandleWidth;
float handleLineRightX = mFrameRectF.right + mHandleWidth;
float handleLineTopY = mFrameRectF.top - mHandleWidth;
float handleLineBottomY = mFrameRectF.bottom + mHandleWidth;
// 左上竖向
RectF ltRectFVertical =
new RectF(handleLineLeftX, handleLineTopY, mFrameRectF.left, handleLineTopY + mHandleSize);
// 左上横向
RectF ltRectFHorizontal =
new RectF(handleLineLeftX, handleLineTopY, handleLineLeftX + mHandleSize, mFrameRectF.top);
RectF rtRectFHorizontal =
new RectF(handleLineRightX - mHandleSize, handleLineTopY, handleLineRightX, mFrameRectF.top);
RectF rtRectFVertical =
new RectF(mFrameRectF.right, handleLineTopY, handleLineRightX, handleLineTopY + mHandleSize);
RectF lbRectFVertical =
new RectF(handleLineLeftX, handleLineBottomY - mHandleSize, mFrameRectF.left, mFrameRectF.bottom);
RectF lbRectFHorizontal =
new RectF(handleLineLeftX, mFrameRectF.bottom, handleLineLeftX + mHandleSize, handleLineBottomY);
RectF rbRectFVertical =
new RectF(mFrameRectF.right, handleLineBottomY - mHandleSize, handleLineRightX, handleLineBottomY);
RectF rbRectFHorizontal =
new RectF(handleLineRightX - mHandleSize, mFrameRectF.bottom, handleLineRightX, handleLineBottomY);
canvas.drawRect(ltRectFVertical, mFramePaint);
canvas.drawRect(ltRectFHorizontal, mFramePaint);
canvas.drawRect(rtRectFVertical, mFramePaint);
canvas.drawRect(rtRectFHorizontal, mFramePaint);
canvas.drawRect(lbRectFVertical, mFramePaint);
canvas.drawRect(lbRectFHorizontal, mFramePaint);
canvas.drawRect(rbRectFVertical, mFramePaint);
canvas.drawRect(rbRectFHorizontal, mFramePaint);
if (mCropMode == CropModeEnum.FREE) {
// 如果当前是自由模式
mFramePaint.setStrokeCap(Paint.Cap.ROUND);
mFramePaint.setStrokeWidth(mHandleWidth + SizeUtils.dp2px(2));
float centerX = mFrameRectF.left + mFrameRectF.width() / 2;
float centerY = mFrameRectF.top + mFrameRectF.height() / 2;
canvas.drawLine(centerX - mHandleSize, mFrameRectF.top, (centerX + mHandleSize), mFrameRectF.top,
mFramePaint);
canvas.drawLine(centerX - mHandleSize, mFrameRectF.bottom, centerX + mHandleSize, mFrameRectF.bottom,
mFramePaint);
canvas.drawLine(mFrameRectF.left, (centerY - mHandleSize), mFrameRectF.left, centerY + mHandleSize,
mFramePaint);
canvas.drawLine(mFrameRectF.right, centerY - mHandleSize, mFrameRectF.right, centerY + mHandleSize,
mFramePaint);
}
}
四、添加MotionEvent控制
手指触摸区域会有10种情况,除了四个边角和边界之外,还有裁剪框中间和裁剪框外部的情况,这里定义一个枚举类来标记这10种情况
/** |
定义默认的触摸模式
// 触摸的情况 |
接下来我们需要在Down事件里记录处理的边界,Move事件里动态判断边界是否需要移动,其他事件里恢复初始的状态
@Override |
1、按下时计算点击的区域位置
private void onActionDown(MotionEvent event) { |
这里的关键在于
if (isInLeftTopCorner(x, y)) {
mTouchArea = TouchAreaEnum.LEFT_TOP;
return;
}
//判断是否在左上角的边界内
private boolean isInLeftTopCorner(float x, float y) {
float dx = x - mFrameRectF.left;
float dy = y - mFrameRectF.top;
return isInsideBound(dx, dy);
}
private boolean isInsideBound(float dx, float dy) {
float d = (float) (Math.pow(dx, 2) + Math.pow(dy, 2));
return (Math.pow(mHandleSize + mTouchPadding, 2)) >= d;
}
这里处理的其实就是判断触摸的点是否在边界的有效范围内,比如现在的点是左上角,那么可触摸区域应该是围绕左上角顶点的四周圆形的一块区域。如下图,应该是黄色的一块圆形区域。```isInsideBound()```做的就是对边界内外的判断。 |
private void onActionMove(MotionEvent event) {
float diffX = event.getX() - mLastX;
float diffY = event.getY() - mLastY;
// 区分点击的区域进行移动
switch (mTouchArea) {
case CENTER: {
moveFrame(diffX, diffY);
break;
}
case LEFT_TOP: {
moveHandleLeftTop(diffX, diffY);
break;
}
case RIGHT_TOP: {
moveHandleRightTop(diffX, diffY);
break;
}
case LEFT_BOTTOM: {
moveHandleLeftBottom(diffX, diffY);
break;
}
case RIGHT_BOTTOM: {
moveHandleRightBottom(diffX, diffY);
break;
}
case CENTER_LEFT: {
moveHandleCenterLeft(diffX);
break;
}
case CENTER_TOP: {
moveHandleCenterTop(diffY);
break;
}
case CENTER_RIGHT: {
moveHandleCenterRight(diffX);
break;
}
case CENTER_BOTTOM: {
moveHandleCenterBottom(diffY);
break;
}
case OUT_OF_BOUNDS: {
break;
}
}
invalidate();
mLastX = event.getX();
mLastY = event.getY();
}
|
private void moveFrame(float x, float y) {
// 1.先平移(这里采取先平移如果条件不满足再后退的策略,避免产生由于Move数值精度不准确产生的边界问题)
mFrameRectF.left += x;
mFrameRectF.right += x;
mFrameRectF.top += y;
mFrameRectF.bottom += y;
// 2.判断有没有超出界外,如果超出则后退
handleMoveBounds();
}
- B、针对边框的边界进行处理,处理边框会有2种情况,一种是自由模式下,长宽都可以分别调整,另外一种是确定比例的模式下,长宽需要一起调整的情况
private void moveHandleLeftTop(float diffX, float diffY) {
if (mCropMode == CropModeEnum.FREE) {
mFrameRectF.left += diffX;
mFrameRectF.top += diffY;
if (isWidthTooSmall()) {
float offsetX = mFrameMinSize - mFrameRectF.width();
mFrameRectF.left -= offsetX;
}
if (isHeightTooSmall()) {
float offsetY = mFrameMinSize - mFrameRectF.height();
mFrameRectF.top -= offsetY;
}
checkScaleBounds();
} else {
float dx = diffX;
float dy = diffX getRatioY() / getRatioX();
mFrameRectF.left += dx;
mFrameRectF.top += dy;
// 控制缩放边界
if (isWidthTooSmall()) {
float offsetX = mFrameMinSize - mFrameRectF.width();
mFrameRectF.left -= offsetX;
// todo 裁剪框比例控制
float offsetY = offsetX getRatioY() / getRatioX();
mFrameRectF.top -= offsetY;
}
if (isHeightTooSmall()) {
float offsetY = mFrameMinSize - mFrameRectF.height();
mFrameRectF.top -= offsetY;
float offsetX = offsetY * getRatioX() / getRatioY();
mFrameRectF.left -= offsetX;
}
float ox, oy;
if (!isInsideX(mFrameRectF.left)) {
ox = mImageRectF.left - mFrameRectF.left;
mFrameRectF.left += ox;
oy = ox * getRatioY() / getRatioX();
mFrameRectF.top += oy;
}
if (!isInsideY(mFrameRectF.top)) {
oy = mImageRectF.top - mFrameRectF.top;
mFrameRectF.top += oy;
ox = oy * getRatioX() / getRatioY();
mFrameRectF.left += ox;
}
}
}
|
if (mCropMode == CropModeEnum.FREE) {
//不管发生什么事,先把过来的数值先加上
mFrameRectF.left += diffX;
mFrameRectF.top += diffY;
//判断变化后的边界有没有比限定的最小边界小,如果小,那么就还原
if (isWidthTooSmall()) {
float offsetX = mFrameMinSize - mFrameRectF.width();
mFrameRectF.left -= offsetX;
}
if (isHeightTooSmall()) {
float offsetY = mFrameMinSize - mFrameRectF.height();
mFrameRectF.top -= offsetY;
}
//处理有没有超出最大边界
checkScaleBounds();
}
|
//这里只需要监测move事件的x或者y的变化,然后通过设定的比例计算出另外的x或者y的坐标,从而限定裁剪框的比例
float dx = diffX;
float dy = diffX * getRatioY() / getRatioX();
mFrameRectF.left += dx;
mFrameRectF.top += dy;
// 控制缩放边界
if (isWidthTooSmall()) {
//同上自由模式的控制,这里也是要讲超界的值恢复,只是这里x,y要分别恢复
float offsetX = mFrameMinSize - mFrameRectF.width();
mFrameRectF.left -= offsetX;
float offsetY = offsetX * getRatioY() / getRatioX();
mFrameRectF.top -= offsetY;
}
if (isHeightTooSmall()) {
float offsetY = mFrameMinSize - mFrameRectF.height();
mFrameRectF.top -= offsetY;
float offsetX = offsetY * getRatioX() / getRatioY();
mFrameRectF.left -= offsetX;
}
//限制不能让裁剪框超出图片边界
float ox, oy;
if (!isInsideX(mFrameRectF.left)) {
ox = mImageRectF.left - mFrameRectF.left;
mFrameRectF.left += ox;
oy = ox * getRatioY() / getRatioX();
mFrameRectF.top += oy;
}
if (!isInsideY(mFrameRectF.top)) {
oy = mImageRectF.top - mFrameRectF.top;
mFrameRectF.top += oy;
ox = oy * getRatioX() / getRatioY();
mFrameRectF.left += ox;
}
|
private void onActionCancel() {
mTouchArea = TouchAreaEnum.OUT_OF_BOUNDS;
invalidate();
}
|
/**
* 左右翻转
*/
public void reverseY() {
if (!mIsReverseY) {
mIsReverseY = true;
} else {
mIsReverseY = false;
}
super.setScaleX(getScaleX() * -1f);
}
|
/**
* 旋转角度的枚举类
*
* @time Created by 2018/8/22 19:07
*/
public enum RotateDegreesEnum {
ROTATE_90D(90), ROTATE_180D(180), ROTATE_270D(270), ROTATE_M90D(-90), ROTATE_M180D(-180), ROTATE_M270D(-270), ROTATE_0D(
0);
private final int VALUE;
RotateDegreesEnum(final int value) {
this.VALUE = value;
}
public int getValue() {
return VALUE;
}
}
|
/**
* 旋转图片
*
* @param degrees
* @param durationMillis 需要调整的动画时间
*/
private void rotateImage(RotateDegreesEnum degrees, int durationMillis) {
if (mIsRotating) {
mValueAnimator.cancel();
}
//先计算旋转后的比例和角度变化
final float currentAngle = mImgAngle;
//新的角度
final float newAngle = (mImgAngle + degrees.getValue());
final float angleDiff = newAngle - currentAngle;
final float currentScale = mCropScale;
//旋转后的比例
final float newScale = calcScale(mViewWidth, mViewHeight, newAngle);
if (mIsAnimationEnabled) {
final float scaleDiff = newScale - currentScale;
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mImgAngle = newAngle % 360;
mCropScale = newScale;
doLayout(mViewWidth, mViewHeight);
mIsRotating = false;
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mIsRotating = true;
}
});
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//动画关键参数变化
float scale = (float) animation.getAnimatedValue();
mImgAngle = currentAngle + angleDiff * scale;
mCropScale = currentScale + scaleDiff * scale;
//核心旋转缩放处理
setMatrix();
invalidate();
}
});
mValueAnimator.setDuration(durationMillis);
mValueAnimator.start();
} else {
//无动画的情况
mImgAngle = newAngle % 360;
mCropScale = newScale;
doLayout(mViewWidth, mViewHeight);
}
}
|
/**
* 获取裁剪后的图片(<Strong>Notice:不包含翻转变化</Strong>)
*
* @return
*/
public Bitmap getCroppedBitmap() {
Bitmap source = getBitmap();
if (source == null)
return null;
Bitmap rotated = getRotatedBitmap(source);
Rect cropRect = calcCropRect(source.getWidth(), source.getHeight());
Bitmap cropped =
Bitmap.createBitmap(rotated, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), null, false);
if (rotated != cropped && rotated != source) {
rotated.recycle();
}
if (mIsReverseY) {
// 如果翻转了照片,那么对照片进行翻转处理
cropped = ImgUtils.reverseImg(cropped, -1, 1);
}
return cropped;
}
|
/**
* 得到旋转后的Bitmap
*
* @param bitmap
* @return
*/
private Bitmap getRotatedBitmap(Bitmap bitmap) {
Matrix rotateMatrix = new Matrix();
rotateMatrix.postRotate(mImgAngle, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, true);
}
##### 2、计算裁剪框相对原图的区域 |
private Rect calcCropRect(int originalImageWidth, int originalImageHeight) {
//旋转之后的宽
float rotatedWidth = getRotatedWidth(mImgAngle, originalImageWidth, originalImageHeight);
//旋转之后的高
float rotatedHeight = getRotatedHeight(mImgAngle, originalImageWidth, originalImageHeight);
//旋转后的相对旋转前的比例
float scaleForOriginal = rotatedWidth / mImageRectF.width();
float offsetX = mImageRectF.left * scaleForOriginal;
float offsetY = mImageRectF.top * scaleForOriginal;
//对旋转后的裁剪框的处理,
int left = Math.round(mFrameRectF.left * scaleForOriginal - offsetX);
int top = Math.round(mFrameRectF.top * scaleForOriginal - offsetY);
int right = Math.round(mFrameRectF.right * scaleForOriginal - offsetX);
int bottom = Math.round(mFrameRectF.bottom * scaleForOriginal - offsetY);
int imageW = Math.round(rotatedWidth);
int imageH = Math.round(rotatedHeight);
return new Rect(Math.max(left, 0), Math.max(top, 0), Math.min(right, imageW), Math.min(bottom, imageH));
}
|
if (mIsReverseY) {
// 如果翻转了照片,那么对照片进行翻转处理
cropped = ImgUtils.reverseImg(cropped, -1, 1);
}
##### 4、拿到裁剪后的图
Bitmap cropped =
Bitmap.createBitmap(rotated, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), null, false);
# 七、开放属性Style
为了能在xml中自定义我们想要的属性,针对开头所说的额外考虑的部分和实际coding时所遇到的一些属性进行整理
得到属性
</attr>
<attr name="background_color" format="reference|color" />
<attr name="overlay_color" format="reference|color" />
<attr name="frame_color" format="reference|color" />
<attr name="handle_color" format="reference|color" />
<attr name="handle_width" format="dimension" />
<attr name="handle_size" format="dimension" />
<attr name="guide_color" format="reference|color" />
<attr name="guide_show_mode">
<enum name="show_always" value="1" />
<enum name="show_on_touch" value="2" />
<enum name="not_show" value="3" />
</attr>
<attr name="handle_show_mode">
<enum name="show_always" value="1" />
<enum name="show_on_touch" value="2" />
<enum name="not_show" value="3" />
</attr>
<attr name="touch_padding" format="dimension" />
<attr name="min_frame_size" format="dimension" />
<attr name="frame_stroke_weight" format="dimension" />
<attr name="guide_stroke_weight" format="dimension" />
<attr name="crop_enabled" format="boolean" />
<attr name="initial_frame_scale" format="float" />
</declare-styleable>
|
/**
* 加载Style自定义属性数据
*
* @param context
* @param attrs
* @param defStyleAttr
*/
private void loadStyleable(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropView, defStyleAttr, 0);
Drawable drawable;
mCropMode = CropModeEnum.SQUARE;
try {
drawable = ta.getDrawable(R.styleable.CropView_img_src);
if (drawable != null)
setImageDrawable(drawable);
for (CropModeEnum mode : CropModeEnum.values()) {
if (ta.getInt(R.styleable.CropView_crop_mode, 3) == mode.getID()) {
mCropMode = mode;
break;
}
}
mBackgroundColor = ta.getColor(R.styleable.CropView_background_color, TRANSPARENT);
mOverlayColor = ta.getColor(R.styleable.CropView_overlay_color, TRANSLUCENT_BLACK);
mFrameColor = ta.getColor(R.styleable.CropView_frame_color, WHITE);
mHandleColor = ta.getColor(R.styleable.CropView_handle_color, WHITE);
mGuideColor = ta.getColor(R.styleable.CropView_guide_color, TRANSLUCENT_WHITE);
for (ShowModeEnum mode : ShowModeEnum.values()) {
if (ta.getInt(R.styleable.CropView_guide_show_mode, 1) == mode.getId()) {
mGuideShowMode = mode;
break;
}
}
for (ShowModeEnum mode : ShowModeEnum.values()) {
if (ta.getInt(R.styleable.CropView_handle_show_mode, 1) == mode.getId()) {
mHandleShowMode = mode;
break;
}
}
setGuideShowMode(mGuideShowMode);
setHandleShowMode(mHandleShowMode);
mHandleSize = ta.getDimensionPixelSize(R.styleable.CropView_handle_size, SizeUtils.dp2px(HANDLE_SIZE));
mHandleWidth = ta.getDimensionPixelSize(R.styleable.CropView_handle_width, SizeUtils.dp2px(HANDLE_WIDTH));
mTouchPadding = ta.getDimensionPixelSize(R.styleable.CropView_touch_padding, 0);
mFrameMinSize =
ta.getDimensionPixelSize(R.styleable.CropView_min_frame_size, SizeUtils.dp2px(FRAME_MIN_SIZE));
mFrameStrokeWeight =
ta.getDimensionPixelSize(R.styleable.CropView_frame_stroke_weight, SizeUtils.dp2px(FRAME_STROKE_WEIGHT));
mGuideStrokeWeight =
ta.getDimensionPixelSize(R.styleable.CropView_guide_stroke_weight, SizeUtils.dp2px(GUIDE_STROKE_WEIGHT));
mIsCropEnabled = ta.getBoolean(R.styleable.CropView_crop_enabled, true);
mInitialFrameScale =
constrain(ta.getFloat(R.styleable.CropView_initial_frame_scale, DEFAULT_INITIAL_SCALE), 0.01f, 1.0f,
DEFAULT_INITIAL_SCALE);
} catch (Exception e) {
e.printStackTrace();
} finally {
ta.recycle();
}
}
|
<?xml version=”1.0” encoding=”utf-8”?>
<com.minminaya.crop.CropView
android:id="@+id/crop_img"
android:layout_width="match_parent"
android:layout_height="583dp"
android:layout_above="@+id/rv"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:padding="10dp"
app:crop_enabled="true"
app:crop_mode="free"
app:background_color="#66FFFFFF"
app:frame_color="@android:color/white"
app:frame_stroke_weight="2dp"
app:guide_color="#66FFFFFF"
app:guide_show_mode="show_always"
app:guide_stroke_weight="2dp"
app:handle_color="@android:color/white"
app:handle_show_mode="show_always"
app:handle_size="24dp"
app:handle_width="3dp"
app:initial_frame_scale="1"
app:min_frame_size="100dp"
app:overlay_color="#AA1C1C1C"
app:touch_padding="8dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="108dp"
android:layout_alignParentBottom="true"
android:background="@android:color/white" />
|
public class MainActivity extends AppCompatActivity {
private CropView mCropView;
private RecyclerView mRecyclerView;
private CropRecyclerViewAdapter mRecyclerViewAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = findViewById(R.id.rv);
mCropView = findViewById(R.id.crop_img);
mRecyclerViewAdapter = new CropRecyclerViewAdapter();
mCropView.setImageResource(R.mipmap.test_pic);
if (mRecyclerView != null) {
mRecyclerViewAdapter = new CropRecyclerViewAdapter();
// 数据来源
mRecyclerViewAdapter.setCommonAdapterBean(handleRvAdapterData());
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
mRecyclerView.setAdapter(mRecyclerViewAdapter);
mRecyclerViewAdapter.setOnItemClickedListener(new CropRecyclerViewAdapter.OnItemClickedListener() {
@Override
public void onClicked(View view, int position) {
handleRvItemClicked(view, position);
}
});
}
}
private CommonAdapterBean handleRvAdapterData() {
CommonAdapterBean CommonAdapterBean = new CommonAdapterBean();
List<Integer> funcPics = CommonAdapterBean.getFuncPics();
List<String> funcNames = CommonAdapterBean.getFuncNames();
String[] funcNameArrays =
new String[] {Constant.CropBean.STR_ROTATION, Constant.CropBean.STR_REVERSION,
Constant.CropBean.STR_RATIO_FREE, Constant.CropBean.STR_RATIO_SQUARE,
Constant.CropBean.STR_RATIO_2_3, Constant.CropBean.STR_RATIO_3_2,
Constant.CropBean.STR_RATIO_3_4, Constant.CropBean.STR_RATIO_4_3,
Constant.CropBean.STR_RATIO_9_16, Constant.CropBean.STR_RATIO_16_9};
for (String funcName : funcNameArrays) {
funcPics.add(R.mipmap.ic_launcher);
funcNames.add(funcName);
}
return CommonAdapterBean;
}
protected void handleRvItemClicked(View view, int position) {
switch (position) {
case Constant.CropBean.INDEX_ROTATION: {
mCropView.rotateImage(CropView.RotateDegreesEnum.ROTATE_M90D);
break;
}
case Constant.CropBean.INDEX_REVERSION: {
mCropView.reverseY();
break;
}
case Constant.CropBean.INDEX_RATIO_FREE: {
mCropView.setCropMode(CropView.CropModeEnum.FREE);
break;
}
case Constant.CropBean.INDEX_RATIO_SQUARE: {
mCropView.setCropMode(CropView.CropModeEnum.SQUARE);
break;
}
case Constant.CropBean.INDEX_RATIO_2_3: {
mCropView.setCropMode(CropView.CropModeEnum.RATIO_2_3);
break;
}
case Constant.CropBean.INDEX_RATIO_3_2: {
mCropView.setCropMode(CropView.CropModeEnum.RATIO_3_2);
break;
}
case Constant.CropBean.INDEX_RATIO_3_4: {
mCropView.setCropMode(CropView.CropModeEnum.RATIO_3_4);
break;
}
case Constant.CropBean.INDEX_RATIO_4_3: {
mCropView.setCropMode(CropView.CropModeEnum.RATIO_4_3);
break;
}
case Constant.CropBean.INDEX_RATIO_9_16: {
mCropView.setCropMode(CropView.CropModeEnum.RATIO_9_16);
break;
}
case Constant.CropBean.INDEX_RATIO_16_9: {
mCropView.setCropMode(CropView.CropModeEnum.RATIO_16_9);
break;
}
}
}
}`
效果如下
源代码
地址:https://github.com/minminaya/CropViewDemo
参考