这篇博客主要讲解了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表示圆角图
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中重新绘制,将我们之前的操作都覆盖了
@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
- 重写
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;
}
}