Android 仿微信, QQ 裁剪javascript
前言
在平時開發中,常常須要實現這樣的功能,拍照 - 裁剪,相冊 - 裁剪。固然,系統也有裁剪的功能,可是因爲機型,系統兼容性等問題,在實際開發當中,咱們一般會本身進行實現。今天,就讓咱們一塊兒來看看怎樣實現。java
這篇博客實現的功能主要有仿微信,QQ 上傳圖像裁剪功能,包括拍照,從相冊選取。裁剪框的樣式有圓形,正方形,九宮格。android
主要講解的功能點git
- 使用說明
- 總體的實現思路
- 裁剪框的實現
- 圖片縮放的實現,包括放大,縮小,移動,裁剪等
咱們先來看看咱們實現的效果圖github
拍照裁剪的 面試
相冊裁剪的 canvas
使用說明
有兩種調用方式微信
第一種
第一種,使用普通的 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) { } });
總體實現思路
從上面的效果圖咱們能夠看到,裁剪功能主要包括兩大塊
- 裁剪框
- 圖片的縮放,移動,裁剪等
所以,爲了方便往後的修改,咱們將裁剪框的功能單獨提取出來,圖片縮放功能提出出來。即裁剪框單獨一個 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 下載地址: https://github.com/gdutxiaoxu/clipimage
特別鳴謝
文中不少代碼參考瞭如下兩篇文章,在他們的基礎之上進行了修改。因爲時間的關係,並無對裁剪框進行更多細節化的定製,好比圖片比例,自定義屬性的暴露等,有興趣的話能夠本身進行添加
github 源碼地址
推薦閱讀
Android 面試必備 - http 與 https 協議