[SML|Gradle|Android] 通过gradle plugin 自动生成资源文件(drawable,color,dimen。。。)

[SML|Gradle|Android] 通过gradle plugin 自动生成资源文件(drawable,color,dimen。。。)

本人所有文章禁止任何形式的转载,谢谢

前言

在一个app 中,UI 通常会给出不同的设计,比如Ok 按钮和Cancel 的样式是不太一样的,每出现一个新的样式,我们就需要创建一个drawable 文件。这都还好,关键是这个drawable 文件有相当多的重复性内容,也就是说完全没有重用。像style 文件还好,不同的style 之间还可以继承。

所以如果能够给出一个自动生成这些文件的工具会更好。

从标题就能够在以后我们要怎么做的了。但是我还是说一下现阶段可以选择的工具

现有的解决方案

  1. 通过注解生成android.graphics.drawable.Drawable(或者完全通过手动调用代码)。然后在代码中使用。
  2. 通过继承现有的View。具体使用时还有点区别,一是通过Provider 自动替换,就像 我们明明用的是android.widget.View, 但是实际展示出来的却是androidx.appcompat.widget。二是在layout 布局中直接使用这些特殊的View。后一个的好处是可以预览到“drawable”的效果。
  3. 通过data binding,在xml 中使用代码,插入android.graphics.drawable.Drawable。(我现阶段使用的)
  4. 使用Material,和方案2 类似,但是有些效果可能没有。
  5. 使用jetpack compose(可能是你最应该选择的)

现有的解决方案有一个问题就是无法预览到效果。虽然预览效果不是什么必须的,但我还是想要搞出来一个拥有预览能力的解决方案。

思考

很显然,自动生成代码吗,注解,gradle plugin,外部工具。

注解的方案还没尝试过,应该也是可以的,但是这类代码不容易写成控制、循环的代码,而且注解的参数有也严格的要求,比如不能使用可变的参数必须得是常量(在kotlin 中更为烦人,只能使用KClass,而不能使用Class)。

我还是选择的是gradle plugin。不过gradle plugin 和外部工具其实非常类似,只是说外部工具有点“脱离生态的感觉“,而且需要”一点点配置“,比如使用者可以把这个工具放到任意的什么目录。

关于通过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. 创建一个模块。也可以不进行创建,把代码放到buildSrc中,但是我都已经给它起了个名字了

    模块就是一个普通的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'
                // 插件的实现类
                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
        @TaskAction
        fun makeResources() {
            colorsMap.entries.joinToString { (colorName, color) ->
                "\n    $color"
            }.also { xml ->
                outputFile.writeXlmWithTags(xml)
            }
        }
    }
    

    outputFile.writeXlmWithTags(xml) 是一个扩展函数,就是把拼接好的内容存储到文件中。

    关于注解OutputFileInput 用来给gradle 进行增量更新判断使用。如果输入输出都没有发生变化,这个任务都会跳过。加上很有必要。

    TaskAction 也是必须的,很显然这个函数不是继承自DefaultTask 的,而且这个抽象类中也没有什么函数要继承。只有加了这个注解,gradle 才会运行这个函数。

    代码并不是必须要这么写,只要逻辑没问题应该就好,更多内容可以查看gradle doc的 working_with_files_in_custom_tasks_and_plugins

  3. 定义一个Plugin

    
    class Sml : Plugin {
        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. 接受参数

    上面的例子中,参数是通过it.colorsMap = mutableMapOf() 指定的,因为我们的插件需要提供给不同的模块使用,现阶段不太灵活。

    interface SmlExtension {
        val color: MapProperty
        val dimen: MapProperty
        val drawables: NamedDomainObjectContainer
    }
    

    仅仅是一个接口,因为我们根本不必实现它,gradle 会帮我们做的,只不过要求是参数需要是制定类型Managed properties。在上面提供的gradle doc 中也包含这部分。

    class Sml : Plugin {
        override fun apply(project: Project) {
            val extension = project.extensions.create("sml", SmlExtension::class.java)
            val rootPath = "${project.buildDir}/generated"
            // ...
        }
    }
    

    然后我们就可以通过extension 获取到参数了。其实也不是,现在获取的数据是空的,因为我们还没有传递参数。

    sml {
        color.set(mutableMapOf("test" to "#ff0000"))
    }
    

    这里接受的是个map 对象,不管数据源是啥,只要最终转化成一个map 即可。

    这里的sml 就是我们创建extension 是传递的那个sml 参数,这个函数是gradle 据此信息自动生成的。

    /**
    * 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): Unit =
        (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("sml", configure)
    
    
  5. 支持更多类型

    上面演示的只有color 和dimen,关于drawable 还没有说。drawable 更为复杂,不能像color 那样用一个MapProperty 就给打发了。在这里我们使用NamedDomainObjectContainer,这个对象是一个Collection,可以放入很多数据。

    interface DrawableDomain {
        // Type must have a read-only 'name' property
        val name: String?
    
        val drawable: Property
    }
    

    不过有一个要求是,范型内要求包含一个不可变的字段name。其实还有一个,其他的字段要求是可序列化的。就在写博客前,用的还是自定义的对象,但是太过麻烦,几乎所有相关的类都需要可序列化,所以最好办法的还是接受一个String。而且这样还有一个好处,等会说。

  6. kotlin dsl

    我希望配置参数时的代码更清晰,所以最好能是这种写法。

    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")
                }
            }
        }
    }
    

    所以需要类似这样的扩展函数(真的是体力活)。

    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. 生成

    现在我们开始生成这些玩具吧,代码写好后,我们需要进行gradle sync,同步之后,在相应模块的任务列表下就能找到我们的任务了。

    TASK.png

    执行任务试试吧。

  8. 成果

    demo.png

    在resource manager 中预览正常

    layout.png

    layout 中预览正常

后言

当前也是有个小问题的,在生成的资源文件中android studio 不提供预览功能。

shape.png

可能Google 的开发人员认为这个是自动生成的,所以你应该对于生成出来的内容拥有绝对的认知吧。

最优先的的选择应该还是jetpack compose,但是如果因为某些原因而不能,那么“SML” 应该是最好的选择了吧

你可能感兴趣的:([SML|Gradle|Android] 通过gradle plugin 自动生成资源文件(drawable,color,dimen。。。))