我们先来看看 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>
以下是效果图:
作者:xuyefeng
链接:https://www.jianshu.com/p/fdcf38516a61
共同学习,写下你的评论
评论加载中...
作者其他优质文章