主题
第 4 章 基础组件 — View / Text / Image / Input / Button / Scroller
学习目标:吃透 Kuikly 6 大基础组件的常用 API;遇到设计稿能从「组件库工具箱」里挑出最合适的零件;理解 Kuikly 「组件 = 容器 + 叶子」的设计哲学。
4.1 组件总览:6 张零件图
┌──────────────────────────────────────────────────────────┐
│ Kuikly 基础组件分类 │
├──────────────────────────────────────────────────────────┤
│ │
│ 容器类(可包子组件) │
│ ├─ View 通用容器,最常用 │
│ ├─ Scroller 可滚动容器 │
│ └─ List 长列表(第 6 章详细讲) │
│ │
│ 叶子类(无子组件) │
│ ├─ Text 文本 │
│ ├─ Image 图片 │
│ ├─ Input 单行输入框 │
│ ├─ TextArea 多行输入框 │
│ └─ Button 按钮(注:Kuikly 中按钮其实可由 View 封装)│
│ │
│ 绘制类 │
│ └─ Canvas 2D 绘图(曲线、圆、动态绘图) │
│ │
└──────────────────────────────────────────────────────────┘📌 设计哲学:Kuikly 没有像 RN 那样 50+ 内置组件,而是用「少量基础组件 + Flexbox」组合出 90% 的页面。剩下 10% 用 Component(自定义组件)扩展。
4.2 View — 万能容器
4.2.1 View 是什么
View 是最基础的容器组件,类似 HTML 的 <div>、Android 的 LinearLayout/FrameLayout。它本身不显示任何内容,作用是:
- 包裹子组件
- 作为 Flexbox 容器排布子组件
- 承载样式(背景、边框、圆角、阴影)
kotlin
View {
attr {
size(200f, 100f)
backgroundColor(Color.WHITE)
borderRadius(8f)
padding(12f)
}
Text { attr { text("我在 View 里") } }
}4.2.2 View 的高频 attr
| 类别 | API | 说明 |
|---|---|---|
| 尺寸 | size(w, h) width(100f) height(50f) | 固定尺寸 |
| 背景 | backgroundColor(Color) backgroundImage(url) | 颜色 / 图片 |
| 渐变 | backgroundLinearGradient(direction, *colors) | 线性渐变 |
| 边框 | borderRadius(8f) border(Border(...)) | 圆角 / 描边 |
| 阴影 | boxShadow(BoxShadow(2f, 4f, 8f, color)) | offset+blur+color |
| 间距 | padding(8f) marginHorizontal(16f) | 内 / 外间距 |
| Flex | flexDirectionRow() justifyContentSpaceBetween() | 布局算法 |
| 绝对定位 | absolutePosition(top, left, right, bottom) | 脱离 Flex 流 |
| 变换 | transform { rotate(45f); scale(1.2f) } | 旋转 / 缩放 |
| 可见性 | opacity(0.5f) visibility(false) | 透明 / 隐藏 |
4.2.3 View 的高频 event
| 事件 | 说明 |
|---|---|
click { e -> } | 单击 |
longPress { e -> } | 长按(默认 500ms) |
doubleClick { e -> } | 双击 |
pan { e -> } | 拖动(拿 dx/dy 实现拖拽) |
pinch { e -> } | 双指缩放(拿 scale) |
screenFrame { fps -> } | 每帧回调(动画专用) |
4.2.4 实战:渐变 + 阴影卡片
kotlin
View {
attr {
size(280f, 100f)
borderRadius(12f)
backgroundLinearGradient(
Direction.TO_RIGHT,
ColorStop(Color(0xFF667EEAL), 0f),
ColorStop(Color(0xFF764BA2L), 1f),
)
boxShadow(BoxShadow(0f, 6f, 12f, Color(0x40000000L)))
allCenter()
}
Text {
attr {
text("Premium Card")
color(Color.WHITE)
fontSize(20f)
fontWeightBold()
}
}
}4.3 Text — 文本
4.3.1 基础用法
kotlin
Text {
attr {
text("Hello Kuikly")
fontSize(16f)
color(Color.BLACK)
}
}4.3.2 Text 高频 attr
| API | 说明 |
|---|---|
text(...) | 显示内容 |
fontSize(16f) | 字号 |
color(Color.BLACK) | 文字颜色 |
fontWeightBold() / fontWeightNormal() / fontWeight400() | 字重 |
fontStyleItalic() | 斜体 |
lineHeight(24f) | 行高 |
letterSpacing(1f) | 字间距 |
textAlignCenter() / textAlignLeft() / textAlignRight() | 对齐 |
numberOfLines(2) | 最多行数(超出折叠) |
textOverFlowEllipsis() | 溢出 ... |
textDecorationLineThrough() | 删除线 |
textDecorationUnderLine() | 下划线 |
textShadow(TextShadow(2f, 2f, 4f, color)) | 文字阴影 |
4.3.3 富文本:Span 拼接
某些场景下需要在一段文字里混合不同样式(如「热门 推荐 ¥99」):
kotlin
RichText {
Span {
attr {
text("【热门】")
color(Color.RED)
fontSize(14f)
fontWeightBold()
}
}
Span {
attr {
text(" 推荐商品 ")
color(Color.BLACK)
fontSize(14f)
}
}
Span {
attr {
text("¥99")
color(Color(0xFFFF5722L))
fontSize(18f)
fontWeightBold()
}
}
}4.3.4 文本省略 + 限行
kotlin
Text {
attr {
text("一段很长很长很长的文本,要在两行内显示,超出部分用省略号表示。")
fontSize(14f)
color(Color.BLACK)
numberOfLines(2)
textOverFlowEllipsis()
lineHeight(20f)
}
}📌 小贴士:
numberOfLines(0)= 不限制行数(默认);numberOfLines(1)= 单行强制不换行。
4.4 Image — 图片
4.4.1 基础用法(3 种来源)
kotlin
// 1. 网络图
Image { attr { src("https://example.com/avatar.png") } }
// 2. 本地资源(Android 端 res/drawable,iOS 端 Assets)
Image { attr { src("ic_back") } }
// 3. Base64
Image { attr { src("data:image/png;base64,iVBORw0KGgoAAAA...") } }4.4.2 高频 attr
| API | 说明 |
|---|---|
src("...") | 图片地址 / 资源名 |
size(80f, 80f) | 必填,没尺寸图片不显示 |
borderRadius(40f) | 圆角,配 size 做圆形头像 |
resizeContain() / resizeCover() / resizeStretch() | 缩放模式 |
tintColor(Color.RED) | 图片着色(适合 icon) |
placeholder(...) | 加载中占位 |
blurRadius(10f) | 高斯模糊(背景图常用) |
4.4.3 高频 event
| 事件 | 说明 |
|---|---|
onLoadFinish { e -> } | 加载完成(成功 / 失败) |
onLoadStart { e -> } | 开始加载 |
click { e -> } | 点击(适合做大图预览) |
4.4.4 圆形头像
kotlin
Image {
attr {
src("https://example.com/avatar.png")
size(48f, 48f)
borderRadius(24f) // ★ 一半的尺寸 = 圆形
backgroundColor(Color(0xFFE0E0E0L)) // 占位灰色
}
}4.4.5 高斯模糊背景图
kotlin
View {
attr { size(375f, 200f) }
Image {
attr {
src("https://example.com/cover.jpg")
size(375f, 200f)
absolutePosition(top = 0f, left = 0f)
blurRadius(20f) // ← 模糊
opacity(0.6f)
}
}
Text {
attr {
text("毛玻璃标题")
color(Color.WHITE)
fontSize(24f)
fontWeightBold()
absolutePosition(bottom = 16f, left = 16f)
}
}
}4.5 Input — 单行输入框
4.5.1 基础用法
kotlin
Input {
attr {
height(44f)
placeholder("请输入手机号")
fontSize(14f)
color(Color.BLACK)
placeholderColor(Color(0xFFBDBDBDL))
paddingHorizontal(12f)
backgroundColor(Color.WHITE)
borderRadius(8f)
}
event {
onTextChange { value ->
// value 是 Input 的最新内容
}
}
}4.5.2 高频 attr
| API | 说明 |
|---|---|
value(...) | 受控值(绑定 observable 实现双向绑定) |
placeholder(...) | 占位提示 |
placeholderColor(Color) | 占位颜色 |
keyboardTypeNumber() | 数字键盘 |
keyboardTypeEmail() | 邮箱键盘 |
inputTypePassword() | 密码(自动遮蔽) |
maxLength(11) | 最大字符数 |
returnKeyTypeDone() | 键盘"完成"按钮文案 |
autofocus(true) | 自动获取焦点 |
4.5.3 高频 event
| 事件 | 说明 |
|---|---|
onTextChange { value -> } | 内容变化时回调 |
onFocus { e -> } | 获得焦点 |
onBlur { e -> } | 失去焦点 |
onSubmit { e -> } | 用户按"完成" |
onLengthBeyondLimit { _ -> } | 字符数超限 |
4.5.4 双向绑定模板
kotlin
private var phone by observable("")
// body() 里
Input {
attr {
value(ctx.phone) // ← 受控
placeholder("手机号")
keyboardTypeNumber()
maxLength(11)
}
event {
onTextChange { v -> ctx.phone = v } // ← 回写
}
}4.6 TextArea — 多行输入框
跟 Input 一脉相承,差别在于:
kotlin
TextArea {
attr {
height(120f)
placeholder("写下你的想法...")
fontSize(14f)
paddingHorizontal(12f)
paddingVertical(10f)
backgroundColor(Color.WHITE)
borderRadius(8f)
// TextArea 特有:
showCount(true) // 右下角显示字符数
maxLength(200)
}
event {
onTextChange { v -> ctx.content = v }
}
}4.7 Button — 按钮(其实是 View 封装)
4.7.1 关键认知
Kuikly 没有专门的 Button 控件 —— 它就是一个带样式 + click 事件的 View + Text。框架提供了 Button 组件作为「快捷套装」:
kotlin
Button {
attr {
size(120f, 44f)
backgroundColor(Color(0xFF1976D2L))
borderRadius(22f)
titleAttr {
text("提交")
color(Color.WHITE)
fontSize(16f)
fontWeightBold()
}
}
event {
click { /* 提交逻辑 */ }
}
}4.7.2 等价的 View 写法
kotlin
View {
attr {
size(120f, 44f)
allCenter()
backgroundColor(Color(0xFF1976D2L))
borderRadius(22f)
}
event { click { /* 提交逻辑 */ } }
Text {
attr {
text("提交")
color(Color.WHITE)
fontSize(16f)
fontWeightBold()
}
}
}📌 何时用 Button:写简单按钮时 Button 更省事;写复杂按钮(带图标、loading 状态、多行文字)建议直接用 View 自己拼。
4.7.3 按钮的 3 种状态(按下 / 禁用 / 加载)
kotlin
private var loading by observable(false)
private var disabled by observable(false)
View {
attr {
size(120f, 44f)
allCenter()
borderRadius(22f)
backgroundColor(
when {
ctx.disabled -> Color(0xFFBDBDBDL)
ctx.loading -> Color(0xFF90CAF9L)
else -> Color(0xFF1976D2L)
}
)
opacity(if (ctx.disabled) 0.6f else 1f)
}
event {
click {
if (ctx.disabled || ctx.loading) return@click
ctx.loading = true
submit { ctx.loading = false }
}
}
if (ctx.loading) {
// Kuikly 提供 ActivityIndicator 组件做转圈
ActivityIndicator { attr { color(Color.WHITE); size(20f, 20f) } }
} else {
Text {
attr { text("提交"); color(Color.WHITE); fontSize(16f); fontWeightBold() }
}
}
}4.8 Scroller — 可滚动容器
4.8.1 基础用法
View 的内容超出屏幕时不会滚动。要滚动必须用 Scroller:
kotlin
Scroller {
attr {
flex(1f) // 占满剩余空间
flexDirectionColumn()
showScrollerIndicator(true) // 显示滚动条
}
Text { attr { text("第 1 段") } }
Text { attr { text("第 2 段") } }
// ... 几十段
Text { attr { text("第 N 段") } }
}4.8.2 高频 attr
| API | 说明 |
|---|---|
flexDirectionColumn() / Row() | 垂直 / 水平滚动 |
pagingEnable(true) | 整页翻动(轮播图) |
showScrollerIndicator(true) | 显示滚动条 |
bounceEnable(true) | iOS 弹性效果 |
pageData("itemSize", 100) | 翻页步长 |
4.8.3 高频 event
| 事件 | 说明 |
|---|---|
scroll { e -> } | 滚动中(高频,慎重) |
scrollEnd { e -> } | 滚动结束 |
dragBegin { e -> } | 用户开始拖动 |
dragEnd { e -> } | 用户停止拖动 |
momentumScrollEnd { e -> } | 惯性滚动结束 |
4.8.4 横向滚动 Tab
kotlin
Scroller {
attr {
flexDirectionRow()
height(40f)
showScrollerIndicator(false)
}
listOf("推荐", "热门", "数码", "服饰", "美妆", "家居", "汽车").forEach { tab ->
View {
attr {
paddingHorizontal(20f)
allCenter()
}
event { click { /* 切换 */ } }
Text {
attr {
text(tab)
fontSize(14f)
color(Color.BLACK)
}
}
}
}
}⚠️ 重要:长列表(>20 个 item)不要用 Scroller,会一次性渲染所有子组件,性能差。要用
List/PageList,第 6 章会详细讲。
4.9 综合实战:登录页
把前面的组件综合用起来:
kotlin
@Page("LoginPage")
internal class LoginPage : Pager() {
private var phone by observable("")
private var code by observable("")
private var loading by observable(false)
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
backgroundColor(Color(0xFFF5F5F7L))
paddingTop(60f)
paddingHorizontal(24f)
}
// ─── Logo ──────────────────
Image {
attr {
src("logo")
size(80f, 80f)
alignSelfCenter()
marginBottom(16f)
}
}
// ─── 标题 ──────────────────
Text {
attr {
text("欢迎回来")
fontSize(24f)
fontWeightBold()
textAlignCenter()
marginBottom(40f)
}
}
// ─── 手机号输入 ────────────
Input {
attr {
height(48f)
placeholder("请输入手机号")
placeholderColor(Color(0xFFBDBDBDL))
keyboardTypeNumber()
maxLength(11)
paddingHorizontal(16f)
backgroundColor(Color.WHITE)
borderRadius(8f)
fontSize(15f)
marginBottom(12f)
value(ctx.phone)
}
event {
onTextChange { v -> ctx.phone = v }
}
}
// ─── 验证码输入 + 发送按钮 ─
View {
attr { flexDirectionRow(); marginBottom(24f) }
Input {
attr {
flex(1f)
height(48f)
placeholder("验证码")
keyboardTypeNumber()
maxLength(6)
paddingHorizontal(16f)
backgroundColor(Color.WHITE)
borderRadius(8f)
fontSize(15f)
marginRight(12f)
value(ctx.code)
}
event { onTextChange { v -> ctx.code = v } }
}
View {
attr {
width(96f); height(48f)
allCenter()
backgroundColor(Color.WHITE)
borderRadius(8f)
border(Border(1f, BorderStyle.SOLID, Color(0xFF1976D2L)))
}
event { click { /* TODO 发验证码 */ } }
Text {
attr {
text("获取验证码")
color(Color(0xFF1976D2L))
fontSize(13f)
}
}
}
}
// ─── 登录按钮 ──────────────
val canLogin = ctx.phone.length == 11 && ctx.code.length == 6
View {
attr {
height(48f)
allCenter()
backgroundColor(
if (canLogin) Color(0xFF1976D2L) else Color(0xFFBDBDBDL)
)
borderRadius(24f)
opacity(if (canLogin) 1f else 0.6f)
}
event {
click {
if (!canLogin || ctx.loading) return@click
ctx.loading = true
// 登录请求...
ctx.loading = false
}
}
Text {
attr {
text(if (ctx.loading) "登录中..." else "登 录")
color(Color.WHITE)
fontSize(16f)
fontWeightBold()
}
}
}
}
}
}4.10 组件设计哲学:什么时候用什么
| 场景 | 选谁 | 为啥 |
|---|---|---|
| 包一组子元素 | View | 通用容器 |
| 内容会超出屏幕 | Scroller | 滚动支持 |
| 可能有上千个 item | List / PageList | 复用机制 |
| 显示文字 | Text | 性能最优 |
| 显示图片 | Image | 自带懒加载 / 缓存 |
| 输入单行 | Input | 输入法支持 |
| 输入多行 | TextArea | 自动换行 + 字数统计 |
| 简单按钮 | Button | 一行写完 |
| 复杂按钮 / icon 按钮 | View + click | 灵活拼装 |
| 圆形头像 | Image + borderRadius | 没有专门 CircleImage |
| Tab 栏 | Scroller (row) | 横向滚动 |
| 复杂自绘图形 | Canvas | 折线图 / 雷达图 |
4.11 章末小结
★ 第 4 章组件零件箱 ★
│
┌──────────────┬─────┴──────┬──────────────┐
│ │ │ │
┌────▼────┐ ┌───▼────┐ ┌───▼────┐ ┌───▼─────┐
│ View │ │ Text │ │ Image │ │ Input │
├─────────┤ ├────────┤ ├────────┤ ├─────────┤
│ 容器 │ │ 文本 │ │ 图片 │ │ 单行输入│
│ Flex │ │ 富文本 │ │ 圆形 │ │ 双向绑定│
│ 渐变 │ │ 省略 │ │ 模糊 │ │ 键盘类型│
└─────────┘ └────────┘ └────────┘ └─────────┘
│
┌───────────────┴────────────────┐
│ │
┌────▼─────┐ ┌─────▼─────┐
│ Button │ │ Scroller │
├──────────┤ ├───────────┤
│ View 封装│ │ 可滚动容器 │
│ titleAttr│ │ pagingEnable│
│ 三态 │ │ 横向 Tab │
└──────────┘ └───────────┘🎤 4.12 章末面试题(10 道高频题)
Q1. Kuikly 中 View 跟 Android 的 View 是同一个东西吗?
答:不是。Kuikly 的 View 是 DSL 中的一个组件描述符,是 Kotlin 类,跑在 commonMain,与平台无关。它的语义类似 HTML <div> —— 通用容器。
底层渲染时,Kuikly Android 端会把 View 翻译成 android.view.ViewGroup 的某个子类,iOS 端翻译成 UIView,鸿蒙端翻译成对应的 ArkUI 组件。
Q2. Image 的 src 支持哪些格式?
答:3 种:
- 网络图:
https://.../http://... - 本地资源:直接传资源名(不带后缀),Android 找
res/drawable,iOS 找Assets.xcassets - Base64:
data:image/png;base64,xxxx
Q3. Image 必须设 size 吗?
答:必须。Kuikly 不像 HTML 那样能"按图片自身尺寸渲染"。如果不设 size,图片会渲染为 0x0 看不见。
如果想"按图片宽高比自适应",可以:
- 知道宽高比 → 写
size(W, W * ratio) - 不知道 → 用
onLoadFinish { e -> }回调拿到e.width / e.height后再赋值
Q4. Input 怎么实现「双向绑定」?Compose 也是这么写的吗?
答:通过 observable 字段 + value/onTextChange 配对:
kotlin
private var text by observable("")
Input {
attr { value(ctx.text) }
event { onTextChange { v -> ctx.text = v } }
}Compose 的写法本质类似:
kotlin
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })这是声明式 UI 框架的"标配模式" —— 状态作为单向数据源,事件回调把变化写回状态。
Q5. 长列表为啥不能用 Scroller?
答:因为 Scroller 会一次性渲染所有子组件,没有复用机制。
如果你在 Scroller 里塞 1000 个 item:
- 启动时 1000 个组件全部走 BuildTree → RenderTree → 创建 1000 个原生控件,首屏时间和内存爆炸
- 滚动时所有 item 都在 DOM 中,每帧都要重排
正确做法:用 List / PageList,它们有「只渲染可视区 ± 缓冲区,离开屏幕就回收」的复用机制(第 6 章详细讲)。
Q6. Button 跟自己用 View + click 拼有什么区别?
答:几乎没区别。Button 只是 Kuikly 提供的「快捷套装」 —— 内部就是 View + Text + click 的预封装。
- 简单按钮(背景色 + 文字 + 点击)→ 用 Button 一行搞定
- 复杂按钮(图标 + loading 状态 + 多行文字)→ 直接用 View 拼,更灵活
- 性能上无任何差异
Q7. 如何让一个 Text 最多显示 2 行,超出用 ... ?
答:3 个 attr 配合:
kotlin
Text {
attr {
text("一段很长的文字...")
numberOfLines(2) // ★ 限两行
textOverFlowEllipsis() // ★ 末尾 ...
lineHeight(20f) // 给个行高更好看
}
}Q8. 想在背景图上加毛玻璃效果怎么做?
答:Image 自带 blurRadius 属性,配 absolutePosition 做层叠:
kotlin
View {
Image {
attr {
src("cover.jpg")
absolutePosition(top = 0f, left = 0f, right = 0f, bottom = 0f)
blurRadius(20f) // ★ 高斯模糊半径
opacity(0.6f)
}
}
// 其他子组件叠在上面...
}如果整张图都不要清晰版,可以直接给 Image 设 blur,性能比"两层 Image"好。
Q9. Scroller 里横向 Tab 滚不动是为啥?
答:检查 4 件事:
- 父容器的
flexDirection是不是 row(不然子组件会撑开 Scroller 的高度而不是宽度) - Scroller 的
flexDirectionRow()设了没 - 子组件总宽度是不是 < Scroller 宽度(如果总宽度还没超出,确实滚不动)
- 父级有没有
overflow:hidden之类把内容裁切了(罕见)
Q10. 如何在用户输入手机号时实时校验、错的输入框红框提示?
答:用 observable + 派生状态 + attr 里读派生状态控制 border:
kotlin
private var phone by observable("")
private val phoneInvalid get() = phone.isNotEmpty() && !phone.matches(Regex("^1[3-9]\\d{9}\$"))
Input {
attr {
value(ctx.phone)
height(44f)
paddingHorizontal(12f)
backgroundColor(Color.WHITE)
borderRadius(8f)
border(
Border(
1f,
BorderStyle.SOLID,
if (ctx.phoneInvalid) Color.RED else Color(0xFFE0E0E0L)
)
)
}
event { onTextChange { v -> ctx.phone = v } }
}
if (ctx.phoneInvalid) {
Text {
attr {
text("手机号格式不对")
color(Color.RED)
fontSize(12f)
marginTop(4f)
}
}
}派生属性会随 phone 变化而自动更新,这是响应式 UI 的精髓。下一章会专门讲。
下一站 → 第 5 章 · 响应式状态 observable / observableList →
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
kotlin
/**
* 第 4 章配套代码 · 组件全家福(每个基础组件最小可运行示例)
*
* 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/ComponentGalleryPage.kt
*
* 一个 Pager 展示所有基础组件,方便对照查阅。
* 学习时建议在 IDE 里一段段注释/取消注释,观察效果。
*/
package com.example.kuikly.pages
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Border
import com.tencent.kuikly.core.base.BorderStyle
import com.tencent.kuikly.core.base.BoxShadow
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ColorStop
import com.tencent.kuikly.core.base.Direction
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.views.Image
import com.tencent.kuikly.core.views.Scroller
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View
@Page("ComponentGalleryPage")
internal class ComponentGalleryPage : Pager() {
override fun body(): ViewBuilder = {
Scroller {
attr {
flex(1f)
backgroundColor(Color(0xFFF5F5F7L))
paddingHorizontal(16f)
paddingTop(20f)
}
// ───── 1. View:渐变 + 阴影卡片 ─────
sectionTitle("1. View / 渐变 / 阴影")
View {
attr {
height(100f)
borderRadius(12f)
allCenter()
backgroundLinearGradient(
Direction.TO_RIGHT,
ColorStop(Color(0xFF667EEAL), 0f),
ColorStop(Color(0xFF764BA2L), 1f),
)
boxShadow(BoxShadow(0f, 6f, 12f, Color(0x40000000L)))
marginBottom(20f)
}
Text {
attr {
text("Premium Card")
color(Color.WHITE)
fontSize(20f)
fontWeightBold()
}
}
}
// ───── 2. Text:富文本 + 限行 ─────
sectionTitle("2. Text / 限行省略")
View {
attr {
backgroundColor(Color.WHITE)
padding(12f)
borderRadius(8f)
marginBottom(20f)
}
Text {
attr {
text(
"Kuikly 是腾讯开源的、基于 Kotlin Multiplatform 的跨端 UI 框架。" +
"支持 Android / iOS / HarmonyOS / Web / 小程序 / macOS 6 端通用," +
"采用原生控件渲染,包体小、性能贴近原生。"
)
fontSize(14f)
color(Color.BLACK)
lineHeight(22f)
numberOfLines(2)
textOverFlowEllipsis()
}
}
}
// ───── 3. Image:圆形头像 ─────
sectionTitle("3. Image / 圆形头像")
View {
attr {
flexDirectionRow()
alignItemsCenter()
marginBottom(20f)
}
Image {
attr {
src("https://example.com/avatar.png")
size(60f, 60f)
borderRadius(30f)
backgroundColor(Color(0xFFE0E0E0L))
marginRight(12f)
}
}
View {
attr { flex(1f) }
Text { attr { text("张三"); fontSize(16f); fontWeightBold() } }
Text {
attr {
text("圆形头像 = 正方形 + borderRadius(size/2)")
fontSize(12f)
color(Color(0xFF9E9E9EL))
marginTop(4f)
}
}
}
}
// ───── 4. Image:毛玻璃背景 ─────
sectionTitle("4. Image / 高斯模糊")
View {
attr {
height(120f)
borderRadius(12f)
backgroundColor(Color(0xFFCFD8DCL))
marginBottom(20f)
}
Image {
attr {
src("https://example.com/cover.jpg")
absolutePosition(top = 0f, left = 0f, right = 0f, bottom = 0f)
borderRadius(12f)
blurRadius(20f)
opacity(0.7f)
}
}
Text {
attr {
text("毛玻璃标题")
color(Color.WHITE)
fontSize(20f)
fontWeightBold()
absolutePosition(bottom = 16f, left = 16f)
}
}
}
// ───── 5. View + Text:带边框圆角胶囊 ─────
sectionTitle("5. 标签 / 胶囊(View + Text)")
View {
attr {
flexDirectionRow()
flexWrapWrap()
marginBottom(20f)
}
listOf(
"Kotlin" to 0xFF7F52FFL,
"Compose" to 0xFF1976D2L,
"KMP" to 0xFFFF5722L,
"Flexbox" to 0xFF388E3CL,
).forEach { (name, color) ->
View {
attr {
paddingHorizontal(12f)
paddingVertical(4f)
marginRight(8f)
marginBottom(8f)
borderRadius(12f)
border(Border(1f, BorderStyle.SOLID, Color(color)))
}
Text {
attr {
text(name)
fontSize(12f)
color(Color(color))
}
}
}
}
}
// ───── 6. View:模拟按钮三态 ─────
sectionTitle("6. 按钮三态(View + click)")
View { attr { flexDirectionRow(); marginBottom(20f) } }
renderButton("正常按钮", Color(0xFF1976D2L), 1f, "✓")
renderButton("禁用按钮", Color(0xFFBDBDBDL), 0.6f, "✕")
renderButton("加载中", Color(0xFF90CAF9L), 1f, "⟳")
}
}
/** 抽出来的「区块标题」ViewBuilder */
private fun sectionTitle(title: String): ViewBuilder = {
Text {
attr {
text(title)
fontSize(14f)
fontWeightBold()
color(Color(0xFF424242L))
marginBottom(8f)
}
}
}
/** 抽出来的「按钮」ViewBuilder(演示参数化复用) */
private fun renderButton(
title: String,
bg: Color,
opacity: Float,
icon: String,
): ViewBuilder = {
View {
attr {
height(40f)
allCenter()
backgroundColor(bg)
opacity(opacity)
borderRadius(20f)
marginBottom(8f)
}
event { click { /* TODO */ } }
Text {
attr {
text("$icon $title")
color(Color.WHITE)
fontSize(14f)
fontWeightBold()
}
}
}
}
}kotlin
/**
* 第 4 章配套代码 · 登录页(综合 Input/Image/Text/View 演示)
*
* 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/LoginPage.kt
*
* 本示例覆盖 6 大基础组件中的 4 个:View / Image / Text / Input
* 重点演示:
* - Input 双向绑定(value + onTextChange)
* - 派生属性(canLogin = phone 11 位 + 验证码 6 位)
* - 按钮 3 态(可点 / 不可点 / 加载中)
* - Flexbox 行内布局(验证码输入 + 发送按钮)
*/
package com.example.kuikly.pages
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Border
import com.tencent.kuikly.core.base.BorderStyle
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.views.Image
import com.tencent.kuikly.core.views.Input
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View
@Page("LoginPage")
internal class LoginPage : Pager() {
// ───── 表单状态(observable 委托 → 自动响应式)─────
private var phone by observable("")
private var code by observable("")
private var loading by observable(false)
private var countdown by observable(0) // 验证码倒计时
// ───── 派生属性:表单是否可提交 ─────
private val canLogin: Boolean
get() = phone.length == 11 &&
phone.matches(Regex("^1[3-9]\\d{9}$")) &&
code.length == 6
private val phoneInvalid: Boolean
get() = phone.isNotEmpty() && !phone.matches(Regex("^1[3-9]\\d{9}$"))
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
backgroundColor(Color(0xFFF5F5F7L))
paddingTop(60f)
paddingHorizontal(24f)
}
// ─── Logo ─────────────────────────────
Image {
attr {
src("logo")
size(80f, 80f)
alignSelfCenter()
marginBottom(16f)
backgroundColor(Color(0xFFE3F2FDL))
borderRadius(16f)
}
}
// ─── 标题 ─────────────────────────────
Text {
attr {
text("欢迎回来")
fontSize(24f)
fontWeightBold()
color(Color.BLACK)
textAlignCenter()
marginBottom(8f)
}
}
Text {
attr {
text("使用手机号 + 验证码登录")
fontSize(13f)
color(Color(0xFF9E9E9EL))
textAlignCenter()
marginBottom(32f)
}
}
// ─── 手机号输入 ───────────────────────
Input {
attr {
height(48f)
placeholder("请输入手机号")
placeholderColor(Color(0xFFBDBDBDL))
keyboardTypeNumber()
maxLength(11)
paddingHorizontal(16f)
backgroundColor(Color.WHITE)
borderRadius(8f)
fontSize(15f)
color(Color.BLACK)
marginBottom(8f)
value(ctx.phone)
border(
Border(
lineWidth = 1f,
lineStyle = BorderStyle.SOLID,
color = if (ctx.phoneInvalid) Color.RED
else Color(0xFFE0E0E0L)
)
)
}
event {
onTextChange { params ->
ctx.phone = params.text
}
}
}
// 错误提示(条件渲染)
if (ctx.phoneInvalid) {
Text {
attr {
text("⚠ 手机号格式不正确")
color(Color.RED)
fontSize(12f)
marginBottom(12f)
marginLeft(4f)
}
}
}
// ─── 验证码输入 + 发送按钮 ────────────
View {
attr {
flexDirectionRow()
marginTop(if (ctx.phoneInvalid) 0f else 4f)
marginBottom(24f)
}
Input {
attr {
flex(1f)
height(48f)
placeholder("6 位验证码")
placeholderColor(Color(0xFFBDBDBDL))
keyboardTypeNumber()
maxLength(6)
paddingHorizontal(16f)
backgroundColor(Color.WHITE)
borderRadius(8f)
fontSize(15f)
color(Color.BLACK)
marginRight(12f)
value(ctx.code)
}
event {
onTextChange { params ->
ctx.code = params.text
}
}
}
// 发送验证码按钮
val canSendCode = ctx.phone.length == 11 &&
!ctx.phoneInvalid &&
ctx.countdown == 0
View {
attr {
width(108f); height(48f)
allCenter()
backgroundColor(Color.WHITE)
borderRadius(8f)
border(
Border(
lineWidth = 1f,
lineStyle = BorderStyle.SOLID,
color = if (canSendCode) Color(0xFF1976D2L)
else Color(0xFFBDBDBDL)
)
)
opacity(if (canSendCode) 1f else 0.6f)
}
event {
click {
if (!canSendCode) return@click
// TODO 调发送验证码 API + 启动倒计时
ctx.countdown = 60
}
}
Text {
attr {
text(
if (ctx.countdown > 0) "${ctx.countdown}s 后重发"
else "获取验证码"
)
color(if (canSendCode) Color(0xFF1976D2L)
else Color(0xFF9E9E9EL))
fontSize(13f)
}
}
}
}
// ─── 登录按钮(3 态:禁用 / 加载 / 可点)──
View {
attr {
height(48f)
allCenter()
backgroundColor(
when {
ctx.loading -> Color(0xFF90CAF9L)
ctx.canLogin -> Color(0xFF1976D2L)
else -> Color(0xFFBDBDBDL)
}
)
borderRadius(24f)
opacity(if (ctx.canLogin || ctx.loading) 1f else 0.6f)
}
event {
click {
if (!ctx.canLogin || ctx.loading) return@click
ctx.loading = true
// TODO 调登录 API
// 完成回调里:ctx.loading = false; 跳首页
}
}
Text {
attr {
text(if (ctx.loading) "登录中…" else "登 录")
color(Color.WHITE)
fontSize(16f)
fontWeightBold()
}
}
}
// ─── 底部协议提示 ─────────────────────
Text {
attr {
text("登录即视为同意《用户协议》和《隐私政策》")
fontSize(11f)
color(Color(0xFF9E9E9EL))
textAlignCenter()
marginTop(20f)
}
}
}
}
}