¡El jefe de Android de Ali P7 te enseña a escribir "buen código" (pesca)!

Este es un pensamiento de un tipo grande en el proceso de escribir código, espero que sea útil para el estudio de todos.

Habla sobre el proceso de cambio de código con la demanda, y se atascó porque el código existente no pudo satisfacer la nueva demanda. Después de leer el código fuente de Android, hice una pausa y refactoré inmediatamente. Pero después de que se completó la refactorización, caí en un profundo pensamiento ...

1. Barra de progreso de color sólido

Inicialmente, el requisito es mostrar la siguiente barra de progreso:

Se puede lograr dibujando dos rectángulos redondeados con una Vista personalizada:

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)
    }
}

Entonces puedes construir una barra de progreso del 30% como esta:

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

2. Barra de progreso gradual

El nuevo requisito es una barra de progreso de degradado. Simplemente agregue un sombreador de degradado al pincel cuando dibuje un rectángulo redondeado:

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)
    }
}

Se ha agregado una nueva propiedad progressColors a ProgressBar, que es una matriz de cadenas utilizada para almacenar colores degradados. Entonces puedes construir una barra de progreso de degradado como esta:

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

Se cumplen los requisitos, pero el código siempre se siente un poco extraño.

Originalmente, se usaba un atributo para expresar la semántica del "color de la barra de progreso", pero ahora se usan dos atributos para expresar la semántica del "color de la barra de progreso" mutuamente excluyentes. Este comportamiento mutuamente excluyente se implementa dentro del control personalizado a través de if-else:

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

Sin duda, esta es una regla tácita para usar ProgressBar, pero no es difícil de entender para los usuarios. Por el momento, no hay una nueva demanda para la barra de progreso, así que mantén el status quo.

3. Barra de progreso de color degradado de varios estados

Una nueva iteración rompió el status quo. Esta vez, el gradiente de la barra de progreso tiene que estar asociado con el valor de progreso, y el efecto es el siguiente:

El color de la barra de progreso se oscurece gradualmente de verde claro, luego pasa a rojo claro y finalmente a rojo oscuro.

Las dos propiedades existentes de ProgressBar que describen el "color de la barra de progreso" no pueden expresar esta nueva semántica, es decir, "un conjunto de estados corresponde a un conjunto de colores" ¿Es necesario agregar una nueva propiedad de tipo Mapa?

Mi instinto me dice que esto es malo. . .

¿Existe alguna solución mejor? De repente, piense en View.setBackground (fondo dibujable), el fondo no solo puede ser un color sólido, un color degradado, sino que también se puede vincular con un grupo de estados:

<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>

Dos estados de control, válidos e inválidos, se definen en xml y se asocian dos elementos de diseño que se pueden utilizar como fondo del control.

¿cómo hiciste eso?

4. Inspirado en el código fuente

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

Después de llamar a setBackgroundDrawable (), el Drawable de fondo se almacenará en la variable mBackground. ¿Cuándo se utilizará esta variable?

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);
        ...
    }
}

Cuando el estado de Vista cambia, se activará automáticamente para volver a dibujar. El primer paso es dibujar el fondo. Pero a View no parece importarle la implementación específica de dibujar el fondo. En su lugar, lo delega en Drawable mBackground y le pasa el lienzo de control:

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

Drawable.draw () es un método abstracto y el dibujo específico se implementa mediante subclases:

// 多状态 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 es el Drawable multiestado definido en el xml anterior. Su lógica de dibujo está en la clase padre DrawableContainer. Cuando se ejecuta draw (), solo se dibuja el mCurrDrawable actual. ¿Dónde está asignado?

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;
    }
}

Cuando cambie el estado StateListDrawable, se seleccionará un Drawable del DrawableContainerState de acuerdo con el índice, y se utilizará como el objeto que se dibujará en la siguiente ejecución de draw (). ¿En qué tipo de contenedor se almacena 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;
            }
            ...
        }
    }
}

Aunque hay muchos detalles en el código fuente que no entiendo, puedo sacar las siguientes conclusiones de aquí:

  1. Ver delega el dibujo del fondo en Drawable. Cuando se establecen diferentes instancias de Drawable para el fondo, se realiza el polimorfismo del fondo.

  2. StateListDrawable es un Drawable especial, que contiene un conjunto de estados y las instancias Drawable correspondientes. Cuando el estado cambia, selecciona el Dibujable apropiado para dibujar.

Estas dos conclusiones son suficientes y se aplicarán a ProgressBar.

5. Más espacio para la barra de progreso

Primero imite Drawable y abstraiga una interfaz Progress:

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

Luego, deje que ProgressBar mantenga la instancia de 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)
    }
}

Después de una capa de abstracción, ProgressBar no tiene una lógica de dibujo de progreso específica, y su función se ha degradado para "dibujar primero el fondo y luego dibujar el primer plano del progreso".

Luego, realice el polimorfismo del estilo de la barra de progreso implementando la interfaz de progreso. La barra de progreso de color sólido se define de la siguiente manera:

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)
        }
    }
}

Luego, puede crear una barra de progreso de color sólido como esta:

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

La barra de progreso gradual de varios estados se define de la siguiente manera:

// 多状态渐变进度条(构造时需传入状态与渐变色的键值对)
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
    }
}

Para StateGradientProgress:

  • El estado es un intervalo de porcentaje. Cuando se expresa en el tipo Int, su rango es 0-100 y el tipo Kotlin correspondiente es IntRange.

  • Correspondiente a cada estado hay un conjunto de valores de color, que se pasan a Shader para dibujar degradados. El tipo de Kotlin correspondiente es IntArray.

Cuando cambie el porcentaje, recorra los pares clave-valor de valor de color de porcentaje para encontrar en qué intervalo se encuentra el porcentaje y, a continuación, busque el valor de color de degradado correspondiente. Map.find () es un nuevo método de extensión:

// 遍历键值对,当键满足条件时,返回对应的键
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
}

Entonces puede construir una barra de progreso de gradiente de varios estados como esta:

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)
    )
}

Entre ellos, stateListOf () es un método de nivel superior utilizado para construir una instancia de 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()) }
        }
    )

El propósito de esto es simplificar el código de compilación; de lo contrario, el código se volverá así:

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())
    )
}

El tipo de 0xARGB en Kotlin es Long y el valor de color recibido por el método de construcción LinearGradient es una matriz int. Por lo tanto, la matriz Long solo se puede convertir en una matriz Int en stateListOf ():

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

6. Contemplación

Al principio, la semántica del código era "dibujar una barra de progreso de color sólido",

A continuación, la semántica del código es "dibujar una barra de progreso de color sólido o degradado",

Después de la refactorización, la semántica del código es "dibujar una barra de progreso".

Al igual que hablar, cuanto más específico es el código, menos espacio para la escalabilidad.

La refactorización agrega escalabilidad al código, pero ¿a qué precio?

Se ha agregado un nuevo nivel de abstracción (interfaz) y se han agregado varias clases que implementan interfaces. Sin duda, esto aumenta la complejidad del código.

¿La complejidad de la introducción coincide con la escalabilidad que proporciona?

¿El ciclo de iteración actual necesita esta escalabilidad?

¿El aumento de la extensibilidad romperá el código existente?

¿El aumento de la escalabilidad afectará el cronograma del proyecto?

Estos problemas me han preocupado durante mucho tiempo. . .

Al igual que el discurso, si hay espacio para cada oración, inevitablemente hará que la gente se sienta cautelosa. Si hay espacio para el código en todas partes, además del aumento de la carga de trabajo, inevitablemente aumentará el costo de comprender el código, e incluso puede hacer que la gente sienta que está "demasiado diseñado para lucirse".

Si la barra de progreso ya no necesita nuevos estilos iterativos, esta ola de refactorización parecerá un poco sobre-diseñada.

Si esta ola de reconstrucción no se ha llevado a cabo antes de la llegada del nuevo estilo de la barra de progreso, la barra de progreso personalizada no parece ser extensible.

Es una habilidad que vale la pena considerar constantemente para distinguir entre lógica "fluxica" e "inmutable" y dejar espacio para ocasiones apropiadas.

Al igual que hablar, algunas cosas en la programación no son ciencia, es más como arte. No existe una fórmula mágica que pueda medir con precisión lo correcto o incorrecto de cada pregunta.

Finalmente, piense en la siguiente conclusión:

La optimización prematura es un lujo para el proyecto, y vale la pena intentar una refactorización continua y gradual.Cuando el diseño original se vuelve cada vez más difícil de hacer frente a los nuevos cambios, no es demasiado tarde para refactorizar.

Hablar es fácil. Enséñame el código

https://github.com/wisdomtl/taylorCode

El código completo está en el paquete test.taylor.com.taylorcode.ui.custom_view.progress_view en este repositorio

Dirección original: https://mp.weixin.qq.com/s/BiWd3nmdJefw5rgNTghJSA

Por fin

Este artículo se ha incluido en el proyecto de código abierto: https://github.com/xieyuliang/Note-Android , que contiene rutas de programación de autoaprendizaje en diferentes direcciones, colección de preguntas de entrevistas / sutras faciales y una serie de artículos técnicos, etc. Los recursos se actualizan continuamente ...

Compártelo aquí esta vez, nos vemos en el próximo artículo .

Supongo que te gusta

Origin blog.csdn.net/BUGgogogo/article/details/113308497
Recomendado
Clasificación