_Android 项目中 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.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
}
}

这里的代码有点长,别着急,我们来慢慢分析一下:

  • 首先是选择在 onMeasure 方法中做shape渲染
  • 其次对 normarlGradientDrawable 设置当前是渐变色渲染还是填充色渲染,渐变色渲染还需要单独控制渲染的方向
  • 然后对 normarlGradientDrawable 设置 shape 模式、圆角以及描边
  • 最后对CommonShapeButton设置background。如果没有开启点击特效,则直接返回normarlGradientDrawable。如果开启了点击特效,那么 5.0 以上启用水波纹效果,5.0 以下启用变色效果。在变色效果的设置中同样初始化了 pressedGradientDrawable 的 shape 属性,并且依次添加进了 stateListDrawable 用作背景显示

到这里就可以实现了用自定义属性控制shape渲染显示 CommonShapeButton 的背景了,这里贴上全部的属性:

接下来我们还需要进行最后的工作
,解决在一个 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
}

我们继续来分析这里的代码:

  • 首先渲染的效率,我们选择在 onLayout 方法中计算一些数值
  • 其次由于我们是支持上下左右四个方向的 drawable,所以需要在 xml 中指定属性 drawablePosition
  • 然后判断是否设置了 drawable 并且 drawable 获取不为空
  • 然后判断 drawable 左右方位,则计算图片的宽度和文字的宽度,然后根据内容的总宽度把 button 的内容全部贴左边缘显示
  • 最后判断 drawable 在上下方位,则计算图片的高度和文字的高度,然后根据内容的总高度把 button 的内容全部贴上边缘显示

到这里就做好了让 drawable 居中显示的准备工作,我们继续往下走:

override fun onDraw(canvas: Canvas) {
// 让图片和文字居中
when {

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

如果你需要这些资料, ⬅ 专栏获取
43)]

[外链图片转存中…(img-BbiCzsFR-1719273277643)]

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

如果你需要这些资料, ⬅ 专栏获取

  • 25
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值