Android圆角图片实现:RoundImageView与Canvas.drawDoubleRoundRect解析
在Android开发中,实现圆角图片是常见的UI需求,传统方案如使用第三方库或XML形状定义存在性能损耗或灵活性不足的问题。本文将详细介绍通过自定义RoundImageView控件,结合Canvas.drawDoubleRoundRect方法实现高效圆角图片的完整方案,并提供性能优化建议。
一、传统圆角实现方案的局限性
1.1 XML形状定义方案
通过res/drawable/rounded_corner.xml定义shape资源:
<shape xmlns:android="http://schemas.android.com/apk/res/android"><corners android:radius="16dp"/><solid android:color="@android:color/transparent"/></shape>
在ImageView中设置background属性:
<ImageViewandroid:layout_width="100dp"android:layout_height="100dp"android:background="@drawable/rounded_corner"android:scaleType="centerCrop"/>
局限性:无法动态修改圆角半径,且当图片宽高比与shape不匹配时会出现显示异常。
1.2 第三方库方案
常见第三方库如Glide的RoundingTransformer:
Glide.with(context).load(url).transform(new RoundedCornersTransformation(16, 0)).into(imageView);
问题:
- 增加APK体积(平均+80KB)
- 转换过程产生额外Bitmap对象
- 无法实现非对称圆角(如仅左上角圆角)
二、Canvas.drawDoubleRoundRect技术解析
2.1 方法原理
Canvas.drawDoubleRoundRect()是Android 3.0(API 11)引入的方法,允许为矩形指定两组不同的圆角半径:
public void drawDoubleRoundRect(RectF outer, float[] outerRadii,RectF inner, float[] innerRadii,Paint paint)
outerRadii:外矩形四个角的半径数组[x1,y1, x2,y2, x3,y3, x4,y4]innerRadii:内矩形四个角的半径数组- 当inner参数为null时,等同于单圆角矩形绘制
2.2 实现RoundImageView的核心步骤
2.2.1 自定义View基础结构
public class RoundImageView extends AppCompatImageView {private float[] outerRadii = new float[8];private float[] innerRadii = new float[8];private RectF outerRect = new RectF();private RectF innerRect = new RectF();private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);private BitmapShader shader;public RoundImageView(Context context) {super(context);init();}// 其他构造方法...}
2.2.2 关键绘制方法实现
@Overrideprotected void onDraw(Canvas canvas) {if (getDrawable() == null) {super.onDraw(canvas);return;}// 1. 初始化ShaderBitmap bitmap = ((BitmapDrawable)getDrawable()).getBitmap();shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);paint.setShader(shader);// 2. 计算绘制区域outerRect.set(0, 0, getWidth(), getHeight());float innerOffset = strokeWidth; // 边框宽度innerRect.set(innerOffset, innerOffset,getWidth()-innerOffset, getHeight()-innerOffset);// 3. 设置圆角半径(示例:左上角圆角)Arrays.fill(outerRadii, 0);outerRadii[0] = cornerRadius; // 左上xouterRadii[1] = cornerRadius; // 左上y// 4. 执行绘制canvas.drawDoubleRoundRect(outerRect, outerRadii,innerRect, innerRadii,paint);}
三、完整实现方案与优化
3.1 完整RoundImageView实现
public class RoundImageView extends AppCompatImageView {private float cornerRadius = 0f;private float strokeWidth = 0f;private int strokeColor = Color.TRANSPARENT;private final float[] outerRadii = new float[8];private final RectF rect = new RectF();private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);private BitmapShader shader;public RoundImageView(Context context) {this(context, null);}public RoundImageView(Context context, AttributeSet attrs) {super(context, attrs);init(attrs);}private void init(AttributeSet attrs) {TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RoundImageView);cornerRadius = a.getDimension(R.styleable.RoundImageView_cornerRadius, 0);strokeWidth = a.getDimension(R.styleable.RoundImageView_strokeWidth, 0);strokeColor = a.getColor(R.styleable.RoundImageView_strokeColor, Color.TRANSPARENT);a.recycle();paint.setStyle(Paint.Style.FILL);}@Overrideprotected void onDraw(Canvas canvas) {Drawable drawable = getDrawable();if (drawable == null) {super.onDraw(canvas);return;}Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);paint.setShader(shader);rect.set(0, 0, getWidth(), getHeight());// 设置四个角的半径Arrays.fill(outerRadii, 0);outerRadii[0] = cornerRadius; // 左上xouterRadii[1] = cornerRadius; // 左上youterRadii[2] = cornerRadius; // 右上xouterRadii[3] = cornerRadius; // 右上youterRadii[4] = cornerRadius; // 右下xouterRadii[5] = cornerRadius; // 右下youterRadii[6] = cornerRadius; // 左下xouterRadii[7] = cornerRadius; // 左下y// 绘制边框if (strokeWidth > 0) {Paint strokePaint = new Paint(paint);strokePaint.setStyle(Paint.Style.STROKE);strokePaint.setStrokeWidth(strokeWidth);strokePaint.setColor(strokeColor);canvas.drawRoundRect(rect, cornerRadius, cornerRadius, strokePaint);}// 绘制圆角内容canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);}public void setCornerRadius(float radius) {this.cornerRadius = radius;invalidate();}}
3.2 性能优化建议
-
硬件加速:确保在AndroidManifest.xml中为Activity启用硬件加速
<application android:hardwareAccelerated="true" ...>
-
Shader复用:在onDraw中避免重复创建BitmapShader对象,可在onSizeChanged中初始化
@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);if (getDrawable() != null) {Bitmap bitmap = ((BitmapDrawable)getDrawable()).getBitmap();shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);paint.setShader(shader);}}
-
避免过度绘制:当圆角半径为0时,直接调用super.onDraw()
@Overrideprotected void onDraw(Canvas canvas) {if (cornerRadius <= 0) {super.onDraw(canvas);return;}// 其他绘制逻辑...}
四、高级功能扩展
4.1 非对称圆角实现
通过分别设置四个角的半径:
public void setCustomCorners(float tl, float tr, float br, float bl) {outerRadii[0] = tl; outerRadii[1] = tl; // 左上outerRadii[2] = tr; outerRadii[3] = tr; // 右上outerRadii[4] = br; outerRadii[5] = br; // 右下outerRadii[6] = bl; outerRadii[7] = bl; // 左下invalidate();}
4.2 动态圆角动画
结合ValueAnimator实现平滑过渡:
ValueAnimator animator = ValueAnimator.ofFloat(0, maxRadius);animator.addUpdateListener(animation -> {cornerRadius = (float) animation.getAnimatedValue();invalidate();});animator.start();
五、最佳实践总结
-
API兼容性:对于API<11的设备,需提供降级方案(如使用反射调用或回退到XML方案)
-
内存管理:
- 大图显示时建议结合BitmapFactory.Options进行采样
- 及时回收不再使用的Bitmap对象
-
性能监控:
- 使用Systrace检测绘制性能
- 避免在onDraw中进行耗时操作
-
测试建议:
- 测试不同尺寸图片的显示效果
- 验证圆角半径为0时的回退行为
- 检查与ImageView.ScaleType的兼容性
通过Canvas.drawDoubleRoundRect实现的RoundImageView方案,相比传统方法具有更高的灵活性和更好的性能表现,特别适合需要动态调整圆角参数或实现复杂圆角效果的场景。在实际开发中,建议根据项目需求选择合适的实现方式,并在性能关键路径上进行充分测试。