这篇博客主要讲解了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
尊重原创,转载请注明出处
本文出自
Path切割
思路
我们可以定义一个圆形Path路径,然后调用canvas.clipPath,将图片
切割成圆形
缺陷
但是这种方法有2个限制:
- cliptPath不支持硬件加速,因此在调用前必须禁用硬件加速,
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
- 这种方式剪裁的是Canvas图形,View的实际形状是不变的,因此只能对src属性有效,对background属性是无效的。
1.定义Radius属性,用来设置圆角半径
注意事项:
- 我们定义radius为
dimension
,这是一个带单位的值(float不带单位) - radius:值默认或者<0,表示圆形图;>0表示圆角图
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分
src
和background
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中重新绘制,将我们之前的操作都覆盖了
@Overrideprotected 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
- 重写
transform
、updateDiskCacheKey
、equals
、hashCode
方法
transform:实现变化的具体细节
-
BitmapPool
:可以用来快速的获取一个Bitmap的资源池,并且通常要在方法中返回这个获取到的Bitmap -
toTransform
:需要变化的Bitmap原始资源;需要注意的是,这个原始资源并不是最初的Bitmap,在调用这个方法之前Glide已经将原始Bitmap进行了合适的缩放 -
outWidth
、outHeight
: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; }}