Get into the habit of writing together! This is the second day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .
In daily development, the rounded corners, gradients, and borders of components such as
TextView
,Button
are generally defined by an xml file, and then implemented using shapes, such as:
<?xml version="1.0" encoding="utf-8"?>
<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>
复制代码
This notation has two disadvantages:
- Causes too many xml files in the resource path, which is not easy to manage
- XML file parsing involves io, which increases parsing time
There are also many solutions on the Internet. The core is generally constructed manually through code GradientDrawable
. This article is no exception. It mainly uses Kotlin密封类、扩展函数、+运算符重载、单链表头插法
such as to achieve the following effects:
mBinding.goMeetingBtn.shape = corner(17) +
stroke(5, "#ff0000") +
gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")
复制代码
Next, I will start to explain step by step how this writing method is encapsulated
GradientDrawable
1. Four basic operations defined by the sealed class
The shape in the xml is ultimately converted into GradientDrawable
. Usually, we use the shape mainly to realize the four operations of View background, rounded corners, gradients, and borders. We define these four operations into four states using the sealed class:
sealed class ShapeDrawable {
class Solid(val color: Any) : ShapeDrawable()
class Corner : ShapeDrawable {
var radiusArray: FloatArray? = null
var radius: Float = 0.0f
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()
}
复制代码
Among them:
Solid
: background color fill, the color to be filled is specified in the construction method
Corner
: background rounded corners, you can specify the respective angles of the four corners separately, or you can specify together
Stroke
: background border, in addition to specifying the thickness and color of the border, you can also specify the border The interval gap and background shape of the upper dotted line
GradientState
: background gradient filling, the gradient direction, start color and end color need to be passed in the construction method
2. The four basic operations are superimposed 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 achieve the superposition of operations by adding "+".
We can implement operator overloading in kotlin. The overloaded function corresponding to "+" is plus
:
sealed class ShapeDrawable {
operator fun plus(shape: ShapeDrawable): ShapeDrawable {
return shape
}
}
复制代码
Solid() + Corner()
In this way , the effect of combining the four basic operations such as
3. "+" how to save the four basic operations of overlay
This is mainly to use the singly linked list idea to combine ShapeDrawable
the specific operation implementation classes into a chain
- Define a next attribute in the sealed class
ShapeDrawable
, pointing to the nextShapeDrawable
specific operation implementationSolid
,Corner
etc.
sealed class ShapeDrawable {
var next: ShapeDrawable? = null
}
复制代码
- Implementing specific operations through the "+"
plus
operator overloading function toShapeDrawable
achieve class insertion
sealed class ShapeDrawable {
var next: ShapeDrawable? = null
operator fun plus(shape: ShapeDrawable): ShapeDrawable {
shape.next = this
return shape
}
}
复制代码
The above needs to be implemented using the head insertion method of the linked list
4. ShapeDrawable
Define GradientDrawable
abstract methods for operations on
Defining operations box
on method encapsulation pairsGradientDrawable
sealed class ShapeDrawable {
var next: ShapeDrawable? = null
abstract fun box(drawable: GradientDrawable?): GradientDrawable
}
复制代码
Concrete implementations are implemented by ShapeDrawable
concrete subclasses Solid、Stroke、GradientState、Corner
of
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.0f
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 {
//因为要构造方法传入,使用时这个需要放在第一位
return GradientDrawable(
orientation, intArrayOf(
startColor.color,
endColor.color
)
)
}
}
}
复制代码
5. Define the extension properties of View to shape
achieve 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
}
复制代码
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")
复制代码
But I found that setting the background operation needs to be called every time ShapeDrawable.Corner()
, ShapeDrawable.Stroke
etc. too much trouble
6. Define general methods to quickly realize ShapeDrawable.xxx
the creation of concrete subclasses
fun solid(color: Any): ShapeDrawable.Solid {
return ShapeDrawable.Solid(color)
}
fun corner(radius: Int): ShapeDrawable.Corner {
return ShapeDrawable.Corner(radius)
}
复制代码
Solid、Corner
Only the creation method of the class is listed here , Stroke、GradientState
similar.
Finally, we can implement the code shown at the beginning of the article:
mBinding.goMeetingBtn.shape = corner(17) +
stroke(5, "#ff0000") +
gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")
复制代码
7. Complete Code Reference
github: Manually build GradientDrawable instead of xml shape