Android 项目中 shape 标签的整理和思考(2)

在之前的博客中,我们曾经讨论设计过一个通用组件:CommonShapeButton 。主要用来移除项目中大量的 shape 文件,提高我们项目的可维护性。有兴趣的朋友可以点击下方链接进行阅读:
Android - Kotlin 是时候跟 shape 标签说拜拜了
这篇博客发布以后,得到了大家的广泛关注,可能大家也切身感受到了 CommonShapeButton 给我们带来的便利。而今天在这里,笔者想要讨论的是这个通用组件不能解决的应用场景,以及给出新的解决方案。

我们先来看看 CommonShapeButton 不能解决的应用场景是什么?这里我们需要回顾下这个通用组件,它本身是用来解决 shape 文件泛滥的问题,支持 shape 的各种特性,同时也支持文本样式和按钮样式。但是归根结底 CommonShapeButton 只是一个 View ,它没有办法解决 ViewGroup 的应用场景。而在实际开发过程中,在 ViewGroup 这一层去设置 shape 样式的背景是一个常见的需求。分析到这里,我们得出结论,我们还需要一个通用组件 CommonShapeViewGroup 来协助我们项目开发。

正当笔者准备着手设计这个新的通用组件的时候,脑中突然闪过一个官方提供的组件 CardView ,这个位于 support-v7 下面的谷歌亲儿子,好像已经解决了我们的问题?于是笔者又去啃了一下官方文档,对这个 CardView 做了一个全面的梳理,发现了它的局限性:

  • CardView 继承自 FrameLayout ,而现在主流的 ViewGroup 应该是 ConstraintLayout 和 RelativeLayout。
  • CardView 支持设置背景颜色,但是只能设置纯色,无法设置渐变颜色。
  • CardView 支持设置圆角大小,但是只能同时设置四个角的圆角大小,无法单一设置左侧圆角或者右侧圆角。
  • CardView 只支持矩形一种形状。
  • CardView 不支持设置描边颜色和描边宽度。

没办法,看来谷歌亲儿子也不顶用,还是自己撸吧。

Talk is cheap. Show me the code

第一步,我们需要确定支持的 ViewGroup 有哪些。还是那句话,现在主流的 ViewGroup 应该是 ConstraintLayout 和 RelativeLayout ,这里需要重点推一波 ConstraintLayout ,自从用了它以后,腰也不酸了,腿也不疼了,妈妈再也不用担心我写布局了。但是考虑到我们程序猿都是重感情的人,之前最爱的 RelativeLayout 也不能说有了新欢就不管了是吧,好吧,把 RelativeLayout 加上,就支持这两兄弟了。

第二步,继续思考如何来设计这个通用组件,主要是从以下几个方面进行了考虑:

  • ViewGroup 的设计要比 View 更简单,因为它是纯展示的,没有交互也不需要动效。
  • 直接继承 ConstraintLayout 和 RelativeLayout ,进行背景的动态设置是最为简单有效的方式。
  • 自定义属性方面,完全可以参照 CommonShapeButton ,去掉一些不需要的属性即可。
  • 新增一个阴影属性,提升一下逼格。控件阴影这个问题,在 5.0 以上也就一行代码的事。在 5.0 以下,笔者花了不少时间,用各种方案做出来的效果都不尽人意。本着宁缺毋滥的原则,最终还是选择了放弃。其实主要原因还是 5.0 以下的用户确实越来越少,花费过多的精力去做一些收效甚微的工作也不符合软件工程的思想。当然这方面有兴趣的朋友,可以在文章的后面拿到源码以后进行自己的扩展和修改。

第三步,思路已经梳理清楚了,那就开撸吧。这里就以 ConstraintLayout 为例,

class ShapeConstraintLayout @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

这里选择了直接继承 ConstraintLayout 进行扩展。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    // 初始化shape
    with(mGradientDrawable) {
        // 渐变色
        if (mStartColor != Color.parseColor("#FFFFFF") && mEndColor != Color.parseColor("#FFFFFF")) {
            colors = intArrayOf(mStartColor, mEndColor)
            when (mOrientation) {
                0 -> orientation = GradientDrawable.Orientation.TOP_BOTTOM
                1 -> orientation = GradientDrawable.Orientation.LEFT_RIGHT
            }
        }
        // 填充色
        else {
            setColor(mFillColor)
        }
        when (mShapeMode) {
            0 -> shape = GradientDrawable.RECTANGLE
            1 -> shape = GradientDrawable.OVAL
            2 -> shape = GradientDrawable.LINE
            3 -> shape = GradientDrawable.RING
        }
        // 统一设置圆角半径
        if (mCornerPosition == -1) {
            cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mCornerRadius.toFloat(), resources.displayMetrics)
        }
        // 根据圆角位置设置圆角半径
        else {
            cornerRadii = getCornerRadiusByPosition()
        }
        // 默认的透明边框不绘制
        if (mStrokeColor != Color.parseColor("#00000000")) {
            setStroke(mStrokeWidth, mStrokeColor)
        }
    }

    // 设置背景
    background = mGradientDrawable

    // 5.0以上设置阴影
    if (mWithElevation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        elevation = DEFAULT_ELEVATION
    }
}

核心代码依然选择在 onMeasure 方法中实现,我们做一个简单的分析:

  • 首先对 mGradientDrawable 设置当前是渐变色渲染还是填充色渲染,渐变色渲染还需要单独控制渲染的方向。
  • 然后对 mGradientDrawable 设置 shape 模式、圆角以及描边。这里的圆角设置区分了统一设置四个角还是根据圆角位置设置。
  • 然后设置 ViewGroup 的背景。
  • 最后在 5.0 以上设置控件阴影。

到这里,就完成了核心实现。下面我们看一下根据圆角位置设置圆角半径的具体实现:

/**
 * 根据圆角位置获取圆角半径
 */
private fun getCornerRadiusByPosition(): FloatArray {
    val result = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
    val cornerRadius = mCornerRadius.toFloat()
    if (containsFlag(mCornerPosition, TOP_LEFT)) {
        result[0] = cornerRadius
        result[1] = cornerRadius
    }
    if (containsFlag(mCornerPosition, TOP_RIGHT)) {
        result[2] = cornerRadius
        result[3] = cornerRadius
    }
    if (containsFlag(mCornerPosition, BOTTOM_RIGHT)) {
        result[4] = cornerRadius
        result[5] = cornerRadius
    }
    if (containsFlag(mCornerPosition, BOTTOM_LEFT)) {
        result[6] = cornerRadius
        result[7] = cornerRadius
    }
    return result
}

/**
 * 是否包含对应flag
 */
private fun containsFlag(flagSet: Int, flag: Int): Boolean {
    return flagSet or flag == flagSet
}

简单分析一下:

  • 自定义圆角位置支持四个方位的:TOP_LEFT、TOP_RIGHT、BOTTOM_RIGHT、BOTTOM_LEFT。
  • 通过自定义属性中的 flag 标签设置了圆角方位支持按位或运算。
  • 生成四个角对应的8位数组,解析 xml 属性根据按位或运算设置对应方位的圆角半径。

到这里,也就是 CommonShapeViewGroup 的全部实现了。其实笔者写到这里的时候,陷入了一个思考,我们到现在实现了 CommonShapeButton 和 CommonShapeViewGroup ,其实这两者的本质都是用代码去实现 shape 效果,也就是对 GradientDrawable 的二次封装,那么我们是不是实现一个封装以后的 CommonShapeDrawable 就可以解决所有问题呢?TextView 、Button 、ConstraintLayout 、RelativeLayout等等以及其他的应用场景都可以适配。笔者产生了这个想法以后,就马上去实现了一个。但是实际开发用起来以后,发现它并不像我们想象的那么方便,需要创建一个 CommonShapeDrawable 对象,然后逐一调用对应的方法去设置 shape 效果,最后还要在一个恰当的时机设置成控件的背景。这跟我们通过 xml 自定义属性就能实现效果来比,繁琐了不少,最终还是选择了放弃。有兴趣的朋友也可以通过这两篇博客的学习,自己去撸一个出来。

题外话说了这么多,这里还是回到 CommonShapeViewGroup ,照例贴上全部的自定义属性:

<declare-styleable name="CommonShapeViewGroup">
    <attr name="csvg_shapeMode" format="enum">
        <enum name="rectangle" value="0" />
        <enum name="oval" value="1" />
        <enum name="line" value="2" />
        <enum name="ring" value="3" />
    </attr>
    <attr name="csvg_fillColor" format="color" />
    <attr name="csvg_strokeColor" format="color" />
    <attr name="csvg_strokeWidth" format="dimension" />
    <attr name="csvg_cornerRadius" format="dimension" />
    <attr name="csvg_cornerPosition">
        <flag name="topLeft" value="1" />
        <flag name="topRight" value="2" />
        <flag name="bottomRight" value="4" />
        <flag name="bottomLeft" value="8" />
    </attr>
    <attr name="csvg_startColor" format="color" />
    <attr name="csvg_endColor" format="color" />
    <attr name="csvg_orientation" format="enum">
        <enum name="TOP_BOTTOM" value="0" />
        <enum name="LEFT_RIGHT" value="1" />
    </attr>
    <attr name="csvg_withElevation" format="boolean" />
</declare-styleable>

以下是效果图:

最后再附上:github地址传送门 喜欢就 star 一下呗。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值