玩转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>

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

相关推荐
雨白5 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹6 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空8 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭8 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日9 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安9 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑9 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟14 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡15 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0015 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体