
距离图片
-
2023年2月16日发(作者:)图⽚裁剪功能的实现
概述
从4⽉初到5⽉份,差不多⼀个多⽉,终于把裁剪图⽚的功能码出来了,期间,解决了⼀个⼜来⼀个问题,好吧,问题总是会有的。
这⾥⼤致介绍这个裁剪功能技术点、主要难点,实现原理。
技术点
图⽚缩放、移动
裁剪区域预览
裁剪(包括越图⽚边界裁剪)
边界限制
主要难点
裁剪区域预览
裁剪
边界限制
实现原理
裁剪预览区域的实现
在我做过的项⽬中,就有使⽤过⼀些⽹络上开源的裁剪功能:半透明遮罩层的矩形预览框功能。它的实现原理是在裁剪预览区域外的地⽅填
充了⼏个半透明的矩形框,进⽽实现了矩形裁剪预览框功能,如下图。
这种功能虽然可以实现预览功能,但是仅仅局限于当预览区外的地⽅可以通过规则的形状填充,如果是圆形的裁剪预览框,那么就没办法通
过这种⽅式来实现了。
所以我们需要另外想过办法来实现圆形的预览框。在⼀开始的时候,我这边的思路是通过在半透明的遮罩层上镂空⼀个预览框。我们来试试
在半透明的遮罩层上叠加⼀个透明的预览框。
publicvoidonDraw(Canvascanvas){
//绘画半透明遮罩
lor(olor(\"#90000000\"));
Paintpaint=newPaint();
or(ARENT);
le();
iAlias(true);
intleft=getWidth()/2;
inttop=getHeight()/2;
//绘制透明预览框
rcle(left,top,300,paint);
}
效果可见下图。
可以看出,虽然在中间⽩⾊的预览框是全透明的⼀个裁剪预览框,,下⾯还是会有⼀层半透明的遮罩层覆盖住图⽚,实现不了预览框全透明
的效果。
看来这种简单的叠加⽅式是⽆法实现我们的需求,所以通过搜寻资料,最终,发现可以采⽤Xfermode⽅式来实现。通过设定Xfermode模
式,可以将两个重叠的层通过⼀定的⽅式来显⽰,例如下层是半透明遮罩,上层是透明圆形框,那么可以通过设置相应的Xfermode模式来
实现。我们改⼀下上⾯的代码:
publicvoidonDraw(Canvascanvas){
//这⾥需要通过bitmap创建canvas才能对Xfermode⽣效果,具体原因这⾥也不⼤清楚
Bitmapbitmap=Bitmap(getWidth(),getHeight(),_4444);
CanvasxFerCanvas=newCanvas(bitmap);
//绘画半透明遮罩
lor(olor(\"#90000000\"));
Paintpaint=newPaint();
or(ARENT);
le();
iAlias(true);
//设置当前画笔的Xfermode模式,不同的模式效果可以参照Google提供的Demo-ApiDemos/Graphics/XferModes
rmode(newPorterDuffXfermode());
intleft=getWidth()/2;
inttop=getHeight()/2;
//绘制透明预览框
rcle(left,top,300,paint);
//最后将⽣成的bitmap绘制到我们的画布上
tmap(bitmap,0,0,null);
e();
();
}
效果可见下图
可以看出,实现了我们想要的效果。
对Xfermode更加详细的讲解可以阅读博⽂,⾥⾯有详细的讲解不同的Xfermode对层叠加的不同效果。虽然这种⽅案可以实现效果,但是这
种⽅案有⼀个很⼤的缺点,就是需要创建⼀个新的Bitmap,会导致内容占⽤率⼤量提⾼。所以这⾥通过了博⽂找到了第⼆种⽅案。第⼆种
⽅案的实现思路是:在绘画半透明遮罩之前,先将画布可以绘画位置限定在裁剪预览框之外,这样绘画的半透明遮罩⾃然就空下了中间的预
览框,这样就实现了该功能。
publicvoidonDraw(Canvascanvas){
Paintpaint=newPaint();
or(ARENT);
le();
iAlias(true);
intleft=getWidth()/2;
inttop=getHeight()/2;
//创建圆形预览框
Pathpath=newPath();
cle(left,top,300,);
//保存当前canvas状态
();
//将当前画布可以绘画区域限制死为预览框外的区域
th(path,ENCE);
//绘画半透明遮罩
lor(olor(\"#90000000\"));
//还原画布状态
e();
}
这⾥就不贴图了,最终效果与采⽤Xfermode叠加⽅案是⼀样,并且不需要创建新的Bitmap,不会导致内存占⽤率⼤量提⾼。但是这种⽅
案也有其局限性,由于我们只能通过Path来限制其在画布可绘画的区域,并且Path只⽀持⼀些⼏何形的图案,所以预览框形状被限死在⼏
何形图案集合内。
这⾥总结⼀下上⾯两种⽅案和其应⽤场景:
如果是⼏何形的预览框,那么⾸推限制绘画区域的⽅案,内存占⽤率低。
如果是⾮⼏何形的预览框(例如卡通形状的预览框),那么在这⾥给出的⽅案⾥,你只能通过Xfermode⽅式来实现了,不过使⽤这种⽅
式需要注意内存的占有率。
图⽚缩放&移动实现
这⾥的图⽚缩放、移动全部通过Matrix实现的。其实移动的实现⽅式可以采⽤两种⽅式:
By(int,int)或者To(int,int)⽅式实现.
图⽚Matrix处理。
但是这⾥由于需要实现缩放功能,所以⼲脆统⼀采⽤Matrix⽅式来实现。Matrix是⼀组参数集合,其中不同的参数对应着不同的功能处
理(平移/缩放等),具体可以查看博⽂
这⾥只讲述Matrix实现的缩放与移动,不对**移动⽅式多做说明。
在通过Matrix实现缩放、移动之前,需要调⽤leType(),将ImageView的缩放⽅
式设置为MATRIX。
Matrix移动实现
Matrix移动的实现⼗分简单,通过记录最后移动点与当前移动点的距离就可以实现移动功能。
//motionX,motionY为当前触摸的坐标
publicvoiddrag(floatmotionX,floatmotionY){
//mLastY,mLastX为上⼀次触摸的坐标
floatmoveX=motionX-mLastX;
floatmoveY=motionY-mLastY;
//通过postTranslate⽅法就可以移动到相应的位置
geMatrix().postTranslate(moveX,moveY);
//重画视图
date();
}
Matrix缩放实现
Martix缩放功能也是相对简单。通过ScaleGestureDetector⽅式实现了缩放功能。我们主要通过实现ScaleGestureDetector,重写
onScale⽅法,当然由于与移动功能叠加,所以需要在缩放的时候,屏蔽掉移动功能,所以我们需要记录缩放开始与结束。
@Override
publicbooleanonScale(ScaleGestureDetectordetector){
//px,py为缩放的中⼼点,以该点为中⼼点进⾏缩放
floatpx=usX();
floatpy=usY();
//缩放的⽐例⼤于1为放⼤,⼩于1为缩⼩
floatscaleFactor=leFactor();
//通过postScale⽅式来实现缩放效果
geMatrix().postScale(scaleFactor,scaleFactor,
px,py);
//重画视图
date();
returntrue;
}
@Override
publicbooleanonScaleBegin(ScaleGestureDetectordetector){
//设置缩放标志位
isScale=true;
returntrue;
}
@Override
publicvoidonScaleEnd(ScaleGestureDetectordetector){
isScale=false;
}
@Override
publicbooleantouch(MotionEventevent){
//缩放⼿势处理
hEvent(event);
//如果不在缩放中,则处理普通的触摸事件
if(!isScale){
}
}
returnfalse;
}
裁剪功能实现
如果没有移动与缩放功能,那么裁剪会是⼀个相当简单的功能,因为其裁剪的位置总是固定的,但是如果加⼊了移动与缩放,那么事情就变
的复杂了。当移动后与缩放后,裁剪的位置与⼤⼩都发⽣了变化,另外,移动和缩放可能导致图⽚部分或者全部不在预览框内,这些情况我
们都需要进⾏处理,下⾯我们看看怎么正确的裁剪出预览框显⽰的图⽚。由于为了照顾到图⽚不在预览框的情况,所以我们采⽤了以下⽅式
来做最终的图⽚裁剪:
publicvoidcrop(){
//其中width与height是最终实际裁剪的图⽚⼤⼩,saveBitmap就是最终裁剪的图⽚
BitmapsaveBitmap=Bitmap(width,height,_4444);
Canvascanvas=newCanvas(saveBitmap);
//bitmap为原图,这⾥就是最终裁剪图⽚的实现⽅式,其中cropRect是裁剪区域,showRect是最终显⽰在画布的区域
tmap(bitmap,cropRect,showRect,newPaint());
}
从上⾯代码段,我们可以清晰的知道影响裁剪的因素有:
最终裁剪的图⽚⼤⼩
实际裁剪的四个⾓的位置(相对于原图)
显⽰裁剪图⽚的四个⾓的位置(相对于画布)
注意以下的计算的前置条件是原图⽚中⼼点与预览框中⼼点均与屏幕中⼼点重叠
其中裁剪的图⽚⼤⼩我们很容易就可以根据裁剪预览框的⼤⼩与原图⽚缩放的倍数来获取。
我们通过裁剪的左上⾓起始坐标与最终裁剪的图⽚⼤⼩,来获取裁剪的四个位置。
//actuallyWidth与actuallyHeight为裁剪的实际长宽
//原图中⼼点x坐标--实际图⽚x坐标中⼼点-横坐标的实际偏移量,就可以得出裁剪的左上⾓位置
//,由于这⾥采⽤⽐较⼼点的⽅式去得到实际横坐标偏移量,所以这⾥可以不⽤理会缩放与移动产⽣的偏移量。
cropLeft=(int)(th()/uallyScrollX()/scale-actuallyWidth/2);
//这⾥也是⼀样
cropTop=(int)(ght()/uallyScrollY()/scale-actuallyHeight/2);
//这⾥会进⾏边界判断,默认右边点为左边点+宽度
intcropRight=cropLeft+actuallyWidth;
intcropBottom=cropTop+actuallyHeight;
//裁剪总宽度超出原图宽度,需要重新设置右边点位置为图⽚宽度
if(cropRight>th()){
cropRight=th();
}
//裁剪总⾼度超出原图⾼度,需要重新设置右边点位置为图⽚⾼度
if(cropBottom>ght()){
cropBottom=ght();
}
⽽显⽰区域的四个⾓位置,获取就相对简单。其中左与上固定为0,剩下的就是右边与底部点了。
//由于裁剪区域与显⽰区域长宽应该是⼀致的,所以这⾥默认右边与底部为最终裁剪⼤⼩
intshowRight=actuallyWidth;
intshowBottom=actuallyHeight;
intcropRight=cropLeft+actuallyWidth;
intcropBottom=cropTop+actuallyHeight;
//裁剪超出图⽚边界超出边界
if(cropRight>th()){
cropRight=th();
//由于左固定为0,那么这⾥相应也要调整右边位置,让宽度与裁剪区域⼀致
showRight=th()-cropLeft;
}
if(cropBottom>ght()){
cropBottom=ght();
//由于上位置固定为0,那么这⾥相应也要调整底部位置,让⾼度与裁剪区域⼀致
showBottom=ght()-cropTop;
}
⾄此,裁剪所需要的参数全部计算完毕,这样就可以正确裁剪出预览框中的内容。
边界限制
为了提升⽤户体验,或者是实现需求,可能我们需要限制缩放&移动的边界,让裁剪预览框的区域可以完全在图⽚⾥⾯,换个意思就是说,
裁剪最后的图⽚⼀定是图⽚上的某个区域,⽽不会出现只裁剪到⼀部分图⽚,另⼀部分是空⽩的。
边界的限制只是针对移动与缩放。下⾯我们分别看看怎么对两者做边界限制
移动边界限制
同样由于涉及到了缩放,移动的边界限制需要特别处理。具体的思路是获取当前移动的距离与当前图⽚在屏幕上实际的四个位置点(左右上
下),例如我们需要判断是否会超过左边界,那么我们会判断横坐标移动的距离+图⽚当前左边位置是否⼤于限制框的左边横坐标,是的
话,那么则视为出界,应当重新计算移动距离。其他三个位置亦是如此,我们还是看下下⾯的代码⽚。
publicvoiddrag(floatmotionX,floatmotionY){
//移动距离
floatmoveX=motionX-mLastX;
floatmoveY=motionY-mLastY;
//mRestrictRect为限制框,这个框实质就是预览框在屏幕上的坐标位置
if(mRestrictRect!=null){
//经过缩放与移动后,图⽚在屏幕上实际的位置
RectFrectF=getCurrentRectF();
//下⾯为四边边界的判断与重计算
if(moveX>0){
if(+moveX>){
moveX=;
}
}else{
if(+moveX<){
moveX=;
}
}
if(moveY>0){
if(+moveY>){
moveY=;
}
}else{
if(+moveY<){
moveY=;
}
}
}
geMatrix().postTranslate(moveX,moveY);
date();
}
缩放边界限制
缩放边界的限制会相对复杂。因为当缩放出界时,需要根据多种情况重新计算缩放所需要的参数。
缩放边界限制的流程是:
1、按照缩放值,获取图⽚将会在屏幕出现的位置
2、判断四个位置是否会超出边界,并记录四个位置边界判断结果。如果缩放后的某个位置会超出限制框的边界位置,那么则限定该坐标为
缩放中⼼点,保证该点位置不移动。例如左边将会出界,那么则以限制框的左边位置作为最终缩放中⼼点横坐标。
3、如果左右或者上下出界,则不⽤进⾏缩放,因为⽆法缩放了。如果不是这种情况,则进⼊4
4、根据Martrix缩放的计算公式推导出,什么缩放倍数下,会达到限制框的边界值。这⾥将会取到四个边界缩放值与当前的缩放值进⾏⽐
对,取其中最⼤的缩放值作为最后的缩放值(因为缩⼩情况才会导致越界)
我们看看具体的代码⽚
publicbooleanonScale(ScaleGestureDetectordetector){
//初始化缩放值
floatpx=usX();
floatpy=usY();
floatscaleFactor=leFactor();
if(mRestrictRect!=null){
MatrixmatrixAfter=newMatrix(geMatrix());
ale(leFactor()
,leFactor(),usX(),usY());
finalBitmapDrawabledrawable=(BitmapDrawable)wable();
finalBitmapbitmap=map();
RectFrectF=newRectF(0,0,th(),ght());
//上⾯的⼤段代码都是为了这⾥,这⾥将会获取按照当前缩放值缩放后的图⽚实际的坐标位置
t(rectF);
booleanisLeftLimit=false,isRightLimit=false,
isTopLimit=false,isBottomLimit=false;
isTopLimit=false,isBottomLimit=false;
//判断缩放后的位置是否会超过边界
if(>){
//超过边界则将点最为最后的缩放中⼼点,让该边界的点固定下来,不被改变
px=;
isLeftLimit=true;
}
if(<){
px=;
isRightLimit=true;
}
if(>){
py=;
isTopLimit=true;
}
if(<){
py=;
isBottomLimit=true;
}
//左右两边或者上下两边都⽆法缩放,就不缩放了
if((isRightLimit&&isLeftLimit)||(isTopLimit&&isBottomLimit)){
returntrue;
}
//重新计算允许的最⼩缩放倍数,根据四条边界的缩放倍数与当前的缩放倍数,
//获取最⼤缩放倍数,因为主要是缩⼩才会导致超越边界
//计算公式是:结果坐标(ResultX)=缩放前坐标(BeforeX)*缩放倍数(scale)
//+中⼼点坐标(centerX)*(1-缩放倍数(scale))
floatmaxScaleLeft=(-px)/(getCurrentRectF().left-px);
if(scaleFactor scaleFactor=maxScaleLeft; } floatmaxScaleRight=(-px)/(getCurrentRectF().right-px); if(scaleFactor scaleFactor=maxScaleRight; } floatmaxScaleTop=(-py)/(getCurrentRectF().top-py); if(scaleFactor scaleFactor=maxScaleTop; } floatmaxSacleBottom=(-py)/(getCurrentRectF().bottom-py); if(scaleFactor scaleFactor=maxSacleBottom; } } //保存当前的缩放值 mScale=mScale*scaleFactor; //执⾏缩放 geMatrix().postScale(scaleFactor,scaleFactor, px,py); date(); returntrue; } GitHub地址 最后附录上EnjoyCrop源码,希望这篇⽂档对你有帮助,谢谢!