hikage-core

Maven CentralMaven metadata URLAndroid Min SDK

这是 Hikage 的核心依赖,你需要引入此模块才能使用 Hikage 的基本功能。

配置依赖

你可以使用如下方式将此模块添加到你的项目中。

我们推荐你优先参考 hikage-bom 使用 BOM 统一管理版本。

在你的项目 gradle/libs.versions.toml 中添加依赖。

[versions]
hikage-core = "<version>"

[libraries]
hikage-core = { module = "com.highcapable.hikage:hikage-core", version.ref = "hikage-core" }

在你的项目 build.gradle.kts 中配置依赖。

implementation(libs.hikage.core)

请将 <version> 修改为此文档顶部显示的版本。

传统方式

在你的项目 build.gradle.kts 中配置依赖。

implementation("com.highcapable.hikage:hikage-core:<version>")

请将 <version> 修改为此文档顶部显示的版本。

功能介绍

你可以 点击这里在新窗口中打开 查看 KDoc。

基本用法

使用下方的代码创建你的第一个 Hikage 布局。

首先,使用 Hikagable 创建一个 Hikage.Delegate 对象。

示例如下

val myLayout = Hikagable {
    LinearLayout {
        TextView {
            text = "Hello, World!"
        }
    }
}

然后,将其设置到你想要显示的父布局或根布局上。

示例如下

// 假设这就是你的 Activity
val activity: Activity
// 实例化 Hikage 对象
val hikage = myLayout.create(activity)
// 得到根布局
val root = hikage.root
// 设置为 Activity 的内容视图
activity.setContentView(root)

这样我们就完成了一个简单的布局创建与设置。

布局约定

Hikage 的布局基本元素基于 Android 原生的 View 组件,所有的布局元素都可以直接使用 Android 原生的 View 组件进行创建。

所有布局的创建过程都会被限定在指定的作用域 Hikage.Performer 中,它被称为布局的 “演奏者”,即饰演布局的角色对象,这个对象可以通过以下几种方式创建并维护。

Hikagable

正如 基本用法 所示,Hikagable 可以直接创建一个 Hikage.DelegateHikage 对象,在 DSL 中,你可以得到 Hikage.Performer 对象对布局内容进行创建。

第一种方案,在任意地方创建。

示例如下

// myLayout 是 Hikage.Delegate 对象
val myLayout = Hikagable {
    // ...
}
// 假设这就是你的 Context
val context: Context
// 在需要 Context 的地方实例化 Hikage 对象
val hikage = myLayout.create(context)

第二种方案,在存在 Context 的地方直接创建。

示例如下

// 假设这就是你的 Context
val context: Context
// 创建布局,myLayout 是 Hikage 对象
val myLayout = Hikagable(context) {
    // ...
}

HikageBuilder

除了上述的方式以外,你还可以维护一个 HikageBuilder 对象来预创建布局。

首先,我们需要创建一个 HikageBuilder 对象并定义为单例。

示例如下

object MyLayout : HikageBuilder {

    override fun build() = Hikagable {
        // ...
    }
}

然后,在需要的地方使用它,可以有如下两种方案。

第一种方案,直接使用 build 创建 Hikage.Delegate 对象。

示例如下

// myLayout 是 Hikage.Delegate 对象
val myLayout = MyLayout.build()
// 假设这就是你的 Context
val context: Context
// 在需要 Context 的地方实例化 Hikage 对象
val hikage = myLayout.create(context)

第二种方案,使用 Context.lazyHikage 创建 Hikage 委托对象。

例如,我们可以在 Activity 中像 ViewBinding 一样使用它。

示例如下

class MyActivity : AppCompatActivity() {

    private val myLayout by lazyHikage(MyLayout)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 得到根布局
        val root = myLayout.root
        // 设置为 Activity 的内容视图
        setContentView(root)
    }
}

或者,直接创建 Hikage 对象。

示例如下

class MyActivity : AppCompatActivity() {

    private val myLayout by lazyHikage {
        // ...
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 得到根布局
        val root = myLayout.root
        // 设置为 Activity 的内容视图
        setContentView(root)
    }
}

基本布局组件

Hikage 采用与 Jetpack Compose 一致的函数式创建组件方案,它的布局使用两种基础组件完成,ViewViewGroup 函数, 它们分别对应于 Android 原生基于 ViewViewGroup 的组件。

View

View 函数的基础参数为以下三个,使用泛型定义创建的 View 对象类型。

如果不声明泛型类型,默认使用 android.view.View 作为创建的对象类型。

参数名称描述
lparams布局参数,即 ViewGroup.LayoutParams,使用 LayoutParams 进行创建
id用于查找已创建对象的 ID,使用字符串定义
attrs用于创建 View 的 XML 属性集合,使用 Hikage.Attribute 进行创建
initView 的初始化方法体,作为最后一位 DSL 参数传入

示例如下

View<TextView>(
    lparams = LayoutParams(),
    id = "my_text_view"
) {
    text = "Hello, World!"
    textSize = 16f
    gravity = Gravity.CENTER
}

ViewGroup

ViewGroup 函数的基础参数为四个,比较于 View 函数多了一个 performer 参数。

它必须声明一个泛型类型,因为 ViewGroup 是抽象类,需要一个具体的实现类。

ViewGroup 额外提供一个基于 ViewGroup.LayoutParams 的泛型参数,用于为子布局提供布局参数,不声明时默认使用 ViewGroup.LayoutParams

参数名称描述
lparams布局参数,即 ViewGroup.LayoutParams,使用 LayoutParams 进行创建
id用于查找已创建对象的 ID,使用字符串定义
attrs用于创建 ViewGroup 的 XML 属性集合,使用 Hikage.Attribute 进行创建
initViewGroup 的初始化方法体,作为 DSL 参数传入
performerHikage.Performer 对象,作为最后一位 DSL 参数传入

performer 参数的作用是向下传递新的 Hikage.Performer 对象,作为子布局的创建者。

示例如下

ViewGroup<LinearLayout, LinearLayout.LayoutParams>(
    lparams = LayoutParams(),
    id = "my_linear_layout",
    // 初始化方法体将在这里使用 `init` 体现
    init = {
        orientation = LinearLayout.VERTICAL
        gravity = Gravity.CENTER
    }
) {
    // 可在这里继续创建子布局
    View()
}

LayoutParams

Hikage 中的布局均可使用 LayoutParams 函数设置布局参数,你可以使用以下参数创建它。

参数名称描述
width手动指定布局宽度
height手动指定布局高度
matchParent是否使用 MATCH_PARENT 作为布局宽度和高度
wrapContent是否使用 WRAP_CONTENT 作为布局宽度和高度
widthMatchParent仅设置宽度为 MATCH_PARENT
heightMatchParent仅设置高度为 MATCH_PARENT
body布局参数的初始化方法体,作为最后一位 DSL 参数传入

在你不设置 LayoutParams 对象或不指定 widthheight 时,Hikage 会自动使用 WRAP_CONTENT 作为布局参数。

body 方法体的类型来源于上层 ViewGroup 提供的第二位泛型参数。

示例如下

View(
    // 假设上层提供的布局参数类型为 LinearLayout.LayoutParams
    lparams = LayoutParams(width = 100.dp) {
        topMargin = 20.dp
    }
)

如果你只需要一个横向填充的布局,可以直接使用 widthMatchParent = true

示例如下

View(
    lparams = LayoutParams(widthMatchParent = true)
)

Layout

Hikage 支持引用第三方布局,你可以传入 XML 布局资源 ID、其它 Hikage 对象以及 View 对象,甚至是 ViewBinding

示例如下

ViewGroup<...> {
    // 引用 XML 布局资源 ID
    Layout(R.layout.my_layout)
    // 引用 ViewBinding
    Layout<MyLayoutBinding>()
    // 引用另一个 Hikage 或 Hikage.Delegate 对象
    Layout(myLayout)
}

定位布局组件

Hikage 支持使用 id 定位组件,在上面的示例中,我们使用了 id 参数设置了组件的 ID。

在设置 ID 后,你可以使用 Hikage.get 方法获取它们。

示例如下

val myLayout = Hikagable {
    View<TextView>(id = "my_text_view") {
        text = "Hello, World!"
    }
}
// 假设这就是你的 Context
val context: Context
// 在需要 Context 的地方实例化 Hikage 对象
val hikage = myLayout.create(context)
// 获取指定的组件,返回 View 类型
val textView = hikage["my_text_view"]
// 获取指定的组件并声明组件类型
val textView = hikage.get<TextView>("my_text_view")
// 如果不确定 ID 是否存在,可以使用 `getOrNull` 方法
val textView = hikage.getOrNull<TextView>("my_text_view")

自定义布局组件

Hikage 可以为组件类名生成对应的布局组件函数 (Hikage Performer),你可以直接使用它们创建组件,而无需再使用泛型声明。

如果你需要 Jetpack 或 Material 提供的组件,可以引入 hikage-widget-androidxhikage-widget-material 模块。

其中 Android 基础组件的声明依赖模块 hikage-widget-foundation 已被自动引入到当前模块中,你无需再单独引入它。

示例如下

LinearLayout(
    lparams = LayoutParams(),
    id = "my_linear_layout",
    init = {
        orientation = LinearLayout.VERTICAL
        gravity = Gravity.CENTER
    }
) {
    TextView(
        lparams = LayoutParams(),
        id = "my_text_view"
    ) {
        text = "Hello, World!"
        textSize = 16f
        gravity = Gravity.CENTER
    }
}

初始化后的 ViewViewGroup 对象会返回其自身对象类型的实例,你可以在接下来的布局中使用它们。

示例如下

val textView = TextView {
    text = "Hello, World!"
    textSize = 16f
    gravity = Gravity.CENTER
}
Button {
    text = "Click Me!"
    setOnClickListener {
        // 直接使用 textView 对象
        textView.text = "Clicked!"
    }
}

你可以继续参考 hikage-gradle-plugin,或者手动引入 hikage-compiler 模块来自动生成你自己的布局组件函数。

我们不再推荐手动创建组件函数,因为其实现成本过高且可能会发生非预期问题,如果你依然决定想要自己创建它,你可以参考以下方案进入完全手动创建组件函数的流程。

示例如下

// 假设你已经定义好了你的自定义组件
class MyCustomView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
    // ...
}

// 下面,创建组件对应的函数
// 自定义组件必须声明此注解
// 声明组件的注解具有传染性,在每个用于构建布局的作用域中,都需要存在此注解
@Hikagable
// 函数的命名可以随意,但是建议使用大驼峰命名
// 函数的签名部分需要固定声明为 `inline fun <reified LP : ViewGroup.LayoutParams> Hikage.Performer<LP>`
inline fun <reified LP : ViewGroup.LayoutParams> Hikage.Performer<LP>.MyCustomView(
    lparams: LayoutParams? = null,
    id: String? = null,
    noinline attrs: HikageAttribute = {},
    noinline init: HikageView<MyCustomView> = {},
    // 如果此组件是容器,可以声明一个 `performer` 参数
    // performer: HikagePerformer<LP> = {}
) = View<MyCustomView>({ context, attrs -> MyCustomView(context, attrs) } ,lparams, id, attrs, init)

组合与拆分布局

在搭建 UI 时,我们通常会将可复用的布局作为组件来使用,如果你不想每一个部分都使用原生的自定义 View 将其分别定制,你可以直接将布局逻辑部分进行拆分。

Hikage 支持将布局拆分为多个部分进行组合,你可以在任何地方使用 Hikagable 函数创建一个新的 Hikage.Delegate 对象。

示例如下

// 假设这是你的主布局
val mainLayout = Hikagable {
    LinearLayout(
        lparams = LayoutParams(matchParent = true),
        init = {
            orientation = LinearLayout.VERTICAL
        }
    ) {
        TextView {
            text = "Hello, World!"
        }
        // 组合子布局
        Layout(subLayout)
    }
}
// 假设这是你的布局子模块
// 由于上层布局使用了 LinearLayout,所以你可以为子布局声明 LinearLayout.LayoutParams
val subLayout = Hikagable<LinearLayout.LayoutParams> {
    TextView(
        lparams = LayoutParams {
            topMargin = 16.dp
        }
    ) {
        text = "Hello, Sub World!"
    }
}

你还可以使用 Kotlin 的最新特性 Context parameters 来更加友好地实现组合布局。

示例如下

Hikagable {
    LinearLayout(
        lparams = LayoutParams(matchParent = true),
        init = {
            orientation = LinearLayout.VERTICAL
        }
    ) {
        TextView {
            text = "Hello, World!"
        }
        // 组合子布局
        SubTextView()
    }
}

val SubTextView = Hikagable {
    TextView {
        textSize = 14f
        text = "Hello, Sub World!"
    }
}

XML 属性集合

特别注意

此功能是 Hikage 的一项运行时扩展,它不作为主要功能默认集成,使用前你需要手动引入 hikage-runtime-attribute 模块,否则以下功能将不会生效且会在运行时显式抛出异常。

Hikage 支持通过参数 attrs 在创建组件时传入 XML 属性集合,这些属性值将在运行时动态解析并设置到组件上,它仅会在组件创建时生效一次。

Hikage 支持大部分通过 XML 定义的属性,这对于一些不能动态修改属性的老旧自定义组件非常友好,你可以直接使用 XML 属性来设置它们的值,而不需要考虑使用反射或者在组件中暴露额外的设置方法。

例如下面这个示例,我们可以通过 HikageAttribute 直接使用 android:text 属性来设置 TextView 的文本内容。

示例如下

TextView(
    attrs = {
        // 声明 "android" 命名空间
        android {
            // 以下内容等价于 android:text="Hello, World!"
            set("text", "Hello, World!")
        }
    }
) {
    // 动态设置的内容会覆盖 XML 属性值
    text = "Hello, World!"
}

HikageAttribute 是 Hikage 构建 XML 属性集合的核心 DSL 模板,每个命名空间都会提供一个 AttributeScope 作用域,在其中你可以使用 set 方法来设置属性值,set 方法的第一个参数为属性名称,第二个参数为属性值。

你可以直接像上面那个示例一样使用命名空间的方案来设置属性值。

示例如下

val myAttrs = HikageAttribute {
    // 声明你自己的命名空间
    namespace("myScope") {
        // 以下内容等价于 myScope:myKey="My Value"
        set("myKey", "My Value")
    }
    // 使用当前项目的命名空间
    app {
        // 以下内容等价于 app:myKey="My Value"
        set("myKey", "My Value")
    }
}

// 然后设置到组件上
MyView(attrs = myAttrs)

你也可以不使用命名空间 DSL 的方式来独立设置每个属性值。

示例如下

val myAttrs = HikageAttribute {
    // 以下内容等价于 myScope:myKey="My Value"
    set("myScope:myKey", "My Value")
    // 或者
    namespace("myScope").set("myKey", "My Value")
}

set 方法的第二个参数提供了三种类型:StringIntBoolean,它们分别对应于 XML 属性值的三种基本类型,Hikage 会根据属性的类型进行动态解析和设置。

示例如下

val myAttrs = HikageAttribute {
    android {
        // 设置 String 类型的属性值
        set("text", "Hello, World!")
        // 使用整型方案设置颜色属性
        set("textColor", Color.RED)
        // 使用十六进制字符串方案设置颜色属性
        set("textColor", "#FFFF0000")
        // 使用布尔值设置属性值
        set("enabled", true)
        // 声明一个资源 ID 作为属性值
        set("background", "@drawable/my_background")
        // 直接使用资源 ID 作为属性值
        set("background", R.drawable.my_background)
        // 使用整型方案设置内边距
        set("padding", 16.dp)
        // 使用字符串方案设置内边距
        set("padding", "16dp")
    }
}

// 然后设置到组件上
TextView(attrs = myAttrs)

创建的 HikageAttribute 支持使用 isNotEmptyisEmpty 方法判断是否为空。

Hikage 支持 HikageAttributeAttributeItem 的互相转换,你可以使用 HikageAttribute.build() 方法构造 List<AttributeItem>

示例如下

val myAttrs = HikageAttribute {
    android {
        set("text", "Hello, World!")
    }
}
// 将 HikageAttribute 转换为 List<AttributeItem>
val items = myAttrs.build()

同时,你也可以通过一个 List<AttributeItem> 来创建一个 HikageAttribute 对象。

示例如下

// 假设这就是你的 List<AttributeItem>
val items: List<AttributeItem>
// 将 List<AttributeItem> 转换为 HikageAttribute
val myAttrs = items.toHikageAttribute()

注意

当你设置一个属性值时,Hikage 会根据属性的类型进行动态解析和设置,如果你提供的属性值类型与属性的实际类型不匹配,可能会导致运行时抛出异常或者静默失败。

Hikage 对于解析动态类型的投射可能存在一些局限性,请确保你提供的属性值类型与属性的实际类型相匹配并尽量优先使用字符串设置属性值,以避免潜在的问题。

属性值在设置后不支持动态修改,这是 Android XML 属性的设计限制,不是 Hikage 的设计缺陷。

Android XML 属性不允许名称重复,因此你不能在 HikageAttribute 中多次设置同一个属性,即使它们的命名空间不同。

特别注意

对于 layout_ 开头的属性,它们属于创建 LayoutParams 时的 XML 属性,如果你手动使用 lparams 参数创建了 LayoutParams,Hikage 将忽略向父布局传递 AttributeSet 并创建新的 LayoutParams,这些属性将不再生效并被覆盖,你只能选择一个方案来设置布局参数。

由于属性解析工作在运行时完成,所以你不能使用 @+id 的方式新增资源 ID,正确的做法是新建 res/values/ids.xml 文件并在其中声明资源 ID。

自定义布局装载器

Hikage 支持自定义布局装载器并同时兼容 LayoutInflater.Factory2,你可以通过以下方式自定义在 Hikage 布局装载过程中的事件和监听。

示例如下

val factory = HikageFactory { parent, base, context, params ->
    // 你可以在这里自定义布局装载器的行为
    // 例如,使用你自己的方式创建一个新的 View 对象
    // `parent` 为当前组件要添加到的 ViewGroup 对象,如果没有则为 `null`
    // `base` 为上一个 HikageFactory 创建的 View 对象,如果没有则为 `null`
    // `params` 对象中包含了组件 ID、AttributeSet、View 的 Class 对象以及构造方法的直接创建函数体
    val view = MyLayoutFactory.createView(context, params)
    // 你还可以在这里对创建的 View 对象进行初始化和设置
    view.setBackgroundColor(Color.RED)
    // 返回创建的 View 对象
    // 返回 `null` 将会使用默认的组件装载方式
    view
}

你还可以直接传入 LayoutInflater 对象以自动装载并使用其中的 LayoutInflater.Factory2

示例如下

// 假设这就是你的 LayoutInflater 对象
val layoutInflater: LayoutInflater
// 通过 LayoutInflater 创建 HikageFactory 对象
val factory = HikageFactory(layoutInflater)
// 你也可以在其中传入参数
val factory = HikageFactory(layoutInflater, HikageFactory.Config())

HikageFactory.Config 支持以下参数。

示例如下

// 定义 HikageFactory.Config 对象
val config = HikageFactory.Config(
    // 是否处理 LayoutInflater 的 mPrivateFactory
    privateFactory = false,
    // 指定需要处理的 View 类名列表,默认处理所有 View
    privateFactoryViews = listOf(
        // 例如处理 FCV
        "androidx.fragment.app.FragmentContainerView"
    )
)
// 然后将其设置到 HikageFactory 对象上
val factory = HikageFactory(layoutInflater, config)

你也可以全局修改 HikageFactory.Config 的默认配置。

示例如下

HikageFactory.Config.defaultProcessPrivateFactory = false
HikageFactory.Config.defaultPrivateFactoryViews = emptyList()

注意

由于完整模拟 LayoutInflater 行为管线会显著降低运行时的性能表现,所以 Hikage 默认不启用 privateFactory

启用后可以提升某些组件的兼容性,例如 FragmentContainerView,你可以在启用的同时将特定需要处理的 View 加入 privateFactoryViews 列表,就像上述示例所描述的那样。

你可以使用以下方式将其设置到你需要装载的 Hikage 布局上。

示例如下

// 假设这就是你的 Context
val context: Context
// 创建 Hikage 对象
val hikage = Hikagable(
    context = context,
    factory = {
        // 添加自定义的 HikageFactory 对象
        add(factory)
        // 直接添加
        add { parent, base, context, params ->
            // ...
            null
        }
        // 连续添加多个
        addAll(factories)
    }
) {
    LinearLayout {
        TextView {
            text = "Hello, World!"
        }
    }
}

小提示

Hikage 在默认装载时将会根据传入 Context 对象的 LayoutInflater.Factory2 对布局进行装载,如果你正在使用 AppCompatActivity, 布局中的组件将会自动被替换为对应的 Compat 组件或 Material 组件,与 XML 布局的特性保持一致。

如果你不需要默认生效此特性,可以使用以下方式全局关闭。

示例如下

Hikage.isAutoProcessWithFactory2 = false

预览布局

Hikage 支持在 Android Studio 中预览布局,借助于 Android Studio 自带的自定义 View 预览插件,你可以使用以下方式预览布局。

你只需要定义一个预览布局的自定义 View 并继承于 HikagePreview

示例如下

class MyLayoutPreview(context: Context, attrs: AttributeSet?) : HikagePreview(context, attrs) {

    override fun build() = Hikagable {
        LinearLayout {
            TextView {
                text = "Hello, World!"
            }
        }
    }
}

然后在你当前的窗口右侧应该会出现预览窗格,打开后点击 “Build & Refresh”,等待编译完成后将会自动显示预览。

如果你没有在右侧看到预览窗格,可以新建一个 XML 布局文件,在其中添加以下样板代码。

示例如下

<?xml version="1.0" encoding="utf-8"?>
<yourpackage.MyLayoutPreview xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

这个时候你应该就可以在右侧的预览窗格中看到预览了,如果你修改了布局代码,点击 “Build & Refresh” 后预览将会自动更新。

小提示

HikagePreview 实现了 HikageBuilder 接口,你可以在 build 方法中返回任意的 Hikage 布局以进行预览。

特别注意

HikagePreview 仅支持在 Android Studio 中预览布局,请勿在运行时使用它或将其添加到任何 XML 布局中。