Make writing a habit together! This is the second day of my participation in the “Gold Digging Day New Plan · April More text challenge”. Click here for more details.

In daily development, the rounded corners, gradients and borders of components such as TextView and Button are generally defined in an XML file and then implemented using Shape. For example:


      
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="7dp" />

    <solid android:color="#2196F3" />

    <stroke android:width="4dp" android:color="#ff0000"/>
</shape>
Copy the code

There are two disadvantages to this notation:

  • The number of XML files in the resource path is excessive and difficult to manage
  • XML file parsing involves IO, which increases the parsing time

There are a number of options available on the web. The core is to manually build GradientDrawable code, and this article is no exception, using Kotlin sealing classes, extension functions, + operator overloading, single-chained header interpolation, and so on:

mBinding.goMeetingBtn.shape = corner(17) +
        stroke(5."#ff0000") +
        gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00"."#0000ff")
Copy the code

So I’m going to take a step by step look at how does this notation encapsulate

1. Definition of sealing classGradientDrawableFour basic operations

GradientDrawable XML shapes are essentially converted to GradientDrawable. We use Shape to implement GradientDrawable views, rounded corners, gradients, and borders. We’ve defined these four states using sealed classes:

sealed class ShapeDrawable {
    class Solid(val color: Any) : ShapeDrawable()

    class Corner : ShapeDrawable {
        var radiusArray: FloatArray? = null
        var radius: Float = 0.0 f

        constructor(radius: Int) : super() {
            this.radius = radius.toFloat()
        }

        constructor(
            topLeftRadius: Int,
            topRightRadius: Int,
            bottomRightRidus: Int,
            bottomLeftRidus: Int
        ) 

    data class Stroke(
        val strokeWidth: Int.val dashColor: Any,
        val dashWidth: Int = 0.val dashGap: Int = 0.val shapeType: Int = GradientDrawable.RECTANGLE
    ) : ShapeDrawable() 

    data class GradientState(
        val orientation: GradientDrawable.Orientation,
        val startColor: Any,
        val endColor: Any
    ) : ShapeDrawable()
}
Copy the code

Corner: the Corner can be used to specify the angles of each of the four corners, and can also be used to specify the same Stroke: GradientState: gradient GradientState: fill the gradient direction, start color, and end color

2. Superimpose the four basic operations through the “+” sign

The four basic operations defined above can be combined with each other to set the View background, a very convenient way to add “+”. We can implement Kotlin’s overloaded operator, “+” corresponds to the overloaded function plus:

sealed class ShapeDrawable {
    operator fun plus(shape: ShapeDrawable): ShapeDrawable {
        return shape
    }
}
Copy the code

In this way, four basic operations such as Solid() + Corner() can be combined with each other

3. How does “+” save the four basic operations of superposition

This is mainly to use the single linked list idea, the ShapeDrawable concrete operation implementation classes into a chain

  • Seal typeShapeDrawableDefine a next attribute to point to the nextShapeDrawableThe specific operation is realizedSolid,Corner, etc.
sealed class ShapeDrawable {
    var next: ShapeDrawable? = null
}
Copy the code
  • Through the “+”plusOperator overload function implementationShapeDrawableThe specific operation implements class insertion
sealed class ShapeDrawable {
    var next: ShapeDrawable? = null

    operator fun plus(shape: ShapeDrawable): ShapeDrawable {
        shape.next = this
        return shape
    }
}
Copy the code

The above need to use the list of header method to achieve

4.ShapeDrawableDefinition ofGradientDrawableAbstract method of the operation of

Define a box method to encapsulate operations on GradientDrawable

sealed class ShapeDrawable {
    var next: ShapeDrawable? = null

    abstract fun box(drawable: GradientDrawable?).: GradientDrawable
}
Copy the code

The concrete implementation is implemented by the concrete subclasses Solid, Stroke, GradientState, and Corner of ShapeDrawable

sealed class ShapeDrawable {
    abstract fun box(drawable: GradientDrawable?).: GradientDrawable

    class Solid: ShapeDrawable() {
        override fun box(drawable: GradientDrawable?).: GradientDrawable { drawable!! .setColor(color.color)return drawable
        }
    }

    class Corner: ShapeDrawable {
        var radiusArray: FloatArray? = null
        var radius: Float = 0.0 f

        override fun box(drawable: GradientDrawable?).: GradientDrawable {
            if (radiusArray == null) { drawable!! .cornerRadius = radius }else{ drawable!! .cornerRadii = radiusArray }return drawable
        }
    }

    data class Stroke: ShapeDrawable() {

        override fun box(drawable: GradientDrawable?).: GradientDrawable { drawable!! .apply { setStroke( strokeWidth.dp.toInt(), dashColor.color, dashWidth.dp, dashGap.dp ) shape = shapeType }return drawable
        }
    }

    data class GradientState: ShapeDrawable() {

        override fun box(drawable: GradientDrawable?).: GradientDrawable {
            // Because the constructor is passed in, this needs to be used first
            return GradientDrawable(
                orientation, intArrayOf(
                    startColor.color,
                    endColor.color
                )
            )
        }
    }
}
Copy the code

5. Define the View’s extended propertiesshapeImplement background Settings

var View.shape: ShapeDrawable
    get() = ShapeDrawable.Empty()
    set(value) {
        var s: ShapeDrawable? = value
        val list = mutableListOf<ShapeDrawable>()
        var drawable: GradientDrawable? = null
        while(s ! =null) {
            if (s is ShapeDrawable.GradientState) {
                drawable = s.box(null)}else {
                list.add(s)
            }
            s = s.next
        }

        if (drawable == null) {
            drawable = GradientDrawable()
        }

        list.forEach {
            it.box(drawable)
        }

        background = drawable
    }
Copy the code

Through the above steps, we can achieve the following effects:

mBinding.goMeetingBtn.shape = ShapeDrawable.Corner(17) +
        ShapeDrawable.Stroke(5."#ff0000") +
        ShapeDrawable.GradientState(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00"."#0000ff")
Copy the code

But it turns out that setting the background operation every time it calls shapedrawable.corner (), shapedrawable.stroke, and so on is too cumbersome

6. Define common methods for quick implementationShapeDrawable.xxxCreation of a concrete subclass

fun solid(color: Any): ShapeDrawable.Solid {
    return ShapeDrawable.Solid(color)
}

fun corner(radius: Int): ShapeDrawable.Corner {
    return ShapeDrawable.Corner(radius)
}
Copy the code

Only the Solid and Corner classes are listed here; Stroke and GradientState are similar.

Finally, we can implement the code shown at the beginning of this article:

mBinding.goMeetingBtn.shape = corner(17) +
        stroke(5."#ff0000") +
        gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00"."#0000ff")
Copy the code

7. Complete code reference

Github: Manually build GradientDrawable instead of XML shape