共享元素转场是一种界面切换时对相同或者相似的两个元素做的一种位置和大小匹配的过渡动画效果,也称一镜到底动效。
如下例所示,在点击图片后,该图片消失,同时在另一个位置出现新的图片,二者之间内容相同,可以对它们添加一镜到底动效。左图为不添加一镜到底动效的效果,右图为添加一镜到底动效的效果,一镜到底的效果能够让二者的出现消失产生联动,使得内容切换过程显得灵动自然而不生硬。
实现一镜到底动效的方式有多种,实际开发过程中,需要依据具体的场景,选择相应的合适的方式进行实现。以下为基本的实现方式介绍。
使用geometryTransition共享元素转场实现一镜到底动效
geometryTransition 用于组件内隐式共享元素转场,在视图状态切换过程中提供丝滑的上下文继承过渡体验。
geometryTransition的使用方式为对需要添加一镜到底动效的两个组件使用geometryTransition接口绑定同一id,这样在其中一个组件消失同时另一个组件创建出现的时候,系统会对二者添加一镜到底动效。
geometryTransition的简单使用
对于同一个页面中的两个元素的一镜到底效果,geometryTransition接口的简单使用示例如下:
import curves from '@ohos.curves';
@Entry
@Component
struct IfElseGeometryTransition {
@State isShow: boolean = false;
build() {
Stack({ alignContent: Alignment.Center }) {
if (this.isShow) {
Image($r('app.media.spring'))
.autoResize(false)
.clip(true)
.width(200)
.height(200)
.borderRadius(100)
.geometryTransition("picture")
.transition(TransitionEffect.OPACITY)
// 在打断场景下,即动画过程中点击页面触发下一次转场,如果不加id,则会出现重影
// 加了id之后,新建的spring图片会复用之前的spring图片节点,不会重新创建节点,也就不会有重影问题
// 加id的规则为加在if和else下的第一个节点上,有多个并列节点则也需要进行添加
.id('item1')
} else {
// geometryTransition此处绑定的是容器,那么容器内的子组件需设为相对布局跟随父容器变化,
// 套多层容器为了说明相对布局约束传递
Column() {
Column() {
Image($r('app.media.sky'))
.size({ width: '100%', height: '100%' })
}
.size({ width: '100%', height: '100%' })
}
.width(100)
.height(100)
// geometryTransition会同步圆角,但仅限于geometryTransition绑定处,此处绑定的是容器
// 则对容器本身有圆角同步而不会操作容器内部子组件的borderRadius
.borderRadius(50)
.clip(true)
.geometryTransition("picture")
// transition保证节点离场不被立即析构,设置通用转场效果
.transition(TransitionEffect.OPACITY)
.position({ x: 40, y: 40 })
.id('item2')
}
}
.onClick(() => {
animateTo({
curve: curves.springMotion()
}, () => {
this.isShow = !this.isShow;
})
})
.size({ width: '100%', height: '100%' })
}
}
geometryTransition结合模态转场使用
更多的场景中,需要对一个页面的元素与另一个页面的元素添加一镜到底动效。可以通过geometryTransition搭配模态转场接口实现。以点击头像弹出个人信息页的demo为例:
class PostData {
avatar: Resource = $r('app.media.flower');
name: string = '';
message: string = '';
images: Resource[] = [];
}
@Entry
@Component
struct Index {
@State isPersonalPageShow: boolean = false;
@State selectedIndex: number = 0;
@State alphaValue: number = 1;
private allPostData: PostData[] = [
{ avatar: $r('app.media.flower'), name: 'Alice', message: '天气晴朗',
images: [$r('app.media.spring'), $r('app.media.tree')] },
{ avatar: $r('app.media.sky'), name: 'Bob', message: '你好世界',
images: [$r('app.media.island')] },
{ avatar: $r('app.media.tree'), name: 'Carl', message: '万物生长',
images: [$r('app.media.flower'), $r('app.media.sky'), $r('app.media.spring')] }];
private onAvatarClicked(index: number): void {
this.selectedIndex = index;
animateTo({
duration: 350,
curve: Curve.Friction
}, () => {
this.isPersonalPageShow = !this.isPersonalPageShow;
this.alphaValue = 0;
});
}
private onPersonalPageBack(index: number): void {
animateTo({
duration: 350,
curve: Curve.Friction
}, () => {
this.isPersonalPageShow = !this.isPersonalPageShow;
this.alphaValue = 1;
});
}
@Builder
PersonalPageBuilder(index: number) {
Column({ space: 20 }) {
Image(this.allPostData[index].avatar)
.size({ width: 200, height: 200 })
.borderRadius(100)
// 头像配置共享元素效果,与点击的头像的id匹配
.geometryTransition(index.toString())
.clip(true)
.transition(TransitionEffect.opacity(0.99))
Text(this.allPostData[index].name)
.font({ size: 30, weight: 600 })
// 对文本添加出现转场效果
.transition(TransitionEffect.asymmetric(
TransitionEffect.OPACITY
.combine(TransitionEffect.translate({ y: 100 })),
TransitionEffect.OPACITY.animation({ duration: 0 })
))
Text('你好,我是' + this.allPostData[index].name)
// 对文本添加出现转场效果
.transition(TransitionEffect.asymmetric(
TransitionEffect.OPACITY
.combine(TransitionEffect.translate({ y: 100 })),
TransitionEffect.OPACITY.animation({ duration: 0 })
))
}
.padding({ top: 20 })
.size({ width: 360, height: 780 })
.backgroundColor(Color.White)
.onClick(() => {
this.onPersonalPageBack(index);
})
.transition(TransitionEffect.asymmetric(
TransitionEffect.opacity(0.99),
TransitionEffect.OPACITY
))
}
build() {
Column({ space: 20 }) {
ForEach(this.allPostData, (postData: PostData, index: number) => {
Column() {
Post({ data: postData, index: index, onAvatarClicked: (index: number) => { this.onAvatarClicked(index) } })
}
.width('100%')
}, (postData: PostData, index: number) => index.toString())
}
.size({ width: '100%', heig