最も詳細なUCropソースの解析かもしれません

18524 ワード

UCropは現在比較的火をつけているオープンソースandroid画像のクリップフレームで、効果は以下の通りです.
preview.gif
アドレス:Gitソースアドレス及び作者自身のロジック解析-中国語翻訳原版英文解析
本文は核心機能コードを重点的に解釈し、プロジェクトの流れを整理し、読むときにソースコードと結びつけて見ることを提案する.
ビジネスプロセス:
画像を選択(システムギャラリーから画像を選択)→画像を配置(操作台に画像を配置)→画像を操作(回転、スケーリング、変位などの操作を含めて必要な画像を得る)→画像を裁断(元のスケーリングボックスに基づいてターゲット画像を裁断または所定のスケーリングに基づいて裁断)→ターゲット画像を取得(bitmapに戻ってローカルに保存)
コード構造解析:
プロジェクトはBulider設計モードを使用し、構造機能の分業が明確である.以下、著者がどのように実現したのかを見てみよう.コアコードの注釈コード構造は大きく3つの部分に分けられる.
  • 第1部:UCropActivity(画像の操作ウィンドウ)主にプロジェクトのエントリといくつかの初期化作業として、カスタムViewをロードします.使用時にターゲットピクチャのURIとターゲットストレージファイルのURIを直接入力すると、クリップが開始されます.ここで著者らはUCropActivityのhelperクラスUCropをカプセル化し,startActivity ForResultとBundleをカプセル化し,Options内部クラスをユーザ初期パラメータとして構成する(settToolbarCropDrawable設定toolbarピクチャ,settToolbarTitleタイトルなど).UCropActivityにはカスタムUCropViewが含まれ、UCropViewにはGestureCropImageViewとOverlayViewの2つのカスタムViewが含まれています.OverlayViewは、クリップボックスの描画を担当します.GestureCropImageViewは、選択画像
  • を操作します.
  • 第2部:OverlayView-クリップボックスの描画を担当するViewは初期化時に判断し、システムが3.0より4.3未満の場合にハードウェア加速を開始するが、なぜこの区間を選択したのか、androidシステム3.0以前にandroidはハードウェア加速をサポートしていなかったため、4.3以降のAndroid 4.4、つまりKitKatバージョンでは、このバージョンはメモリ方法を大きく最適化した.一方、メモリ使用を最適化することで、一方、Dalvik仮想マシンの代わりにARTランタイムを使用することはオプションでサポートされ、アプリケーションの実行効率が向上するため、4.4を選択した後、ハードウェアアクセラレータをオンにしない.クリップボックスの描画は主に2つの部分に分けられる:1、drawlines()クリップボックス内の分割セグメントを描画する、2、drawRect()矩形を描画することによって、横長線平均分割付きの矩形クリップボックスコアメソッドを形成する:drawCropGrid()このメソッドの主な機能はクリップボックスを描画することであり、設定されたクリップボックス分割線数に基づいてcanvasを使用する.drawlines()メソッド分割線と制限矩形を描画し、具体的なコードは以下の通りです:クリップボックスを描画する:protected void drawCropGrid(@NonNull Canvas canvas){//クリップボックスif(mShowCropGrid){//矩形データが空かどうかを判断し、mGridPointsが空であれば充填データif(mGridPoints==null&!mCropViewRect.isEmpty()に入ります//////この配列はcanvas.drawLinesの最初のパラメータであり、このパラメータは要素個数が4の倍数mGridPoints=new float[(mCropGridRowCount)*4+(mCropGridColumnCount)*4];int index=0;//////////データを組み立て、データは各セグメントの座標点for(int i=0;i 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 1 , 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(); } }この部分は主に第1層のクリップボックスの描画を担当し、クリップボックスマトリクスを保存して後期クリップ用
  • に使用する.
    第3部:GestureCropImageView-操作選択画像
    この部分はプロジェクトの最も核心的な部分であるべきで、論理を実現する作者は彼の説明文章の中でもはっきり言っている.この部分の論理解結合は非常によく、Viewの機能論理を3層に分け、各層がそれぞれの機能を担当しています.
  • 第1層:TransformImageView extends ImageView彼の仕事:1.画像ソースから画像2を手に入れる.マトリクスを変換(平行移動、スケール、回転)し、現在のピクチャに適用するレイヤでは、トリミングやジェスチャーなどの動作はわかりませんが、サブクラス呼び出しのためのジェスチャー動作操作が提供されます.ここで著者らは、onDrawメソッドを書き換えるのではなく、ImageViewのMatrixを使用して回転スケールを行うことを選択した.onDrawメソッドはフラッシュスクリーンがある可能性があり、性能の悪いマシンでは体験が悪い可能性があり、Matrixを使用すると多くのことができるからだ.Matrixはandroidの中で実は1つの3*3の行列は以下の通りです:MSCALE_X MSKEW_X MTRANS_X MSKEY_Y MSCALE_Y MTRANS_Y MPERSP_0 MPERSP_1 MPERSP_2 Matrixの画像に対する処理は4種類の基本変換に分けられる:Translate平行変換Rotate回転変換Scale変換Skew誤接変換プロジェクトでは上位3種類しか使用されず、対応するapiは以下の通りである://平行変換、X軸でx複数の距離を平行移動、Y軸でy複数の距離を平行移動postTranslate(float x,floaty)//回転変換、degrees回転度数、px、py回転原点postRotate(float degrees、float px、float py);スケーリング変換x変換距離、y変換距離、px、pyスケーリング原点postScale(float sx、float sx、float px、float py);著者らはこの層でpostTranslate,postScale,postRotateの3つの方法をサブクラス呼び出しに暴露し、Matrix属性をカプセル化した後、setImageMatrix()を呼び出して変換効果を実現し、mCurrentImageMatrix.mapPointsという方法は、現在の画像の角点と記憶する中心点を更新するものであり、これらの変数はCropImageViewで使用する.Matrixの使い方について詳しくないのはここMatrix Matrix原理
  • 第2層:CropImageView extends TransformImageView彼がしなければならないこと:1.切り取った境界とメッシュ2を描く.ユーザが画像に対して操作することによって、トリミング領域に空白が生じた場合、画像は自動的に境界に移動して空白領域を埋めるべきである.親のメソッドを継承し、マトリクス(最小スケールと最大スケールを制限)4を操作するには、より細かいルールを使用します.追加方法と縮小方法(アニメーション変換)5.画像を裁断する層は、画像を変換したり裁断したりするすべての操作をほぼ網羅していますが、これらのことをする方法を示すだけで、ジェスチャーをサポートする必要があります.この層で著者らは、上部のViewTransformImageViewのピクチャロード完了コールバックメソッドを設定するonImageLaidOutメソッドを書き換えた.また、画面の中央に画像を移動する操作(ImageViewで設定されているScaleTypeがMatrixであるため、画像が最初は画面上にデフォルトである)という層の操作は、画像オフセットクリップボックスオフセット計算、画像帰位アニメーション処理の3つのステップに分けられる.カット画像の最初のステップ(最も複雑なステップ):指がスクリーンから離れるときに、カット領域に画像があることを保証しなければならない.カット領域でシフト変換によってカット領域に移動しない場合は、コード:
     public void setImageToWrapCropBounds(boolean animate) {
      //                   
      if (mBitmapLaidOut && !isImageWrapCropBounds()) {
    
          //     X,Y  
          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);
    
          //             
          boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);
    
          //         
          if (willImageWrapCropBoundsAfterTranslate) {
              //       
              final float[] imageIndents = calculateImageIndents();
              //     ,               
              deltaX = -(imageIndents[0] + imageIndents[2]);
              deltaY = -(imageIndents[1] + imageIndents[3]);
          } else {
              //          ,      
              RectF tempCropRect = new RectF(mCropRect);
              mTempMatrix.reset();
              //      
              mTempMatrix.setRotate(getCurrentAngle());
              mTempMatrix.mapRect(tempCropRect);
    
              //         
              final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);
              //      
              deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],
                      tempCropRect.height() / currentImageSides[1]);
              deltaScale = deltaScale * currentScale - currentScale;
          }
    
          //      
          if (animate) {
              post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(
                      CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,
                      currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));
          } else {
              //     ,         
              postTranslate(deltaX, deltaY);
              if (!willImageWrapCropBoundsAfterTranslate) {
                  zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY());
              }
          }
      }
    
    }
  • を参照してください.
    ステップ2:作者はここでRunableスレッドを使用して操作し、時間差の計算を使用してアニメーションを移動し、アニメーションをよりリアルに見せる
    この方法は主にオフセット回帰のアニメーションを1つの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.
     *    ,           ,  CubicEasing   ,               。
     *                     ,            。
     *   ,           ,                     ,Runnable      。
     */
    private static class WrapCropBoundsRunnable implements Runnable {
    
        private final WeakReference 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();
            //     ,  500ms,
            float currentMs = Math.min(mDurationMs, now - mStartTime);
    
            //          ,               。
            float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
            float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
            float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
    
            //           
            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());
                }
                //             ,    
                if (!cropImageView.isImageWrapCropBounds()) {
                    cropImageView.post(this);
                }
            }
        }
    }
    

    もう一つのRunableメソッドクラスは、ダブルクリックで拡大するときに使用します.
    同様に時間差を用いてオフセットサイズを計算したアニメーション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 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();
            }
        }
    
    }
    
  • 第3ステップ:最後のステップ、画像
  • をクリップ
       /**
       * 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) {
        //     
        cancelAllAnimations();
        //        ,       
        setImageToWrapCropBounds(false);
    
        //      ,       :mCropRect        ,          ,      ,       
        final ImageState imageState = new ImageState(
                mCropRect, RectUtils.trapToRect(mCurrentImageCorners),
                getCurrentScale(), getCurrentAngle());
    
        //    ,mMaxResultImageSizeX,mMaxResultImageSizeY:         、  。
        final CropParameters cropParameters = new CropParameters(
                mMaxResultImageSizeX, mMaxResultImageSizeY,
                compressFormat, compressQuality,
                getImageInputPath(), getImageOutputPath(), getExifInfo());
        //      AsyncTask   
        new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback).execute();
      }
    

    クリップ部分のコアコード:float resizeScale=resize()crop(resizeScale);
        //      ,                        
       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);
    
        //            
        boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight);
        Log.i(TAG, "Should crop: " + shouldCrop);
        if (shouldCrop) {
            //  C++    
            boolean cropped = cropCImg(mImageInputPath, mImageOutputPath,
                    left, top, mCroppedImageWidth, mCroppedImageHeight, mCurrentAngle, resizeScale,
                    mCompressFormat.ordinal(), mCompressQuality,
                    mExifInfo.getExifDegrees(), mExifInfo.getExifTranslation());
            //        EXIF  
            if (cropped && mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {
                ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath);
            }
            return cropped;
        } else {
            //            
            FileUtils.copyFile(mImageInputPath, mImageOutputPath);
            return false;
        }
    }
    

    階層3:
    GestureImageView extends CropImageView彼の機能:ユーザーのジェスチャーを傍受し、適切な方法を呼び出す
    システムはジェスチャー操作に対してすでに傍受方法を持っているので,著者らはここでシステムの傍受方法:ScaleGestureDetector:スクリーン上で2本の指をスケーリングするジェスチャーを検出するために使用した.GestureListener:このクラスでは多くのジェスチャーを識別することができます.著者らはここでonDoubleTapをダブルクリックし、onScrollをドラッグし、2つのジェスチャー処理を書き直しました.RotationGestureDetector:2本以上の指がスクリーンに触れると回転イベントが発生します.このインタフェースでコールバックします.
    /**
     * 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;
        }
    
        //         
        mGestureDetector.onTouchEvent(event);
        //      
        if (mIsScaleEnabled) {
            mScaleDetector.onTouchEvent(event);
        }
        //    
        if (mIsRotateEnabled) {
            mRotateDetector.onTouchEvent(event);
        }
        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
            //                  
            setImageToWrapCropBounds();
        }
        return true;
    }
    

    大まかなコアロジックは基本的にこれらの項目の中の非同期操作についてAsyncTaskを使用し、全部で2つの主要なAsyncTask:BitmapLoadTaskは初めてloadピクチャに入るために使用され、BitmapCropTaskピクチャは非同期操作を裁断する.
    プロジェクトに関連する技術点:
    カスタムView、ジェスチャー操作リスニング、Matrixはピクチャ変換スケールを実現し、CanvasはViewを描画し、exifはピクチャ情報、ファイル記憶操作、および大量の計算を格納する.
    疑问があるのはコメントエリアでコメントして一绪に讨论することができます~~end