DaVinCi

Project Url: leobert-lan/DaVinCi
Introduction: An Android library to help create background drawable and ColorStateList without xml, writen in kotlin, support Java/Koltin code invoking or using in layout-xml with DataBinding
More: Author   ReportBugs   
Tags:

GitHub

如果出现 guava 中类重复,可添加进行解决

implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava")

是什么?

在 Android 上取代 xml 方式定义 Shape/GradientDrawable 以及 ColorStateList 的方案。

  • 支持在 Java/Kotlin 业务代码 中使用
  • 配合 DataBinding 可以在 XML 布局文件 中使用

哪些情况下需要它

  • 觉得 xml 太啰嗦
  • 命名困难,资源管理困难
  • 项目样式非常多但复用度不高
  • UI 做不到对样式规范化管理
  • 切换一个文件打断思路的成本太高

为什么叫 DaVinCi

最开始用于解决 Shape/GradientDrawable,而几何类绘制是毕加索最擅长的,然而这个名字早已被使用,梵高又太抽象了,索性就叫达芬奇了, 毕竟我也很讨厌命名。

Picasso has been used, Van Gogh's paints is too abstract, thus da VinCi ran into my mind.

如何使用

目前已迁移发布到 MavenCentral

allprojects {
    repositories {
        mavenCentral()
    }
}

and dependency in module's build.gradle:

implementation "io.github.leobert-lan:davinci-anno:0.0.2" //注解
kapt or ksp "io.github.leobert-lan:davinci-anno-ksp:0.0.2" //注解处理器,支持 kapt 或者 ksp
implementation "io.github.leobert-lan:davinci:0.0.5" //核心库
debugImplementation "io.github.leobert-lan:davinci-style-viewer:0.0.1" //预览

最新版本:

  • :

  • :

  • :

  • :

具体使用方式

详见:leobert.github.io/DaVinCi

或参考项目 Demo

API 功能简介

DaVinCi 目前提供三类功能:

  • func1:通过 逻辑构建反序列 DSL 定制 Drawable or ColorStateList 并进行设置
  • func2:配置 Drawable or ColorStateList 的 Style 或 StyleFactory 并进行应用
  • func3:对 Style 或 StyleFactory 对应的 Drawable or ColorStateList 进行 APP 方式预览

其中,func1 是主要功能。而 func1 有一个使用缺点:"未复用或者不便于复用",所以增加了 func2 提供了可复用性,并同步增加 func3,增加了预览

func1 系列 -- 构建 AST

目前(0.0.8)版本实现的支持创建:

  • ColorStateList
  • GradientDrawable
  • StateListDrawable

为了方便描述,我们将这三类简称为 Target

而创建的方法有二:

  • 直接基于实际需要的语法树结构,利用面向对象的封装构建出 AST 结构,传入上下文(DaVinCi)后,直接完成 Target 的构建。
  • 传入语法树 DSL(序列化的 String),指定 AST 根节点,传入上下文(DaVinCi)后,解析出 AST 结构(区别于前者,这里是解析 DSL 建立 AST),进而完成 Target 的构建

简而言之,这两者方式都是指导 DaVinCi 创建怎样的 Target

而这个指导方式又存在两种场景:

  • 在 xml 布局文件中,需要配合 DataBinding,实质上,通过 DataBinding 和预置的 BindingAdapter 配置,最终运行时的细节等同场景 2
  • 在逻辑代码(Java、Kotlin)中直接使用

我为什么实现这样的方案:在最初,我的目标就不仅止于更方便地 定义和设置背景 ,而是希望透过序列化的信息传输+反序列化 DSL 以实现 皮肤包、线上更新背景 等功能,虽然它们距离实现还有点远

ColorStateList

相对比较简单

//选中不是指获取焦点,而是 CheckBox 等选中的口语化表达
DaVinCiExpression.stateColor()
    .color(Color.parseColor("#ff00ff")).states(State.PRESSED_F, State.CHECKED_T) //选中 未按下
    .color(Color.parseColor("#0000dd")).states(State.PRESSED_T, State.CHECKED_T) //选中 按下
    .color(Color.parseColor("#ff0000")).states(State.CHECKED_T.name) //选中 实际上是冗余配置,会按照第一行的来
    .color(Color.parseColor("#00aa00")).states(State.CHECKED_F)// 未选中
GradientDrawable

对应 xml 定义 Drawable 资源的 root-tag "shape"

构建一个 Shape 的 AST Root

DaVinCiExpression.shape(): Shape

指定 Shape 的类型

一般常用的是 rectAngle 和 oval

fun rectAngle(): Shape
fun oval(): Shape
fun ring(): Shape
fun line(): Shape

rectAngle 时的圆角

fun corner(@Px r: Int): Shape
fun corner(str: String): Shape
fun corners(@Px lt: Int, @Px rt: Int, @Px rb: Int, @Px lb: Int): Shape

尺寸均可以 "xdp" 表达 dp 值,如"3dp"即为 3 个 dp,"3"则为 3 个 px。

对于 oval,可以使用 corner 标识其 radius

目前(0.0.8)并未完整的支持 ring

填充色

fun solid(str: String): Shape //色值 "#ffffffff" 或者 "rc/颜色资源名"
fun solid(@ColorInt color: Int): Shape

描边

fun stroke(width: String, color: String): Shape
fun stroke(width: String, color: String, dashGap: String, dashWidth: String): Shape
fun stroke(@Px width: Int, @ColorInt colorInt: Int): Shape

渐变

fun gradient(
    type: String = Gradient.TYPE_LINEAR,
    @ColorInt startColor: Int,
    @ColorInt endColor: Int,
    angle: Int = 0
): Shape

fun gradient(
    type: String = Gradient.TYPE_LINEAR, @ColorInt startColor: Int,
    @ColorInt centerColor: Int?, @ColorInt endColor: Int,
    centerX: Float,
    centerY: Float,
    angle: Int = 0
): Shape

fun gradient(startColor: String, endColor: String, angle: Int): Shape

fun gradient(type: String = Gradient.TYPE_LINEAR, startColor: String, endColor: String, angle: Int = 0): Shape

fun gradient(
    type: String = Gradient.TYPE_LINEAR,
    startColor: String,
    centerColor: String?,
    endColor: String,
    centerX: Float,
    centerY: Float,
    angle: Int
): Shape

尺寸说明:0.0.8 之前,只能使用 px 或 dp,e.g.: '1' is one pixel,'1dp' is one dp; 0.0.8 及其以后,增加了 '1pt','1mm','1sp'等

StateListDrawable

以此为例:

DaVinCiExpression.stateListDrawable()
    .shape(DaVinCiExpression.shape().stroke("1", "#ff653c").corner("2dp"))
    .states(State.CHECKED_F, State.ENABLE_T)

    .shape(DaVinCiExpression.shape().solid("#ff653c").corner("2dp,2dp,0,0"))
    .states(State.CHECKED_T, State.ENABLE_T)

    .shape(DaVinCiExpression.shape().rectAngle().solid("#80ff3c08").corner("10dp")).states(State.ENABLE_F)

小结:以上以 0.0.8 版本为例,列举了 逻辑构建 的主要 API,而 DSL 的规则将单独介绍(看到这些字表示我还在犯懒,没有写完)

func1 补充 -- 使用 AST 生成的结果(Drawable、ColorStateList)

构建完 AST 后就具备了创建 Target 的能力,但我们最终的目标是使用 Target,DaVinCi 中对使用场景也进行了针对性封装:

  • 给 View 设置背景 -- 内置策略
  • 给 TextView 设置文字色 -- 内置策略
  • 自由扩展

一般情况下,运用:

DaVinCiExpression#applyInto(view: View)

即可为 View 按照 内置策略 使用 DaVinCiExpression 创建的 Target。

DaVinCi 作为上下文,接受一个 Applier 实例作为 Target 的消费者

interface Applier {
    val context: Context

    fun getTag(id: Int): Any?

    fun applyDrawable(drawable: Drawable?)

    fun applyColorStateList(csl: ColorStateList?)
}

并可以使用一下 API 运用内置策略:

//给 View 设置背景
fun View.viewBackground(): Applier

//给 TextView 设置文字色
fun TextView.csl(): Applier

//自动应用以上两者规则
fun View.applier(): Applier {
    return when (this) {
        is TextView -> this.csl().viewBackground()
        else -> this.viewBackground()
    }
}

通过如下 API 获得 DaVinCi 实例并设置 Applier 策略:

DaVinCi.of(text: String? = null, applier: Applier? = null): DaVinCi

如果使用序列化的 DSL,将其作为 text 入参,并按照根类型调用下文 API;否则 text 参数为 null,基于手动创建的 AST 树调用以下 API

按照类型使用以下 API:

class DaCinCi {

    fun applySld(exp: DaVinCiExpression.StateListDrawable)

    fun applySld(exp: DaVinCiExpression.StateListDrawable, onComplete: (() -> Unit)? = null)

    fun applyShape(exp: DaVinCiExpression.Shape)

    fun applyShape(exp: DaVinCiExpression.Shape, onComplete: (() -> Unit)? = null)

    fun applyCsl(exp: DaVinCiExpression.ColorStateList)

    fun applyCsl(exp: DaVinCiExpression.ColorStateList, onComplete: (() -> Unit)? = null)
}

可参考下文代码:


fun demoOfApplier() {
    val view = findViewById<TextView>(R.id.test2)

    val dsl = """ sld:[ 
            shape:[ 
                gradient:[ type:linear;startColor:#ff3c08;endColor:#353538 ];
                st:[ Oval ];
                state:[ ${State.ENABLE_T.name}|${State.PRESSED_T.name} ];
                corners:[ 40dp ];
                stroke:[ width:4dp;color:#000000 ]
            ];
            shape:[ 
                gradient:[ type:linear;startColor:#ff3c08;endColor:#353538 ];
                st:[ Oval ];
                corners:[ 40dp ];
                state:[ ${State.ENABLE_T.name} ];
                stroke:[ width:4dp;color:rc/colorAccent ]
            ];
            shape:[ 
                gradient:[ type:linear;startColor:#ff3c08;endColor:#353538 ];
                st:[ Oval ];
                state:[ ${State.ENABLE_F.name} ];
                corners:[ 40dp ];
                stroke:[ width:4dp;color:#000000 ]
            ]
        ]
        """.trimIndent()

    //内置策略
    DaVinCi.of(dsl, view.viewBackground())
        .applySld(DaVinCiExpression.StateListDrawable.of(false))

    DaVinCi.of(null, view.csl())
        .applyCsl(
            DaVinCiExpression.stateColor()
                .color("#e5332c").states(State.PRESSED_T)
                .color("#667700").states(State.PRESSED_F)
        )

    DaVinCi.of(null, view.applier())
        .applyCsl(
            DaVinCiExpression.stateColor()
                .color("#e5332c").states(State.PRESSED_T)
                .color("#667700").states(State.PRESSED_F)
        )

    //自由使用 csl
    val daVinCi = DaVinCi.of(null, Applier.csl(this) {
        //自由使用
    })
    val cslExp = DaVinCiExpression.stateColor()
        .color("#e5332c").states(State.PRESSED_T)
        .color("#667700").states(State.PRESSED_F)

    daVinCi.applyCsl(cslExp)
    daVinCi.applyCsl(cslExp, onComplete = {
        //onComplete
    })


    //自由使用 shape、sld
    val daVinCi2 = DaVinCi.of(null, Applier.drawable(this) {
        //自由使用
    })
    //shape 或者 stateListDrawable
    val drawableExp = DaVinCiExpression.shape().oval()
        .corner("40dp") //这个就没啥用了
        .solid(resources.getColor(R.color.colorPrimaryDark))
        .stroke(12, Color.parseColor("#26262a"))

    //如果为 stateListDrawable 则使用 applySld
    daVinCi2.applyShape(drawableExp)
    daVinCi.applyShape(drawableExp, onComplete = {
        //onComplete
    })

    //以上两者虽然相对简单,但存在一处缺陷:无法使用 tag 语法,没有实现从 View 中查询 tag,如果需要使用 View 中的 tag,则可以如下:

    val daVinCi3 = DaVinCi.of(null, Applier.ViewComposer(view)
        .drawable {
            //自由使用 drawable
        }.csl {
            //自由使用 ColorStateList
        }
    )

}

在 XML 中使用:

请结合 Demo 查阅,API 含义很简单,不再赘述。

func2 配置 Drawable or ColorStateList 的 Style 或 StyleFactory 并进行应用

使用 Style 或 StyleFactory 需要使用注解,按照项目实际使用 AnnotationProcessor 或 Kapt(or ksp)

以 KAPT 为例:

kapt {
    this.arguments {
        this.arg("daVinCi.verbose", "true") //编译日志开关
        this.arg("daVinCi.pkg", "com.example.simpletest") //生成类的 package
        this.arg("daVinCi.module", "App") //生成类的前缀
        this.arg("daVinCi.preview", "false") //需要预览则打开
    }
}

引入注解和注解处理器

"io.github.leobert-lan:davinci-anno:0.0.2"
"io.github.leobert-lan:davinci-anno-ksp:0.0.2"

Application 初始化时调用:{daVinCi.module}DaVinCiStyles.register() {daVinCi.module} 为配置的前缀

给定唯一的 Style 命名,并定义 Style、StyleFactory:

//定义 style
@DaVinCiStyle(styleName = "btn_style.main") //给定唯一的 Style 命名
@StyleViewer(
    height = 40, width = ViewGroup.LayoutParams.MATCH_PARENT,
    type = StyleViewer.FLAG_CSL or StyleViewer.FLAG_BG, background = "#ffffff"
)
class DemoStyle : StyleRegistry.Style("btn_style.main" /*给定唯一的 Style 命名*/) {
    init {
        Utils.timeCost("create DemoStyle 'btn_style.main'") {
            this.registerSld(
                exp = DaVinCiExpression.stateListDrawable()
                    .shape(DaVinCiExpression.shape().stroke("1", "#ff653c").corner("2dp"))
                    .states(State.CHECKED_F, State.ENABLE_T)

                    .shape(DaVinCiExpression.shape().solid("#ff653c").corner("2dp,2dp,0,0"))
                    .states(State.CHECKED_T, State.ENABLE_T)

                    .shape(DaVinCiExpression.shape().rectAngle().solid("#80ff3c08").corner("10dp")).states(State.ENABLE_F)

            ).registerCsl(
                exp = DaVinCiExpression.stateColor()
                    .color("#000000").states(State.ENABLE_T, State.CHECKED_T)
                    .color("#666666").states(State.ENABLE_T, State.CHECKED_F)
                    .color("#ffffff").states(State.ENABLE_F)
            )
        }
    }
}

//定义 StyleFactory
@DaVinCiStyleFactory(styleName = "btn_style.sub") //给定唯一的 Style 命名
class DemoStyleFactory : StyleRegistry.Style.Factory() {
  override val styleName: String = "btn_style.sub" //给定唯一的 Style 命名

  override fun apply(style: StyleRegistry.Style) {
    style.registerSld(
      DaVinCiExpression.stateListDrawable()
        .shape(DaVinCiExpression.shape().rectAngle().solid("#80ff3c08").corner("10dp"))
        .states(State.ENABLE_F)
        .shape(
          DaVinCiExpression.shape().rectAngle().corner("10dp")
            .gradient("#ff3c08", "#ff653c", 0)
        ).states(State.ENABLE_T)
    )
  }
}

并通过以下 API 使用:

@BindingAdapter("daVinCiStyle")
fun View.daVinCiStyle(styleName: String) {
    with(StyleRegistry.find(styleName)) {

        this?.applyTo(daVinCi = DaVinCi.of(null, this@daVinCiStyle.applier()), releaseAfter = true)
            ?: Log.d(DaVinCiExpression.sLogTag, "could not found style with name $styleName")
    }
}

func3 使用 APP 形式预览 Style、StyleFactory

目前尚未开发 DataBinding xml 预览

对于有复用度需求的 Style、StyleFactory 可以进行预览:

添加:io.github.leobert-lan:davinci-style-viewer:0.0.1 并在 Application 中调用: AppDaVinCiStylePreviewInjector.register() 注意配置的前缀

桌面会多出一个 Activity 入口。进入后是一个列表,列表的 ItemView 应用了样式,并可以设置 Enable、Checked

当然,需要对 Style 和 StyleFactory 使用如下注解:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
public annotation class StyleViewer(
    val height: Int = 48, // 预览区高度 dp
    val width: Int = -1 /*android.view.ViewGroup.LayoutParams.MATCH_PARENT = -1*/, //预览区宽度 dp
    val background: String = "#ffffff", 预览区背景,可避免撞色
    val type: Int = FLAG_BG or FLAG_CSL, //用途,使用默认即可
) {
    public companion object {
        public const val FLAG_BG: Int = 1 shl 0
        public const val FLAG_CSL: Int = 1 shl 1
    }
}

项目内容简介

  • anno_ksp :style 注册和预览的 APT or KSP
  • annotation : style 注册与预览配置相关的注解
  • davinci :主功能 API
  • davinci_styles_viewer :扩展,用于样式预览
  • app: Demo
  • 废弃
    • preview_anno_ksp:废弃,style 注册和预览的 KSP 实现
    • annotation-java:废弃,实测 KSP 是否对 Java 注解有效

几篇相关拙作:

重大变更版本的 ReleaseNote

  • DaVinCi-0.0.6 -- 让功能更加准确
  • DaVinCi-0.0.7 -- 移除了了 0.0.6 中大部分标记废除的内容,提供了更多方便的 API

TODO

0.0.8 性能优化:对象池化,平滑内存使用,减少 GC && 解析任务分离到工作线程,提高主线程利用率(大量的数组、集合、字符串操作确实耗时)

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools