1. 程式人生 > >基於PhotoView的頭像/圓形裁剪控件

基於PhotoView的頭像/圓形裁剪控件

過程 應該 效果 preview ppm int() 問題 measure drawing

常見的圖片裁剪有兩種,一種是圖片固定,裁剪框移動放縮來確定裁剪區域,早期見的比較多,缺點在於不能直接預覽裁剪後的效果;還有一種現在比較普遍了,就是裁剪框固定,直接拖動縮放圖片,便於預覽裁剪結果。

我做的這個控件屬於後者。一般來說,做圖片裁剪的思路無外乎是先監聽手勢,獲取坐標,再對圖片變形,最後確定裁剪區域的坐標對位圖進行裁剪,最後保存圖片到本地。我嘛還是個技術小白,一想到要監控手勢這些就頭疼,碰巧項目之前為了做查看大圖而引入了大名鼎鼎的第三方圖片查看控件——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裁剪圖片為圓形圖片的實現原理與代碼

基於PhotoView的頭像/圓形裁剪控件