页面间转场是用户从一个页面切换到另一个页面时的过程,一个无缝流畅的转场动效可以提升用户的交互体验。从主页到详情页、从列表页到结果页都需要去设置一些转场动效使得用户体验更加流畅。基于用户行为和应用设计模式,总结出了一些常见的转场场景,包括层级转场、搜索转场、新建转场、编辑转场、通用转场、跨应用转场。针对这些转场场景,根据“人因研究”(在 HarmonyOS 中,通过大量的人因研究为UX设计提供了系统性的科学指导),给各位开发者推荐一些适合本场景下转场动效,常见的转场动效有左右位移遮罩动效、一镜到底动效等。
HarmonyOS为开发者提供了丰富的转场能力,如UIAbility转场、页面路由转场、组件转场。同时,HarmonyOS提供了一些基于场景化封装的相关高级模板化转场,如导航转场、模态转场、共享元素转场,来实现页面之间的转场效果。
在实际开发过程中,需要把上述UX设计视角转换为开发实现视角,即使用HarmonyOS提供的转场能力和动画能力来实现UX设计的场景和动效。在视角转化上,包含了分析UX设计视角、设计转场方案、使用转场与动画能力、使用高级模板化转场、调试和优化,详细可以参考《合理使用动画》。通过以上步骤,开发者可以将UX设计视角转换为开发实现视角,并将设计师提供的转场场景和动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,并提供良好的用户体验。
图1 合理使用导航组件和转场动效
HarmonyOS系统为开发者提供了丰富的转场动效库,使开发者能够轻松实现各种转场动画效果。以下是一些在HarmonyOS系统中提供的转场动效:
开发者可以根据具体需求,在应用的不同场景中应用这些转场动效,以提升用户体验和界面的吸引力。需要注意的是,为了最佳的用户体验,开发者应根据界面的功能和特点,合理选择转场动效,并遵循动效的使用准则,以确保转场动效在视觉和交互上的一致性。具体实现效果,请参考下一章节案例。
层级转场
层级转场是指在用户界面中,从一个层级结构的界面状态转换到另一个层级结构的界面状态的过程,它通常用于在应用中进行页面间的导航和视图层级的变化。层级转场的场景可以划分为卡片、图表展开和列表展开:
图2 左右位移遮罩在列表展开场景下的用例
图3 一镜到底在单体卡片场景下的用例
对于层级转场,推荐使用系统转场,页面转场采用左右位移的运动方式,不应单帧直接切换或上下位移切换,曲线优先使用弹簧曲线。
搜索转场
搜索转场是指在用户执行搜索操作,如在搜索栏中输入关键词并按下搜索按钮,或者直接触摸搜索图标时,应用改变应用页面以显示搜索结果的过程。它包含了固定搜索区域和非固定搜索区域两种情况:
图4 淡入淡出在固定搜索区域场景下的用例
图5一镜到底在非固定搜索区域场景下的用例
对于搜索转场,推荐使用共享元素转场,搜索框作为持续存在的元素串联前后两个界面,其他元素可采用淡入淡出或者其他过渡方式,不应单帧切换或非共享元素的方式转场。
新建转场
新建转场是指用户创建新内容或实体时,应用页面发生的过渡效果,它可以让用户感知到新的事物的添加或创建,并提供一种连贯和引人注目的视觉切换。由于新建页面中需要完成整个页面的替换,推荐开发者使用左右位移遮罩作为转场动效,如下图所示。
图6 左右位移遮罩在新建文本场景下的用例
对于新建转场,推荐使用系统转场,页面转场采用左右位移的运动方式,不应单帧直接切换或上下位移切换,曲线优先使用弹簧曲线。
编辑转场
用户对现有内容或实体进行编辑时,例如点击“编辑”按钮,选择要编辑的项目或内容,或者执行其他与编辑相关的动作,应用应提供动效引导用户进入一个用于编辑现有内容的页面,修改所需的信息。在这个场景下,开发者需要达成的视觉效果是从编辑按键处弹出编辑页面,类似于单体卡片展开的效果。但由于一般的编辑按键并没有分明的外框,并不适用一镜到底的动效,此时淡入淡出能够提供类似于一镜到底的效果,如下图所示。
图7 淡入淡出在编辑联系人信息场景下的用例
对于编辑转场,推荐使用系统转场,页面转场采用淡入淡出的过渡方式,不应单帧直接切换或位移切换。
通用转场
通用转场是一种广泛适用于不同情境和应用类型的页面过渡效果,目的是提供一种通用的、可重复使用的方式,以改善用户页面之间的切换,增强用户体验。其关键点在于要适用各种应用情境,包括不同类型的应用(例如社交媒体、电子商务、新闻等)和不同操作(例如导航、搜索、编辑等)。这就需要一种通用的、不需要复杂操作的动效来完成跳转任务,而缩放能够满足绝大多数用户的需求和视觉体验感受,如下图所示。
图8 缩放在单体卡片场景下的用例
跨应用转场
跨应用转场是指用户从一个应用程序切换到另一个应用程序,用户能够无缝地从一个应用切换到另一个应用,而不会感到中断或不适。和以上几类转场都不同的是,用户点击应用内的链接、按钮或执行其他与外部应用交互的动作后,页面的跳转已经不仅仅存在于页面与页面之间,而是应用与应用之间,为此,推荐开发者使用专为此设计的左右间隔位移动效,跳转效果如下图所示。
图9左右间隔位移在跨应用跳转场景下的用例
转场是由交互行为引起的界面变化,分析界面元素在过程中的意义,定义其在转场中所在的类型,并将它们进行分类,元素所属的类别会影响它们使用怎样的转场能力,同时也将决定用什么类型的曲线和时长。
图10分析元素示例
如上图中示例,①是进场元素,②是出场元素,③是持续元素,④是静⽌元素。
接下来,开发者需要根据分析的元素类型选择合适的转场能力,并综合考虑元素和页面的整体感官效果。不同的元素类型可能需要不同的转场方式来展现其特定的特征和交互效果。
开发人员接收到设计需求后,需要选择合适的转场能力完成该设计。HarmonyOS为开发者提供了UIAbility转场、页面路由和组件转场三种方式,在选择转场方式时,开发者需要考虑用户体验、界面一致性和业务需求,确保所选导航组件能够提供直观、易用的导航方式,帮助应用实现更好的转场效果。
转场动画是指对将要出现或消失的组件做动画,对始终出现的组件做动画应使用属性动画。转场动画主要为了让开发者从繁重的消失节点管理中解放出来,如果用属性动画做组件转场,开发者需要在动画结束回调中删除组件节点。同时,由于动画结束前已经删除的组件节点可能会重新出现,还需要在结束回调中增加对节点状态的判断。
转场动画分为基础转场和高级模板化转场,出现/消失转场是一种基础转场,是对新增、消失的控件实现动画效果的能力。为了简化开发者工作,HarmonyOS提供了以下高级模板,将属性动画和出现消失动画封装,开发者只需调用接口,可以轻松的完成页面转场:
说明
在实现组件出现和消失的动画效果时,相比于组件动画(animateTo),推荐优先使用transition。因为animateTo需要在动画前后做两次属性更新,而transition只需做一次条件改变更新,性能更好。此外,使用transition可以避免在结束回调中做复杂逻辑处理,开发实现更容易。
场景描述
在日常的各类应用交互场景中,搜索转场是极为常见的页面转场。通过点击当前页面的搜索栏会跳转进入搜索输入页面,详细效果如下所示。
图11 共享元素转场实现搜索转场
实现原理
在本案例中,搜索框会在转场中持续存在,且在转场前后有位置上的变化,可以使用共享元素转场让搜索框在转场过程中进行丝滑的上下文过渡。其实现步骤如下所示。
开发步骤
在转场前页面的搜索组件上设置geometryTransition属性,并设置显示动画animateTo,其中curve为动画曲线。
@Entry
@Component
struct SearchLongTakeTransitionPageOne {
@State translateY: number = 0;
@State transitionEffect: TransitionEffect = TransitionEffect.IDENTITY;
private pageInfos: NavPathStack = new NavPathStack();
private showSearchPage(): void {
this.transitionEffect = TransitionEffect.OPACITY;
animateTo({
curve: curves.interpolatingSpring(0, 1, 342, 38)
}, () => {
this.pageInfos.pushPath({ name: 'SearchLongTakeTransitionPageTwo' }, false);
})
}
build() {
NavDestination() {
Column({ space: 20 }) {
Search({ placeholder: 'Search' })
.height(40)
.placeholderColor($r('sys.color.mask_secondary'))
.width('100%')
.geometryTransition('SEARCH_ONE_SHOT_DEMO_TRANSITION_ID', { follow: true })
.backgroundColor('#0D000000')
.defaultFocus(false)
.focusOnTouch(false)
.focusable(false)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Up) {
this.showSearchPage();
}
})
}
.size({
width: '90%',
height: '100%'
})
}
.transition(TransitionEffect.OPACITY)
.backgroundColor('#F1F3F5')
.title(getResourceString($r('app.string.search_title'), this))
.onReady((context: NavDestinationContext) => {
this.pageInfos = context.pathStack;
})
.onBackPressed(() => {
this.transitionEffect = TransitionEffect.IDENTITY;
this.pageInfos.pop(true);
return true;
})
}
}
在转场后的搜索组件上绑定唯一ID。
Search({ placeholder: 'DevEco Studio' })
.height(40)
.placeholderColor($r('sys.color.mask_secondary'))
.width('85%')
.backgroundColor('#0D000000')
.height(40)
.geometryTransition('SEARCH_ONE_SHOT_DEMO_TRANSITION_ID')
.transition(TransitionEffect.opacity(0.99))
.defaultFocus(true)
.focusOnTouch(true)
场景描述
如图所示,在进入第一个页面时为半模态转场,通过半模态展现多种登录的方式。点击进入第二个页面时为全模态转场,展示了手机验证码登录页面。
图12 模态转场实现通用转场
实现原理
在半模态转场和全模态转场中,两者实现的步骤基本相同,具体调用的接口有差异,详细实现步骤如下所示。
开发步骤
@Builder
halfModalLogin() {
// semi-modal window page
Column() {
Text($r('app.string.multimodaltransion_after_login_more_service'))
.fontColor(Color.Black)
.fontSize($r('app.integer.font_size_normal'))
.padding({ top: $r('app.integer.padding_top_large') })
Text($r('app.string.multimodaltransion_user_phone_number'))
.fontColor(Color.Black)
.fontSize($r('app.integer.font_size_large'))
.fontWeight(Constants.FONT_WEIGHT_SM)
.padding({
top: $r('app.integer.font_size_large'),
bottom: $r('app.integer.multimodaltransion_margin_default')
})
Text($r('app.string.multimodaltransion_get_service'))
.fontColor($r('app.color.multimodaltransion_grey_9'))
.fontSize($r('app.integer.multimodaltransion_row_text_font_size'))
.padding({ bottom: $r('app.integer.multimodaltransion_height_fifty') })
Button($r('app.string.multimodaltransion_phone_start_login'))
.fontColor(Color.White)
.type(ButtonType.Normal)
.backgroundColor($r('app.color.multimodaltransion_red'))
.onClick(() => {
if (this.isConfirmed) {
promptAction.showToast({ message: $r('app.string.multimodaltransion_login_success') });
AppStorage.set('login', true);
this.pageInfos.pop();
} else {
promptAction.showToast({ message: $r('app.string.multimodaltransion_please_read_and_agree') });
}
})
.width($r('app.string.multimodaltransion_size_ninety_percent'))
.height($r('app.integer.multimodaltransion_height_fifty'))
.margin({
left: $r('app.integer.main_page_padding2'),
right: $r('app.integer.main_page_padding2'),
bottom: $r('app.integer.multimodaltransion_row_padding_bottom')
})
Button($r('app.string.multimodaltransion_captcha_login_text'))
.fontColor(Color.Black)
.borderRadius($r('app.integer.multimodaltransion_border_radius'))
.type(ButtonType.Normal)
.backgroundColor($r('app.color.multimodaltransion_btn_bgc'))
.border({
color: $r('app.color.multimodaltransion_half_modal_btn_bgc'),
width: Constants.DEFAULT_ONE
})
.onClick(() => {
if (this.isConfirmed) {
this.isPresentInLoginView = true;
this.isConfirmed = false;
this.isShowTransition = false;
} else {
promptAction.showToast({ message: $r('app.string.multimodaltransion_please_read_and_agree') });
}
})
.width($r('app.string.multimodaltransion_size_ninety_percent'))
.height($r('app.integer.multimodaltransion_height_fifty'))
.margin({ bottom: $r('app.integer.font_size_large') })
Blank()
Row() {
Checkbox({ name: Constants.CHECK_BOX_NAME1 })
.select(this.isConfirmed)
.width($r('app.integer.font_size_sm'))
.onChange((value: boolean) => {
this.isConfirmed = value;
})
Text() {
Span($r('app.string.multimodaltransion_read_and_agree'))
.fontColor($r('app.color.multimodaltransion_grey_9'))
Span($r('app.string.multimodaltransion_server_proxy_rule_detail'))
.fontColor($r('app.color.multimodaltransion_note_color'))
.onClick(() => {
promptAction.showToast({ message: $r('app.string.multimodaltransion_only_show_ui') });
})
}.fontSize($r('app.integer.font_size_sm'))
}
.margin({ left: $r('app.integer.multimodaltransion_other_ways_icon_height') })
.width($r('app.string.multimodaltransion_size_full'))
}
}
build() {
NavDestination() {
Column() {
//The Text component is bound for semi-modal display
Text()
.bindSheet($$this.isPresent, this.halfModalLogin(), {
height: this.sheetHeight,
dragBar: this.showDragBar,
preferType: this.isCenter ? SheetType.CENTER : SheetType.POPUP,
backgroundColor: $r('app.color.multimodaltransion_btn_bgc'),
showClose: true,
shouldDismiss: ((sheetDismiss: SheetDismiss) => {
sheetDismiss.dismiss();
this.pageInfos.pop();
})
})
// ...
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.justifyContent(FlexAlign.Center)
.size({
width: $r('app.string.multimodaltransion_size_full'),
height: $r('app.string.multimodaltransion_size_full')
})
.padding($r('app.integer.multimodaltransion_padding_default'))
}
}
@Builder
defaultLogin() {
Column() {
// CheckBox to control, semi-modal, full-modal, and semi-modal confirmations in the login page
CaptchaLogin({
isPresent: $isPresent,
isPresentInLoginView: $isPresentInLoginView,
isShowTransition: $isShowTransition
})
}
}
Text()
.bindContentCover($$this.isPresentInLoginView, this.defaultLogin())
合理使用页面间转场是提升用户体验的重要技术之一,在应用开发过程中,通过动效的运用,可以使应用界面更加生动、流畅,并且能够引导用户的注意力,提高用户的操作效率。合理使用动效需要考虑以下几点:
在应用开发过程中,开发者可以借助HarmonyOS中提供的导航组件和转场动效,简化开发流程,提高开发效率,实现符合规范要求的转场动效效果。