これは、コードを書く過程での大物の考えです。皆さんの研究に役立つことを願っています。
需要に応じてコードが変更されるプロセスについて説明しますが、既存のコードが新しい需要を満たすことができなかったためにスタックしました。Androidのソースコードを読んだ後、すぐに一時停止してリファクタリングしました。しかし、リファクタリングが完了した後、私は深く考えました...
1.単色のプログレスバー
最初の要件は、次のプログレスバーを表示することです。
これは、カスタムビューで2つの角の丸い長方形を描画することで実現できます。
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
}
2.段階的なプログレスバー
新しい要件は、グラデーションプログレスバーです。角の丸い長方形を描くときは、ブラシにグラデーションシェーダーを追加するだけです。
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)
}
}
新しいプロパティprogressColorsがProgressBarに追加されました。これは、グラデーションの色を格納するために使用される文字列配列です。次に、次のようなグラデーションプログレスバーを作成できます。
ProgressBar(context).apply {
percentage = 30
backgroundRx = 20f
backgroundRy = 20f
backgroundColor = "#e9e9e9"
progressColors = arrayOf("#ff00ff", "#00ff00")
progressRx = 15f
progressRy = 15f
padding = 2f
}
要件は満たされていますが、コードは常に少し奇妙に感じます。
元々、1つの属性を使用して「プログレスバーの色」のセマンティクスを表現していましたが、現在は2つの属性を使用して「プログレスバーの色」のセマンティクスを相互に排他的に表現しています。この相互に排他的な動作は、if-elseを介してカスタムコントロール内に実装されます。
progressPaint.shader =
if (progressColors.isEmpty()) null
else LinearGradient(0f, 0f, width * progress, height.toFloat(), _colors, null, Shader.TileMode.CLAMP)
これは間違いなくProgressBarを使用するという暗黙のルールですが、ユーザーが理解するのは難しくありません。当分の間、プログレスバーに対する新たな需要はないので、現状を維持するだけです。
3.マルチステートグラデーションカラープログレスバー
新しい反復は現状を打破しました。今回は、プログレスバーのグラデーションをプログレス値に関連付ける必要があり、その効果は次のとおりです。
プログレスバーの色は、薄緑から徐々に暗くなり、次に薄赤に変わり、最後に濃い赤に変わります。
「プログレスバーの色」を説明するProgressBarの2つの既存のプロパティでは、この新しいセマンティクス、つまり「状態のセットは色のセットに対応する」を表現できません。新しい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>
有効と無効の2つのコントロール状態がxmlで定義され、2つのドローアブルが関連付けられており、コントロールの背景として使用できます。
どうやったの?
4.ソースコードに触発された
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をDrawable mBackgroundに委任し、コントロールキャンバスキャンバスを渡します。
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;
}
...
}
}
}
ソースコードにはわからない詳細がたくさんありますが、ここから大まかに次の結論を導き出すことができます。
ビューは、背景の描画をDrawableに委任します。背景に異なるDrawableインスタンスが設定されると、背景のポリモーフィズムが実現されます。
StateListDrawableは、一連の状態と対応するDrawableインスタンスを保持する特別なDrawableです。状態が変化すると、描画する適切なDrawableが選択されます。
これらの2つの結論で十分であり、ProgressBarに適用されます。
5.プログレスバー用のスペースを増やす
まず、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には特定の進行状況描画ロジックがなく、その機能は「最初に背景を描画し、次に進行状況を前景に描画する」ように機能が低下しています。
次に、プログレスインターフェイスを実装して、プログレスバースタイルのポリモーフィズムを実現します。単色のプログレスバーは次のように定義されます。
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です。
-
各状態に対応するのは、グラデーションを描画するためにシェーダーに渡される色の値のセットです。対応する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())
)
}
Kotlinの0xARGBのタイプはLongであり、LinearGradient構築メソッドによって受信されるカラー値はint配列です。したがって、Long配列はstateListOf()でのみInt配列に変換できます。
// 遍历 Long 数组,并强转每个元素为 Int
fun Array<out Long>.toIntArray(): IntArray {
return IntArray(size) { index -> this[index].toInt() }
}
6.熟考
当初、コードのセマンティクスは「単色のプログレスバーを描画する」でした。
次に、コードのセマンティクスは「単色またはグラデーションカラーのプログレスバーを描画する」です。
リファクタリング後、コードのセマンティクスは「プログレスバーを描画する」です。
話すのと同じように、コードが具体的であるほど、スケーラビリティの余地は少なくなります。
リファクタリングはコードにスケーラビリティを追加しますが、どのくらいの価格ですか?
新しいレベルの抽象化(インターフェース)が追加され、インターフェースを実装するいくつかのクラスが追加されました。これは間違いなくコードの複雑さを増します。
導入の複雑さは、それが提供するスケーラビリティと一致していますか?
現在の反復サイクルにはこのスケーラビリティが必要ですか?
拡張性を高めると、既存のコードが壊れますか?
スケーラビリティの向上はプロジェクトのスケジュールに影響しますか?
これらの問題は私を長い間悩ませてきました。。。
スピーチと同じように、各文に余裕があれば、必然的に人は用心深くなります。どこにでもコードの余地があると、作業負荷の増加に加えて、必然的にコードを理解するためのコストが増加し、これが「誇示するように過剰に設計されている」と人々に感じさせることさえあります。
プログレスバーが新しいスタイルを繰り返す必要がなくなった場合、このリファクタリングの波は少し過剰に設計されているように見えます。
新しいスタイルのプログレスバーが到着する前にこの再構築の波が実行されなかった場合、カスタムプログレスバーは拡張可能ではないように見えます。
「流動的」論理と「不変」論理を区別し、適切な機会のための余地を残すことは、常に検討する価値のあるスキルです。
話すのと同じように、プログラミングのいくつかのことは科学ではなく、芸術のようなものです。各質問の正誤を正確に測定できる特効薬の公式はありません。
最後に、次の結論を考えてください。
時期尚早の最適化はプロジェクトにとって贅沢であり、継続的かつ段階的なリファクタリングを試す価値があります。元の設計が新しい変更に対応するのがますます困難になっても、リファクタリングするのに遅すぎることはありません。
話は安いです、コードを見せてください
https://github.com/wisdomtl/taylorCode
完全なコードは、このリポジトリのtest.taylor.com.taylorcode.ui.custom_view.progress_viewパッケージにあります。
元のアドレス:https://mp.weixin.qq.com/s/BiWd3nmdJefw5rgNTghJSA
やっと
この記事はオープンソースプロジェクトに含まれています:https://github.com/xieyuliang/Note-Androidには、さまざまな方向への自己学習プログラミングルート、インタビューの質問収集/顔の経典、および一連の技術記事が含まれています。など。リソースは継続的に更新されています。
今回はここで共有してください。次の記事でお会いしましょう。