Skip to content

第 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)内 / 外间距
FlexflexDirectionRow() 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滚动支持
可能有上千个 itemList / 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 种:

  1. 网络图https://... / http://...
  2. 本地资源:直接传资源名(不带后缀),Android 找 res/drawable,iOS 找 Assets.xcassets
  3. Base64data: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 件事:

  1. 父容器的 flexDirection 是不是 row(不然子组件会撑开 Scroller 的高度而不是宽度)
  2. Scroller 的 flexDirectionRow() 设了没
  3. 子组件总宽度是不是 < Scroller 宽度(如果总宽度还没超出,确实滚不动)
  4. 父级有没有 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)
                }
            }
        }
    }
}

ComponentGalleryPage.kt ↗ · LoginPage.kt ↗