首先要把思路弄清楚,先将这个项目运行在手机上,玩一玩,看看他都有哪些功能,然后再考虑那些功能是怎么实现的。
项目Github链接
第一阶段
折腾一番后就大致有个印象了,接下就可以分析具体功能了,且不看布局这些是怎么弄的,先从这个库的入口开始分析,就是我们是怎么与图片进行交互的,手势! 对,我们通过手势来改变图片的显示,可以平移,旋转,放大,我们应该从这里切入,整个思路就会比较清晰。
那么,问题来了。
如何分配这些功能,我应该创建几个类,他们之间应该是怎样的继承关系等等,这就是体现作者功底的时候了,究竟该如何架构,是很值得研究的问题。
1.手势的处理
我们需要一个类,这个类可以识别用户的手势,然后调用相应的方法。这里肯定涉及到多点触控
问题1:如何判断用户的最后一根手指已经释放?
假如我们现在已经拿到了用户的手势,那么肯定需要接着调用某个类的某种方法来进行处理,比如,我们已经识别出是一个平移的手势,势必有一个postTranslate(disX,disY)方法来对应。
那具体平移的操作又该放在哪个类中呢,还有缩放,旋转,裁剪等等方法。
问题2:如何确定类的功能,如何去耦合
我是想不到一个完美的方案来解决这个问题,或者说,我必须要在做的过程中慢慢的优化架构,而暂时不具备在开始项目的时候就直接描绘出轮廓,毕竟是菜鸟。
分层的思想
作者在这里分出了三个类,各有不同的功能
GestureCropImageView
专门识别用户的手势,平移,缩放,旋转。他并不关心怎样去做具体的操作,也不关心裁剪。
TransformImageView
基本功能包括加载图片,对图片进行平移,缩放,旋转,并且通知监听者正在进行哪些操作。
CropImageView
核心类。具体的手势识别,具体的平移操作,这些都只是一个功能点,这个类的作用更像是一个管理者,负责将这些功能点组合起来,达到好的用户体验和库真正功能的实现。
第二阶段
第一阶段只是一个大致的印象,有了一个宏观脉络,可以到现在,我们还一行代码都没看呢?
折腾这个库的时候,我很好奇他进去的时候是如何摆放图片和裁剪提示框的,如何做到正好能让他们吻合。
问题3:怎样初始化我们的裁剪控件,达到和裁剪提示框吻合的效果
作者使用了一个uCropView作为外层嵌套布局,里面包含我们的裁剪提示框OverlayView和裁剪控件GestureCropImageView.
因为作者将这个库设计得很灵活,用户可以选择指定的裁剪比例,有16:9,4:3等,还可以使用原生图片的宽高比,并且在控件中可以动态调整裁剪比例,这就必然需要两个控件之间进行交互,最好的办法就是设置一个监听器。
mGestureCropImageView.setCropBoundsChangeListener(new CropImageView.CropBoundsChangeListener() {
@Override
public void onCropBoundsChangedRotate(float cropRatio) {
if (mOverlayView != null) {
mOverlayView.setTargetAspectRatio(cropRatio);
mOverlayView.postInvalidate();
}
}
});
具体使用场景我们后面再看。
根据作者的布局,TransformImageView 是顶层父类(至于为什么这么架构,也可以有小小的思考)。看看他做了哪些事情。
这时我们要跳到外面的UCropActivity中去看看数据是怎样给的。
在setImageData()方法中,会指定图片的uri
mGestureCropImageView.setImageUri(inputUri, mOutputUri);
TransformImageView
public void setImageUri(@NonNull Uri imageUri, @NonNull Uri outputUri) throws Exception {
mImageUri = imageUri;
int maxBitmapSize = getMaxBitmapSize();
BitmapLoadUtils.decodeBitmapInBackground(getContext(), imageUri, outputUri, maxBitmapSize, maxBitmapSize,
new BitmapLoadCallback() {
@Override
public void onBitmapLoaded(@NonNull final Bitmap bitmap) {
mBitmapWasLoaded = true;
setImageBitmap(bitmap);
invalidate();
}
@Override
public void onFailure(@NonNull Exception bitmapWorkerException) {
Log.e(TAG, "onFailure: setImageUri", bitmapWorkerException);
if (mTransformImageListener != null) {
mTransformImageListener.onLoadFailure(bitmapWorkerException);
}
}
});
}
我们暂时只需要知道这是一个后台解析图片的方法,成功后可以拿到Bitmap,设置给ImageView,然后重新布局,重新绘制。
就我感觉,我们之前说的根据裁剪比例来布局图片现在应该可以体现了。
根据上面的代码,我们已经在UCropView中给裁剪控件设置了比例,CropImageView重写了onImageLaidOut()方法,很重要,这里会确定裁剪框矩形和图片矩形。
//在Random模式下,走的是这段代码
if (mTargetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
mTargetAspectRatio = drawableWidth / drawableHeight;
}
setupCropBounds(); //设置裁剪框矩形
setupInitialImagePosition(drawableWidth, drawableHeight); //设置图片初始位置
至于设置裁剪框的细节代码,我们后面会拉出来分析,这里是梳理前期的流程。
if (mCropBoundsChangeListener != null) {
mCropBoundsChangeListener.onCropBoundsChangedRotate(mTargetAspectRatio);
}
当我们确定了裁剪框的位置之后,通知OverlayView进行重绘(这又是另一细节问题了)。
然后,我们摆放图片的位置。
private void setupInitialImagePosition(float drawableWidth, float drawableHeight) {
float cropRectWidth = mCropRect.width();
float cropRectHeight = mCropRect.height();
float widthScale = cropRectWidth / drawableWidth;
float heightScale = cropRectHeight / drawableHeight;
mMinScale = Math.max(widthScale, heightScale);
mMaxScale = mMinScale * mMaxScaleMultiplier;
float tw = (cropRectWidth - drawableWidth * mMinScale) / 2.0f + mCropRect.left;
float th = (cropRectHeight - drawableHeight * mMinScale) / 2.0f + mCropRect.top;
mCurrentImageMatrix.reset();
mCurrentImageMatrix.postScale(mMinScale, mMinScale);
mCurrentImageMatrix.postTranslate(tw, th);
}
这里传入的是原生图片的宽高。
因为图片的位置和裁剪框的位置在初始时肯定是不吻合的,我们可能需要平移,缩放之后才能让他们吻合(又一个细节)
到这里,图片已经正确摆放,和裁剪提示框吻合。
第三阶段
图片正确的摆放之后,剩下的就回到我们之前的问题了—识别手势,调用方法。
如上所说,识别手势的方法全在GestureCropImageView 这个类中了。
小技巧:自己无需计算细节来识别手势,只需使用已经封装好的类,将事件传递给他即可,他自然会给你提供回调方法的。
@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();
}
}
在回调方法中就是各种postXX了,这里有个小细节,因为之前没有做过多点触控,这里正好回答第一个问题,怎样判断最后一根手指释放了。
对于多根手指,最后一根的释放触发ACTION_UP,之前的手指释放事件触发ACTION_POINTER_UP。
所有的平移,缩放,旋转都是交给TransformImageView 这个类来完成的,一起来看看。
以平移为例
public void postTranslate(float deltaX, float deltaY) {
if (deltaX != 0 || deltaY != 0) {
mCurrentImageMatrix.postTranslate(deltaX, deltaY);
setImageMatrix(mCurrentImageMatrix);
}
}
其他的方法也都是通过Matrix实现的,我也是第一次接触,感觉是一个强大的类,暂时只需要知道他怎么用,有需要的可以深入研究。
当你设置了这个矩阵之后,在View的绘制过程中会进行判断,根据该矩阵对canvas作出变换,使你能够画出想要的结果。
好了,流程梳理就到这了,后面的文章会接着分析一个个具体的实现,欢迎大家交流。