_Android 项目中 shape 标签的整理和思考,面试官不讲武德
<gradientandroid:startColor="#0f000000"android:endColor="#00000000"android:angle="270"/></shape>
<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#fbfbfd" /><strokeandroid:width="1px"android:color="#dad9de" />
<cornersandroid:radius="10dp" />
</shape>
真的是不看不知道,一看吓一跳。原来我们项目中大量存在的 shape 文件其实都是大同小异的,涉及到最常见的 shape 变化:圆角,描边,填充以及渐变。 进一步分析,我们又发现:
有些时候填充颜色是相同的,只不过圆角半径不同,我们就得新增一个 shape 文件。
有些时候圆角半径是相同的,只不过填充颜色不同,我们又得新增一个 shape 文件。
有些时候两个负责不同业务模块的同事,各自新增一个同样样式的 shape 文件。
等等一些情况,让我们陷入了 shape 文件的无限新增与维护中。我们不禁要思考,有没有办法可以把这些 shape 统一起来管理呢?xml 书写出来的代码最终不都是会对应一个内存中的对象吗?我们能不能从管理 shape 文件过度到管理一个对象呢?
Talk is cheap. Show me the code
第一步,我们需要确定 shape 标签对应的类到底是哪一个?第一反应就是 ShapeDrawable,顾名思义嘛。然后残酷的事实告诉我们其实是 GradientDrawable 这兄弟。浏览 GradientDrawable 类的方法结构,从中我们也找到了 setColor()、setCornerRadius()、setStroke() 等目标方法。好吧,不管怎样,先找到正主了。
第二步,继续思考如何来设计这个通用控件,主要从以下几个方面进行了考虑:
shape 的应用场景有可能是文字标签,也有可能是响应按钮,所以需要文本和按钮两种样式,两者的主要区别在于按钮样式在普通状态下和按压状态下都具有阴影。
为了提升用户体验,设计了通用控件的按压动效。针对 5.0 以上的用户开启按压水波纹效果,针对 5.0 以下的用户开启按压变色效果。 结合以上两点,通用控件的实现考虑直接继承 AppCompatButton 进行扩展。
具体的业务场景中,通用控件的使用还有可能伴随着 drawable,并且要求 drawable 和文字一起居中显示。其实这个问题本来是不需要单独考虑的,但是 Android 有个坑,在一个按钮控件中设置 drawable 以后,默认是贴着控件边缘显示的,所以这个坑需要单独填。
自定义控件属性支持 shape 模式、填充颜色、按压颜色、描边颜色、描边宽度、圆角半径、按压动效是否开启、渐变开始颜色、渐变结束颜色、渐变方向、drawable 方位。
第三步,思路已经梳理清楚了,那就开撸。
class CommonShapeButton @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0) : AppCompatButton(context, attrs, defStyleAttr) {
这里实现了继承 AppCompatButton 进行扩展,默认样式 defStyleAttr 传递的是 0,那么 CommonShapeButton 的默认表现形式就是文本样式。
如果想要采用按钮样式,则需要先自定义一个按钮样式,原因是系统按钮的样式自带了 minWidth、minHeight 以及 padding,在具体业务中会影响到我们的按钮显示,所以在自定义按钮样式中重置了这三个属性:
有了自定义按钮样式,那么想要 CommonShapeButton 采用按钮样式,则采用如下形式:
<com.blue.view.CommonShapeButtonstyle="@style/CommonShapeButtonStyle"android:layout_width="300dp"android:layout_height="50dp"/>
到这里就可以实现简单的文本样式和按钮样式的切换了。 接下来我们就要进行关键的 shape 渲染了:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)// 初始化 normal 状态 with(normalGradientDrawable) {// 渐变色 if (mStartColor != Color.parseColor("#FFFFFF") && mEndColor != Color.parseColor("#FFFFFF")) {colors = intArrayOf(mStartColor, mEndColor)when (mOrientation) {0 -> orientation = GradientDrawable.Orientation.TOP_BOTTOM1 -> orientation = GradientDrawable.Orientation.LEFT_RIGHT}}// 填充色 else {setColor(mFillColor)}when (mShapeMode) {0 -> shape = GradientDrawable.RECTANGLE1 -> shape = GradientDrawable.OVAL2 -> shape = GradientDrawable.LINE3 -> shape = GradientDrawable.RING}cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mCornerRadius.toFloat(), resources.displayMetrics)// 默认的透明边框不绘制,否则会导致没有阴影 if (mStrokeColor != Color.parseColor("#00000000")) {setStroke(mStrokeWidth, mStrokeColor)}}
// 是否开启点击动效 background = if (mActiveEnable) {// 5.0 以上水波纹效果 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {RippleDrawable(ColorStateList.valueOf(mPressedColor), normalGradientDrawable, null)}// 5.0 以下变色效果 else {// 初始化 pressed 状态 with(pressedGradientDrawable) {setColor(mPressedColor)when (mShapeMode) {0 -> shape = GradientDrawable.RECTANGLE1 -> shape = GradientDrawable.OVAL2 -> shape = GradientDrawable.LINE3 -> shape = GradientDrawable.RING}cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mCornerRadius.toFloat(), resources.displayMetrics)setStroke(mStrokeWidth, mStrokeColor)}
// 注意此处的 add 顺序,normal 必须在最后一个,否则其他状态无效// 设置 pressed 状态 stateListDrawable.apply {addState(intArrayOf(android.R.attr.state_pressed), pressedGradientDrawable)// 设置 normal 状态 addState(intArrayOf(), normalGradientDrawable)}}} else {normalGradientDrawable}}
这里的代码有点长,别着急,我们来慢慢分析一下:
首先是选择在 onMeasure 方法中
做 shape 渲染
其次对 normarlGradientDrawable 设置当前是渐变色渲染还是填充色渲染,渐变色渲染还需要单独控制渲染的方向
然后对 normarlGradientDrawable 设置 shape 模式、圆角以及描边
最后对 CommonShapeButton 设置 background。如果没有开启点击特效,则直接返回 normarlGradientDrawable。如果开启了点击特效,那么 5.0 以上启用水波纹效果,5.0 以下启用变色效果。在变色效果的设置中同样初始化了 pressedGradientDrawable 的 shape 属性,并且依次添加进了 stateListDrawable 用作背景显示
到这里就可以实现了用自定义属性控制 shape 渲染显示 CommonShapeButton 的背景了,这里贴上全部的属性:
<declare-styleable name="CommonShapeButton"><attr name="csb_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="csb_fillColor" format="color" /><attr name="csb_pressedColor" format="color" /><attr name="csb_strokeColor" format="color" /><attr name="csb_strokeWidth" format="dimension" /><attr name="csb_cornerRadius" format="dimension" /><attr name="csb_activeEnable" format="boolean" /><attr name="csb_drawablePosition" format="enum"><enum name="left" value="0" /><enum name="top" value="1" /><enum name="right" value="2" /><enum name="bottom" value="3" /></attr><attr name="csb_startColor" format="color" />
评论