一、提升动画感知流畅度
HarmonyOS动效引力体系,围绕回归本源设计理念,打造了自然、流畅、品质一体的操作体验。基于视觉效果设计,我们可以将动效划分为特征动效、转场动效、手势动效、微动效、插画动效。在特征动效中呈现出天体运动“力”的即视感;转场动效表现出物体在运动过程中“力”的秩序感;手势动效打造出元素运动互相影响“力”的控制感;微动效和插画动效辅助HarmonyOS动效引力体系,增加用户的操作趣味性和浏览愉悦感。动效要始终围绕操作符合用户心理预期,物体运动符合真实世界,元素表现形态凸显产品的品牌与调性,从用户感知角度提升流畅度。
HarmonyOS系统为开发者提供了丰富的动画能力,在实际开发过程中,我们需要把上述UX设计视角转换为开发实现视角,即使用HarmonyOS系统提供的动画能力来实现UX设计的场景和动效,一般来说需要采取如下步骤完成视角转换:
- 了解系统能力:首先,开发者需要深入了解HarmonyOS系统提供的动画能力。这包括了解动画以及如何在HarmonyOS应用中使用相关API。
- 分析UX设计视角:仔细分析UX设计所提供的动效,理解设计师的意图。
- 设计动画方案:基于分析的结果,设计出合理的动画方案。确定动画的触发时机、动画的类型和参数等。
- 使用动画能力:利用HarmonyOS提供的动画能力,如属性动画、路径动画等或者调用三方库,完成设计效果。
- 调试和优化:在实施动画的过程中,进行调试和优化。确保动画效果流畅,动效符合预期,且满足性能要求。
通过以上步骤,开发者可以将UX设计视角转换为开发实现视角,并将设计师提供的动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,提升动画感知流畅度并提供良好的用户体验。
合理使用动画
二、动效场景设计
在设计动效过程中,要清楚地理解动效在系统中承载的作用,动效能体现页面的流畅过渡、对象的明确提示、元素的层级关系、产品的品牌印象等。
2.1 特征动效
特征动效主要打造 “天体拟物感知”,提供一种天体拟物的品牌效应和宇宙空间感的交互体验,它将力赋予元素,更直观地传递出形象化、拟物化、动态化的设计理念,在不同场景上表达新颖个性的同时又凸显了独特的产品调性。它可以广泛应用于开场动画、加载动画、下载动画等场景。
特征动效是指在用户界面中突出某个特定元素的动画效果。通过特征动效,可以吸引用户的注意力,提升用户体验。例如,在一个应用程序中,当用户点击”下载”按钮时,渐变显示出进度条并动态加载(如下图所示)。
点击特征动效示意图
2.2 转场动效
转场动效是指在不同页面或视图之间切换时使用的动画效果。通过转场动效,可以平滑地过渡到下一个页面或视图,增加界面间的连贯性和流畅性。
转场动效示意图
2.3 手势动效
手势动效是指根据用户的手势操作而产生的动画效果。通过手势动效,可以增强用户与设备之间的互动体验。我们主张无阻塞感的动效设计,结合运用 HarmonyOS 动效物理引擎,将自然属性运用到界面的操作中,比如摩擦力、弹性、碰撞影响等。
- 点击:点击的接触过程中有一段100ms~300ms的时长是无反馈状态,为了提升感知体验,可以在按下那一刻即响应动效反馈。这一可先行的触控响应机制强化了界面元素的视觉反馈,为理解界面状态提供了更多的线索信息。
- 滑动:滑动手势是用户进行滑动操作时产生的相应动画效果,例如随手指移动的平滑过渡动画,增强了界面的流畅性。保证对象动效反馈的结果与手势动作的连贯性是滑动手势动效设计的关键。
- 翻动:翻动手势动效通常用于模拟翻书或翻页的效果,用户可以通过拖拽或抛滑手势来翻转页面或切换内容,界面元素会产生相应的翻页动画,提供更真实的交互体验。翻页有成功与否,未成功会停留在当前内容上;成功则显示下一页/几页的内容。为了提示性,翻页也有过界拖拽的场景。
- 夹捏:捏合手势是指双/多指合拢或分开的动作,常用于缩放或旋转对象。手势过程中需要令对象跟随手势做出相应的响应趋势。
- 拖拽:拖拽手势是指手指按下同时进行移动的动作,动效设计了对象通过拖拽行为进行状态转换的整个过程,以确保用户操作的连贯性和流畅性。
手势动效示意图
2.4 微动效
微动效是指在界面中细微的动画效果,用于增加界面的生动感和交互性。微动效可以体现在按钮的点击效果、图标的变化、文本的出现等。例如,当用户打开某个面板时,可以使用微小的缩放或颜色变化来体现(如图所示)。
2.5 插画动效
插画动效是指在界面中应用的基于插画的动画效果。通过插画动效,可以为界面增添趣味和个性化。例如,在一个游戏应用中,可以使用插画动效来展示角色的动作、表情或者场景的变化(如图所示)。
通过动画的方式丰富视觉元素所要表达的信息,可以引导解读功能信息并串联前后画面,便于用户理解,也使画面表现更富有生命力。
插画动效示意
三、动画能力选型
开发人员接收到设计需求后,需要选择合适的动画能力完成该设计。HarmonyOS为开发者提供了系统能力、资源调用、三方库三种方式,在选择动画能力时,开发者需要考虑目标和需求以及效率和质量,合理选择能够满足需求的工具、追求高效率和高质量的结果导向,帮助应用实现更好的动画效果。
3.1 系统能力
- 属性动画:通过更改组件的属性值实现渐变过渡效果,例如缩放、旋转、平移等。支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。
- 显式动画:可以通过用户的直接操作或应用程序的特定逻辑来触发,例如按钮点击时的缩放动画、列表项展开时的渐变动画等。HarmonyOS提供了全局animateTo显式动画接口来指定由于闭包代码导致状态变化的插入过渡动效。
- 转场动画:转场动画可以实现平滑的界面切换效果,例如页面之间的淡入淡出、滑动切换、旋转切换等,增强了界面的连贯性和吸引力。具体使用方法可参考《合理使用页面间转场》。
- 路径动画:指对象沿着指定路径进行移动的动画效果。通过设置路径可以实现视图沿着预定义的路径进行移动,例如曲线运动、圆周运动等,为用户呈现更加生动的交互效果。
- 粒子动画:通过大量小颗粒的运动来形成整体动画效果。通过对粒子在颜色、透明度、大小、速度、加速度、自旋角度等维度变化做动画,来营造一种氛围感。
3.2 资源调用
- GIF动画:GIF动画可以在特定位置循环播放,为应用界面增添生动的视觉效果。在开发中,可以使用Image组件来实现GIF动画的播放。通过在特定位置放置Image组件,并加载GIF格式的图像,开发者可以轻松实现动画效果,具体实现可以参考后文加载GIF实现微动效。
- 帧动画:通过逐帧播放一系列图片来实现动画效果,在开发中可以使用ImageAnimator组件来实现帧动画的播放。开发者可以配置需要播放的图片列表,以及每张图片的播放时长,从而实现精细的动画效果。
3.3 三方库
- Lottie:解析Adobe After Effects软件通过Bodymovin插件导出的json格式的动画,并在移动设备上进行本地渲染。Lottie动画可以在各种屏幕尺寸和分辨率上呈现,并且支持动画的交互性,通过添加触摸事件或其他用户交互操作,使动画更加生动和具有响应性。
- SVG:通过将SVG图片解析并渲染到页面上并对SVG图片样式动态改变实现动画。OHOS-SVG不仅能够提供高质量的图形呈现,而且还能够实现图形样式的实时更新,为用户带来更加丰富的视觉体验。
动画能力对比
动画能力 | 特点 |
---|---|
系统能力 | 可以直接调用,性能优秀,但太复杂的动画不便于实现 |
GIF | 可设计、直接调用组件实现,但文件占用空间大,掉帧严重、会出现失真、模糊、锯齿等现象 |
帧动画 | 兼容性高、直接调用组件实现,但需要大量图片,占据大量内存 |
Lottie | 跨平台、可设计,但性能难以提升、帧率较低 |
SVG | 可代码编辑,文件较小、无损伸缩,但实现、维护成本高,并且复杂度高会减慢渲染速度 |
四、提升动画运行流畅度
动画在应用开发中扮演着重要的角色,能够提升用户体验,传达信息,引导用户操作,提升应用品质和增加视觉吸引力。而动画的性能表现也至关重要,优化可以从属性更新和布局等几个方面考虑,尽可能减少冗余刷新。本文将介绍如下4种优化手段,通过这些优化手段的单个使用或组合使用,可以对动画帧率、应用卡顿等方面带来优化,提升性能和用户体验:
- 使用系统提供的动画接口:系统接口经过精心设计和优化,能够在不同设备上提供流畅的动画效果,最大程度地减少丢帧率和卡顿现象。
- 使用图形变换属性变化组件布局:通过对组件的图形变换属性进行调整,而不是直接修改组件的布局属性,可以减少不必要的布局计算和重绘操作,从而降低丢帧率,提升动画的流畅度和响应速度。
- 参数相同时使用同一个animateTo:当多个动画的参数相同时,合并它们并使用同一个animateTo方法进行处理能够有效减少不必要的计算和渲染开销。
- 多次animateTo时统一更新状态变量:在进行多次动画操作时,统一更新状态变量可以避免不必要的状态更新和重复渲染,从而减少性能开销。
- 使用renderGroup:在单一页面上存在大量应用动效的组件时,可以使用renderGroup方法来解决卡顿问题,从而提升动画性能。
五、使用系统提供的动画接口
一般而言,在HarmonyOS应用开发中,动画设计实现可以通过自定义动画或系统提供的动画接口两种方式来实现。
- 方式一,自定义动画:
自定义动画是指通过编写自定义的动画逻辑和计算过程来实现特定的动画效果。开发人员可以根据应用的需求和设计要求,使用自定义的动画算法和逻辑来创建独特的动画效果。自定义动画的优势在于可以实现非常个性化的动画效果,并且能够完全控制动画的每一个细节,但需要开发人员具备一定的动画算法和计算能力。
- 方式二,系统动画接口:
系统动画接口是指通过使用系统提供的动画框架和接口来实现动画效果。在移动应用开发中,通常会使用属性动画来实现各种动画效果。通过可动画属性改变引起UI上产生的连续视觉效果,即为属性动画。属性动画是最基础易懂的动画,ArkUI提供两种属性动画接口animateTo和animation驱动组件属性按照动画曲线等动画参数进行连续的变化,产生属性动画。使用系统提供的动画接口可以简化动画的实现过程,并且能够充分利用系统优化的动画计算和渲染能力,从而提高动画的性能和流畅度。
同一界面多个按钮同时缩放示意图
5.1 自定义动画
播放动画时,系统需要在一个刷新周期内完成动画变化曲线的计算,完成组件布局绘制等操作。使用了自定义动画,动画曲线计算过程很容易引起UI线程高负载,易导致丢帧。
使用自定义动画实现按键缩放,具体实现代码如下:
@Entry
@Component
struct CustomAnimationExample {
@State widthSize: number = 80
@State heightSize: number = 40
@State flag: boolean = true
// 自定义动画函数
computeSize() {
let duration = 2000
// 设定循环周期
let period = 1
let widthSizeEnd = 0
let heightSizeEnd = 0
if (this.flag) {
widthSizeEnd = 50
heightSizeEnd = 25
} else {
widthSizeEnd = 80
heightSizeEnd = 40
}
// 计算循环次数
let doTimes = duration / period
// 计算每次循环变化量
let deltaHeight = (heightSizeEnd - this.heightSize) / doTimes
let deltaWeight = (widthSizeEnd - this.widthSize) / doTimes
// 循环,组件每个周期增加一部分宽高
for (let i = 1; i <= doTimes; i++) {
let t = period * (i);
setTimeout(() => {
this.heightSize = this.heightSize + deltaHeight
this.widthSize = this.widthSize + deltaWeight
}, t)
}
this.flag = !this.flag
}
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Button()
.width(this.widthSize)
.height(this.heightSize)
Button()
.width(this.widthSize)
.height(this.heightSize)
Button()
.width(this.widthSize)
.height(this.heightSize)
}
Row({ space: 10 }) {
Button()
.width(this.widthSize)
.height(this.heightSize)
Button()
.width(this.widthSize)
.height(this.heightSize)
Button()
.width(this.widthSize)
.height(this.heightSize)
}
Button('click me')
.fontSize(20)
.onClick(() => {
let delay = 500
// 调用自定义函数
setTimeout(() => {
this.computeSize()
}, delay)
})
.width('50%')
.height('12%')
.backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
5.2 系统动画接口
系统提供的动画接口,只需设置曲线类型、终点位置、时长等信息,就能够满足常用的动画功能,减少UI主线程的负载。
5.2.1 使用属性动画实现按键缩放,具体实现代码如下:
@Entry
@Component
struct PropertyAnimateToExample {
@State widthSize: number = 80;
@State heightSize: number = 40;
@State flag: boolean = true;
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Button()
.width(this.widthSize)
.height(this.heightSize)// 对Button组件的宽高属性进行动画配置
.animation({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
})
Button()
.width(this.widthSize)
.height(this.heightSize)// 对Button组件的宽高属性进行动画配置
.animation({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
})
Button()
.width(this.widthSize)
.height(this.heightSize)// 对Button组件的宽高属性进行动画配置
.animation({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
})
}
Button('click me')
.fontSize(20)
.onClick((event?: ClickEvent | undefined) => {
if (this.flag) {
this.widthSize = 50
this.heightSize = 25
} else {
this.widthSize = 80
this.heightSize = 40
}
this.flag = !this.flag
})
.width('50%')
.height('12%')
.backgroundColor(0x317aff)
}.width('100%').margin({ top: 50 })
}
}
示意图
5.2.2 使用显式动画实现按键缩放,具体实现代码如下:
@Entry
@Component
struct ExplicitAnimateToExample {
@State widthSize: number = 80;
@State heightSize: number = 40;
@State flag: boolean = true;
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Button()
.width(this.widthSize)
.height(this.heightSize)
Button()
.width(this.widthSize)
.height(this.heightSize)
Button()
.width(this.widthSize)
.height(this.heightSize)
}
Button('click me')
.fontSize(20)
.onClick((event?: ClickEvent | undefined) => {
// 对Button组件的宽高属性进行动画配置
if (this.flag) {
animateTo({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
}, () => {
this.widthSize = 50;
this.heightSize = 25;
})
} else {
animateTo({
duration: 2000, // 动画时长
curve: Curve.Linear, // 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal // 动画模式
}, () => {
this.widthSize = 80;
this.heightSize = 40;
})
}
this.flag = !this.flag;
})
.width('50%')
.height('12%')
.backgroundColor(0x317aff)
}.width('100%').margin({ top: 50 })
}
}
示意图
六、使用图形变换属性变化组件
在应用开发中,改动组件的布局显示可以通过改动布局属性、改动图形变换属性两种方式来实现。
- 方式一,改动布局属性
常见的布局属性包括位置、大小、内边距、外边距、对齐方式、权重等。当这些布局属性发生改变时,界面将重新布局以适应新的属性值。- 方式二,改动图形变换属性
图形变换属性是指对组件布局结果的变换操作,如平移、旋转、缩放等操作。通过改变这些图形变换属性,可以实现对组件布局完成后,在界面上的位置和形态进行动态变换。
布局属性和图形变换属性的可替换关系如下表所示:
图形变换属性 | 布局属性 |
---|---|
rotate | / |
translate | position、offset |
scale | width、height、Size |
transform | / |
说明
表格中的对应关系仅供参考,开发者可以自行发掘更多的替代关系。
场景设计
针对同一界面多个图片同时缩放并位移的场景(如下图所示),分别通过改变布局属性、改变图形变换属性实现。
6.1 改变布局属性
布局属性发生变化时,系统需要重新计算组件的位置和大小,然后进行重新布局。这个过程需要消耗大量的计算资源和时间,尤其是在界面中包含大量组件或者复杂布局时,性能开销会更加明显。
同一界面多个图片同时缩放并位移示意图
通过组件的width、height、position属性来改变组件大小和位置,具体代码实现如下:
@Entry
@Component
struct ChangeLayoutByAnimateTo {
@State imageWidth: number = 60;
@State imageHeight: number = 60;
@State xPosition: number = 0;
@State yPosition: number = 0;
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Image($r('app.media.icon_like'))
.width(this.imageWidth)
.height(this.imageHeight)
.position({ x: this.xPosition, y: this.yPosition })
Image($r('app.media.icon_like'))
.width(this.imageWidth)
.height(this.imageHeight)
.position({ x: this.xPosition + 90, y: this.yPosition })
Image($r('app.media.icon_like'))
.width(this.imageWidth)
.height(this.imageHeight)
.position({ x: this.xPosition + 180, y: this.yPosition })
}
Button("布局属性")
.fontSize(20)
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
// 间隔播放位置、宽高变化
if (i % 2 == 0) {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageWidth = 120;
this.imageHeight = 120;
this.xPosition = 15;
this.yPosition = 15;
})
}, 1000 * i)
} else {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageWidth = 60;
this.imageHeight = 60;
this.xPosition = 0;
this.yPosition = 0;
})
}, 1000 * i)
}
}
})
.margin({ top: 200 })
}.width('100%').margin({ top: 50 })
}
}
6.2 改变图形变换属性
图形变换只是对组件的显示效果进行变换,而不会改变其在布局中的位置和大小,因此不会触发重新布局的计算过程。这使得使用图形变换属性来实现动画效果时,能够提升界面的流畅性和性能表现。
通过组件的scale、translate属性来改变组件大小和位置,具体代码实现如下:
@Entry
@Component
struct ChangeImageByAnimateTo {
@State imageScaleX: number = 1;
@State imageScaleY: number = 1;
@State imageTranslateX: number = 0;
@State imageTranslateY: number = 0;
build() {
Column({ space: 10 }) {
Row({ space: 10 }) {
Image($r('app.media.icon_like'))
.width(60)
.height(60)
.scale({
x: this.imageScaleX,
y: this.imageScaleY,
centerX: 0,
centerY: 0
})
.translate({ x: this.imageTranslateX, y: this.imageTranslateY })
Image($r('app.media.icon_like'))
.width(60)
.height(60)
.scale({
x: this.imageScaleX,
y: this.imageScaleY,
centerX: 0,
centerY: 0
})
.translate({ x: this.imageTranslateX, y: this.imageTranslateY })
Image($r('app.media.icon_like'))
.width(60)
.height(60)
.scale({
x: this.imageScaleX,
y: this.imageScaleY,
centerX: 0,
centerY: 0
})
.translate({ x: this.imageTranslateX, y: this.imageTranslateY })
}
// 其他相似布局
Button("图形变换属性")
.fontSize(20)// 按键属性设置
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
if (i % 2 == 0) {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageScaleX = 2;
this.imageScaleY = 2;
this.imageTranslateX = 15;
this.imageTranslateY = 15;
})
}, 1000 * i)
} else {
setTimeout(() => {
animateTo({ duration: 1000 }, () => {
this.imageScaleX = 1;
this.imageScaleY = 1;
this.imageTranslateX = 0;
this.imageTranslateY = 0;
})
}, 1000 * i)
}
}
})
}.width('100%').margin({ top: 100 })
}
}