在之前的博客中,我们曾经讨论设计过一个通用组件: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 ,照例贴上全部的自定义属性:
以下是效果图:
最后再附上:github地址传送门 喜欢就 star 一下呗。