Android圆角图片实现:RoundImageView与Canvas.drawDoubleRoundRect解析

Android圆角图片实现:RoundImageView与Canvas.drawDoubleRoundRect解析

在Android开发中,实现圆角图片是常见的UI需求,传统方案如使用第三方库或XML形状定义存在性能损耗或灵活性不足的问题。本文将详细介绍通过自定义RoundImageView控件,结合Canvas.drawDoubleRoundRect方法实现高效圆角图片的完整方案,并提供性能优化建议。

一、传统圆角实现方案的局限性

1.1 XML形状定义方案

通过res/drawable/rounded_corner.xml定义shape资源:

  1. <shape xmlns:android="http://schemas.android.com/apk/res/android">
  2. <corners android:radius="16dp"/>
  3. <solid android:color="@android:color/transparent"/>
  4. </shape>

在ImageView中设置background属性:

  1. <ImageView
  2. android:layout_width="100dp"
  3. android:layout_height="100dp"
  4. android:background="@drawable/rounded_corner"
  5. android:scaleType="centerCrop"/>

局限性:无法动态修改圆角半径,且当图片宽高比与shape不匹配时会出现显示异常。

1.2 第三方库方案

常见第三方库如Glide的RoundingTransformer:

  1. Glide.with(context)
  2. .load(url)
  3. .transform(new RoundedCornersTransformation(16, 0))
  4. .into(imageView);

问题

  • 增加APK体积(平均+80KB)
  • 转换过程产生额外Bitmap对象
  • 无法实现非对称圆角(如仅左上角圆角)

二、Canvas.drawDoubleRoundRect技术解析

2.1 方法原理

Canvas.drawDoubleRoundRect()是Android 3.0(API 11)引入的方法,允许为矩形指定两组不同的圆角半径:

  1. public void drawDoubleRoundRect(RectF outer, float[] outerRadii,
  2. RectF inner, float[] innerRadii,
  3. Paint paint)
  • outerRadii:外矩形四个角的半径数组[x1,y1, x2,y2, x3,y3, x4,y4]
  • innerRadii:内矩形四个角的半径数组
  • 当inner参数为null时,等同于单圆角矩形绘制

2.2 实现RoundImageView的核心步骤

2.2.1 自定义View基础结构

  1. public class RoundImageView extends AppCompatImageView {
  2. private float[] outerRadii = new float[8];
  3. private float[] innerRadii = new float[8];
  4. private RectF outerRect = new RectF();
  5. private RectF innerRect = new RectF();
  6. private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  7. private BitmapShader shader;
  8. public RoundImageView(Context context) {
  9. super(context);
  10. init();
  11. }
  12. // 其他构造方法...
  13. }

2.2.2 关键绘制方法实现

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. if (getDrawable() == null) {
  4. super.onDraw(canvas);
  5. return;
  6. }
  7. // 1. 初始化Shader
  8. Bitmap bitmap = ((BitmapDrawable)getDrawable()).getBitmap();
  9. shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
  10. paint.setShader(shader);
  11. // 2. 计算绘制区域
  12. outerRect.set(0, 0, getWidth(), getHeight());
  13. float innerOffset = strokeWidth; // 边框宽度
  14. innerRect.set(innerOffset, innerOffset,
  15. getWidth()-innerOffset, getHeight()-innerOffset);
  16. // 3. 设置圆角半径(示例:左上角圆角)
  17. Arrays.fill(outerRadii, 0);
  18. outerRadii[0] = cornerRadius; // 左上x
  19. outerRadii[1] = cornerRadius; // 左上y
  20. // 4. 执行绘制
  21. canvas.drawDoubleRoundRect(outerRect, outerRadii,
  22. innerRect, innerRadii,
  23. paint);
  24. }

三、完整实现方案与优化

3.1 完整RoundImageView实现

  1. public class RoundImageView extends AppCompatImageView {
  2. private float cornerRadius = 0f;
  3. private float strokeWidth = 0f;
  4. private int strokeColor = Color.TRANSPARENT;
  5. private final float[] outerRadii = new float[8];
  6. private final RectF rect = new RectF();
  7. private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  8. private BitmapShader shader;
  9. public RoundImageView(Context context) {
  10. this(context, null);
  11. }
  12. public RoundImageView(Context context, AttributeSet attrs) {
  13. super(context, attrs);
  14. init(attrs);
  15. }
  16. private void init(AttributeSet attrs) {
  17. TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RoundImageView);
  18. cornerRadius = a.getDimension(R.styleable.RoundImageView_cornerRadius, 0);
  19. strokeWidth = a.getDimension(R.styleable.RoundImageView_strokeWidth, 0);
  20. strokeColor = a.getColor(R.styleable.RoundImageView_strokeColor, Color.TRANSPARENT);
  21. a.recycle();
  22. paint.setStyle(Paint.Style.FILL);
  23. }
  24. @Override
  25. protected void onDraw(Canvas canvas) {
  26. Drawable drawable = getDrawable();
  27. if (drawable == null) {
  28. super.onDraw(canvas);
  29. return;
  30. }
  31. Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
  32. shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
  33. paint.setShader(shader);
  34. rect.set(0, 0, getWidth(), getHeight());
  35. // 设置四个角的半径
  36. Arrays.fill(outerRadii, 0);
  37. outerRadii[0] = cornerRadius; // 左上x
  38. outerRadii[1] = cornerRadius; // 左上y
  39. outerRadii[2] = cornerRadius; // 右上x
  40. outerRadii[3] = cornerRadius; // 右上y
  41. outerRadii[4] = cornerRadius; // 右下x
  42. outerRadii[5] = cornerRadius; // 右下y
  43. outerRadii[6] = cornerRadius; // 左下x
  44. outerRadii[7] = cornerRadius; // 左下y
  45. // 绘制边框
  46. if (strokeWidth > 0) {
  47. Paint strokePaint = new Paint(paint);
  48. strokePaint.setStyle(Paint.Style.STROKE);
  49. strokePaint.setStrokeWidth(strokeWidth);
  50. strokePaint.setColor(strokeColor);
  51. canvas.drawRoundRect(rect, cornerRadius, cornerRadius, strokePaint);
  52. }
  53. // 绘制圆角内容
  54. canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
  55. }
  56. public void setCornerRadius(float radius) {
  57. this.cornerRadius = radius;
  58. invalidate();
  59. }
  60. }

3.2 性能优化建议

  1. 硬件加速:确保在AndroidManifest.xml中为Activity启用硬件加速

    1. <application android:hardwareAccelerated="true" ...>
  2. Shader复用:在onDraw中避免重复创建BitmapShader对象,可在onSizeChanged中初始化

    1. @Override
    2. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    3. super.onSizeChanged(w, h, oldw, oldh);
    4. if (getDrawable() != null) {
    5. Bitmap bitmap = ((BitmapDrawable)getDrawable()).getBitmap();
    6. shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    7. paint.setShader(shader);
    8. }
    9. }
  3. 避免过度绘制:当圆角半径为0时,直接调用super.onDraw()

    1. @Override
    2. protected void onDraw(Canvas canvas) {
    3. if (cornerRadius <= 0) {
    4. super.onDraw(canvas);
    5. return;
    6. }
    7. // 其他绘制逻辑...
    8. }

四、高级功能扩展

4.1 非对称圆角实现

通过分别设置四个角的半径:

  1. public void setCustomCorners(float tl, float tr, float br, float bl) {
  2. outerRadii[0] = tl; outerRadii[1] = tl; // 左上
  3. outerRadii[2] = tr; outerRadii[3] = tr; // 右上
  4. outerRadii[4] = br; outerRadii[5] = br; // 右下
  5. outerRadii[6] = bl; outerRadii[7] = bl; // 左下
  6. invalidate();
  7. }

4.2 动态圆角动画

结合ValueAnimator实现平滑过渡:

  1. ValueAnimator animator = ValueAnimator.ofFloat(0, maxRadius);
  2. animator.addUpdateListener(animation -> {
  3. cornerRadius = (float) animation.getAnimatedValue();
  4. invalidate();
  5. });
  6. animator.start();

五、最佳实践总结

  1. API兼容性:对于API<11的设备,需提供降级方案(如使用反射调用或回退到XML方案)

  2. 内存管理

    • 大图显示时建议结合BitmapFactory.Options进行采样
    • 及时回收不再使用的Bitmap对象
  3. 性能监控

    • 使用Systrace检测绘制性能
    • 避免在onDraw中进行耗时操作
  4. 测试建议

    • 测试不同尺寸图片的显示效果
    • 验证圆角半径为0时的回退行为
    • 检查与ImageView.ScaleType的兼容性

通过Canvas.drawDoubleRoundRect实现的RoundImageView方案,相比传统方法具有更高的灵活性和更好的性能表现,特别适合需要动态调整圆角参数或实现复杂圆角效果的场景。在实际开发中,建议根据项目需求选择合适的实现方式,并在性能关键路径上进行充分测试。