preface

About the Fun Series

This is a scheme that has been put into experimental production by our project for nearly a year. Although it is still in the experimental stage, it has good stability and practicality.

DaVinCi warehouse link

Background: XML is widely used in Android to define resources. For a view’s background style, a number of GradientDrawable, StateListDrawable resources need to be defined. When the project is large. These resources can be difficult to manage.

Indeed, from a best practice point of view, the resources in the project should be named appropriately to meet the query index rules, the Style should be defined according to the design Style, and the Style should be used to constrain the Style when defining the view. That’s good. However, according to the current situation of domestic practitioners, most teams dealing with large projects do not have the necessary conditions to do this.

Before going into practice, let’s reflect on why this is the case, as follows:

  • Lack or frequent changes at the topDesign language.That may not be quite the right word
  • The previous page has been running online, the design of a new page cluster changed the design style, did not arrange the original content of the unified modification and give time.
  • The above two causes the full stack Style confusion
  • When there are more than three styles of Style, the development team usually choosesRuin it, I'm tired, who moves full stack style with who urgent.

OK, so if you’ve chosen destruction, why not choose a more comfortable way to deal with common background issues?

Pick a target — the most commonly used Drawable resource

After some sloppy filtering, we quickly nailed the target: selector, shape

For example, to catch a glimpse of a leopard:

This is only a small part of the project, I believe you will have such pain points in the project.

Regardless of whether the naming rule is reasonable or not, this is a very anti-human setting, like you forgot your password, apply for a reset, follow a series of password rules, finally set a password you worry about forgetting again, submit a reminder: not the same as the old password.

If we know the Drawable system, we know that the selector and shape correspond to:

  • StateListDrawable
  • GradientDrawable

If it’s not clear what the Drawable system is,Take a quick look at my previous blog post, Think Twice: Revisiting Drawable

Another way to define resources and resolve them

When Drawable resources are used in a layout file, the configuration properties are resolved after the View is created, and Drawable related resources are loaded and used by DrawableInflater.

I won’t expand on that. There is no doubt that if we were to replace the XML-based way of defining resources, we would have to adopt a new approach. However, we do not intend to abandon the use of XML files to define layout resources

And do not keep you in suspense, then search the gut, to meet:

  • Discard individual file definitions
  • Convenient. You don’t need a manual

Two elements, just two keywords to think of: DSL, OO, yes, domain specific language and object-oriented.

Strictly speaking, the two are basically mutually exclusive.

But I’m sorry, I have to stop here, I have to talk about something else, and then I’ll come back to this.

Note that a large section will be devoted to:

  • Explains how to discard custom Views and properties
  • Use Builder to simplify Drawable construction
  • Introduction of DSL
  • Use DSL to solve this problem

Personally, I think using custom grammar and interpreters to handle grammar parsing is fun and worth playing, but at my level, it can be tricky to read, and if not very interesting, I can skip to: DSL vs. OO

Get rid of custom views and properties

In the beginning, I considered this option. But using custom LayoutInflaters or hook system LayoutInflaters can affect some bad technologies

Not to mention interfering with other dark technologies, the need to deal with the combination of attributes rigorously and provide a well-developed query manual is inhumane,

Customizing Lint rules based on a dozen property combination scenarios is annoying and not fun at all

So it was rejected outright

If you want to do a good job, you must use a Drawable object Builder

Because the internals of StateListDrawable and GradientDrawable are more detailed, this is equivalent to saying that building instance objects for both is more complex.

This fits nicely into the Builder pattern usage scenario, where we first design a Builder to handle both builds and verify the information during the build

This is a rather boring thing, the code is omitted. See DaVinCiCore. Kt

After we have fully considered the various states: shape, gradient Angle, way, fill, stroke, size, specified drawable, etc., we can “easily” create drawable.

A set of DSLS that fit the scenario – define the rules

Before we expand, let’s review the basics of DSLS.

DSL is Domain Specified Language.

Wikipedia’s description of this entry:

A specialized computer language designed for a specific task.

A special computer language designed to solve a specific class of problems

Martin’s description of it, though, seems rather advanced, but I like this one better:

A computer programming language of limited expressiveness focused on a particular domain.

A computer language that inhibits expression in order to focus on a particular field.

This inhibition allows it to focus on certain areas and abandon others in order to achieve more efficient and accurate purposes.

As we know, XML protocol is very extensible, and this extensibility makes its parsing very tedious, which leads to efficiency problems. In Android, in order to make XML more extensible and efficient, Inflater types are customized to handle specific problems.

Obviously, we’re not going to step on the shoulders of the giants this time, but we’re going to step off the edge on certain issues.

To construct a GradientDrawable from our accumulated knowledge, we might use:

  • Shape of the shape
  • SolidColor fill color solidColor
  • Fillet related:
    • cornersRadius
    • cornersBottomLeftRadius
    • cornersBottomRightRadius
    • cornersTopLeftRadius
    • cornersTopRightRadius
  • Fill gradient:
    • Gradient direction Angle
    • GradientCenterX gradientCenterX
    • Y gradientCenterY
    • gradientStartColor
    • A gradientCenterColor
    • gradientEndColor
  • Gradient form
  • GradientRadius of the form RADIAL_GRADIENT
  • useLevel
  • padding
  • sizeWidth
  • sizeHeight
  • strokeWidth
  • StrokeColor strokeColor
  • Dash segment width
  • The interval between the dashed lines

Of course, we need to take into account the different states, and some of the details, which I won’t go into here

At this point we have two choice directions to make our DSL look like this:

shape:[
    gradient:[type:linear;startColor:#ff3c08;endColor:#353538 ];
    st:[Oval];
    corners:[40 dp];
    stroke:[width:4 dp;color:rc / colorAccent ]
]
Copy the code

Ps, because the target is fixed to set background, this description is ignored in the syntax

Or an INSERT statement similar to SQL.

However, the latter field is too much, it is not suitable to read, and the expression of SQL relative to the problem we want to deal with, or a little too strong.

Ok, let’s design the rules more carefully.

Terminator:

  • [ ]In which the clauses of the current field are placed, for example:
Domain: [subdomains1Shape: []] : [st: [Oval]]Copy the code
  • ;Is used when the current domain has multiple subdomains;separated

Non-terminal:

  • Shape: for creating a GradientDrawable
  • St: indicates the shape type. The enum value is
    • Rectangle
    • Oval
    • Line
    • Ring
  • Corners: Settings related to rounded corners. The values are placed in []. One value represents four corners, and four values represent the corresponding Settings of the four values: upper left, upper right, lower right and lower left
  • Solid: Solid color filling, [] inside the color value, color value expressed in the following
  • Gradient: the subcommand is placed in []
    • Type: Gradient type, enumerated as:
      • linear
      • radial
      • sweep
    • StartColor: starting color
    • CenterColor: color in the middle
    • EndColor: the end of the color
    • CenterX: point x
    • CenterY: midpoint y
    • Angle: Gradient Angle
  • [] [] [
    • Width: stroke width
    • Color: Stroke color
    • Wide dashWidth: the dotted line
    • DashGap: dashGap
  • Size: size
    • width:
    • height:
  • Padding: padding
    • left
    • top
    • right
    • bottom

Special rules:

  • Size description: Pure number represents px, value + DP represents DP value,wOn behalf ofwrap_content.mOn behalf ofmatch_parent
  • Color expression: “# FFFFFF “is a color string representing the int value of the ARGB value, “rc/ resource name” is a resource reference, and “@idName” is used to get the tag of the target View, which is either a color string or an ARGB color

To reduce the number of classes appropriately, we agree:

  • Domains that do not have subdomains are reduced to properties toProperty name: Property valueIn a way that is no longer needed[]symbol
  • If only one attribute of a domain exists or the attribute name has been specified, the attribute name can be ignored and the attribute value can be used directly

Note: When I rearranged, I found that the ShapeType and Corners in the original code were not re-corrected according to the above convention. This is a forgotten bug. To be more precise, it is a grammatical rule defect caused by the omission of terminals when subfields are reduced to attributes.

The root cause of this bug is that I wanted to reduce the number of small classes and reduce the complexity of parsing to some extent by combining the non-terminal identifier with the terminal identifier instead of using the original non-terminal identifier.

Note 2: The body GradientDrawable. Why use Shape to correspond?

This is because in most of the articles that exist widely in China, Gradient corresponds to “color Gradient” and defines this resource file as “shape”, shape with “fill” and “stroke”. The Android resource definition syntax is similar. We are all used to it, so we just respect it.

Interpreter – handles expression parsing

Among GOF’s design patterns, the Interpreter Pattern, which provides a way to evaluate a language’s syntax or expressions, is a behavioral Pattern.

Note that in the actual scenario of this problem, the frequency of a statement or clause may not be too high, but the interpreter pattern is still applicable to the scenario.

Let’s review the advantages and disadvantages of the interpreter pattern:

Advantages:

  • Good scalability, flexible.
  • Easy to implement simple grammar.

Disadvantages:

  • Difficult to maintain for complex grammars
  • May cause class inflation
  • Using recursive method, the hierarchy is too deep, and efficiency problems may occur

Defining context

Where core:DaVinCiCore is the builder mentioned above and does not follow the common naming. View :View is the view to be operated on.

Source code boring, slightly

Abstract expression

sealed class DaVinCiExpression(var daVinCi: DaVinCi? = null) {

    // Node name
    protected var tokenName: String? = null

    // The text content
    protected var text: String? = null

    // If the actual attribute needs to be parsed from the text, manually create and give the proprietary attribute, set to false, so that it will not be overridden
    protected var parseFromText = true

    abstract fun injectThenParse(daVinCi: DaVinCi?).

    /* * Execution method */
    abstract fun interpret(a)

    open fun startTag(a): String = ""

    companion object {
        @JvmStatic
        fun shape(a): Shape = Shape(true)

        const val sLogTag = "DaVinCi"

        const val END = "]"

        const val NEXT = "];"

        const val sResourceColor = "rc/"}}Copy the code

Terminal handling

You just have to deal with sibling domains, for example, we know that solid and stroke are sibling domains,

 protected class ListExpression(daVinCi: DaVinCi? = null.private val manual: Boolean = false) :
    DaVinCiExpression(daVinCi) {
    private val list: ArrayList<DaVinCiExpression> = ArrayList()

    fun append(exp: DaVinCiExpression) {
        list.add(exp)
    }

    override fun injectThenParse(daVinCi: DaVinCi?). {
        this.daVinCi = daVinCi
        if (manual) {
            list.forEach { it.injectThenParse(daVinCi) }
            return
        }

        // In the ListExpression parsing expression, loop through each word of the statement until the terminal expression or exception exitsdaVinCi? .let {var i = 0
            while (i < 100) { // true, syntax error is a bit scary, first limit 100
                if (it.currentToken == null) { // Get the current node. If null, the expression is missing
                    println("Error: The Expression Missing ']'! ")
                    break
                } else if (it.equalsWithCommand(END)) {
                    it.next()
                    // The parse ends normally
                    break
                } else if (it.equalsWithCommand(NEXT)) {
                    // Go to the next parse at the same level
                    it.next()
                } else { // Create a Command expression
                    try {
                        val expressions: DaVinCiExpression = CommandExpression(it)
                        list.add(expressions)
                    } catch (e: Exception) {
                        if (DaVinCi.enableDebugLog) Log.e(sLogTag, "Wrong grammar.", e)
                        break
                    }
                }
                i++
            }
            if (i == 100) {
                if (DaVinCi.enableDebugLog) Log.e(sLogTag, "Syntax error, enter infinite loop, force out.")}}}override fun interpret(a) { // Each expression in the loop list is interpreted and executed
        list.forEach { it.interpret() }
    }

    override fun toString(a): String {
        val b = StringBuilder()

        val iMax: Int = list.size - 1
        if (iMax == -1) return ""
        var i = 0
        while (true) {
            b.append(list[i].toString())
            if (i == iMax) return b.toString()
            b.append("; ")
            i++
        }
    }
}
Copy the code

Rule handling for non-terminals


open class CommandExpression(daVinCi: DaVinCi? = null.val manual: Boolean = false) :
    DaVinCiExpression(daVinCi) {
    private var expressions: DaVinCiExpression? = null

    init {
        // Because of the nested layer, and as a parent class, avoid recursion
        if (this: :class= =CommandExpression::class)
            onParse(daVinCi)
    }

    override fun injectThenParse(daVinCi: DaVinCi?). {
        onParse(daVinCi)
    }

    protected fun toPx(str: String, context: Context): Int? {
        / / a little
    }

    protected fun parseColor(text: String?).: Int? {
        / / a little
    }

    protected fun parseInt(text: String? , default:Int?).: Int? {
        / / a little
    }

    protected fun parseFloat(text: String? , default:Float?).: Float? {
        / / a little
    }

    protected fun getTag(context: Context? , resName:String): String? {
        / / a little
    }

    protected fun getColor(context: Context? , resName:String?).: Int? {
        / / a little
    }

    @Throws(Exception::class)
    private fun onParse(daVinCi: DaVinCi?). {
        this.daVinCi = daVinCi
        if (manual) returndaVinCi? .let { expressions =when (it.currentToken) {
                Corners.tag -> Corners(it)
                Solid.tag -> Solid(it)
                ShapeType.tag -> ShapeType(it)
                Stroke.tag -> Stroke(it)
                Size.tag -> Size(it)
                Padding.tag -> Padding(it)
                Gradient.tag -> Gradient(it)
                else -> throw Exception("cannot parse ${it.currentToken}")}}}protected fun asPrimitiveParse(start: String, daVinCi: DaVinCi?). {
        this.daVinCi = daVinCi daVinCi? .let { tokenName = it.currentToken it.next()if (start == tokenName) {
                this.text = it.currentToken
                it.next()
            } else {
                it.next()
            }
        }
    }

    override fun interpret(a){ expressions? .interpret() }override fun toString(a): String {
        return "$expressions"}}Copy the code

Take solid as an example:

class Solid(daVinCi: DaVinCi? = null, manual: Boolean = false) :
    CommandExpression(daVinCi, manual) {
    @ColorInt
    internal var colorInt: Int? = null // This is parsed, do not mess with the value

    companion object {
        const val tag = "solid:["
    }

    init {
        injectThenParse(daVinCi)
    }

    override fun injectThenParse(daVinCi: DaVinCi?). {
        this.daVinCi = daVinCi

        if (manual) {
            if (parseFromText)
                colorInt = parseColor(text)
            return
        }
        colorInt = null
        asPrimitiveParse(tag, daVinCi)
        colorInt = parseColor(text)

    }

    override fun interpret(a) {
        if(tag == tokenName || manual) { daVinCi? .let { colorInt? .let { color -> it.core.setSolidColor(color) } } } }override fun toString(a): String {
        return "$tag ${if (parseFromText) text else colorInt? .run { text }} $END"}}Copy the code

Similarly, we deal with:

  • Corners
  • ShapeType
  • Stroke
  • Size
  • Padding
  • Gradient

Can.

The most important Shape

At this point, we just need to parse shape:[] again to get the job done.

Very simple, as long as we identify, the subdomain description clause can be extracted, use; To split the clause, we only need to use ListExpression to store the clause.

The code is slightly

Note that at this point we are done defining and parsing the grammar. Note that all the bodies are GradientDrawable so far. His grammar is complicated enough.

StateListDrawable corresponds to various states that we do not extend in the grammar, otherwise the length of a single statement would be terrible.

Whether it’s DSL or OO

We talked about this earlier, to satisfy

  • Discard individual file definitions
  • Convenient. You don’t need a manual

Two elements, just two keywords to think of: DSL, OO, domain specific language, and Object-oriented.

At that point, we moved on to other topics, along with the fact that we had already implemented the core DSL solution.

We noticed that if we were using a DSL, it would be very inhumane to use the expression as a string.

We are unlikely to follow the CSS-style path of web technology

So, what we’re doing now is chicken ribs?

This is a question I cannot answer because I am not tall enough.

But that doesn’t stop us from exploring: How can we use OO ideas to make building easier

On the basis of the related classes of grammar symbols, object-oriented

In the previous work, we defined a bunch of terminal and non-terminal classes whose syntax tree structure was obtained by directly inverting a string of grammar expressions.

On the flip side, we can operate directly object-oriented, and we can build the desired syntax tree directly.

As long as you have the correct syntax tree, you can still get the desired result after execution.

With this in mind, coding is easy, and we omit the relevant source code here.

Note: At this point, it does not matter whether the syntax tree is constructed with object oriented or syntax expression strings. Both are essentially syntax trees that describe Drawable building rules, just different expressions in the two worlds

The last step is to take advantage of DataBinding and use it directly in XML

We know that with DataBinding, declarations can be implemented directly in XML

Combined with the BindingAdapter mechanism, we can achieve the goal of declaring the background.


@BindingAdapter(
    "daVinCi_bg"."daVinCi_bg_pressed"."daVinCi_bg_unpressed"."daVinCi_bg_checkable"."daVinCi_bg_uncheckable"."daVinCi_bg_checked"."daVinCi_bg_unchecked",
    requireAll = false
)
fun View.daVinCi(
    normal: DaVinCiExpression? = null,
    pressed: DaVinCiExpression? = null, unpressed: DaVinCiExpression? = null,
    checkable: DaVinCiExpression? = null, uncheckable: DaVinCiExpression? = null,
    checked: DaVinCiExpression? = null, unchecked: DaVinCiExpression? = null
) {
    val daVinCi = DaVinCi(null.this)
    // For multiple builds
    val daVinCiLoop = DaVinCi(null.this) normal? .let { daVinCi.apply { currentToken = normal.startTag() }if (DaVinCi.enableDebugLog) Log.d(sLogTag, "${this.logTag()} daVinCi normal:$normal")

        normal.injectThenParse(daVinCi)
        normal.interpret()
    }

    pressed?.let {
        simplify(daVinCiLoop, it, "pressed".this) daVinCi.core.setPressedDrawable(daVinCiLoop.core.build()) daVinCiLoop.core.clear() } unpressed? .let { simplify(daVinCiLoop, it,"unpressed".this) daVinCi.core.setUnPressedDrawable(daVinCiLoop.core.build()) daVinCiLoop.core.clear() } checkable? .let { simplify(daVinCiLoop, it,"checkable".this) daVinCi.core.setCheckableDrawable(daVinCiLoop.core.build()) daVinCiLoop.core.clear() } uncheckable? .let { simplify(daVinCiLoop, it,"uncheckable".this) daVinCi.core.setUnCheckableDrawable(daVinCiLoop.core.build()) daVinCiLoop.core.clear() } checked? .let { simplify(daVinCiLoop, it,"checked".this) daVinCi.core.setCheckedDrawable(daVinCiLoop.core.build()) daVinCiLoop.core.clear() } unchecked? .let { simplify(daVinCiLoop, it,"unchecked".this)
        daVinCi.core.setUnCheckedDrawable(daVinCiLoop.core.build())
        daVinCiLoop.core.clear()
    }


    // The following is omitted
    // private var enabledDrawable: Drawable? = null
    // private var unEnabledDrawable: Drawable? = null
    // private var selectedDrawable: Drawable? = null
    // private var focusedDrawable: Drawable? = null
    // private var focusedHovered: Drawable? = null
    // private var focusedActivated: Drawable? = null
    // private var unSelectedDrawable: Drawable? = null
    // private var unFocusedDrawable: Drawable? = null
    // private var unFocusedHovered: Drawable? = null
    // private var unFocusedActivated: Drawable? = null

    ViewCompat.setBackground(this, daVinCi.core.build())
}
Copy the code

Example:


      
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="a"
            type="String" />

        <import type="osp.leobert.android.davinci.DaVinCiExpression" />

    </data>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <LinearLayout
            daVinCi_bg="@{DaVinCiExpression.shape().solid(`#eaeaea`)}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="10dp">

            <TextView
                android:id="@+id/test"
                daVinCi_bg="@{DaVinCiExpression.shape().corner(60).solid(`@i2`).stroke(`4dp`,`@i2`)}"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_marginTop="10dp"
                android:background="@drawable/test"
                android:gravity="center"
                android:text="@string/app_name">

                <tag
                    android:id="@id/i1"
                    android:value="@color/colorPrimaryDark" />

                <tag
                    android:id="@id/i2"
                    android:value="@color/colorAccent" />
            </TextView>

            <Button
                daVinCi_bg_pressed="@{DaVinCiExpression.shape().corner(`10dp,15dp,20dp,30dp`).stroke(`4dp`,`@i2`).gradient(`#26262a`,`#ff0699`,0)}"
                daVinCi_bg_unpressed="@{DaVinCiExpression.shape().corner(60).solid(`@i1`).stroke(`4dp`,`@i2`)}"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:text="Hello World!">

                <tag
                    android:id="@id/i1"
                    android:value="@color/colorPrimaryDark" />

                <tag
                    android:id="@id/i2"
                    android:value="@color/colorAccent" />
            </Button>

            <TextView
                android:id="@+id/test2"
                daVinCi_bg="@{DaVinCiExpression.shape().corner(`10dp,15dp,20dp,30dp`).stroke(`4dp`,`@i2`).gradient(`#26262a`,`#ff0699`,0)}"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_marginTop="10dp"
                android:background="@drawable/test"
                android:gravity="center"
                android:text="@string/app_name">

                <tag
                    android:id="@id/i1"
                    android:value="@color/colorPrimaryDark" />

                <tag
                    android:id="@id/i2"
                    android:value="@color/colorAccent" />
            </TextView>

            <CheckBox
                daVinCi_bg="@{DaVinCiExpression.shape().corner(60).solid(`@i2`).stroke(`4dp`,`@i2`)}"
                daVinCi_bg_pressed="@{DaVinCiExpression.shape().corner(`10dp,15dp,20dp,30dp`).stroke(`4dp`,`@i2`).gradient(`#26262a`,`#ff0699`,0)}"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_marginTop="10dp"
                android:background="@drawable/test"
                android:gravity="center"
                android:text="Error demonstration: daVinCi_bg can only be used by itself, once there are others, you need to use the corresponding pair.">

                <tag
                    android:id="@id/i1"
                    android:value="@color/colorPrimaryDark" />

                <tag
                    android:id="@id/i2"
                    android:value="@color/colorAccent" />
            </CheckBox>

            <CheckBox
                daVinCi_bg_checked="@{DaVinCiExpression.shape().corner(60).solid(`@i2`).stroke(`4dp`,`@i2`)}"
                daVinCi_bg_unchecked="@{DaVinCiExpression.shape().corner(`10dp,15dp,20dp,30dp`).stroke(`4dp`,`@i2`).gradient(`#26262a`,`#ff0699`,0)}"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_marginTop="10dp"
                android:background="@drawable/test"
                android:gravity="center"
                android:text="Check status">

                <tag
                    android:id="@id/log_tag"
                    android:value="Test log tag" />

                <tag
                    android:id="@id/i1"
                    android:value="@color/colorPrimaryDark" />

                <tag
                    android:id="@id/i2"
                    android:value="@color/colorAccent" />

                <tag
                    android:id="@id/i3"
                    android:value="@string/app_name" />
            </CheckBox>

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

</layout>
Copy the code

Summary and Outlook

In this passage, we start from a question:

Resource files defined in XML are difficult to manage and maintain

At first, a tentative alternative to DEFINING background resource files in XML was proposed. And carried on the knowledge expansion and expansion. Finally, the desired goal was achieved.

However, there is a point to using XML files or other forms of files to define resources, although the disadvantages of this approach have long been criticized, and in emerging technologies, the location of resources and code is beginning to converge.

We know that Compose this revolutionary technology, new things to replace old things, it is not happen overnight, old things will suddenly disappear.

In this article, I see it as a fun, trendy experiment. And I personally think that this scheme is still valuable.

And on that basis, you can continue to define style and use the ColorStateList grammar expression.

Today is New Year’s Eve, I wish everyone a happy New Year’s Eve.