如何在Bitmap截取任意形状

转载注明出处:简书-十个雨点

现在许多截屏应用中都实现了任意形状截图,我一开始有些疑惑:到底是如何判断一个像素点是在曲线内部还是外部的呢,因为多边形是否包含点的判断还是比较复杂的,计算起来复杂度可不低,后来看了一些资料,发现完全不是我想的那么复杂,很简单就能实现。多简单呢,往下看。

先看最终效果:
曲线截图效果

也可以下载全能分词体验

以全屏截屏并裁剪出任意形状的图形为例,除了在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对象,是全屏截屏的结果,可以参考Android上如何实现矩形区域截屏

完整代码可以参考Bigbang项目的MarkSizeView和ScreenCapture中的startCapture方法。
相关文章:

Android上如何实现矩形区域截屏
Android如何判断NavigationBar是否显示(获取屏幕真实的高度)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,464评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,357评论 0 17
  • 有理想没信仰。我们从来就不缺伟大的理想,但是却没有信仰,缺乏对“道”(因果报应等天道、地道、人道)的敬畏,在追求“...
    Lady_艾米阅读 359评论 0 0
  • 二、人的本性 人有两种本性:一种是精神的或较高尚的本性,另一种是物质的或较低下的本性。前者使他接近上帝,后者使他只...
    新园读书会阅读 336评论 0 1
  • 文/听昕。 从此天南地北,你是她人夫,我是陌路人。 我已经很久很久都没有想起你了,久到在想起你的那一...
    卿昕Y阅读 722评论 2 1