HarmonyOS开发:自定义动效设计与品牌化动效系统
HarmonyOS开发:自定义动效设计与品牌化动效系统
📌 核心要点:从零构建一套属于你自己品牌的动效Token系统,让每一个动画细节都传递品牌个性,而不是千篇一律的默认效果。
一、背景与动机
你打开两个不同的App,发现它们的按钮点击效果一模一样,页面转场一模一样,下拉刷新一模一样——就像走进了两家装修完全一样的餐厅,连菜单都一样。这能叫品牌吗?
动效是品牌的"肢体语言"。就像一个人走路、挥手、转身的姿态能让你认出他是谁,一个App的动效风格也应该让用户感受到品牌的独特气质。微信的"抖一抖"和抖音的"滑动切换"完全不同,这就是品牌化动效的力量。
然而,HarmonyOS默认提供的动画曲线和转场效果是"通用型"的——它们适合大多数场景,但不属于任何品牌。如果你想让你的App拥有独特的动效DNA,就需要建立一套品牌化动效系统:自定义曲线、自定义转场、动效Token,以及将这些Token与品牌色深度结合。
这篇文章,我们就来一步步搭建这套系统。
二、核心原理
2.1 品牌化动效设计原则
品牌化动效不是"越花哨越好",它需要遵循三个核心原则:
graph TD
A[品牌化动效三原则]:::primary --> B[一致性<br/>Consistency]:::info
A --> C[表达性<br/>Expressiveness]:::info
A --> D[克制性<br/>Restraint]:::info
B --> B1[同一品牌下所有动效<br/>遵循统一规则]:::success
C --> C1[动效传递品牌个性<br/>活泼/沉稳/科技感]:::success
D --> D1[动效服务于功能<br/>不喧宾夺主]:::success
B1 --> E[示例:所有按钮使用<br/>同一条弹性曲线]:::warning
C1 --> F[示例:金融App用沉稳曲线<br/>社交App用弹性曲线]:::warning
D1 --> G[示例:核心操作有动效<br/>辅助操作无动效]:::warning
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef success fill:#009688,stroke:#00796B,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
一致性:你的App里,所有按钮的点击反馈应该用同一条曲线,所有页面的转场应该用同一个时长范围,所有元素的出场顺序应该遵循同一套编排规则。用户不需要思考"这个按钮为什么反应不一样"。
表达性:动效是品牌个性的放大器。一个面向儿童的App可以用弹性十足的曲线,一个金融App则应该用沉稳克制的缓动。你的品牌是"活泼"还是"专业"?动效应该回答这个问题。
克制性:不是每个元素都需要动效。核心交互有动效,辅助操作可以没有。动效太多就像一个人说话时手舞足蹈——分散注意力,降低信息传达效率。
2.2 自定义动画曲线(CubicBezier)
动画曲线是动效的"灵魂"。HarmonyOS通过ICurve接口和Curve.cubicBezierCurve()方法支持自定义贝塞尔曲线。
一条三次贝塞尔曲线由4个控制点定义:P0(0,0)、P1(x1,y1)、P2(x2,y2)、P3(1,1)。我们只需要指定中间两个控制点P1和P2:
cubic-bezier(x1, y1, x2, y2)
其中x1、x2的值必须在[0,1]范围内,y1、y2可以超出这个范围(超出时曲线会有"回弹"效果)。
不同品牌可以定义不同的曲线特征:
| 品牌类型 | 曲线特征 | 示例值 |
|---|---|---|
| 科技品牌 | 快速响应,干净利落 | (0.1, 0.0, 0.2, 1.0) |
| 生活品牌 | 温和舒适,有弹性 | (0.4, 0.0, 0.2, 1.4) |
| 金融品牌 | 沉稳可靠,无弹性 | (0.4, 0.0, 0.6, 1.0) |
| 儿童品牌 | 活泼跳跃,强弹性 | (0.5, 1.5, 0.5, 1.0) |
2.3 动效Token系统设计
动效Token是将动效参数"设计系统化"的核心机制。就像颜色有Color Token一样,动效也应该有自己的Token体系:
Motion Token 层级结构:
├── Duration(时长)
│ ├── duration-instant: 0ms
│ ├── duration-short1: 50ms
│ ├── duration-short2: 100ms
│ ├── duration-short3: 150ms
│ ├── duration-medium1: 200ms
│ ├── duration-medium2: 300ms
│ ├── duration-long1: 400ms
│ ├── duration-long2: 500ms
│ └── duration-extra-long: 700ms
├── Easing(缓动)
│ ├── easing-standard: cubic-bezier(0.2, 0, 0, 1)
│ ├── easing-emphasized: cubic-bezier(0.2, 0, 0, 1)
│ ├── easing-brand-primary: 自定义品牌曲线1
│ └── easing-brand-secondary: 自定义品牌曲线2
├── Choreography(编排)
│ ├── stagger-short: 30ms
│ ├── stagger-medium: 50ms
│ └── stagger-long: 80ms
└── Scale(缩放系数)
├── scale-touch: 0.97
├── scale-press: 0.95
└── scale-brand-bounce: 1.05
三、代码实战
3.1 基础用法:自定义动画曲线
先从最基础的自定义曲线开始,为品牌定义专属的缓动函数:
// 品牌化动效Token定义
export class BrandMotionTokens {
// ====== 时长Token ======
static readonly DURATION_INSTANT: number = 0
static readonly DURATION_SHORT: number = 100
static readonly DURATION_MEDIUM: number = 250
static readonly DURATION_LONG: number = 400
static readonly DURATION_EXTRA_LONG: number = 600
// ====== 自定义缓动曲线 ======
// 品牌主曲线 - 带有轻微弹性的温暖感
static readonly EASING_BRAND_PRIMARY: ICurve =
Curve.cubicBezierCurve(0.4, 0.0, 0.2, 1.3)
// 品牌辅助曲线 - 干净利落的科技感
static readonly EASING_BRAND_SECONDARY: ICurve =
Curve.cubicBezierCurve(0.1, 0.0, 0.2, 1.0)
// 品牌强调曲线 - 强弹性,用于重要反馈
static readonly EASING_BRAND_EMPHASIS: ICurve =
Curve.cubicBezierCurve(0.5, 1.6, 0.4, 0.8)
// ====== 编排Token ======
static readonly STAGGER_SHORT: number = 30
static readonly STAGGER_MEDIUM: number = 50
static readonly STAGGER_LONG: number = 80
// ====== 缩放Token ======
static readonly SCALE_TOUCH: number = 0.97
static readonly SCALE_PRESS: number = 0.93
static readonly SCALE_BOUNCE: number = 1.08
// ====== 品牌色与动效结合 ======
static readonly BRAND_COLOR_PRIMARY: string = '#FF6B35'
static readonly BRAND_COLOR_SECONDARY: string = '#004E89'
static readonly BRAND_GLOW_COLOR: string = '#FF6B3540' // 带透明度的品牌色
}
// 使用品牌曲线的按钮组件
@Component
struct BrandButton {
@Prop text: string = ''
@Prop type: 'primary' | 'secondary' = 'primary'
@State isPressed: boolean = false
private onTap?: () => void
build() {
Row() {
Text(this.text)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.type === 'primary' ? Color.White : BrandMotionTokens.BRAND_COLOR_PRIMARY)
}
.padding({ horizontal: 24, vertical: 12 })
.backgroundColor(this.type === 'primary' ? BrandMotionTokens.BRAND_COLOR_PRIMARY : Color.Transparent)
.borderWidth(this.type === 'secondary' ? 2 : 0)
.borderColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
.borderRadius(24)
// 品牌化缩放效果
.scale({
x: this.isPressed ? BrandMotionTokens.SCALE_PRESS : 1.0,
y: this.isPressed ? BrandMotionTokens.SCALE_PRESS : 1.0
})
// 品牌化阴影效果
.shadow(this.isPressed ? null : {
radius: this.type === 'primary' ? 12 : 0,
color: BrandMotionTokens.BRAND_GLOW_COLOR,
offsetY: 4
})
.onClick(() => {
// 按下动画 - 使用品牌强调曲线
animateTo({
duration: BrandMotionTokens.DURATION_SHORT,
curve: BrandMotionTokens.EASING_BRAND_EMPHASIS
}, () => {
this.isPressed = true
})
// 回弹动画 - 使用品牌主曲线
setTimeout(() => {
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: BrandMotionTokens.EASING_BRAND_PRIMARY
}, () => {
this.isPressed = false
})
}, 80)
this.onTap?.()
})
}
}
3.2 进阶用法:自定义转场动画与品牌色结合
品牌化转场不仅仅是换一条曲线,更是将品牌色彩融入转场过程。比如,页面切换时使用品牌色作为过渡色,或者在转场中加入品牌标志性的动效元素:
// 品牌化转场动画系统
export class BrandTransitionManager {
// 品牌化容器转换 - 带品牌色扩散效果
static containerTransformWithBrand(
sharedId: string,
brandColor: string,
duration: number = BrandMotionTokens.DURATION_LONG
): void {
animateTo({
duration: duration,
curve: BrandMotionTokens.EASING_BRAND_PRIMARY,
onFinish: () => {
console.info('品牌容器转换完成')
}
}, () => {
// 状态变更由调用方处理
})
}
// 品牌化共享轴转场 - 带品牌色渐变
static sharedAxisWithBrand(
axis: 'x' | 'y',
direction: 'forward' | 'backward',
brandColor: string
): TransitionEffect {
let offsetX = axis === 'x' ? (direction === 'forward' ? 60 : -60) : 0
let offsetY = axis === 'y' ? (direction === 'forward' ? 60 : -60) : 0
return TransitionEffect.OPACITY
.combine(TransitionEffect.translate({ x: offsetX, y: offsetY }))
.combine(TransitionEffect.backgroundColor(brandColor))
}
// 品牌化淡入淡出 - 带品牌色中间帧
static fadeThroughWithBrand(brandColor: string): TransitionEffect {
return TransitionEffect.OPACITY
.combine(TransitionEffect.backgroundColor(brandColor))
}
}
// 品牌化页面转场完整示例
@Entry
@Component
struct BrandTransitionDemo {
@State currentPage: number = 0
@State transitionProgress: number = 0
@State showBrandOverlay: boolean = false
// 页面数据
private pages: PageData[] = [
{ id: 0, title: '首页', color: '#FF6B35', icon: '🏠' },
{ id: 1, title: '探索', color: '#004E89', icon: '🔍' },
{ id: 2, title: '我的', color: '#2EC4B6', icon: '👤' }
]
build() {
Stack() {
// 页面内容
Column() {
if (this.currentPage === 0) {
this.HomePage()
} else if (this.currentPage === 1) {
this.ExplorePage()
} else {
this.ProfilePage()
}
}
.width('100%')
.height('100%')
// 品牌色转场覆盖层
if (this.showBrandOverlay) {
Column() {
// 品牌Logo或标志性元素
Text('B')
.fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.width('100%')
.height('100%')
.backgroundColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
.justifyContent(FlexAlign.Center)
.opacity(0.9)
.transition(TransitionEffect.OPACITY)
}
// 底部导航
this.BrandBottomNav()
}
.width('100%')
.height('100%')
}
// ====== 首页 ======
@Builder HomePage() {
Scroll() {
Column({ space: 16 }) {
// 品牌标题
Text('欢迎回来')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
// 品牌化卡片列表
ForEach(['今日推荐', '热门活动', '新品上线'], (item: string, index: number) => {
this.BrandCard(item, index)
})
}
.padding(20)
}
.layoutWeight(1)
.width('100%')
.transition(
BrandTransitionManager.sharedAxisWithBrand('x', 'forward', BrandMotionTokens.BRAND_COLOR_PRIMARY)
)
}
// ====== 品牌化卡片 ======
@Builder BrandCard(title: string, index: number) {
Row() {
Column() {
Text(title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#212121')
Text('查看详情 →')
.fontSize(13)
.fontColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 品牌色装饰圆
Circle()
.width(48)
.height(48)
.fill(BrandMotionTokens.BRAND_GLOW_COLOR)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({ radius: 8, color: '#0a000000', offsetY: 2 })
// 品牌化出场动画 - 交错编排
.transition(
TransitionEffect.OPACITY
.combine(TransitionEffect.translate({ y: 30 }))
.combine(TransitionEffect.scale({ x: 0.95, y: 0.95 }))
)
.onClick(() => {
// 品牌化点击反馈
animateTo({
duration: BrandMotionTokens.DURATION_SHORT,
curve: BrandMotionTokens.EASING_BRAND_EMPHASIS
}, () => {
// 缩放反馈
})
})
}
@Builder ExplorePage() {
Column() {
Text('探索页面')
.fontSize(24)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.transition(
BrandTransitionManager.sharedAxisWithBrand('x', 'forward', BrandMotionTokens.BRAND_COLOR_SECONDARY)
)
}
@Builder ProfilePage() {
Column() {
Text('个人中心')
.fontSize(24)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.transition(
BrandTransitionManager.fadeThroughWithBrand(BrandMotionTokens.BRAND_COLOR_PRIMARY)
)
}
// ====== 品牌化底部导航 ======
@Builder BrandBottomNav() {
Row() {
ForEach(this.pages, (page: PageData, index: number) => {
Column() {
Text(page.icon)
.fontSize(24)
Text(page.title)
.fontSize(11)
.fontColor(this.currentPage === index ? BrandMotionTokens.BRAND_COLOR_PRIMARY : '#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.height(56)
// 品牌化选中指示器
.borderWidth({ top: this.currentPage === index ? 3 : 0 })
.borderColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
.onClick(() => {
if (this.currentPage !== index) {
// 品牌化转场:先显示品牌色覆盖层,再切换页面
animateTo({
duration: BrandMotionTokens.DURATION_SHORT,
curve: BrandMotionTokens.EASING_BRAND_SECONDARY
}, () => {
this.showBrandOverlay = true
})
// 延迟切换页面
setTimeout(() => {
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: BrandMotionTokens.EASING_BRAND_PRIMARY
}, () => {
this.currentPage = index
this.showBrandOverlay = false
})
}, 100)
}
})
})
}
.width('100%')
.height(56)
.backgroundColor(Color.White)
}
}
interface PageData {
id: number
title: string
color: string
icon: string
}
3.3 完整示例:品牌化组件库动效系统
下面是一个可运行的品牌化组件库动效系统,包含按钮、卡片、对话框、列表等常用组件的品牌化动效实现:
// ====== 品牌化组件库动效系统 ======
// 品牌动效编排器 - 管理交错动画
export class BrandChoreographer {
private static instance: BrandChoreographer | null = null
private staggerTimers: number[] = []
static getInstance(): BrandChoreographer {
if (!BrandChoreographer.instance) {
BrandChoreographer.instance = new BrandChoreographer()
}
return BrandChoreographer.instance
}
// 编排列表项交错出场
staggerItems(itemCount: number, staggerDelay: number, callback: (index: number) => void): void {
for (let i = 0; i < itemCount; i++) {
const timer = setTimeout(() => {
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: BrandMotionTokens.EASING_BRAND_PRIMARY
}, () => {
callback(i)
})
}, i * staggerDelay)
this.staggerTimers.push(timer)
}
}
// 取消所有编排动画
cancelAll(): void {
this.staggerTimers.forEach(timer => clearTimeout(timer))
this.staggerTimers = []
}
}
// 品牌化对话框组件
@Component
struct BrandDialog {
@State isVisible: boolean = false
@State scaleValue: number = 0.8
@State opacityValue: number = 0
@Prop title: string = ''
@Prop message: string = ''
@Prop confirmText: string = '确定'
@Prop cancelText: string = '取消'
private onConfirm?: () => void
private onCancel?: () => void
show(): void {
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: BrandMotionTokens.EASING_BRAND_EMPHASIS
}, () => {
this.isVisible = true
this.scaleValue = 1.0
this.opacityValue = 1.0
})
}
dismiss(): void {
animateTo({
duration: BrandMotionTokens.DURATION_SHORT,
curve: BrandMotionTokens.EASING_BRAND_SECONDARY
}, () => {
this.scaleValue = 0.8
this.opacityValue = 0
})
setTimeout(() => {
this.isVisible = false
}, BrandMotionTokens.DURATION_SHORT)
}
build() {
if (this.isVisible) {
Stack() {
// 遮罩层
Column()
.width('100%')
.height('100%')
.backgroundColor('#80000000')
.opacity(this.opacityValue)
.onClick(() => this.dismiss())
// 对话框主体
Column() {
Text(this.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Text(this.message)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 12 })
.textAlign(TextAlign.Start)
// 按钮区域
Row({ space: 12 }) {
Button(this.cancelText)
.fontSize(14)
.fontColor('#666666')
.backgroundColor('#F5F5F5')
.borderRadius(20)
.layoutWeight(1)
.onClick(() => this.dismiss())
Button(this.confirmText)
.fontSize(14)
.fontColor(Color.White)
.backgroundColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
.borderRadius(20)
.layoutWeight(1)
.onClick(() => {
this.dismiss()
this.onConfirm?.()
})
}
.margin({ top: 24 })
}
.width('85%')
.padding(24)
.backgroundColor(Color.White)
.borderRadius(24)
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.shadow({ radius: 24, color: '#1a000000', offsetY: 8 })
}
.width('100%')
.height('100%')
}
}
}
// 品牌化列表项组件 - 带交错出场
@Component
struct BrandListItem {
@Prop title: string = ''
@Prop subtitle: string = ''
@Prop icon: string = ''
@State isVisible: boolean = false
@State translateY: number = 30
@State itemOpacity: number = 0
@Prop index: number = 0
aboutToAppear(): void {
// 交错出场
setTimeout(() => {
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: BrandMotionTokens.EASING_BRAND_PRIMARY
}, () => {
this.isVisible = true
this.translateY = 0
this.itemOpacity = 1
})
}, this.index * BrandMotionTokens.STAGGER_MEDIUM)
}
build() {
Row() {
// 品牌色图标
Column() {
Text(this.icon)
.fontSize(20)
}
.width(44)
.height(44)
.borderRadius(12)
.backgroundColor(BrandMotionTokens.BRAND_GLOW_COLOR)
.justifyContent(FlexAlign.Center)
// 文字内容
Column() {
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#212121')
Text(this.subtitle)
.fontSize(13)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
// 品牌化箭头
Text('›')
.fontSize(20)
.fontColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 8 })
.translate({ y: this.translateY })
.opacity(this.itemOpacity)
}
}
// ====== 完整品牌化页面 ======
@Entry
@Component
struct BrandMotionSystemDemo {
@State listItems: ListItemData[] = []
@State showDialog: boolean = false
private dialogController: BrandDialog | null = null
private choreographer: BrandChoreographer = BrandChoreographer.getInstance()
aboutToAppear(): void {
// 初始化列表数据
this.listItems = [
{ title: '账户安全', subtitle: '密码、指纹、人脸识别', icon: '🔒' },
{ title: '消息通知', subtitle: '推送、短信、邮件提醒', icon: '🔔' },
{ title: '存储管理', subtitle: '缓存、下载、离线数据', icon: '💾' },
{ title: '隐私设置', subtitle: '权限、数据、追踪保护', icon: '🛡️' },
{ title: '外观主题', subtitle: '深色模式、字体大小', icon: '🎨' },
{ title: '关于应用', subtitle: '版本信息、开源许可', icon: 'ℹ️' }
]
}
build() {
Column() {
// 品牌化顶部栏
this.BrandHeader()
// 品牌化列表
Scroll() {
Column({ space: 8 }) {
ForEach(this.listItems, (item: ListItemData, index: number) => {
BrandListItem({
title: item.title,
subtitle: item.subtitle,
icon: item.icon,
index: index
})
})
}
.padding(16)
}
.layoutWeight(1)
// 品牌化底部操作栏
this.BrandFooter()
}
.width('100%')
.height('100%')
.backgroundColor('#F8F8F8')
}
@Builder BrandHeader() {
Column() {
Text('品牌化动效系统')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#212121')
Text('每一个动画细节都传递品牌个性')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 4 })
}
.width('100%')
.padding({ horizontal: 20, vertical: 24 })
.alignItems(HorizontalAlign.Start)
.backgroundColor(Color.White)
}
@Builder BrandFooter() {
Row() {
BrandButton({ text: '重置动画', type: 'secondary' })
.layoutWeight(1)
.onClick(() => {
this.choreographer.cancelAll()
})
BrandButton({ text: '播放动画', type: 'primary' })
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
// 重新触发所有列表项的出场动画
this.choreographer.staggerItems(
this.listItems.length,
BrandMotionTokens.STAGGER_MEDIUM,
(index: number) => {
console.info(`第${index}项出场`)
}
)
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
}
}
interface ListItemData {
title: string
subtitle: string
icon: string
}
四、踩坑与注意事项
坑点1:cubicBezierCurve的y值超出范围会导致"过冲"
当你设置y1或y2大于1.0时(比如1.3、1.5),曲线会产生"过冲"效果——属性值会先超过目标值再回弹。这可以模拟弹性效果,但如果过冲幅度太大,会导致元素"飞出"可视区域。建议y值不要超过1.5。
坑点2:品牌色覆盖层不能阻挡交互
在品牌化转场中使用颜色覆盖层时,一定要在转场结束后移除覆盖层或设置hitTestBehavior(HitTestMode.None),否则覆盖层会阻挡底层页面的触摸事件。
坑点3:交错动画的延迟时间要考虑列表长度
如果你的列表有50个元素,每个延迟50ms,最后一个元素的出场时间是2.5秒——这太长了。建议对长列表使用"分组交错":每5个元素为一组,组内交错30ms,组间交错100ms。
坑点4:品牌化曲线不要和系统默认曲线混用
最忌讳的是同一个App里,按钮点击用品牌曲线,页面转场用默认曲线,列表滑动又用另一种曲线。这会让用户感觉"这个App的动效乱七八糟"。要么全部用品牌曲线,要么不用。
坑点5:对话框的弹性动画不要过度
品牌化对话框的出场动画加一点弹性是好的,但弹性幅度过大会让对话框看起来"晃来晃去",用户会觉得不稳定。对话框是"严肃"的UI元素,弹性曲线的y值建议控制在1.1以内。
坑点6:动效Token的命名要有语义
不要用curve1、curve2这种命名。应该用easing-brand-primary、easing-brand-emphasis这种语义化命名。半年后你回来看代码,curve2是什么意思?没人知道。但easing-brand-emphasis一看就知道是"品牌强调曲线"。
坑点7:品牌色与动效结合时注意色值格式
在动效中使用品牌色时,特别是做透明度渐变,一定要用8位十六进制色值(如#FF6B3540),而不是6位。6位色值不支持透明度通道,会导致动画效果和设计稿不一致。
五、HarmonyOS 6适配说明
API差异
| API | HarmonyOS 5.0 | HarmonyOS 6.0 | 迁移建议 |
|---|---|---|---|
| Curve.cubicBezierCurve | 返回ICurve接口 | 返回ICurve接口,新增曲线组合 | 可链式组合多条曲线 |
| animateTo | 单次调用 | 支持嵌套animateTo | 复杂编排可使用嵌套调用 |
| TransitionEffect | 静态组合 | 新增.animation()链式配置 | 使用链式配置更清晰 |
| MotionPath | 不支持 | 新增运动路径动画 | 品牌化路径动画使用此API |
| SpringMotion | 需手动实现 | 新增SpringMotion内置类 | 弹性动画使用SpringMotion |
行为变更
- 曲线组合:6.0中可以通过
Curve.compose()将多条曲线组合使用,例如先快后慢再快的效果 - SpringMotion内置化:6.0新增
SpringMotion类,支持阻尼、刚度、质量等物理参数的弹性动画,不再需要用cubicBezier模拟弹性 - 动画中断恢复:6.0中动画被中断时,新动画会从当前值开始而不是目标值,避免了"跳帧"问题
- TransitionEffect链式配置:6.0新增
.animation()方法,可以在TransitionEffect上直接配置时长和曲线
适配代码
// HarmonyOS 6.0 品牌化弹性动画 - 使用SpringMotion
@Component
struct BrandButtonV6 {
@Prop text: string = ''
@State scaleValue: number = 1.0
build() {
Row() {
Text(this.text)
.fontSize(16)
.fontColor(Color.White)
}
.padding({ horizontal: 24, vertical: 12 })
.backgroundColor(BrandMotionTokens.BRAND_COLOR_PRIMARY)
.borderRadius(24)
.scale({ x: this.scaleValue, y: this.scaleValue })
.onClick(() => {
// 6.0使用SpringMotion实现品牌化弹性效果
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
// 6.0新增:弹簧动画配置
curve: new SpringMotion({
stiffness: 400, // 刚度:值越大回弹越快
damping: 20, // 阻尼:值越大振荡越少
mass: 1.0 // 质量:值越大惯性越大
})
}, () => {
this.scaleValue = 0.93
})
// 回弹
setTimeout(() => {
animateTo({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: new SpringMotion({
stiffness: 300,
damping: 15,
mass: 1.0
})
}, () => {
this.scaleValue = 1.0
})
}, 100)
})
}
}
// 6.0链式TransitionEffect配置
@Component
struct BrandCardV6 {
@State isVisible: boolean = false
build() {
Column() {
Text('品牌卡片')
.fontSize(18)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
// 6.0链式配置:直接在TransitionEffect上设置动画参数
.transition(
TransitionEffect.OPACITY
.combine(TransitionEffect.translate({ y: 30 }))
.animation({
duration: BrandMotionTokens.DURATION_MEDIUM,
curve: BrandMotionTokens.EASING_BRAND_PRIMARY
})
)
}
}
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
品牌化动效系统不是"锦上添花"的装饰,而是品牌识别度的核心组成部分。当用户闭上眼睛,仅凭手指的触感就能辨认出你的App时——你的品牌化动效就成功了。
建立动效Token系统是第一步,也是最关键的一步。它确保了整个App动效的一致性,也为后续的维护和迭代提供了清晰的接口。记住:好的动效系统像好的设计系统一样,是"约束"出来的,不是"自由"出来的。
最后,品牌化动效的最高境界是"克制"。不是每个元素都需要动效,不是每次交互都需要品牌色。在关键节点留下品牌印记,在辅助操作保持安静——这才是成熟品牌化动效的标志。
- 点赞
- 收藏
- 关注作者
评论(0)