合理使用动画
概述
动画是应用开发中必不可少的部分,它可以使应用程序更加生动和易于互动,一方面可以提升用户体验、增强诗句吸引力,另一方面可以引导用户操作、提高信息传达效率。应用程序中,页面层级间的转场、点击交互、手势操控都可以添加动画。
合理使用动画可以通过以下两方面考虑:
- 提升动画感知流畅度:使用合适的动画能力将UX设计视角转换为开发实现视角,并将设计师提供的动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,提升动画感知流畅度并提供良好的用户体验。
- 提升动画流畅度:优化动画资源的加载和释放,避免内存泄漏和资源浪费;合理使用动画缓存和复用,减少不必要的重复绘制,提高动画的运行效率。
在使用动画时,需要根据具体场景和用户需求进行合理的设计和运用,并且需要注意动画的性能和影响,及时采取相应的优化措施。
通过合理使用动画,可以提升动画的感知流畅度和运行流畅度,从而提升应用程序的用户体验和性能。
提升动画感知流畅度
HarmonyOS系统为开发者提供了丰富的动画能力,在实际开发过程中,我们需要把UX设计视角转化为开发实现视角,即使用HarmonyOS系统提供的动画能力来实现UX设计的场景和动效,一般来说需要采取如下步骤完成视角转换:
- 了解系统能力:首先,开发者需要深入了解HarmonyOS系统提供的动画能力。这包括了解动画以及如何在HarmonyOS应用中使用相关API。
- 分析UX设计视角:仔细分析UX设计所提供的动效,理解设计师的意图。
- 设计动画方案:基于分析的结果,设计出合理的动画方案。确定动画的触发时机、动画的类型和参数等。
- 使用动画能力:利用HarmonyOS提供的动画能力,如属性动画、路径动画等或者调用三方库,完成设计效果。
- 调试和优化:在实施动画的过程中,进行调试和优化。确保动画效果流畅,动效符合预期,且满足性能要求。
通过以上步骤,开发者可以将UX设计视角转换为开发实现视角,并将设计师提供的动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,提升动画感知流畅度并提供良好的用户体验。
动效场景设计
在设计动效过程中,要清楚地理解动效在系统中承载的作用,动效能体现页面的流畅过渡、对象的明确提示、元素的层级关系、产品的品牌印象等。
特征动效
特征动效是指在用户界面中突出某个特定元素的动画效果。通过特征动效、可以吸引用户的注意力,提升用户体验,它可以广泛应用于开场动画、加载动画、下载动画等场景。例如,在一个应用程序中,当用户点击”下载”按钮时,渐变显示出进度条并动态加载(如下图所示)。
转场动效
转场动效是指在不同页面或视图之间切换时使用的动画效果。通过转场动效,可以平滑地过渡到下一个页面或视图,增加界面间地连贯性和流畅性。
HarmonyOS系统为开发者提供了丰富地转场动效库,使开发者能够轻松实现各种转场动画效果。开发者可以根据具体需求,在应用的不同场景中应用这些转场动效,以提升用户体验和界面的吸引力。需要注意的是,为了最佳的用户体验,开发者应根据界面的功能和特点,合理选择转场动效,并遵循动效的使用准则,以确保转场动效在视觉和交互上的一致性。
手势动效
手势动效是指根据用户的手势操作而产生的动画效果。通过手势动效,可以增强用户与设备之间的互动体验。我们主张无阻塞感的动效设计,结合运用HarmonyOS 动效物理引擎,将自然属性运用到界面的操作中,比如摩擦力、弹性、碰撞影响等。
- 点击:点击的接触过程中有一段100ms~300ms的市场是无反馈状态,为了提升感知体验,可以在按下那一刻即响应动效反馈。这一可先行的触控响应机制强化了界面元素的视觉反馈,为理解界面状态提供了更多的线索信息。
- 滑动:滑动手势是用户进行滑动操作时产生的相应动画效果,例如随手指移动的平滑过渡动画,增强了界面的流畅性。保证对象动效反馈的结果与手势动作的连贯性是滑动手势动效设计的关键。
- 翻动:反动手势动效通常用户模拟翻书或翻页的效果,用可以通过拖拽或抛滑手势来翻转页面或切换内容,界面元素会产生相应的翻页动画,提供更真实地交互体验。翻页有成功与否,未成功会停留在当前内容上;成功则显示下一页/几页的内容。为了提示性,翻页也有过界拖拽的场景。
- 夹捏:捏合手势是指双/多指合拢或分开的动作,常用于缩放或旋转对象。手势过程中1需要令对象跟随手势做出相应的响应趋势。
- 拖拽:拖拽手势是指手指按下同时进行移动的动作,动效设计了对象通过拖拽行为进行状态转换的整个过程,以确保用户操作的连贯性和流畅性。
微动效
微动效是指界面中细微的动画效果,用于增加界面的生动感和交互性。微动效可以体现在按钮的点击效果、图标的变化、文本的出现等。例如,当用户打开某个面板时,可以使用微小的缩放或颜色变化来体现
插画动效
插画动效是指在界面中应用的基于插画的动画效果。通过插画动效,可以为界面添加趣味和个性化。例如,在一个游戏应用中,可以使用插画动效来展示角色的动作、表情或者场景的变化。
通过动画的方式丰富视觉元素所要表达的信息,可以引导解读功能信息并串联前后画面,便于用户理解,也使画面表现更富有生命力。
动画能力选型
开发人员接收到设计需求后,需要选择合适的动画能力完成该设计。HarmonyOS为开发者提供了系统能力、资源调用、三方库三种方式,在选择动画能力时,开发者需要考虑目标和需求以及效率和质量,合理选择能够满足需求的工具、追求高效率和高质量的结果导向,帮助应用实现更好的动画效果。
系统能力
- 属性动画:通过更改组件的属性值实现渐变的过渡效果,例如缩放、旋转、平移等。支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。
- 显式动画:可以通过用户的直接操作或应用程序的特定逻辑来触发,例如按钮点击时的缩放动画、列表项展开时的渐变动画等。HarmonyOS提供了全局animateTo显式动画接口来指定由于闭包代码导致状态变化的插入过渡动效。
- 转场动画:转场动画可以实现平滑的界面切换效果,例如页面之间的淡入淡出、滑动切换、旋转切换等,增强了界面的连贯性和吸引力。
- 路径动画:指对象沿着指定路径进行移动的动画效果。通过设置路径可以实现视图沿着预定义的路径进行移动,例如曲线运动、圆周运动等,为用户呈现更加生动的交互效果。
- 粒子动画:通过大量小颗粒的运动来形成整体动画效果。通过对例子在颜色、透明度、大小、速度、加速度、自旋角度等维度变化做动画,来营造一种氛围感。
资源调用
- GIF动画:GIF动画可以在特定位置循环播放,为应用界面增添生动的视觉效果。在开发者中,可以使用Image组件来实现GIF动画的播放。通过在特定位置放置Image组件,并加载GIF格式的图像,开发者可以轻松实现动画效果。
- 帧动画:通过逐帧播放一系列图片来实现动画效果,在开发中可以使用ImageAnimator组件来实现帧动画的播放时长,从而实现精细的动画效果。
三方库
- Lottie:解析Adobe After Effects软件通过Bodymovin插件导出的json格式的动画,并在移动设备上进行本地渲染。Lottie动画可以在各种屏幕尺寸和分辨率上呈现,并且支持动画的交互性,通过添加触摸事件或其他用户交互操作,使动画更加生动和具有响应性。
- SVG:通过将SVG图片解析并渲染到页面上,并对SVG图片样式动态改变实现动画。OHOS-SVG不仅能够提供高质量的图形呈现,而且还能够实现图形样式的实时更新,为用户带来更加丰富的视觉体验。
动画实践案例
使用属性动画实现微动效
在本场景中,随着用户滑动,顶端图标和文字需要随着主体内容变更而切换,其最终实现效果如下图所示:
在本场景中,对应位置的内容通过形状变化可以实现替换的预期效果,具体实现方式如下:
- 设置需要变化的组件:确定需要进行形状变化的组件或元素,例如按钮、图标等。
- 设置需要变化的内容:确定需要替换的具体内容,例如不同的图标、文字等。
- 设置animation内的动画属性:使用动画属性来实现形状变化,可以选择缩放、旋转、透明度等动画效果,并设置动画的持续时间、延迟等属性,以达到预期的替换效果。
属性动画需要设置对应的变化取值。具体代码实现如下:
export const ICON_SUBTITLE_ARRAY: IconSubtitle[] = [
{ icon: $r('app.media.ic_design_style'), title:'设计风格', enTitle: 'DESIGN STYLE' },
{ icon: $r('app.media.ic_building'), title:'建筑信息', enTitle: 'BUILDING INFORMATION' },
{ icon: $r('app.media.ic_geography_icon'), title:'地理位置', enTitle: 'GEOGRAPHIC LOCATION' }
]
随后对需要变化的组件设置animation属性,实现其动画。具体代码实现如下:
StyleTitle(item: IconSubtitle, index: number) {
// 读取需要变化的信息
Column() {
Image(item.icon)
// 图片对应属性
…
Text(item.title)
// 标题对应属性
…
Text(item.enTitle)
// 副标题对应属性
…
}
// 列组件对应属性设置
…
// 动画组件与进入方式
.animation({
duration: Const.TITLE_ICON_ANIMATION_DURATION,
curve: Curve.EaseOut
})
}
使用路径动画实现特征动效
在本场景中,需要实现当页面显示后,对应位置的小图标持续上下跳动,其最终实现效果如下图所示:
在本场景中,小图标的运动方式是随着路径上下移动,一方面,路径清晰简单,另一方面,触发事件仅为界面出现,只需要无限循环相同路径动画。
所以选择路径动画实现,具体实现方式如下:
- 设计动画路径:首先设计Path属性,用于描述路径的形状和轨迹。
- 设置路径动画属性:利用motionPath属性设置组件的动画路径,设置动画的起始值为0,结束值为1,代表动画的进度。
- 设置动画触发条件:使用通用事件(超链接)点击、出现等,选择对应需求的触发方式。
- 实现animateTo显示动画接口:animateTo调用路径动画,设置循环次数、时长、运动曲线等相关属性。
路径动画包含motionPath属性设置和的动画触发两个部分。具体代码实现如下:
@Entry
@Component
struct AnimationExample {
// 动画控制器
@State toggle: boolean = true
// 需要设置动画的组件
build() {
Stack() {
Image($r('app.media.icon_bg'))
// 图片的属性值设置
…
Image(this.item.icon)
// 图片的属性值设置
…
}
// 路径属性设置
.motionPath({ path: 'Mstart.x start.y L 0 30 L 0 0 ', from: 0.0, to: 1.0 })
// 设置界面出现时触发路径动画
.onAppear(() => {
animateTo({ duration: 4000, curve: Curve.Linear, iterations: -1 }, () => {
// 通过this.toggle变化组件的位置
this.toggle = !this.toggle
})
})
}
}
使用粒子动画实现插画动效
在本场景中,需要在背景页面上实现飘雪效果以避免太过单调,其具体实现效果如下图所示:
在本场景中,雪以粒子的形式飘下,可以优先选择HarmonyOS提供的粒子动画,配置相关的属性信息实现需求的效果。具体实现方式如下:
- 创建各个粒子动画的粒子发射装置:首先创建一个粒子发射器,用于控制和发射雪花粒子动画,确定粒子的发射位置、数量、形状等参数。
- 配置粒子的属性:通过配置粒子的颜色、透明度、大小、速度、加速度和旋转速度等属性,来实现雪花的真实飘落效果。
- 集合粒子动画:将配置好的粒子动画效果进行集合,以实现整体的雪花飘落效果,确保各个粒子动画协调一致地展现雪花飘落的场景。
粒子组件一般叠加显示在图片上,具体代码实现如下:
Particle({
particles: [{
// 粒子发射器配置
emitter: {
particle: {
type: ParticleType.POINT,
config: {
radius: 10
},
count: 1000,
lifetime: 50000
},
emitRate: 3,
position: [0, 0],
shape: ParticleEmitterShape.ELLIPSE
},
color: {
range: [Color.Red, Color.Yellow],
updater: {
type: ParticleUpdater.CURVE,
// 粒子集合
config: [
// 单独粒子的颜色变化、速度等属性
{
from: Color.White,
to: Color.Pink,
startMillis: 0,
endMillis: 3000,
curve: Curve.FastOutLinearIn
},
// 其他单个粒子设定
…
]
}
},
// 加速度的配置,从大小和方向两个维度变化,speed表示加速度大小,angle表示加速度方向
acceleration: {
speed: {
range: [200, 500],
updater: {
type: ParticleUpdater.RANDOM,
config: [1, 20]
}
},
angle: {
range: [80, 100]
}
}
}]
})
加载GIF实现微动效
在本场景中,需要实现小魔法棒反复播放的微动效,其具体实现效果如下图所示:
- 在本场景中,由于需要循环播放小魔法棒的动画,而使用Image组件渲染的GIF格式资源会自动循环播放,并且该图片较小,受失真、模糊、锯齿等因素的影响较小,所以选择加载GIF实现该动画。具体实现方式如下:
- 资源加载:首先需要将小魔法棒的动画制作成GIF格式的图片,并将其作为资源加载到应用程序中。
- 设置相关属性和事件:在Image组件中,设置GIF格式资源的相关属性,例如尺寸、位置等,并可以根据需要添加事件处理。
Image组件可以直接绑定GIF格式图像,具体实现代码如下:
Image($r('app.media.ic_topics_gif'))
.width($r('app.float.topics_select_width'))
.height($r('app.float.topics_select_height'))
.margin({ right: $r('app.float.topic_margin_right'), bottom: $r('app.float.topic_margin_bottom') })
.onClick(() => {
const bundleName = (getContext(this) as common.UIAbilityContext).applicationInfo.name;
router.pushUrl({ url: `@bundle:${bundleName}/topic/ets/pages/ThemeSettingPage` });
})
加载lottie库实现特征动效
在本场景中,需要在点击徽章后将其放大并播放烟花动效,其具体实现效果如下图所示:
在本场景中,一方面需要实现的动画较为复杂,而Lottie 是使用 Adobe After Effects 制作动画,设计师可以使用他完成包括路径动画、渐变、遮罩效果等各种复杂的动画效果,而开发者可以直接将这些动画效果集成到应用中;另一方面需要人为对其实现控制,而Lottie 支持在动画中添加交互元素,例如点击、拖动等,并且该界面是一个纯粹播放动画界面,对性能要求不高。所以使用Lottie是合适的解决方案,具体实现方式如下:
- 获取Lottie文件:设计师提供对应动画的JSON 格式文件
- 导入Lottie组件:在相应的类中引入组件并构建渲染上下文
- 将Lottie文件添加到项目中:將动画需要的json文件放到pages同级别目录下,然后引用,或者直接传递动画的json数据
- 关联画布:将Lottie动画和画布绑定
- 加载动画:点击按钮加载动画可按照正常逻辑放在点击事件内,如果想要实现进入页面自动播放动画,需要结合Canvas组件的onReady()生命回调周期实现,加载动画时机需放置在onReady()生命周期回调内或及之后。
使用Lottie图像需要先在相应的类中引入组件:
import lottie from '@ohos/lottie'
在本场景下,先将Lottie动画与画布绑定,同时还需要将画布同全模态转场绑定,点击徽章图标后实现全模态跳转并显示画布和Lottie动画,具体实现代码如下:
//构建渲染上下文
private mainRenderingSettings: RenderingContextSettings = new RenderingContextSettings(true)
private mainCanvasRenderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.mainRenderingSettings)
build() {
Column() {
// 显示徽章
List({ space: Constants.MIDDLE_SPACE }) {
ForEach(ACHIEVE_IMAGE_LIST, (item: AchieveImage) => {
ListItem() {
Image(this.getShowImg(item))
// 图片的属性值
…
// 点击事件
.onClick(() => {
if (this.learnedIds.includes(item.pathId)) {
lottie.loadAnimation({
container: this.mainCanvasRenderingContext,
renderer: 'canvas',
loop: false,
autoplay: false,
name: item.pathId,
path: item.lottiePath
})
lottie.play()
this.clickedItem = item;
this.isShow = true;
}
})
}, (item: AchieveImage) => JSON.stringify(item))
}
// 模态转场
.bindContentCover(
this.isShow,
this.playLottieBuilder(),
{ modalTransition: ModalTransition.ALPHA, backgroundColor: $r('app.color.achieve_background_color'), onDisappear: () => {lottie.destroy()}}
)
// 列表属性
…
}
// 列容器属性
…
}
//模态转场后页面
@Builder playLottieBuilder() {
Column() {
Column() {
// 建立画布
Canvas(this.mainCanvasRenderingContext)
.height('50%')
.width('80%')
.backgroundColor($r('app.color.achieve_background_color'))
.onReady(() => {
if (this.clickedItem != null) {
lottie.loadAnimation({
container: this.mainCanvasRenderingContext,
renderer: 'canvas',
loop: false,
autoplay: true,
name: this.clickedItem.pathId,
path: this.clickedItem.lottiePath
})
}
})
.onClick(() => {
this.isShow = false;
})
}
Column() {
Button('知道啦')
.onClick(() => {
this.isShow = false;
})
}
}
}
}
提升动画感知流畅度总结
在应用开发中,动画可以为用户界面增添生动、流畅的交互效果,提升用户对应用的好感度。然而,滥用动画也会导致应用性能下降,消耗过多的系统资源,甚至影响用户体验。
本文重点描述了如何在特定设计场景下充分利用HarmonyOS提供的动画能力,以确保动画既能够增强用户体验,又不至于对应用性能造成负面影响。开发者需要在实现动画效果时,注意控制动画的复杂度和频率,以节约系统资源,提升应用的性能表现。提升动画感知流畅度需要考虑以下几点:
- 用户体验:动画应该能够提升用户体验,而不是仅仅为了动画而动画。动画应该能够使用户界面更加生动、直观和易于理解,而不应该过于花哨或者繁琐。
- 性能优化:动画的流畅性对于用户体验至关重要。在设计动画时,需要考虑到设备的性能和资源消耗,避免过多的动画效果导致性能下降或者卡顿。
- 可访问性:在设计动画时,需要考虑到一些用户可能存在的视觉或认知障碍。动画效果应该不会影响到用户对应用界面的理解和操作。
- 上下文适应:动画效果应该根据应用的具体场景和功能进行设计,与应用的整体风格和设计语言保持一致。
合理利用HarmonyOS提供的动画能力可以有效提升应用的用户体验和吸引力,但开发者需要在设计和实现过程中综合考虑动画的使用场景、频率以及对系统资源的影响,扬长避短,确保应用的性能和稳定性。
提升动画运行流畅度
动画在应用开发中扮演着重要的角色,能够提升用户体验,传达信息,引导用户操作,提升应用品质和增加视觉吸引力。而动画的性能表现也至关重要,优化可以从属性更新和布局等几个方面考虑,尽可能减少冗余刷新。
可以通过以下四种优化手段的单个使用或组合使用,对动画帧率、应用卡顿等方面带来优化,提升性能和用户体验:
- 使用系统提供的动画接口:系统接口经过精心设计和优化,能够在不同设备上提供流畅的动画效果,最大程度的减少丢帧率和卡顿现象。
- 使用图形变换属性变化组件布局:通过对组件的图形变换属性进行调整,而不是直接修改组件的布局属性,额可以减少不必要的布局计算和重绘操作,从而降低丢帧率,提升动画的流畅度和响应速度
- 参数相同时使用同一个animateTo:当多个动画得参数相同时,合并它们并使用同一个animateTo方法进行处理能够有效减少不必要的计算和渲染开销。
- 多次animateTo时统一更新状态变量:在进行多次动画操作时,统一更新状态变量可以避免不必要的状态更新和重复渲染,从而减少性能开销。
使用系统提供的动画接口
在HarmonyOS应用开发中,动画设计实现可以通过自定义动画或系统提供的动画接口两种方式来实现。
方式一,自定义动画:
自定义动画是指通过编写自定义得动画逻辑和计算过程来实现特定的动画效果。开发人员可以根据应用的需求和设计要求,使用自定义的动画算法和逻辑来创建独特的动画效果。自定义动画的优势在于可以实现非常个性化的动画效果,并且能够完全控制动画的每一个细节,但需要开发人员具备一定的动画算法和计算能力。
方式二,系统动画接口:
系统动画接口是指通过使用系统提供的动画框架和接口来实现动画效果。
在移动应用开发中,通常会使用属性动画来实现各种动画效果。
通过可动画属性改变引起UI上产生的连续视觉效果,即为属性动画。
属性动画是最基础易懂得的动画,ArkUI提供两种属性动画给接口animateTo
和animation
驱动组件属性按照动画曲线等动画参数进行连续的变化,产生属性动画。
使用系统提供的动画接口可以减缓动画的实现过程,并且能够充分利用系统优化的动画计算和渲染能力,从而提高动画的性能和流畅度。
场景设计
针对同一界面多个按钮同时缩放的场景(如下图所示),分别用自定义动画、属性动画、显式动画实现。
自定义动画
播放动画时,系统需要在一个刷新周期内完成动画变化曲线的计算,完成组件布局绘制等操作。使用了自定义动画,动画曲线计算过程很容易引起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() {
Button()
.width(this.widthSize)
.height(this.heightSize)
// 其他相似布局
…
Button('click me')
.onClick(() => {
let delay = 500
// 调用自定义函数
setTimeout(() => {
this.computeSize()
}, delay)
})
.width('50%').height('15%').backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
系统动画接口
系统提供的动画接口,只需设置曲线类型、终点位置、时长等信息,就能满足常用的动画功能,减少UI主线程的负载。
- 使用属性动画实现按键缩放,具体实现代码如下:
@Entry
@Component
struct PropertyAnimateToExample {
@State widtSize: number = 80;
@State heightSize: number = 40;
@State flag: boolean = true;
build() {
Column() {
Button()
.width(this.widthSize)
.height(this.heightSize)
.animation({
duration: 2000, // 动画时长
curve: Curve.Linear,// 动画曲线
delay: 500, // 动画延迟
iterations: 1, // 播放次数
playMode: PlayMode.Normal//动画模式
})
// 其他相似布局
...
Button('click me')
.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('15%').backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
- 使用显式动画实现按键缩放,具体实现代码如下:
@Entry
@Component
struct ExplicitAnimateToExample {
@State widthSize: number = 80;
@State heightSize: number = 40;
@State flag: boolean = true;
build() {
Column() {
Button()
.width(this.widthSize)
.height(this.heightSize)
// 其他相似布局
…
Button('click me')
.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('15%').backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
用图形变换属性变化组件
在应用开发中,改动组件的布局显示可以通过改动布局属性、改动图形变换属性两种方式来实现。
- 方式一,改动布局属性
常见的布局属性包括位置、大小、内边距、外边距、对齐方式、权重等。当这些布局属性发生改变时,界面将重新布局以适应新的属性值。
- 方式二,改动图形变换属性
图形变换属性是指对组件布局结果的变换操作,如平移、旋转、缩放等操作。通过改变这些图形变换属性,可以实现对组件布局完成后,在界面上的位置和形态进行动态变换。
场景设计
针对同一界面多个图片同时缩放并位移的场景(如下图所示),分别通过改变布局属性、改变图形变换属性实现。
改变布局属性
布局属性发生变化时,系统需要重新计算组件的位置和大小,然后进行重新布局。这个过程需要消耗大量的计算资源和时间,尤其是在界面中包含大量组件或者复杂布局时,性能开销会更加明显。
通过组件的width、height、position属性来改变组件大小和位置,具体代码实现如下:
@Entry
@Component
struct Index {
@State imageWidth: number = 60;
@State imageHeight: number = 60;
@State xPosition: number = 0;
@State yPosition: number = 0;
build() {
Column(){
Image($r('app.media.sample'))
.width(this.imageWidth)
.height(this.imageHeight)
.position({x: this.xPosition, y: this.yPosition})
// 其他相似布局
…
Button("布局属性")
// 按键属性设置
…
.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)
}
}
})
}.width('100%').margin({ top: 5 })
}
}
改变图形变换属性
图形变换只是对组件的显示效果进行变换,而不会改变其在布局中的位置和大小,因此不会触发重新布局的计算过程。这使得使用图形变换属性来实现动画效果时,能够提升界面的流畅性和性能表现。
通过组件的scale、translate属性来改变组件大小和位置,具体代码实现如下:
@Entry
@Component
struct Index {
@State imageScaleX: number = 1;
@State imageScaleY: number = 1;
@State imageTranslateX: number = 0;
@State imageTranslateY: number = 0;
build() {
Column() {
Image($r('app.media.like'))
.scale({ x: this.imageScaleX, y: this.imageScaleY, centerX: 0, centerY: 0 })
.translate({ x: this.imageTranslateX, y: this.imageTranslateY })
// 其他相似布局
…
Button("图形变换属性")
// 按键属性设置
…
.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: 5 })
}
}
使用图形变化属性改变图片大小和位置时,能够显著降低丢帧率和大卡顿的发生频率。界面布局是非常耗时的操作,因此频繁地改动布局属性会导致界面性能下降,出现卡顿现象,影响用户体验。因此,在动画能够使用图形变化属性实现的情况下,开发者应尽量使用图形变化属性实现,保持动画的流畅性和稳定性,提升应用的性能表现。
参数相同时使用同一个animateTo
每次调用animateTo方法,都会触发一次属性变化,这意味着在每次动画执行时都需要进行动画前后的对比,以确定属性的变化情况。当多次连续调用animateTo时,会增加额外的布局计算和绘制开销,从而降低性能表现。特别是当这些animateTo操作针对同一个组件的属性时,会导致该组件更新的次数增加,进一步影响性能。
在实际开发中,如果多个属性需要以相同的动画参数进行变化,推荐将它们放到同一个动画闭包中执行。通过将多个属性的动画操作合并到同一个动画闭包中,可以减少对组件的多次更新,避免重复的布局计算和绘制操作,提升动画效果的性能。
除了性能方面的优势,将多个属性的动画操作合并到同一个动画闭包中还有助于提高代码的可读性和维护性。通过集中管理相关联的属性变化,可以使代码结构更加清晰,便于后续的维护和修改。
场景设计
针对多个相同组件同时修改多个属性的场景(如下图所示),分别使用多个animateTo闭包和一个animateTo闭包实现。
代码实现
- 将相同动画参数的状态变量更新放在不同的动画闭包中,具体代码实现如下:
@Entry
@Component
struct MyComponent {
@State w:number = 150
@State h:number = 2
@State brightNum:number = 1.5
@State color:Color = Color.Red
// 动画闭包1,设置宽度变化
func1() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.w = (this.w === 80 ? 150 : 80);
});
}
// 动画闭包2,设置颜色变化
func2() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
}
// 动画闭包3,设置高度变化
func3() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.h = (this.h === 2 ? 5 : 2);
});
}
// 动画闭包4,设置高光变化
func4() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.brightNum= (this.brightNum=== 1.5 ? 1 : 1.5);
});
}
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
.brightness(this.brightNum)
// 其他相似布局
…
Button("click nFunc")
// 按键属性设置
…
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
this.func1();
this.func2();
this.func3();
this.func4();
}, 1000 * i)
}
})
}
}
}
- 将相同动画参数的动画合并在一个动画闭包中,具体代码实现如下:
@Entry
@Component
struct MyComponent {
@State w:number = 150
@State h:number = 2
@State brightNum:number = 1.5
@State color:Color = Color.Red
// 统一动画闭包,同时设置四个属性变化
func() {
animateTo({curve: Curve.Sharp, duration: 1000}, () => {
this.w = (this.w === 80 ? 150 : 80);
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
this.h = (this.h === 2 ? 5 : 2);
this.brightNum= (this.brightNum=== 1.5 ? 1 : 1.5);
});
}
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
.brightness(this.brightNum)
// 其他相似布局
…
Button("click oneFunc")
// 按键属性设置
…
.onClick(() => {
let doTimes = 10;
// 按播放次数循环播放动画
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
this.func();
}, 1000 * i)
}
})
}
}
}
将多个属性变化动画合并到同一个animateTo动画闭包中能够显著降低丢帧率和大卡顿的发生频率。合并动画操作可以减少不必要的布局计算和绘制开销,从而提升动画的流畅性和性能表现,有助于优化动画效果的性能。
多次animateTo时统一更新状态变量
使用animateTo方法执行动画时,会对执行动画闭包前后的状态进行对比,然后只对差异部分进行动画处理。
在动画执行过程中,脏节点是指在界面上需要进行重新绘制的区域。如果状态发生了变化,ArkUI会跟踪这些变化,并在动画闭包执行前进行状态对比,相关的脏节点会被标记为需要刷新,以便在动画闭包执行前对这些脏节点进行重新绘制。这样,只有发生变化的部分才会被纳入动画处理,而不需要重新绘制整个界面。
这种差异对比的方式能够显著减少不必要的绘制操作,提高动画的性能和流畅度。
场景设计
针对多个相同组件修改多个属性进行动画的场景(如下图所示),分别在多个animateTo之间更新状态变量、在animateTo之前显式指定属性初值、在animateTo之前使用原始状态。
在多个animateTo之间更新状态变量
如果多个animateTo之间存在状态更新,会导致执行下一个animateTo之前又存在需要更新的脏节点,可能造成冗余更新,其动画更新流程如下图所示:
在第一个animateTo前,重新设置了w属性,所以Row组件需要更新一次。在第一个animateTo的动画闭包中,改变了w属性,所以Row组件又需要更新一次并对比产生宽高动画。第二个animateTo前,重新设置了color属性,所以Row组件又需要更新一次。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了4次属性。
此外还更改了与动画无关的状态h,如果不需要改变无关状态,则不应改变造成冗余更新。
其具体代码实现如下:
@Entry
@Component
struct MyComponent {
@State w: number = 100
@State h: number = 2
@State color: Color = Color.Red
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
// 其他相似布局
…
Button("click1")
// 按键属性设置
…
.onClick(() => {
let doTimes = 5;
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
this.w = 80
// h是非动画属性
this.h = 4
animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
this.w = (this.w === 80 ? 150 : 80);
});
// 在两个animateTo之间更新状态变量
this.color = Color.Yellow
animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
}, 2000 * i)
}
})
}
}
}
统一更新状态变量
- 在animateTo之前显式指定属性初值
在animateTo之前显式的指定所有需要动画的属性初值,统一更新到节点中,然后再做动画,其动画更新流程如下图所示:
在第一个animateTo之前,重新设置了w和color属性,所以Row需要更新一次。在第一个animateTo的动画闭包中,改变了w属性,所以Row组件需要更新一次并对比产生宽高动画。在第二个animateTo之前,由于没有执行额外的语句,不存在需要更新的脏状态变量和脏节点,无需更新。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了3次属性。
其具体代码实现如下:
@Entry
@Component
struct MyComponent {
@State w: number = 100
@State h: number = 2
@State color: Color = Color.Red
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
// 其他相似布局
…
Button("click2")
// 按键属性设置
…
.onClick(() => {
let doTimes = 5;
for (let i = 0; i < doTimes; i++) {
setTimeout(() => {
// 在动画之前显式的指定所有需要动画的属性初值
this.w = 80
this.color = Color.Yellow
// 动画1,修改宽度
animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
this.w = (this.w === 80 ? 150 : 80);
});
// 动画2,修改颜色
animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
// 动画完成后刷新非动画属性
this.h = 5
}, 2000 * i)
}
})
}
}
}
- 在animateTo之前使用原始状态
在animateTo之前使用原始状态,让动画从原始状态过渡到指定状态,其动画更新流程如下图所示:
在第一个animateTo之前,不存在需要更新的脏状态变量和脏节点,无需更新。在第一个animateTo的动画闭包中,改变了w属性,所以Row组件需要更新一次并对比产生宽高动画。在第二个animateTo之前,由于也没有执行额外的语句,不存在需要更新的脏状态变量和脏节点,无需更新。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了2次属性。
其具体代码实现如下:
@Entry
@Component
struct MyComponent {
//原始状态
@State w: number = 80
@State h: number = 5
@State color: Color = Color.Yellow
build() {
Column() {
Row()
.width(this.w)
.backgroundColor(this.color)
.height(this.h)
// 其他相似布局
…
Button("click3")
// 按键属性设置
…
.onClick(() => {
let doTimes = 5;
for (let i = 0; i < doTimes; i++) {
// 直接使用原始状态实现动画
setTimeout(() => {
animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
this.w = (this.w === 80 ? 150 : 80);
});
animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
});
}, 2000 * i)
}
})
}
}
}
性能分析结论
在进行属性变化动画时,显式指定属性初值或者使用原始状态作为动画的起始状态能够显著降低丢帧率和大卡顿的发生频率。因此,在进行动画操作时,合理管理状态变量的更新和初始值设定对于优化动画效果的性能至关重要,开发者应根据需要,尽可能地避免在多个animateTo之间更新状态变量,从而提升动画的流畅性和性能表现。
使用renderGroup
renderGroup是组件通用方法,它代表了渲染绘制的一个组合。其核心功能就是标记组件,在绘制阶段将组件和其子组件的绘制结果进行合并并缓存,以达到复用的效果,从而降低绘制负载。
首次绘制组件时,若组件被标记为启用renderGroup状态,将对组件和其子组件进行离屏绘制,将绘制结果进行缓存。此后当需要重新绘制组件时,就会优先使用缓存而不必重新绘制,从而降低绘制负载,优化渲染性能。组件渲染流程图如下所示:
在进行缓存更新时,需要满足以下三个条件:
- 组件在当前组件树上。
- 组件renderGroup被标记为true。
- 组件内容被标脏。
在进行缓存清理时,需要满足以下任意条件:
- 组件不存在于组件树上。
- 组件renderGroup被标记为false。
具体缓存管理流程图如下所示:
说明
为了能使renderGroup功能生效,组件存在以下约束。
- 组件内容固定不变:父组件和其子组件各属性保持固定,不发生变化。如果父组件内容不是固定的,也就是说其子组件中上存在某些属性变化或者样式变化的组件,此时如果使用renderGroup,那么缓存的利用率将大大下降,并且有可能需要不断执行缓存更新逻辑,在这种情况下,不仅不能优化卡顿效果,甚至还可能使卡顿恶化。例如:文本内容使用双向绑定的动态数据;图片资源使用gif格式;使用video组件播放视频。
- 子组件无动效:由父组件统一应用动效,其子组件均无动效。如果子组件上也应用动效,那么子组件相对父组件就不再是静止的,每一帧都有可能需要更新缓存,更新逻辑同样需要消耗系统资源。
场景设计
此案例在同一个页面下使用了固定的图片和文本内容,并且每个组件统一使用旋转和缩放的动效,并且重复使用了60个这样的组件。
代码实现
实现页面中需要使用的组件,并设置renderGroup属性,用@StorageLink修饰绑定renderGroupFlag标识。renderGroup属性的设置要满足父组件统一应用动效,子组件无动效,所以,动效在父组件上统一实现,未在IconItem上设置动效。在Flex组件中,Image和Text都无动效设置,所以在最外层Flex上设置renderGroup属性即可。
具体代码如下所示:
// IconItem.ets
@Component
export struct IconItem {
@StorageLink('renderGroupFlag') renderGroupFlag: boolean = false;
image: string | Resource = '';
text: string | Resource = '';
build() {
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.Center,
alignContent: FlexAlign.Center
}) {
Image(this.image)
.height(20)
.width(20)
.objectFit(ImageFit.Contain)
.margin({ left: 15 })
Text(this.text)
.fontSize(10)
.fontColor("#182431")
.margin({ top: 5 })
.width(50)
.opacity(0.8)
.textAlign(TextAlign.Center)
}
.backgroundColor('#e3e3e3')
.width(50)
.height(50)
.borderRadius(25)
// 在IconItem内调用renderGroup,false为关闭,true为开启
.renderGroup(this.renderGroupFlag)
}
}
实现展示的页面,将控制renderGroup的标识renderGroupFlag通过APPStorage进行存储。
// Index.ets
import { IconItem } from './IconItem'
// IconItem相关数据
class IconItemSource {
image: string | Resource = ''
text: string | Resource = ''
constructor(image: string | Resource = '', text: string | Resource = '') {
this.image = image;
this.text = text;
}
}
@Entry
@Component
struct Index {
// renderGroup接口是否开启
@State renderGroupFlag: boolean = false;
private iconItemSourceList: IconItemSource[] = [];
aboutToAppear() {
// 遍历添加60个IconItem的数据
for (let index = 0; index < 20; index++) {
const numStart: number = index * 3;
// 此处循环使用三张图片资源
this.iconItemSourceList.push(
new IconItemSource($r('app.media.album'), `item${numStart + 1}`),
new IconItemSource($r('app.media.applet'), `item${numStart + 2}`),
new IconItemSource($r('app.media.cards'), `item${numStart + 3}`),
);
}
}
build() {
Column() {
Row() {
Row() {
Text('场景示例')
.fontSize(24)
.lineHeight(24)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.margin({ left: 30 })
}
// 动态切换renderGroup功能
Stack({ alignContent: Alignment.End }) {
Button(this.renderGroupFlag ? 'renderGroup已开启' : 'renderGroup已关闭', {
type: ButtonType.Normal,
stateEffect: true
})
.fontSize(12)
.borderRadius(8)
.backgroundColor(0x317aff)
.width(150)
.height(30)
.margin({ right: 30 })
.onClick(() => {
this.renderGroupFlag = !this.renderGroupFlag;
AppStorage.setOrCreate('renderGroupFlag', this.renderGroupFlag)
})
}
}
.height(56)
.width('100%')
.backgroundColor(Color.White)
.justifyContent(FlexAlign.SpaceBetween)
// IconItem放置在grid内
GridRow({
columns: 6,
gutter: { x: 0, y: 0 },
breakpoints: { value: ["400vp", "600vp", "800vp"],
reference: BreakpointsReference.WindowSize },
direction: GridRowDirection.Row
}) {
ForEach(this.iconItemSourceList, (item: IconItemSource) => {
GridCol() {
IconItem({ image: item.image, text: item.text })
.transition(
TransitionEffect.scale({ x: 0.5, y: 0.5 })
.animation({duration: 3000, curve: Curve.FastOutSlowIn, iterations: -1 })
.combine(TransitionEffect.rotate({ z: 1, angle: 360 })
.animation({ duration: 3000, curve: Curve.Linear, iterations: -1 }))
)
}
.height(70)
.width('25%')
})
}
.width("100%")
.height("100%")
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
}
}
总结
在满足上述的约束后,使用renderGroup时,能够显著降低丢帧率、最大连续丢帧数和平均1s大卡顿次数。因此,在在单一页面上存在大量应用动效的组件,且组件和其子组件各属性保持固定、组件统一应用动效时,开发者可以使用renderGroup来提升应用的性能,保证应用及动画的流畅性。
提升动画运行流畅度总结
本文通过4个测试验证了使用系统提供的动画接口、使用图形变换属性变化组件布局、参数相同时使用同一个animateTo、多次animateTo时统一更新状态变量这四项措施的有效性。
动画的性能表现对于用户体验、设备资源消耗、应用稳定性和流量消耗等方面都具有重要意义:
- 用户体验:流畅的动画能够提升用户体验,使用户感到界面更加生动和直观。相反,卡顿或者延迟的动画会带来不良的体验。
- 设备资源消耗:动画的性能表现会直接影响设备的资源消耗,尤其是CPU和GPU的占用。低性能的动画可能导致设备发热、耗电量增加,甚至影响其他应用的运行。
- 应用稳定性:性能较差的动画可能会导致应用崩溃或者卡顿,影响应用的稳定性和可靠性。
- 节省流量:高性能的动画可以减少数据传输量,对于移动应用来说,这意味着可以节省用户的流量消耗。
- 适应不同设备:不同设备的性能差异很大,提升动画性能可以使应用在各种设备上都能够流畅运行。
在实际开发中,开发人员可以结合本文提出的四项措施,并根据具体场景和需求进行优化,从而达到更好的动画性能表现。
合理使用动画总结
在实际开发过程中,开发者应该充分认识到动画对用户体验的重要性。动画不仅可以增强用户对应用的吸引力,还能够提升用户的参与感和操作的直观性。
本文通过两个角度介绍了如何合理使用动画:
- 提升动画感知流畅度:首先,从UX设计视角出发,开发者需要理解动画在用户体验中的作用和意义。其次,开发者需要具备一定的动画实现技术,能够根据设计需求,合理、高效地使用动画能力实现各类动画效果。
- 提升动画运行流畅度:为了进一步提升动画性能,开发者还需要采取一些措施。例如,可以通过使用系统提供的动画接口、使用图形变换属性变化组件布局、参数相同时使用同一个animateTo、多次animateTo时统一更新状态变量等方式,有效减少动画对系统资源的占用,提升动画的流畅度和稳定性。
总之,合理使用动画对于提升用户体验至关重要。开发者需要从UX设计视角转换为开发实现视角,充分认识动画在用户体验中的作用和意义,并通过合理的实现和性能优化来提升动画的效果和稳定性。
合理使用转场
合理使用页面间转场概述
页面间转场是用户从一个页面切换到另一个页面时的过程,一个无缝流畅的转场动效可以提升用户的交互体验。从主页到详情页、从列表页到结果页都需要去设置一些转场动效使得用户体验更加流畅。基于用户行为和应用设计模式,我们总结出了一些常见的转场场景,包括层级转场、搜索转场、新建转场、编辑转场、通用转场、跨应用转场。针对这些转场场景,根据“人因研究”(在 HarmonyOS 中,通过大量的人因研究为UX设计提供了系统性的科学指导),给各位开发者推荐一些适合本场景下转场动效,常见的转场动效有左右位移遮罩动效、一镜到底动效等。
HarmonyOS系统为开发者提供了丰富的转场能力和动画能力,在实际开发过程中,我们需要把上述UX设计视角转换为开发实现视角,即使用HarmonyOS系统提供的转场能力和动画能力来实现UX设计的场景和动效,一般来说需要采取如下步骤完成视角转换:
- 了解系统能力:首先,开发者需要深入了解HarmonyOS系统提供的转场能力和动画功能。这包括了解页面转场的机制、转场动画的类型和特性,以及如何在HarmonyOS应用中使用相关API。
- 分析UX设计视角:仔细分析UX设计所提供的转场场景和动效。理解设计师的意图,包括页面之间的过渡效果、元素的动画行为,以及何时触发和结束转场动效。
- 设计转场方案:基于分析的结果,设计出合理的转场方案。确定页面切换的触发时机、转场动画的类型和参数等。
- 使用转场能力:利用HarmonyOS提供的转场能力,如UIAbility转场、页面路由转场、组件转场,来实现页面之间的转场效果。
- 使用动画能力:利用HarmonyOS提供的动画能力,如属性动画、出现/消失转场动画等来实现元素的平移、缩放、旋转、透明度等动画行为。
- (可选)使用高级模板化转场:HarmonyOS已经为我们提供了一些基于场景化封装的相关高级模板化转场,如导航转场、模态转场、共享元素转场。
- 调试和优化:在实施转场和动效的过程中,进行调试和优化。确保转场效果流畅,动效符合预期,且满足性能要求。
通过以上步骤,开发者可以将UX设计视角转换为开发实现视角,并将设计师提供的转场场景和动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,并提供良好的用户体验。
转场场景设计
转场动效
HarmonyOS系统为开发者提供了丰富的转场动效库,使开发者能够轻松实现各种转场动画效果。以下是一些在HarmonyOS系统中提供的转场动效:
- 左右位移遮罩动效:这种效果在转场过程中,页面元素会以左右方向进行位移,并且使用遮罩效果来过渡。这种转场效果常用于切换不同页面或者展示不同内容的情况,能够给用户带来明显的页面变化感。
- 左右间隔位移动效:这种效果在转场过程中,页面元素会以左右方向进行位移,但是与左右位移遮罩转场不同的是,元素之间会有一定的间隔。这种转场效果常用于展示多个相关页面之间的切换,能够给用户带来清晰的页面切换感。
- 一镜到底动效:这种效果在转场过程中,整个页面会以一种平滑的方式从一个场景过渡到另一个场景,仿佛是通过一镜到底的方式切换。这种转场效果常用于展示不同页面之间的关联性,能够给用户带来流畅的视觉体验。
- 淡入淡出动效:这种效果在转场过程中,页面元素会以逐渐淡入或淡出的方式进行过渡。这种转场效果常用于切换不同页面或者展示不同内容的情况,能够给用户带来柔和的视觉过渡效果。
- 缩放动效:这种效果在转场过程中,页面元素会以放大或缩小的方式进行过渡。这种转场效果常用于突出某个元素或者展示不同页面之间的层次感,能够给用户带来视觉上的冲击和焦点转移。
开发者可以根据具体需求,在应用的不同场景中应用这些转场动效,以提升用户体验和界面的吸引力。需要注意的是,为了最佳的用户体验,开发者应根据界面的功能和特点,合理选择转场动效,并遵循动效的使用准则,以确保转场动效在视觉和交互上的一致性。具体实现效果,请参考下一章节案例。
转场场景
层级转场
层级转场是指在用户界面中,从一个层级结构的界面状态转换到另一个层级结构的界面状态的过程,它通常用于在应用中进行页面间的导航和视图层级的变化。层级转场的场景可以划分为卡片、图标展开和列表展开:
- 列表展开:通常是完整的页面替换,开发者可以使用左右位移遮罩动效完成这类变化,见图2。
- 卡片/图表展开:单体独立卡片展开推荐使用一镜到底动效,见图3;相对复杂的组合卡片样式则需要由开发者以更为符合用户视觉流畅感为标准,根据实际情况选择左右位移遮罩动效(见图4)或一镜到底动效。
**图4 **左右位移遮罩在混合样式场景下的用例
对于层级转场,推荐使用系统转场,页面转场采用左右位移的运动方式,不应单帧直接切换或上下位移切换,曲线优先使用弹簧曲线。
搜索转场
搜索转场是指在用户执行搜索操作,如在搜索栏中输入关键词并按下搜索按钮、或者直接触摸搜索图标时,应用改变应用页面以显示搜索结果的过程。它包含了固定搜索区域和非固定搜索区域两种情况:
- 固定搜索区域:在固定搜索区域中,大部分空间是不需要变化的,只是在上面增加了一层蒙版。主要变化区域集中在页眉,即搜索框和返回按键。当用户触发搜索操作时,页面可以使用淡入淡出动效来优化搜索体验,搜索框和返回按键通过渐变的方式进入视图,从而吸引用户的注意力,见图5。
- 非固定搜索区域:在非固定搜索区域中,页面的变化更加复杂。为了保持用户的注意力和流畅的体验,可以使用一镜到底的动效,让搜索框始终保持在用户视线焦点中,相对忽视页面中其余变动较大的部分,见图6。
**图5 **淡入淡出在固定搜索区域场景下的用例
**图6 **一镜到底在非固定搜索区域场景下的用例
对于搜索转场,推荐使用共享元素转场,搜索框作为持续存在的元素串联前后两个界面,其他元素可采用淡入淡出或者其他过渡方式,不应单帧切换或非共享元素的方式转场
新建转场
新建转场是指用户创建新内容或实体时,应用页面发生的过渡效果,它可以让用户感知到新的事物的添加或创建,并提供一种连贯和引人注目的视觉切换。由于新建页面中需要完成整个页面的替换,推荐开发者使用左右位移遮罩作为转场动效,如下图所示
**图7 **左右位移遮罩在新建闹钟场景下的用例
对于新建转场,推荐使用系统转场,页面转场采用左右位移的运动方式,不应单帧直接切换或上下位移切换,曲线优先使用弹簧曲线。
编辑转场
用户对现有内容或实体进行编辑时,例如点击“编辑”按钮、选择要编辑的项目或内容,或者执行其他与编辑相关的动作,应用应提供动效引导用户进入一个用于编辑现有内容的页面,修改所需的信息。在这个场景下,开发者需要达成的视觉效果是从编辑按键处弹出编辑页面,类似于单体卡片展开的效果。但由于一般的编辑按键并没有分明的外框,并不适用一镜到底的动效,此时淡入淡出能够提供类似于一镜到底的效果,如下图所示。
**图8 **淡入淡出在编辑联系人信息场景下的用例
对于编辑转场,推荐使用系统转场,页面转场采用淡入淡出的过渡方式,不应单帧直接切换或位移切换。
通用转场
通用转场是一种广泛适用于不同情境和应用类型的页面过渡效果,目的是提供一种通用的、可重复使用的方式,以改善用户页面之间的切换,增强用户体验。其关键点在于要适用各种应用情境,包括不同类型的应用(例如社交媒体、电子商务、新闻等)和不同操作(例如导航、搜索、编辑等)。这就需要一种通用的、不需要复杂操作的动效来完成跳转任务,而缩放能够满足绝大多数用户的需求和视觉体验感受,如下图所示。
**图9 **缩放在单体卡片场景下的用例
跨应用转场
跨应用转场是指用户从一个应用程序切换到另一个应用程序,用户能够无缝地从一个应用切换到另一个应用,而不会感到中断或不适。和以上几类转场都不同的是,用户点击应用内的链接、按钮或执行其他与外部应用交互的动作后,页面的跳转已经不仅仅存在于页面与页面之间,而是应用与应用之间,为此,推荐开发者使用专为此设计的左右间隔位移动效,跳转效果如下图所示。
**图10 **左右间隔位移在跨应用跳转场景下的用例
场景解构
转场是由交互行为引起的界面变化,分析界面元素在过程中的意义,定义其在转场中所在的类型,并将它们进行分类,元素所属的类别会影响它们使用怎样的转场能力,同时也将决定用什么类型的曲线和时长。
- 进场元素:转场中新出现的元素,一般是结果界面上的构成元素。
- 出场元素:转场中消失的元素,一般是上一界面中的构成元素。
- 持续元素:转场中持续存在的元素,可以是元素在布局上的变化,也可以是某种连续性的动画效果,整个过程无中断。
- 静止元素:转场中无任何变化的元素。**图11 **分析元素示例
如上图中示例,①是进场元素,②是出场元素,③是持续元素,④是静⽌元素。
接下来,开发者需要根据分析的元素类型选择合适的转场能力,并综合考虑元素和页面的整体感官效果。不同的元素类型可能需要不同的转场方式来展现其特定的特征和交互效果。
转场场景开发
转场能力
开发人员接收到设计需求后,需要选择合适的转场能力完成该设计。HarmonyOS为开发者提供了UIAbility转场、页面路由和组件转场三种方式,在选择转场方式时,开发者需要考虑用户体验、界面一致性和业务需求,确保所选导航组件能够提供直观、易用的导航方式,帮助应用实现更好的转场效果。
- UIAbility组件间交互:UIAbility是系统调度的最小单元。通过调用startAbility()方法启动UIAbility实现在设备内的功能模块之间的跳转。该UIAbility可以是应用内的其他UIAbility,也可以是其他应用的UIAbility(例如启动三方支付UIAbility)。
- 页面路由(@ohos.router):页面路由指在应用程序中实现不同页面之间的跳转和数据传递。HarmonyOS提供了Router模块,通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面。
- 组件转场:组件转场是指通过HarmonyOS提供的各类组件来实现转场效果,以便更加便捷地展示不同的内容或功能模块。在组件转场中,可以使用诸如页面切换动画、过渡效果、布局变化等方式来实现页面之间的平滑切换。
动画能力
转场动画是指对将要出现或消失的组件做动画,对始终出现的组件做动画应使用属性动画。转场动画主要为了让开发者从繁重的消失节点管理中解放出来,如果用属性动画做组件转场,开发者需要在动画结束回调中删除组件节点。同时,由于动画结束前已经删除的组件节点可能会重新出现,还需要在结束回调中增加对节点状态的判断。
转场动画分为基础转场和高级模板化转场,出现/消失转场是一种基础转场,是对新增、消失的控件实现动画效果的能力。为了简化开发者工作,HarmonyOS提供了以下高级模板,将属性动画和出现消失动画封装,开发者只需调用接口,可以轻松的完成页面转场:
- 导航转场:页面的路由转场方式,对应一个界面消失,另外一个界面出现的动画效果,如设置应用一级菜单切换到二级界面。
- 模态转场:新的界面覆盖在旧的界面之上的动画,旧的界面不消失,新的界面出现,如弹框就是典型的模态转场动画。
- 共享元素转场:共享元素转场是一种界面切换时对相同或者相似的元素做的一种位置和大小匹配的过渡动画效果。
说明
在实现组件出现和消失的动画效果时,相比于组件动画(animateTo),推荐优先使用transition。因为animateTo需要在动画前后做两次属性更新,而transition只需做一次条件改变更新,性能更好。此外,使用transition可以避免在结束回调中做复杂逻辑处理,开发实现更容易。
最佳实践案例
如表1为各场景推荐动效及其转场能力,根据表中推荐的动效及其转场能力,本文将针对HMOS世界App已有的转场案例,对如何在具体场景下选用转场能力实现动效具体分析。
转场场景 | 动效 | 推荐使用的转场能力 | ||
---|---|---|---|---|
层级转场 | 卡片/图标展开 | 组合卡片样式 | 左右遮罩位移(可考虑一镜到底) | 页面路由转场、导航转场模板 |
单体卡片样式 | 一镜到底 | 模态转场模板、共享元素转场模板 | ||
列表展开 | 列表样式(非卡片) | 左右遮罩位移 | 页面路由转场、导航转场模板 | |
混合样式(卡片+列表+图标) | 左右遮罩位移 | 页面路由转场、导航转场模板 | ||
搜索转场 | 固定搜索区域 | 淡入淡出 | 组件转场、模态转场模板 | |
非固定搜索区域 | 一镜到底 | 共享元素转场模板、模态转场模板 | ||
新建转场 | 左右遮罩位移 | 组件转场、模态转场模板 | ||
编辑转场 | 淡入淡出 | 组件转场 | ||
通用转场 | 缩放 | 组件转场、模态转场模板 | ||
跨应用转场 | 左右间隔位移 | 组件转场 |
导航转场模板实现层级转场
场景设计
在本场景中,需要在地图上显示6个具体的坐标位置,点击对应的坐标icon后,能跳转到对应的详情介绍页面中去,其交互逻辑如下图所示:
其最终实现效果如下图所示:
场景开发
在这个场景中,进场元素是一个全新的图片页面,出场元素为地图页面及其icon图标,没有持续元素和静止元素。开发者常用的页面路由方式,需要为每个坐标icon创建对应的页面,并在点击每个坐标icon时,将对应页面入栈,同时需要处理页面跳转和导航栏的显示与隐藏。这样的实现方式会导致路由栈的对应关系变得复杂,不利于开发者的设计和维护。
相比之下,使用Navigation可以简化开发流程。Navigation提供了一套统一的导航管理机制,用于管理应用中的页面栈和页面跳转。它会自动为每个坐标icon生成对应的页面,并提供默认的点击响应处理。这样,开发者不需要自定义点击事件逻辑,也不需要手动管理路由栈的对应关系。
使用Navigation可以有效简化开发过程,提高开发效率。开发者只需要关注坐标icon的展示和属性设置,而不需要过多关注页面跳转和路由栈的维护。这样可以使开发者更专注于界面设计和功能实现,同时减少了开发和维护的复杂性。
Navigation组件的实现方式如下:
- 页面栈管理:Navigation维护了一个页面栈,用于管理应用中打开的页面。通过页面栈,我们可以方便地进行页面的打开、关闭和切换等操作。
- 页面跳转:通过Navigation,我们可以实现页面之间的跳转。可以使用push方法将目标页面压入页面栈,实现页面的跳转。也可以使用replace方法替换当前页面为目标页面。
- 参数传递:在页面跳转过程中,我们经常需要将一些参数传递给目标页面。可以通过Navigation的参数机制来实现参数的传递。例如,可以将参数附加在目标页面的路径中,或者使用query参数、state参数等方式进行传递。
- 页面返回:在目标页面完成操作后,用户可能需要返回到上一个页面。可以使用Navigation的back方法来实现页面的返回。back方法可以指定返回的页面和返回方式,例如返回上一个页面、返回到指定页面等。
代码实现
Navigation组件的页面包含主页和内容页。主页由标题栏、内容区和工具栏组成,可在内容区中使用NavRouter子组件实现导航栏功能。内容页主要显示NavDestination子组件中的内容。具体实现代码如下:
build() {
Stack() {
// Navigation组件一般可以用来实现标题栏、工具栏、导航栏等。
Navigation() {
//分别对每一个地图上的导航mapElements进行渲染和链接
ForEach(this.mapElements,
(item: MapElement) => {
Column() {
// NavRouter导航组件,默认提供点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter必须包含两个子组件,其中第二个子组件必须为NavDestination。
NavRouter() {
//地图上对应导航位置图标资源的渲染
AnimIcon({ item: item })
......
// 作为NavRouter组件的子组件,用于显示导航内容区。其中IntroductionPage即为即将跳转的对应页面
NavDestination() {
IntroductionPage({currentZoneId: item.id})
}
}
// 指定点击NavRouter跳转到NavDestination页面时使用的路由模式。此处不需要保存原页面,选择replace方式,跳转后页面销毁,且该页面信息从路由栈中清除。
.mode(NavRouteMode.REPLACE)
}
......
},
(item: MapElement) => JSON.stringify(item)
)
}
// 将mode属性设置为NavigationMode.Stack,Navigation组件即可设置为单页面显示模式。
.mode(NavigationMode.Stack)
.hideNavBar(true)
.hideTitleBar(true)
.hideBackButton(true)
.hideToolBar(true)
}
}
Tabs组件实现层级转场
场景设计
如下图所示,在本场景中,需要完成是效果是在用户主页切换显示收藏、浏览、成就三个页面。
最终实现的动画效果如下图所示:
场景开发
在这个场景中,进场元素是另一个标签页,出场元素为原先的标签页,持续元素是【收藏】、【浏览】和【成就】三个标签按钮,静止元素是不涉及切换的标签按钮。考虑到页面数量较少且切换效果要求符合界面设计需求,选择使用Tabs组件来实现标签页切换是一个不错的选择,理由如下:
- Tabs组件可以对页面内容进行分类,其提供了标签页切换的模板,可以方便地在主页上展示【收藏】、【浏览】和【成就】三个标签,并通过点击不同的标签页来切换显示内容。
- Tabs组件的内置左右位移遮罩动效可以很好地满足界面设计的需求,给用户带来良好的交互体验。
- Tabs组件封装了实现标签页切换的接口TabsController,使得开发者可以更方便地进行开发。通过配置Tabs的属性和事件,开发者可以自定义标签页的样式、切换效果,以及处理标签页切换时的逻辑操作。这样可以提高开发效率,同时确保标签页切换的功能和效果符合需求。
Tabs组件的实现方式如下:
- 创建标签页:使用TabContent组件来创建标签页,并设置每个标签页的内容和样式。
- 切换标签页:当用户点击不同的标签页时,应用需要切换到相应的标签页。可以通过监听标签页的点击事件,或者使用TabsController来控制标签页的切换。
- 显示当前标签页内容:当切换到不同的标签时,应用需要显示当前标签页的内容。可以根据当前选中的标签页,动态加载并显示相应的内容。
Tabs除了默认的标签页切换效果,开发者还通过animationDuration属性设置TabContent滑动动画时长。
代码实现
Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。其中,TabContent是内容页,TabBar是导航页签栏。具体实现代码如下:
build() {
......
ListItem() {
// Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。
Tabs() {
// TabContent仅在Tabs中使用,对应一个切换页签的内容视图
TabContent() {
// 收藏列表页面
CollectedResourceView({ innerScroller: this.innerScroller, outerScroller: this.outScroller })
}
// tabBar设置TabBar上显示内容。每一个TabContent对应的内容需要有一个页签,可以通过TabContent的tabBar属性进行配置。
.tabBar(this.MineTabs(TabInfo.COLLECTED, $r('app.string.my_collected')))
.backgroundColor($r('app.color.hmos_background_color_white'))
TabContent() {
// 浏览列表页面
ViewedResourceView({ innerScroller: this.innerScroller, outerScroller: this.outScroller })
}
.tabBar(this.MineTabs(TabInfo.VIEWED, $r('app.string.my_viewed')))
.backgroundColor($r('app.color.hmos_background_color_white'))
TabContent() {
// 成就展示页面
AchievesView()
}
.tabBar(this.MineTabs(TabInfo.ACHIEVEMENT, $r('app.string.my_achieve')))
.backgroundColor($r('app.color.hmos_background_color_white'))
}
.onChange((index: number) => {
this.currentIndex = index;
})
......
}
......
}
模态转场模板实现通用转场
场景设计
在本场景中,当用户点击介绍详情页中的图片时,图片会放大展示,覆盖在原有界面上,并且点击空白处完成返回操作,如下图所示:
**图16 **图片点击前后交互逻辑
最终实现的动画效果如下图所示:
**图17 **图片详情展示效果图
说明
模态转场分为全屏模态转场、半模态转场、菜单弹出效果、气泡弹窗效果,本例中展示的是全屏模态转场,更多模态转场请参考官网样例。
场景开发
在这个场景中,进场元素是一个图片页面,出场元素为滑动窗口的页面,持续元素是持续放大的图片,没有静止元素。可以通过触发模态转场效果,将大图以覆盖的方式展示在当前界面上。这样,用户可以直接在当前界面上查看大图,并且不会感到界面的切换和截断。当用户想要返回上级详情页时,只需要点击大图的空白处,即可关闭大图,恢复到原先的界面状态。
相比于常规的页面跳转方式,模态转场模板可以让新的界面覆盖在旧的界面上,使得旧的界面不消失,从而避免了用户产生截断的感觉。并且,使用模态转场可以提供更快速的页面跳转效果,通过在当前页面上弹出模态框或对话框,可以让用户专注于当前操作同时不会丢失原页面的上下文,更流畅地浏览和操作应用。同时,模态转场也能够减少页面销毁和创建的开销,从而提高应用的性能和响应速度。
模态转场的实现方式如下:
- 创建模态框或对话框:首先,我们需要在原页面中创建一个模态框或对话框,并设置其内容和样式。可以使用Dialog组件或自定义组件来实现模态框或对话框的创建。
- 显示模态框或对话框:当需要显示模态框或对话框时,可以调用show方法来显示它。在调用show方法时,可以指定模态框或对话框的样式、动效效果等。
- 处理模态框或对话框的关闭:当模态框或对话框关闭后,我们需要处理其关闭事件。可以通过监听模态框的cancel事件或对话框的dismiss事件获取关闭事件,并在事件处理函数中执行相应的操作。
- 返回原页面:在模态框或对话框关闭后,开发者可以调用back方法来实现页面的返回。在back方法中,可以指定返回的页面和返回方式,例如返回上一个页面、返回到指定页面等。
代码实现
通过bindContentCover属性为组件绑定全屏模态页面,在组件插入和删除时可通过设置转场参数ModalTransition显示过渡动效。具体实现代码如下:
build() {
Stack() {
ForEach(this.introductionData.imageList, (item: ResourceStr, index?: number) => {
if (index !== undefined) {
Row() {
Image(item)
.objectFit(ImageFit.ScaleDown)
// geometryTransition绑定id
.geometryTransition(index === this.currentIndex ? 'share_' + this.currentIndex : '')
......
}
.onClick(() => {
// 设置动画参数,curves.springMotion构造弹性动画对象
animateTo({ curve: curves.springMotion(Const.SPRING_RESPONSE, Const.DAMPING_FRACTION) }, () => {
this.isPresent = !this.isPresent;
});
})
}
}, (item: ResourceStr) => JSON.stringify(item))
}
.width(Const.FULL_PERCENT)
.height($r('app.float.swiper_height'))
.alignContent(Alignment.Center)
// 通过bindContentCover属性为组件绑定全屏模态页面,在组件插入和删除时,可通过设置转场参数ModalTransition显示过渡动效。
.bindContentCover($$this.isPresent, this.PhotoBuilder, {modalTransition: ModalTransition.ALPHA, onAppear: () => {}, onDisappear: () => {}})
}
实现bindContentCover方法的builder参数,具体实现代码如下:
@Builder
PhotoBuilder() {
ImageViewDialog({ isPresent: $isPresent, currentIndex: $currentIndex })
.width(Const.FULL_PERCENT)
.height(Const.FULL_PERCENT)
.transition(TransitionEffect.opacity(1))
.backgroundBlurStyle(this.isPresent ? BlurStyle.Regular : BlurStyle.NONE, { colorMode: ThemeColorMode.DARK })
}
ImageViewDialog组件实现代码如下:
build() {
Stack() {
Swiper() {
ForEach(this.introductionData.imageList, (item: Resource, index?: number) => {
Column() {
......
Image(item)
.width(Const.FULL_PERCENT)
.syncLoad(true)
.objectFit(ImageFit.Contain)
.gesture(
PinchGesture()
.onActionStart(() => {
this.isGesture = true;
})
.onActionUpdate((event?: GestureEvent) => {
if (event) {
this.imgScale = this.curScale * event.scale;
}
})
.onActionEnd(() => {
this.limitScale(false);
})
)
// geometryTransition绑定id
.geometryTransition(index === this.currentIndex && !this.isGesture ? 'share_' + index : '')
......
}
.width(Const.FULL_PERCENT)
.height(Const.FULL_PERCENT)
.justifyContent(FlexAlign.Center)
})
}
......
}
}
总结与回顾
合理使用页面间转场是提升用户体验的重要技术之一,在应用开发过程中,通过动效的运用,可以使应用界面更加生动、流畅,并且能够引导用户的注意力,提高用户的操作效率。合理使用动效需要考虑以下几点:
- 符合界面设计需求:动效应该与应用的整体设计风格和用户期望相匹配,不应过于炫目或过于简单。动效的设计应该与应用的功能和交互逻辑相符,能够提供有意义的反馈和引导。
- 提高用户体验:动效可以增强用户对界面操作的反馈感知,使用户能够更直观地理解应用的状态和变化。例如,在页面切换时使用渐变效果或位移动画,可以使用户感到界面的流畅性和连贯性,提高用户的满意度和使用体验。
- 控制动效的频率和时长:动效的过度使用可能会让用户感到疲劳或干扰用户的操作。因此,在设计动效时需要注意控制动效的频率和时长,避免过多的动效干扰用户的操作和阅读。
- 考虑性能和流畅度:在使用动效时,需要考虑应用的性能和流畅度。过多或复杂的动效可能会导致应用的性能下降,影响用户的体验。因此,在设计动效时需要合理权衡动效的效果和应用的性能,确保动效的运行流畅和稳定。
在应用开发过程中,开发者可以借助HarmonyOS中提供的导航组件和转场动效,简化开发流程,提高开发效率,实现符合规范要求的转场动效效果。