如何在Bitmap截取任意形状

来源:互联网 发布:ios6.1.3可用软件 编辑:程序博客网 时间:2024/04/30 13:10

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

先看最终效果:

曲线截图效果

以全屏截屏并裁剪出任意形状的图形为例,除了在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是否显示(获取屏幕真实的高度)

0 0
原创粉丝点击