目录
- 目录
- 概述
- 相关文章
- 处理流程
- 裁剪方式
- 裁剪原理
- 裁剪流程
- 图片加载
- 图片绘制
- 获取屏幕的大小
- 计算图片绘制区域
- 绘制图片
- 计算裁剪框
- 绘制裁剪框
- 裁剪操作
-
- 计算实际裁剪区域
- 裁剪并保存图片
-
- 关于移动的限制
- 关于缩放的限制
- 使用方式
- 源码
- 裁剪类源码
- 接口源码
- GitHub地址
- 示例GIF
- 示例图片裁剪
- 修复代码之前存在的问题
概述
相关文章
AbsTouchEventHandle
触摸事件
TouchUtils
触摸事件辅助工具类
此工具基于以上两个类
处理流程
裁剪方式
图片的裁剪方式有很多种,最常见的有两种吧.
但不管是哪什么类型的裁剪方式,至少需要处理的有以下两个点:
- 图片显示
- 裁剪框显示
裁剪方式的不同在于以上两部分的处理方式不同而不同.
-
系统默认方式
图片加载满屏幕,不可移动或缩放,裁剪框移动或者缩放选择裁剪区域完成裁剪
-
微信裁剪方式
裁剪框加载屏幕,不可移动或者缩放,图片移动或者缩放选择裁剪区域完成裁剪
以上两种方式并没很大的差别.但第二种方式可能会更好.因为在裁剪一个比较大的图片时,想更细节地裁剪一部分图片时,第二种方式更容易也更好地做到这一点.
第一种方式(系统默认的)只能是裁剪框缩放,当裁剪的范围很小时,就不容易控制了.
这里我们使用第二种裁剪方式(微信的裁剪方式).
裁剪原理
裁剪原理其实在上面裁剪方式中也基本提及了,主要是通过图片或者裁剪框的缩放来确定裁剪的区域.
我们使用微信的裁剪方式,则是通过图片缩放来确定裁剪区域.
这里图片需要完成的操作有移动和缩放.因此我们使用之前处理触摸事件的类进行处理.
,触摸事件处理类
AbsTouchEventHandle
,移动与缩放辅助工具类
TouchUtils
关于以上两个类的使用,查看相关的文章即可.
裁剪流程
关于流程,我们从图片加载开始到图片裁剪结束并保存做一个完整的流程进行分析.
- 图片加载
- 图片绘制
- 裁剪框绘制(裁剪框使用正方形)
- 图片移动或者缩放(从而改变裁剪区域)
- 裁剪图片
- 保存图片
这里我们确定需要使用的一些变量.
//图片来源路径
String inputPath;
//图片输出存放路径
String outputPath;
//裁剪框
RectF mCropRectf;
//图片绘制区域
RectF mBitmapRectf;
//裁剪的源图片
Bitmap mBitmap;
图片加载
图片的加载并没有什么特别的,通过图片的路径
inputPath
加载出裁剪的源图片即可;
这里需要注意的是,某些情况下,需要裁剪后的图片的质量和大小并不是很高(如头像),这种情况下如果源图片很大的话,加载到内存是很容易导致一些问题的.所以建议:
在不必要的情况下,尽量不要加载源图片进行裁剪,而是加载出适当的缩略图进行裁剪
图片绘制
图片绘制和裁剪框的绘制是一个重点.当然图片的移动与缩放也是重点,但使用之前的两个触摸事件处理类已经处理了这些事件,所以在这里我们并不需要去担心这个问题.
首先,确定图片初始化时绘制的区域.在裁剪中,我们希望达到的效果是:
默认情况下,裁剪的图片全屏显示,并且裁剪框应该居中同时也全屏显示.
因此图片的绘制就很明确了.我们需要全屏显示图片.
获取屏幕的大小
全屏显示我们需要获取到当前屏幕的大小.
//存放屏幕宽高大小的全局变量
PointF mViewParmas;
//获取屏幕宽高大小
private void getViewParams() {
if (mIsFirstGetViewParams) {
mViewParams.x = mDrawView.getWidth();
mViewParams.y = mDrawView.getHeight();
}
}
其中使用变量
mIsFirstGetViewParams
检测是因为屏幕的大小不会改变,我们只需要获取一次就够了.避免了反复获取.
获取了屏幕的大小,接下来就是计算图片绘制的区域并绘制图片了.
计算图片绘制区域
计算图片的绘制区域是一个比较复杂的过程.因为初始状态图片必须是全屏显示.
事实上我们并不需要去拉伸图片以填充屏幕,我们需要做的只是放大或者缩放图片以适应整个屏幕.
基于这个原则,我们需要考虑图片本身大小与屏幕宽高大小的关系.
由于图片可以缩放,我们现在假设图片与屏幕宽高在同一个等级.则可能有以下情况:
- 图片宽/高与屏幕宽/高相等(完美情况)
- 图片宽=屏幕宽且高<=屏幕高(宽主导)
- 图片高=屏幕高且宽<=屏幕宽(高主导)
- 图片为正方形图片
由于图片需要填充屏幕, 当出现某一边主导的情况时,我们需要以该边为基础填充屏幕,剩下的另一边只能是小于屏幕对应边
;但我们可以让图片保持居中,这样会看起来更合理正常.
此处的计算方式如下:
- 假设以宽填充屏幕,计算填充后的高度再与屏幕高度比较.(方式1)
- 同理以高填充屏幕,计算填充后的宽度与屏幕宽度比较.(方式2)
- 在任何一种方式中,只要计算得到的高或者宽不超过当前屏幕允许的高或者宽,该方式即可用于图片填充屏幕
//计算最大绘制宽高
//此处不使用屏幕大小是为了四边留空白
float largeDrawWidth = mViewParams.x * ;
float largeDrawHeight = mViewParams.y * ;
//获取图片宽高,也作为最终图片绘制的宽高
int width = mBitmap.getWidth();
int height = mBitmap.getHeight();
//以图片高填充屏幕,计算填充的宽度
int tempDrawWidth = (int) ((largeDrawHeight / height) * width);
//以图片宽填充屏幕,计算填充的高度
int tempDrawHeight = (int) ((largeDrawWidth / width) * height);
//若填充宽度小于最大绘制宽度时
if (tempDrawWidth < largeDrawWidth) {
//以高填充(填充后的宽不超过屏幕宽,图片可以完全显示)
width = tempDrawWidth;
height = (int) largeDrawHeight;
} else {
//否则,以宽填充,填充后的高不超过屏幕宽,图片可以完全显示
height = tempDrawHeight;
width = (int) largeDrawWidth;
}
//不存在以下的情况:
//不管以高填充还是宽填充,填充后的另一边都超过屏幕的长度,数学证明略
绘制图片
计算完图片的绘制区域后必然是绘制图片了.绘制图片时有一个需要注意的地方就是之前在
TouchUtils
类的使用中提及的,拖动本身的偏移量处理由
TouchUtils
类管理,而绘制元素只保持自身的属性.
因此在绘制时需要将偏移量添加到绘制区域中,生成一个实际的(带偏移参数)绘制区域.
//以图片绘制区域为基础创建实际的绘制区域
RectF moveRectf = new RectF(src);
//X/Y添加上偏移量
moveRectf.left += mTouch.getDrawOffsetX();
moveRectf.top += mTouch.getDrawOffsetY();
moveRectf.right += mTouch.getDrawOffsetX();
moveRectf.bottom += mTouch.getDrawOffsetY();
//返回带偏移参数的绘制区域
return moveRectf;
计算裁剪框
首先,裁剪框使用一个
RectF
对象来保存需要显示可裁剪的区域(透明区域,可以直接显示后面的图片);
关于裁剪框的位置与大小是比较好确定的.
- 裁剪框是正方形
- 裁剪框屏幕居中
- 裁剪框以屏幕最小边为边长
由于前面我们已经得到了图片的绘制区域(默认)的大小,此时图片绘制区域是填充整个屏幕的(严格来说是某一边填充屏幕),其中的
mBitmapRectf.width()
与
mBitmapRectf.height()
分别是填充后的图片的宽高的缩放长度(与图片实际的宽高比例是一致的).
因此可以取其中最小的一边作为屏幕裁剪框的边长大小.
//计算绘制区域的最小边,与裁剪区域的大小进行比较
float smallDrawSize = ;
if (mBitmapRecf.width() > mBitmapRecf.height()) {
smallDrawSize = mBitmapRecf.height();
} else {
smallDrawSize = mBitmapRecf.width();
}
//创建裁剪区域对象
if (mCropRecf == null) {
mCropRecf = new RectF();
}
//以屏幕中心创建裁剪框区域坐标
float centerX = mViewParams.x / ;
float centerY = mViewParams.y / ;
mCropRecf.left = centerX - smallDrawSize / ;
mCropRecf.top = centerY - smallDrawSize / ;
mCropRecf.right = mCropRecf.left + smallDrawSize;
mCropRecf.bottom =mCropRecf.top + smallDrawSize;
这里需要注意的是,使用默认的图片绘制区域的宽/高中最小边作为裁剪框的边长而不是屏幕的最小边作为裁剪框的边长的原因:
可能存在一种情况,图片的宽高比例达到100:1(类似的极致性比例),这种情况下必定以图片的长为填充边,所得的高将很小很小;
如果裁剪以屏幕最小边为边长,则会裁剪到图片以外的区域(不存在数据,根本无法裁剪);
以图片绘制区域最小边确保了裁剪框裁剪的区域必定在图片范围内,也不造成裁剪到图片以外的区域从而导致某些异常
绘制裁剪框
裁剪框的绘制并不是直接绘制裁剪框,因为我们需要保持裁剪区域的透明以明确裁剪的范围.
裁剪框以外的区域使用半透明的黑色覆盖达到表示该部分区域不属于裁剪范围的效果.
因此,绘制裁剪区域实际上是 以半透明黑色绘制裁剪框以外的区域
,保持裁剪框的透明以达到裁剪框的效果.
//将裁剪框以外的区域分为四个矩形,并分别计算四个无效区域的坐标
RectF topRecf = new RectF(, , mViewParams.x, mCropRecf.top);
RectF bottomRecf = new RectF(, mCropRecf.bottom, mViewParams.x, mViewParams.y);
RectF leftRecf = new RectF(, mCropRecf.top, mCropRecf.left, mCropRecf.bottom);
RectF rightRecf = new RectF(mCropRecf.right, mCropRecf.top, mViewParams.x, mCropRecf.bottom);
//设置半透明画笔并绘制无效区域
mPaint.setColor(Color.BLACK);
mPaint.setAlpha((int) ( * ));
canvas.drawRect(topRecf, mPaint);
canvas.drawRect(bottomRecf, mPaint);
canvas.drawRect(leftRecf, mPaint);
canvas.drawRect(rightRecf,mPaint);
裁剪操作
图片的裁剪操作其实就是在图片上指定区域的像素保存下来.因此裁剪一张图片必须保证:
裁剪的区域在图片的有效范围内(不可以超出图片区域)
裁剪的是基于图片本身的像素裁剪,所以需要将裁剪区域缩放到以图片自身大小等级从而确定裁剪区域
计算实际裁剪区域
由于图片是被缩放过的,对应的裁剪框中的位置也是不是真实图片中的裁剪位置.需要缩放裁剪框从而得到真实的裁剪区域.
计算裁剪框的实际位置时,需要
- 计算裁剪框实际的宽高(裁剪框为正方形,仅计算一边即可)
- 计算裁剪框在真实图片中的位置
//图片实际宽高
int bmpWidth = bitmap.getWidth();
int bmpHeight = bitmap.getHeight();
//图片绘制的区域宽高
float drawWidth = bmpRectF.width();
float drawHeight = bmpRectF.height();
//当前需要裁剪图片区域的宽高
float cropWidth = cropRectF.width();
float cropHeight = cropRectF.height();
//计算实际的裁剪宽高
float realCropWidth = (cropWidth / drawWidth) * bmpWidth;
float realCropHeight = realCropWidth;
//计算实际裁剪区域的坐标
//以当前左上角坐标在缩放图片的相对位置,计算出等比情况下在真实图片中的相对位置
float realCropTop = ((cropRectF.top - bmpRectF.top) / drawHeight) * bmpHeight;
float realCropLeft = ((cropRectF.left - bmpRectF.left) / drawWidth) * bmpWidth;
//创建实际裁剪区域
Rect srcRect = new Rect();
srcRect.top = (int) realCropTop;
srcRect.left = (int) realCropLeft;
srcRect.bottom = srcRect.top + (int) realCropWidth;
srcRect.right = srcRect.left + (int) realCropHeight;
//返回实际的裁剪区域坐标
return srcRect;
裁剪并保存图片
裁剪图片的很简单,通过系统方法从图片中保存下裁剪区域的像素即可.
//裁剪当前的图片
Bitmap cropBmp = Bitmap.createBitmap(mBitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
图片的保存通过
Bitmap
提供的方法即可通过流保存到文件中.同时保存时还可以指定输出图片的质量,图片格式.
图片的大小由质量.大小及格式决定,压缩率更高,质量越低,图片宽高越小的,图片大小也就越小.
关于移动的限制
前面使用
TouchUtils
时已经提到提供了回调接口用于确定是否允许界面移动.往往在实际的操作也功能需求中这是需要限定的条件.
在此处也是需要限定的.
图片的移动不可以超过裁剪框所在的区域. 必须确保任何时候裁剪框都在图片的范围内
否则在裁剪时就可能出现裁剪到图片以外的区域,这会造成某些异常的抛出.
检测方式也很简单,由于裁剪框必须在图片内部,所以保证:
裁剪框的左边界永远在图片的左边界右边;
裁剪框的右边界永远在图片的右边界左边;
裁剪框的上边界永远在图片上边界的下边;
裁剪框的下边界永远在图片下边界的上边;
//此处仅列出 是否允许移动 的回调判断
@Override
public boolean isCanMovedOnX(float moveDistanceX, float newOffsetX) {
//mTempDstRectf是一个全局复用的暂存的rectf
//重置为图片原始绘制区域
mTempDstRectf.set(mBitmapRecf);
//使用新偏移量
mTempDstRectf.offset(newOffsetX, );
//判断当前新图片区域与裁剪区域left/right方向是否越界
return (mTempDstRectf.left <=mCropRecf.left && mTempDstRectf.right >=mCropRecf.right);
}
@Override
public boolean isCanMovedOnY(float moveDistacneY, float newOffsetY) {
//同理isCanMovedOnX()
mTempDstRectf.set(mBitmapRecf);
mTempDstRectf.offset(, newOffsetY);
return (mTempDstRectf.top <= mCropRecf.top && mTempDstRectf.bottom >= mCropRecf.bottom);
}
以下为修复前的代码,这部分判断是错误的.因为忽略了一个小细节问题.
float moveDistance = Math.abs(moveDistacneY);
RectF drawRectf = this.getRectfAfterMove(mBitmapRecf);
if (moveDistacneY > ) {
return moveDistance <= (mCropRecf.top - drawRectf.top);
} else {
return moveDistance <= (drawRectf.bottom - mCropRecf.bottom);
}
分析以上的代码可知,判断的原则还不变.原理上来说是没有错误的.但是存在的问题是:
是指触摸点按下
moveDistance
事件时坐标与当前触摸点坐标的距离.
ACTION_DOWN
而在以上错误代码中,
getRectfAfterMove()
方法已经将最近一次
ACTION_MOVE
事件中偏移量添加到临时的图片绘制区域对象中.
此时得到的
drawRectf
为上一次
ACTION_MOVE
事件后界面的位置;此时再添加上
moveDistance
并与裁剪区域比较时,实际上多了一部分的偏移量.
这个地方不应该使用
getRectfAfterMove()
去偏移图片绘制区域,而应该使用
newOffset
.
变量为此次
newOffset
事件与
ACTION_MOVE
事件之间坐标的距离,也就是我们需要判断的距离.
ACTION_DOWN
不使用
ACTION_DOWN
事件时界面的位置加上
moveDistance
进行判断的原因是,
TouchUtils
并没有提供
ACTION_DOWN
时偏移量的方法.
尽管实际上
TouchUtils
是存在这个变量的(
见mTempDrawOffset
),但不对外开放是为了保证方法的明确性.同时提供移动偏移量(
mDrawOffset
)和移动前偏移量(
mTempDrawOffset
)会很容易使调用者混乱.
详见TouchUtils类
关于缩放的限制
图片允许进行缩放,但裁剪框必须在图片内部.而裁剪框大小是固定的(在创建并显示初始图片时已经确定了).
因此图片缩放是有限制的,图片放大可以由用户自定义,此裁剪View中也没有限制;但是缩小的极限是不能小于裁剪框.
图片在缩小时需要保持图片的最小边要永远大于裁剪框,这样才能确保裁剪框内的区域是有效的图片区域.
//获取图片的宽高
float oldWidth = mTempBmpRectF.width();
float oldHeigh = mTempBmpRectF.height();
//计算新缩放的宽高
float newWidth = oldWidth * newScaleRate;
float newHeight = oldHeigh * newScaleRate;
//获取新缩放图片的最短边
float smallSize = newWidth > newHeight ? newHeight : newWidth;
//最短边必须大于裁剪区域的边
//否则不允许缩放
return smallSize >mCropRecf.width();
以上是缩放图片时的限制,需要注意的这里使用的绘制区域
mTempBmpRectf
,实际绘制时使用的变量是
mBmpRectf
.
此处是
TouchUtils
中提及的,每次一次缩放时,都会保存缩放前的图片大小,缩放时不断重绘界面使用的是
mBmpRectf
,此变量值会在缩放期间不断改变.
mTempBmpRectf
为暂存值,暂存了每一次缩放前图片的大小.在缩放结束之后会保存此次缩放后的大小.
(详见TouchUtils)
使用方式
关于此类的使用方式,后面会有一个专门使用此类进行裁剪图片的
Activity
详解,在一般情况下只要调用该
Activity
就可以进行正常的图片裁剪了.
当然不管是此类还是裁剪的
Activity
,都可以根据自己的需要进行修改.
以下附上简单的使用方式:
//将 CropView 添加到xml界面中
//加载一张图片,略
Bitmap mphoto;
//直接设置图片即可(在onCreate中)
mCropView.setImageBitmap(mphoto);
//若是在其它情况下需要修改图片时,应该手动调用一次重绘操作,裁剪View才会更新界面图片
//mCropView.postInvalidate();
//裁剪并保存当前裁剪框中的图片
//outputPath为裁剪后图片输出路径(完整的文件名哦,不只是文件夹)
//如:/storage/cropBmp.png
mCropView.restoreBitmap(outputPath);
//图片回收
//当裁剪完确定不再需要裁剪时,回收图片,请注意需要不再裁剪并不显示图片时才回收(基本上是 Activity 退出时再回收)
mCropView.recycleBitmap();
关于图片的回收工作,其实由于裁剪View中使用的是赋值时图片的引用对象,所以在外部直接回收图片也是可以的.但是
mPhoto.recycle();
中作了安全处理,所以建议还不管有没有回收图片,都进行一次
CropView
cropView.recycleBitmap();
源码
源码是裁剪工具View的源码.需要注意View本身只是个间接处理对象,仅仅是用于将界面显示给用户.实际的绘制和操作是在内部类
CropDrawUtils
处理
关于使用的接口并不需要特别关注,只是为了方便View的部分方法与此裁剪处理类的方法签名统一而使用的.
不使用接口的直接删除接口及 @Override 即可
裁剪类源码
/**
* Created by taro on 16/4/8.
*/
public class CropDraw extends AbsTouchEventHandle implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent, ICropDrawAction {
//创建工具类
private TouchUtils mTouch = new TouchUtils();
private View mDrawView = null;
public static final float DEFAULT_CROP_WIDTH = ;
public static final float DEFAULT_CROP_HEIGHT = ;
private Paint mPaint = null;
private Bitmap mBitmap = null;
private RectF mCropRecf = null;
private RectF mTempDstRectf = null;
private RectF mBitmapRecf = null;
private PointF mViewParams = null;
private float mCropWidth = DEFAULT_CROP_WIDTH;
private float mCropHeight = DEFAULT_CROP_HEIGHT;
//暂时性保存的半径(同理在绘制时也需要一个暂时性存放的数据)
private RectF mTempBmpRectF = null;
private boolean mIsFirstGetViewParams = true;
//针对构造函数,可有不同的需求,在此例中,其实并不需要context
//此参数是可有可无的,有时自定义绘制界面需要加载一些资源什么的需要用到context,
//这个时候就有用了,这个看需要
public CropDraw(View drawView) {
this.mDrawView = drawView;
//设置工具类的监听事件
mTouch.setMoveEvent(this);
mTouch.setScaleEvent(this);
mTouch.setIsShowLog(false);
//绑定view与触摸事件
this.mDrawView.setOnTouchListener(this);
initial();
}
/**
* 绘制界面
*
* @param canvas
*/
public void onDraw(Canvas canvas) {
getViewParams();
drawBitmap(canvas);
drawCropRecf(canvas);
mPaint.setAlpha();
}
@Override
public void setImageBitmap(Bitmap src) {
if (src == null) {
return;
} else {
mBitmap = src;
mBitmapRecf.setEmpty();
mDrawView.postInvalidate();
}
}
/**
* 设置裁剪区域的宽高大小,此方法应该在界面绘制之前调用有效
*
* @param eachSize 边长
* @return
*/
public boolean setCropWidthAndHeight(int eachSize) {
mCropWidth = eachSize;
mCropHeight = eachSize;
//调整裁剪区域宽高大小
//若调整了返回true,否则返回false
//此处返回相反的值是因为:使用指定参数设置成功的话返回true,若调整过了则使用不到参数,返回False
return !isAdjustCropWidthAndHeight();
}
/**
* 初始化数据及对象
*/
private void initial() {
mPaint = new Paint();
mBitmapRecf = new RectF();
mViewParams = new PointF();
mTempDstRectf = new RectF();
mCropRecf = new RectF();
this.setIsShowLog(false, null);
}
/**
* 调整裁剪区域的宽高大小
*
* @return
*/
private boolean isAdjustCropWidthAndHeight() {
boolean isChanged = false;
//默认裁剪区域的大小与图片最大边大小相同,都是90%
float largeCropSizeX = mViewParams.x * f;
float largeCropSizeY = mViewParams.y * f;
float largeCropSize = largeCropSizeX < largeCropSizeY ? largeCropSizeX : largeCropSizeY;
if (mCropWidth > largeCropSize || mCropWidth <= ) {
mCropWidth = largeCropSize;
isChanged = true;
}
if (mCropHeight > largeCropSize || mCropHeight <= ) {
mCropHeight = largeCropSize;
isChanged = true;
}
return isChanged;
}
/**
* 获取当前view的宽高等参数
*/
private void getViewParams() {
if (mIsFirstGetViewParams) {
mViewParams.x = mDrawView.getWidth();
mViewParams.y = mDrawView.getHeight();
}
}
/**
* 绘制图片
*
* @param canvas
* @return
*/
private boolean drawBitmap(Canvas canvas) {
if (mBitmap != null && !mBitmap.isRecycled() && canvas != null) {
//第一次绘制先计算图片的绘制大小
if (mBitmapRecf == null || mBitmapRecf.isEmpty()) {
//计算图片缩放显示的绘制区域
int width = mBitmap.getWidth();
int height = mBitmap.getHeight();
//计算最大绘制区域
//此处不使用屏幕大小是为了四边留空白
float largeDrawWidth = mViewParams.x * f;
float largeDrawHeight = mViewParams.y * f;
//以图片高填充屏幕,计算填充的宽度
int tempDrawWidth = (int) ((largeDrawHeight / height) * width);
//以图片宽填充屏幕,计算填充的高度
int tempDrawHeight = (int) ((largeDrawWidth / width) * height);
//若填充宽度小于最大绘制宽度时
if (tempDrawWidth < largeDrawWidth) {
//以高填充(填充后的宽不超过屏幕宽,图片可以完全显示)
width = tempDrawWidth;
height = (int) largeDrawHeight;
} else {
//否则,以宽填充,填充后的高不超过屏幕宽,图片可以完全显示
height = tempDrawHeight;
width = (int) largeDrawWidth;
}
//不存在以下的情况:
//不管以高填充还是宽填充,填充后的另一边都超过屏幕的长度,数学证明略
//创建绘制区域
mBitmapRecf.left = (mViewParams.x - width) / ;
mBitmapRecf.right = mBitmapRecf.left + width;
mBitmapRecf.top = (mViewParams.y - height) / ;
mBitmapRecf.bottom = mBitmapRecf.top + height;
//计算绘制区域的最小边,与裁剪区域的大小进行比较
float smallDrawSize = ;
if (mBitmapRecf.width() > mBitmapRecf.height()) {
smallDrawSize = mBitmapRecf.height();
} else {
smallDrawSize = mBitmapRecf.width();
}
//创建裁剪区域对象
float centerX = mViewParams.x / ;
float centerY = mViewParams.y / ;
mCropRecf.left = centerX - smallDrawSize / ;
mCropRecf.top = centerY - smallDrawSize / ;
mCropRecf.right = mCropRecf.left + smallDrawSize;
mCropRecf.bottom = mCropRecf.top + smallDrawSize;
//更新裁剪区域的边大小
mCropWidth = mCropRecf.width();
mCropHeight = mCropRecf.height();
Log.i("bmpdraw", "bitmap\nleft=" + mBitmapRecf.left + "\nright=" + mBitmapRecf.right
+ "\ntop=" + mBitmapRecf.top + "\nbottom=" + mBitmapRecf.bottom);
Log.i("bmpcrop", "crop\nleft=" + mCropRecf.left + "\nright=" + mCropRecf.right
+ "\ntop=" + mCropRecf.top + "\nbottom=" + mCropRecf.bottom);
}
RectF moveRectf = getRectfAfterMove(mBitmapRecf, mTempDstRectf);
canvas.drawBitmap(mBitmap, null, moveRectf, mPaint);
// Log.i("bmpmove", "bitmap\nleft=" + moveRectf.left + "\nright=" + moveRectf.right
// + "\ntop=" + moveRectf.top + "\nbottom=" + moveRectf.bottom);
return true;
} else {
return false;
}
}
/**
* 获取移动后的实际绘制区域
*
* @param src 默认的绘制区域
* @param dst
* @return 返回新的绘制区域对象
*/
private RectF getRectfAfterMove(RectF src, RectF dst) {
if (src != null) {
if (dst == null) {
dst = new RectF(src);
} else {
dst.set(src);
}
//X/Y添加上偏移量
dst.left += mTouch.getDrawOffsetX();
dst.top += mTouch.getDrawOffsetY();
dst.right += mTouch.getDrawOffsetX();
dst.bottom += mTouch.getDrawOffsetY();
return dst;
} else {
return null;
}
}
/**
* 获取当前裁剪图片区域在实际图片中的裁剪区域
*
* @param bitmap 原始图片对象
* @param cropRectF 当前裁剪区域
* @param bmpRectF 图片绘制区域
* @return 返回实际图片中的裁剪区域大小
*/
private Rect getBitmapScaleRect(Bitmap bitmap, RectF cropRectF, RectF bmpRectF) {
//图片存在且相应的区域都有效的情况下才能进行计算
if (bitmap != null && cropRectF != null && bmpRectF != null) {
//图片实际宽高
int bmpWidth = bitmap.getWidth();
int bmpHeight = bitmap.getHeight();
//图片绘制的区域宽高
float drawWidth = bmpRectF.width();
float drawHeight = bmpRectF.height();
//当前需要裁剪图片区域的宽高
float cropWidth = cropRectF.width();
float cropHeight = cropRectF.height();
//计算实际的裁剪宽高
float realCropWidth = (cropWidth / drawWidth) * bmpWidth;
float realCropHeight = realCropWidth;
//计算实际裁剪区域的坐标
float realCropTop = ((cropRectF.top - bmpRectF.top) / drawHeight) * bmpHeight;
float realCropLeft = ((cropRectF.left - bmpRectF.left) / drawWidth) * bmpWidth;
//创建实际裁剪区域
Rect srcRect = new Rect();
srcRect.top = (int) realCropTop;
srcRect.left = (int) realCropLeft;
srcRect.bottom = srcRect.top + (int) realCropWidth;
srcRect.right = srcRect.left + (int) realCropHeight;
return srcRect;
} else {
return null;
}
}
@Override
public boolean restoreBitmap(String fileNameWithPath, Bitmap.CompressFormat bmpFormat, boolean isRecycleBmp, int bmpQuality) {
if (!TextUtils.isEmpty(fileNameWithPath) && !mBitmap.isRecycled()) {
//默认使用PNG
if (bmpFormat == null) {
String lowerPath = fileNameWithPath.toLowerCase();
if (lowerPath.endsWith("png")) {
bmpFormat = Bitmap.CompressFormat.PNG;
} else if (lowerPath.endsWith("jpg") || lowerPath.endsWith("jpeg")) {
bmpFormat = Bitmap.CompressFormat.JPEG;
} else {
bmpFormat = Bitmap.CompressFormat.PNG;
}
}
if (bmpQuality > || bmpQuality < ) {
bmpQuality = ;
}
//获取图片裁剪区域
Rect srcRect = getBitmapScaleRect(mBitmap, mCropRecf, getRectfAfterMove(mBitmapRecf, mTempDstRectf));
//裁剪当前的图片
Bitmap cropBmp = Bitmap.createBitmap(mBitmap, srcRect.left, srcRect.top, srcRect.width(), srcRect.height());
try {
File bitmapFile = new File(fileNameWithPath);
if (!bitmapFile.exists()) {
bitmapFile.createNewFile();
}
//将图片保存到文件中
FileOutputStream out = new FileOutputStream(bitmapFile);
cropBmp.compress(bmpFormat, bmpQuality, out);
if (isRecycleBmp) {
//回收图片
mBitmap.recycle();
}
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
//不管是否裁剪成功,都回收裁剪后的图片,因为这部分是用不到的
if (cropBmp != null && !cropBmp.isRecycled()) {
cropBmp.recycle();
}
}
} else {
return false;
}
}
@Override
public void recycleBitmap() {
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
}
}
/**
* 绘制图片裁剪界面,实际上只是绘制一个透明的蒙层
*
* @param canvas
* @return
*/
private boolean drawCropRecf(Canvas canvas) {
if (canvas != null && mCropRecf != null) {
RectF topRecf = new RectF(, , mViewParams.x, mCropRecf.top);
RectF bottomRecf = new RectF(, mCropRecf.bottom, mViewParams.x, mViewParams.y);
RectF leftRecf = new RectF(, mCropRecf.top, mCropRecf.left, mCropRecf.bottom);
RectF rightRecf = new RectF(mCropRecf.right, mCropRecf.top, mViewParams.x, mCropRecf.bottom);
mPaint.setColor(Color.BLACK);
mPaint.setAlpha((int) ( * ));
canvas.drawRect(topRecf, mPaint);
canvas.drawRect(bottomRecf, mPaint);
canvas.drawRect(leftRecf, mPaint);
canvas.drawRect(rightRecf, mPaint);
return true;
} else {
return false;
}
}
@Override
public void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) {
//工具类默认处理的单点触摸事件
mTouch.singleTouchEvent(event, extraMotionEvent);
}
@Override
public void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) {
//工具类默认处理的多点(实际只处理了两点事件)触摸事件
mTouch.multiTouchEvent(event, extraMotionEvent);
}
@Override
public void onSingleClickByTime(MotionEvent event) {
//基于时间的单击事件
//按下与抬起时间不超过500ms
}
@Override
public void onSingleClickByDistance(MotionEvent event) {
//基于距离的单击事件
//按下与抬起的距离不超过20像素(与时间无关,若按下不动几小时后再放开只要距离在范围内都可以触发)
}
@Override
public void onDoubleClickByTime() {
//基于时间的双击事件
//单击事件基于clickByTime的两次单击
//两次单击之间的时间不超过250ms
}
@Override
public boolean isCanMovedOnX(float moveDistanceX, float newOffsetX) {
mTempDstRectf.set(mBitmapRecf);
mTempDstRectf.offset(newOffsetX, );
return (mTempDstRectf.left <= mCropRecf.left && mTempDstRectf.right >= mCropRecf.right);
}
@Override
public boolean isCanMovedOnY(float moveDistacneY, float newOffsetY) {
mTempDstRectf.set(mBitmapRecf);
mTempDstRectf.offset(, newOffsetY);
return (mTempDstRectf.top <= mCropRecf.top && mTempDstRectf.bottom >= mCropRecf.bottom);
}
@Override
public void onMove(int suggestEventAction) {
mDrawView.postInvalidate();
}
@Override
public void onMoveFail(int suggetEventAction) {
}
@Override
public boolean isCanScale(float newScaleRate) {
if (mTempBmpRectF == null) {
mTempBmpRectF = new RectF(mBitmapRecf);
}
float oldWidth = mTempBmpRectF.width();
float oldHeigh = mTempBmpRectF.height();
float newWidth = oldWidth * newScaleRate;
float newHeight = oldHeigh * newScaleRate;
//获取绘制区域的最短边
float smallSize = newWidth > newHeight ? newHeight : newWidth;
//最短边必须大于裁剪区域的边
return smallSize > mCropRecf.width();
}
@Override
public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
//更新当前的数据
//newScaleRate缩放比例一直是相对于按下时的界面的相对比例,所以在移动过程中
//每一次都是要与按下时的界面进行比例缩放,而不是针对上一次的结果
//使用这种方式一方面在缩放时的思路处理是比较清晰的
//另一方面是缩放的比例不会数据很小(若相对于上一次,每一次move移动几个像素,
//这种情况下缩放的比例相对上一次肯定是0.0XXXX,数据量一小很容易出现一些不必要的问题)
if (mTempBmpRectF == null) {
mTempBmpRectF = new RectF(mBitmapRecf);
}
float lastWidth = mTempBmpRectF.width();
float lastHeight = mTempBmpRectF.height();
float newWidth = lastWidth * newScaleRate;
float newHeight = lastHeight * newScaleRate;
mBitmapRecf.left = mTempBmpRectF.centerX() - newWidth / ;
mBitmapRecf.top = mTempBmpRectF.centerY() - newHeight / ;
mBitmapRecf.right = mBitmapRecf.left + newWidth;
mBitmapRecf.bottom = mBitmapRecf.top + newHeight;
//当返回的标志为true时,提醒为已经到了up事件
//此时应该把最后一次缩放的比例当做最终的数据保存下来
if (isNeedStoreValue) {
mTempBmpRectF.set(mBitmapRecf);
}
}
@Override
public void onScale(int suggestEventAction) {
if (suggestEventAction == MotionEvent.ACTION_POINTER_UP) {
//调整缩放后图片的位置必须在裁剪框中
RectF drawRectf = this.getRectfAfterMove(mBitmapRecf, mTempDstRectf);
float offsetX = mTouch.getDrawOffsetX();
float offsetY = mTouch.getDrawOffsetY();
if (drawRectf.left > mCropRecf.left) {
offsetX += mCropRecf.left - drawRectf.left;
}
if (drawRectf.top > mCropRecf.top) {
offsetY += mCropRecf.top - drawRectf.top;
}
if (drawRectf.right < mCropRecf.right) {
offsetX += mCropRecf.right - drawRectf.right;
}
if (drawRectf.bottom < mCropRecf.bottom) {
offsetY += mCropRecf.bottom - drawRectf.bottom;
}
mTouch.setOffsetX(offsetX);
mTouch.setOffsetY(offsetY);
}
mDrawView.postInvalidate();
}
@Override
public void onScaleFail(int suggetEventAction) {
}
}
接口源码
另附上接口文件
/**
* Created by taro on 16/4/8.
*/
public interface ICropDrawAction {
/**
* 保存图片
*
* @param fileNameWithPath 保存路径
* @param bmpFormat 保存图片的格式
* @param isRecycleBmp 是否回收图片,若true则回收图片,若false则不回收图片
* @param bmpQuality 保存图片的质量,在0-100,格式为PNG时此参数无效.
* @return 若成功保存图片返回true, 否则返回false
*/
public boolean restoreBitmap(String fileNameWithPath, Bitmap.CompressFormat bmpFormat, boolean isRecycleBmp, int bmpQuality);
/**
* 回收图片
*/
public void recycleBitmap();
/**
* 设置裁剪区域的宽高大小,此方法应该在界面绘制之前调用有效
*
* @param eachSize 边长
* @return
*/
public boolean setCropWidthAndHeight(int eachSize);
/**
* 设置imageBitmap
*
* @param src
*/
public void setImageBitmap(Bitmap src);
}
GitHub地址
https://github.com/CrazyTaro/TouchEventHandle
以上为裁剪View的代码,项目中
CropView
是使用于
Activity
中,后面会详细说明该
Activity
并附带简单的图片辅助功能.
示例GIF
示例图片裁剪
修复代码之前存在的问题
可以看到在图片移动时可能会将图片移动到界面裁剪区域以外,这种情况下下面会出现一条白条(背景色).这种情况下无法保存裁剪的图片,会出错.
回到目录