玩转ShapeableImageView:实现灵活的自定义形状与边框效果

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>

详细使用可以参考代码中的注释,不再挨个解释了,最后再列一下大致的关系图:

相关推荐
V少年28 分钟前
深入浅出H.264码流分析
android
Double Point1 小时前
(四十三)Dart 中的空安全与 `required` 关键字
android·安全
奔跑吧 android1 小时前
【android bluetooth 协议分析 01】【HCI 层介绍 1】【hci_packets.pdl 介绍】
android·bluetooth·bt·gabeldorsche·gd·aosp13·bluedroid
冰糖葫芦三剑客3 小时前
安卓 手机拨打电话录音保存地址适配
android
匹马夕阳4 小时前
(十五)安卓开发中不同类型的view之间继承关系详解
android
Jomurphys6 小时前
Android Studio - 解决 Please Select Android SDK
android·android studio
stevenzqzq6 小时前
kotlin扩展函数
android·开发语言·kotlin
V少年7 小时前
深入浅出Java内存模型(JMM)
android
行墨7 小时前
插件资源隔离冲突‌解决方案
android
Hello姜先森7 小时前
Kotlin日常使用函数记录
android·开发语言·kotlin