[SML|Gradle|Android] 通過gradle plugin 自動生成資源文件(drawable焦辅,color,dimen椿胯。筷登。。)

[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)有的解決方案

  1. 通過注解生成android.graphics.drawable.Drawable(或者完全通過手動調(diào)用代碼)。然后在代碼中使用户秤。
  2. 通過繼承現(xiàn)有的View码秉。具體使用時還有點區(qū)別,一是通過Provider 自動替換,就像 我們明明用的是android.widget.View鸡号, 但是實際展示出來的卻是androidx.appcompat.widget转砖。二是在layout 布局中直接使用這些特殊的View。后一個的好處是可以預覽到“drawable”的效果鲸伴。
  3. 通過data binding府蔗,在xml 中使用代碼,插入android.graphics.drawable.Drawable汞窗。(我現(xiàn)階段使用的)
  4. 使用Material姓赤,和方案2 類似痛黎,但是有些效果可能沒有兢哭。
  5. 使用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,其實就是仿照Sassxml 改的趟济。如果你愿意可以叫他“斯麥魯乱投,斯麥魯”??

開始

項目使用kotlin 以及kotlin dsl 編寫,所以需要有一定的前置知識顷编。

  1. 創(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"
            }
        }
    }
    
  2. 定義一個任務

    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)容存儲到文件中素挽。

    關于注解OutputFileInput 用來給gradle 進行增量更新判斷使用蔑赘。如果輸入輸出都沒有發(fā)生變化,這個任務都會跳過。加上很有必要缩赛。

    TaskAction 也是必須的锌历,很顯然這個函數(shù)不是繼承自DefaultTask 的,而且這個抽象類中也沒有什么函數(shù)要繼承峦筒。只有加了這個注解究西,gradle 才會運行這個函數(shù)。

    代碼并不是必須要這么寫物喷,只要邏輯沒問題應該就好卤材,更多內(nèi)容可以查看gradle doc的 working_with_files_in_custom_tasks_and_plugins

  3. 定義一個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 只有兩個debugrelease扇丛,顯然我們要為這兩個都生成任務。

    variant.registerGeneratedResFolders(project.files(colorsOutputDirectory).builtBy(it)) 也是必須的尉辑,要不然android studio不知道我們生成的文件在哪里帆精。

    生成的文件中僅包含color,其他的資源照貓畫虎就可以全部做出來隧魄。

  4. 接受參數(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)
    
    
  5. 支持更多類型

    上面演示的只有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眠寿。而且這樣還有一個好處躬翁,等會說。

  6. 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")
        //}
    }
    
  7. 生成

    現(xiàn)在我們開始生成這些玩具吧殷费,代碼寫好后,我們需要進行gradle sync低葫,同步之后详羡,在相應模塊的任務列表下就能找到我們的任務了。

    TASK.png

    執(zhí)行任務試試吧嘿悬。

  8. 成果

    demo.png

    在resource manager 中預覽正常??

    layout.png

    layout 中預覽正常??

后言

當前也是有個小問題的实柠,在生成的資源文件中android studio 不提供預覽功能。

shape.png

可能Google 的開發(fā)人員認為這個是自動生成的善涨,所以你應該對于生成出來的內(nèi)容擁有絕對的認知吧窒盐。

最優(yōu)先的的選擇應該還是jetpack compose,但是如果因為某些原因而不能钢拧,那么“SML” 應該是最好的選擇了吧??

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蟹漓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子源内,更是在濱河造成了極大的恐慌葡粒,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嗽交,居然都是意外死亡卿嘲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門夫壁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拾枣,“玉大人,你說我怎么就攤上這事盒让》徘埃” “怎么了?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵糯彬,是天一觀的道長凭语。 經(jīng)常有香客問我,道長撩扒,這世上最難降的妖魔是什么似扔? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮搓谆,結果婚禮上炒辉,老公的妹妹穿的比我還像新娘。我一直安慰自己泉手,他們只是感情好黔寇,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斩萌,像睡著了一般缝裤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颊郎,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天憋飞,我揣著相機與錄音,去河邊找鬼姆吭。 笑死榛做,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的内狸。 我是一名探鬼主播检眯,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼昆淡!你這毒婦竟也來了锰瘸?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤瘪撇,失蹤者是張志新(化名)和其女友劉穎获茬,沒想到半個月后港庄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡恕曲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年鹏氧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佩谣。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡把还,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出茸俭,到底是詐尸還是另有隱情吊履,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布调鬓,位于F島的核電站艇炎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏腾窝。R本人自食惡果不足惜缀踪,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虹脯。 院中可真熱鬧驴娃,春花似錦、人聲如沸循集。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咒彤。三九已至疆柔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蔼紧,已是汗流浹背婆硬。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奸例,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓向楼,卻偏偏與公主長得像查吊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子湖蜕,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容