這篇文章講的是制作一個比較簡單的自定義View---圖片裁減器,為了降低難度眉厨,我只實現(xiàn)了矩形的裁減。
先看看效果圖:
在開始碼代碼之前先想一下我們的裁減器TailorView都需要實現(xiàn)哪些功能:
1.打開系統(tǒng)相冊兽狭,從中選中一張圖片交給我們的Activity.
2.將圖片加載到一個ImageView上.
3.顯示一個矩形裁減框憾股,并且可以放大、縮小椭符、拖動.
4.將圖片中在裁減框范圍之外的部分加上一個黑色濾鏡.
5.獲取并生成裁減器框內(nèi)部分的圖片并生成Bitmap.
好了荔燎,然后我們就開始碼代碼嘍~
首先是打開相冊并選擇一張圖片,這件事我們交個一個Activity:MainActivity销钝,這也是我們的入口Activity有咨。注意由于我們需要取得位于SD卡上的圖片文件,故需要再AndroidManifest文件中聲明如下權限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
同時由于我的測試機型是Android6.0.1的蒸健,故還需要再運行時動態(tài)監(jiān)測權限并申請權限座享。方便起見,我們打開應用的第一時間就申請權限:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}
,READ_EXTERNAL_STORAGE_REQUEST_CODE);
}
這就是在運行時動態(tài)申請權限的代碼似忧,這里我只申請了READ_EXTERNAL_STROAGE,其他權限同樣處理方法渣叛。
那么我們怎樣才知道用戶是否授予了權限呢?很簡單盯捌,只需要重寫Activity的onRequestPermissionsResult方法即可淳衙,然后在里面判斷用戶是否給予了權限:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode==READ_EXTERNAL_STORAGE_REQUEST_CODE&&grantResults.length>0){
//do something
}
}
注意這里的READ_EXTERNAL_STORAGE_REQUEST_CODE和申請權限時的是同一個,都是我們自定義的常量。
好了箫攀,權限的問題我們已經(jīng)搞定了肠牲,接下來就是調(diào)用系統(tǒng)相冊了,很簡單靴跛,只需一個Intent即可:
Intent intent = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, IMAGE_CODE);
然后就會自動打開系統(tǒng)相冊缀雳,并且當你選擇了一張圖片的時候,就會返回梢睛。
接下來就是獲取這張我們選擇的圖片,這里需要重寫onActivityResult方法肥印,因為我們調(diào)用系統(tǒng)相冊時利用的是startActivityForResult(),故當Activity返回時會調(diào)用onActivityResult方法,連同返回的數(shù)據(jù)绝葡。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==IMAGE_CODE&&resultCode==RESULT_OK&&data!=null){
Uri selectedImage=data.getData();
String[] filePathColumn={MediaStore.Images.Media.DATA};
Cursor cursor=getContentResolver().query(selectedImage,
filePathColumn,null,null,null);
if(cursor!=null){
cursor.moveToFirst();
String picturePath=
cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
cursor.close();
Intent intent=new Intent(this,TailorActivity.class);
intent.putExtra("picturePath",picturePath);
startActivityForResult(intent,IMAGE_TAILORED);
}
}
}
同樣深碱,這里的IMAGE_CODE就是startActivityForResylt中的IMAGE_CODE.這樣我們就獲取了圖片的絕對路徑,接下來就是加載這個圖片到ImageView中了挤牛。這里我新建了一個Activity:TailorActivity,并在其布局里放了一個占據(jù)全屏的ImageView莹痢,我們將圖片路徑一并傳給Activity:TailorActivity即可种蘸。
Intent intent=new Intent(this,TailorActivity.class);
intent.putExtra("picturePath",picturePath);
startActivityForResult(intent,IMAGE_TAILORED);
在TailorActivity先從Intent中取出路徑墓赴,并加載圖片:
mBitmap= BitmapFactory.decodeFile(imagePath);
mImageView.setImageBitmap(mBitmap);
接下來就是今天的主角:自定義View----TailorView
我先說一下這個View的結構:TailorView內(nèi)部維護四個Point類,每個Point很簡單只包含一個坐標x和y,這四個Point對應著我們要在屏幕上繪制的裁減框的四個角航瞭。我們每次draw的時候先根據(jù)這四個Point的位置畫出四條線诫硕,就是裁減框的邊界,然后繪制四個小的實心圓刊侯,當用戶拖拽任意個=一個實心圓時章办,我們的裁剪框做出相應的大小改變。當用戶點擊裁剪框中間的部分時滨彻,裁剪框跟隨者用戶手指左右平移藕届。
其中裁剪框放大、縮小亭饵、平移的邏輯都是在View的onTouchEvent中實現(xiàn)的休偶。
先看看我們的onDraw:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//drawLines
mPaint.setStyle(Paint.Style.STROKE);
for (int i=0;i<4;i++){
canvas.drawLine(points[i].getX(),points[i].getY()
,points[(i+1)%4].getX(),points[(i+1)%4].getY(),mPaint);
}
mPaint.setStyle(Paint.Style.FILL);
//draw points
for (int i=0;i<4;i++){
canvas.drawCircle(points[i].getX(),points[i].getY(),mRadius,mPaint);
}
}
這樣很輕松的就繪制除了四個邊和四個實心角。
由于我們的TailorView有一個默認初始大小辜羊,因此在xml布局文件中使用的時候都不需要指定寬高了踏兜,我們也不用重寫onMeasure()方法了,算是偷了個懶八秃。
我們要對我們的TailorView做出一些限制:
1.四個點的順序不能亂碱妆,因為后面我們還要靠這四個點的順序來構造一個Rect,這個Rect就是不會被黑色濾鏡覆蓋的范圍昔驱。
2.我們的TailorView所能移動的地方存在一個邊界疹尾,就是ImageView中顯示的圖片的實際邊界。
四個點的順序不能亂,那就在onTouchEvent中檢測四個點的相對坐標即可:
//check if points gets collide
private boolean pointsCross(int x, int y, int selectedIndex) {
if ((selectedIndex==0&&points[1].x-x<=mTouchSlop)
||(selectedIndex==0&&points[3].y-y<=mTouchSlop)) return true;
if ((selectedIndex==1&&x-points[0].x<=mTouchSlop)
||(selectedIndex==1&&points[2].y-y<=mTouchSlop)) return true;
if ((selectedIndex==2&&y-points[1].y<=mTouchSlop)
||(selectedIndex==2&&x-points[3].x<=mTouchSlop)) return true;
if ((selectedIndex==3&&points[2].x-x<=mTouchSlop)
||(selectedIndex==3&&y-points[0].y<=mTouchSlop)) return true;
return false;
}
可以看出纳本,我的做法是當相鄰兩個點的某一方向上的距離小于mTouchSlop時睡雇,即視為沖突,就無法再任由用戶移動了饮醇。
然后是我自定義了一個Bounds類它抱,也很簡單,就是包含x1,x2,y1,y1朴艰,即為邊界的左上和右下點的坐標,在初始化的時候我們利用ImageView中Bitmp的實際邊界观蓄,給我們的mBound進行賦值。然后同樣在onTouchEvent中進行移動的時候祠墅,檢測是否和邊界沖突侮穿,倘若沖突,就停止移動:
private boolean checkOutBounds(int x, int y) {
return mBounds.x1<=x&&mBounds.x2>=x&&mBounds.y1<=y&&mBounds.y2>=y;
}
然后就是判斷點擊的點是在裁剪框內(nèi)部還是在四個邊角上:
private boolean inCenter(int x,int y){
for(int i=0;i<4;i++){
if (inCircle(x,y,points[i])) return false;
}
return mRect.contains(x,y);
}
private boolean inCircle(int x,int y,Point p){
return Math.hypot(x-p.getX(),y-p.getY())<=checkLength;
}
當用戶點擊的是邊框內(nèi)部的時候毁嗦,我們就隨用戶的移動移動整個裁剪框:
//move the whole rect
private void moveWhole(int delX,int delY){
for (int i=0;i<4;i++){
if (!checkOutBounds(points[i].x+delX,points[i].y+delY)) return;
}
for (int i=0;i<4;i++){
points[i].x+=delX;
points[i].y+=delY;
}
mRect=new Rect(points[0].getX(),points[0].getY(),points[2].getX(),points[2].getY());
mListener.onRectPositionChange();
}
當用戶點擊的是某個邊角時亲茅,相應的需要改變其相鄰的兩個Point的x或y:
if (shouldMove){
p.setX(x).setY(y);
//change the relative two circle's position
if (selectedIndex%2==0){
points[(selectedIndex+1)%4].setY(y);
points[(selectedIndex+3)%4].setX(x);
}else{
points[(selectedIndex+1)%4].setX(x);
points[(selectedIndex+3)%4].setY(y);
}
if (mListener!=null){
mListener.onRectPositionChange();
}
mRect=new Rect(points[0].getX(),points[0].getY(),points[2].getX(),points[2].getY());
}
這樣,裁剪框的邏輯基本就實現(xiàn)了狗准,完整的代碼我會放在后面克锣。
然后是我們的另一個自定義View---FilterView,其功能就是給出了裁剪框以外的部分加上一個黑色濾鏡腔长,這個就很好實現(xiàn)了袭祟,我們只需要讓FilterView充滿屏幕,然后繪制一個半透明黑色背景色即可捞附。
直接看onDraw方法吧:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//draw filter color except the rect consists the four points
mPaint.setAlpha(alpha);
canvas.drawRect(0,0,mWidth,mRect.top,mPaint);
canvas.drawRect(0,mRect.bottom,mWidth,mHeight,mPaint);
canvas.drawRect(0,mRect.top,mRect.left,mRect.bottom,mPaint);
canvas.drawRect(mRect.right,mRect.top,mWidth,mRect.bottom,mPaint);
}
這里的alpha我的取值是0xAA巾乳。
這里的mRect就是從TailorView那里取得的Rect,注意我們的TailorView的Rect在不停的改變鸟召,因此我們需要定義一個接口Listener對其實現(xiàn)監(jiān)聽:
public interface RectPositionChangeListener{
void onRectPositionChange();
}
然后我們給TailorView設置一個Listener胆绊,在其內(nèi)部每當Rect變化時就調(diào)用onRectPOsitionChange()方法即可。
最后就是我們截取圖片的功能了欧募,思路是先利用View的getDrawingCache方法獲取屏幕截圖压状,然后再利用Rect的坐標從截圖中生成一張新的圖片:
//return a screen capture bitmap unscaled
public static Bitmap getScreenCapture(Activity activity){
View view=activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bitmap=view.getDrawingCache();
int []wh=getScreenWidthAndHeight(activity);
Bitmap scaled=Bitmap.createBitmap(bitmap,0,0,wh[0],wh[1]);
view.setDrawingCacheEnabled(false);
view.destroyDrawingCache();
return scaled;
}
//this screen bitmap contain the statusBar, remember to minus its height.
Bitmap screen=ScreenUtils.getScreenCapture(this);
Rect rect=mTailorView.getRect();
Bitmap bitmap=Bitmap.createBitmap(screen,rect.left,rect.top+mTailorView.statusBarHeight
,rect.right-rect.left,rect.bottom-rect.top);
注意這個截圖是包含狀態(tài)欄的,故需要對坐標進行一下處理槽片。
至此何缓,我們的自定義圖片裁減器就基本完成了,當然有很多缺點和bug还栓,但作為一個自定義View學習Demo還是足夠了碌廓。
文章中都是抽離出了部分代碼,可能閱讀起來不太容易理解剩盒,完整的源代碼見此,加上注釋理解起來應該好很多谷婆。