前言及效果展示
最近项目里要用到图片裁剪的功能,于是乎写了一个支持裁剪的自定义View
然后做了一个简单的demo项目。在这里分享出来
先来看下具体的效果吧
首先是一个入口页面,就是一个按钮,支持跳转到裁剪页面
当然这里会传一张固定的图片进去
裁剪界面我做成了一个Activity
长这样:
最后,裁剪完了之后把图片带回去
下面看下具体的实现介绍
文末会放上demo代码
核心工具类
现在安装裁剪的流程来介绍核心的一些实现
完整的内容可以参看文末的源码
首先看首页Mainactivity是如何跳转的:
findViewById(R.id.clipimgBtn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, ClipPictureActivity.class);
String imgpath = "/storage/emulated/0/DCIM/test.PNG";
ClipPictureStrategy clipPictureStrategy = new ClipPictureStrategy(imgpath);
clipPictureStrategy.setClipmsg("裁剪图片");
intent.putExtra("strategy", clipPictureStrategy);
startActivityForResult(intent, requestCode);
}
});
这边跳转裁剪页面时,把参数列表做成了一个实体类ClipPictureStrategy
这个类可以用来定制我们要裁剪的一些参数
其中图片路径是必须传的
其他的有默认值
这块提一下,其实这里可以用kotlin实现
或者采用建造者模式
但因为是demo,就不过分炫技了
这个类里面目前有下面这些属性
相关属性的作用都在下面注释了
private String imagePath = "";//图片路径
private String clipmsg = "";//裁剪框底部文案
private float screenHeightFloat = 0.3f;//裁剪框距离顶部占据屏幕高度的百分比(默认距离屏幕顶部的距离为屏幕高度的30%)
private float screenWidthFloat = 0.6f;//裁剪框的宽占据屏幕宽度的百分比(默认宽度为屏幕宽度的60%)
有了这些属性,我们在绘制页面时,就可以方便地处理裁剪框的位置和大小了
当然,这边裁剪框是正方形的,也可以改成矩形
下面来到裁剪页面
这个页面的功能,其实都是由QRCropImageView这个自定义View来实现的
它里面支持对图片的旋转,缩放,平移
然后根据裁剪框的大小来裁剪图片
这边也把源码贴上来了
/**
* @introduce : 裁剪图片控件
* <p>
* creattime : 2021/11/25
* <p>
* author : xiongyp
**/
@SuppressLint("AppCompatCustomView")
public class QRCropImageView extends ImageView {
private float x_down = 0;
private float y_down = 0;
private PointF mid = new PointF();
private float oldDist = 1f;
private float oldRotation = 0;
private Matrix matrix;
private Matrix matrix1 = new Matrix();
private Matrix savedMatrix = new Matrix();
private static final int NONE = 0;
private static final int DRAG = 1;
private static final int ZOOM = 2;
private int mode = NONE;
private boolean matrixCheck = false;
private int widthScreen;
private int heightScreen;
private Bitmap gintama;
private Bitmap drawBitmap;
private Paint paintLine;
private Paint paintShade;
private Paint paintCorner;
private Rect rectTop;
private Rect rectBottom;
private Rect rectLeft;
private Rect rectRight;
private Rect cropRect;
private int lineWidth;
private int cornerWidth;
private int cornerLength;
private int paddingH;
private Activity activity;
private int left, top, right, bottom;
private int rectHeight = 0;
private int marginTop = 0;
private int width2;
private int width;
private int height;
public QRCropImageView(Activity activity, Bitmap bitmap) {
super(activity);
gintama = bitmap;
this.activity = activity;
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
widthScreen = dm.widthPixels;
heightScreen = dm.heightPixels;
matrix = new Matrix();
paintLine = new Paint(Paint.ANTI_ALIAS_FLAG);
paintLine.setColor(Color.WHITE);
paintLine.setStyle(Paint.Style.STROKE);//设置空心
lineWidth = (int) dp2px(activity, 0.5f);
paintLine.setStrokeWidth(lineWidth);
paintShade = new Paint(Paint.ANTI_ALIAS_FLAG);
paintShade.setColor(Color.parseColor("#CC000000"));
cornerWidth = (int) dp2px(activity, 4);
cornerLength = (int) dp2px(activity, 25);
paintCorner = new Paint(Paint.ANTI_ALIAS_FLAG);
paintCorner.setColor(Color.WHITE);
paintCorner.setStrokeWidth(cornerWidth);
paddingH = (int) dp2px(activity, 30);
// 初始化居中缩放
int bw = gintama.getWidth();
int bh = gintama.getHeight();
float scale = Math.min(1f * widthScreen / bw, 1f * heightScreen / bh);
Log.e("scale==", "" + scale);
drawBitmap = scaleBitmap(gintama, scale);
matrix.postTranslate(0, 1f * heightScreen / 2 - (bh * scale / 2));
}
protected void onDraw(Canvas canvas) {
width = getWidth();
height = getHeight();
canvas.save();
canvas.drawBitmap(drawBitmap, matrix, null);
canvas.restore();
paddingH = (width - rectHeight) / 2;
// 四边线
left = paddingH;
top = marginTop;
right = width - paddingH;
// bottom = (int) (height * 0.51);
bottom = top + rectHeight;
if (rectTop == null) {
rectTop = new Rect(0, 0, width, top);
rectBottom = new Rect(0, bottom, width, height);
rectLeft = new Rect(0, top, left, bottom);
rectRight = new Rect(right, top, width, bottom);
}
//绘制上下蒙层
canvas.drawRect(rectTop, paintShade);
canvas.drawRect(rectBottom, paintShade);
//绘制左右蒙层
canvas.drawRect(rectLeft, paintShade);
canvas.drawRect(rectRight, paintShade);
// 裁切区域矩形
if (cropRect == null) {
cropRect = new Rect(left, top, right, bottom);
}
canvas.drawRect(cropRect, paintLine);
width2 = cornerWidth / 2;
// 绘制四角
canvas.drawLine(left, top + width2, left + cornerLength, top + width2, paintCorner);
canvas.drawLine(left + width2, top, left + width2, top + cornerLength, paintCorner);
canvas.drawLine(right, top + width2, right - cornerLength, top + width2, paintCorner);
canvas.drawLine(right - width2, top, right - width2, top + cornerLength, paintCorner);
canvas.drawLine(left, bottom - width2, left + cornerLength, bottom - width2, paintCorner);
canvas.drawLine(left + width2, bottom, left + width2, bottom - cornerLength, paintCorner);
canvas.drawLine(right, bottom - width2, right - cornerLength, bottom - width2, paintCorner);
canvas.drawLine(right - width2, bottom, right - width2, bottom - cornerLength, paintCorner);
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mode = DRAG;
x_down = event.getX();
y_down = event.getY();
savedMatrix.set(matrix);
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode = ZOOM;
oldDist = spacing(event);
oldRotation = rotation(event);
savedMatrix.set(matrix);
midPoint(mid, event);
break;
case MotionEvent.ACTION_MOVE:
if (mode == ZOOM) {
matrix1.set(savedMatrix);
float rotation = rotation(event) - oldRotation;
float newDist = spacing(event);
float scale = newDist / oldDist;
matrix1.postScale(scale, scale, mid.x, mid.y);// 縮放
matrix1.postRotate(rotation, mid.x, mid.y);// 旋轉
matrixCheck = matrixCheck();
if (matrixCheck == false) {
matrix.set(matrix1);
invalidate();
}
} else if (mode == DRAG) {
matrix1.set(savedMatrix);
matrix1.postTranslate(event.getX() - x_down, event.getY()
- y_down);// 平移
matrixCheck = matrixCheck();
matrixCheck = matrixCheck();
if (matrixCheck == false) {
matrix.set(matrix1);
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
}
return true;
}
private boolean matrixCheck() {
float[] f = new float[9];
matrix1.getValues(f);
// 图片4个顶点的坐标
float x1 = f[0] * 0 + f[1] * 0 + f[2];
float y1 = f[3] * 0 + f[4] * 0 + f[5];
float x2 = f[0] * drawBitmap.getWidth() + f[1] * 0 + f[2];
float y2 = f[3] * drawBitmap.getWidth() + f[4] * 0 + f[5];
float x3 = f[0] * 0 + f[1] * drawBitmap.getHeight() + f[2];
float y3 = f[3] * 0 + f[4] * drawBitmap.getHeight() + f[5];
float x4 = f[0] * drawBitmap.getWidth() + f[1] * drawBitmap.getHeight() + f[2];
float y4 = f[3] * drawBitmap.getWidth() + f[4] * drawBitmap.getHeight() + f[5];
// 图片现宽度
double width = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
// 缩放比率判断
if (width < widthScreen / 3 || width > widthScreen * 3) {
return true;
}
// 出界判断
if ((x1 < widthScreen / 3 && x2 < widthScreen / 3
&& x3 < widthScreen / 3 && x4 < widthScreen / 3)
|| (x1 > widthScreen * 2 / 3 && x2 > widthScreen * 2 / 3
&& x3 > widthScreen * 2 / 3 && x4 > widthScreen * 2 / 3)
|| (y1 < heightScreen / 3 && y2 < heightScreen / 3
&& y3 < heightScreen / 3 && y4 < heightScreen / 3)
|| (y1 > heightScreen * 2 / 3 && y2 > heightScreen * 2 / 3
&& y3 > heightScreen * 2 / 3 && y4 > heightScreen * 2 / 3)) {
return true;
}
return false;
}
// 旋转
public void drawRotation(int rotation) {
matrix.preRotate(rotation, (float) drawBitmap.getWidth() / 2, (float) drawBitmap.getHeight() / 2); //要旋转的角度
invalidate();
}
// 触碰两点间距离
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
// 取手势中心点
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
// 取旋转角度
private float rotation(MotionEvent event) {
double delta_x = (event.getX(0) - event.getX(1));
double delta_y = (event.getY(0) - event.getY(1));
double radians = Math.atan2(delta_y, delta_x);
return (float) Math.toDegrees(radians);
}
// 将移动,缩放以及旋转后的图层保存为新图片
// 本例中沒有用到該方法,需要保存圖片的可以參考
public Bitmap createCropBitmap() {
// 创建旋转缩放样本
Bitmap bitmap = Bitmap.createBitmap(widthScreen, heightScreen,
Bitmap.Config.ARGB_8888); // 背景图片
Canvas canvas = new Canvas(bitmap); // 新建画布
canvas.drawBitmap(drawBitmap, matrix, null); // 画图片
canvas.save(); // 保存画布
canvas.restore();
// 返回裁切样本
return Bitmap.createBitmap(bitmap, cropRect.left, cropRect.bottom - (cropRect.bottom - cropRect.top)
, cropRect.right - cropRect.left, cropRect.bottom - cropRect.top);
}
/**
* 根据给定的宽和高进行拉伸
*
* @param origin 原图
* @param scale 缩放比例
* @return new Bitmap
*/
private Bitmap scaleBitmap(Bitmap origin, float scale) {
if (origin == null) {
return null;
}
int height = origin.getHeight();
int width = origin.getWidth();
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);// 使用后乘
Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
return newBM;
}
public int getRectHeight() {
return rectHeight;
}
//矩形的宽高
public void setRectHeight(int rectHeight) {
this.rectHeight = rectHeight;
invalidate();
}
//距离顶部的高度
public void setMarginTop(int marginTop) {
this.marginTop = marginTop;
invalidate();
}
public int getMarginTop() {
return marginTop;
}
/**
* 将dp单位转成px
*
* @param context context {@link Context}
* @param value value
* @return 转化之后的值
*/
public float dp2px(@Nullable Context context, float value) {
if (context == null) {
return Float.MIN_EXPONENT;
}
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics);
}
}
裁剪页面这边还有一些对于图片的压缩功能,也做成了一个工具类
/**
* @introduce :
*
* creattime : 2021/12/13
*
* author : xiongyp
*
**/
public class StreamUtil {
/**
* 根据路径获得图片并压缩,返回bitmap(票小秘压缩方案)
*
* @param filePath filePath
* @return Bitmap
*/
public static Bitmap getSmallBitmap(String filePath, int srcWidth, int srcHeight) {
try {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
// Calculate inSampleSize
options.inSampleSize = computeSize(srcWidth, srcHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
return bitmap;
} catch (Exception e) {
Log.e("CassStreamUtil", e.getMessage());
}
return null;
}
/**
* 这里的算法是以短边压缩到1000~2000之间为目标,通过计算到1000的比值,然后需要将采样率控制为2的倍数
* 所以需要使用方法{@link #calInSampleSize(int)}进行计算
* (票小秘压缩方案)
*
* @return 采样率
*/
public static int computeSize(int srcWidth, int srcHeight) {
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;
int shortSide = Math.min(srcWidth, srcHeight);
int rate = (int) Math.floor(shortSide / 1000.0);
return calInSampleSize(rate);
}
/**
* 通过移位操作计算采样率,是某个整数对应的二进制数保留最高位为1,其他位置为0的结果
* (票小秘压缩方案)
*
* @param rate 比例
* @return 采样率
*/
private static int calInSampleSize(int rate) {
int i = 0;
while ((rate >> (++i)) != 0) ;
return 1 << --i;
}
}
当然,如果你想和demo里一样要保存图片,记得申请存储权限
一些异常
在运行demo时,发现了系统的一个API的问题
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
这个decodeFile返回的bitmap为null
查阅了相关资料,解决方法如下:
在配置清单的Application标签里加入如下内容即可:
android:requestLegacyExternalStorage="true"
源码放送
百度网盘如下:
链接:https://pan.baidu.com/s/1rkeWDYOwSNoFcENPOahQHA
提取码:wks2