从原生组件到自定义组件
掌握了 Column、Row、List 这些基础组件后,下一步是把重复出现的界面块抽成自己的组件。自定义组件有两个价值:一是复用,二是把样式与状态封装起来,让页面主体只关心业务。ArkUI 的声明式写法天然利于组件化——一个 struct 就是一块界面。
进阶的关键不是会用多少内置组件,而是能把一屏界面拆成职责清晰、可独立复用的若干自定义组件。
这篇从组件封装讲到动画与转场,目标是让界面既好看又顺滑。
@Builder 与组件封装
用 struct 声明一个自定义组件,外部通过参数传入内容,内部用 build 方法描述布局。简单的局部复用可以直接用 @Builder 装饰的函数,避免为一小段布局专门建 struct。
@Component
struct SectionCard {
@Prop title: string
@Builder content() {
// 由调用方传入
}
build() {
Column({ space: 12 }) {
Text(this.title).fontSize(18).fontWeight(FontWeight.Bold)
this.content()
}.padding(16).backgroundColor(Color.White).borderRadius(16)
}
}
这样 SectionCard 就成了一个带标题的卡片容器,任何页面都能复用它的圆角、留白与阴影,只换标题和内容。
状态在组件间的传递
自定义组件好不好用,取决于状态传递是否清晰。ArkUI 提供一组装饰器来描述数据流的方向:@Prop 是父到子的单向只读,@Link 是父子双向绑定,@State 是组件内部自己的状态,@Provide/@Consume 做跨层级共享。
- @State组件内部状态,变了就刷新。
- @Prop父传子、单向、只读副本。
- @Link父子双向同步同一份状态。
- @Provide/@Consume跨多层级共享,避免层层透传。
原则上:能单向就别双向,能内部就别外泄。状态边界清晰,界面才好维护。
属性动画与显式动画
ArkUI 的动画分两种思路。属性动画给属性加 animateTo,状态一变,属性自动从旧值平滑过渡到新值;显式动画用 animateTo 包一段状态修改,指定曲线与时长。
Button("展开")
.onClick(() => {
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
this.expanded = !this.expanded
})
})
Text("内容").height(this.expanded ? 120 : 0).opacity(this.expanded ? 1 : 0)
改一个布尔值,高度和透明度就自动动起来了。曲线选 EaseOut 更贴近人对"快速到位再缓停"的直觉。
页面转场与共享元素
页面跳转默认是整页淡入,略显生硬。用 transition 给进入退出加缩放与位移,特别是让两张页面里的同一张卡片做共享元素转场——点列表卡片,它放大铺成详情页,视觉上连贯很多。
核心是给共享元素设同一个 transitionId,系统会自动在两个页面间补间。别给所有元素都加转场,挑关键的一两个就够。
手势联动动画
把手势和动画接起来,界面就有了"跟手"的质感。用 PanGesture 拿到偏移量,实时驱动组件的平移与透明度;抬手时用 animateTo 让它回弹到目标位置。下拉刷新、侧滑删除都是这套模式。
- 手势进行中:直接改状态,跟手渲染。
- 手势结束:按速度与位置判断,动画归位。
- 注意不要在手势回调里做重计算,保持每帧轻量。
性能与卡顿优化
动画掉帧往往不是动画本身慢,而是每帧做了太多事。常见做法:把动画作用在 transform 与 opacity 这类合成层属性上,避免触发布局重排;长列表里控制同时进行的动画数量;列表项用 key 稳定标识,减少不必要的重建。
用 DevEco Studio 的性能工具跑一遍动画页面,看帧率曲线。低于 60 帧就逐帧排查,通常是某段在主线程做了同步 IO 或大数据计算。
实战:一个可展开卡片
把组件化、状态与动画串起来:做一个点击展开详情的卡片。
@Component
struct ExpandCard {
@State private var open = false
build() {
Column() {
Text("标题").onClick(() => {
animateTo({ duration: 260 }, () => { this.open = !this.open })
})
if (this.open) {
Text("这里是展开后的长内容……")
.transition(Transition.opacity.combine(Transition.scale))
}
}
}
}
点击标题,内容以淡入加缩放的方式展开;再点收起。组件自包含状态,页面里直接复用即可。这就是从原生组件走向自定义 UI 的完整一步:封装、传态、动效,再加一点点性能意识,界面就既有质感又跟手。