Android 仿微信, QQ 裁剪javascript
在平时开发中,常常须要实现这样的功能,拍照 - 裁剪,相册 - 裁剪。固然,系统也有裁剪的功能,可是因为机型,系统兼容性等问题,在实际开发当中,咱们一般会本身进行实现。今天,就让咱们一块儿来看看怎样实现。java
这篇博客实现的功能主要有仿微信,QQ 上传图像裁剪功能,包括拍照,从相册选取。裁剪框的样式有圆形,正方形,九宫格。android
主要讲解的功能点git
咱们先来看看咱们实现的效果图github
拍照裁剪的 canvas
有两种调用方式bash
第一种,使用普通的 startActivityForResult 进行调用,并重写 onActivityResult 方法,在里面根据 requestCode 进行处理微信
ClipImageActivity.goToClipActivity(this, uri);
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
switch (requestCode) {
case REQ_CLIP_AVATAR: //剪切图片返回
if (resultCode == RESULT_OK) {
final Uri uri = intent.getData();
if (uri == null) {
return;
}
String cropImagePath = FileUtil.getRealFilePathFromUri(getApplicationContext(), uri);
----
}
复制代码
第二种调用 ClipImageActivity.goToClipActivity 方法,结果以 callBack 回调的方式返回回来,这种看起来比较直观点,我的也比较喜欢这种方法。它的实现原理是经过空白的 fragment 处理实现的,有兴趣的能够看我这一篇博客 Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResultide
ClipImageActivity.goToClipActivity(this, uri, new ActivityResultHelper.Callback() {
@Override
public void onActivityResult(int resultCode, Intent data) {
}
});
复制代码
从上面的效果图咱们能够看到,裁剪功能主要包括两大块post
所以,为了方便往后的修改,咱们将裁剪框的功能单独提取出来,图片缩放功能提出出来。即裁剪框单独一个 View。
下面,让咱们一块儿来看看裁剪框功能的实现。
裁剪框主要有两层,第一层,裁剪框的实现(包括圆形,长方形,九宫格形状),第二层,在裁剪区域上面盖上一层蒙层。
蒙层的实现咱们是经过 Xfermode 实现的
xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
//经过Xfermode的DST_OUT来产生中间的透明裁剪区域,必定要另起一个Layer(层)
canvas.saveLayer(0, 0, this.getWidth(), this.getHeight(), null, Canvas.ALL_SAVE_FLAG);
//设置背景
canvas.drawColor(Color.parseColor("#a8000000"));
paint.setXfermode(xfermode);
复制代码
绘制圆形裁剪框很容易实现,主要肯定圆心和半径便可
//中间的透明的圆
canvas.drawCircle(this.getWidth() / 2, this.getHeight() / 2, clipRadiusWidth, paint);
//白色的圆边框
canvas.drawCircle(this.getWidth() / 2, this.getHeight() / 2, clipRadiusWidth, borderPaint);
复制代码
绘制长方形的话主要要肯定四个点的坐标 left ,top, right, botom。 很简单
left = mHorizontalPadding;
top = this.getHeight() / 2 - clipWidth / 2;
right = this.getWidth() - mHorizontalPadding;
botom = this.getHeight() / 2 + clipWidth / 2;
复制代码
//绘制中间白色的矩形蒙层
canvas.drawRect(mHorizontalPadding, this.getHeight() / 2 - clipWidth / 2,
this.getWidth() - mHorizontalPadding, this.getHeight() / 2 + clipWidth / 2, paint);
//绘制白色的矩形边框
canvas.drawRect(mHorizontalPadding, this.getHeight() / 2 - clipWidth / 2,
this.getWidth() - mHorizontalPadding, this.getHeight() / 2 + clipWidth / 2, borderPaint);
复制代码
九宫格的绘制稍微繁琐一点,分三个步骤
咱们来看一下绘制九宫格引导线的
private void drawGuidelines(@NonNull Canvas canvas, Rect clipRect) {
final float left = clipRect.left;
final float top = clipRect.top;
final float right = clipRect.right;
final float bottom = clipRect.bottom;
final float oneThirdCropWidth = (right - left) / 3;
final float x1 = left + oneThirdCropWidth;
//引导线竖直方向第一条线
canvas.drawLine(x1, top, x1, bottom, mGuidelinePaint);
final float x2 = right - oneThirdCropWidth;
//引导线竖直方向第二条线
canvas.drawLine(x2, top, x2, bottom, mGuidelinePaint);
final float oneThirdCropHeight = (bottom - top) / 3;
final float y1 = top + oneThirdCropHeight;
//引导线水平方向第一条线
canvas.drawLine(left, y1, right, y1, mGuidelinePaint);
final float y2 = bottom - oneThirdCropHeight;
//引导线水平方向第二条线
can
复制代码
绘制四个直角的
private void drawCorners(@NonNull Canvas canvas, Rect clipRect) {
final float left = clipRect.left;
final float top = clipRect.top;
final float right = clipRect.right;
final float bottom = clipRect.bottom;
//简单的数学计算
final float lateralOffset = (mCornerThickness - mBorderThickness) / 2f;
final float startOffset = mCornerThickness - (mBorderThickness / 2f);
//左上角左面的短线
canvas.drawLine(left - lateralOffset, top - startOffset, left - lateralOffset, top + mCornerLength, mCornerPaint);
//左上角上面的短线
canvas.drawLine(left - startOffset, top - lateralOffset, left + mCornerLength, top - lateralOffset, mCornerPaint);
//右上角右面的短线
canvas.drawLine(right + lateralOffset, top - startOffset, right + lateralOffset, top + mCornerLength, mCornerPaint);
//右上角上面的短线
canvas.drawLine(right + startOffset, top - lateralOffset, right - mCornerLength, top - lateralOffset, mCornerPaint);
//左下角左面的短线
canvas.drawLine(left - lateralOffset, bottom + startOffset, left - lateralOffset, bottom - mCornerLength, mCornerPaint);
//左下角底部的短线
canvas.drawLine(left - startOffset, bottom + lateralOffset, left + mCornerLength, bottom + lateralOffset, mCornerPaint);
//右下角左面的短线
canvas.drawLine(right + lateralOffset, bottom + startOffset, right + lateralOffset, bottom - mCornerLength, mCornerPaint);
//右下角底部的短线
canvas.drawLine(right + startOffset, bottom + lateralOffset, right - mCornerLength, bottom + lateralOffset, mCornerPaint);
}
复制代码
图片裁剪框的实现到此讲解完毕,更多细节请参考 ClipView
这里咱们是经过 ClipViewLayout 实现的,它是 RelativeLayout 的子类,里面含有 ImageView 和 ClipView(裁剪框)。咱们经过监听 ClipViewLayout 的 onTouchEvent 事件,设置 imageView 的图片矩阵。
咱们先来了解一下,主要有三种模式,NONE,DRAG, ZOOM。NONE 表示初始模式,DRAG 表示拖拽模式,ZOOM 表示缩放模式
private static final int NONE = 0;
//动做标志:拖动
private static final int DRAG = 1;
//动做标志:缩放
private static final int ZOOM = 2;
复制代码
当咱们多个手指按下的时候,加入两个手指之间的距离超过 10,此时咱们认为进入 ZOOM 模式。DRAG 模式的话当咱们手指按下的时候进入。NONE 模式,当咱们手机抬起的时候,进入复位模式。
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN");
mSavedMatrix.set(mMatrix);
//设置开始点位置
mStart.set(event.getX(), event.getY());
mode = DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
//开始放下时候两手指间的距离
mOldDist = spacing(event);
if (mOldDist > 10f) {
mSavedMatrix.set(mMatrix);
midPoint(mMid, event);
mode = ZOOM;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
复制代码
接下来咱们一块儿来看一下,咱们 action_move 的时候,咱们是怎样进行移动和缩放的。
移动的话相对比较简单,首先它会计算出咱们这一次 event 事件相对咱们 action_down 时候 event 事件的偏移量 dx, dy。接着调用 mMatrix.postTranslate(dx, dy),进行矩阵的移动。最后,再检测是否超出边界。
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: mode =" + mode);
if (mode == DRAG) { //拖动
mMatrix.set(mSavedMatrix);
float dx = event.getX() - mStart.x;
float dy = event.getY() - mStart.y;
mVerticalPadding = mClipView.getClipRect().top;
mMatrix.postTranslate(dx, dy);
//检查边界
checkBorder();
}
---
mImageView.setImageMatrix(mMatrix);
复制代码
边界检测 主要是检查缩放,移动后的图片矩阵的 left, top,right, bottom 是否在图片裁剪框以内,若是在的话,须要对图片矩阵进行移动。确保边界不在裁剪框以内。
/**
* 边界检测
*/
private void checkBorder() {
RectF rect = getMatrixRectF(mMatrix);
float deltaX = 0;
float deltaY = 0;
int width = mImageView.getWidth();
int height = mImageView.getHeight();
// 若是宽或高大于屏幕,则控制范围 ; 这里的0.001是由于精度丢失会产生问题,可是偏差通常很小,因此咱们直接加了一个0.01
if (rect.width() + 0.01 >= width - 2 * mHorizontalPadding) {
// 图片矩阵的最左边 > 裁剪边框的左边
if (rect.left > mHorizontalPadding) {
deltaX = -rect.left + mHorizontalPadding;
}
// 图片矩阵的最右边 < 裁剪边框的右边
if (rect.right < width - mHorizontalPadding) {
deltaX = width - mHorizontalPadding - rect.right;
}
}
if (rect.height() + 0.01 >= height - 2 * mVerticalPadding) {
// 图片矩阵的 top > 裁剪边框的 top
if (rect.top > mVerticalPadding) {
deltaY = -rect.top + mVerticalPadding;
}
// 图片矩阵的 bottom < 裁剪边框的 bottom
if (rect.bottom < height - mVerticalPadding) {
deltaY = height - mVerticalPadding - rect.bottom;
}
}
Log.i(TAG, "checkBorder: deltaX=" + deltaX + " deltaY = " + deltaY);
mMatrix.postTranslate(deltaX, deltaY);
}
复制代码
裁剪功能的实现
/**
* 获取剪切图
*/
public Bitmap clip() {
mImageView.setDrawingCacheEnabled(true);
mImageView.buildDrawingCache();
Rect rect = mClipView.getClipRect();
Bitmap cropBitmap = null;
Bitmap zoomedCropBitmap = null;
try {
cropBitmap = Bitmap.createBitmap(mImageView.getDrawingCache(), rect.left, rect.top, rect.width(), rect.height());
// 对图片进行压缩,这里由于 mPreViewW 与宽高是相等的,因此压缩比例是 1:1,能够根据须要本身调整
zoomedCropBitmap = BitmapUtil.zoomBitmap(cropBitmap, mPreViewW, mPreViewW);
} catch (Exception e) {
e.printStackTrace();
}
if (cropBitmap != null) {
cropBitmap.recycle();
}
// 释放资源
mImageView.destroyDrawingCache();
return zoomedCropBitmap;
}
复制代码
这个 Demo 涉及到的 Android 技术点实际上是蛮多的,能够说是麻雀虽小,五脏俱全。Android 7.0 图片拍照适配,6.0 动态权限申请,Android 使用空白 fragment 处理 onActivityResult,动态权限申请,自定义 View,View 的事件分发机制等等。
这篇博客主要是介绍我的认为比较重要的技术点,其余的能够自行取了解。最后,提供一下 demo 下载地址: github.com/gdutxiaoxu/…
文中不少代码参考了如下两篇文章,在他们的基础之上进行了修改。因为时间的关系,并无对裁剪框进行更多细节化的定制,好比图片比例,自定义属性的暴露等,有兴趣的话能够本身进行添加
github 源码地址
扫一扫,欢迎关注个人公众号。若是你有好的文章,也欢迎你的投稿。