從零開始制作圖片裁減器

這篇文章講的是制作一個比較簡單的自定義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還是足夠了碌廓。

文章中都是抽離出了部分代碼,可能閱讀起來不太容易理解剩盒,完整的源代碼見此,加上注釋理解起來應該好很多谷婆。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纪挎,更是在濱河造成了極大的恐慌期贫,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件异袄,死亡現(xiàn)場離奇詭異通砍,居然都是意外死亡,警方通過查閱死者的電腦和手機烤蜕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門封孙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讽营,你說我怎么就攤上這事虎忌。” “怎么了橱鹏?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵膜蠢,是天一觀的道長。 經(jīng)常有香客問我莉兰,道長挑围,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任贮勃,我火速辦了婚禮贪惹,結果婚禮上苏章,老公的妹妹穿的比我還像新娘寂嘉。我一直安慰自己,他們只是感情好枫绅,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布泉孩。 她就那樣靜靜地躺著,像睡著了一般并淋。 火紅的嫁衣襯著肌膚如雪寓搬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天县耽,我揣著相機與錄音句喷,去河邊找鬼。 笑死兔毙,一個胖子當著我的面吹牛唾琼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播澎剥,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锡溯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祭饭,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤芜茵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后倡蝙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體九串,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年寺鸥,在試婚紗的時候發(fā)現(xiàn)自己被綠了蒸辆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡析既,死狀恐怖躬贡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情眼坏,我是刑警寧澤拂玻,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站宰译,受9級特大地震影響檐蚜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沿侈,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一闯第、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缀拭,春花似錦咳短、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至褐荷,卻和暖如春勾效,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叛甫。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工层宫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人其监。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓萌腿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棠赛。 傳聞我的和親對象是個殘疾皇子哮奇,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容