美文网首页我爱编程
Android 项目中 shape 标签的整理和思考(2)

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

作者: xuyefeng | 来源:发表于2018-08-08 15:41 被阅读737次

    在之前的博客中,我们曾经讨论设计过一个通用组件: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>
    

    以下是效果图:

    image

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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/zhaavftx.html