当前位置:网站首页 > 经验分享 > 正文

原码是什么,怎么表示

讲述一个代码随需求而变的过程,曾一度因为既有代码不能满足新的需求而卡壳。在阅读了 Android 源码后茅塞顿开,立马一顿重构。但重构完成之后,我陷入了沉思。。。。

纯色进度条

最开始,需求是展示如下进度条:

用自定义 View 画两个圆角矩形就能实现:

class ProgressBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) :View(context, attrs, defStyleAttr) { // 背景色 var backgroundColor: String = "#00ff00" set(value) { field = value barPaint.color = Color.parseColor(value) } // 进度条色 var progressColor: String = "#0000ff" set(value) { field = value progressPaint.color = Color.parseColor(value) } // 内边距 var paddingStart: Float = 0f set(value) { field = value.dp } var paddingEnd: Float = 0f set(value) { field = value.dp } var paddingTop: Float = 0f set(value) { field = value.dp } var paddingBottom: Float = 0f set(value) { field = value.dp } var padding: Float = 0f set(value) { field = value.dp paddingStart = value paddingEnd = value paddingTop = value paddingBottom = value } // 背景圆角 var backgroundRx: Float = 0f set(value) { field = value.dp } var backgroundRy: Float = 0f set(value) { field = value.dp } // 进度条圆角 var progressRx: Float = 0f set(value) { field = value.dp } var progressRy: Float = 0f set(value) { field = value.dp } // 进度(0-100) var percentage: Int = 0 // 背景画笔 private var barPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor(backgroundColor) style = Paint.Style.FILL } // 进度条画笔 var progressPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor(progressColor) style = Paint.Style.FILL } // 进度条区域 var progressRectF = RectF() // 背景区域 private var backgroundRectF = RectF() override fun onDraw(canvas: Canvas?) { // 背景撑满整个控件 backgroundRectF.set(0f, 0f, width.toFloat(), height.toFloat()) // 画背景圆角矩形 canvas?.drawRoundRect(backgroundRectF, backgroundRx, backgroundRy, barPaint) // 画进度条圆角矩形 val foregroundWidth = width * percentage/100F val foregroundTop = paddingTop val foregroundRight = foregroundWidth - paddingEnd val foregroundBottom = height.toFloat() - paddingBottom val foregroundLeft = paddingStart progressRectF.set(foregroundLeft, foregroundTop, foregroundRight, foregroundBottom) canvas?.drawRoundRect(progressRectF, progressRx, progressRy, progressPaint) } } 

然后就可以像这样构建一个30%的进度条:

ProgressBar(context).apply { percentage = 30 backgroundRx = 20f backgroundRy = 20f backgroundColor = "#e9e9e9" progressColor = "#ff00ff" progressRx = 15f progressRy = 15f padding = 2f } 

渐变进度条

新的需求是渐变色的进度条。只需在绘制圆角矩形时为画笔加上渐变 Shader 即可:

class ProgressBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) :View(context, attrs, defStyleAttr) { // 背景色 var backgroundColor: String = "#00ff00" set(value) { field = value barPaint.color = Color.parseColor(value) } // 进度条色 var progressColor: String = "#0000ff" set(value) { field = value progressPaint.color = Color.parseColor(value) } // 渐变色(String数组) var progressColors = emptyArray<String>() set(value) { field = value // 将 string 色值转换成 int _colors = value.map { Color.parseColor(it) }.toIntArray() } // 渐变色(int数组) private var _colors = intArrayOf() ... override fun onDraw(canvas: Canvas?) { // 画背景圆角矩形 backgroundRectF.set(0f, 0f, width.toFloat(), height.toFloat()) canvas?.drawRoundRect(backgroundRectF, backgroundRx, backgroundRy, barPaint) // 画进度条圆角矩形 val foregroundWidth = width * percentage/100F val foregroundTop = paddingTop val foregroundRight = foregroundWidth - paddingEnd val foregroundBottom = height.toFloat() - paddingBottom val foregroundLeft = paddingStart progressRectF.set(foregroundLeft, foregroundTop, foregroundRight, foregroundBottom) // 如果没有渐变色值就用纯色背景,否则构建渐变 Shader progressPaint.shader = if (progressColors.isEmpty()) null else LinearGradient(0f, 0f, width * progress, height.toFloat(), _colors, null, Shader.TileMode.CLAMP) canvas?.drawRoundRect(progressRectF, progressRx, progressRy, progressPaint) } } 

ProgressBar新增了一个属性progressColors,它是一个 String 数组,用于存储渐变色。然后就可以像这样构建渐变色进度条:

ProgressBar(context).apply { percentage = 30 backgroundRx = 20f backgroundRy = 20f backgroundColor = "#e9e9e9" progressColors = arrayOf("#ff00ff", "#00ff00") progressRx = 15f progressRy = 15f padding = 2f } 

需求实现完了,但总感觉代码有些奇怪。

原先用一个属性表达了“进度条颜色”这个语义,现在用两个属性互斥地表达了“进度条颜色”这个语义。这种互斥行为是通过if-else在自定义控件内部实现的:

progressPaint.shader = if (progressColors.isEmpty()) null else LinearGradient(0f, 0f, width * progress, height.toFloat(), _colors, null, Shader.TileMode.CLAMP) 

这无疑是一个使用ProgressBar的潜规则,但对于使用者也不难理解。暂时也没有进度条的新需求,就先维持现状吧。

多状态渐变色进度条

一次新的迭代打破了现状。这次进度条的渐变色得和进度值关联,效果如下:

进度条颜色从浅绿色逐渐变深,然后过渡到浅红,最后深红。

ProgressBar已有的两个描述“进度条颜色”的属性都不能表达这个新的语义,即“一组状态对应于一组颜色”,难道还得新增一个Map类型的属性?

直觉告诉我这样做很不好。。。

那有什么更好的方案吗?突然想到View.setBackground(Drawable background),背景不仅可以是纯色、渐变色,还可以和一组状态联动:

<selector xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 控件有效 --> <item android:state_enable="true" android:drawable="@drawable/pic1" /> <!-- 控件无效 --> <item android:state_disable="false" android:drawable="@drawable/pic2" /> </selector> 

xml 中定义了有效和无效这两种控件状态并关联了两个 drawable,它可以作为控件的背景。

这是怎么做到的?

源码中得到启发

public class View { private Drawable mBackground;// 背景Drawable public void setBackgroundDrawable(Drawable background) { ... mBackground = background; ... } } 

调用setBackgroundDrawable()后,背景 Drawable 会被存储在mBackground变量中。这个变量什么时候会被用到?

public class View { // 绘制 View public void draw(Canvas canvas) { // 绘制背景 if (!dirtyOpaque) { drawBackground(canvas); } ... } // 绘制背景 private void drawBackground(Canvas canvas) { final Drawable background = mBackground; // 将绘制委托给 mBackground 对象 background.draw(canvas); ... } // view 状态变更 public void setEnabled(boolean enabled) { ... // 触发重绘 invalidate(true); ... } } 

当 View 状态发生变化时,会触发自身重绘,第一步绘制的是背景。但 View 好像并不关心绘制背景的具体实现,而是把它委托给了mBackground这个 Drawable,并将控件画布canvas传递给它:

public abstract class Drawable { // 在指定 Canvas 上绘制当前 Drawable public abstract void draw(@NonNull Canvas canvas); } 

Drawable.draw()是一个抽象方法,具体绘制啥交由子类实现:

// 多状态 Drawable public class StateListDrawable extends DrawableContainer {} public class DrawableContainer extends Drawable { // 当前 Drawable private Drawable mCurrDrawable; @Override public void draw(Canvas canvas) { // 绘制当前 Drawable if (mCurrDrawable != null) { mCurrDrawable.draw(canvas); } ... } 

StateListDrawable就是上面 xml 中定义的多状态 Drawable,它的绘制逻辑在父类DrawableContainer中,当draw()执行时,仅绘制当前的mCurrDrawable,它是在哪里被赋值的?

public class DrawableContainer extends Drawable { // Drawable 容器 private DrawableContainerState mDrawableContainerState; // 根据索引值选择 Drawable public boolean selectDrawable(int index) { ... // 从 Drawable 容器中根据索引值挑选 Drawable final Drawable d = mDrawableContainerState.getChild(index); // 将选中的 Drawable 赋值给 mCurrDrawable mCurrDrawable = d; } } public class StateListDrawable extends DrawableContainer { // 当 Drawable 状态变化时回调此方法 @Override protected boolean onStateChange(int[] stateSet) { ... // 挑选 Drawable return selectDrawable(idx) || changed; } } 

StateListDrawable状态发生变化时,会从DrawableContainerState中根据索引挑选一张 Drawable,它就作为下一次draw()执行时被绘制的对象。那 Drawable 是存放在什么样的容器中?

public class DrawableContainer extends Drawable { // Drawable容器 public abstract static class DrawableContainerState extends ConstantState { // Drawable数组 Drawable[] mDrawables; // 根据索引获取 Drawable public final Drawable getChild(int index) { final Drawable result = mDrawables[index]; if (result != null) { return result; } ... } } } 

虽然源码中还有超多细节不理解,但看到这里可以粗略地得出下面的结论:

  1. View 将绘制背景委托给 Drawable,当为背景设置不同 Drawable 实例时,就实现了背景的多态。
  2. StateListDrawable 是一个特殊的 Drawable,它持有一组状态和与之对应的 Drawable 实例。状态变化时,它挑选合适的 Drawable 进行绘制。

这两个结论已经够用了,将它们沿用到ProgressBar

更有余地的进度条

先模仿 Drawable,抽象出一个Progress接口:

//进度接口 interface Progress { // 绘制进度 fun draw(canvas: Canvas?, progressBar: ProgressBar) // 进度百分比变化回调(并不是每个进度实例都关心百分比变化,所以留了一个空实现) fun onPercentageChange(old: Int, new: Int) {} } 

然后让ProgressBar持有Progress实例:

class ProgressBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { // 进度实例 var progress: Progress? = null // 进度条百分比,当百分比变化时先回调接口再触发重绘 var percentage: Int by Delegates.observable(0) { _, oldValue, newValue -> progress?.onPercentageChange(oldValue, newValue) postInvalidate() } override fun onDraw(canvas: Canvas?) { // 绘制进度条背景 backgroundRectF.set(0f, 0f, width.toFloat(), height.toFloat()) canvas?.drawRoundRect(backgroundRectF, backgroundRx, backgroundRy, barPaint) // 计算进度条绘制区域 val foregroundWidth = width * percentage/100F val foregroundTop = paddingTop val foregroundRight = foregroundWidth - paddingEnd val foregroundBottom = height.toFloat() - paddingBottom val foregroundLeft = paddingStart progressRectF.set(foregroundLeft, foregroundTop, foregroundRight, foregroundBottom) // 将绘制任务委托给进度实例 progress?.draw(canvas, this) } } 

经过一层抽象,ProgressBar没有具体的进度绘制逻辑,它的功能已经退化为“先绘制背景,再绘制进度前景”。

接着通过实现Progress接口来实现进度条样式多态,纯色进度条定义如下:

class SolidColorProgress(var solidColor: String) : Progress { // 纯色画笔 private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor(solidColor) style = Paint.Style.FILL } // 绘制纯色矩形 override fun draw(canvas: Canvas?, progressBar: ProgressBar) { progressBar.run { canvas?.drawRoundRect(progressRectF, progressRx, progressRy, paint) } } } 

然后就可以像这样构建纯色进度条:

ProgressBar(context).apply { percentage = 30 backgroundColor = "#e9e9e9" progress = SolidColorProgress("#ff00ff") } 

多状态渐变进度条定义如下:

// 多状态渐变进度条(构造时需传入状态与渐变色的键值对) class StateGradientProgress(var stateMap: Map<IntRange, IntArray>) : Progress { // 当前应该绘制的渐变色值 private var currentColors: IntArray? = null private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } // 默认渐变色值 private val DEFAULT_COLORS = intArrayOf(0xFFFF00FF.toInt(), 0xFF0000FF.toInt()) override fun draw(canvas: Canvas?, progressBar: ProgressBar) { // 构建线性渐变 Shader 并绘制渐变圆角矩形 progressBar.run { paint.shader = LinearGradient( progressRectF.left, progressRectF.top, progressRectF.right, progressRectF.bottom, currentColors, null, Shader.TileMode.CLAMP ) canvas?.drawRoundRect(progressRectF, progressRx, progressRy, paint) } } // 当进度百分比变化时,选择合适的颜色值进行绘制 override fun onPercentageChange(old: Int, new: Int) { currentColors = stateMap.find { new in it.key } ?: DEFAULT_COLORS } } 

对于StateGradientProgress来说:

  • 状态是一个百分比区间,用 Int 类型表示时,它的范围是 0-100,对应的 Kotlin 类型就是IntRange
  • 与每个状态对应的是一组色值,用于传递给Shader绘制渐变,对应的 Kotlin 类型就是IntArray

当百分比发生变化时,遍历百分比色值键值对,找到百分比落在哪个区间,也就找到了对应的渐变色值。Map.find()是一个新增的扩展方法:

// 遍历键值对,当键满足条件时,返回对应的键 inline fun <K, V> Map<K, V>.find(predicate: (Map.Entry<K, V>) -> Boolean): V? { forEach { entry -> if (predicate(entry)) return entry.value } return null } 

然后就可以像这样构建多状态渐变进度条了:

ProgressBar(context).apply { backgroundColor = "#F5F5F5" progress = stateListOf( 0..19 to arrayOf(0x8000FFE5, 0x80E7FFAA), 20..59 to arrayOf(0xFF00FFE5, 0xFFE7FFAA), 60..79 to arrayOf(0xCCFE579B, 0xCCF9FF19), 80..100 to arrayOf(0xFFFE579B, 0xFFF9FF19) ) } 

其中stateListOf()是一个顶层方法,用于构建StateGradientProgress实例:

// 将一组 Pair 转换成 Map 传入 StateGradientProgress 实例 fun stateListOf(vararg states: Pair<IntRange, Array<Long>>) = StateGradientProgress( mutableMapOf<IntRange, IntArray>().apply { // 将 Long 数组转换成 Int 数组 states.forEach { state -> put(state.first, state.second.toIntArray()) } } ) 

这样做的目的是简化构建代码,否则代码就会变成这样:

ProgressBar(context).apply { backgroundColor = "#F5F5F5" progress = stateListOf( 0..19 to intArrayOf(0x8000FFE5.toInt(), 0x80E7FFAA.toInt()), 20..59 to intArrayOf(0xFF00FFE5.toInt(), 0xFFE7FFAA.toInt()), 60..79 to intArrayOf(0xCCFE579B.toInt(), 0xCCF9FF19.toInt()), 80..100 to intArrayOf(0xFFFE579B.toInt(), 0xFFF9FF19.toInt()) ) } 

0xARGB在 Kotlin 中的类型是Long,而LinearGradient的构造方法接收的色值是 int 数组。所以只能在stateListOf()中将 Long 数组转换成 Int 数组:

// 遍历 Long 数组,并强转每个元素为 Int fun Array<out Long>.toIntArray(): IntArray { return IntArray(size) { index -> this[index].toInt() } } 

沉思

最开始,代码的语义是“画一条纯色进度条”,

接下来,代码的语义是“画一条纯色或者渐变色进度条”,

重构后,代码的语义是“画一条进度条”。

就像说话一样,代码写得越具体,就越没有余地留给扩展性

重构为代码增加了扩展性,但代价是什么?

新增了一个抽象层次(接口),新增了若干个实现接口的类。这无疑增加了代码的复杂度。

引入的复杂度配得上它提供的扩展性吗?

当前的迭代周期需要这种扩展性吗?

增加扩展性会破坏既有代码吗?

增加扩展性会影响项目进度吗?

这些问题困扰了我许久。。。

就像讲话一样,如果句句留有余地,不免给人感觉谨小慎微。若代码也处处留有余地,除了工作量增加外,也不免增加了代码的理解成本,甚至可能让人觉得这是“过度设计式地卖弄”。

如果进度条需求从此不再迭代新增样式,这波重构就显得有点过度设计

如果在进度条新样式到来之前还未进行这波重构,自定义进度条就显得没有扩展性

辨别出“善变的”与“不变的”逻辑,在合适的场合留有余地,是一项值得不断斟酌的技能。

就像说话一样,编程中的有些东西不是科学,它更像艺术。没有银弹般的公式可以精准衡量每个问题的对错。

最后,想下一个抛砖引玉的结论:

过早的优化对于项目来说是奢侈的,而持续渐进的重构是值得尝试的,当原有设计越来越难应付新变化时,顺手重构一波也是不迟的。

Talk is cheap, show me the code

完整代码在这个repo中的test.taylor.com.taylorcode.ui.custom_view.progress_view包下

推荐阅读

这是读源码长知识系列的第六篇,系列文章目录如下:

  1. 读源码长知识 | 更好的RecyclerView点击监听器
  2. Android自定义控件 | 源码里有宝藏之自动换行控件
  3. Android自定义控件 | 小红点的三种实现(下)
  4. 读源码长知识 | 动态扩展类并绑定生命周期的新方式
  5. 读源码长知识 | Android卡顿真的是因为”掉帧“?
  6. 读原码长知识 | 就像讲话一样,写代码也要留有余地!?

版权声明


相关文章:

  • 数据分析师如何做好数据分析2025-08-21 10:01:05
  • Linux磁盘分区消失怎么恢复2025-08-21 10:01:05
  • frpc服务器2025-08-21 10:01:05
  • java获取hash值2025-08-21 10:01:05
  • 单点登录概念2025-08-21 10:01:05
  • php面试题目100及最佳答案2025-08-21 10:01:05
  • 找出数组中最大和最小的数字2025-08-21 10:01:05
  • 操作系统中断处理步骤2025-08-21 10:01:05
  • 表单form的基本语法2025-08-21 10:01:05
  • linux6.5防火墙关闭命令2025-08-21 10:01:05