可能是最詳細(xì)的UCrop源碼解析

UCrop是目前比較火的開源android圖片剪裁框架,效果如下:

preview.gif

地址:
Git源碼地址

本文重點(diǎn)解釋核心功能代碼变勇,梳理項(xiàng)目流程坦报,建議閱讀時(shí)結(jié)合源碼一起看~司抱。


業(yè)務(wù)流程:

選擇圖片(從系統(tǒng)圖片庫選擇圖片)→ 放置圖片(把圖片放置到操作臺)→ 操作圖片(包括旋轉(zhuǎn),縮放,位移等操作來得到需要的圖片)→ 剪裁圖片(根據(jù)原始比例剪裁框剪裁目標(biāo)圖片或根據(jù)給定的比例剪裁)→ 獲得目標(biāo)圖片(返回bitmap并保存到本地)

代碼結(jié)構(gòu)解析:

項(xiàng)目使用Bulider設(shè)計(jì)模式碴犬,結(jié)構(gòu)功能分工明確,下面就來看看作者是怎么實(shí)現(xiàn)的梆暮,注意看核心代碼的注釋
代碼結(jié)構(gòu)大致分為三個(gè)部分:

  • 第一部分: UCropAcitivity(圖片的操作窗口)
    它主要作為項(xiàng)目的入口和一些初始化工作以及加載自定義View服协;
    使用時(shí)直接傳入一個(gè)目標(biāo)圖片的URI和目標(biāo)存儲文件的URI即可開始剪裁。
    這里作者封裝了一個(gè)UCropActivity的helper類UCrop啦粹,把startActivityForResult和Bundle封裝在了里面偿荷,Options內(nèi)部類用作用戶初始參數(shù)配置包括(setToolbarCropDrawable設(shè)置toolbar圖片,setToolbarTitle標(biāo)題等等)唠椭。

    UCropActivity中包含自定義UCropView跳纳,UCropView中包含 GestureCropImageView和OverlayView兩個(gè)自定義View。

    OverlayView負(fù)責(zé)繪制剪裁框
    GestureCropImageView負(fù)責(zé)操作選擇圖片

  • 第二部分:OverlayView-負(fù)責(zé)繪制剪裁框

    View在初始化的時(shí)候做了判斷贪嫂,當(dāng)系統(tǒng)大于3.0小于4.3時(shí)啟動硬件加速寺庄,為什么選擇這個(gè)區(qū)間呢,因?yàn)樵赼ndroid系統(tǒng)3.0之前android還不支持硬件加速力崇,而在4.3之后Android 4.4斗塘,也就是KitKat版本,這個(gè)版本對內(nèi)存方法做了很大的優(yōu)化:一方面是通過優(yōu)化內(nèi)存使用餐曹,另一方面是可選地支持使用ART運(yùn)行時(shí)替換Dalvik虛擬機(jī)逛拱,來提高應(yīng)用程序的運(yùn)行效率,所以選擇4.4之后不開啟硬件加速台猴。

    剪裁框的繪制主要分為兩個(gè)部分:
    1朽合、drawlines()繪制剪裁框內(nèi)分割線段,
    2、drawRect()繪制矩形饱狂,從而形成了一個(gè)帶橫縱線平均分割的矩形剪裁框

    核心方法:
    drawCropGrid()該方法主要功能為繪制剪裁框,根據(jù)設(shè)定的剪裁框分割線數(shù)量使用canvas.drawlines()方法繪制分割線和限制矩形曹步,具體代碼如下:
    繪制剪裁框:
    protected void drawCropGrid(@NonNull Canvas canvas) {
    //判斷是否顯示剪裁框
    if (mShowCropGrid) {
    //判斷矩形數(shù)據(jù)是否為空,mGridPoints如果等于空的話進(jìn)入填充數(shù)據(jù)
    if (mGridPoints == null && !mCropViewRect.isEmpty()) {
    //該數(shù)組為canvas.drawLines的第一個(gè)參數(shù)休讳,該參數(shù)要求其元素個(gè)數(shù)為4的倍數(shù)
    mGridPoints = new float[(mCropGridRowCount) * 4 + (mCropGridColumnCount) * 4];
    int index = 0;
    //組裝數(shù)據(jù)举瑰,數(shù)據(jù)為每一組線段的坐標(biāo)點(diǎn)
    for (int i = 0; i < mCropGridRowCount; i++) {
    mGridPoints[index++] = mCropViewRect.left;
    mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;
    mGridPoints[index++] = mCropViewRect.right;
    mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;
    }

              for (int i = 0; i < mCropGridColumnCount; i++) {
                  mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left;
                  mGridPoints[index++] = mCropViewRect.top;
                  mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left;
                  mGridPoints[index++] = mCropViewRect.bottom;
              }
          }
          //繪制線段
          if (mGridPoints != null) {
              canvas.drawLines(mGridPoints, mCropGridPaint);
          }
      }
      //繪制矩形包裹線段
      if (mShowCropFrame) {
          canvas.drawRect(mCropViewRect, mCropFramePaint);
      }
      //繪制邊角包裹,mFreestyleCropMode此參數(shù)如果等于1的話 剪裁框?yàn)榭梢苿訝顟B(tài)科盛,一般不用
      if (mFreestyleCropMode != FREESTYLE_CROP_MODE_DISABLE) {
          canvas.save();
    
          mTempRect.set(mCropViewRect);
          mTempRect.inset(mCropRectCornerTouchAreaLineLength, -mCropRectCornerTouchAreaLineLength);
          canvas.clipRect(mTempRect, Region.Op.DIFFERENCE);
    
          mTempRect.set(mCropViewRect);
          mTempRect.inset(-mCropRectCornerTouchAreaLineLength, mCropRectCornerTouchAreaLineLength);
          canvas.clipRect(mTempRect, Region.Op.DIFFERENCE);
    
          canvas.drawRect(mCropViewRect, mCropFrameCornersPaint);
    
          canvas.restore();
      }
    

    }
    這部分主要負(fù)責(zé)第一層的剪裁框繪制,保存剪裁框矩陣用于后期剪裁用


第三部分:GestureCropImageView-負(fù)責(zé)操作選擇圖片

這一部分應(yīng)該是項(xiàng)目最核心的部分,實(shí)現(xiàn)邏輯作者在他的說明文章中也說的比較清楚罢坝。
這一部分的邏輯解耦做的非常好棵癣,把View的功能邏輯劃分為3層过椎,每一層負(fù)責(zé)各自的功能:

  • 第一層:
    TransformImageView extends ImageView
    他的工作:
    1.從圖片源拿到圖片
    2.將矩陣進(jìn)行轉(zhuǎn)換(平移橡庞、縮放、旋轉(zhuǎn))留晚,并應(yīng)用到當(dāng)前圖片上
    這一層并不知道裁剪或者手勢等行為酵紫,但提供了手勢行為操作供子類調(diào)用。
    這里作者選擇使用ImageView的Matrix做旋轉(zhuǎn)縮放而沒有使用重寫onDraw方法,因?yàn)閛nDraw方法重繪有可能會有閃屏的情況而且在性能比較差的機(jī)器上可能體驗(yàn)會很差奖地,使用Matrix則會好許多橄唬。
    Matrix在android中其實(shí)一個(gè)3*3的矩陣如下:
    MSCALE_X MSKEW_X MTRANS_X
    MSKEY_Y MSCALE_Y MTRANS_Y
    MPERSP_0 MPERSP_1 MPERSP_2
    Matrix對圖像的處理分為四類基本變換:
    Translate 平移變換
    Rotate 旋轉(zhuǎn)變換
    Scale 縮放變換
    Skew 錯(cuò)切變換
    在項(xiàng)目中只用到了前三種,它們對應(yīng)的api如下:
    //平移變換参歹,在X軸上平移x多個(gè)距離仰楚,在Y軸上平移y多個(gè)距離
    postTranslate(float x,floaty);

    //旋轉(zhuǎn)變換,degrees旋轉(zhuǎn)度數(shù)泽示,px缸血,py旋轉(zhuǎn)原點(diǎn)
    postRotate(float degrees,float px,float py);

    //縮放變換 x變換距離蜜氨,y變換距離械筛,px,py縮放原點(diǎn)
    postScale(float sx,float sx,float px,float py);

    作者在這一層寫了postTranslate飒炎,postScale埋哟,postRotate三個(gè)方法暴露給子類調(diào)用,封裝Matrix屬性后在調(diào)用setImageMatrix()實(shí)現(xiàn)變換效果郎汪,mCurrentImageMatrix.mapPoints這個(gè)方法為更新當(dāng)前圖像的角點(diǎn)和存儲的中心點(diǎn)赤赊,這些變量在CropImageView中會被使用到.

    關(guān)于Matrix的用法不熟悉的可以看這里Matrix
    Matrix原理

  • 第二層:
    CropImageView extends TransformImageView
    他要做的事:
    1.畫出裁剪的邊界和網(wǎng)格
    2.為裁剪區(qū)域設(shè)置一張圖片(如果用戶對圖片操作導(dǎo)致裁剪區(qū)域內(nèi)出現(xiàn)了空白,那么圖片應(yīng)該要自動移動到邊界填充空白區(qū)域)
    3.繼承父親的方法煞赢,使用更精細(xì)的規(guī)則來操作矩陣(限制最小和最大的縮放比例)
    4.添加方法和縮小的方法(動畫變換)
    5.裁剪圖片
    這一層幾乎囊括了所有的要對圖片進(jìn)行變換和裁剪的所有操作,但也僅僅是指明了做這些事情的方法抛计,我們還需要支持手勢。

    在這一層中作者重寫了onImageLaidOut方法照筑,該方法為上層View TransformImageView的圖片加載完畢回調(diào)方法吹截,在此方法中作者設(shè)置了初始要剪裁的矩形,并且把圖片移動到屏幕居中的位置操作(因?yàn)镮mageView設(shè)置的ScaleType為Matrix所以圖像一開始是默認(rèn)在屏幕上方的)

    這一層中的操作大概可以分為三步操作:圖片偏移剪裁框偏移計(jì)算凝危、圖片歸位動畫處理波俄,剪裁圖片
    第一步(也是最復(fù)雜的一種):
    當(dāng)手指離開屏幕時(shí)要保證圖片處于剪裁區(qū)域中如果不在剪裁區(qū)域中通過位移變換來移動到剪裁區(qū)域,看代碼:

     public void setImageToWrapCropBounds(boolean animate) {
      //如果圖片加載完畢并且圖片不處于剪裁區(qū)域
      if (mBitmapLaidOut && !isImageWrapCropBounds()) {
    
          //獲取中心點(diǎn)X,Y坐標(biāo)
          float currentX = mCurrentImageCenter[0];
          float currentY = mCurrentImageCenter[1];
          //獲取縮放比例
          float currentScale = getCurrentScale();
    
          //獲取偏移距離
          float deltaX = mCropRect.centerX() - currentX;
          float deltaY = mCropRect.centerY() - currentY;
          float deltaScale = 0;
    
          mTempMatrix.reset();
          mTempMatrix.setTranslate(deltaX, deltaY);
    
          final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
          mTempMatrix.mapPoints(tempCurrentImageCorners);
    
          //判斷圖片是否包含在剪裁區(qū)域
          boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);
    
          //如果包含在剪裁區(qū)域
          if (willImageWrapCropBoundsAfterTranslate) {
              //獲取偏移的距離
              final float[] imageIndents = calculateImageIndents();
              //偏移的距離蛾默,橫坐標(biāo)加橫坐標(biāo) 縱坐標(biāo)加縱坐標(biāo)
              deltaX = -(imageIndents[0] + imageIndents[2]);
              deltaY = -(imageIndents[1] + imageIndents[3]);
          } else {
              //如果不包含在剪裁區(qū)域懦铺,創(chuàng)建臨時(shí)矩形
              RectF tempCropRect = new RectF(mCropRect);
              mTempMatrix.reset();
              //設(shè)置偏移角度
              mTempMatrix.setRotate(getCurrentAngle());
              mTempMatrix.mapRect(tempCropRect);
    
              //獲得矩形的邊長坐標(biāo)
              final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);
              //獲取放大比例
              deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],
                      tempCropRect.height() / currentImageSides[1]);
              deltaScale = deltaScale * currentScale - currentScale;
          }
    
          //如果需要?jiǎng)赢?      if (animate) {
              post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(
                      CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,
                      currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));
          } else {
              //不需要?jiǎng)赢嫞苯右苿拥侥繕?biāo)位置
              postTranslate(deltaX, deltaY);
              if (!willImageWrapCropBoundsAfterTranslate) {
                  zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY());
              }
          }
      }
    

    }

第二步:作者在這里使用了一個(gè)Runable線程來操作支鸡,使用時(shí)間差值的計(jì)算來移動動畫冬念,使動畫看起來更真實(shí)

此方法主要處理偏移回歸的動畫 寫在一個(gè)Runable子線程中
/**
 * This Runnable is used to animate an image so it fills the crop bounds entirely.
 * Given values are interpolated during the animation time.
 * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
 * or when certain conditions inside {@link WrapCropBoundsRunnable#run()} method are triggered.
 * 在這里,我計(jì)算出當(dāng)前流逝的時(shí)間牧挣,使用CubicEasing這個(gè)類急前,我對平移量和縮放量進(jìn)行插值操作。
 * 使用插值器替換過的值確實(shí)可以改善你的動畫浸踩,使人們的眼睛看起來更自然叔汁。
 * 最終,這些值被應(yīng)用到圖片矩陣,當(dāng)時(shí)間溢出或者圖片完全填充了裁剪區(qū)域的時(shí)候据块,Runnable任務(wù)就會停止码邻。
 */
private static class WrapCropBoundsRunnable implements Runnable {

    private final WeakReference<CropImageView> mCropImageView;

    private final long mDurationMs, mStartTime;
    private final float mOldX, mOldY;
    private final float mCenterDiffX, mCenterDiffY;
    private final float mOldScale;
    private final float mDeltaScale;
    private final boolean mWillBeImageInBoundsAfterTranslate;

    public WrapCropBoundsRunnable(CropImageView cropImageView,
                                  long durationMs,
                                  float oldX, float oldY,
                                  float centerDiffX, float centerDiffY,
                                  float oldScale, float deltaScale,
                                  boolean willBeImageInBoundsAfterTranslate) {

        mCropImageView = new WeakReference<>(cropImageView);

        mDurationMs = durationMs;
        mStartTime = System.currentTimeMillis();
        mOldX = oldX;
        mOldY = oldY;
        mCenterDiffX = centerDiffX;
        mCenterDiffY = centerDiffY;
        mOldScale = oldScale;
        mDeltaScale = deltaScale;
        mWillBeImageInBoundsAfterTranslate = willBeImageInBoundsAfterTranslate;
    }

    @Override
    public void run() {
        CropImageView cropImageView = mCropImageView.get();
        if (cropImageView == null) {
            return;
        }

        long now = System.currentTimeMillis();
        //花費(fèi)的時(shí)間,最多500ms另假,
        float currentMs = Math.min(mDurationMs, now - mStartTime);

        //計(jì)算出當(dāng)前流逝的時(shí)間像屋,我對平移量和縮放量進(jìn)行插值操作。
        float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
        float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
        float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);

        //如果時(shí)間溢出 停止任務(wù)
        if (currentMs < mDurationMs) {
            cropImageView.postTranslate(newX - (cropImageView.mCurrentImageCenter[0] - mOldX), newY - (cropImageView.mCurrentImageCenter[1] - mOldY));
            if (!mWillBeImageInBoundsAfterTranslate) {
                cropImageView.zoomInImage(mOldScale + newScale, cropImageView.mCropRect.centerX(), cropImageView.mCropRect.centerY());
            }
            //如果圖片還沒填充滿剪裁區(qū)域边篮,繼續(xù)移動
            if (!cropImageView.isImageWrapCropBounds()) {
                cropImageView.post(this);
            }
        }
    }
}
另一個(gè)Runable方法類己莺,用于雙擊放大時(shí)使用

同樣使用了時(shí)間差值計(jì)算偏移大小動畫
MaxScale為圖片最大的放大值,大小為最小尺寸的10倍
minScale為圖片縮小的最小值戈轿,大小為初始矩形的寬和高分別除以剪裁框的寬高取最小值凌受。

/**
 * This Runnable is used to animate an image zoom.
 * Given values are interpolated during the animation time.
 * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
 * or when certain conditions inside {@link ZoomImageToPosition#run()} method are triggered.
 */
private static class ZoomImageToPosition implements Runnable {

    private final WeakReference<CropImageView> mCropImageView;

    private final long mDurationMs, mStartTime;
    private final float mOldScale;
    private final float mDeltaScale;
    private final float mDestX;
    private final float mDestY;

    public ZoomImageToPosition(CropImageView cropImageView,
                               long durationMs,
                               float oldScale, float deltaScale,
                               float destX, float destY) {

        mCropImageView = new WeakReference<>(cropImageView);

        mStartTime = System.currentTimeMillis();
        mDurationMs = durationMs;
        mOldScale = oldScale;
        mDeltaScale = deltaScale;
        mDestX = destX;
        mDestY = destY;
    }

    @Override
    public void run() {
        CropImageView cropImageView = mCropImageView.get();
        if (cropImageView == null) {
            return;
        }

        long now = System.currentTimeMillis();
        float currentMs = Math.min(mDurationMs, now - mStartTime);
        float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);

        if (currentMs < mDurationMs) {
            cropImageView.zoomInImage(mOldScale + newScale, mDestX, mDestY);
            cropImageView.post(this);
        } else {
            cropImageView.setImageToWrapCropBounds();
        }
    }

}
  • 第三步:最后一步,剪裁圖片
   /**
   * Cancels all current animations and sets image to fill crop area (without animation).
   * Then creates and executes {@link BitmapCropTask} with proper parameters.
   */
  public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressQuality,
                             @Nullable BitmapCropCallback cropCallback) {
    //結(jié)束子線程
    cancelAllAnimations();
    //設(shè)置要剪裁的圖片思杯,不需要位移動畫
    setImageToWrapCropBounds(false);

    //存儲圖片信息胜蛉,四個(gè)參數(shù)分別為:mCropRect要剪裁的圖片矩陣,當(dāng)前圖片要剪裁的矩陣色乾,當(dāng)前放大的值誊册,當(dāng)前旋轉(zhuǎn)的角度
    final ImageState imageState = new ImageState(
            mCropRect, RectUtils.trapToRect(mCurrentImageCorners),
            getCurrentScale(), getCurrentAngle());

    //剪裁參數(shù),mMaxResultImageSizeX暖璧,mMaxResultImageSizeY:剪裁圖片的最大寬度案怯、高度。
    final CropParameters cropParameters = new CropParameters(
            mMaxResultImageSizeX, mMaxResultImageSizeY,
            compressFormat, compressQuality,
            getImageInputPath(), getImageOutputPath(), getExifInfo());
    //剪裁操作放到AsyncTask中執(zhí)行
    new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback).execute();
  }

剪裁部分的核心代碼: float resizeScale = resize(); crop(resizeScale);

    //調(diào)整剪裁大小澎办,如果有設(shè)置最大剪裁大小也會在這里做調(diào)整到設(shè)置范圍
   private float resize() {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(mImageInputPath, options);

    boolean swapSides = mExifInfo.getExifDegrees() == 90 || mExifInfo.getExifDegrees() == 270;
    float scaleX = (swapSides ? options.outHeight : options.outWidth) / (float) mViewBitmap.getWidth();
    float scaleY = (swapSides ? options.outWidth : options.outHeight) / (float) mViewBitmap.getHeight();

    float resizeScale = Math.min(scaleX, scaleY);

    mCurrentScale /= resizeScale;

    resizeScale = 1;
    if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) {
        float cropWidth = mCropRect.width() / mCurrentScale;
        float cropHeight = mCropRect.height() / mCurrentScale;

        if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {

            scaleX = mMaxResultImageSizeX / cropWidth;
            scaleY = mMaxResultImageSizeY / cropHeight;
            resizeScale = Math.min(scaleX, scaleY);

            mCurrentScale /= resizeScale;
        }
    }
    return resizeScale;
}
  
  /**
  * 剪裁圖片
  */
  private boolean crop(float resizeScale) throws IOException {
    ExifInterface originalExif = new ExifInterface(mImageInputPath);

    //四舍五入取整
    int top = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale);
    int left = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale);
    mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale);
    mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale);

    //計(jì)算出圖片是否需要被剪裁
    boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight);
    Log.i(TAG, "Should crop: " + shouldCrop);
    if (shouldCrop) {
        //調(diào)用C++方法剪裁
        boolean cropped = cropCImg(mImageInputPath, mImageOutputPath,
                left, top, mCroppedImageWidth, mCroppedImageHeight, mCurrentAngle, resizeScale,
                mCompressFormat.ordinal(), mCompressQuality,
                mExifInfo.getExifDegrees(), mExifInfo.getExifTranslation());
        //剪裁成功復(fù)制圖片EXIF信息
        if (cropped && mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {
            ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath);
        }
        return cropped;
    } else {
        //直接復(fù)制圖片到目標(biāo)文件夾
        FileUtils.copyFile(mImageInputPath, mImageOutputPath);
        return false;
    }
}
第三層:

GestureImageView extends CropImageView
他的功能:
監(jiān)聽用戶的手勢嘲碱,調(diào)用合適的方法

由于系統(tǒng)對手勢操作已經(jīng)有了監(jiān)聽方法,所以作者在這里使用了系統(tǒng)的監(jiān)聽方法:
ScaleGestureDetector:用來檢測兩個(gè)手指在屏幕上做縮放的手勢浮驳。
GestureListener:這個(gè)類我們可以識別很多的手勢悍汛,作者在這里重寫了雙擊onDoubleTap,拖動onScroll至会,兩種手勢處理离咐。
RotationGestureDetector:兩只以上的手指觸摸屏幕才會產(chǎn)生旋轉(zhuǎn)事件用這個(gè)接口回調(diào)。

/**
 * If it's ACTION_DOWN event - user touches the screen and all current animation must be canceled.
 * If it's ACTION_UP event - user removed all fingers from the screen and current image position must be corrected.
 * If there are more than 2 fingers - update focal point coordinates.
 * Pass the event to the gesture detectors if those are enabled.
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
        cancelAllAnimations();
    }

    if (event.getPointerCount() > 1) {
        mMidPntX = (event.getX(0) + event.getX(1)) / 2;
        mMidPntY = (event.getY(0) + event.getY(1)) / 2;
    }

    //雙擊監(jiān)聽和拖動監(jiān)聽
    mGestureDetector.onTouchEvent(event);
    //兩指縮放監(jiān)聽
    if (mIsScaleEnabled) {
        mScaleDetector.onTouchEvent(event);
    }
    //旋轉(zhuǎn)監(jiān)聽
    if (mIsRotateEnabled) {
        mRotateDetector.onTouchEvent(event);
    }
    if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
        //最后一指抬起時(shí)判斷圖片是否填充剪裁框
        setImageToWrapCropBounds();
    }
    return true;
}

大致的核心邏輯基本就這些
項(xiàng)目中的異步操作使用AsyncTask奉件,一共兩個(gè)主要的AsyncTask:BitmapLoadTask用于初次進(jìn)入load圖片宵蛀,BitmapCropTask圖片剪裁異步操作。


項(xiàng)目涉及到的技術(shù)點(diǎn):

自定義View县貌,手勢操作監(jiān)聽术陶,Matrix實(shí)現(xiàn)圖片變換縮放,Canvas繪制View煤痕,exif存儲圖片信息梧宫,文件存儲操作接谨,以及大量的計(jì)算。

*有疑問的可以在評論區(qū)留言一起討論~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末塘匣,一起剝皮案震驚了整個(gè)濱河市脓豪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忌卤,老刑警劉巖扫夜,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異驰徊,居然都是意外死亡笤闯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門棍厂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颗味,“玉大人,你說我怎么就攤上這事勋桶⊥蜒茫” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵例驹,是天一觀的道長。 經(jīng)常有香客問我退唠,道長鹃锈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任瞧预,我火速辦了婚禮屎债,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘垢油。我一直安慰自己盆驹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布滩愁。 她就那樣靜靜地躺著躯喇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪硝枉。 梳的紋絲不亂的頭發(fā)上廉丽,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音妻味,去河邊找鬼正压。 笑死,一個(gè)胖子當(dāng)著我的面吹牛责球,可吹牛的內(nèi)容都是我干的焦履。 我是一名探鬼主播拓劝,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嘉裤!你這毒婦竟也來了凿将?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤价脾,失蹤者是張志新(化名)和其女友劉穎牧抵,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侨把,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡犀变,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秋柄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片获枝。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖骇笔,靈堂內(nèi)的尸體忽然破棺而出省店,到底是詐尸還是另有隱情,我是刑警寧澤笨触,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布懦傍,位于F島的核電站,受9級特大地震影響芦劣,放射性物質(zhì)發(fā)生泄漏粗俱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一虚吟、第九天 我趴在偏房一處隱蔽的房頂上張望寸认。 院中可真熱鬧,春花似錦串慰、人聲如沸偏塞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灸叼。三九已至,卻和暖如春掂碱,著一層夾襖步出監(jiān)牢的瞬間怜姿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工疼燥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沧卢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓醉者,卻偏偏與公主長得像但狭,于是被迫代替她去往敵國和親披诗。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容