[SML|Gradle|Android] 通過gradle plugin 自動生成資源文件(drawable哩盲,color前方,dimen。廉油。镣丑。)
本人所有文章禁止任何形式的轉載,謝謝
前言
在一個app 中娱两,UI 通常會給出不同的設計莺匠,比如Ok 按鈕和Cancel 的樣式是不太一樣的,每出現(xiàn)一個新的樣式十兢,我們就需要創(chuàng)建一個drawable 文件趣竣。這都還好摇庙,關鍵是這個drawable 文件有相當多的重復性內(nèi)容,也就是說完全沒有重用遥缕。像style 文件還好卫袒,不同的style 之間還可以繼承。
所以如果能夠給出一個自動生成這些文件的工具會更好单匣。
從標題就能夠在以后我們要怎么做的了夕凝。但是我還是說一下現(xiàn)階段可以選擇的工具
現(xiàn)有的解決方案
- 通過注解生成
android.graphics.drawable.Drawable
(或者完全通過手動調(diào)用代碼)。然后在代碼中使用户秤。 - 通過繼承現(xiàn)有的View码秉。具體使用時還有點區(qū)別,一是通過Provider 自動替換,就像 我們明明用的是
android.widget.View
鸡号, 但是實際展示出來的卻是androidx.appcompat.widget
转砖。二是在layout 布局中直接使用這些特殊的View。后一個的好處是可以預覽到“drawable”的效果鲸伴。 - 通過data binding府蔗,在xml 中使用代碼,插入
android.graphics.drawable.Drawable
汞窗。(我現(xiàn)階段使用的) - 使用
Material
姓赤,和方案2 類似痛黎,但是有些效果可能沒有兢哭。 - 使用jetpack compose(可能是你最應該選擇的)
現(xiàn)有的解決方案有一個問題就是無法預覽到效果。雖然預覽效果不是什么必須的贤壁,但我還是想要搞出來一個擁有預覽能力的解決方案蜘矢。
思考
很顯然狂男,自動生成代碼嗎,注解品腹,gradle plugin岖食,外部工具。
注解的方案還沒嘗試過舞吭,應該也是可以的泡垃,但是這類代碼不容易寫成控制羡鸥、循環(huán)的代碼蔑穴,而且注解的參數(shù)有也嚴格的要求,比如不能使用可變的參數(shù)必須得是常量(在kotlin 中更為煩人存和,只能使用KClass
,而不能使用Class
)藏澳。
我還是選擇的是gradle plugin。不過gradle plugin 和外部工具其實非常類似肘交,只是說外部工具有點“脫離生態(tài)的感覺“笆载,而且需要”一點點配置“扑馁,比如使用者可以把這個工具放到任意的什么目錄涯呻。
關于通過gradle plugin 生成代碼,有很多博客腻要,可以對照著看复罐。
https://juejin.cn/post/6887581345384497165
https://medium.com/@magicbluepenguin/how-to-create-your-first-custom-gradle-plugin-efc1333d4419
接下來就要展示如果完成這個工具。
哎呦雄家,還沒有給這個插件起個名字呢效诅!其實名字就在標題里SML
,其實就是仿照Sass
把xml
改的趟济。如果你愿意可以叫他“斯麥魯乱投,斯麥魯”??
開始
項目使用kotlin 以及kotlin dsl 編寫,所以需要有一定的前置知識顷编。
-
創(chuàng)建一個模塊戚炫。也可以不進行創(chuàng)建,把代碼放到buildSrc中媳纬,但是我都已經(jīng)給它起了個名字了??
模塊就是一個普通的kotlin library 即可双肤。
plugins { id 'org.jetbrains.kotlin.jvm' id 'java-gradle-plugin' id 'maven-publish' }
因為代碼不是在buildSrc 中,所以要應該這個插件钮惠,需要通過maven publish
gradlePlugin { plugins { // 聲明插件信息茅糜,這里的 hello 名字隨意 hello { version('0.0.1') // 插件ID id = 'com.storyteller_f.sml' // 插件的實現(xiàn)類 implementationClass = 'com.storyteller_f.sml.Sml' } } } publishing { repositories { maven { // $rootDir 表示你項目的根目錄 url = "$rootDir/repo" } } }
-
定義一個任務
internal open class ColorTask : DefaultTask() { @get:OutputFile lateinit var outputFile: File @get:Input lateinit var colorsMap: MutableMap<String, String> @TaskAction fun makeResources() { colorsMap.entries.joinToString { (colorName, color) -> "\n <color name=\"$colorName\">$color</color>" }.also { xml -> outputFile.writeXlmWithTags(xml) } } }
outputFile.writeXlmWithTags(xml)
是一個擴展函數(shù),就是把拼接好的內(nèi)容存儲到文件中素挽。關于注解
OutputFile
和Input
用來給gradle 進行增量更新判斷使用蔑赘。如果輸入輸出都沒有發(fā)生變化,這個任務都會跳過。加上很有必要缩赛。TaskAction
也是必須的锌历,很顯然這個函數(shù)不是繼承自DefaultTask
的,而且這個抽象類中也沒有什么函數(shù)要繼承峦筒。只有加了這個注解究西,gradle 才會運行這個函數(shù)。代碼并不是必須要這么寫物喷,只要邏輯沒問題應該就好卤材,更多內(nèi)容可以查看gradle doc的 working_with_files_in_custom_tasks_and_plugins
-
定義一個Plugin
class Sml : Plugin<Project> { override fun apply(project: Project) { val rootPath = "${project.buildDir}/generated" project.android().variants().all { variant -> val subPath = variant.dirName val colorsOutputDirectory = File(File(rootPath, "sml_res_colors"), subPath).apply { mkdirs() } project.tasks.register(taskName(variant, "Colors"), ColorTask::class.java) { it.group = "sml" it.outputFile = File(colorsOutputDirectory, "values/generated_colors.xml") it.colorsMap = mutableMapOf() variant.registerGeneratedResFolders(project.files(colorsOutputDirectory).builtBy(it)) } } } private fun taskName(variant: BaseVariant, type: String) = "generate$type${variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }}" }
關于其中的
android
的作用是判斷當前是不是一個安卓項目,然后獲取variants
峦失,一個沒有進行過特殊處理的項目variant 只有兩個debug
和release
扇丛,顯然我們要為這兩個都生成任務。variant.registerGeneratedResFolders(project.files(colorsOutputDirectory).builtBy(it))
也是必須的尉辑,要不然android studio不知道我們生成的文件在哪里帆精。生成的文件中僅包含color,其他的資源照貓畫虎就可以全部做出來隧魄。
-
接受參數(shù)
上面的例子中卓练,參數(shù)是通過
it.colorsMap = mutableMapOf()
指定的,因為我們的插件需要提供給不同的模塊使用购啄,現(xiàn)階段不太靈活襟企。interface SmlExtension { val color: MapProperty<String, String> val dimen: MapProperty<String, String> val drawables: NamedDomainObjectContainer<DrawableDomain> }
僅僅是一個接口,因為我們根本不必實現(xiàn)它狮含,gradle 會幫我們做的顽悼,只不過要求是參數(shù)需要是制定類型
Managed properties
。在上面提供的gradle doc 中也包含這部分几迄。class Sml : Plugin<Project> { override fun apply(project: Project) { val extension = project.extensions.create("sml", SmlExtension::class.java) val rootPath = "${project.buildDir}/generated" // ... } }
然后我們就可以通過
extension
獲取到參數(shù)了蔚龙。其實也不是,現(xiàn)在獲取的數(shù)據(jù)是空的映胁,因為我們還沒有傳遞參數(shù)木羹。sml { color.set(mutableMapOf("test" to "#ff0000")) }
這里接受的是個map 對象,不管數(shù)據(jù)源是啥屿愚,只要最終轉化成一個map 即可汇跨。
這里的sml 就是我們創(chuàng)建extension 是傳遞的那個
sml
參數(shù),這個函數(shù)是gradle 據(jù)此信息自動生成的妆距。/** * Retrieves the [sml][com.storyteller_f.sml.SmlExtension] extension. */ val org.gradle.api.Project.`sml`: com.storyteller_f.sml.SmlExtension get() = (this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("sml") as com.storyteller_f.sml.SmlExtension /** * Configures the [sml][com.storyteller_f.sml.SmlExtension] extension. */ fun org.gradle.api.Project.`sml`(configure: Action<com.storyteller_f.sml.SmlExtension>): Unit = (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("sml", configure)
-
支持更多類型
上面演示的只有color 和dimen穷遂,關于drawable 還沒有說。drawable 更為復雜娱据,不能像color 那樣用一個
MapProperty<String, String>
就給打發(fā)了蚪黑。在這里我們使用NamedDomainObjectContainer
盅惜,這個對象是一個Collection
,可以放入很多數(shù)據(jù)忌穿。interface DrawableDomain { // Type must have a read-only 'name' property val name: String? val drawable: Property<String> }
不過有一個要求是抒寂,范型內(nèi)要求包含一個不可變的字段
name
。其實還有一個掠剑,其他的字段要求是可序列化的??屈芜。就在寫博客前,用的還是自定義的對象朴译,但是太過麻煩井佑,幾乎所有相關的類都需要可序列化,所以最好辦法的還是接受一個String眠寿。而且這樣還有一個好處躬翁,等會說。 -
kotlin dsl
我希望配置參數(shù)時的代碼更清晰盯拱,所以最好能是這種寫法盒发。
sml { color.set(mutableMapOf("test" to "#ff0000")) dimen.set(mutableMapOf("test1" to "12")) drawables { register("hello") { Rectangle { solid("#00ff00") corners("12dp") } } register("test") { Oval { solid("#ff0000") } } register("test1") { Ring("10dp", "1dp") { ring("#ffff00", "10dp") } } register("test2") { Line { line("#ff0000", "10dp") } } } }
所以需要類似這樣的擴展函數(shù)(真的是體力活)。
fun DrawableDomain.Oval(block: OvalShapeDrawable.() -> Unit) { drawable.set(OvalShapeDrawable().apply { start() }.apply(block).output()) } fun DrawableDomain.Ring(innerRadius: String, thickness: String, block: RingShapeDrawable.() -> Unit) { drawable.set(RingShapeDrawable(innerRadius, thickness, false).apply { start() }.apply(block).output()) } fun DrawableDomain.Ring(innerRadiusRatio: Float, thicknessRatio: Float, block: RingShapeDrawable.() -> Unit) { drawable.set(RingShapeDrawable(innerRadiusRatio.toString(), thicknessRatio.toString(), true).apply { start() }.apply(block).output()) } fun DrawableDomain.Line(block: LineShapeDrawable.() -> Unit) { drawable.set(LineShapeDrawable().apply { start() }.apply(block).output()) }
具體相關的類狡逢,可以去我的github 看https://github.com/storytellerF/common-ui-list-structure宁舰。
上面這種寫法你也許不喜歡,不過沒關系甚侣,因為drawable 最終接受的是一個字符串明吩,所以你也可以寫成這樣(這就是我上面說的好處)间学。
register("test2") { drawable.set("hello world") //Line { // line("#ff0000", "10dp") //} }
-
生成
現(xiàn)在我們開始生成這些玩具吧殷费,代碼寫好后,我們需要進行gradle sync低葫,同步之后详羡,在相應模塊的任務列表下就能找到我們的任務了。
執(zhí)行任務試試吧嘿悬。
-
成果
在resource manager 中預覽正常??
layout 中預覽正常??
后言
當前也是有個小問題的实柠,在生成的資源文件中android studio 不提供預覽功能。
可能Google 的開發(fā)人員認為這個是自動生成的善涨,所以你應該對于生成出來的內(nèi)容擁有絕對的認知吧窒盐。
最優(yōu)先的的選擇應該還是jetpack compose,但是如果因為某些原因而不能钢拧,那么“SML” 應該是最好的選擇了吧??