做者:咕咚移动技术团队-Bluephp
在 Android 开发中,使用 shape 标签能够很方便的帮咱们构建资源文件,跟传统的 png 图片相比:android
关于 shape 标签如何使用,在网上一搜一大把,笔者就不在这里赘述了,今天咱们要讨论的是 shape 标签泛滥成灾之后带来的后果。这里先给你们看一个维护超过了 5 年的项目的 drawable 目录 git
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#66000000" />
<corners android:radius="15dp" />
</shape>
复制代码
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android: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" />
<stroke android:width="1px" android:color="#dad9de" />
<corners android:radius="10dp" />
</shape>
复制代码
真的是不看不知道,一看吓一跳。原来咱们项目中大量存在的 shape 文件其实都是大同小异的,涉及到最多见的 shape 变化:圆角,描边,填充以及渐变。 进一步分析,咱们又发现:github
等等一些状况,让咱们陷入了 shape 文件的无限新增与维护中。咱们不由要思考,有没有办法能够把这些 shape 统一块儿来管理呢?xml 书写出来的代码最终不都是会对应一个内存中的对象吗?咱们能不能从管理 shape 文件过分到管理一个对象呢?canvas
Talk is cheap. Show me the codeapp
第一步,咱们须要肯定 shape 标签对应的类究竟是哪个?第一反应就是 ShapeDrawable,顾名思义嘛。而后残酷的事实告诉咱们实际上是 GradientDrawable 这兄弟。浏览 GradientDrawable 类的方法结构,从中咱们也找到了setColor()、setCornerRadius()、setStroke() 等目标方法。好吧,无论怎样,先找到正主了。ide
第二步,继续思考如何来设计这个通用控件,主要从如下几个方面进行了考虑:ui
第三步,思路已经梳理清楚了,那就开撸。spa
class CommonShapeButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatButton(context, attrs, defStyleAttr) {
复制代码
这里实现了继承 AppCompatButton 进行扩展,默认样式 defStyleAttr 传递的是 0,那么 CommonShapeButton 的默认表现形式就是文本样式。设计
若是想要采用按钮样式,则须要先自定义一个按钮样式,缘由是系统按钮的样式自带了 minWidth、minHeight 以及 padding,在具体业务中会影响到咱们的按钮显示,因此在自定义按钮样式中重置了这三个属性:
<!-- 自定义按钮样式 -->
<style name="CommonShapeButtonStyle" parent="@style/Widget.AppCompat.Button"> <item name="android:minWidth">0dp</item> <item name="android:minHeight">0dp</item> <item name="android:padding">0dp</item> </style>
复制代码
有了自定义按钮样式,那么想要 CommonShapeButton 采用按钮样式,则采用以下形式:
<com.blue.view.CommonShapeButton style="@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_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
}
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.RECTANGLE
1 -> shape = GradientDrawable.OVAL
2 -> shape = GradientDrawable.LINE
3 -> 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
}
}
复制代码
这里的代码有点长,别着急,咱们来慢慢分析一下:
到这里就能够实现了用自定义属性控制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" />
<attr name="csb_endColor" format="color" />
<attr name="csb_orientation" format="enum">
<enum name="TOP_BOTTOM" value="0" />
<enum name="LEFT_RIGHT" value="1" />
</attr>
</declare-styleable>
复制代码
接下来咱们还须要进行最后的工做,解决在一个 button 中添加 drawable 不居中显示的问题
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// 若是xml中配置了drawable则设置padding让文字移动到边缘与drawable靠在一块儿
// button中配置的drawable默认贴着边缘
if (mDrawablePosition > -1) {
compoundDrawables?.let {
val drawable: Drawable? = compoundDrawables[mDrawablePosition]
drawable?.let {
// 图片间距
val drawablePadding = compoundDrawablePadding
when (mDrawablePosition) {
// 左右drawable
0, 2 -> {
// 图片宽度
val drawableWidth = it.intrinsicWidth
// 获取文字宽度
val textWidth = paint.measureText(text.toString())
// 内容总宽度
contentWidth = textWidth + drawableWidth + drawablePadding
val rightPadding = (width - contentWidth).toInt()
// 图片和文字所有靠在左侧
setPadding(0, 0, rightPadding, 0)
}
// 上下drawable
1, 3 -> {
// 图片高度
val drawableHeight = it.intrinsicHeight
// 获取文字高度
val fm = paint.fontMetrics
// 单行高度
val singeLineHeight = Math.ceil(fm.descent.toDouble() - fm.ascent.toDouble()).toFloat()
// 总的行间距
val totalLineSpaceHeight = (lineCount - 1) * lineSpacingExtra
val textHeight = singeLineHeight * lineCount + totalLineSpaceHeight
// 内容总高度
contentHeight = textHeight + drawableHeight + drawablePadding
// 图片和文字所有靠在上侧
val bottomPadding = (height - contentHeight).toInt()
setPadding(0, 0, 0, bottomPadding)
}
}
}
}
}
// 内容居中
gravity = Gravity.CENTER
// 可点击
isClickable = true
}
复制代码
咱们继续来分析这里的代码:
到这里就作好了让 drawable 居中显示的准备工做,咱们继续往下走:
override fun onDraw(canvas: Canvas) {
// 让图片和文字居中
when {
contentWidth > 0 && (mDrawablePosition == 0 || mDrawablePosition == 2) -> canvas.translate((width - contentWidth) / 2, 0f)
contentHeight > 0 && (mDrawablePosition == 1 || mDrawablePosition == 3) -> canvas.translate(0f, (height - contentHeight) / 2)
}
super.onDraw(canvas)
}
复制代码
接下来咱们就是在 onDraw 方法中,利用在 onLayout 方法中计算的数值,平移 button 的内容,从而实现让 drawable 和文字一块儿居中显示。
到这里咱们就完成了 CommonShapeButton 的所有设计和实现,如下是效果图: