ShapeableImageView 是 Android Material Components 库中的一个控件,用于轻松实现自定义形状的 ImageView。通过 ShapeableImageView,开发者可以轻松地实现圆形、圆角矩形、不同角的圆角以及其他复杂形状,而无需依赖 XML 的 shape 文件或者第三方库。
ShapeableImageView
为什么使用 ShapeableImageView?
- 强大且灵活:支持圆角矩形、椭圆、三角形、多边形等形状。
- 更简单的代码:不需要再定义复杂的 shape 文件。
- 实时修改形状:可以动态改变形状和边框。
- 依赖官方库:无需引入第三方库,减少冗余。
使用示例
            
            
              kotlin
              
              
            
          
          implementation "com.google.android.material:material:1.9.0"XML中设置
以下是 ShapeableImageView 支持的主要 XML 属性:
| 属性 | 作用 | 
|---|---|
| app:strokeWidth | 边框宽度 | 
| app:strokeColor | 边框颜色 | 
| app:shapeAppearance | 设置基础形状样式,定义控件的外观形状 | 
| app:shapeAppearanceOverlay | 对控件形状进行局部覆盖,通常用来在继承 shapeAppearance 的基础上,细化某些部分(如某些角的大小或形状)。 | 
- 圆形设置
            
            
              kotlin
              
              
            
          
          <com.google.android.material.imageview.ShapeableImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@drawable/icon_cat_w"
        app:shapeAppearance="@style/circleCornerStyle" />在style中定义circleCornerStyle如下,设置圆角方式:
            
            
              kotlin
              
              
            
          
          <style name="circleCornerStyle">
     <item name="cornerFamily">rounded</item>
     <item name="cornerSize">50%</item>
</style>cornerFamily :定义角的类型(rounded 表示圆角,cut 表示切角)。 cornerSize:定义角的大小,可以是具体大小,也可以是百分比。
效果图:

如果在上述XML中加入strokeColor、strokeWidth属性,即可实现边框效果。
            
            
              kotlin
              
              
            
          
          <com.google.android.material.imageview.ShapeableImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@drawable/icon_cat_w"
        android:padding="2dp"
        app:strokeColor="@color/red"
        app:strokeWidth="4dp"
        app:shapeAppearance="@style/circleCornerStyle" />效果图:

可以看到上述XML中还设置了padding,且padding的数值是strokeWidth的一半,这里的padding是必须要设置的,否则边框会展示不全。
- 圆角矩形设置
            
            
              kotlin
              
              
            
          
          <com.google.android.material.imageview.ShapeableImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@drawable/icon_cat_w"
        app:shapeAppearance="@style/roundedCornerStyle" />
            
            
              kotlin
              
              
            
          
          <style name="roundedCornerStyle">
    <item name="cornerFamily">rounded</item>
    <item name="cornerSize">10dp</item>
</style>效果图:

            
            
              kotlin
              
              
            
          
          <com.google.android.material.imageview.ShapeableImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@drawable/icon_cat_w"
        android:padding="2dp"
        app:strokeColor="@color/red"
        app:strokeWidth="4dp"
        app:shapeAppearance="@style/roundedCornerStyle" />效果图:

在style中可以设置的常用属性:
- cornerSize:设置所有角的圆角大小。可以是具体尺寸(如dp)或相对尺寸(如百分比)。
- cornerSizeTopLeft、cornerSizeTopRight、cornerSizeBottomRight、cornerSizeBottomLeft:分别设置左上角、右上角、右下角、左下角的圆角大小。
- cornerFamily:设置所有角的圆角类型,可以是圆角(rounded)或切角(cut)。
- cornerFamilyTopLeft、cornerFamilyTopRight、cornerFamilyBottomRight、cornerFamilyBottomLeft:分别设置左上角、右上角、右下角、左下角的圆角类型。
看到这里会有个疑问,为什么shapeAppearance里的属性是通过style来设置呢?这些属性在 ShapeableImageView 又是如何处理的呢?  通过源码可以看到 shapeAppearance 里定义的属性并不是通过ShapeableImageView解析,而是通过一个叫ShapeAppearanceModel的类来处理的,继续看这个类的作用。
 通过源码可以看到 shapeAppearance 里定义的属性并不是通过ShapeableImageView解析,而是通过一个叫ShapeAppearanceModel的类来处理的,继续看这个类的作用。
代码设置
ShapeAppearanceModel
ShapeAppearanceModel 专门用于定义和管理控件的形状(例如圆角矩形、圆形、特定角的圆角等)。与 ShapeableImageView、TextView等控件结合使用时,可以轻松实现自定义形状。
自定义控件的形状(例如圆角矩形、圆形等)通常需要依赖 shape 文件(XML)。然而,这种方式往往需要编写较多的 XML 文件,增加了项目复杂度。而通过 ShapeAppearanceModel,就可以动态定义和管理控件的形状,灵活控制圆角半径、角的形状(直角或圆角),还可以实现复杂的形状设计,而无需额外的 shape XML 文件。

在 ShapeAppearanceModel.Builder 中,提供了一组用于定义形状外观(例如角和边处理)的方法。以下是各个方法的详细含义:
角(Corner)的设置方法
- setAllCorners(@CornerFamily int cornerFamily, @Dimension float cornerSize):设置所有四个角的角处理类型和角大小。
- setAllCorners(@NonNull CornerTreatment cornerTreatment):设置所有四个角的角处理(CornerTreatment)。
- setAllCornerSizes(@NonNull CornerSize cornerSize):设置所有四个角的角大小。
- setAllCornerSizes(@Dimension float cornerSize):设置所有四个角的固定大小。
- setTopLeftCornerSize(...)、setTopRightCornerSize(...)、setBottomRightCornerSize(...)、setBottomLeftCornerSize(...):单独设置某个角的大小。
- setTopLeftCorner(@CornerFamily int cornerFamily, @Dimension float cornerSize)、setTopRightCorner(...)、setBottomRightCorner(...)、setBottomLeftCorner(...):单独设置某个角的角处理类型和大小。
CornerTreatment
CornerTreatment 用于控制控件角部外观(如圆角、切角等)。通过 CornerTreatment,可以实现各种角部效果,从简单的圆角到自定义复杂的角部形状。它决定了控件的 4 个角(左上角、右上角、左下角、右下角)应该如何显示。通过 CornerTreatment,可以为控件实现圆角、切角等不同样式的角部效果。CornerTreatment 是一个抽象类,Material Design 提供了以下常用的实现类:
- RoundedCornerTreatment(默认):实现圆角效果,适用于大多数场景。
- CutCornerTreatment:实现切角效果,将角部切掉一部分,形成斜角。
- 自定义 CornerTreatment:通过继承 CornerTreatment,可以实现更加复杂的角部形状。
ShapeAppearanceModel 提供了多种方法来为控件设置角部效果,最常用的是以下两种:
- 统一设置所有角的 CornerTreatment:调用 setAllCorners 方法,可以为所有角设置相同的效果。
- 分别设置四个角的 CornerTreatment:调用 setTopLeftCorner、setTopRightCorner、setBottomLeftCorner 和 setBottomRightCorner 方法,可以为每个角设置不同的效果。
自定义EdgeTreatment
getEdgePath 是一个抽象方法,必须在自定义 EdgeTreatment 中重写,它定义了边缘的路径形状。
            
            
              kotlin
              
              
            
          
          override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath)length : 边缘的总长度。 center : 边缘的中心点,通常是 length / 2。 interpolation : 插值因子,用于定义边缘形状的渐变程度。当 interpolation = 0.0,边缘形状通常会被缩小到最小化,形状可能完全退化为一条直线;当 interpolation = 1.0,边缘形状按照原始设计完全展开,表现出完整的效果。 shapePath: 用于描述边缘形状的路径。
示例代码
            
            
              kotlin
              
              
            
          
          class WaveEdgeTreatment(private val waveHeight: Float, private val waveLength: Float) : EdgeTreatment() {
    override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
        val interpolatedHeight = waveHeight * interpolation // 根据插值因子调整波浪高度
        val numberOfWaves = (length / waveLength).toInt() // 计算波浪的个数
        shapePath.lineTo(0f, 0f) // 起点
        for (i in 0 until numberOfWaves) {
            val startX = i * waveLength
            val halfWaveX = startX + waveLength / 2
            val endX = startX + waveLength
            // 波浪的凸起部分
            shapePath.quadToPoint(
                halfWaveX, -interpolatedHeight, // 波浪高度(负值向上)
                endX, 0f // 回到水平线
            )
        }
        shapePath.lineTo(length, 0f) // 到达边缘末尾
    }
}边(Edge)的设置方法
- setAllEdges(@NonNull EdgeTreatment edgeTreatment):设置所有四条边的边处理。
- setLeftEdge(@NonNull EdgeTreatment leftEdge):设置左边的边处理。
- setTopEdge(@NonNull EdgeTreatment topEdge):设置顶部的边处理。
- setRightEdge(@NonNull EdgeTreatment rightEdge):设置右边的边处理。
- setBottomEdge(@NonNull EdgeTreatment bottomEdge):设置底部的边处理。
EdgeTreatment
EdgeTreatment 是一种抽象类,用于指定形状的边缘(即控件的上下左右四个边)的外观。通过实现或使用 EdgeTreatment,我们可以为控件边缘设计一些特殊的效果,比如波浪形、锯齿形、凹陷或凸起等。换句话说,EdgeTreatment 是 ShapeAppearanceModel 的边缘装饰器,可以改变控件的形状边缘,用于自定义卡片、按钮、对话框等。EdgeTreatment 的常用子类:
- EdgeTreatment(默认行为):没有特殊样式,边缘是直线。
- TriangleEdgeTreatment:用于在边缘创建等腰三角形的凸起或凹陷。如TriangleEdgeTreatment(float size, boolean inside),size表示三角形的高度。inside含义:true 表示三角形是凹陷,false 表示三角形是凸起。
- OffsetEdgeTreatment:用于将某种 EdgeTreatment 偏移指定的距离。如OffsetEdgeTreatment(EdgeTreatment edgeTreatment, float offset),edgeTreatment表示被偏移的边缘处理。offset表示的偏移量。
- 自定义 EdgeTreatment:可以通过继承 EdgeTreatment 类来定义完全自定义的边缘形状。
综合示例
先上效果图:  关键代码如下:
 关键代码如下:
            
            
              kotlin
              
              
            
          
          private val mRvShapeAble: RecyclerView by id(R.id.rv_shape_able_view)
data class ShapeItem(val type: Int, var desc: String = "")
// 示例数据
val imageList = mutableListOf<ShapeItem>().apply {
            //作用在TextView上
            add(ShapeItem(TYPE_0))
            add(ShapeItem(TYPE_1))
            add(ShapeItem(TYPE_2))
            add(ShapeItem(TYPE_3))
            add(ShapeItem(TYPE_4))
            add(ShapeItem(TYPE_5))
            //作用在ShapeableImageView上
            add(ShapeItem(TYPE_6))
            add(ShapeItem(TYPE_7))
            add(ShapeItem(TYPE_8))
            add(ShapeItem(TYPE_9))
            add(ShapeItem(TYPE_10))
            add(ShapeItem(TYPE_11))
            add(ShapeItem(TYPE_12))
            add(ShapeItem(TYPE_13))
            add(ShapeItem(TYPE_14))
            add(ShapeItem(TYPE_15))
            add(ShapeItem(TYPE_16))
            add(ShapeItem(TYPE_17))
}
mRvShapeAble.layoutManager = GridLayoutManager(this, 3)
mRvShapeAble.adapter = ShapeAdapter(imageList)
//Adapter
class ShapeAdapter(private val list: List<ShapeItem>) :
        RecyclerView.Adapter<ShapeAdapter.ShapeViewHolder>() {
        inner class ShapeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val ivShape: ShapeableImageView = itemView.findViewById(R.id.iv_shape_able)
            val tvShape: TextView = itemView.findViewById(R.id.textView)
        }
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShapeViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_shape_able_view, parent, false)
            return ShapeViewHolder(view)
        }
        override fun onBindViewHolder(holder: ShapeViewHolder, position: Int) {
            val ivShape = holder.ivShape
            val tvShape = holder.tvShape
            if (position > TYPE_5) {
                //ImageView
                ivShape.visible()
                tvShape.gone()
            } else {
                //TextView
                ivShape.gone()
                tvShape.visible()
            }
            when (position) {
                TYPE_0 -> {
                    //圆角矩形
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.ROUNDED, 16f)
                        .build()
                    val drawable = MaterialShapeDrawable(shapeAppearanceModel)
                    tvShape.background = drawable
                }
                TYPE_1 -> {
                    //圆角矩形
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(RoundedCornerTreatment())
                        .setAllCornerSizes(RelativeCornerSize(0.5f))
                        .build()
                    val drawable = MaterialShapeDrawable(shapeAppearanceModel).apply {
                        setStroke(2.dp2px().toFloat(), ColorStateList.valueOf(Color.RED)) //设置描边宽度及颜色
                        setPadding(1.dp2px(), 1.dp2px(), 1.dp2px(), 1.dp2px()) //设置padding
                    }
                    tvShape.layoutParams.run {
                        width = LayoutParams.MATCH_PARENT
                        height = 50.dp2px()
                    }
                    tvShape.background = drawable
                }
                TYPE_2 -> {
                    //切边
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.CUT, 20f) // 设置圆角
                        .build()
                    val materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
                    tvShape.background = materialShapeDrawable
                }
                TYPE_3 -> {
                    //聊天气泡
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.ROUNDED, 16f) // 设置圆角
                        .setBottomEdge(TriangleEdgeTreatment(16f, false)) //凸边
                        //.setRightEdge(TriangleEdgeTreatment(16f, false))
                        //.setLeftEdge(TriangleEdgeTreatment(16f, false))
                        .build()
                    val materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
                    tvShape.run {
                        (parent as? ViewGroup)?.clipChildren = false
                        tvShape.layoutParams.height = 38.dp2px()
                        setPadding(20.dp2px(), 5.dp2px(), 20.dp2px(), 5.dp2px())
                        background = materialShapeDrawable
                    }
                }
                TYPE_4 -> {
                    //聊天气泡
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.ROUNDED, 16f) // 设置圆角
                        .setBottomEdge(OffsetEdgeTreatment(TriangleEdgeTreatment(16f, false),-15.dp2px().toFloat())) //凸边
                        .build()
                    val materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
                    tvShape.run {
                        (parent as? ViewGroup)?.clipChildren = false
                        tvShape.layoutParams.height = 38.dp2px()
                        setPadding(20.dp2px(), 5.dp2px(), 20.dp2px(), 5.dp2px())
                        background = materialShapeDrawable
                    }
                }
                TYPE_5 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.ROUNDED, 14f) // 设置圆角
                        .setAllEdges(TriangleEdgeTreatment(12f, true)) //凹边
                        .build()
                    val materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
                    tvShape.run {
                        tvShape.layoutParams.height = 50.dp2px()
                        setPadding(20.dp2px(), 5.dp2px(), 20.dp2px(), 5.dp2px())
                        background = materialShapeDrawable
                    }
                }
                TYPE_6 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.ROUNDED, 16f)
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_7 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.CUT, 16f)
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_8 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        //RoundedCornerTreatment圆角  CutCornerTreatment切边
                        .setAllCorners(CutCornerTreatment())
                        //AbsoluteCornerSize具体数值 RelativeCornerSize比例(0.0-1.0)
                        .setAllCornerSizes(16f) //.setAllCornerSizes(RelativeCornerSize(0.5f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_9 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(RoundedCornerTreatment())
                        .setAllCornerSizes(RelativeCornerSize(0.5f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_10 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setTopLeftCornerSize(RelativeCornerSize(0.5f))
                        .setTopLeftCorner(RoundedCornerTreatment())
                        .setBottomRightCorner(RoundedCornerTreatment())
                        .setBottomRightCornerSize(RelativeCornerSize(0.5f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_11 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setTopRightCornerSize(RelativeCornerSize(0.5f))
                        .setTopRightCorner(RoundedCornerTreatment())
                        .setBottomLeftCorner(RoundedCornerTreatment())
                        .setBottomLeftCornerSize(RelativeCornerSize(0.5f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_12 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CornerFamily.ROUNDED, 16f)
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                    ivShape.run {
                        //设置边框
                        strokeColor = ColorStateList.valueOf(Color.RED)
                        strokeWidth = 4.dp2px().toFloat()
                        setPadding(2.dp2px(), 2.dp2px(), 2.dp2px(), 2.dp2px())
                    }
                }
                TYPE_13 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(RoundedCornerTreatment())
                        .setAllCornerSizes(RelativeCornerSize(0.5f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                    ivShape.run {
                        //设置边框
                        strokeColor = ColorStateList.valueOf(Color.RED)
                        strokeWidth = 4.dp2px().toFloat()
                        setPadding(2.dp2px(), 2.dp2px(), 2.dp2px(), 2.dp2px())
                    }
                }
                TYPE_14 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllEdges(TriangleEdgeTreatment(16f, true))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                    ivShape.run {
                        //设置边框
                        strokeColor = ColorStateList.valueOf(Color.RED)
                        strokeWidth = 4.dp2px().toFloat()
                        setPadding(2.dp2px(), 2.dp2px(), 2.dp2px(), 2.dp2px())
                    }
                }
                TYPE_15 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllEdges(OffsetEdgeTreatment(TriangleEdgeTreatment(16f, true), 50f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_16 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setLeftEdge(TriangleEdgeTreatment(16f, true))
                        //.setAllEdges(OffsetEdgeTreatment(TriangleEdgeTreatment(16f, true),50f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
                TYPE_17 -> {
                    val shapeAppearanceModel = ShapeAppearanceModel.builder()
                        .setAllCorners(CutCornerTreatment())
                        .setAllCornerSizes(RelativeCornerSize(0.5f))
                        .build()
                    ivShape.shapeAppearanceModel = shapeAppearanceModel
                }
            }
        }
        override fun getItemCount(): Int = list.size
    }Adapter中对应的item_shape_able_view.xml:
            
            
              kotlin
              
              
            
          
          <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="6dp"
    android:gravity="center"
    android:orientation="vertical">
    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/iv_shape_able"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:scaleType="centerCrop"
        android:src="@drawable/icon_cat_w" />
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingStart="20dp"
        android:paddingTop="16dp"
        android:paddingEnd="20dp"
        android:paddingBottom="16dp"
        android:text="Shapeable"
        android:textColor="@color/white"
        android:textSize="12sp" />
</LinearLayout>详细使用可以参考代码中的注释,不再挨个解释了,最后再列一下大致的关系图: 