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的类来处理的,继续看这个类的作用。
代码设置
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>
详细使用可以参考代码中的注释,不再挨个解释了,最后再列一下大致的关系图: