Android开发Android技术知识程序员

可能是最详细的UCrop源码解析

2017-02-22  本文已影响2515人  koala_

UCrop是目前比较火的开源android图片剪裁框架,效果如下:

preview.gif

地址:
Git源码地址
以及作者自己的逻辑解析-中文翻译
原版英文解析

本文重点解释核心功能代码,梳理项目流程,建议阅读时结合源码一起看~。


业务流程:

选择图片(从系统图片库选择图片)→ 放置图片(把图片放置到操作台)→ 操作图片(包括旋转,缩放,位移等操作来得到需要的图片)→ 剪裁图片(根据原始比例剪裁框剪裁目标图片或根据给定的比例剪裁)→ 获得目标图片(返回bitmap并保存到本地)

代码结构解析:

项目使用Bulider设计模式,结构功能分工明确,下面就来看看作者是怎么实现的,注意看核心代码的注释
代码结构大致分为三个部分:


第三部分:GestureCropImageView-负责操作选择图片

这一部分应该是项目最核心的部分,实现逻辑作者在他的说明文章中也说的比较清楚。
这一部分的逻辑解耦做的非常好,把View的功能逻辑划分为3层,每一层负责各自的功能:

第二步:作者在这里使用了一个Runable线程来操作,使用时间差值的计算来移动动画,使动画看起来更真实

此方法主要处理偏移回归的动画 写在一个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<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();
        //花费的时间,最多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<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) {
    //结束子线程
    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;
    }
}
第三层:

GestureImageView extends CropImageView
他的功能:
监听用户的手势,调用合适的方法

由于系统对手势操作已经有了监听方法,所以作者在这里使用了系统的监听方法:
ScaleGestureDetector:用来检测两个手指在屏幕上做缩放的手势。
GestureListener:这个类我们可以识别很多的手势,作者在这里重写了双击onDoubleTap,拖动onScroll,两种手势处理。
RotationGestureDetector:两只以上的手指触摸屏幕才会产生旋转事件用这个接口回调。

/**
 * 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,一共两个主要的AsyncTask:BitmapLoadTask用于初次进入load图片,BitmapCropTask图片剪裁异步操作。


项目涉及到的技术点:

自定义View,手势操作监听,Matrix实现图片变换缩放,Canvas绘制View,exif存储图片信息,文件存储操作,以及大量的计算。

有疑问的可以在评论区留言一起讨论~~
end

上一篇下一篇

猜你喜欢

热点阅读