This is the second part of a series on “killing XML”. The last part is to kill the layout file in the res/ Layout directory. This part is to kill the shape configuration file in the Res/Drawable directory.

XML resource files in Android decouple static configuration from dynamic code for centralized management. But it can be a performance drag, not only by increasing package size, but also by consuming I/O to read XML.

The actual project has 650+ layout files (2.9 MB) and 1000+ Drawable non-image files (700 KB), if these files can be destroyed, it can also contribute to the shrink package.

The “Kill XML” series of files directory is as follows:

  1. Android performance optimization | to shorten the building layout is 20 times (below)

  2. No longer do kill XML | write XML for various shapes

Configure shapes statically with XML

How many

files need to be defined for the following interface?

The answer is six. Most irritating of all, the contents of the six files are almost identical:


      
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="15dp" />
    <solid android:color="#FFEBF1" />
</shape>
Copy the code

The only difference is the color and the rounded corners. The awkward thing about the

file is that it can’t be reused.

Build shapes dynamically with code

The Java class for the

tag is a GradientDrawable that can be dynamically built with code:

// Build the GradientDrawable object and set the properties
val drawable = GradientDrawable().apply {
    shape = GradientDrawable.RECTANGLE / / rectangle
    cornerRadius = 10f / / the rounded
    colors = intArrayOf(Color.parseColor("#ff00ff"),Color.parseColor("#800000ff")) / / gradients
    gradientType = GradientDrawable.LINEAR_GRADIENT // The gradient type
    orientation = GradientDrawable.Orientation.LEFT_RIGHT // Gradient direction
    setStroke(2.dp,Color.parseColor("#ffff00")) // Stroke width and color
}
// Set the GradientDrawable object as the control backgroundfindViewById<Text>(R.id.tvTitle)? .background = drawableCopy the code

With the help of apply(), this code is still fairly straightforward. You can do better with the blessing of DSL.

Building a layout DSL

Review the dynamically built layout DSL from the previous article:

class MainActivity : AppCompatActivity() {
	// Dynamically build the layout with DSL
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // Set the rounded gradient shape for TextView
                background_res = R.drawable.shape
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // Set the layout to content View
        setContentView(contentView)
    }
}
Copy the code

The DSL moves the layout that was originally defined in XML into the Activity, eliminating the need for I/O to read a single XML file.

Each attribute assigned in the Build layout DSL is a predefined extended attribute, such as layout_width:

// Extend a property of type Int for the View
inline var View.layout_width: Int 
    get() {
        return 0
    }
    // When the extended property is assigned, convert it to MarginLayoutParams and set it to the layout parameter of the control
    set(value) {
        val w = if (value > 0) value.dp else value
        valh = layoutParams? .height ? :0
        layoutParams = ViewGroup.MarginLayoutParams(w, h)
    }

// Extend the property for an Int value, converting it to a DP value
val Int.dp: Int
    get() {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            this.toFloat(),
            Resources.getSystem().displayMetrics
        ).toInt()
    }
Copy the code

A detailed explanation of dynamically building a layout DSL can be found here

New shape properties

Use the same idea to add a “shape property” to the View to hide (or increase the readability of the build code) the details of the GradientDrawable build:

inline var View.shape: GradientDrawable
    get() {
        return GradientDrawable()
    }
    set(value) {
        background = value
    }
Copy the code

Add a shape property to the View, which is of type GradientDrawable.

inline fun shape(init: GradientDrawable. () - >Unit) = GradientDrawable().apply(init)
Copy the code

Add a top-level method for building an instance GradientDrawable.

Why add another method to do the same thing when a direct GradientDrawable() will build the instance?

The magic of this method is that it takes a lambda with a receiver, the GradientDrawable.() -> Unit. This hides the details of the “build” and “set value” actions and allows the build to be done in declarative statements:

class MainActivity : AppCompatActivity() {
	// Dynamically build the layout with DSL
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // Set the shape for the TextView
                shape = shape {
                    shape = GradientDrawable.RECTANGLE
                    cornerRadius = 10f
                    colors = intArrayOf(Color.parseColor("#ff00ff"),Color.parseColor("#800000ff")) 
                    gradientType = GradientDrawable.LINEAR_GRADIENT 
                    orientation = GradientDrawable.Orientation.LEFT_RIGHT 
                    setStroke(2.dp,Color.parseColor("#ffff00")}}}}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // Set the layout to content View
        setContentView(contentView)
    }
}
Copy the code

Convert a static resource referenced through R.drable.shpae into a dynamic build.

The above code has already killed the layout and shape files, but the process of setting values for shapes is a bit verbose and can be simplified by pre-defining extended properties:

// Extend the fillet radius attribute for GradientDrawable
inline var GradientDrawable.corner_radius: Int
    get() {
        return -1
    }
    set(value) {
    	// Convert the radius value to dp value
        cornerRadius = value.dp.toFloat()
    }

// Extend the gradient attribute for GradientDrawable
inline var GradientDrawable.gradient_colors: List<String>
    get() {
        return emptyList()
    }
    set(value) {
    	// Convert string to color Int value
        colors = value.map { Color.parseColor(it) }.toIntArray()
    }

// Extend the stroke property for GradientDrawable
inline var GradientDrawable.strokeAttr: Stroke?
    get() {
        return null
    }
    set(value) {
    	// Disassemble the stroke data entity class and pass it to setStroke()value? .apply { setStroke(width.dp, Color.parseColor(color), dashWidth.dp, dashGap.dp) } }// Stroke data entity class
data class Stroke(
    var width: Int = 0.var color: String = "# 000000".var dashWidth: Float = 0f.var dashGap: Float = 0f
)

// Give the constant a shorter alias to increase readability
val shape_rectangle = GradientDrawable.RECTANGLE
val gradient_type_linear = GradientDrawable.LINEAR_GRADIENT
val gradient_left_right = GradientDrawable.Orientation.LEFT_RIGHT
Copy the code

Use these attributes to further simplify the build process:

class MainActivity : AppCompatActivity() {
	// Dynamically build the layout with DSL
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // Set the shape for the TextView
                shape = shape {
                    corner_radius = 10
                    shape = shape_rectangle
                    gradientType = gradient_type_linear
                    orientation = gradient_left_right
                    gradient_colors = listOf("#ff00ff"."#800000ff")
                    strokeAttr = Stroke(2."#ffff00")}}}}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // Set the layout to content View
        setContentView(contentView)
    }
}
Copy the code

Added color state list attribute

In the actual business scenario, the background color of the button usually has several states. At first, I control the change of the background color by using this code:

val tv = findViewById<TextView>(R.id.tv)

fun onResponse(success: Boolean){
	if (success) {
        tv.bacground = R.drawable.success
    } else {
        tv.background = R.drawable.fail
    }
}
Copy the code

In this way, the semantics are simple and clear, but the disadvantage is that ** “the control logic of the single property of the control is scattered everywhere” **.

Suppose that there are two network requests and one interface button that can affect the background color of the control, then Tv.background = R.drawable. XXX will be scattered among the two network requests and one control click event callback.

This increases the complexity of later maintenance, because “What background color does this control render under different conditions?” This knowledge is torn apart and scattered in different places, and you have to find all the pieces and put them together in order to know the details of the story so that you can modify it.

You must have experienced such an experience, just simply want to change the value of a property of a control, but it is not successful, because n other places are also changing it… This kind of hidden plot makes people crazy.)

Then I learned

:


      
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/bg_red" android:state_enabled="true" />
    <item android:drawable="@drawable/bg_blue" android:state_enabled="false" />
</selector>
Copy the code

< Selector > The colors corresponding to different states are configured in advance and stored in a file, so that the logic of changing the control background color can be clear at a moment, and then the control state can be changed in different business scenarios:

val tv = findViewById<TextView>(R.id.tv)
tv.background = R.drawable.selector

fun onResponse(success: Boolean){
	if (success) {
    	tv.enable = true
    } else {
    	tv.enbale = false}}Copy the code

Is it also possible to dynamically build the

configuration file?

// Extend the color state list attribute for GradientDrawable
inline var GradientDrawable.color_state_list: List<Pair<IntArray, String>>
    get() {
        return listOf(intArrayOf() to "# 000000")}@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    set(value) {
    	// The status list
        val states = mutableListOf<IntArray>()
        // Color list
        val colors = mutableListOf<Int> ()// Convert the property values to a list of states and a list of colors, respectively
        value.forEach { pair ->
            states.add(pair.first)
            colors.add(Color.parseColor(pair.second))
        }
        // Create a ColorStateList object and call setColor(ColorStateList ColorStateList)
        color = ColorStateList(states.toTypedArray(), colors.toIntArray())
    }
    
// Give the constant a shorter alias to increase readability
val state_enable = android.R.attr.state_enabled
val state_disable = -android.R.attr.state_enabled
val state_pressed = android.R.attr.state_pressed
val state_unpressed = -android.R.attr.state_pressed
Copy the code

You can then build a list of color states for the TextView at the same time as building it like this:

class MainActivity : AppCompatActivity() {
	// Dynamically build the layout with DSL
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // Set the shape for the TextView
                shape = shape {
                    corner_radius = 10
                    shape = shape_rectangle
                    gradientType = gradient_type_linear
                    orientation = gradient_left_right
                    gradient_colors = listOf("#ff00ff"."#800000ff")
                    strokeAttr = Stroke(2."#ffff00")
                    // Build a color state list for the shape
                    color_state_list = listOf(
                        // Display a color in the Enable and Pressed states
                        intArrayOf(state_enable, state_pressed) to "#007EFF".// Display another color in the disable and unpressed states
                        intArrayOf(state_disable, state_unpressed) to "#FDB2DA")}}}}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // Set the layout to content View
        setContentView(contentView)
    }
}
Copy the code

Added the Drawable status list attribute

Binding color and state can only meet a small fraction of the requirements, and most of the time you need to use a more varied form of drawable and state binding. The Java class corresponding to the < Selector > is StateListDrawable, so we add the drawable state list property to the control:

inline var View.background_drawable_state_list: List<Pair<IntArray, Drawable>>
    get() {
        return listOf(intArrayOf() to GradientDrawable())
    }
    set(value) {
    	// Build a StateListDrawable instance and convert the attribute values into states
        background = StateListDrawable().apply {
            value.forEach { pair ->
            	// Add a relationship between StateListDrawable and Drawable instances
                addState(pair.first, pair.second)
            }
        }
    }
Copy the code

Previously, in order for a button to have a different background when it is clickable and when it is not, you need to define three XML files: selectors. XML + shape_clickable. XML + shape_unclickable.

class MainActivity : AppCompatActivity() {
	// Dynamically build the layout with DSL
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // Set the drawable status list for TextView
                background_drawable_state_list = listOf(
                	Clickable state = rectangle with rounded corners
                    intArrayOf(state_enable) to shape {
                        shape = shape_rectangle
                        corner_radius = 10
                        solid_color = "#FDB2DA"
                    },
                    // Non-clickable state = rounded rectangle with opacity
                    intArrayOf(state_disable) to shape {
                        shape = shape_rectangle
                        corner_radius = 10
                        solid_color = "#80FDB2DA"})}}}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // Set the layout to content View
        setContentView(contentView)
    }
}
Copy the code

Talk is cheap, show me the code

Recommended reading

  • Kotlin base | entrusted and its application
  • Kotlin basic grammar | refused to noise
  • Kotlin advanced | not variant, covariant and inverter
  • Kotlin combat | after a year, with Kotlin refactoring a custom controls
  • Kotlin combat | kill shape with syntactic sugar XML file
  • Kotlin base | literal-minded Kotlin set operations
  • Kotlin source | magic weapon to reduce the complexity of code
  • Why Kotlin coroutines | CoroutineContext designed indexed set? (a)
  • Kotlin advanced | the use of asynchronous data stream Flow scenarios