干掉shape,手动构建GradientDrwable

3,619 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

日常开发中,实现比如TextViewButton等组件的圆角、渐变、边框一般都是定义个xml文件,然后使用shape实现,比如:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="7dp" />

    <solid android:color="#2196F3" />

    <stroke android:width="4dp" android:color="#ff0000"/>
</shape>

这种写法有两个不好的地方:

  • 造成资源路径下xml文件数量过多,不好管理
  • xml文件解析涉及到了io,增加了解析耗时 现在网上也有很多的方案,核心一般都是通过代码手动构建GradientDrawable,本文也不例外,主要利用Kotlin密封类、扩展函数、+运算符重载、单链表头插法等实现下面这种效果:
mBinding.goMeetingBtn.shape = corner(17) +
        stroke(5, "#ff0000") +
        gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")

0ec1a95445aaf015a29798de591a6f4.jpg 接下来,就开始一步步的讲解这种写法是如何封装的

1.密封类定义GradientDrawable四大基本操作

xml中shape最终是要转换成GradientDrawable,平常我们使用shape主要就是为了实现View背景、圆角、渐变、边框四种操作,我们就将这四种操作利用密封类定义成四种状态:

sealed class ShapeDrawable {
    class Solid(val color: Any) : ShapeDrawable()

    class Corner : ShapeDrawable {
        var radiusArray: FloatArray? = null
        var radius: Float = 0.0f

        constructor(radius: Int) : super() {
            this.radius = radius.toFloat()
        }

        constructor(
            topLeftRadius: Int,
            topRightRadius: Int,
            bottomRightRidus: Int,
            bottomLeftRidus: Int
        ) 

    data class Stroke(
        val strokeWidth: Int,
        val dashColor: Any,
        val dashWidth: Int = 0,
        val dashGap: Int = 0,
        val shapeType: Int = GradientDrawable.RECTANGLE
    ) : ShapeDrawable() 

    data class GradientState(
        val orientation: GradientDrawable.Orientation,
        val startColor: Any,
        val endColor: Any
    ) : ShapeDrawable()
}

其中:
Solid:背景颜色填充,构造方法中指定要填充的颜色
Corner:背景圆角,可以分别指定四个角各自的角度,也可以共同指定
Stroke:背景边框,除了需要指定边框粗细、颜色,还可以指定边框上虚线的间隔gap、背景形状
GradientState:背景渐变填充,构造方法中需要传入渐变方向、起始颜色和结束颜色

2.通过"+"号实现四大基本操作叠加

上面定义的四大基本操作是可以互相进行组合来设置View背景,一种非常方便的方式通过相加"+"实现操作的叠加。
我们可以kotlin的运算符重载实现,"+"对应的重载函数为plus:

sealed class ShapeDrawable {
    operator fun plus(shape: ShapeDrawable): ShapeDrawable {
        return shape
    }
}

这样就可以实现诸如Solid() + Corner()等四大基本操作互相组合的效果

3."+"如何保存叠加的四大基本操作

这个主要是利用单链表思想,将ShapeDrawable的具体操作实现类组合成一条链

  • 密封类ShapeDrawable中定义个next属性,指向下一个ShapeDrawable具体操作实现的SolidCorner等等
sealed class ShapeDrawable {
    var next: ShapeDrawable? = null
}
  • 通过"+"的plus运算符重载函数实现ShapeDrawable具体操作实现类的插入
sealed class ShapeDrawable {
    var next: ShapeDrawable? = null

    operator fun plus(shape: ShapeDrawable): ShapeDrawable {
        shape.next = this
        return shape
    }
}

上面需要使用链表的头插法实现

4.ShapeDrawable定义对GradientDrawable的操作的抽象方法

定义box方法封装对GradientDrawable的操作

sealed class ShapeDrawable {
    var next: ShapeDrawable? = null

    abstract fun box(drawable: GradientDrawable?): GradientDrawable
}

具体的实现由ShapeDrawable的具体子类Solid、Stroke、GradientState、Corner实现

sealed class ShapeDrawable {
    abstract fun box(drawable: GradientDrawable?): GradientDrawable

    class Solid: ShapeDrawable() {
        override fun box(drawable: GradientDrawable?): GradientDrawable {
            drawable!!.setColor(color.color)
            return drawable
        }
    }

    class Corner: ShapeDrawable {
        var radiusArray: FloatArray? = null
        var radius: Float = 0.0f

        override fun box(drawable: GradientDrawable?): GradientDrawable {
            if (radiusArray == null) {
                drawable!!.cornerRadius = radius
            } else {
                drawable!!.cornerRadii = radiusArray
            }
            return drawable
        }
    }

    data class Stroke: ShapeDrawable() {

        override fun box(drawable: GradientDrawable?): GradientDrawable {
            drawable!!.apply {
                setStroke(
                    strokeWidth.dp.toInt(),
                    dashColor.color,
                    dashWidth.dp,
                    dashGap.dp
                )
                shape = shapeType
            }

            return drawable
        }
    }

    data class GradientState: ShapeDrawable() {

        override fun box(drawable: GradientDrawable?): GradientDrawable {
            //因为要构造方法传入,使用时这个需要放在第一位
            return GradientDrawable(
                orientation, intArrayOf(
                    startColor.color,
                    endColor.color
                )
            )
        }
    }
}

5.定义View的扩展属性shape实现背景设置

var View.shape: ShapeDrawable
    get() = ShapeDrawable.Empty()
    set(value) {
        var s: ShapeDrawable? = value
        val list = mutableListOf<ShapeDrawable>()
        var drawable: GradientDrawable? = null
        while (s != null) {
            if (s is ShapeDrawable.GradientState) {
                drawable = s.box(null)
            } else {
                list.add(s)
            }
            s = s.next
        }

        if (drawable == null) {
            drawable = GradientDrawable()
        }

        list.forEach {
            it.box(drawable)
        }

        background = drawable
    }

通过以上几步,我们就可以实现如下效果:

mBinding.goMeetingBtn.shape = ShapeDrawable.Corner(17) +
        ShapeDrawable.Stroke(5, "#ff0000") +
        ShapeDrawable.GradientState(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")

但是发现,设置背景操作每次都需要调用ShapeDrawable.Corner()ShapeDrawable.Stroke等等太过于麻烦

6.定义通用方法快速实现ShapeDrawable.xxx具体子类的创建

fun solid(color: Any): ShapeDrawable.Solid {
    return ShapeDrawable.Solid(color)
}

fun corner(radius: Int): ShapeDrawable.Corner {
    return ShapeDrawable.Corner(radius)
}

这里只列出了Solid、Corner类的创建方法,Stroke、GradientState类似。

最终我们就可以实现文章开头一开始展示的代码:

mBinding.goMeetingBtn.shape = corner(17) +
        stroke(5, "#ff0000") +
        gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")

7.完整的代码参考

github:手动构建GradientDrawable替代xml的shape