前言

在平时开发中,经常需要实现这样的功能,拍照 - 裁剪,相册 - 裁剪。当然,系统也有裁剪的功能,但是由于机型,系统兼容性等问题,在实际开发当中,我们通常会自己进行实现。今天,就让我们一起来看看怎样实现。

这篇博客实现的功能主要有仿微信,QQ 上传图像裁剪功能,包括拍照,从相册选取。裁剪框的样式有圆形,正方形,九宫格。

主要讲解的功能点

  1. 使用说明
  2. 整体的实现思路
  3. 裁剪框的实现
  4. 图片缩放的实现,包括放大,缩小,移动,裁剪等

我们先来看看我们实现的效果图

微信小程序美工制作背景图规格(腾讯大牛动态教学)(1)

使用说明

有两种调用方式

第一种

第一种,使用普通的 startActivityForResult 进行调用,并重写 onActivityResult 方法,在里面根据 requestCode 进行处理

1ClipImageActivity.goToClipActivity(this,uri); 2@Override 3protectedvoidonActivityResult(intrequestCode,intresultCode,Intentintent){ 4switch(requestCode){ 5caseREQ_CLIP_AVATAR://剪切图片返回 6if(resultCode==RESULT_OK){ 7finalUriuri=intent.getData(); 8if(uri==null){ 9return; 10} 11StringcropImagePath=FileUtil.getRealFilePathFromUri(getApplicationContext(),uri); 12 13 14---- 15 16}

第二种

第二种调用 ClipImageActivity.goToClipActivity 方法,结果以 callBack 回调的方式返回回来,这种看起来比较直观点,个人也比较喜欢这种方法。它的实现原理是通过空白的 fragment 处理实现的,有兴趣的可以看我这一篇博客 Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult

1ClipImageActivity.goToClipActivity(this,uri,newActivityResultHelper.Callback(){ 2@Override 3publicvoidonActivityResult(intresultCode,Intentdata){ 4 5} 6});

整体实现思路

从上面的效果图我们可以看到,裁剪功能主要包括两大块

  1. 裁剪框
  2. 图片的缩放,移动,裁剪等

因此,为了方便日后的修改,我们将裁剪框的功能单独提取出来,图片缩放功能提出出来。即裁剪框单独一个 View。

下面,让我们一起来看看裁剪框功能的实现。

裁剪框功能的实现

微信小程序美工制作背景图规格(腾讯大牛动态教学)(2)

裁剪框主要有两层,第一层,裁剪框的实现(包括圆形,长方形,九宫格形状),第二层,在裁剪区域上面盖上一层蒙层。

蒙层

蒙层的实现我们是通过 Xfermode 实现的

1xfermode=newPorterDuffXfermode(PorterDuff.Mode.DST_OUT); 2 3 4//通过Xfermode的DST_OUT来产生中间的透明裁剪区域,一定要另起一个Layer(层) 5Canvas.saveLayer(0,0,this.getWidth(),this.getHeight(),null,Canvas.ALL_SAVE_FLAG); 6 7//设置背景 8canvas.drawColor(Color.parseColor("#a8000000")); 9paint.setXfermode(xfermode);

圆形裁剪框的实现

绘制圆形裁剪框很容易实现,主要确定圆心和半径即可

1//中间的透明的圆 2canvas.drawCircle(this.getWidth()/2,this.getHeight()/2,clipRadiusWidth,paint); 3//白色的圆边框 4canvas.drawCircle(this.getWidth()/2,this.getHeight()/2,clipRadiusWidth,borderPaint);

正方形裁剪框的实现

微信小程序美工制作背景图规格(腾讯大牛动态教学)(3)

绘制长方形的话主要要确定四个点的坐标 left ,top, right, botom。很简单

1left = mHorizontalPadding; 2top=this.getHeight()/2-clipWidth/2; 3right = this.getWidth() - mHorizontalPadding; 4botom=this.getHeight()/2 clipWidth/2;

1//绘制中间白色的矩形蒙层 2canvas.drawrect(mHorizontalPadding,this.getHeight()/2-clipWidth/2, 3this.getWidth()-mHorizontalPadding,this.getHeight()/2 clipWidth/2,paint); 4//绘制白色的矩形边框 5canvas.drawRect(mHorizontalPadding,this.getHeight()/2-clipWidth/2, 6this.getWidth()-mHorizontalPadding,this.getHeight()/2 clipWidth/2,borderPaint);

九宫格的

微信小程序美工制作背景图规格(腾讯大牛动态教学)(4)

九宫格的绘制稍微繁琐一点,分三个步骤

  1. 绘制长方形边框
  2. 绘制九宫格引导线
  3. 绘制裁剪边框的是个直角

绘制长方形边框的这里就不说了,比较简单。

我们来看一下绘制九宫格引导线的

1privatevoiddrawGuidelines(@NonNullCanvascanvas,RectclipRect){ 2 3finalfloatleft=clipRect.left; 4finalfloattop=clipRect.top; 5finalfloatright=clipRect.right; 6finalfloatbottom=clipRect.bottom; 7 8finalfloatoneThirdCropWidth=(right-left)/3; 9 10finalfloatx1=left oneThirdCropWidth; 11//引导线竖直方向第一条线 12canvas.drawLine(x1,top,x1,bottom,mGuidelinePaint); 13finalfloatx2=right-oneThirdCropWidth; 14//引导线竖直方向第二条线 15canvas.drawLine(x2,top,x2,bottom,mGuidelinePaint); 16 17finalfloatoneThirdCropHeight=(bottom-top)/3; 18 19finalfloaty1=top oneThirdCropHeight; 20//引导线水平方向第一条线 21canvas.drawLine(left,y1,right,y1,mGuidelinePaint); 22finalfloaty2=bottom-oneThirdCropHeight; 23//引导线水平方向第二条线 24can

绘制四个直角的

1privatevoiddrawCorners(@NonNullCanvascanvas,RectclipRect){ 2 3finalfloatleft=clipRect.left; 4finalfloattop=clipRect.top; 5finalfloatright=clipRect.right; 6finalfloatbottom=clipRect.bottom; 7 8//简单的数学计算 9finalfloatlateralOffset=(mCornerThickness-mBorderThickness)/2f; 10finalfloatstartOffset=mCornerThickness-(mBorderThickness/2f); 11 12//左上角左面的短线 13canvas.drawLine(left-lateralOffset,top-startOffset,left-lateralOffset,top mCornerLength,mCornerPaint); 14//左上角上面的短线 15canvas.drawLine(left-startOffset,top-lateralOffset,left mCornerLength,top-lateralOffset,mCornerPaint); 16 17//右上角右面的短线 18canvas.drawLine(right lateralOffset,top-startOffset,right lateralOffset,top mCornerLength,mCornerPaint); 19//右上角上面的短线 20canvas.drawLine(right startOffset,top-lateralOffset,right-mCornerLength,top-lateralOffset,mCornerPaint); 21 22//左下角左面的短线 23canvas.drawLine(left-lateralOffset,bottom startOffset,left-lateralOffset,bottom-mCornerLength,mCornerPaint); 24//左下角底部的短线 25canvas.drawLine(left-startOffset,bottom lateralOffset,left mCornerLength,bottom lateralOffset,mCornerPaint); 26 27//右下角左面的短线 28canvas.drawLine(right lateralOffset,bottom startOffset,right lateralOffset,bottom-mCornerLength,mCornerPaint); 29//右下角底部的短线 30canvas.drawLine(right startOffset,bottom lateralOffset,right-mCornerLength,bottom lateralOffset,mCornerPaint); 31}

图片裁剪框的实现到此讲解完毕,更多细节请参考 ClipView

图片的缩放,移动

实现原理简述

这里我们是通过 ClipViewLayout 实现的,它是 RelativeLayout 的子类,里面含有 ImageView 和 ClipView(裁剪框)。我们通过监听 ClipViewLayout 的 onTouchEvent 事件,设置 imageView 的图片矩阵。

我们先来了解一下,主要有三种模式,NONE,DRAG, ZOOM。NONE 表示初始模式,DRAG 表示拖拽模式,ZOOM 表示缩放模式

1privatestaticfinalintNONE=0; 2//动作标志:拖动 3privatestaticfinalintDRAG=1; 4//动作标志:缩放 5privatestaticfinalintZOOM=2;

当我们多个手指按下的时候,加入两个手指之间的距离超过 10,此时我们认为进入 ZOOM 模式。DRAG 模式的话当我们手指按下的时候进入。NONE 模式,当我们手机抬起的时候,进入复位模式。

1switch(event.getAction()&MotionEvent.ACTION_MASK){ 2caseMotionEvent.ACTION_DOWN: 3Log.d(TAG,"onTouchEvent:ACTION_DOWN"); 4mSavedMatrix.set(mMatrix); 5//设置开始点位置 6mStart.set(event.getX(),event.getY()); 7mode=DRAG; 8break; 9caseMotionEvent.ACTION_POINTER_DOWN: 10//开始放下时候两手指间的距离 11mOldDist=spacing(event); 12if(mOldDist>10f){ 13mSavedMatrix.set(mMatrix); 14midPoint(mMid,event); 15mode=ZOOM; 16} 17break; 18caseMotionEvent.ACTION_UP: 19caseMotionEvent.ACTION_POINTER_UP: 20mode=NONE; 21break;

接下来我们一起来看一下,我们 action_move 的时候,我们是怎样进行移动和缩放的。

移动的话相对比较简单,首先它会计算出我们这一次 event 事件相对我们 action_down 时候 event 事件的偏移量 dx, dy。接着调用 mMatrix.postTranslate(dx, dy),进行矩阵的移动。最后,再检测是否超出边界。

1caseMotionEvent.ACTION_MOVE: 2Log.d(TAG,"onTouchEvent:mode=" mode); 3if(mode==DRAG){//拖动 4mMatrix.set(mSavedMatrix); 5floatdx=event.getX()-mStart.x; 6floatdy=event.getY()-mStart.y; 7 8mVerticalPadding=mClipView.getClipRect().top; 9mMatrix.postTranslate(dx,dy); 10//检查边界 11checkBorder(); 12} 13--- 14mImageView.setImageMatrix(mMatrix);

边界检测 主要是检查缩放,移动后的图片矩阵的 left, top,right, bottom 是否在图片裁剪框之内,如果在的话,需要对图片矩阵进行移动。确保边界不在裁剪框之内。

1/** 2*边界检测 3*/ 4privatevoidcheckBorder(){ 5RectFrect=getMatrixRectF(mMatrix); 6floatdeltaX=0; 7floatdeltaY=0; 8intwidth=mImageView.getWidth(); 9intheight=mImageView.getHeight(); 10//如果宽或高大于屏幕,则控制范围;这里的0.001是因为精度丢失会产生问题,但是误差一般很小,所以我们直接加了一个0.01 11if(rect.width() 0.01>=width-2*mHorizontalPadding){ 12//图片矩阵的最左边>裁剪边框的左边 13if(rect.left>mHorizontalPadding){ 14deltaX=-rect.left mHorizontalPadding; 15} 16//图片矩阵的最右边<裁剪边框的右边 17if(rect.right<width-mHorizontalPadding){ 18deltaX=width-mHorizontalPadding-rect.right; 19} 20} 21if(rect.height() 0.01>=height-2*mVerticalPadding){ 22//图片矩阵的top>裁剪边框的top 23if(rect.top>mVerticalPadding){ 24deltaY=-rect.top mVerticalPadding; 25} 26//图片矩阵的bottom<裁剪边框的bottom 27if(rect.bottom<height-mVerticalPadding){ 28deltaY=height-mVerticalPadding-rect.bottom; 29} 30} 31 32Log.i(TAG,"checkBorder:deltaX=" deltaX "deltaY=" deltaY); 33 34mMatrix.postTranslate(deltaX,deltaY); 35}

裁剪功能的实现

1/** 2*获取剪切图 3*/ 4publicBitmapclip(){ 5mImageView.setDrawingCacheEnabled(true); 6mImageView.buildDrawingCache(); 7Rectrect=mClipView.getClipRect(); 8BitmapcropBitmap=null; 9BitmapzoomedCropBitmap=null; 10try{ 11cropBitmap=Bitmap.createBitmap(mImageView.getDrawingCache(),rect.left,rect.top,rect.width(),rect.height()); 12//对图片进行压缩,这里因为mPreViewW与宽高是相等的,所以压缩比例是1:1,可以根据需要自己调整 13zoomedCropBitmap=BitmapUtil.zoomBitmap(cropBitmap,mPreViewW,mPreViewW); 14}catch(Exceptione){ 15e.printStackTrace(); 16} 17if(cropBitmap!=null){ 18cropBitmap.recycle(); 19} 20//释放资源 21mImageView.destroyDrawingCache(); 22returnzoomedCropBitmap; 23}

题外话

这个 Demo 涉及到的 Android 技术点其实是蛮多的,可以说是麻雀虽小,五脏俱全。Android 7.0 图片拍照适配,6.0 动态权限申请,Android 使用空白 fragment 处理 onActivityResult,动态权限申请,自定义 View,View 的事件分发机制等等。

这篇博客主要是介绍个人认为比较重要的技术点,其他的可以自行取了解。最后,提供一下 demo 下载地址:https://github.com/gdutxiaoxu/clipimagea

文章不易,如果大家喜欢这篇文章,或者对你有帮助希望大家多多,点赞,转发,关注 哦。文章会持续更新的。绝对干货!!!

,