如何在Bitmap截取任意形状

lwcs3300 7年前
   <p>现在许多截屏应用中都实现了任意形状截图,我一开始有些疑惑:到底是如何判断一个像素点是在曲线内部还是外部的呢,因为多边形是否包含点的判断还是比较复杂的,计算起来复杂度可不低,后来看了一些资料,发现完全不是我想的那么复杂,很简单就能实现。多简单呢,往下看。</p>    <p>先看最终效果:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2f5323b4c14a8eb143e6afbf26b50b30.gif"></p>    <p>曲线截图效果</p>    <p>以全屏截屏并裁剪出任意形状的图形为例,除了在 Android上如何实现矩形区域截屏 中截屏的操作以外,还需要额外实现两个部分:</p>    <ol>     <li>根据用户的操作,绘制出选择的曲线图形;</li>     <li>根据这个图形截取图片。</li>    </ol>    <p>第一步、根据用户的操作,绘制出选择的曲线图形</p>    <p>首先设计一个用于保存用户绘制图形的数据结构,如下:</p>    <pre>  <code class="language-java">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();      }    }</code></pre>    <p>这里实现了Parcelable 接口,因为本来要考虑到通过Intent传递数据,后来发现没有这个必要了,但也没有改回来了,请不要在意。</p>    <p>在onTouchEvent中记录用户手指的拖动轨迹,并在onDraw中绘制出来,代码如下:</p>    <pre>  <code class="language-java">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);              }          }      }  }</code></pre>    <p>其中值得注意的是markPaint这个画笔,其设置如下,它的功能是在半透明的背景上,把选中的区域的背景色去除掉(设置成PorterDuff.Mode.CLEAR):</p>    <pre>  <code class="language-java">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);</code></pre>    <p>还要注意在onDraw中,使用isUp来标识是拖动过程中还是拖动完成,这两部分的绘制方式有点区别:拖动过程中绘制的是手指划动的曲线,所以使用drawLine就行了;而拖动完成以后,需要根据划动的路径绘制成封闭图形,所以使用Path进行绘制。</p>    <p>第二步、根据曲线图形截取图片</p>    <p>就像本文开头就说到的,如果要计算一个曲线图形内包含的每个像素,再去bitmap中去拿对应的像素,计算量就会比较大了,</p>    <p>好在系统已经给我们提供了更简单的方法,原理是:</p>    <ol>     <li>创建一张空的bitmap</li>     <li>在这张bitmap中进行绘制出曲线图形</li>     <li>以PorterDuff.Mode.SRC_IN的方式,再在这个bitmap上把需要截取的图片绘制一次,这时候这张bitmap就是你需要的结果。关于PorterDuff.Mode.SRC_IN的含义,可以看这篇 PorterDuff.Mode</li>    </ol>    <p>代码实现如下:</p>    <pre>  <code class="language-java">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);  }</code></pre>    <p>其中bitmap对象,是全屏截屏的结果。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/d64cf9f69d05</p>    <p> </p>