概述
從4月初到5月份 ,差不多一個多月,終於把裁剪圖片的功能碼出來了,期間,解決了一個又來一個問題,好吧,問題總是會有的。
這里大致介紹這個裁剪功能技術點、主要難點,實現原理。
技術點####
- 圖片縮放、移動
- 裁剪區域預覽
- 裁剪(包括越圖片邊界裁剪)
- 邊界限制
主要難點
- 裁剪區域預覽
- 裁剪
- 邊界限制
實現原理####
裁剪預覽區域的實現#####
在我做過的項目中,就有使用過一些網絡上開源的裁剪功能:半透明遮罩層的矩形預覽框功能。它的實現原理是在裁剪預覽區域外的地方填充了幾個半透明的矩形框,進而實現了矩形裁剪預覽框功能,如下圖。
這種功能雖然可以實現預覽功能,但是僅僅局限於當預覽區外的地方可以通過規則的形狀填充,如果是圓形的裁剪預覽框,那么就沒辦法通過這種方式來實現了。
所以我們需要另外想過辦法來實現圓形的預覽框。在一開始的時候,我這邊的思路是通過在半透明的遮罩層上鏤空一個預覽框。我們來試試在半透明的遮罩層上疊加一個透明的預覽框。
public void onDraw(Canvas canvas){
//繪畫半透明遮罩
canvas.drawColor(Color.parseColor("#90000000"));
Paint paint = new Paint();
paint.setColor(Color.TRANSPARENT);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
int left = getWidth()/2;
int top = getHeight()/2;
//繪制透明預覽框
canvas.drawCircle(left, top, 300, paint);
}
效果可見下圖。
可以看出,雖然在中間白色的預覽框是全透明的一個裁剪預覽框,,下面還是會有一層半透明的遮罩層覆蓋住圖片,實現不了預覽框全透明的效果。
看來這種簡單的疊加方式是無法實現我們的需求,所以通過搜尋資料,最終,發現可以采用Xfermode方式來實現。通過設定Xfermode模式,可以將兩個重疊的層通過一定的方式來顯示,例如下層是半透明遮罩,上層是透明圓形框,那么可以通過設置相應的Xfermode模式來實現。我們改一下上面的代碼:
public void onDraw(Canvas canvas){
//這里需要通過bitmap創建canvas才能對Xfermode生效果,具體原因這里也不大清楚
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
Canvas xFerCanvas = new Canvas(bitmap);
//繪畫半透明遮罩
xFerCanvas.drawColor(Color.parseColor("#90000000"));
Paint paint = new Paint();
paint.setColor(Color.TRANSPARENT);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
//設置當前畫筆的Xfermode模式,不同的模式效果可以參照Google提供的Demo-ApiDemos/Graphics/XferModes
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
int left = getWidth()/2;
int top = getHeight()/2;
//繪制透明預覽框
xFerCanvas.drawCircle(left, top, 300, paint);
//最后將生成的bitmap繪制到我們的畫布上
canvas.drawBitmap(bitmap,0,0,null);
bitmap.recycle();
System.gc();
}
效果可見下圖
可以看出,實現了我們想要的效果。
對Xfermode更加詳細的講解可以閱讀博文時之沙-Android 顏色渲染(九) PorterDuff及Xfermode詳解,里面有詳細的講解不同的Xfermode對層疊加的不同效果。雖然這種方案可以實現效果,但是這種方案有一個很大的缺點,就是需要創建一個新的Bitmap,會導致內容占用率大量提高。所以這里通過了博文JianTao_Yang-Android ImageCropper 矩形 圓形 裁剪框找到了第二種方案。第二種方案的實現思路是:在繪畫半透明遮罩之前,先將畫布可以繪畫位置限定在裁剪預覽框之外,這樣繪畫的半透明遮罩自然就空下了中間的預覽框,這樣就實現了該功能。
public void onDraw(Canvas canvas){
Paint paint = new Paint();
paint.setColor(Color.TRANSPARENT);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
int left = getWidth()/2;
int top = getHeight()/2;
//創建圓形預覽框
Path path = new Path();
path.addCircle(left, top, 300, Path.Direction.CW);
//保存當前canvas 狀態
canvas.save();
//將當前畫布可以繪畫區域限制死為預覽框外的區域
canvas.clipPath(path, Region.Op.DIFFERENCE);
//繪畫半透明遮罩
canvas.drawColor(Color.parseColor("#90000000"));
//還原畫布狀態
canvas.restore();
}
這里就不貼圖了,最終效果與采用Xfermode疊加方案是一樣,並且不需要創建新的Bitmap,不會導致內存占用率大量提高。但是這種方案也有其局限性,由於我們只能通過Path來限制其在畫布可繪畫的區域,並且Path只支持一些幾何形的圖案,所以預覽框形狀被限死在幾何形圖案集合內。
這里總結一下上面兩種方案和其應用場景:
- 如果是幾何形的預覽框,那么首推限制繪畫區域的方案,內存占用率低。
- 如果是非幾何形的預覽框(例如卡通形狀的預覽框),那么在這里給出的方案里,你只能通過Xfermode方式來實現了,不過使用這種方式需要注意內存的占有率。
圖片縮放&移動實現#####
這里的圖片縮放、移動全部通過Matrix實現的。其實移動的實現方式可以采用兩種方式:
- View.scrollBy(int,int)或者View.scrollTo(int,int)方式實現.
- 圖片Matrix處理。
但是這里由於需要實現縮放功能,所以干脆統一采用Matrix方式來實現。Matrix是一組參數集合,其中不同的參數對應着不同的功能處理(平移/縮放等),具體可以查看博文Qiengo-Android Matrix
這里只講述Matrix實現的縮放與移動,不對View.scroll**移動方式多做說明。
在通過Matrix實現縮放、移動之前,需要調用ImageView.setScaleType(ImageView.ScaleType.MATRIX),將ImageView的縮放方式設置為MATRIX。
Matrix移動實現
Matrix移動的實現十分簡單,通過記錄最后移動點與當前移動點的距離就可以實現移動功能。
//motionX,motionY為當前觸摸的坐標
public void drag(float motionX, float motionY) {
//mLastY,mLastX 為上一次觸摸的坐標
float moveX = motionX -mLastX;
float moveY = motionY - mLastY;
//通過postTranslate方法就可以移動到相應的位置
mView.getImageMatrix().postTranslate(moveX,moveY);
//重畫視圖
mView.invalidate();
}
Matrix縮放實現
Martix縮放功能也是相對簡單。通過ScaleGestureDetector方式實現了縮放功能。我們主要通過實現ScaleGestureDetector,重寫onScale方法,當然由於與移動功能疊加,所以需要在縮放的時候,屏蔽掉移動功能,所以我們需要記錄縮放開始與結束。
@Override
public boolean onScale(ScaleGestureDetector detector) {
//px,py為縮放的中心點,以該點為中心點進行縮放
float px = detector.getFocusX();
float py = detector.getFocusY();
//縮放的比例 大於1為放大, 小於1為縮小
float scaleFactor= detector.getScaleFactor();
//通過postScale方式來實現縮放效果
mView.getImageMatrix().postScale(scaleFactor,scaleFactor,
px,py);
//重畫視圖
mView.invalidate();
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//設置縮放標志位
isScale = true;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
isScale = false;
}
@Override
public boolean touch(MotionEvent event){
//縮放手勢處理
mGestureDetector.onTouchEvent(event);
//如果不在縮放中,則處理普通的觸摸事件
if(!isScale){
}
}
return false;
}
裁剪功能實現#####
如果沒有移動與縮放功能,那么裁剪會是一個相當簡單的功能,因為其裁剪的位置總是固定的,但是如果加入了移動與縮放,那么事情就變的復雜了。當移動后與縮放后,裁剪的位置與大小都發生了變化,另外,移動和縮放可能導致圖片部分或者全部不在預覽框內,這些情況我們都需要進行處理,下面我們看看怎么正確的裁剪出預覽框顯示的圖片。由於為了照顧到圖片不在預覽框的情況,所以我們采用了以下方式來做最終的圖片裁剪:
public void crop() {
//其中width與height是最終實際裁剪的圖片大小,saveBitmap就是最終裁剪的圖片
Bitmap saveBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
Canvas canvas = new Canvas(saveBitmap);
//bitmap為原圖,這里就是最終裁剪圖片的實現方式,其中cropRect是裁剪區域,showRect是最終顯示在畫布的區域
canvas.drawBitmap(bitmap, cropRect, showRect, new Paint());
}
從上面代碼段,我們可以清晰的知道影響裁剪的因素有:
- 最終裁剪的圖片大小
- 實際裁剪的四個角的位置(相對於原圖)
- 顯示裁剪圖片的四個角的位置(相對於畫布)
注意以下的計算的前置條件是原圖片中心點與預覽框中心點均與屏幕中心點重疊
其中裁剪的圖片大小我們很容易就可以根據裁剪預覽框的大小與原圖片縮放的倍數來獲取。
我們通過裁剪的左上角起始坐標與最終裁剪的圖片大小,來獲取裁剪的四個位置。
//actuallyWidth與actuallyHeight為裁剪的實際長寬
//原圖中心點x坐標--實際圖片x坐標中心點-橫坐標的實際偏移量,就可以得出裁剪的左上角位置
//,由於這里采用比較心點的方式去得到實際橫坐標偏移量,所以這里可以不用理會縮放與移動產生的偏移量。
cropLeft = (int) (bitmap.getWidth()/2-mImageView.getActuallyScrollX()/scale-actuallyWidth/2);
//這里也是一樣
cropTop = (int) (bitmap.getHeight()/2-mImageView.getActuallyScrollY()/scale - actuallyHeight/2);
//這里會進行邊界判斷,默認右邊點為左邊點+寬度
int cropRight = cropLeft+actuallyWidth;
int cropBottom = cropTop+actuallyHeight;
//裁剪總寬度超出原圖寬度,需要重新設置右邊點位置為圖片寬度
if(cropRight>bitmap.getWidth()){
cropRight = bitmap.getWidth();
}
//裁剪總高度超出原圖高度,需要重新設置右邊點位置為圖片高度
if(cropBottom>bitmap.getHeight()){
cropBottom = bitmap.getHeight();
}
而顯示區域的四個角位置,獲取就相對簡單。其中左與上固定為0,剩下的就是右邊與底部點了。
//由於裁剪區域與顯示區域長寬應該是一致的,所以這里默認右邊與底部為最終裁剪大小
int showRight = actuallyWidth;
int showBottom = actuallyHeight;
int cropRight = cropLeft+actuallyWidth;
int cropBottom = cropTop+actuallyHeight;
//裁剪超出圖片邊界超出邊界
if(cropRight>bitmap.getWidth()){
cropRight = bitmap.getWidth();
//由於左固定為0,那么這里相應也要調整右邊位置,讓寬度與裁剪區域一致
showRight = bitmap.getWidth()-cropLeft;
}
if(cropBottom>bitmap.getHeight()){
cropBottom = bitmap.getHeight();
//由於上位置固定為0,那么這里相應也要調整底部位置,讓高度與裁剪區域一致
showBottom = bitmap.getHeight()-cropTop;
}
至此,裁剪所需要的參數全部計算完畢,這樣就可以正確裁剪出預覽框中的內容。
邊界限制#####
為了提升用戶體驗,或者是實現需求,可能我們需要限制縮放&移動的邊界,讓裁剪預覽框的區域可以完全在圖片里面,換個意思就是說,裁剪最后的圖片一定是圖片上的某個區域,而不會出現只裁剪到一部分圖片,另一部分是空白的。
邊界的限制只是針對移動與縮放。下面我們分別看看怎么對兩者做邊界限制
移動邊界限制
同樣由於涉及到了縮放,移動的邊界限制需要特別處理。具體的思路是獲取當前移動的距離與當前圖片在屏幕上實際的四個位置點(左右上下),例如我們需要判斷是否會超過左邊界,那么我們會判斷橫坐標移動的距離+圖片當前左邊位置是否大於限制框的左邊橫坐標,是的話,那么則視為出界,應當重新計算移動距離。其他三個位置亦是如此,我們還是看下下面的代碼片。
public void drag(float motionX, float motionY) {
//移動距離
float moveX = motionX -mLastX;
float moveY = motionY - mLastY;
//mRestrictRect為限制框,這個框實質就是預覽框在屏幕上的坐標位置
if(mRestrictRect!=null){
//經過縮放與移動后,圖片在屏幕上實際的位置
RectF rectF = getCurrentRectF();
//下面為四邊邊界的判斷與重計算
if(moveX>0){
if(rectF.left+moveX>mRestrictRect.left){
moveX = mRestrictRect.left-rectF.left;
}
}else {
if(rectF.right+moveX<mRestrictRect.right){
moveX = mRestrictRect.right- rectF.right;
}
}
if(moveY>0){
if(rectF.top+moveY>mRestrictRect.top){
moveY = mRestrictRect.top-rectF.top;
}
}else {
if(rectF.bottom+moveY<mRestrictRect.bottom){
moveY = mRestrictRect.bottom- rectF.bottom;
}
}
}
mView.getImageMatrix().postTranslate(moveX,moveY);
mView.invalidate();
}
縮放邊界限制
縮放邊界的限制會相對復雜。因為當縮放出界時,需要根據多種情況重新計算縮放所需要的參數。
縮放邊界限制的流程是:
1、按照縮放值,獲取圖片將會在屏幕出現的位置
2、判斷四個位置是否會超出邊界,並記錄四個位置邊界判斷結果。如果縮放后的某個位置會超出限制框的邊界位置,那么則限定該坐標為縮放中心點,保證該點位置不移動。例如左邊將會出界,那么則以限制框的左邊位置作為最終縮放中心點橫坐標。
3、如果左右或者上下出界,則不用進行縮放,因為無法縮放了。如果不是這種情況,則進入4
4、根據Martrix縮放的計算公式推導出,什么縮放倍數下,會達到限制框的邊界值。這里將會取到四個邊界縮放值與當前的縮放值進行比對,取其中最大的縮放值作為最后的縮放值(因為縮小情況才會導致越界)
我們看看具體的代碼片
public boolean onScale(ScaleGestureDetector detector) {
//初始化縮放值
float px = detector.getFocusX();
float py = detector.getFocusY();
float scaleFactor= detector.getScaleFactor();
if(mRestrictRect!=null){
Matrix matrixAfter = new Matrix(mView.getImageMatrix());
matrixAfter.postScale(detector.getScaleFactor()
,detector.getScaleFactor(),detector.ge tFocusX(),detector.getFocusY());
final BitmapDrawable drawable = (BitmapDrawable) mView.getDrawable();
final Bitmap bitmap = drawable.getBitmap();
RectF rectF = new RectF(0,0,bitmap.getWidth(),bitmap.getHeight());
//上面的大段代碼都是為了這里,這里將會獲取按照當前縮放值縮放后的圖片實際的坐標位置
matrixAfter.mapRect(rectF);
boolean isLeftLimit = false ,isRightLimit = false ,
isTopLimit = false ,isBottomLimit =false;
//判斷縮放后的位置是否會超過邊界
if(rectF.left>mRestrictRect.left){
//超過邊界則將點最為最后的縮放中心點,讓該邊界的點固定下來,不被改變
px = mRestrictRect.left;
isLeftLimit = true;
}
if(rectF.right<mRestrictRect.right){
px = mRestrictRect.right;
isRightLimit = true;
}
if(rectF.top>mRestrictRect.top){
py = mRestrictRect.top;
isTopLimit = true;
}
if(rectF.bottom<mRestrictRect.bottom){
py = mRestrictRect.bottom;
isBottomLimit = true;
}
//左右兩邊或者上下兩邊都無法縮放,就不縮放了
if((isRightLimit&&isLeftLimit)||(isTopLimit&&isBottomLimit)){
return true;
}
//重新計算允許的最小縮放倍數,根據四條邊界的縮放倍數與當前的縮放倍數,
//獲取最大縮放倍數,因為主要是縮小才會導致超越邊界
//計算公式是: 結果坐標(ResultX) = 縮放前坐標(Before X)*縮放倍數(scale)
//+中心點坐標(center X)*(1-縮放倍數(scale))
float maxScaleLeft = (mRestrictRect.left-px)/(getCurrentRectF().left-px);
if(scaleFactor<maxScaleLeft){
scaleFactor = maxScaleLeft;
}
float maxScaleRight = (mRestrictRect.right-px)/(getCurrentRectF().right-px);
if(scaleFactor<maxScaleRight){
scaleFactor = maxScaleRight;
}
float maxScaleTop = (mRestrictRect.top-py)/(getCurrentRectF().top-py);
if(scaleFactor<maxScaleTop){
scaleFactor = maxScaleTop;
}
float maxSacleBottom = (mRestrictRect.bottom-py)/(getCurrentRectF().bottom-py);
if(scaleFactor<maxSacleBottom){
scaleFactor = maxSacleBottom;
}
}
//保存當前的縮放值
mScale=mScale*scaleFactor;
//執行縮放
mView.getImageMatrix().postScale(scaleFactor,scaleFactor,
px,py);
mView.invalidate();
return true;
}
GitHub地址#####
最后附錄上EnjoyCrop源碼EnjoyCrop,希望這篇文檔對你有幫助,謝謝!