Kuikly 开发笔记

参考链接

Kuikly - 组件

Pager

Pager为Kuikly页面的入口类,类似iOS UI中的VC, Pager也有类似VC的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 已创建
override fun created() {
super.created()

}

// 页面在屏幕上可见
override fun pageDidAppear() {
super.pageDidAppear()

}

// 页面在屏幕上消失
override fun pageDidDisappear() {
super.pageDidDisappear()

}

// 页面即将消失
override fun pageWillDestroy() {
super.pageWillDestroy()

}

一个常见的Page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 设置路由跳转名
@Page("my_ctstom_name")
// 定义一个自己的类, 继承于 BasePager
internal class MyCustomPage : BasePager() {
// 伴生对象, Kotlin 中,类没有静态成员的概念,但通过 companion object,你可以模拟静态成员的行为
companion object {
// 定义一个静态常量 TAG
private const val TAG = "MyCustomPageTag"
}

// 定义 currentName 是个可观察的属性, 设置给 UI 组件后, 值变化会直接更新 UI 组件的内容, 类似 OC 的 RAC
var currentName by observable("")

// 构造 self.view
override fun build(): ViewBuilder {
// 使用 this 会导致循环引用, 所以一般在这里定义一个 ctx
val ctx = this
return {
// 布局属性
attr {
backgroundColor(Color.WHITE)
}
// 子 View
View {

}
}
}
override fun createPlugins(): Map<String, () -> PluginModule> {
val plugins = HashMap<String, () -> PluginModule>().apply {
putAll(super.createPlugins()) // 基类的
hashMapOf(
MediaPlugin.PLUGIN_NAME to { MediaPlugin() } // 再增加一个
).let { putAll(it) }
}
return plugins + BizPluginBuilderList.getSomePlugin() // 这里也能加
}
}

VM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
internal class MyCustomPageModel : BasePageStateViewModel<MyCustomPage>() {
// 监听数组
var languageList: ObservableList<LanguageInfo> by observableList()
// 监听变量
var displayName by observable("")

// 懒加载
val settingInfo by lazy { SettingItemInfo("") }

// 函数定义
fun updateJumpParams(param : Boolean = false) : Boolean {
// ...
}
}

// 在 Page 中
internal class MyCustomPage : BasePager() {
val viewModel: MyCustomPageModel by lazy {
getViewModel(MyCustomPageModel::class)
}
}

// 在 ViewModelFactory.kt 增加一行
fun createViewModel(modelClass: KClass<out BaseViewModel<out BasePager>>): BaseViewModel<out BasePager>? {
return when (modelClass) {
// ...
MyCustomPageModel::class -> MyCustomPageModel()
// ...
}
}

数据刷新

by observable

变量被设置为 by observable 后, UI 绑定这个变量会直接变化

1
2
3
4
5
6
7
var isOpen by observable(false)

Switch {
attr {
isOn(ctx.isOpen)
}
}

vbind

组件使用vbind包裹,当vbind内属性发生改变时,整个组件会重新绘制

1
2
3
4
5
6
7
8
9
10
11
vbind({ ctx.displaySomething }) {
if ( xx ) {
View {

}
} else if ( xx ) {
View {

}
}
}

vfor

对于数量可变的列表数据,使用vfor包裹来实现cell,列表变化时会重新绘制

1
2
3
4
5
6
7
vfor({ ctx.dataList }) { itemData ->
PersonInfoCustomizeCell {
attr {
item = itemData
}
}
}

vif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vif({ xxx }) {
View {

}
}
velseif({ }) {
View {

}
}
velse {
View {

}
}

ComposeView

1
2
3
4
5
class CustomView : ComposeView<>() {
}
internal fun ViewContainer<*, *>.Custom(init: CustomView.() -> Unit) {
addChild(CustomView(), init)
}

外部设置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CustomView : ComposeView<CustomViewAttr>() {

override fun body(): ViewBuilder {
val ctx = this
val bottomArrowX = ctx.attr.bottomArrowX
// ...
}

override fun createAttr(): CustomViewAttr {
return CustomViewAttr()
}
}

internal class CustomViewAttr : ComposeAttr() {
var bottomArrowX = 0f
// ...
}

// 其他类调用
class OtherClassView : ComposeView() {

override fun body(): ViewBuilder {
val ctx = this

View {
Custom {
attr {
bottomArrowX = showX
}
}
}
}
}

事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class CustomView : ComposeView<CustomViewEvent>() {
override fun body(): ViewBuilder {
val ctx = this

View {
event {
click {
KLog.i(TAG, "onSendClick")
ctx.event.onSendClick?.invoke(ctx.attr.customViewParam)
}
}
}
}
}

class CustomViewEvent : ComposeEvent() {
var onSendClick: ((CustomViewParam) -> Unit)? = null
}
// 其他类调用
class OtherClassView : ComposeView() {

override fun body(): ViewBuilder {
val ctx = this

View {
Custom {
event {
onSendClick = { customViewParam ->
// 事件处理
}
}
}
}
}
}

布局

布局属性

Flex 布局

flexDirection 主轴

  • flexDirectionColumn(默认): 主轴方向为竖直方向,子孩子从上往下布局
  • flexDirectionRow: 主轴方向为水平方向,子孩子从左往右布局
  • flexDirectionColumnReverse: 主轴方向为竖直方向,子孩子从下往上布局
  • flexDirectionRowReverse: 主轴方向为水平方向,子孩子从右往左布局

justifyContent 主轴分布模式

  • justifyContentFlexStart(默认): 主轴开始的位置进行对齐
  • justifyContentFlexEnd: 主轴结束的位置进行对齐
  • justifyContentCenter: 主轴的中间位置进行对齐
  • justifyContentSpaceBetween: 主轴两端对齐,子孩子之间的间隔都相等
  • justifyContentSpaceAround: 每个项目两侧的间隔相等。所以,子孩子之间的间隔比项目与边框的间隔大一倍
  • justifyContentSpaceEvenly: Flex Item之间的间距相等,包括与边缘位置的距离

alignItems 交叉轴

  1. alignItemsFlexStart: 交叉轴的起点对齐
  2. alignItemsFlexEnd: 交叉轴的终点位置对齐
  3. alignItemsCenter: 交叉轴的中点位置对齐
  4. alignItemsStretch(默认): 如果 Flex 容器的孩子没有指定大小(高度或者宽度,取决于交叉轴是水平还是竖直)的话,将占满 Flex 容器
  • 当 flexDirection 为 flexDirectionRow 时,交叉轴的方向为竖直方向,此时 alignItems 各个属性的效果为
    企业微信截图_6d150c72-644a-42cd-9a28-8b92a9a082df
  • 当 flexDirection 为 flexDirectionColumn 时,交叉轴的方向为水平方向,此时 alignItems 的各个属性效果为:
    企业微信截图_ce12e3a8-ebf2-4c89-a397-638292dbc2c5

Flex Item 布局属性

上面讲述的 Flex Container 属性,是针对 Flex Container 下的所有孩子生效。Flex Item 也可自己设置布局属性,覆盖 Flex Container 的属性。

alignSelf

alignSelf属性是控制Flex Item自身在 Flex 容器的交叉轴上的对齐方式,会覆盖 Flex 容器指定的 alignItems 属性,可选值为:

  1. alignSelfFlexStart: Flex Item 自身在 Flex 容器的交叉轴的起点对齐
  2. alignSelfCenter: Flex Item 自身在 Flex 容器的交叉轴中点对齐
  3. alignSelfFlexEnd: Flex Item 自身在 Flex 容器的交叉轴终点对齐
  4. alignItemsStretch: Flex Item 自身在交叉轴方向上铺满Flex 容器
  • alignSelf 与 alignItems 差不多,具体的对齐方向与 flexDirection 的值有关
  • 当 flexDirection 的值为 flexDirectionColumn 时,Flex Item 设置 alignSelf 的效果如下:

flex

Flex Item 在主轴上,占据 Flex 容器的剩余可用空间的比例。 可用空间是指 Flex 容器除去已经被占用的空间,剩下的空间大小, 比如: 顶部是一个 titlebar,剩下的空间分配给列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
View {
attr {
size(screenWidth, screenHeight)
flexDirectionColumn() // 主轴方向为竖直方向,孩子从上往下排列
// alignItems默认值为 alignItemsStretch, 因此在交叉轴,即水平方向上
// 其孩子的宽度与父亲一样大
}

View { // title bar
attr {
height(56f)
// 宽度为父亲的宽度, 因为父亲的alignItems 默认为 alignItemsStretch,
// 在交叉轴上的大小会占满父亲
}
}

List {
attr {
flex(1f) // 表示占满父容器主轴上的可用空间,即占满父亲可用的高度(screenHeight - 56f)
}
}
}

绝对布局

与 superView 一样大

1
2
3
4
5
View {
attr {
absolutePositionAllZero()
}
}

保持间距

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
View {
attr {
size(screenWidth, screenHeight)
}

Image {
attr {
positionAbsolute()
// 表示宽高与父容器的宽高一样大
top(0f)
bottom(0f)
right(0f)
left(0f)
}
}
}

常用UI组件

设置背景色/圆角

1
2
3
4
attr {
backgroundColor(ctx.buildThemedColor("skin_floor_color"))
borderRadius(10f)
}

文本

1
2
3
4
5
6
7
8
9
10
Text {
attr {
fontSize(36f.pxw)
fontWeight500()
marginTop(40f.pxw)
marginBottom(10f.pxw)
text(itemData.title)
color(ctx.buildThemedColor("skin_text_main_color"))
}
}

加载图片

1
2
3
4
5
6
7
Image {
attr {
src("https://musicx.y.qq.com/kuikly/assets/ic_svip_quality_guide_check.png")
size(DESIGN_DESC_ICON_SIZE.pxh(), DESIGN_DESC_ICON_SIZE.pxh())
marginRight(DESIGN_DESC_ICON_MARGIN_RIGHT.pxh())
}
}

列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
List {
attr {
positionAbsolute()
}
vfor({ ctx.dataList }) { itemData ->
View {
attr {
Size(pagerWidth, 40F)
flexDirectionRow()
}

Image {
attr {
size(30F, 30F)
src(itemData.avatarUrl)
}
}

Text {
attr {
fontSize(36f.pxw)
fontWeight500()
marginTop(40f.pxw)
marginBottom(10f.pxw)
text(itemData.title)
color(ctx.buildThemedColor("skin_text_main_color"))
}
}
}
}
}

列表 cell 高度设置(在 cell 设置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
List {
ref {
// it 是一个隐式名称(implicit name)的参数,用于表示单个参数的 lambda 表达式中的参数。当你在 lambda 表达式中只有一个参数时,可以使用 it 来代替显式声明参数名称
ctx.listViewRef = it
}
attr {
absolutePositionAllZero()
}
vfor({ ctx.dataList }) { itemData ->
PersonInfoCustomizeCell {
attr {
item = itemData
}
}
}
}

// cell 单独设置
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
height(120F)
}
View {
attr {
backgroundColor(Color.WHITE)
borderRadius(10F)
positionAbsolute()
top(0F)
left(20F)
right(20F)
bottom(10F)
flexDirectionRow()
}
}
}
}

列表调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.tencent.kuikly.core.views.ListView

internal class xxxPager : BasePager() {
var listViewRef : ViewRef<ListView<*, *>>? = null


override fun build(): ViewBuilder {
val ctx = this
return {
List {
ref {
ctx.listViewRef = it
}
attr {
absolutePositionAllZero()
}
}
}
}
override fun viewDidLayout() {
super.viewDidLayout()

this.listViewRef?.view?.setContentInset(top = 500F)
this.listViewRef?.view?.setContentOffset(0F, -500F) // 设置 inset 后 自动产生了一个 offsetY 偏移
}
}

subView 回调

1
2
3
4
5
6
7
8
// subView 先获取到 pager
var pager = getPager()
// 判定 pager 类型,
if (pager is SomePager)
{
// 调用被标记为 observable 的对象, 达到调用的目的
pager.isShowAutoTranslateWarningDialog = true
}

弹窗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 是否需要展示自动翻译弹窗
var isShowAutoTranslateWarningDialog by observable(false)

return {
View {
// 自动翻译弹窗
ctx.buildAutoTanslateWarningDialog().invoke(this);
}
}

private fun buildAutoTanslateWarningDialog(): ViewBuilder {
val ctx = this
return {
SendMsgWarningDialog {
attr {
showDialog = ctx.isShowAutoTranslateWarningDialog
title = StringConst.AUTOTRANSLATE_DIALOG_TITLE
content = StringConst.AUTOTRANSLATE_DIALOG_CONTENT
}
event {
onButtonSureClick = {
ctx.isShowAutoTranslateWarningDialog = false
KLog.i(TAG, "[clickedTranslateBtnForAlert] click sure ")
}
onButtonCancelClick = {
// 取消
ctx.isShowAutoTranslateWarningDialog = false
KLog.i(TAG, "[clickedTranslateBtnForAlert] click cancel ")
}
}
}
}
}


// 最后 在需要的时候
this.isShowAutoTranslateWarningDialog = true

逻辑

日志

1
2
3
4
5
KLog.i(
TAG,
"[onMessageBodyLongPress] msgFrame=$msgFrame, msgBodyFrame=$msgBodyFrame, clickX=$clickX, clickY=$clickY, msgBodyLeft=$msgBodyLeft, messageModel=$messageModel"
)
KLog.i(TAG, "created artist_enc_uin: fromOne ${fromOne} fromTwo ${fromTwo}")

缓存

1
bubblePlugin?.read(key = KEY_HAVE_DISPLAYED_GUIDE_VIEW + userId + myOwnEncryptUin)

判空(null 执行 toInt() 会崩溃)

1
!clickCountBefore.isNullOrEmpty()

类型转换

1
2
3
4
// toLong() 在遇到 "abc" 这样的字符串 会崩溃, 要用 toLongOrNull, toInt() 也有类似问题
var clickTs = clickTsBefore?.toLongOrNull() ?: 0L

val tsStr = clickTs.toString()

延时处理

1
2
3
setTimeout(500) {
// xxx
}

获取时间戳

1
var ts = DateTime.currentTimestamp(); // 毫秒

枚举

1
2
3
4
5
6
internal enum class MediaType {
PHOTO,
CAMERA,
VIDEO,
VOICE
}

字符串兜底值

1
2
3
4
var fromOne = pagerData.getParamString("artist_enc_uin") ?: ""
var fromTwo = pagerData.getExtrasParamString("artist_enc_uin") ?: ""
// 兼容web侧跳转
var from = fromOne.ifEmpty { fromTwo }

通知

1
2
3
4
5
6
7
8
9
10
11
12
// 监听
notifyModule.addNotify("xx") {
// let 在非空的情况下对一个对象执行一个代码块(block), 作用是对非空对象进行操作,避免了空值检查的需要
it?.getString("xx")?.let { returnName ->
// xx
}
}

// 发通知
notifyModule.postNotify("xx", JSONObject().apply {
put("xx", "value")
})

页面跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 跳转封装
fun openPage(page: String, jsonObject: JSONObject? = null, mode: Int = TABBAR_DISPLAY_HIDE_ALL, statusLight : Boolean = true, extras: JSONObject? = null) {
Bridge.getPlugin<UIPlugin>(UIPlugin.PLUGIN_NAME)?.apply {
openKuikly(KuiklyRouterInfo().apply {
router = KuiklyRouterInfo.ROUTER_OPEN_QMPAGE
pageName = page
params = jsonObject
// 状态栏模式
container = Container().apply {
tabbarMode = mode
this.statusLight = statusLight
}
pExtras = extras
})
}
}

// 调用
openPage("my_ctstom_name", JSONObject().apply {
put("uin", uin)
put("XX", xx)
})

调用 Native 的 scheme

1
2
3
4
5
6
7
8
//  使用 plugin 记得在 pager 的 createPlugins 加一下, 
uiPlugin?.openNative(
module = "ui",
method = "xxxMethod",
params = JSONObject().apply {
put("color", 1)
}
)

ViewBuilder

直接返回一个方法体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
override fun build(): ViewBuilder {
val ctx = this
return {
// 复用分割线, invoke 是 kotlin 的语法
ctx.cutOffLineViews().invoke(this)
}
}
// 定义一个可复用的标签
private fun cutOffLineViews(): ViewBuilder {
return {
View {
attr {
backgroundColor(Color(0xffEBEBEB))
height(1f)
margin(40f)
}
}
}
}

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 数组 
listOf("*.jar", "*.aar")
// 可变数组
val topImages = mutableListOf<DressDialogTopImage>()
topImages.add(topImage)

// 遍历
val array = intArrayOf(1, 2, 3, 4, 5)

for (element in array) {
println(element)
}

// 使用 for 循环和索引
val array = intArrayOf(1, 2, 3, 4, 5)

for (index in array.indices) {
println("Index: $index, Value: ${array[index]}")
}

哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 哈希表
mapOf("h" to h, "m" to m, "s" to s)
// 可变哈希表
val data = mutableMapOf()
data[""] = ""

// 遍历
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
map.forEach { (key, value) ->
println("Key: $key, Value: $value")
}
// 遍历键/值
val map = mapOf("a" to 1, "b" to 2, "c" to 3)

for (key in map.keys) {
println("Key: $key")
}
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道