这篇博客主要讲解了Android实现圆形图片的4种方式。

Android中并没有一个原生的控件,可以显示圆形或圆角图片,因此需要我们自己去定义这样一个控件。

实现圆形/圆角图片的核心思想,就是按照一定形状切割/绘制我们的原始控件,大概有以下4种方法:

  • 利用canvas.clipPath方法,按照自定义的Path图形去切割控件
  • 利用canvas.setBitmapShader,按照自定义的BitmapShader去重新绘制控件
  • 利用view.setOutlineProvider/setClipToOutline,按照自定义的Outline去切割控件
  • 利用Glide的Transformation变换,显示圆形图片

关于ImageView的几个知识点:

  • ImageView显示图片,底层是通过Canvas将我们的图片资源画到View控件上实现的;
    因此,要让其显示圆形图片,只需要对Canvas进行相应的变化,比如切割圆形、绘制圆形。
  • 编写自定义控件时,要继承AppCompatImageView,而不是ImageView,
    因为AppCompatImageView拥有ImageView没有的功能,比如Tinting

尊重原创,转载请注明出处 https://segmentfault.com/a/11...
本文出自 强哥大天才的博客

Path切割

思路

我们可以定义一个圆形Path路径,然后调用canvas.clipPath,将图片切割成圆形

缺陷

但是这种方法有2个限制:

  • cliptPath不支持硬件加速,因此在调用前必须禁用硬件加速,
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
  • 这种方式剪裁的是Canvas图形,View的实际形状是不变的,
    因此只能对src属性有效,对background属性是无效的。

1.定义Radius属性,用来设置圆角半径

注意事项:

  • 我们定义radius为dimension,这是一个带单位的值(float不带单位)
  • radius:值默认或者<0,表示圆形图;>0表示圆角图
<declare-styleable name="RoundImageView">
    <attr name="radius" format="dimension" />
</declare-styleable>

2.定义RoundImageView自定义圆形控件

注意事项

  • 设置圆形:path.addCircle
  • 设置圆角:path.addRoundRect
  • canvas.clipPath:不支持硬件加速,所以在使用前需要禁止硬件加速
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
  • clipPath要在super.onDraw方法前,调用,否则无效(canvas已经被设置给View了)
  • 在onSizeChanged方法中,获取宽高
public class RoundImageView extends AppCompatImageView {

    private RectF mRect;
    private Path mPath;
    private float mRadius;

    public RoundImageView(Context context) {
        this(context, null);
    }

    public RoundImageView(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public RoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView(context);
    }

    /**
     * 获取属性
     */
    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        mRadius = ta.getDimension(R.styleable.RoundImageView_radius, -1);
        ta.recycle();
    }

    /**
     * 初始化
     */
    private void initView(Context context) {
        mRect = new RectF();
        mPath = new Path();
        setLayerType(LAYER_TYPE_SOFTWARE, null);        // 禁用硬件加速
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mRadius < 0) {
            clipCircle(w, h);
        } else {
            clipRoundRect(w, h);
        }
    }

    /**
     * 圆角
     */
    private void clipRoundRect(int width, int height) {
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = width;
        mRect.bottom = height;
        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CW);
    }

    /**
     * 圆形
     */
    private void clipCircle(int width, int height) {
        int radius = Math.min(width, height)/2;
        mPath.addCircle(width/2, height/2, radius, Path.Direction.CW);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.clipPath(mPath);
        super.onDraw(canvas);
    }
}

BitmapShader绘制

思路

通过Canvas.drawCircle自己去绘制一个圆形图片,并设置给ImageView;

  • 通过drawable资源获取Bitmap资源
  • 根据Bitmap,创建一个BitmapShader着色器
  • 对BitmapShader做矩阵变化,调整着色器大小至合适的尺寸
  • 将作色器设置给画笔Paint
  • 调用canvas.drawCircle让canvas根据画笔,去绘制一个圆形图片

缺陷

这种方式有个限制,就是如果要定义一个圆角图片,必须调用canvas.drawRoundRect进行绘制,但是这个方法要求API>=21

这里,我们可以看到,ImageView底层显示图片的原理,就是利用Canvas将我们的图片资源给绘制到View控件上

1. 从图片资源中,获取Bitmap

Drawable转Bitmap的2种方式

  • 直接从BitmapDrawable中获取
  • 利用Canvas去创建一个Bitmap,然后调用drawable.draw(canvas),自己去绘制Bitmap

注意事项:

  • Drawable不能从构造方法中,获取,这个时候获取到的是null
  • Drawable分srcbackground
private void initBitmap() {
    Drawable drawable1 = getDrawable();
    Drawable drawable2 = getBackground();
    Drawable drawable = drawable1==null ? drawable2 : drawable1;          // 不能在构造方法中获取drawable,为null
    if (drawable instanceof BitmapDrawable) {
        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        mBitmap = bitmapDrawable.getBitmap();
    } else {
        int width = drawable.getIntrinsicWidth();       // 图片的原始宽度
        int height = drawable.getIntrinsicHeight();     // 图片的原始高度
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mBitmap);
//            drawable.setBounds(0,0,width,height);
        drawable.draw(canvas); 
    }
}

2. 根据Bitmap,创建着色器BitmapShader

BitmapShader着色器

  • TileMode瓷砖类型:当Canvas的宽高大于Bitmap的尺寸时,采取的重复策略

    • TileMode.MIRROR:图片镜像铺开
    • TileMode.REPEAT:图片重复铺开
    • TileMode.CLAMP:复用最后一个像素点
  • setLocalMatrix:对着色器中的Bitmap进行矩阵变化
private void initShader(Bitmap bitmap) {
    mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

    int bitmapWidth = bitmap.getWidth();
    int bitmapHeight = bitmap.getHeight();
    float sx = mWidth * 1.0f / bitmapWidth;
    float sy = mHeight * 1.0f / bitmapHeight;
    float scale = Math.max(sx, sy);

    Matrix matrix = new Matrix();
    matrix.setScale(scale, scale);
    mShader.setLocalMatrix(matrix);
}

3. 将着色器BitmapShader,设置给Paint

mPaint.setShader(mShader);

4. 利用Canvas,自己绘制圆形/圆角图

注意点:

  • drawRoundRect只适用于Android 21及其以上版本
  • 要删除 super.onDraw(canvas):否则Canvas又会在ImageView中重新绘制,将我们之前的操作都覆盖了
@Override
protected void onDraw(Canvas canvas) {
    initPaint();
    if (mRadius < 0) {
        float radius = Math.min(mWidth, mHeight) / 2;
        canvas.drawCircle(mWidth/2, mHeight/2, radius, mPaint);
    } else {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {    // 21及其以上
            canvas.drawRoundRect(0, 0, mWidth, mHeight, mRadius, mRadius, mPaint);
        } else {
            super.onDraw(canvas);
        }
    }
    // super.onDraw(canvas);
}

完整代码

public class RoundImageView2 extends AppCompatImageView {

    private int mWidth;
    private int mHeight;
    private float mRadius;
    private Paint mPaint;
    private Bitmap mBitmap;
    private BitmapShader mShader;

    public RoundImageView2(Context context) {
        this(context, null);
    }

    public RoundImageView2(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public RoundImageView2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView(context);
    }

    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        mRadius = ta.getDimension(R.styleable.RoundImageView_radius, -1);
        ta.recycle();
    }

    private void initView(Context context) {
        mPaint = new Paint();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        initPaint();
        if (mRadius < 0) {
            float radius = Math.min(mWidth, mHeight) / 2;
            canvas.drawCircle(mWidth/2, mHeight/2, radius, mPaint);
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {    // 21及其以上
                canvas.drawRoundRect(0, 0, mWidth, mHeight, mRadius, mRadius, mPaint);
            } else {
                super.onDraw(canvas);
            }
        }

//        super.onDraw(canvas);
    }

    /**
     * 设置画笔
     */
    private void initPaint() {
        initBitmap();
        initShader(mBitmap);
        mPaint.setShader(mShader);
    }

    /**
     * 获取Bitmap
     */
    private void initBitmap() {
        Drawable drawable1 = getDrawable();
        Drawable drawable2 = getBackground();
        Drawable drawable = drawable1==null ? drawable2 : drawable1;          // 不能在构造方法中获取drawable,为null
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            mBitmap = bitmapDrawable.getBitmap();
        } else {
            int width = drawable.getIntrinsicWidth();     
            int height = drawable.getIntrinsicHeight(); 
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(mBitmap);
//            drawable.setBounds(0,0,width,height);
            drawable.draw(canvas); 
        }
    }

    /**
     * 获取BitmapShader
     */
    private void initShader(Bitmap bitmap) {
        mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();
        float sx = mWidth * 1.0f / bitmapWidth;
        float sy = mHeight * 1.0f / bitmapHeight;
        float scale = Math.max(sx, sy);

        Matrix matrix = new Matrix();
        matrix.setScale(scale, scale);
        mShader.setLocalMatrix(matrix);
    }
}

OutlineProvider切割

思路

通过view.setOutlineProvider,给我们的View控件设置一个圆形轮廓,然后让View根据轮廓提供者进行切割

这个方法不同于前2种,前面2种方法,都是针对Canvas做文章,因此只能适用于图片的圆形处理;而这个方法是实实在在的对View进行了切割,不仅仅局限于图片,还可以针对任何其他View控件进行剪裁,适用范围更广(比如我们可以将整个页面变成一个圆形显示)

缺陷

但是这个方法有个限制,就是OutlineProvider只能适用于API>=21的版本,无法兼容低版本

OutlineProvider轮廓提供者

OutlineProvider轮廓提供者,可以给View提供一个外轮廓,并且让其根据轮廓进行剪切

  • view.setOutlineProvider:设置轮廓提供者
  • view.setClipToOutline:根据轮廓进行剪切
  • outline.setOval:画一个圆形轮廓
  • outline.setRect:画一个矩形轮廓

注意事项:

  • OutlineProvider要求API必须>=21;
  • OutlineProvider必须重写getOutline方法,其中参数Outline,就是提供给View的轮廓,我们可以根据需要自定义形状

完整代码

public class RoundImageView3 extends AppCompatImageView {

    private float mRadius;

    public RoundImageView3(Context context) {
        this(context, null);
    }

    public RoundImageView3(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public RoundImageView3(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView();
    }

    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        mRadius = ta.getDimension(R.styleable.RoundImageView_radius, -1);
        ta.recycle();
    }

    private void initView() {
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            ViewOutlineProvider outlineProvider = new ViewOutlineProvider(){
                @Override
                public void getOutline(View view, Outline outline) {
                    int width = view.getWidth();
                    int height = view.getHeight();
                    if (mRadius < 0) {
                        int radius = Math.min(width, height) / 2;
                        Rect rect = new Rect(width/2-radius, height/2-radius, width/2+radius, height/2+radius);
                        outline.setOval(rect);  // API>=21
                    } else {
                        Rect rect = new Rect(0, 0, width, height);
                        outline.setRoundRect(rect, mRadius);
                    }
                }
            };
            setClipToOutline(true);
            setOutlineProvider(outlineProvider);
        }
    }

}

Glide显示圆形/圆角图片

思路
通过Glide图片加载框架实现,我们只需要给RequestOptions添加一个CircleCrop变换,即可实现圆形图片效果;如果要实现圆角图片,则需要自己去定义一个BitmapTransformation

缺陷

没有缺陷

1. Glide实现圆形图片

Glide内置了很多针对图形的Transformation变换,我们可以借助其中的CircleCrop选项非常方便的实现圆形图片的效果。

  • 创建一个RequestOptions选项
  • 给RequestOptions,添加CircleCrop变换
  • 通过apply,将RequestOptions设置给Glide的RequestBuilder

下面2种方式,都可以实现圆形图片的效果,只是写法不一样:

public static void loadCircleImage1(Context context, String url, ImageView imageView) {
    Glide.with(context)
            .load(url)
            .apply(RequestOptions.circleCropTransform())
            .into(imageView);
}

public static void loadCircleImage2(Context context, String url, ImageView imageView) {
    RequestOptions options = new RequestOptions()
            .circleCrop();
    Glide.with(context)
            .load(url)
            .apply(options)
            .into(imageView);
}

2. Glide显示圆角图片

Glide并没有像提供CircleCrop那样,提供一个圆角图片的Transformation,因此如果需要显示圆角图片,那么就需要自己去定义一个Transformation。

那么,要怎么去定义一个Transformation呢?我们可以参考Circrop的做法:

  • 写一个类继承BitmapTransformation
  • 重写transformupdateDiskCacheKeyequalshashCode方法

transform:实现变化的具体细节

  • BitmapPool:可以用来快速的获取一个Bitmap的资源池,并且通常要在方法中返回这个获取到的Bitmap
  • toTransform:需要变化的Bitmap原始资源;需要注意的是,这个原始资源并不是最初的Bitmap,在调用这个方法之前Glide已经将原始Bitmap进行了合适的缩放
  • outWidthoutHeight:Bitmap的理想尺寸;需要注意的是,这个尺寸并不是Bitmap的尺寸,也不是ImageView的尺寸,Glide给我们返回的这个尺寸是ImageView的最小宽高值(如果ImageView的宽高都是match_parent,那么返回的是ImageView的最大宽高值)

CircleCrop的源码

public class CircleCrop extends BitmapTransformation {
  // The version of this transformation, incremented to correct an error in a previous version.
  // See #455.
  private static final int VERSION = 1;
  private static final String ID = "com.bumptech.glide.load.resource.bitmap.CircleCrop." + VERSION;
  private static final byte[] ID_BYTES = ID.getBytes(CHARSET);

  public CircleCrop() {
    // Intentionally empty.
  }

  /**
   * @deprecated Use {@link #CircleCrop()}.
   */
  @Deprecated
  public CircleCrop(@SuppressWarnings("unused") Context context) {
    this();
  }

  /**
   * @deprecated Use {@link #CircleCrop()}
   */
  @Deprecated
  public CircleCrop(@SuppressWarnings("unused") BitmapPool bitmapPool) {
    this();
  }

  // Bitmap doesn't implement equals, so == and .equals are equivalent here.
  @SuppressWarnings("PMD.CompareObjectsWithEquals")
  @Override
  protected Bitmap transform(
      @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
    return TransformationUtils.circleCrop(pool, toTransform, outWidth, outHeight);
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof CircleCrop;
  }

  @Override
  public int hashCode() {
    return ID.hashCode();
  }

  @Override
  public void updateDiskCacheKey(MessageDigest messageDigest) {
    messageDigest.update(ID_BYTES);
  }
}

自定义的一个圆角BitmapTransformation

这个实现细节,与前面的“利用BitmapShader绘制一个圆角图片”基本是一样的。

public class GlideRoundRect extends BitmapTransformation {

    private float mRadius;
    private static final int VERSION = 1;
    private static final String ID = BuildConfig.APPLICATION_ID + ".GlideRoundRect." + VERSION;
    private static final byte[] ID_BYTES = ID.getBytes(CHARSET);
    @Override
    public void updateDiskCacheKey(MessageDigest messageDigest) {
        messageDigest.update(ID_BYTES);
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof GlideRoundRect;
    }

    @Override
    public int hashCode() {
        return ID.hashCode();
    }

    public GlideRoundRect(float radius) {
        super();
        mRadius = radius;
    }

    @Override
    protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
        return roundRectCrop(pool, toTransform);
    }

    private Bitmap roundRectCrop(BitmapPool pool, Bitmap source) {
        if (source == null)
            return null;
        // 1. 根据source,创建一个BitmapShader
        BitmapShader bitmapShader = new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        Paint paint = new Paint();
        paint.setShader(bitmapShader);
        // 2. 获取一个新的Bitmap
        int sourceWidth = source.getWidth();
        int sourceHeight = source.getHeight();
        Bitmap bitmap = pool.get(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888);
        // 3. 给新的Bitmap附上图形
        Canvas canvas = new Canvas(bitmap);
        RectF rect = new RectF(0, 0, sourceWidth, sourceHeight);
        canvas.drawRoundRect(rect, mRadius, mRadius, paint);
        // 4. 返回Bitmap
        return bitmap;
    }

}

强哥大天才
6 声望1 粉丝

引用和评论

0 条评论