android 细数实现圆形图片的方法

360 阅读2分钟

一、前言

CircleImageView 实现方法有很多种,各有优缺点,因此需要按照不同的场景使用。我们今天使用修改图片像素的方法实现 CircleImageView,主要知识点无非是勾股定理和点到圆形的距离初中数学知识。

旋转效果图

其他效果

二、现有方法

2.1、clipPath 裁剪画布

该方法Android 4.3 (API Level 18)版本之后开始支持硬件加速,但仍然不支持抗锯齿。

int saveCount = canvas.save();
// 平移到View中间,注意这里是矩阵操作,实际上并不是平移画布,在2d坐标系中可以认为是平移坐标系
canvas.translate(width/2f,height/2f); path.reset();
float radius = Math.min(mBitmap.getWidth(), mBitmap.getHeight()) / 3f;
path.addCircle(0, 0, radius, Path.Direction.CCW);
//计算Path外围矩形区域Rect
path.computeBounds(mainRect,false);
canvas.clipPath(path);
canvas.drawBitmap(mBitmap, null, mainRect, mCommonPaint);
canvas.restoreToCount(saveCount);

2.2、使用 PorterDuffXfermode

PorterDuffXfermode 是 Android 主流的图片合成工具,支持模式多,稳定性强,效果好,质量高,支持抗锯齿备受广大开发者喜爱,可以说是很多应用开发的首选,缺点是需要开启LayerType 缓冲,gpu缓冲和软件缓冲都可以。

使用PorterDuffXfermode可以使用下面方式动态设置

  • view.isHardwareAccelerated() 为true硬件加速时,可以判断开始gpu缓冲 (LAYER_TYPE_HARDWAR),否则开启softwrare缓冲 (LAYER_TYPE_SOFTWARE)。 

  • canvas.isHardwareAccelerated() 是最准确的判断方式,但是其使用也有两个要点: 为true硬件加速时,google建议不要使用canvas.saveLayer,而是使用view.setLayerType(LAYER_TYPE_HARDWARE,null),因为canvas.saveLayer作为离屏渲染,在硬件绘制时,开启LAYER_TYPE_HARDWAR因为存在gpu缓冲,所以其实是没必要的在做saveLayer处理。但是,目前仍然有些方法需要saveLayer,如maskFilter、LearText

另一方面,如果绘制流程是创建一个Bitmap,先往Bitmap上绘制,在往Canvas上绘制,实际上这个时候本质上是软件绘制,不需要任何setLayerType。

注意:补充一下,图上说的setShadowLayer是除文本之外,并不是排除文本,而是只有文本,应该是翻译问题

下面是xformode方式的实现

 /**
     * 绘制圆形图片
     *
     */
    @Override  
    protected void onDraw(Canvas canvas) {  
  
      int saveCount = canvas.save();
      canvas.translate(width/2f,height/2f);
      Bitmap output = getCircleBitmap(mBitmap,mCommonPaint);
      final RectF rectSrc = new RectF(-output.getWidth()/2f, -output.getHeight()/2f, output.getWidth()/2f, output.getHeight()/2f);
      canvas.drawBitmap(output, null, rectSrc, mCommonPaint);//        
      canvas.restoreToCount(saveCount);canvas.restoreToCount(saveCount);
    }  
  
      /**
     * 获取圆形图片方法
     * @param bitmap
     * @return Bitmap
     */
    private Bitmap getCircleBitmap(Bitmap bitmap,Paint paint) {
        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
                bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);

        final int color = 0xff424242;  //非透明view都行

        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        paint.setAntiAlias(true);
        paint.setColor(color);
        float radius = Math.min(output.getWidth(), output.getHeight()) / 3f;
        canvas.drawCircle(output.getWidth()/2f,  output.getHeight()/2f, radius, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        paint.setXfermode(null);
        return output;
    }

2.3、设置画笔 Paint 的 Shader

该方法是 Glide 和 picasso 使用的方法,用法简单便捷,内存占有率和性能都不错,因为Shader具备性能加速效果。

    @Override  
    protected void onDraw(Canvas canvas) {  
  
        Drawable drawable = getDrawable();  
        if (null != drawable) {  
            Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();  
            Bitmap b = transform(bitmap);  
            final Rect rectSrc = new Rect(0, 0, b.getWidth(), b.getHeight());  
            final Rect rectDest = new Rect(0,0,getWidth(),getHeight());
            paint.reset();  
            canvas.drawBitmap(b, rectSrc, rectDest, paint);  
  
        } else {  
            super.onDraw(canvas);  
        }  
    } 

public Bitmap transform(Bitmap source) {
            int size = Math.min(source.getWidth(), source.getHeight());

            int x = (source.getWidth() - size) / 2;
            int y = (source.getHeight() - size) / 2;

            Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
            if (squaredBitmap != source) {
                source.recycle();
            }

            Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());

            Canvas canvas = new Canvas(bitmap);
            Paint paint = new Paint();
            BitmapShader shader = new BitmapShader(squaredBitmap,
                    BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);

          float mScale = (mRadius * 2.0f) / Math.min(bitmap.getHeight(), bitmap.getWidth());

          Matrix matrix = new Matrix();
          matrix.setScale(mScale, mScale);
          bitmapShader.setLocalMatrix(matrix);


            paint.setShader(shader);
            paint.setAntiAlias(true);

            float r = size / 2f;
            canvas.drawCircle(r, r, r, paint);

            squaredBitmap.recycle();
            return bitmap;
   }

2.4、修改像素

该方法无法支持抗锯齿,并且不支持 Bitmap.Config.HARDWARE 格式的 bitmap,主要逻辑是计算圆形,然后通过圆运动学原理,提取像素到新的Bitmap,有很多缺点

  • 性能较差
  • 存在锯齿

下面是完整的绘制逻辑

    @Override
    public void onDraw(Canvas canvas) {
        int width = getWidth();
        int height = getHeight();


        int minSize = Math.min(width, height) / 2;
        Drawable drawable = getDrawable();
        if (drawable != null && minSize != 0) {

            if (Math.min(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()) == 0) {
                return;
            }

            int intrinsicWidth = drawable.getIntrinsicWidth();
            int intrinsicHeight = drawable.getIntrinsicHeight();

            float R = Math.min(intrinsicWidth, intrinsicHeight) / 2;

            Bitmap bmp = transformBitmap(drawable, intrinsicWidth, intrinsicHeight, R);

            Matrix imageMatrix = getImageMatrix();

            if ((imageMatrix == null || imageMatrix.isIdentity()) && getPaddingTop() == 0 && getPaddingLeft() == 0) {
                drawCircleImage(canvas, bmp);

            } else {
                if (imageMatrix != null && !imageMatrix.isIdentity()) {
                    canvas.concat(imageMatrix);
                }
                final int saveCount = canvas.getSaveCount();
                canvas.save();

                if (getCropToPadding()) {
                    final int scrollX = getScrollX();
                    final int scrollY = getScrollY();
                    canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(),
                            scrollX + getRight() - getLeft() - getPaddingRight(),
                            scrollY + getBottom() - getTop() - getPaddingBottom());
                }

                canvas.translate(getPaddingLeft(), getPaddingTop());
                drawCircleImage(canvas, bmp);
                canvas.restoreToCount(saveCount);

            }
            if (bmp != null && !bmp.isRecycled()) {
                bmp.recycle();
            }
        } else {
            super.onDraw(canvas);
        }
    }

    private void drawCircleImage(Canvas canvas, Bitmap bmp) {
        try {
            DrawFilter drawFilter = canvas.getDrawFilter();
            canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
            canvas.drawBitmap(bmp, 0, 0, null);
            canvas.setDrawFilter(drawFilter);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
    }

    @NonNull
    private Bitmap transformBitmap(Drawable drawable, int intrinsicWidth, int intrinsicHeight, float r) {
        Bitmap bmp = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888);
        Canvas targetCanvas = new Canvas(bmp);
        try {
            drawable.draw(targetCanvas);
            for (int y = 0; y < intrinsicHeight; y++) {
                for (int x = 0; x < intrinsicWidth; x++) {
                    if ((Math.pow(x - intrinsicWidth / 2, 2) + Math.pow(y - intrinsicHeight / 2, 2)) <= Math.pow(r, 2)) {
                        continue;
                    }
                    bmp.setPixel(x, y, Color.TRANSPARENT);
                }
            }
        } catch (Exception e) {
            NCFLog.e("transformBitmap", "e=" + e.getLocalizedMessage());
            e.printStackTrace();
        }
        return bmp;
    }

    public boolean isHardware(Bitmap sourceBitmap) {
        if (sourceBitmap == null) return false;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            return sourceBitmap.getConfig() == Bitmap.Config.HARDWARE;
        }
        return false;
    }

三、总结

以上圆形图片实现的一些方法,从各方比较,Shader优势明确,但是每种都有他自身的特点。修改像素是最差的。引申思考,在图片特效中,如果我们把图片分块绘制,类似圆周计算时不断切割圆的方法,理论上也能实现圆形图片,同样,类似百叶窗和瓦片效果也是可以的,后续我们有机会利用下分割图片实现一些特效。