如何在Bitmap截取任意形狀

lwcs3300 7年前發布 | 11K 次閱讀 Bitmap 安卓開發 Android開發 移動開發

現在許多截屏應用中都實現了任意形狀截圖,我一開始有些疑惑:到底是如何判斷一個像素點是在曲線內部還是外部的呢,因為多邊形是否包含點的判斷還是比較復雜的,計算起來復雜度可不低,后來看了一些資料,發現完全不是我想的那么復雜,很簡單就能實現。多簡單呢,往下看。

先看最終效果:

曲線截圖效果

以全屏截屏并裁剪出任意形狀的圖形為例,除了在 Android上如何實現矩形區域截屏 中截屏的操作以外,還需要額外實現兩個部分:

  1. 根據用戶的操作,繪制出選擇的曲線圖形;
  2. 根據這個圖形截取圖片。

第一步、根據用戶的操作,繪制出選擇的曲線圖形

首先設計一個用于保存用戶繪制圖形的數據結構,如下:

public static class GraphicPath implements Parcelable {
    protected GraphicPath(Parcel in) {
        int size=in.readInt();
        int[] x=new int[size];
        int[] y=new int[size];
        in.readIntArray(x);
        in.readIntArray(y);
        pathX=new ArrayList<>();
        pathY=new ArrayList<>();

        for (int i=0;i<x.length;i++){
            pathX.add(x[i]);
        }

        for (int i=0;i<y.length;i++){
            pathY.add(y[i]);
        }
    }

    public static final Creator<GraphicPath> CREATOR = new Creator<GraphicPath>() {
        @Override
        public GraphicPath createFromParcel(Parcel in) {
            return new GraphicPath(in);
        }

        @Override
        public GraphicPath[] newArray(int size) {
            return new GraphicPath[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(pathX.size());
        dest.writeIntArray(getXArray());
        dest.writeIntArray(getYArray());
    }

    public List<Integer> pathX;
    public List<Integer> pathY;

    public GraphicPath(){
        pathX=new ArrayList<>();
        pathY=new ArrayList<>();
    }

    private int[] getXArray(){
        int[] x=new int[pathX.size()];
        for (int i=0;i<x.length;i++){
            x[i]=pathX.get(i);
        }
        return x;
    }

    private int[] getYArray(){
        int[] y=new int[pathY.size()];
        for (int i=0;i<y.length;i++){
            y[i]=pathY.get(i);
        }
        return y;
    }

    public void addPath(int x,int y){
        pathX.add(x);
        pathY.add(y);
    }

    public void clear(){
        pathX.clear();
        pathY.clear();
    }

    public int getTop(){
        int min=pathY.size()>0?pathY.get(0):0;
        for (int y:pathY){
            if (y<min){
                min=y;
            }
        }
        return min;
    }

    public int getLeft(){
        int min=pathX.size()>0?pathX.get(0):0;
        for (int x:pathX){
            if (x<min){
                min=x;
            }
        }
        return min;
    }

    public int getBottom(){
        int max=pathY.size()>0?pathY.get(0):0;
        for (int y:pathY){
            if (y>max){
                max=y;
            }
        }
        return max;
    }
    public int getRight(){
        int max=pathX.size()>0?pathX.get(0):0;
        for (int x:pathX){
            if (x>max){
                max=x;
            }
        }
        return max;
    }
    public int size(){
        return pathY.size();
    }

}

這里實現了Parcelable 接口,因為本來要考慮到通過Intent傳遞數據,后來發現沒有這個必要了,但也沒有改回來了,請不要在意。

在onTouchEvent中記錄用戶手指的拖動軌跡,并在onDraw中繪制出來,代碼如下:

public boolean onTouchEvent(MotionEvent event) {
    if (!isEnabled()){
        return false;
    }
    int x= (int) event.getX();
    int y= (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isUp = false;
            downX = x;
            downY = y;
            isMoveMode = false;
            startX = (int) event.getX();
            startY = (int) event.getY();
            endX = startX;
            endY = startY;
            mGraphicPath.clear();
            mGraphicPath.addPath(x,y);
            break;
        case MotionEvent.ACTION_MOVE:
            if (isButtonClicked) {
                break;
            }
            mGraphicPath.addPath(x,y);
            break;
        case MotionEvent.ACTION_UP:            
            isUp = true;
            mGraphicPath.addPath(x,y);
            break;
            case MotionEvent.ACTION_CANCEL:
            isUp = true;
            break;
    }
    postInvalidate();
    return true;
}

protected void onDraw(Canvas canvas) {
    int width = getWidth();
    int height=getHeight();
    //draw unmarked
    canvas.drawRect(0,0,width,height,unMarkPaint);
    if (isUp) {                
        Path path = new Path();
        if (mGraphicPath.size() > 1) {
            path.moveTo(mGraphicPath.pathX.get(0), mGraphicPath.pathY.get(0));
            for (int i = 1; i < mGraphicPath.size(); i++) {
                path.lineTo(mGraphicPath.pathX.get(i), mGraphicPath.pathY.get(i));
            }
        } else {
            return;
        }
        canvas.drawPath(path, markPaint);            
    }else {
        if (mGraphicPath.size() > 1) {
            for (int i = 1; i < mGraphicPath.size(); i++) {
                canvas.drawLine(mGraphicPath.pathX.get(i-1), mGraphicPath.pathY.get(i-1),mGraphicPath.pathX.get(i), mGraphicPath.pathY.get(i),markPaint);
            }
        }
    }
}

其中值得注意的是markPaint這個畫筆,其設置如下,它的功能是在半透明的背景上,把選中的區域的背景色去除掉(設置成PorterDuff.Mode.CLEAR):

markPaint=new Paint();
markPaint.setColor(markedColor);
markPaint.setStyle(Paint.Style.FILL_AND_STROKE);
markPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
markPaint.setColor(markedColor);
markPaint.setStrokeWidth(strokeWidth);
markPaint.setAntiAlias(true);

還要注意在onDraw中,使用isUp來標識是拖動過程中還是拖動完成,這兩部分的繪制方式有點區別:拖動過程中繪制的是手指劃動的曲線,所以使用drawLine就行了;而拖動完成以后,需要根據劃動的路徑繪制成封閉圖形,所以使用Path進行繪制。

第二步、根據曲線圖形截取圖片

就像本文開頭就說到的,如果要計算一個曲線圖形內包含的每個像素,再去bitmap中去拿對應的像素,計算量就會比較大了,

好在系統已經給我們提供了更簡單的方法,原理是:

  1. 創建一張空的bitmap
  2. 在這張bitmap中進行繪制出曲線圖形
  3. 以PorterDuff.Mode.SRC_IN的方式,再在這個bitmap上把需要截取的圖片繪制一次,這時候這張bitmap就是你需要的結果。關于PorterDuff.Mode.SRC_IN的含義,可以看這篇 PorterDuff.Mode

代碼實現如下:

mRect=new Rect(mGraphicPath.getLeft(),mGraphicPath.getTop(),mGraphicPath.getRight(),mGraphicPath.getBottom());
if (mRect.left < 0)
    mRect.left = 0;
if (mRect.right < 0)
    mRect.right = 0;
if (mRect.top < 0)
    mRect.top = 0;
if (mRect.bottom < 0)
    mRect.bottom = 0;
int cut_width = Math.abs(mRect.left - mRect.right);
int cut_height = Math.abs(mRect.top - mRect.bottom);
if (cut_width > 0 && cut_height > 0) {
    Bitmap cutBitmap = Bitmap.createBitmap(bitmap, mRect.left, mRect.top, cut_width, cut_height);
    LogUtil.d(TAG, "bitmap cuted second");
    //上面是將全屏截圖的結果先裁剪成需要的大小,下面是裁剪成曲線圖形區域
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStyle(Paint.Style.FILL_AND_STROKE);
    paint.setColor(Color.WHITE);
    Bitmap temp = Bitmap.createBitmap(cut_width, cut_height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(temp);

    Path path = new Path();
    if (mGraphicPath.size() > 1) {
        path.moveTo((float) ((mGraphicPath.pathX.get(0)-mRect.left)), (float) ((mGraphicPath.pathY.get(0)- mRect.top)));
        for (int i = 1; i < mGraphicPath.size(); i++) {
            path.lineTo((float) ((mGraphicPath.pathX.get(i)-mRect.left)), (float) ((mGraphicPath.pathY.get(i)- mRect.top)));
        }
    } else {
        return;
    }
    canvas.drawPath(path, paint);
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    // 關鍵代碼,關于Xfermode和SRC_IN請自行查閱
    canvas.drawBitmap(cutBitmap, 0 , 0, paint);
    LogUtil.d(TAG, "bitmap cuted third");

    saveCutBitmap(temp);
}

其中bitmap對象,是全屏截屏的結果。

 

來自:http://www.jianshu.com/p/d64cf9f69d05

 

 本文由用戶 lwcs3300 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!