基于PhotoView的头像/圆形裁剪控件

      常见的图片裁剪有两种,一种是图片固定,裁剪框移动放缩来确定裁剪区域,早期见的比较多,缺点在于不能直接预览裁剪后的效果;还有一种现在比较普遍了,就是裁剪框固定,直接拖动缩放图片,便于预览裁剪结果。

      我做的这个控件属于后者。一般来说,做图片裁剪的思路无外乎是先监听手势,获取坐标,再对图片变形,最后确定裁剪区域的坐标对位图进行裁剪,最后保存图片到本地。我嘛还是个技术小白,一想到要监控手势这些就头疼,碰巧项目之前为了做查看大图而引入了大名鼎鼎的第三方图片查看控件——PhotoView。于是转念一想,能不能把到图片变形为止的前几步交给PhotoView来搞定,我只要负责确定确定裁剪区域后面这几步呢。后来掉了好几个坑导致偷懒也没轻松多少其实ε=(´ο`*)))唉~

裁剪控件示意图                  控件层次图 

    先简要介绍一下设计思路,如上图所示,主要分为两部分,上层是遮罩(也可以理解为是裁剪框),用于预览裁剪后的效果;下层是PhotoView,这里多包了一层改为正方形显示。

  下面是遮罩的代码,比较简单,这里就不赘述了。

 1 /**
 2  * Created by MandyLu on 2018/7/14.
 3  * 圆形裁剪框
 4  */
 5 public class CircleCropView extends View {
 6     public final int CIRCLE_MARGIN = 50;
 7 
 8     public CircleCropView(Context context) {
 9         super(context);
10     }
11 
12     public CircleCropView(Context context, @Nullable AttributeSet attrs) {
13         super(context, attrs);
14     }
15 
16     public CircleCropView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
17         super(context, attrs, defStyleAttr);
18     }
19 
20     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
21     public CircleCropView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
22         super(context, attrs, defStyleAttr, defStyleRes);
23     }
24 
25     @Override
26     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
27         super.onMeasure(widthMeasureSpec, widthMeasureSpec);
28     }
29 
30     @RequiresApi(api = Build.VERSION_CODES.O)
31     @Override
32     protected void onDraw(Canvas canvas) {
33         canvas.save();
34 
35         Path path = new Path();
36         Rect viewDrawingRect = new Rect();
37         getDrawingRect(viewDrawingRect);
38 
39         float radius = viewDrawingRect.width() / 2 - CIRCLE_MARGIN;
40         path.addCircle(viewDrawingRect.left + radius + CIRCLE_MARGIN,
41                 viewDrawingRect.top + radius + CIRCLE_MARGIN, radius, Path.Direction.CW);
42 
43         Paint outsidePaint = new Paint();
44         outsidePaint.setAntiAlias(true);
45         outsidePaint.setARGB(151, 0, 0, 0);
46 
47         canvas.clipPath(path, Region.Op.DIFFERENCE);
48         canvas.drawRect(viewDrawingRect, outsidePaint);
49         canvas.restore();
50     }
51 }

      SquarePhotoView只是在PhotoView的基础上改了长宽,重写一下onMeasure方法即可:

1     @Override
2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3         super.onMeasure(widthMeasureSpec, widthMeasureSpec);
4     }

  那么现在最关键的一步,就是从PhotoView获取当前图片显示区域的Drawable或Bitmap了。粗略看了一下PhotoView的函数,并没有找到能用的(囧)。解决第一个坑的笨办法就是,自己动手丰衣足食——直接拿原图的bitmap,然后问PhotoView要当前图片的变形矩阵,自个儿通过矩阵一步步变形拿到对应的位图。

      思路其实是没问题的,然而第二个坑又出现了(囧)。这里的变形矩阵,我最早百度的结果是getSuppMatrix,源码我没有细看,但掉坑的过程中据我观察,猜测应该是对应最新一次的手势变形结果(不确定= =,也可能是其他坑综合导致的错误结果)。总之最后我查了一会源码,最终确定用的是getDisplayMatrix。

      紧接着是第三个坑,坑多了就习惯了。矩阵中的XY位移量,我起初以为是显示区域中心相对于原图中心的位移,即如果仅有缩放操作的话,位移应该为0。但实际通过特殊位置(例如取四个顶点)的裁剪结果来看,这里的XY位移量实际最后显示区域左上角的点相对原点(即原图左上角)的位移,简单点说,可以把位移量作为最终显示区域左上角的坐标。

      然后我就迎来了第四个坑(🙂)。这个坑现在回头看其实是很简单不应该栽进去的,然而当时还没想通的时候确实很慌(唉)。这个坑的问题就出在,Matrix里的值是基于手势的,也就是说,是基于屏幕像素(换句话说,是基于实际显示的图片)的。而对位图进行裁剪时,是基于原图像素的。那么这里还存在一个为了正常显示而导致的缩放比例的问题,例如原图是3000x4000,由于屏幕分辨率是1080*1920,那么实际显示时,图片是缩小了的,这个比例是9/25。所以在裁剪的过程中,需要把位移量再放大25/9倍进行还原。

  下面是裁剪部分的关键代码(最后偷了一下懒,没有裁圆形,只是用CIrcleImageView显示):

 1   fun cropImage(){
 2         var degree = ImageUtils.readPictureDegree(imagePath)
 3         var bitmap = ImageUtils.getRotatedBitmap(BitmapFactory.decodeFile(imagePath),degree)
 4 
 5         var width: Int = 0
 6         var startX: Int = 0
 7         var startY: Int = 0
 8         if (bitmap.width < bitmap.height){
 9             startY = (bitmap.height - bitmap.width) / 2
10             width = bitmap.width
11         }else{
12             startX = (bitmap.width - bitmap.height) / 2
13             width = bitmap.height
14         }
15 
16         var matrix = Matrix()
17         photo_preview.getDisplayMatrix(matrix)//获取变形矩阵,直接取scaleX或translationX没用
18         var values = FloatArray(9, {0.0f})
19         matrix.getValues(values)
20 
21         var expWidth = Math.round(bitmap.width * values[0])//缩放x
22         var expHeight = Math.round(bitmap.height * values[4])//缩放y
23         
24         var bitmap1 = Bitmap.createScaledBitmap(bitmap, expWidth, expHeight, false)
25 
26         val ratio = width * 1.0f / photo_preview.width
27         startX =  Math.round(startX * values[0] - values[2] * ratio)
28         startY = Math.round(startY * values[4] - values[5] * ratio)
29         var bitmap2 = Bitmap.createBitmap(bitmap1, startX, startY, width, width, null, false)
30 
31         saveImage(bitmap2)
32     }

   这里还有几个小坑需要解释一下:

  1. 读取bitmap时需要注意一下角度。这个是我在裁剪本地图片和网络图片的时候发现的,有些是正的有些就是转了90度。每个手机也不一定一样,所以保险起见,需要从图片的EXIF信息里面获取需要旋转的角度,然后再进一步处理。
  2. 我这里因为最终显示的是正方形,而且选的scaleType是centerCrop。所以默认就是显示中间的那一块。所以裁减时的原点也需要从正方形的左上角开始。这里是计算两种情况下的原点坐标:
    1         var startX: Int = 0
    2         var startY: Int = 0
    3         if (bitmap.width < bitmap.height){
    4             startY = (bitmap.height - bitmap.width) / 2
    5             width = bitmap.width
    6         }else{
    7             startX = (bitmap.width - bitmap.height) / 2
    8             width = bitmap.height
    9         }    

    缩放操作后,原点坐标也随之变换,乘以相应的缩放比例,再根据相应的位移量确定裁剪区域的位置。

  3. 原本想直接使用Matrix进行变形,失败(原因不明)。查看别的裁剪控件源码,决定使用createScaledBitmap来进行方法操作。

  最后还是要检讨一下:耍了小聪明想抄点近路,结果因为不熟悉源码,遇到坑的时候也只能当成黑盒;只能通过不断实验来猜测问题所在,反倒是花了更多时间,得不偿失了。以后有时间的时候,还是应该仔细研究源码,踏踏实实从原理出发解决问题(* ̄︶ ̄)~

 

最后,感谢几位博主的无私分享,特此鸣谢~

>>>Android Bitmap 常见的几个操作:缩放,裁剪,旋转,偏移

>>>Android ImageCropper 矩形 圆形 裁剪框

>>>Android裁剪图片为圆形图片的实现原理与代码

 

posted @ 2018-07-29 19:08  Mandy鹿  阅读(1002)  评论(1编辑  收藏  举报