1e41f4b71Sopenharmony_ci# Shared Element Transition 2e41f4b71Sopenharmony_ci 3e41f4b71Sopenharmony_ciShared element transition is a type of transition achieved by animating the size and position between styles of the same or similar elements during page switching. 4e41f4b71Sopenharmony_ci 5e41f4b71Sopenharmony_ciLet's look at an example. After an image is clicked, it disappears, and a new image appears in another position. Because the two images have the same content, we can add shared element transition to them. The figures below show the results with and without a shared element transition. Clearly, the presence of the shared element transition renders the transition natural and smooth. 6e41f4b71Sopenharmony_ci 7e41f4b71Sopenharmony_ci| 8e41f4b71Sopenharmony_ci---|--- 9e41f4b71Sopenharmony_ci 10e41f4b71Sopenharmony_ciThere are multiple methods to implement shared element transition. Choose one that is appropriate to your use case. The following outlines the basic implementation methods, ranked from most to least recommended. 11e41f4b71Sopenharmony_ci 12e41f4b71Sopenharmony_ci## Directly Changing the Original Container Without Creating New Containers 13e41f4b71Sopenharmony_ci 14e41f4b71Sopenharmony_ciThis method does not create new containers. Instead, it triggers [transition](../reference/apis-arkui/arkui-ts/ts-transition-animation-component.md) by adding or removing components on an existing container and pairs it with the [property animation](./arkts-attribute-animation-apis.md) of components. 15e41f4b71Sopenharmony_ci 16e41f4b71Sopenharmony_ciThis example implements a shared element transition for the scenario where, as a component is expanded, sibling components in the same container disappear or appear. Specifically, property animations are applied to width and height changes of a component before and after the expansion; enter/exit animations are applied to the sibling components as they disappear or disappear. The basic procedure is as follows: 17e41f4b71Sopenharmony_ci 18e41f4b71Sopenharmony_ci1. Build the component to be expanded, and build two pages for it through state variables: one for the normal state and one for the expanded state. 19e41f4b71Sopenharmony_ci 20e41f4b71Sopenharmony_ci ```ts 21e41f4b71Sopenharmony_ci class Tmp { 22e41f4b71Sopenharmony_ci set(item: PostData): PostData { 23e41f4b71Sopenharmony_ci return item 24e41f4b71Sopenharmony_ci } 25e41f4b71Sopenharmony_ci } 26e41f4b71Sopenharmony_ci // Build two pages for the normal and expanded states of the same component, which are then used based on the declared state variables. 27e41f4b71Sopenharmony_ci @Component 28e41f4b71Sopenharmony_ci export struct MyExtendView { 29e41f4b71Sopenharmony_ci // Declare the isExpand variable to be synced with the parent component. 30e41f4b71Sopenharmony_ci @Link isExpand: boolean; 31e41f4b71Sopenharmony_ci // You need to implement the list data. 32e41f4b71Sopenharmony_ci @State cardList: Array<PostData> = xxxx; 33e41f4b71Sopenharmony_ci 34e41f4b71Sopenharmony_ci build() { 35e41f4b71Sopenharmony_ci List() { 36e41f4b71Sopenharmony_ci // Customize the expanded component as required. 37e41f4b71Sopenharmony_ci if (this.isExpand) { 38e41f4b71Sopenharmony_ci Text('expand') 39e41f4b71Sopenharmony_ci .transition(TransitionEffect.translate({y:300}).animation({ curve: curves.springMotion(0.6, 0.8) })) 40e41f4b71Sopenharmony_ci } 41e41f4b71Sopenharmony_ci 42e41f4b71Sopenharmony_ci ForEach(this.cardList, (item: PostData) => { 43e41f4b71Sopenharmony_ci let Item: Tmp = new Tmp() 44e41f4b71Sopenharmony_ci let Imp: Tmp = Item.set(item) 45e41f4b71Sopenharmony_ci let Mc: Record<string, Tmp> = {'cardData': Imp} 46e41f4b71Sopenharmony_ci MyCard(Mc) // Encapsulated widget, which needs to be implemented by yourself. 47e41f4b71Sopenharmony_ci }) 48e41f4b71Sopenharmony_ci } 49e41f4b71Sopenharmony_ci .width(this.isExpand? 200:500) // Define the attributes of the expanded component as required. 50e41f4b71Sopenharmony_ci .animation({ curve: curves.springMotion()}) // Bind an animation to component attributes. 51e41f4b71Sopenharmony_ci } 52e41f4b71Sopenharmony_ci } 53e41f4b71Sopenharmony_ci ... 54e41f4b71Sopenharmony_ci ``` 55e41f4b71Sopenharmony_ci 56e41f4b71Sopenharmony_ci2. Expand the component to be expanded. Use state variables to control the disappearance or appearance of sibling components, and apply the enter/exit transition to the disappearance and appearance. 57e41f4b71Sopenharmony_ci 58e41f4b71Sopenharmony_ci ```ts 59e41f4b71Sopenharmony_ci class Tmp{ 60e41f4b71Sopenharmony_ci isExpand: boolean = false; 61e41f4b71Sopenharmony_ci set(){ 62e41f4b71Sopenharmony_ci this.isExpand = !this.isExpand; 63e41f4b71Sopenharmony_ci } 64e41f4b71Sopenharmony_ci } 65e41f4b71Sopenharmony_ci let Exp:Record<string,boolean> = {'isExpand': false} 66e41f4b71Sopenharmony_ci @State isExpand: boolean = false 67e41f4b71Sopenharmony_ci 68e41f4b71Sopenharmony_ci ... 69e41f4b71Sopenharmony_ci List() { 70e41f4b71Sopenharmony_ci // Control the appearance or disappearance of sibling components through the isExpand variable, and configure the enter/exit transition. 71e41f4b71Sopenharmony_ci if (!this.isExpand) { 72e41f4b71Sopenharmony_ci Text ('Collapse') 73e41f4b71Sopenharmony_ci .transition(TransitionEffect.translate({y:300}).animation({ curve: curves.springMotion(0.6, 0.9) })) 74e41f4b71Sopenharmony_ci } 75e41f4b71Sopenharmony_ci 76e41f4b71Sopenharmony_ci MyExtendView(Exp) 77e41f4b71Sopenharmony_ci .onClick(() => { 78e41f4b71Sopenharmony_ci let Epd:Tmp = new Tmp() 79e41f4b71Sopenharmony_ci Epd.set() 80e41f4b71Sopenharmony_ci }) 81e41f4b71Sopenharmony_ci 82e41f4b71Sopenharmony_ci // Control the appearance or disappearance of sibling components through the isExpand variable, and configure the enter/exit transition. 83e41f4b71Sopenharmony_ci if (this.isExpand) { 84e41f4b71Sopenharmony_ci Text ('Expand') 85e41f4b71Sopenharmony_ci .transition(TransitionEffect.translate({y:300}).animation({ curve: curves.springMotion() })) 86e41f4b71Sopenharmony_ci } 87e41f4b71Sopenharmony_ci } 88e41f4b71Sopenharmony_ci ... 89e41f4b71Sopenharmony_ci ``` 90e41f4b71Sopenharmony_ci 91e41f4b71Sopenharmony_ciBelow is the complete sample code and effect. 92e41f4b71Sopenharmony_ci 93e41f4b71Sopenharmony_ci```ts 94e41f4b71Sopenharmony_ciclass PostData { 95e41f4b71Sopenharmony_ci avatar: Resource = $r('app.media.flower'); 96e41f4b71Sopenharmony_ci name: string = ''; 97e41f4b71Sopenharmony_ci message: string = ''; 98e41f4b71Sopenharmony_ci images: Resource[] = []; 99e41f4b71Sopenharmony_ci} 100e41f4b71Sopenharmony_ci 101e41f4b71Sopenharmony_ci@Entry 102e41f4b71Sopenharmony_ci@Component 103e41f4b71Sopenharmony_cistruct Index { 104e41f4b71Sopenharmony_ci @State isExpand: boolean = false; 105e41f4b71Sopenharmony_ci @State @Watch('onItemClicked') selectedIndex: number = -1; 106e41f4b71Sopenharmony_ci 107e41f4b71Sopenharmony_ci private allPostData: PostData[] = [ 108e41f4b71Sopenharmony_ci { avatar: $r('app.media.flower'), name: 'Alice', message: 'It's sunny.', 109e41f4b71Sopenharmony_ci images: [$r('app.media.spring'), $r('app.media.tree')] }, 110e41f4b71Sopenharmony_ci { avatar: $r('app.media.sky'), name: 'Bob', message: 'Hello World', 111e41f4b71Sopenharmony_ci images: [$r('app.media.island')] }, 112e41f4b71Sopenharmony_ci { avatar: $r('app.media.tree'), name: 'Carl', message: 'Everything grows.', 113e41f4b71Sopenharmony_ci images: [$r('app.media.flower'), $r('app.media.sky'), $r('app.media.spring')] }]; 114e41f4b71Sopenharmony_ci 115e41f4b71Sopenharmony_ci private onItemClicked(): void { 116e41f4b71Sopenharmony_ci if (this.selectedIndex < 0) { 117e41f4b71Sopenharmony_ci return; 118e41f4b71Sopenharmony_ci } 119e41f4b71Sopenharmony_ci animateTo({ 120e41f4b71Sopenharmony_ci duration: 350, 121e41f4b71Sopenharmony_ci curve: Curve.Friction 122e41f4b71Sopenharmony_ci }, () => { 123e41f4b71Sopenharmony_ci this.isExpand = !this.isExpand; 124e41f4b71Sopenharmony_ci }); 125e41f4b71Sopenharmony_ci } 126e41f4b71Sopenharmony_ci 127e41f4b71Sopenharmony_ci build() { 128e41f4b71Sopenharmony_ci Column({ space: 20 }) { 129e41f4b71Sopenharmony_ci ForEach(this.allPostData, (postData: PostData, index: number) => { 130e41f4b71Sopenharmony_ci // When a post is clicked, other posts disappear from the tree. 131e41f4b71Sopenharmony_ci if (!this.isExpand || this.selectedIndex === index) { 132e41f4b71Sopenharmony_ci Column() { 133e41f4b71Sopenharmony_ci Post({ data: postData, selecteIndex: this.selectedIndex, index: index }) 134e41f4b71Sopenharmony_ci } 135e41f4b71Sopenharmony_ci .width('100%') 136e41f4b71Sopenharmony_ci // Apply opacity and translate transition effects to the disappearing posts. 137e41f4b71Sopenharmony_ci .transition(TransitionEffect.OPACITY 138e41f4b71Sopenharmony_ci .combine(TransitionEffect.translate({ y: index < this.selectedIndex ? -250 : 250 })) 139e41f4b71Sopenharmony_ci .animation({ duration: 350, curve: Curve.Friction})) 140e41f4b71Sopenharmony_ci } 141e41f4b71Sopenharmony_ci }, (postData: PostData, index: number) => index.toString()) 142e41f4b71Sopenharmony_ci } 143e41f4b71Sopenharmony_ci .size({ width: '100%', height: '100%' }) 144e41f4b71Sopenharmony_ci .backgroundColor('#40808080') 145e41f4b71Sopenharmony_ci } 146e41f4b71Sopenharmony_ci} 147e41f4b71Sopenharmony_ci 148e41f4b71Sopenharmony_ci@Component 149e41f4b71Sopenharmony_ciexport default struct Post { 150e41f4b71Sopenharmony_ci @Link selecteIndex: number; 151e41f4b71Sopenharmony_ci 152e41f4b71Sopenharmony_ci @Prop data: PostData; 153e41f4b71Sopenharmony_ci @Prop index: number; 154e41f4b71Sopenharmony_ci 155e41f4b71Sopenharmony_ci @State itemHeight: number = 250; 156e41f4b71Sopenharmony_ci @State isExpand: boolean = false; 157e41f4b71Sopenharmony_ci @State expandImageSize: number = 100; 158e41f4b71Sopenharmony_ci @State avatarSize: number = 50; 159e41f4b71Sopenharmony_ci 160e41f4b71Sopenharmony_ci build() { 161e41f4b71Sopenharmony_ci Column({ space: 20 }) { 162e41f4b71Sopenharmony_ci Row({ space: 10 }) { 163e41f4b71Sopenharmony_ci Image(this.data.avatar) 164e41f4b71Sopenharmony_ci .size({ width: this.avatarSize, height: this.avatarSize }) 165e41f4b71Sopenharmony_ci .borderRadius(this.avatarSize / 2) 166e41f4b71Sopenharmony_ci .clip(true) 167e41f4b71Sopenharmony_ci 168e41f4b71Sopenharmony_ci Text(this.data.name) 169e41f4b71Sopenharmony_ci } 170e41f4b71Sopenharmony_ci .justifyContent(FlexAlign.Start) 171e41f4b71Sopenharmony_ci 172e41f4b71Sopenharmony_ci Text(this.data.message) 173e41f4b71Sopenharmony_ci 174e41f4b71Sopenharmony_ci Row({ space: 15 }) { 175e41f4b71Sopenharmony_ci ForEach(this.data.images, (imageResource: Resource, index: number) => { 176e41f4b71Sopenharmony_ci Image(imageResource) 177e41f4b71Sopenharmony_ci .size({ width: this.expandImageSize, height: this.expandImageSize }) 178e41f4b71Sopenharmony_ci }, (imageResource: Resource, index: number) => index.toString()) 179e41f4b71Sopenharmony_ci } 180e41f4b71Sopenharmony_ci 181e41f4b71Sopenharmony_ci if (this.isExpand) { 182e41f4b71Sopenharmony_ci Column() { 183e41f4b71Sopenharmony_ci Text('Comments') 184e41f4b71Sopenharmony_ci // Apply enter/exit transition effects to the text in the comments area. 185e41f4b71Sopenharmony_ci .transition( TransitionEffect.OPACITY 186e41f4b71Sopenharmony_ci .animation({ duration: 350, curve: Curve.Friction })) 187e41f4b71Sopenharmony_ci .padding({ top: 10 }) 188e41f4b71Sopenharmony_ci } 189e41f4b71Sopenharmony_ci .transition(TransitionEffect.asymmetric( 190e41f4b71Sopenharmony_ci TransitionEffect.opacity(0.99) 191e41f4b71Sopenharmony_ci .animation({ duration: 350, curve: Curve.Friction }), 192e41f4b71Sopenharmony_ci TransitionEffect.OPACITY.animation({ duration: 0 }) 193e41f4b71Sopenharmony_ci )) 194e41f4b71Sopenharmony_ci .size({ width: '100%'}) 195e41f4b71Sopenharmony_ci } 196e41f4b71Sopenharmony_ci } 197e41f4b71Sopenharmony_ci .backgroundColor(Color.White) 198e41f4b71Sopenharmony_ci .size({ width: '100%', height: this.itemHeight }) 199e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Start) 200e41f4b71Sopenharmony_ci .padding({ left: 10, top: 10 }) 201e41f4b71Sopenharmony_ci .onClick(() => { 202e41f4b71Sopenharmony_ci this.selecteIndex = -1; 203e41f4b71Sopenharmony_ci this.selecteIndex = this.index; 204e41f4b71Sopenharmony_ci animateTo({ 205e41f4b71Sopenharmony_ci duration: 350, 206e41f4b71Sopenharmony_ci curve: Curve.Friction 207e41f4b71Sopenharmony_ci }, () => { 208e41f4b71Sopenharmony_ci // Animate the width and height of the expanded post, and apply animations to the profile picture and image sizes. 209e41f4b71Sopenharmony_ci this.isExpand = !this.isExpand; 210e41f4b71Sopenharmony_ci this.itemHeight = this.isExpand ? 780 : 250; 211e41f4b71Sopenharmony_ci this.avatarSize = this.isExpand ? 75: 50; 212e41f4b71Sopenharmony_ci this.expandImageSize = (this.isExpand && this.data.images.length > 0) 213e41f4b71Sopenharmony_ci ? (360 - (this.data.images.length + 1) * 15) / this.data.images.length : 100; 214e41f4b71Sopenharmony_ci }) 215e41f4b71Sopenharmony_ci }) 216e41f4b71Sopenharmony_ci } 217e41f4b71Sopenharmony_ci} 218e41f4b71Sopenharmony_ci``` 219e41f4b71Sopenharmony_ci 220e41f4b71Sopenharmony_ci 221e41f4b71Sopenharmony_ci 222e41f4b71Sopenharmony_ci## Creating a Container and Migrating Components Across Containers 223e41f4b71Sopenharmony_ci 224e41f4b71Sopenharmony_ciUse [NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md) and [custom placeholder nodes](arkts-user-defined-place-hoder.md) with [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md) for migrating components across different nodes. Then combine the migration with the property animations to achieve shared element transition. This method can be integrated with various transition styles, including navigation transitions ([Navigation](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md)) and sheet transitions ([bindSheet](../reference/apis-arkui/arkui-ts/ts-universal-attributes-sheet-transition.md#bindsheet)). 225e41f4b71Sopenharmony_ci 226e41f4b71Sopenharmony_ci### Using with Stack 227e41f4b71Sopenharmony_ci 228e41f4b71Sopenharmony_ciWith the **Stack** container, where later defined components appear on top, you can control the z-order to ensure that the component is on top after being migrated across nodes. For example, in the scenario of expanding and collapsing widgets, the implementation steps are as follows: 229e41f4b71Sopenharmony_ci 230e41f4b71Sopenharmony_ci- When expanding a widget, obtain the source node (node A)'s position and migrate the components to a higher-level node (node B) with the same position. 231e41f4b71Sopenharmony_ci 232e41f4b71Sopenharmony_ci- Add a property animation to node B to make it expand and move to the expanded position, creating a shared element transition. 233e41f4b71Sopenharmony_ci 234e41f4b71Sopenharmony_ci- When collapsing the widget, add a property animation to node B to make it collapse and move back to the position of node A, creating a shared element transition. 235e41f4b71Sopenharmony_ci 236e41f4b71Sopenharmony_ci- At the end of the animation, use a callback to migrate the components from node B back to node A. 237e41f4b71Sopenharmony_ci 238e41f4b71Sopenharmony_ci```ts 239e41f4b71Sopenharmony_ci// Index.ets 240e41f4b71Sopenharmony_ciimport { createPostNode, getPostNode, PostNode } from "../PostNode" 241e41f4b71Sopenharmony_ciimport { componentUtils, curves } from '@kit.ArkUI'; 242e41f4b71Sopenharmony_ci 243e41f4b71Sopenharmony_ci@Entry 244e41f4b71Sopenharmony_ci@Component 245e41f4b71Sopenharmony_cistruct Index { 246e41f4b71Sopenharmony_ci // Create an animation class. 247e41f4b71Sopenharmony_ci @State AnimationProperties: AnimationProperties = new AnimationProperties(); 248e41f4b71Sopenharmony_ci private listArray: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8 ,9, 10]; 249e41f4b71Sopenharmony_ci 250e41f4b71Sopenharmony_ci build() { 251e41f4b71Sopenharmony_ci // Common parent component for widget collapsed and expanded states 252e41f4b71Sopenharmony_ci Stack() { 253e41f4b71Sopenharmony_ci List({space: 20}) { 254e41f4b71Sopenharmony_ci ForEach(this.listArray, (item: number) => { 255e41f4b71Sopenharmony_ci ListItem() { 256e41f4b71Sopenharmony_ci // Widget collapsed state 257e41f4b71Sopenharmony_ci PostItem({ index: item, AnimationProperties: this.AnimationProperties }) 258e41f4b71Sopenharmony_ci } 259e41f4b71Sopenharmony_ci }) 260e41f4b71Sopenharmony_ci } 261e41f4b71Sopenharmony_ci .clip(false) 262e41f4b71Sopenharmony_ci .alignListItem(ListItemAlign.Center) 263e41f4b71Sopenharmony_ci if (this.AnimationProperties.isExpandPageShow) { 264e41f4b71Sopenharmony_ci // Widget expanded state 265e41f4b71Sopenharmony_ci ExpandPage({ AnimationProperties: this.AnimationProperties }) 266e41f4b71Sopenharmony_ci } 267e41f4b71Sopenharmony_ci } 268e41f4b71Sopenharmony_ci .key('rootStack') 269e41f4b71Sopenharmony_ci .enabled(this.AnimationProperties.isEnabled) 270e41f4b71Sopenharmony_ci } 271e41f4b71Sopenharmony_ci} 272e41f4b71Sopenharmony_ci 273e41f4b71Sopenharmony_ci@Component 274e41f4b71Sopenharmony_cistruct PostItem { 275e41f4b71Sopenharmony_ci @Prop index: number 276e41f4b71Sopenharmony_ci @Link AnimationProperties: AnimationProperties; 277e41f4b71Sopenharmony_ci @State nodeController: PostNode | undefined = undefined; 278e41f4b71Sopenharmony_ci // Hide detailed content when the widget is collapsed. 279e41f4b71Sopenharmony_ci private showDetailContent: boolean = false; 280e41f4b71Sopenharmony_ci 281e41f4b71Sopenharmony_ci aboutToAppear(): void { 282e41f4b71Sopenharmony_ci this.nodeController = createPostNode(this.getUIContext(), this.index.toString(), this.showDetailContent); 283e41f4b71Sopenharmony_ci if (this.nodeController != undefined) { 284e41f4b71Sopenharmony_ci // Set a callback to trigger when the widget returns from expanded to collapsed state. 285e41f4b71Sopenharmony_ci this.nodeController.setCallback(this.resetNode.bind(this)); 286e41f4b71Sopenharmony_ci } 287e41f4b71Sopenharmony_ci } 288e41f4b71Sopenharmony_ci resetNode() { 289e41f4b71Sopenharmony_ci this.nodeController = getPostNode(this.index.toString()); 290e41f4b71Sopenharmony_ci } 291e41f4b71Sopenharmony_ci 292e41f4b71Sopenharmony_ci build() { 293e41f4b71Sopenharmony_ci Stack() { 294e41f4b71Sopenharmony_ci NodeContainer(this.nodeController) 295e41f4b71Sopenharmony_ci } 296e41f4b71Sopenharmony_ci .width('100%') 297e41f4b71Sopenharmony_ci .height(100) 298e41f4b71Sopenharmony_ci .key(this.index.toString()) 299e41f4b71Sopenharmony_ci .onClick( ()=> { 300e41f4b71Sopenharmony_ci if (this.nodeController != undefined) { 301e41f4b71Sopenharmony_ci // The widget node is removed from the tree when collapsed. 302e41f4b71Sopenharmony_ci this.nodeController.onRemove(); 303e41f4b71Sopenharmony_ci } 304e41f4b71Sopenharmony_ci // Trigger the animation for changing from the folded state to the collapsed state. 305e41f4b71Sopenharmony_ci this.AnimationProperties.expandAnimation(this.index); 306e41f4b71Sopenharmony_ci }) 307e41f4b71Sopenharmony_ci } 308e41f4b71Sopenharmony_ci} 309e41f4b71Sopenharmony_ci 310e41f4b71Sopenharmony_ci@Component 311e41f4b71Sopenharmony_cistruct ExpandPage { 312e41f4b71Sopenharmony_ci @Link AnimationProperties: AnimationProperties; 313e41f4b71Sopenharmony_ci @State nodeController: PostNode | undefined = undefined; 314e41f4b71Sopenharmony_ci // Show detailed content when the widget is expanded. 315e41f4b71Sopenharmony_ci private showDetailContent: boolean = true; 316e41f4b71Sopenharmony_ci 317e41f4b71Sopenharmony_ci aboutToAppear(): void { 318e41f4b71Sopenharmony_ci // Obtain the corresponding widget component by index. 319e41f4b71Sopenharmony_ci this.nodeController = getPostNode(this.AnimationProperties.curIndex.toString()) 320e41f4b71Sopenharmony_ci // Update to show detailed content. 321e41f4b71Sopenharmony_ci this.nodeController?.update(this.AnimationProperties.curIndex.toString(), this.showDetailContent) 322e41f4b71Sopenharmony_ci } 323e41f4b71Sopenharmony_ci 324e41f4b71Sopenharmony_ci build() { 325e41f4b71Sopenharmony_ci Stack() { 326e41f4b71Sopenharmony_ci NodeContainer(this.nodeController) 327e41f4b71Sopenharmony_ci } 328e41f4b71Sopenharmony_ci .width('100%') 329e41f4b71Sopenharmony_ci .height(this.AnimationProperties.changedHeight ? '100%' : 100) 330e41f4b71Sopenharmony_ci .translate({ x: this.AnimationProperties.translateX, y: this.AnimationProperties.translateY }) 331e41f4b71Sopenharmony_ci .position({ x: this.AnimationProperties.positionX, y: this.AnimationProperties.positionY }) 332e41f4b71Sopenharmony_ci .onClick(() => { 333e41f4b71Sopenharmony_ci animateTo({ curve: curves.springMotion(0.6, 0.9), 334e41f4b71Sopenharmony_ci onFinish: () => { 335e41f4b71Sopenharmony_ci if (this.nodeController != undefined) { 336e41f4b71Sopenharmony_ci // Execute the callback to obtain the widget component from the folded node. 337e41f4b71Sopenharmony_ci this.nodeController.callCallback(); 338e41f4b71Sopenharmony_ci // The widget component of the currently expanded node is removed from the tree. 339e41f4b71Sopenharmony_ci this.nodeController.onRemove(); 340e41f4b71Sopenharmony_ci } 341e41f4b71Sopenharmony_ci // The widget expands to the expanded state node and is removed from the tree. 342e41f4b71Sopenharmony_ci this.AnimationProperties.isExpandPageShow = false; 343e41f4b71Sopenharmony_ci this.AnimationProperties.isEnabled = true; 344e41f4b71Sopenharmony_ci } 345e41f4b71Sopenharmony_ci }, () => { 346e41f4b71Sopenharmony_ci // The widget returns from the expanded state to the collapsed state. 347e41f4b71Sopenharmony_ci this.AnimationProperties.isEnabled = false; 348e41f4b71Sopenharmony_ci this.AnimationProperties.translateX = 0; 349e41f4b71Sopenharmony_ci this.AnimationProperties.translateY = 0; 350e41f4b71Sopenharmony_ci this.AnimationProperties.changedHeight = false; 351e41f4b71Sopenharmony_ci // Update to hide detailed content. 352e41f4b71Sopenharmony_ci this.nodeController?.update(this.AnimationProperties.curIndex.toString(), false); 353e41f4b71Sopenharmony_ci }) 354e41f4b71Sopenharmony_ci }) 355e41f4b71Sopenharmony_ci } 356e41f4b71Sopenharmony_ci} 357e41f4b71Sopenharmony_ci 358e41f4b71Sopenharmony_ciclass RectInfo { 359e41f4b71Sopenharmony_ci left: number = 0; 360e41f4b71Sopenharmony_ci top: number = 0; 361e41f4b71Sopenharmony_ci right: number = 0; 362e41f4b71Sopenharmony_ci bottom: number = 0; 363e41f4b71Sopenharmony_ci width: number = 0; 364e41f4b71Sopenharmony_ci height: number = 0; 365e41f4b71Sopenharmony_ci} 366e41f4b71Sopenharmony_ci 367e41f4b71Sopenharmony_ci// Encapsulated animation class. 368e41f4b71Sopenharmony_ci@Observed 369e41f4b71Sopenharmony_ciclass AnimationProperties { 370e41f4b71Sopenharmony_ci public isExpandPageShow: boolean = false; 371e41f4b71Sopenharmony_ci // Control whether the component responds to click events. 372e41f4b71Sopenharmony_ci public isEnabled: boolean = true; 373e41f4b71Sopenharmony_ci // Index of the expanded widget. 374e41f4b71Sopenharmony_ci public curIndex: number = -1; 375e41f4b71Sopenharmony_ci public translateX: number = 0; 376e41f4b71Sopenharmony_ci public translateY: number = 0; 377e41f4b71Sopenharmony_ci public positionX: number = 0; 378e41f4b71Sopenharmony_ci public positionY: number = 0; 379e41f4b71Sopenharmony_ci public changedHeight: boolean = false; 380e41f4b71Sopenharmony_ci private calculatedTranslateX: number = 0; 381e41f4b71Sopenharmony_ci private calculatedTranslateY: number = 0; 382e41f4b71Sopenharmony_ci // Set the position of the widget relative to the parent component after it is expanded. 383e41f4b71Sopenharmony_ci private expandTranslateX: number = 0; 384e41f4b71Sopenharmony_ci private expandTranslateY: number = 0; 385e41f4b71Sopenharmony_ci 386e41f4b71Sopenharmony_ci public expandAnimation(index: number): void { 387e41f4b71Sopenharmony_ci // Record the index of the widget in the expanded state. 388e41f4b71Sopenharmony_ci if (index != undefined) { 389e41f4b71Sopenharmony_ci this.curIndex = index; 390e41f4b71Sopenharmony_ci } 391e41f4b71Sopenharmony_ci // Calculate the position of the collapsed widget relative to the parent component. 392e41f4b71Sopenharmony_ci this.calculateData(index.toString()); 393e41f4b71Sopenharmony_ci // The widget in expanded state is added to the tree. 394e41f4b71Sopenharmony_ci this.isExpandPageShow = true; 395e41f4b71Sopenharmony_ci // Property animation for widget expansion. 396e41f4b71Sopenharmony_ci animateTo({ curve: curves.springMotion(0.6, 0.9) 397e41f4b71Sopenharmony_ci }, () => { 398e41f4b71Sopenharmony_ci this.translateX = this.calculatedTranslateX; 399e41f4b71Sopenharmony_ci this.translateY = this.calculatedTranslateY; 400e41f4b71Sopenharmony_ci this.changedHeight = true; 401e41f4b71Sopenharmony_ci }) 402e41f4b71Sopenharmony_ci } 403e41f4b71Sopenharmony_ci 404e41f4b71Sopenharmony_ci // Obtain the position of the component that needs to be migrated across nodes, and the position of the common parent node before and after the migration, to calculate the animation parameters for the animating component. 405e41f4b71Sopenharmony_ci public calculateData(key: string): void { 406e41f4b71Sopenharmony_ci let clickedImageInfo = this.getRectInfoById(key); 407e41f4b71Sopenharmony_ci let rootStackInfo = this.getRectInfoById('rootStack'); 408e41f4b71Sopenharmony_ci this.positionX = px2vp(clickedImageInfo.left - rootStackInfo.left); 409e41f4b71Sopenharmony_ci this.positionY = px2vp(clickedImageInfo.top - rootStackInfo.top); 410e41f4b71Sopenharmony_ci this.calculatedTranslateX = px2vp(rootStackInfo.left - clickedImageInfo.left) + this.expandTranslateX; 411e41f4b71Sopenharmony_ci this.calculatedTranslateY = px2vp(rootStackInfo.top - clickedImageInfo.top) + this.expandTranslateY; 412e41f4b71Sopenharmony_ci } 413e41f4b71Sopenharmony_ci 414e41f4b71Sopenharmony_ci // Obtain the position information of the component based on its ID. 415e41f4b71Sopenharmony_ci private getRectInfoById(id: string): RectInfo { 416e41f4b71Sopenharmony_ci let componentInfo: componentUtils.ComponentInfo = componentUtils.getRectangleById(id); 417e41f4b71Sopenharmony_ci 418e41f4b71Sopenharmony_ci if (!componentInfo) { 419e41f4b71Sopenharmony_ci throw Error('object is empty'); 420e41f4b71Sopenharmony_ci } 421e41f4b71Sopenharmony_ci 422e41f4b71Sopenharmony_ci let rstRect: RectInfo = new RectInfo(); 423e41f4b71Sopenharmony_ci const widthScaleGap = componentInfo.size.width * (1 - componentInfo.scale.x) / 2; 424e41f4b71Sopenharmony_ci const heightScaleGap = componentInfo.size.height * (1 - componentInfo.scale.y) / 2; 425e41f4b71Sopenharmony_ci rstRect.left = componentInfo.translate.x + componentInfo.windowOffset.x + widthScaleGap; 426e41f4b71Sopenharmony_ci rstRect.top = componentInfo.translate.y + componentInfo.windowOffset.y + heightScaleGap; 427e41f4b71Sopenharmony_ci rstRect.right = 428e41f4b71Sopenharmony_ci componentInfo.translate.x + componentInfo.windowOffset.x + componentInfo.size.width - widthScaleGap; 429e41f4b71Sopenharmony_ci rstRect.bottom = 430e41f4b71Sopenharmony_ci componentInfo.translate.y + componentInfo.windowOffset.y + componentInfo.size.height - heightScaleGap; 431e41f4b71Sopenharmony_ci rstRect.width = rstRect.right - rstRect.left; 432e41f4b71Sopenharmony_ci rstRect.height = rstRect.bottom - rstRect.top; 433e41f4b71Sopenharmony_ci 434e41f4b71Sopenharmony_ci return { 435e41f4b71Sopenharmony_ci left: rstRect.left, 436e41f4b71Sopenharmony_ci right: rstRect.right, 437e41f4b71Sopenharmony_ci top: rstRect.top, 438e41f4b71Sopenharmony_ci bottom: rstRect.bottom, 439e41f4b71Sopenharmony_ci width: rstRect.width, 440e41f4b71Sopenharmony_ci height: rstRect.height 441e41f4b71Sopenharmony_ci } 442e41f4b71Sopenharmony_ci } 443e41f4b71Sopenharmony_ci} 444e41f4b71Sopenharmony_ci``` 445e41f4b71Sopenharmony_ci 446e41f4b71Sopenharmony_ci```ts 447e41f4b71Sopenharmony_ci// PostNode.ets 448e41f4b71Sopenharmony_ci// Cross-container migration 449e41f4b71Sopenharmony_ciimport { UIContext } from '@ohos.arkui.UIContext'; 450e41f4b71Sopenharmony_ciimport { NodeController, BuilderNode, FrameNode } from '@ohos.arkui.node'; 451e41f4b71Sopenharmony_ciimport { curves } from '@kit.ArkUI'; 452e41f4b71Sopenharmony_ci 453e41f4b71Sopenharmony_ciclass Data { 454e41f4b71Sopenharmony_ci item: string | null = null 455e41f4b71Sopenharmony_ci isExpand: Boolean | false = false 456e41f4b71Sopenharmony_ci} 457e41f4b71Sopenharmony_ci 458e41f4b71Sopenharmony_ci@Builder 459e41f4b71Sopenharmony_cifunction PostBuilder(data: Data) { 460e41f4b71Sopenharmony_ci // Place the cross-container migration component inside @Builder. 461e41f4b71Sopenharmony_ci Column() { 462e41f4b71Sopenharmony_ci Row() { 463e41f4b71Sopenharmony_ci Row() 464e41f4b71Sopenharmony_ci .backgroundColor(Color.Pink) 465e41f4b71Sopenharmony_ci .borderRadius(20) 466e41f4b71Sopenharmony_ci .width(80) 467e41f4b71Sopenharmony_ci .height(80) 468e41f4b71Sopenharmony_ci 469e41f4b71Sopenharmony_ci Column() { 470e41f4b71Sopenharmony_ci Text('Click to expand Item ' + data.item) 471e41f4b71Sopenharmony_ci .fontSize(20) 472e41f4b71Sopenharmony_ci Text ('Shared element transition') 473e41f4b71Sopenharmony_ci .fontSize(12) 474e41f4b71Sopenharmony_ci .fontColor(0x909399) 475e41f4b71Sopenharmony_ci } 476e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Start) 477e41f4b71Sopenharmony_ci .justifyContent(FlexAlign.SpaceAround) 478e41f4b71Sopenharmony_ci .margin({ left: 10 }) 479e41f4b71Sopenharmony_ci .height(80) 480e41f4b71Sopenharmony_ci } 481e41f4b71Sopenharmony_ci .width('90%') 482e41f4b71Sopenharmony_ci .height(100) 483e41f4b71Sopenharmony_ci // Display detailed content in expanded state. 484e41f4b71Sopenharmony_ci if (data.isExpand) { 485e41f4b71Sopenharmony_ci Row() { 486e41f4b71Sopenharmony_ci Text('Expanded') 487e41f4b71Sopenharmony_ci .fontSize(28) 488e41f4b71Sopenharmony_ci .fontColor(0x909399) 489e41f4b71Sopenharmony_ci .textAlign(TextAlign.Center) 490e41f4b71Sopenharmony_ci .transition(TransitionEffect.OPACITY.animation({ curve: curves.springMotion(0.6, 0.9) })) 491e41f4b71Sopenharmony_ci } 492e41f4b71Sopenharmony_ci .width('90%') 493e41f4b71Sopenharmony_ci .justifyContent(FlexAlign.Center) 494e41f4b71Sopenharmony_ci } 495e41f4b71Sopenharmony_ci } 496e41f4b71Sopenharmony_ci .width('90%') 497e41f4b71Sopenharmony_ci .height('100%') 498e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Center) 499e41f4b71Sopenharmony_ci .borderRadius(10) 500e41f4b71Sopenharmony_ci .margin({ top: 15 }) 501e41f4b71Sopenharmony_ci .backgroundColor(Color.White) 502e41f4b71Sopenharmony_ci .shadow({ 503e41f4b71Sopenharmony_ci radius: 20, 504e41f4b71Sopenharmony_ci color: 0x909399, 505e41f4b71Sopenharmony_ci offsetX: 20, 506e41f4b71Sopenharmony_ci offsetY: 10 507e41f4b71Sopenharmony_ci }) 508e41f4b71Sopenharmony_ci 509e41f4b71Sopenharmony_ci} 510e41f4b71Sopenharmony_ci 511e41f4b71Sopenharmony_ciclass __InternalValue__{ 512e41f4b71Sopenharmony_ci flag:boolean =false; 513e41f4b71Sopenharmony_ci}; 514e41f4b71Sopenharmony_ci 515e41f4b71Sopenharmony_ciexport class PostNode extends NodeController { 516e41f4b71Sopenharmony_ci private node: BuilderNode<Data[]> | null = null; 517e41f4b71Sopenharmony_ci private isRemove: __InternalValue__ = new __InternalValue__(); 518e41f4b71Sopenharmony_ci private callback: Function | undefined = undefined 519e41f4b71Sopenharmony_ci private data: Data | null = null 520e41f4b71Sopenharmony_ci 521e41f4b71Sopenharmony_ci makeNode(uiContext: UIContext): FrameNode | null { 522e41f4b71Sopenharmony_ci if(this.isRemove.flag == true){ 523e41f4b71Sopenharmony_ci return null; 524e41f4b71Sopenharmony_ci } 525e41f4b71Sopenharmony_ci if (this.node != null) { 526e41f4b71Sopenharmony_ci return this.node.getFrameNode(); 527e41f4b71Sopenharmony_ci } 528e41f4b71Sopenharmony_ci 529e41f4b71Sopenharmony_ci return null; 530e41f4b71Sopenharmony_ci } 531e41f4b71Sopenharmony_ci 532e41f4b71Sopenharmony_ci init(uiContext: UIContext, id: string, isExpand: boolean) { 533e41f4b71Sopenharmony_ci if (this.node != null) { 534e41f4b71Sopenharmony_ci return; 535e41f4b71Sopenharmony_ci } 536e41f4b71Sopenharmony_ci // Create a node, during which the UIContext should be passed. 537e41f4b71Sopenharmony_ci this.node = new BuilderNode(uiContext) 538e41f4b71Sopenharmony_ci // Create an offline component. 539e41f4b71Sopenharmony_ci this.data = { item: id, isExpand: isExpand } 540e41f4b71Sopenharmony_ci this.node.build(wrapBuilder<Data[]>(PostBuilder), this.data) 541e41f4b71Sopenharmony_ci } 542e41f4b71Sopenharmony_ci 543e41f4b71Sopenharmony_ci update(id: string, isExpand: boolean) { 544e41f4b71Sopenharmony_ci if (this.node !== null) { 545e41f4b71Sopenharmony_ci // Call update to perform an update. 546e41f4b71Sopenharmony_ci this.data = { item: id, isExpand: isExpand } 547e41f4b71Sopenharmony_ci this.node.update(this.data); 548e41f4b71Sopenharmony_ci } 549e41f4b71Sopenharmony_ci } 550e41f4b71Sopenharmony_ci 551e41f4b71Sopenharmony_ci setCallback(callback: Function | undefined) { 552e41f4b71Sopenharmony_ci this.callback = callback 553e41f4b71Sopenharmony_ci } 554e41f4b71Sopenharmony_ci 555e41f4b71Sopenharmony_ci callCallback() { 556e41f4b71Sopenharmony_ci if (this.callback != undefined) { 557e41f4b71Sopenharmony_ci this.callback(); 558e41f4b71Sopenharmony_ci } 559e41f4b71Sopenharmony_ci } 560e41f4b71Sopenharmony_ci 561e41f4b71Sopenharmony_ci onRemove(){ 562e41f4b71Sopenharmony_ci this.isRemove.flag = true; 563e41f4b71Sopenharmony_ci // Trigger rebuild when the component is migrated out of the node. 564e41f4b71Sopenharmony_ci this.rebuild(); 565e41f4b71Sopenharmony_ci this.isRemove.flag = false; 566e41f4b71Sopenharmony_ci } 567e41f4b71Sopenharmony_ci} 568e41f4b71Sopenharmony_ci 569e41f4b71Sopenharmony_cilet gNodeMap: Map<string, PostNode | undefined> = new Map(); 570e41f4b71Sopenharmony_ci 571e41f4b71Sopenharmony_ciexport const createPostNode = 572e41f4b71Sopenharmony_ci (uiContext: UIContext, id: string, isExpand: boolean): PostNode | undefined => { 573e41f4b71Sopenharmony_ci let node = new PostNode(); 574e41f4b71Sopenharmony_ci node.init(uiContext, id, isExpand); 575e41f4b71Sopenharmony_ci gNodeMap.set(id, node); 576e41f4b71Sopenharmony_ci return node; 577e41f4b71Sopenharmony_ci } 578e41f4b71Sopenharmony_ci 579e41f4b71Sopenharmony_ciexport const getPostNode = (id: string): PostNode | undefined => { 580e41f4b71Sopenharmony_ci if (!gNodeMap.has(id)) { 581e41f4b71Sopenharmony_ci return undefined 582e41f4b71Sopenharmony_ci } 583e41f4b71Sopenharmony_ci return gNodeMap.get(id); 584e41f4b71Sopenharmony_ci} 585e41f4b71Sopenharmony_ci 586e41f4b71Sopenharmony_ciexport const deleteNode = (id: string) => { 587e41f4b71Sopenharmony_ci gNodeMap.delete(id) 588e41f4b71Sopenharmony_ci} 589e41f4b71Sopenharmony_ci``` 590e41f4b71Sopenharmony_ci 591e41f4b71Sopenharmony_ci 592e41f4b71Sopenharmony_ci 593e41f4b71Sopenharmony_ci### Using with Navigation 594e41f4b71Sopenharmony_ci 595e41f4b71Sopenharmony_ciYou can use the [customNavContentTransition](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#customnavcontenttransition11) (see [Example 3](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#example-3)) capability of [Navigation](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md) to implement shared element transition, during which, the component is migrated from the disappearing page to the appearing page. 596e41f4b71Sopenharmony_ci 597e41f4b71Sopenharmony_ciThe following is the procedure for implementing the expanding and collapsing of a thumbnail: 598e41f4b71Sopenharmony_ci 599e41f4b71Sopenharmony_ci- Configure custom navigation transition animations between **PageOne** and **PageTwo** using **customNavContentTransition**. 600e41f4b71Sopenharmony_ci 601e41f4b71Sopenharmony_ci- Implement the custom shared element transition with property animations. This is done by capturing the position information of components relative to the window, which allows for the correct matching of the components' positions, scales, and other information on **PageOne** and **PageTwo**, that is, the starting and ending property information for the animation. 602e41f4b71Sopenharmony_ci 603e41f4b71Sopenharmony_ci- After the thumbnail is clicked, the shared element transitions from **PageOne** to **PageTwo**, triggering a custom animation that expands the element from a thumbnail to full-screen on **PageTwo**. 604e41f4b71Sopenharmony_ci 605e41f4b71Sopenharmony_ci- When returning to the thumbnail from the full-screen state, a custom transition animation from **PageTwo** to **PageOne** is triggered, animating the shared element from full-screen to the thumbnail state on **PageOne**, and the component is migrated back to **PageOne** after the transition. 606e41f4b71Sopenharmony_ci 607e41f4b71Sopenharmony_ci``` 608e41f4b71Sopenharmony_ci├──entry/src/main/ets // Code directory 609e41f4b71Sopenharmony_ci│ ├──CustomTransition 610e41f4b71Sopenharmony_ci│ │ ├──AnimationProperties.ets // Encapsulation of shared element transition animation 611e41f4b71Sopenharmony_ci│ │ └──CustomNavigationUtils.ets // Custom transition animation configuration for Navigation 612e41f4b71Sopenharmony_ci│ ├──entryability 613e41f4b71Sopenharmony_ci│ │ └──EntryAbility.ets // Entry point class 614e41f4b71Sopenharmony_ci│ ├──NodeContainer 615e41f4b71Sopenharmony_ci│ │ └──CustomComponent.ets // Custom placeholder node 616e41f4b71Sopenharmony_ci│ ├──pages 617e41f4b71Sopenharmony_ci│ │ ├──Index.ets // Navigation page 618e41f4b71Sopenharmony_ci│ │ ├──PageOne.ets // Thumbnail page 619e41f4b71Sopenharmony_ci│ │ └──PageTwo.ets // Full-screen page 620e41f4b71Sopenharmony_ci│ └──utils 621e41f4b71Sopenharmony_ci│ ├──ComponentAttrUtils.ets // Component position acquisition 622e41f4b71Sopenharmony_ci│ └──WindowUtils.ets // Window information 623e41f4b71Sopenharmony_ci└──entry/src/main/resources // Resource files 624e41f4b71Sopenharmony_ci``` 625e41f4b71Sopenharmony_ci 626e41f4b71Sopenharmony_ci```ts 627e41f4b71Sopenharmony_ci// Index.ets 628e41f4b71Sopenharmony_ciimport { AnimateCallback, CustomTransition } from '../CustomTransition/CustomNavigationUtils'; 629e41f4b71Sopenharmony_ci 630e41f4b71Sopenharmony_ciconst TAG: string = 'Index'; 631e41f4b71Sopenharmony_ci 632e41f4b71Sopenharmony_ci@Entry 633e41f4b71Sopenharmony_ci@Component 634e41f4b71Sopenharmony_cistruct Index { 635e41f4b71Sopenharmony_ci private pageInfos: NavPathStack = new NavPathStack(); 636e41f4b71Sopenharmony_ci // Allow custom transition for specific pages by name. 637e41f4b71Sopenharmony_ci private allowedCustomTransitionFromPageName: string[] = ['PageOne']; 638e41f4b71Sopenharmony_ci private allowedCustomTransitionToPageName: string[] = ['PageTwo']; 639e41f4b71Sopenharmony_ci 640e41f4b71Sopenharmony_ci aboutToAppear(): void { 641e41f4b71Sopenharmony_ci this.pageInfos.pushPath({ name: 'PageOne' }); 642e41f4b71Sopenharmony_ci } 643e41f4b71Sopenharmony_ci 644e41f4b71Sopenharmony_ci private isCustomTransitionEnabled(fromName: string, toName: string): boolean { 645e41f4b71Sopenharmony_ci // Both clicks and returns require custom transitions, so they need to be judged separately. 646e41f4b71Sopenharmony_ci if ((this.allowedCustomTransitionFromPageName.includes(fromName) 647e41f4b71Sopenharmony_ci && this.allowedCustomTransitionToPageName.includes(toName)) 648e41f4b71Sopenharmony_ci || (this.allowedCustomTransitionFromPageName.includes(toName) 649e41f4b71Sopenharmony_ci && this.allowedCustomTransitionToPageName.includes(fromName))) { 650e41f4b71Sopenharmony_ci return true; 651e41f4b71Sopenharmony_ci } 652e41f4b71Sopenharmony_ci return false; 653e41f4b71Sopenharmony_ci } 654e41f4b71Sopenharmony_ci 655e41f4b71Sopenharmony_ci build() { 656e41f4b71Sopenharmony_ci Navigation(this.pageInfos) 657e41f4b71Sopenharmony_ci .hideNavBar(true) 658e41f4b71Sopenharmony_ci .customNavContentTransition((from: NavContentInfo, to: NavContentInfo, operation: NavigationOperation) => { 659e41f4b71Sopenharmony_ci if ((!from || !to) || (!from.name || !to.name)) { 660e41f4b71Sopenharmony_ci return undefined; 661e41f4b71Sopenharmony_ci } 662e41f4b71Sopenharmony_ci 663e41f4b71Sopenharmony_ci // Control custom transition routes by the names of 'from' and 'to'. 664e41f4b71Sopenharmony_ci if (!this.isCustomTransitionEnabled(from.name, to.name)) { 665e41f4b71Sopenharmony_ci return undefined; 666e41f4b71Sopenharmony_ci } 667e41f4b71Sopenharmony_ci 668e41f4b71Sopenharmony_ci // Check whether the transition pages have registered animations to decide whether to perform a custom transition. 669e41f4b71Sopenharmony_ci let fromParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(from.index); 670e41f4b71Sopenharmony_ci let toParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(to.index); 671e41f4b71Sopenharmony_ci if (!fromParam.animation || !toParam.animation) { 672e41f4b71Sopenharmony_ci return undefined; 673e41f4b71Sopenharmony_ci } 674e41f4b71Sopenharmony_ci 675e41f4b71Sopenharmony_ci // After all judgments are made, construct customAnimation for the system side to call and execute the custom transition animation. 676e41f4b71Sopenharmony_ci let customAnimation: NavigationAnimatedTransition = { 677e41f4b71Sopenharmony_ci onTransitionEnd: (isSuccess: boolean) => { 678e41f4b71Sopenharmony_ci console.log(TAG, `current transition result is ${isSuccess}`); 679e41f4b71Sopenharmony_ci }, 680e41f4b71Sopenharmony_ci timeout: 2000, 681e41f4b71Sopenharmony_ci transition: (transitionProxy: NavigationTransitionProxy) => { 682e41f4b71Sopenharmony_ci console.log(TAG, 'trigger transition callback'); 683e41f4b71Sopenharmony_ci if (fromParam.animation) { 684e41f4b71Sopenharmony_ci fromParam.animation(operation == NavigationOperation.PUSH, true, transitionProxy); 685e41f4b71Sopenharmony_ci } 686e41f4b71Sopenharmony_ci if (toParam.animation) { 687e41f4b71Sopenharmony_ci toParam.animation(operation == NavigationOperation.PUSH, false, transitionProxy); 688e41f4b71Sopenharmony_ci } 689e41f4b71Sopenharmony_ci } 690e41f4b71Sopenharmony_ci }; 691e41f4b71Sopenharmony_ci return customAnimation; 692e41f4b71Sopenharmony_ci }) 693e41f4b71Sopenharmony_ci } 694e41f4b71Sopenharmony_ci} 695e41f4b71Sopenharmony_ci``` 696e41f4b71Sopenharmony_ci 697e41f4b71Sopenharmony_ci```ts 698e41f4b71Sopenharmony_ci// PageOne.ets 699e41f4b71Sopenharmony_ciimport { CustomTransition } from '../CustomTransition/CustomNavigationUtils'; 700e41f4b71Sopenharmony_ciimport { MyNodeController, createMyNode, getMyNode } from '../NodeContainer/CustomComponent'; 701e41f4b71Sopenharmony_ciimport { ComponentAttrUtils, RectInfoInPx } from '../utils/ComponentAttrUtils'; 702e41f4b71Sopenharmony_ciimport { WindowUtils } from '../utils/WindowUtils'; 703e41f4b71Sopenharmony_ci 704e41f4b71Sopenharmony_ci@Builder 705e41f4b71Sopenharmony_ciexport function PageOneBuilder() { 706e41f4b71Sopenharmony_ci PageOne(); 707e41f4b71Sopenharmony_ci} 708e41f4b71Sopenharmony_ci 709e41f4b71Sopenharmony_ci@Component 710e41f4b71Sopenharmony_ciexport struct PageOne { 711e41f4b71Sopenharmony_ci private pageInfos: NavPathStack = new NavPathStack(); 712e41f4b71Sopenharmony_ci private pageId: number = -1; 713e41f4b71Sopenharmony_ci @State myNodeController: MyNodeController | undefined = new MyNodeController(false); 714e41f4b71Sopenharmony_ci 715e41f4b71Sopenharmony_ci aboutToAppear(): void { 716e41f4b71Sopenharmony_ci let node = getMyNode(); 717e41f4b71Sopenharmony_ci if (node == undefined) { 718e41f4b71Sopenharmony_ci // Create a custom node. 719e41f4b71Sopenharmony_ci createMyNode(this.getUIContext()); 720e41f4b71Sopenharmony_ci } 721e41f4b71Sopenharmony_ci this.myNodeController = getMyNode(); 722e41f4b71Sopenharmony_ci } 723e41f4b71Sopenharmony_ci 724e41f4b71Sopenharmony_ci private doFinishTransition(): void { 725e41f4b71Sopenharmony_ci // Migrate the node back from PageTwo to PageOne when the transition on PageTwo ends. 726e41f4b71Sopenharmony_ci this.myNodeController = getMyNode(); 727e41f4b71Sopenharmony_ci } 728e41f4b71Sopenharmony_ci 729e41f4b71Sopenharmony_ci private registerCustomTransition(): void { 730e41f4b71Sopenharmony_ci // Register the custom animation protocol. 731e41f4b71Sopenharmony_ci CustomTransition.getInstance().registerNavParam(this.pageId, 732e41f4b71Sopenharmony_ci (isPush: boolean, isExit: boolean, transitionProxy: NavigationTransitionProxy) => {}, 500); 733e41f4b71Sopenharmony_ci } 734e41f4b71Sopenharmony_ci 735e41f4b71Sopenharmony_ci private onCardClicked(): void { 736e41f4b71Sopenharmony_ci let cardItemInfo: RectInfoInPx = 737e41f4b71Sopenharmony_ci ComponentAttrUtils.getRectInfoById(WindowUtils.window.getUIContext(), 'card'); 738e41f4b71Sopenharmony_ci let param: Record<string, Object> = {}; 739e41f4b71Sopenharmony_ci param['cardItemInfo'] = cardItemInfo; 740e41f4b71Sopenharmony_ci param['doDefaultTransition'] = (myController: MyNodeController) => { 741e41f4b71Sopenharmony_ci this.doFinishTransition() 742e41f4b71Sopenharmony_ci }; 743e41f4b71Sopenharmony_ci this.pageInfos.pushPath({ name: 'PageTwo', param: param }); 744e41f4b71Sopenharmony_ci // The custom node is removed from the tree of PageOne. 745e41f4b71Sopenharmony_ci if (this.myNodeController != undefined) { 746e41f4b71Sopenharmony_ci (this.myNodeController as MyNodeController).onRemove(); 747e41f4b71Sopenharmony_ci } 748e41f4b71Sopenharmony_ci } 749e41f4b71Sopenharmony_ci 750e41f4b71Sopenharmony_ci build() { 751e41f4b71Sopenharmony_ci NavDestination() { 752e41f4b71Sopenharmony_ci Stack() { 753e41f4b71Sopenharmony_ci Column({ space: 20 }) { 754e41f4b71Sopenharmony_ci Row({ space: 10 }) { 755e41f4b71Sopenharmony_ci Image($r("app.media.avatar")) 756e41f4b71Sopenharmony_ci .size({ width: 50, height: 50 }) 757e41f4b71Sopenharmony_ci .borderRadius(25) 758e41f4b71Sopenharmony_ci .clip(true) 759e41f4b71Sopenharmony_ci 760e41f4b71Sopenharmony_ci Text('Alice') 761e41f4b71Sopenharmony_ci } 762e41f4b71Sopenharmony_ci .justifyContent(FlexAlign.Start) 763e41f4b71Sopenharmony_ci 764e41f4b71Sopenharmony_ci Text('Hello World') 765e41f4b71Sopenharmony_ci 766e41f4b71Sopenharmony_ci NodeContainer(this.myNodeController) 767e41f4b71Sopenharmony_ci .size({ width: 320, height: 250 }) 768e41f4b71Sopenharmony_ci .onClick(() => { 769e41f4b71Sopenharmony_ci this.onCardClicked() 770e41f4b71Sopenharmony_ci }) 771e41f4b71Sopenharmony_ci } 772e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Start) 773e41f4b71Sopenharmony_ci .margin(30) 774e41f4b71Sopenharmony_ci } 775e41f4b71Sopenharmony_ci } 776e41f4b71Sopenharmony_ci .onReady((context: NavDestinationContext) => { 777e41f4b71Sopenharmony_ci this.pageInfos = context.pathStack; 778e41f4b71Sopenharmony_ci this.pageId = this.pageInfos.getAllPathName().length - 1; 779e41f4b71Sopenharmony_ci this.registerCustomTransition(); 780e41f4b71Sopenharmony_ci }) 781e41f4b71Sopenharmony_ci .onDisAppear(() => { 782e41f4b71Sopenharmony_ci CustomTransition.getInstance().unRegisterNavParam(this.pageId); 783e41f4b71Sopenharmony_ci // The custom node is removed from the tree of PageOne. 784e41f4b71Sopenharmony_ci if (this.myNodeController != undefined) { 785e41f4b71Sopenharmony_ci (this.myNodeController as MyNodeController).onRemove(); 786e41f4b71Sopenharmony_ci } 787e41f4b71Sopenharmony_ci }) 788e41f4b71Sopenharmony_ci } 789e41f4b71Sopenharmony_ci} 790e41f4b71Sopenharmony_ci``` 791e41f4b71Sopenharmony_ci 792e41f4b71Sopenharmony_ci```ts 793e41f4b71Sopenharmony_ci// PageTwo.ets 794e41f4b71Sopenharmony_ciimport { CustomTransition } from '../CustomTransition/CustomNavigationUtils'; 795e41f4b71Sopenharmony_ciimport { AnimationProperties } from '../CustomTransition/AnimationProperties'; 796e41f4b71Sopenharmony_ciimport { RectInfoInPx } from '../utils/ComponentAttrUtils'; 797e41f4b71Sopenharmony_ciimport { getMyNode, MyNodeController } from '../NodeContainer/CustomComponent'; 798e41f4b71Sopenharmony_ci 799e41f4b71Sopenharmony_ci@Builder 800e41f4b71Sopenharmony_ciexport function PageTwoBuilder() { 801e41f4b71Sopenharmony_ci PageTwo(); 802e41f4b71Sopenharmony_ci} 803e41f4b71Sopenharmony_ci 804e41f4b71Sopenharmony_ci@Component 805e41f4b71Sopenharmony_ciexport struct PageTwo { 806e41f4b71Sopenharmony_ci @State pageInfos: NavPathStack = new NavPathStack(); 807e41f4b71Sopenharmony_ci @State AnimationProperties: AnimationProperties = new AnimationProperties(); 808e41f4b71Sopenharmony_ci @State myNodeController: MyNodeController | undefined = new MyNodeController(false); 809e41f4b71Sopenharmony_ci 810e41f4b71Sopenharmony_ci private pageId: number = -1; 811e41f4b71Sopenharmony_ci 812e41f4b71Sopenharmony_ci private shouldDoDefaultTransition: boolean = false; 813e41f4b71Sopenharmony_ci private prePageDoFinishTransition: () => void = () => {}; 814e41f4b71Sopenharmony_ci private cardItemInfo: RectInfoInPx = new RectInfoInPx(); 815e41f4b71Sopenharmony_ci 816e41f4b71Sopenharmony_ci @StorageProp('windowSizeChanged') @Watch('unRegisterNavParam') windowSizeChangedTime: number = 0; 817e41f4b71Sopenharmony_ci @StorageProp('onConfigurationUpdate') @Watch('unRegisterNavParam') onConfigurationUpdateTime: number = 0; 818e41f4b71Sopenharmony_ci 819e41f4b71Sopenharmony_ci aboutToAppear(): void { 820e41f4b71Sopenharmony_ci // Migrate the custom node to the current page. 821e41f4b71Sopenharmony_ci this.myNodeController = getMyNode(); 822e41f4b71Sopenharmony_ci } 823e41f4b71Sopenharmony_ci 824e41f4b71Sopenharmony_ci private unRegisterNavParam(): void { 825e41f4b71Sopenharmony_ci this.shouldDoDefaultTransition = true; 826e41f4b71Sopenharmony_ci } 827e41f4b71Sopenharmony_ci 828e41f4b71Sopenharmony_ci private onBackPressed(): boolean { 829e41f4b71Sopenharmony_ci if (this.shouldDoDefaultTransition) { 830e41f4b71Sopenharmony_ci CustomTransition.getInstance().unRegisterNavParam(this.pageId); 831e41f4b71Sopenharmony_ci this.pageInfos.pop(); 832e41f4b71Sopenharmony_ci this.prePageDoFinishTransition(); 833e41f4b71Sopenharmony_ci this.shouldDoDefaultTransition = false; 834e41f4b71Sopenharmony_ci return true; 835e41f4b71Sopenharmony_ci } 836e41f4b71Sopenharmony_ci this.pageInfos.pop(); 837e41f4b71Sopenharmony_ci return true; 838e41f4b71Sopenharmony_ci } 839e41f4b71Sopenharmony_ci 840e41f4b71Sopenharmony_ci build() { 841e41f4b71Sopenharmony_ci NavDestination() { 842e41f4b71Sopenharmony_ci // Set alignContent to TopStart for Stack; otherwise, during height changes, both the snapshot and content will be repositioned with the height relayout. 843e41f4b71Sopenharmony_ci Stack({ alignContent: Alignment.TopStart }) { 844e41f4b71Sopenharmony_ci Stack({ alignContent: Alignment.TopStart }) { 845e41f4b71Sopenharmony_ci Column({space: 20}) { 846e41f4b71Sopenharmony_ci NodeContainer(this.myNodeController) 847e41f4b71Sopenharmony_ci if (this.AnimationProperties.showDetailContent) 848e41f4b71Sopenharmony_ci Text('Expanded content') 849e41f4b71Sopenharmony_ci .fontSize(20) 850e41f4b71Sopenharmony_ci .transition(TransitionEffect.OPACITY) 851e41f4b71Sopenharmony_ci .margin(30) 852e41f4b71Sopenharmony_ci } 853e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Start) 854e41f4b71Sopenharmony_ci } 855e41f4b71Sopenharmony_ci .position({ y: this.AnimationProperties.positionValue }) 856e41f4b71Sopenharmony_ci } 857e41f4b71Sopenharmony_ci .scale({ x: this.AnimationProperties.scaleValue, y: this.AnimationProperties.scaleValue }) 858e41f4b71Sopenharmony_ci .translate({ x: this.AnimationProperties.translateX, y: this.AnimationProperties.translateY }) 859e41f4b71Sopenharmony_ci .width(this.AnimationProperties.clipWidth) 860e41f4b71Sopenharmony_ci .height(this.AnimationProperties.clipHeight) 861e41f4b71Sopenharmony_ci .borderRadius(this.AnimationProperties.radius) 862e41f4b71Sopenharmony_ci // Use expandSafeArea to create an immersive effect for Stack, expanding it upwards to the status bar and downwards to the navigation bar. 863e41f4b71Sopenharmony_ci .expandSafeArea([SafeAreaType.SYSTEM]) 864e41f4b71Sopenharmony_ci // Clip the height. 865e41f4b71Sopenharmony_ci .clip(true) 866e41f4b71Sopenharmony_ci } 867e41f4b71Sopenharmony_ci .backgroundColor(this.AnimationProperties.navDestinationBgColor) 868e41f4b71Sopenharmony_ci .hideTitleBar(true) 869e41f4b71Sopenharmony_ci .onReady((context: NavDestinationContext) => { 870e41f4b71Sopenharmony_ci this.pageInfos = context.pathStack; 871e41f4b71Sopenharmony_ci this.pageId = this.pageInfos.getAllPathName().length - 1; 872e41f4b71Sopenharmony_ci let param = context.pathInfo?.param as Record<string, Object>; 873e41f4b71Sopenharmony_ci this.prePageDoFinishTransition = param['doDefaultTransition'] as () => void; 874e41f4b71Sopenharmony_ci this.cardItemInfo = param['cardItemInfo'] as RectInfoInPx; 875e41f4b71Sopenharmony_ci CustomTransition.getInstance().registerNavParam(this.pageId, 876e41f4b71Sopenharmony_ci (isPush: boolean, isExit: boolean, transitionProxy: NavigationTransitionProxy) => { 877e41f4b71Sopenharmony_ci this.AnimationProperties.doAnimation( 878e41f4b71Sopenharmony_ci this.cardItemInfo, isPush, isExit, transitionProxy, 0, 879e41f4b71Sopenharmony_ci this.prePageDoFinishTransition, this.myNodeController); 880e41f4b71Sopenharmony_ci }, 500); 881e41f4b71Sopenharmony_ci }) 882e41f4b71Sopenharmony_ci .onBackPressed(() => { 883e41f4b71Sopenharmony_ci return this.onBackPressed(); 884e41f4b71Sopenharmony_ci }) 885e41f4b71Sopenharmony_ci .onDisAppear(() => { 886e41f4b71Sopenharmony_ci CustomTransition.getInstance().unRegisterNavParam(this.pageId); 887e41f4b71Sopenharmony_ci }) 888e41f4b71Sopenharmony_ci } 889e41f4b71Sopenharmony_ci} 890e41f4b71Sopenharmony_ci``` 891e41f4b71Sopenharmony_ci 892e41f4b71Sopenharmony_ci```ts 893e41f4b71Sopenharmony_ci// CustomNavigationUtils.ets 894e41f4b71Sopenharmony_ci// Configure custom transition animations for Navigation. 895e41f4b71Sopenharmony_ciexport interface AnimateCallback { 896e41f4b71Sopenharmony_ci animation: ((isPush: boolean, isExit: boolean, transitionProxy: NavigationTransitionProxy) => void | undefined) 897e41f4b71Sopenharmony_ci | undefined; 898e41f4b71Sopenharmony_ci timeout: (number | undefined) | undefined; 899e41f4b71Sopenharmony_ci} 900e41f4b71Sopenharmony_ci 901e41f4b71Sopenharmony_ciconst customTransitionMap: Map<number, AnimateCallback> = new Map(); 902e41f4b71Sopenharmony_ci 903e41f4b71Sopenharmony_ciexport class CustomTransition { 904e41f4b71Sopenharmony_ci private constructor() {}; 905e41f4b71Sopenharmony_ci 906e41f4b71Sopenharmony_ci static delegate = new CustomTransition(); 907e41f4b71Sopenharmony_ci 908e41f4b71Sopenharmony_ci static getInstance() { 909e41f4b71Sopenharmony_ci return CustomTransition.delegate; 910e41f4b71Sopenharmony_ci } 911e41f4b71Sopenharmony_ci 912e41f4b71Sopenharmony_ci // Register the animation callback for a page, where name is the identifier for the page's animation callback. 913e41f4b71Sopenharmony_ci // animationCallback indicates the animation content to be executed, and timeout indicates the timeout for ending the transition. 914e41f4b71Sopenharmony_ci registerNavParam( 915e41f4b71Sopenharmony_ci name: number, 916e41f4b71Sopenharmony_ci animationCallback: (operation: boolean, isExit: boolean, transitionProxy: NavigationTransitionProxy) => void, 917e41f4b71Sopenharmony_ci timeout: number): void { 918e41f4b71Sopenharmony_ci if (customTransitionMap.has(name)) { 919e41f4b71Sopenharmony_ci let param = customTransitionMap.get(name); 920e41f4b71Sopenharmony_ci if (param != undefined) { 921e41f4b71Sopenharmony_ci param.animation = animationCallback; 922e41f4b71Sopenharmony_ci param.timeout = timeout; 923e41f4b71Sopenharmony_ci return; 924e41f4b71Sopenharmony_ci } 925e41f4b71Sopenharmony_ci } 926e41f4b71Sopenharmony_ci let params: AnimateCallback = { timeout: timeout, animation: animationCallback }; 927e41f4b71Sopenharmony_ci customTransitionMap.set(name, params); 928e41f4b71Sopenharmony_ci } 929e41f4b71Sopenharmony_ci 930e41f4b71Sopenharmony_ci unRegisterNavParam(name: number): void { 931e41f4b71Sopenharmony_ci customTransitionMap.delete(name); 932e41f4b71Sopenharmony_ci } 933e41f4b71Sopenharmony_ci 934e41f4b71Sopenharmony_ci getAnimateParam(name: number): AnimateCallback { 935e41f4b71Sopenharmony_ci let result: AnimateCallback = { 936e41f4b71Sopenharmony_ci animation: customTransitionMap.get(name)?.animation, 937e41f4b71Sopenharmony_ci timeout: customTransitionMap.get(name)?.timeout, 938e41f4b71Sopenharmony_ci }; 939e41f4b71Sopenharmony_ci return result; 940e41f4b71Sopenharmony_ci } 941e41f4b71Sopenharmony_ci} 942e41f4b71Sopenharmony_ci``` 943e41f4b71Sopenharmony_ci 944e41f4b71Sopenharmony_ci```ts 945e41f4b71Sopenharmony_ci// Add the {"routerMap": "$profile:route_map"} configuration to the project configuration file module.json5. 946e41f4b71Sopenharmony_ci// route_map.json 947e41f4b71Sopenharmony_ci{ 948e41f4b71Sopenharmony_ci "routerMap": [ 949e41f4b71Sopenharmony_ci { 950e41f4b71Sopenharmony_ci "name": "PageOne", 951e41f4b71Sopenharmony_ci "pageSourceFile": "src/main/ets/pages/PageOne.ets", 952e41f4b71Sopenharmony_ci "buildFunction": "PageOneBuilder" 953e41f4b71Sopenharmony_ci }, 954e41f4b71Sopenharmony_ci { 955e41f4b71Sopenharmony_ci "name": "PageTwo", 956e41f4b71Sopenharmony_ci "pageSourceFile": "src/main/ets/pages/PageTwo.ets", 957e41f4b71Sopenharmony_ci "buildFunction": "PageTwoBuilder" 958e41f4b71Sopenharmony_ci } 959e41f4b71Sopenharmony_ci ] 960e41f4b71Sopenharmony_ci} 961e41f4b71Sopenharmony_ci``` 962e41f4b71Sopenharmony_ci 963e41f4b71Sopenharmony_ci```ts 964e41f4b71Sopenharmony_ci// AnimationProperties.ets 965e41f4b71Sopenharmony_ci// Encapsulation of shared element transition animation 966e41f4b71Sopenharmony_ciimport { curves } from '@kit.ArkUI'; 967e41f4b71Sopenharmony_ciimport { RectInfoInPx } from '../utils/ComponentAttrUtils'; 968e41f4b71Sopenharmony_ciimport { WindowUtils } from '../utils/WindowUtils'; 969e41f4b71Sopenharmony_ciimport { MyNodeController } from '../NodeContainer/CustomComponent'; 970e41f4b71Sopenharmony_ci 971e41f4b71Sopenharmony_ciconst TAG: string = 'AnimationProperties'; 972e41f4b71Sopenharmony_ci 973e41f4b71Sopenharmony_ciconst DEVICE_BORDER_RADIUS: number = 34; 974e41f4b71Sopenharmony_ci 975e41f4b71Sopenharmony_ci// Encapsulate the custom shared element transition animation, which can be directly reused by other APIs to reduce workload. 976e41f4b71Sopenharmony_ci@Observed 977e41f4b71Sopenharmony_ciexport class AnimationProperties { 978e41f4b71Sopenharmony_ci public navDestinationBgColor: ResourceColor = Color.Transparent; 979e41f4b71Sopenharmony_ci public translateX: number = 0; 980e41f4b71Sopenharmony_ci public translateY: number = 0; 981e41f4b71Sopenharmony_ci public scaleValue: number = 1; 982e41f4b71Sopenharmony_ci public clipWidth: Dimension = 0; 983e41f4b71Sopenharmony_ci public clipHeight: Dimension = 0; 984e41f4b71Sopenharmony_ci public radius: number = 0; 985e41f4b71Sopenharmony_ci public positionValue: number = 0; 986e41f4b71Sopenharmony_ci public showDetailContent: boolean = false; 987e41f4b71Sopenharmony_ci 988e41f4b71Sopenharmony_ci public doAnimation(cardItemInfo_px: RectInfoInPx, isPush: boolean, isExit: boolean, 989e41f4b71Sopenharmony_ci transitionProxy: NavigationTransitionProxy, extraTranslateValue: number, prePageOnFinish: (index: MyNodeController) => void, myNodeController: MyNodeController|undefined): void { 990e41f4b71Sopenharmony_ci // Calculate the ratio of the widget's width and height to the window's width and height. 991e41f4b71Sopenharmony_ci let widthScaleRatio = cardItemInfo_px.width / WindowUtils.windowWidth_px; 992e41f4b71Sopenharmony_ci let heightScaleRatio = cardItemInfo_px.height / WindowUtils.windowHeight_px; 993e41f4b71Sopenharmony_ci let isUseWidthScale = widthScaleRatio > heightScaleRatio; 994e41f4b71Sopenharmony_ci let initScale: number = isUseWidthScale ? widthScaleRatio : heightScaleRatio; 995e41f4b71Sopenharmony_ci 996e41f4b71Sopenharmony_ci let initTranslateX: number = 0; 997e41f4b71Sopenharmony_ci let initTranslateY: number = 0; 998e41f4b71Sopenharmony_ci let initClipWidth: Dimension = 0; 999e41f4b71Sopenharmony_ci let initClipHeight: Dimension = 0; 1000e41f4b71Sopenharmony_ci // Ensure that the widget on PageTwo expands to the status bar at the top. 1001e41f4b71Sopenharmony_ci let initPositionValue: number = -px2vp(WindowUtils.topAvoidAreaHeight_px + extraTranslateValue);; 1002e41f4b71Sopenharmony_ci 1003e41f4b71Sopenharmony_ci if (isUseWidthScale) { 1004e41f4b71Sopenharmony_ci initTranslateX = px2vp(cardItemInfo_px.left - (WindowUtils.windowWidth_px - cardItemInfo_px.width) / 2); 1005e41f4b71Sopenharmony_ci initClipWidth = '100%'; 1006e41f4b71Sopenharmony_ci initClipHeight = px2vp((cardItemInfo_px.height) / initScale); 1007e41f4b71Sopenharmony_ci initTranslateY = px2vp(cardItemInfo_px.top - ((vp2px(initClipHeight) - vp2px(initClipHeight) * initScale) / 2)); 1008e41f4b71Sopenharmony_ci } else { 1009e41f4b71Sopenharmony_ci initTranslateY = px2vp(cardItemInfo_px.top - (WindowUtils.windowHeight_px - cardItemInfo_px.height) / 2); 1010e41f4b71Sopenharmony_ci initClipHeight = '100%'; 1011e41f4b71Sopenharmony_ci initClipWidth = px2vp((cardItemInfo_px.width) / initScale); 1012e41f4b71Sopenharmony_ci initTranslateX = px2vp(cardItemInfo_px.left - (WindowUtils.windowWidth_px / 2 - cardItemInfo_px.width / 2)); 1013e41f4b71Sopenharmony_ci } 1014e41f4b71Sopenharmony_ci 1015e41f4b71Sopenharmony_ci // Before the transition animation starts, calculate scale, translate, position, and clip height & width to ensure that the node's position is consistent before and after migration. 1016e41f4b71Sopenharmony_ci console.log(TAG, 'initScale: ' + initScale + ' initTranslateX ' + initTranslateX + 1017e41f4b71Sopenharmony_ci ' initTranslateY ' + initTranslateY + ' initClipWidth ' + initClipWidth + 1018e41f4b71Sopenharmony_ci ' initClipHeight ' + initClipHeight + ' initPositionValue ' + initPositionValue); 1019e41f4b71Sopenharmony_ci // Transition to the new page 1020e41f4b71Sopenharmony_ci if (isPush && !isExit) { 1021e41f4b71Sopenharmony_ci this.scaleValue = initScale; 1022e41f4b71Sopenharmony_ci this.translateX = initTranslateX; 1023e41f4b71Sopenharmony_ci this.clipWidth = initClipWidth; 1024e41f4b71Sopenharmony_ci this.clipHeight = initClipHeight; 1025e41f4b71Sopenharmony_ci this.translateY = initTranslateY; 1026e41f4b71Sopenharmony_ci this.positionValue = initPositionValue; 1027e41f4b71Sopenharmony_ci 1028e41f4b71Sopenharmony_ci animateTo({ 1029e41f4b71Sopenharmony_ci curve: curves.interpolatingSpring(0, 1, 328, 36), 1030e41f4b71Sopenharmony_ci onFinish: () => { 1031e41f4b71Sopenharmony_ci if (transitionProxy) { 1032e41f4b71Sopenharmony_ci transitionProxy.finishTransition(); 1033e41f4b71Sopenharmony_ci } 1034e41f4b71Sopenharmony_ci } 1035e41f4b71Sopenharmony_ci }, () => { 1036e41f4b71Sopenharmony_ci this.scaleValue = 1.0; 1037e41f4b71Sopenharmony_ci this.translateX = 0; 1038e41f4b71Sopenharmony_ci this.translateY = 0; 1039e41f4b71Sopenharmony_ci this.clipWidth = '100%'; 1040e41f4b71Sopenharmony_ci this.clipHeight = '100%'; 1041e41f4b71Sopenharmony_ci // The page corner radius matches the system corner radius. 1042e41f4b71Sopenharmony_ci this.radius = DEVICE_BORDER_RADIUS; 1043e41f4b71Sopenharmony_ci this.showDetailContent = true; 1044e41f4b71Sopenharmony_ci }) 1045e41f4b71Sopenharmony_ci 1046e41f4b71Sopenharmony_ci animateTo({ 1047e41f4b71Sopenharmony_ci duration: 100, 1048e41f4b71Sopenharmony_ci curve: Curve.Sharp, 1049e41f4b71Sopenharmony_ci }, () => { 1050e41f4b71Sopenharmony_ci // The page background gradually changes from transparent to the set color. 1051e41f4b71Sopenharmony_ci this.navDestinationBgColor = '#00ffffff'; 1052e41f4b71Sopenharmony_ci }) 1053e41f4b71Sopenharmony_ci 1054e41f4b71Sopenharmony_ci // Return to the previous page. 1055e41f4b71Sopenharmony_ci } else if (!isPush && isExit) { 1056e41f4b71Sopenharmony_ci 1057e41f4b71Sopenharmony_ci animateTo({ 1058e41f4b71Sopenharmony_ci duration: 350, 1059e41f4b71Sopenharmony_ci curve: Curve.EaseInOut, 1060e41f4b71Sopenharmony_ci onFinish: () => { 1061e41f4b71Sopenharmony_ci if (transitionProxy) { 1062e41f4b71Sopenharmony_ci transitionProxy.finishTransition(); 1063e41f4b71Sopenharmony_ci } 1064e41f4b71Sopenharmony_ci prePageOnFinish(myNodeController); 1065e41f4b71Sopenharmony_ci // The custom node is removed from the tree of PageTwo. 1066e41f4b71Sopenharmony_ci if (myNodeController != undefined) { 1067e41f4b71Sopenharmony_ci (myNodeController as MyNodeController).onRemove(); 1068e41f4b71Sopenharmony_ci } 1069e41f4b71Sopenharmony_ci } 1070e41f4b71Sopenharmony_ci }, () => { 1071e41f4b71Sopenharmony_ci this.scaleValue = initScale; 1072e41f4b71Sopenharmony_ci this.translateX = initTranslateX; 1073e41f4b71Sopenharmony_ci this.translateY = initTranslateY; 1074e41f4b71Sopenharmony_ci this.radius = 0; 1075e41f4b71Sopenharmony_ci this.clipWidth = initClipWidth; 1076e41f4b71Sopenharmony_ci this.clipHeight = initClipHeight; 1077e41f4b71Sopenharmony_ci this.showDetailContent = false; 1078e41f4b71Sopenharmony_ci }) 1079e41f4b71Sopenharmony_ci 1080e41f4b71Sopenharmony_ci animateTo({ 1081e41f4b71Sopenharmony_ci duration: 200, 1082e41f4b71Sopenharmony_ci delay: 150, 1083e41f4b71Sopenharmony_ci curve: Curve.Friction, 1084e41f4b71Sopenharmony_ci }, () => { 1085e41f4b71Sopenharmony_ci this.navDestinationBgColor = Color.Transparent; 1086e41f4b71Sopenharmony_ci }) 1087e41f4b71Sopenharmony_ci } 1088e41f4b71Sopenharmony_ci } 1089e41f4b71Sopenharmony_ci} 1090e41f4b71Sopenharmony_ci``` 1091e41f4b71Sopenharmony_ci 1092e41f4b71Sopenharmony_ci```ts 1093e41f4b71Sopenharmony_ci// ComponentAttrUtils.ets 1094e41f4b71Sopenharmony_ci// Obtain the position of the component relative to the window. 1095e41f4b71Sopenharmony_ciimport { componentUtils, UIContext } from '@kit.ArkUI'; 1096e41f4b71Sopenharmony_ciimport { JSON } from '@kit.ArkTS'; 1097e41f4b71Sopenharmony_ci 1098e41f4b71Sopenharmony_ciexport class ComponentAttrUtils { 1099e41f4b71Sopenharmony_ci // Obtain the position information of the component based on its ID. 1100e41f4b71Sopenharmony_ci public static getRectInfoById(context: UIContext, id: string): RectInfoInPx { 1101e41f4b71Sopenharmony_ci if (!context || !id) { 1102e41f4b71Sopenharmony_ci throw Error('object is empty'); 1103e41f4b71Sopenharmony_ci } 1104e41f4b71Sopenharmony_ci let componentInfo: componentUtils.ComponentInfo = context.getComponentUtils().getRectangleById(id); 1105e41f4b71Sopenharmony_ci 1106e41f4b71Sopenharmony_ci if (!componentInfo) { 1107e41f4b71Sopenharmony_ci throw Error('object is empty'); 1108e41f4b71Sopenharmony_ci } 1109e41f4b71Sopenharmony_ci 1110e41f4b71Sopenharmony_ci let rstRect: RectInfoInPx = new RectInfoInPx(); 1111e41f4b71Sopenharmony_ci const widthScaleGap = componentInfo.size.width * (1 - componentInfo.scale.x) / 2; 1112e41f4b71Sopenharmony_ci const heightScaleGap = componentInfo.size.height * (1 - componentInfo.scale.y) / 2; 1113e41f4b71Sopenharmony_ci rstRect.left = componentInfo.translate.x + componentInfo.windowOffset.x + widthScaleGap; 1114e41f4b71Sopenharmony_ci rstRect.top = componentInfo.translate.y + componentInfo.windowOffset.y + heightScaleGap; 1115e41f4b71Sopenharmony_ci rstRect.right = 1116e41f4b71Sopenharmony_ci componentInfo.translate.x + componentInfo.windowOffset.x + componentInfo.size.width - widthScaleGap; 1117e41f4b71Sopenharmony_ci rstRect.bottom = 1118e41f4b71Sopenharmony_ci componentInfo.translate.y + componentInfo.windowOffset.y + componentInfo.size.height - heightScaleGap; 1119e41f4b71Sopenharmony_ci rstRect.width = rstRect.right - rstRect.left; 1120e41f4b71Sopenharmony_ci rstRect.height = rstRect.bottom - rstRect.top; 1121e41f4b71Sopenharmony_ci return { 1122e41f4b71Sopenharmony_ci left: rstRect.left, 1123e41f4b71Sopenharmony_ci right: rstRect.right, 1124e41f4b71Sopenharmony_ci top: rstRect.top, 1125e41f4b71Sopenharmony_ci bottom: rstRect.bottom, 1126e41f4b71Sopenharmony_ci width: rstRect.width, 1127e41f4b71Sopenharmony_ci height: rstRect.height 1128e41f4b71Sopenharmony_ci } 1129e41f4b71Sopenharmony_ci } 1130e41f4b71Sopenharmony_ci} 1131e41f4b71Sopenharmony_ci 1132e41f4b71Sopenharmony_ciexport class RectInfoInPx { 1133e41f4b71Sopenharmony_ci left: number = 0; 1134e41f4b71Sopenharmony_ci top: number = 0; 1135e41f4b71Sopenharmony_ci right: number = 0; 1136e41f4b71Sopenharmony_ci bottom: number = 0; 1137e41f4b71Sopenharmony_ci width: number = 0; 1138e41f4b71Sopenharmony_ci height: number = 0; 1139e41f4b71Sopenharmony_ci} 1140e41f4b71Sopenharmony_ci 1141e41f4b71Sopenharmony_ciexport class RectJson { 1142e41f4b71Sopenharmony_ci $rect: Array<number> = []; 1143e41f4b71Sopenharmony_ci} 1144e41f4b71Sopenharmony_ci``` 1145e41f4b71Sopenharmony_ci 1146e41f4b71Sopenharmony_ci```ts 1147e41f4b71Sopenharmony_ci// WindowUtils.ets 1148e41f4b71Sopenharmony_ci// Window information 1149e41f4b71Sopenharmony_ciimport { window } from '@kit.ArkUI'; 1150e41f4b71Sopenharmony_ci 1151e41f4b71Sopenharmony_ciexport class WindowUtils { 1152e41f4b71Sopenharmony_ci public static window: window.Window; 1153e41f4b71Sopenharmony_ci public static windowWidth_px: number; 1154e41f4b71Sopenharmony_ci public static windowHeight_px: number; 1155e41f4b71Sopenharmony_ci public static topAvoidAreaHeight_px: number; 1156e41f4b71Sopenharmony_ci public static navigationIndicatorHeight_px: number; 1157e41f4b71Sopenharmony_ci} 1158e41f4b71Sopenharmony_ci``` 1159e41f4b71Sopenharmony_ci 1160e41f4b71Sopenharmony_ci```ts 1161e41f4b71Sopenharmony_ci// EntryAbility.ets 1162e41f4b71Sopenharmony_ci// Add capture of window width and height in onWindowStageCreate at the application entry. 1163e41f4b71Sopenharmony_ci 1164e41f4b71Sopenharmony_ciimport { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; 1165e41f4b71Sopenharmony_ciimport { hilog } from '@kit.PerformanceAnalysisKit'; 1166e41f4b71Sopenharmony_ciimport { display, window } from '@kit.ArkUI'; 1167e41f4b71Sopenharmony_ciimport { WindowUtils } from '../utils/WindowUtils'; 1168e41f4b71Sopenharmony_ci 1169e41f4b71Sopenharmony_ciconst TAG: string = 'EntryAbility'; 1170e41f4b71Sopenharmony_ci 1171e41f4b71Sopenharmony_ciexport default class EntryAbility extends UIAbility { 1172e41f4b71Sopenharmony_ci private currentBreakPoint: string = ''; 1173e41f4b71Sopenharmony_ci 1174e41f4b71Sopenharmony_ci onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 1175e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); 1176e41f4b71Sopenharmony_ci } 1177e41f4b71Sopenharmony_ci 1178e41f4b71Sopenharmony_ci onDestroy(): void { 1179e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); 1180e41f4b71Sopenharmony_ci } 1181e41f4b71Sopenharmony_ci 1182e41f4b71Sopenharmony_ci onWindowStageCreate(windowStage: window.WindowStage): void { 1183e41f4b71Sopenharmony_ci // Main window is created, set main page for this ability 1184e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); 1185e41f4b71Sopenharmony_ci 1186e41f4b71Sopenharmony_ci // Obtain the window width and height. 1187e41f4b71Sopenharmony_ci WindowUtils.window = windowStage.getMainWindowSync(); 1188e41f4b71Sopenharmony_ci WindowUtils.windowWidth_px = WindowUtils.window.getWindowProperties().windowRect.width; 1189e41f4b71Sopenharmony_ci WindowUtils.windowHeight_px = WindowUtils.window.getWindowProperties().windowRect.height; 1190e41f4b71Sopenharmony_ci 1191e41f4b71Sopenharmony_ci this.updateBreakpoint(WindowUtils.windowWidth_px); 1192e41f4b71Sopenharmony_ci 1193e41f4b71Sopenharmony_ci // Obtain the height of the upper avoid area (such as the status bar). 1194e41f4b71Sopenharmony_ci let avoidArea = WindowUtils.window.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); 1195e41f4b71Sopenharmony_ci WindowUtils.topAvoidAreaHeight_px = avoidArea.topRect.height; 1196e41f4b71Sopenharmony_ci 1197e41f4b71Sopenharmony_ci // Obtain the height of the navigation bar. 1198e41f4b71Sopenharmony_ci let navigationArea = WindowUtils.window.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); 1199e41f4b71Sopenharmony_ci WindowUtils.navigationIndicatorHeight_px = navigationArea.bottomRect.height; 1200e41f4b71Sopenharmony_ci 1201e41f4b71Sopenharmony_ci console.log(TAG, 'the width is ' + WindowUtils.windowWidth_px + ' ' + WindowUtils.windowHeight_px + ' ' + 1202e41f4b71Sopenharmony_ci WindowUtils.topAvoidAreaHeight_px + ' ' + WindowUtils.navigationIndicatorHeight_px); 1203e41f4b71Sopenharmony_ci 1204e41f4b71Sopenharmony_ci // Listen for changes in the window size, status bar height, and navigation bar height, and update accordingly. 1205e41f4b71Sopenharmony_ci try { 1206e41f4b71Sopenharmony_ci WindowUtils.window.on('windowSizeChange', (data) => { 1207e41f4b71Sopenharmony_ci console.log(TAG, 'on windowSizeChange, the width is ' + data.width + ', the height is ' + data.height); 1208e41f4b71Sopenharmony_ci WindowUtils.windowWidth_px = data.width; 1209e41f4b71Sopenharmony_ci WindowUtils.windowHeight_px = data.height; 1210e41f4b71Sopenharmony_ci this.updateBreakpoint(data.width); 1211e41f4b71Sopenharmony_ci AppStorage.setOrCreate('windowSizeChanged', Date.now()) 1212e41f4b71Sopenharmony_ci }) 1213e41f4b71Sopenharmony_ci 1214e41f4b71Sopenharmony_ci WindowUtils.window.on('avoidAreaChange', (data) => { 1215e41f4b71Sopenharmony_ci if (data.type == window.AvoidAreaType.TYPE_SYSTEM) { 1216e41f4b71Sopenharmony_ci let topRectHeight = data.area.topRect.height; 1217e41f4b71Sopenharmony_ci console.log(TAG, 'on avoidAreaChange, the top avoid area height is ' + topRectHeight); 1218e41f4b71Sopenharmony_ci WindowUtils.topAvoidAreaHeight_px = topRectHeight; 1219e41f4b71Sopenharmony_ci } else if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) { 1220e41f4b71Sopenharmony_ci let bottomRectHeight = data.area.bottomRect.height; 1221e41f4b71Sopenharmony_ci console.log(TAG, 'on avoidAreaChange, the navigation indicator height is ' + bottomRectHeight); 1222e41f4b71Sopenharmony_ci WindowUtils.navigationIndicatorHeight_px = bottomRectHeight; 1223e41f4b71Sopenharmony_ci } 1224e41f4b71Sopenharmony_ci }) 1225e41f4b71Sopenharmony_ci } catch (exception) { 1226e41f4b71Sopenharmony_ci console.log('register failed ' + JSON.stringify(exception)); 1227e41f4b71Sopenharmony_ci } 1228e41f4b71Sopenharmony_ci 1229e41f4b71Sopenharmony_ci windowStage.loadContent('pages/Index', (err) => { 1230e41f4b71Sopenharmony_ci if (err.code) { 1231e41f4b71Sopenharmony_ci hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); 1232e41f4b71Sopenharmony_ci return; 1233e41f4b71Sopenharmony_ci } 1234e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); 1235e41f4b71Sopenharmony_ci }); 1236e41f4b71Sopenharmony_ci } 1237e41f4b71Sopenharmony_ci 1238e41f4b71Sopenharmony_ci updateBreakpoint(width: number) { 1239e41f4b71Sopenharmony_ci let windowWidthVp = width / (display.getDefaultDisplaySync().densityDPI / 160); 1240e41f4b71Sopenharmony_ci let newBreakPoint: string = ''; 1241e41f4b71Sopenharmony_ci if (windowWidthVp < 400) { 1242e41f4b71Sopenharmony_ci newBreakPoint = 'xs'; 1243e41f4b71Sopenharmony_ci } else if (windowWidthVp < 600) { 1244e41f4b71Sopenharmony_ci newBreakPoint = 'sm'; 1245e41f4b71Sopenharmony_ci } else if (windowWidthVp < 800) { 1246e41f4b71Sopenharmony_ci newBreakPoint = 'md'; 1247e41f4b71Sopenharmony_ci } else { 1248e41f4b71Sopenharmony_ci newBreakPoint = 'lg'; 1249e41f4b71Sopenharmony_ci } 1250e41f4b71Sopenharmony_ci if (this.currentBreakPoint !== newBreakPoint) { 1251e41f4b71Sopenharmony_ci this.currentBreakPoint = newBreakPoint; 1252e41f4b71Sopenharmony_ci // Use the state variable to record the current breakpoint value. 1253e41f4b71Sopenharmony_ci AppStorage.setOrCreate('currentBreakpoint', this.currentBreakPoint); 1254e41f4b71Sopenharmony_ci } 1255e41f4b71Sopenharmony_ci } 1256e41f4b71Sopenharmony_ci 1257e41f4b71Sopenharmony_ci onWindowStageDestroy(): void { 1258e41f4b71Sopenharmony_ci // Main window is destroyed, release UI related resources 1259e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); 1260e41f4b71Sopenharmony_ci } 1261e41f4b71Sopenharmony_ci 1262e41f4b71Sopenharmony_ci onForeground(): void { 1263e41f4b71Sopenharmony_ci // Ability has brought to foreground 1264e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); 1265e41f4b71Sopenharmony_ci } 1266e41f4b71Sopenharmony_ci 1267e41f4b71Sopenharmony_ci onBackground(): void { 1268e41f4b71Sopenharmony_ci // Ability has back to background 1269e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); 1270e41f4b71Sopenharmony_ci } 1271e41f4b71Sopenharmony_ci} 1272e41f4b71Sopenharmony_ci``` 1273e41f4b71Sopenharmony_ci 1274e41f4b71Sopenharmony_ci```ts 1275e41f4b71Sopenharmony_ci// CustomComponent.ets 1276e41f4b71Sopenharmony_ci// Custom placeholder node with cross-container migration capability 1277e41f4b71Sopenharmony_ciimport { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI'; 1278e41f4b71Sopenharmony_ci 1279e41f4b71Sopenharmony_ci@Builder 1280e41f4b71Sopenharmony_cifunction CardBuilder() { 1281e41f4b71Sopenharmony_ci Image($r("app.media.card")) 1282e41f4b71Sopenharmony_ci .width('100%') 1283e41f4b71Sopenharmony_ci .id('card') 1284e41f4b71Sopenharmony_ci} 1285e41f4b71Sopenharmony_ci 1286e41f4b71Sopenharmony_ciexport class MyNodeController extends NodeController { 1287e41f4b71Sopenharmony_ci private CardNode: BuilderNode<[]> | null = null; 1288e41f4b71Sopenharmony_ci private wrapBuilder: WrappedBuilder<[]> = wrapBuilder(CardBuilder); 1289e41f4b71Sopenharmony_ci private needCreate: boolean = false; 1290e41f4b71Sopenharmony_ci private isRemove: boolean = false; 1291e41f4b71Sopenharmony_ci 1292e41f4b71Sopenharmony_ci constructor(create: boolean) { 1293e41f4b71Sopenharmony_ci super(); 1294e41f4b71Sopenharmony_ci this.needCreate = create; 1295e41f4b71Sopenharmony_ci } 1296e41f4b71Sopenharmony_ci 1297e41f4b71Sopenharmony_ci makeNode(uiContext: UIContext): FrameNode | null { 1298e41f4b71Sopenharmony_ci if(this.isRemove == true){ 1299e41f4b71Sopenharmony_ci return null; 1300e41f4b71Sopenharmony_ci } 1301e41f4b71Sopenharmony_ci if (this.needCreate && this.CardNode == null) { 1302e41f4b71Sopenharmony_ci this.CardNode = new BuilderNode(uiContext); 1303e41f4b71Sopenharmony_ci this.CardNode.build(this.wrapBuilder) 1304e41f4b71Sopenharmony_ci } 1305e41f4b71Sopenharmony_ci if (this.CardNode == null) { 1306e41f4b71Sopenharmony_ci return null; 1307e41f4b71Sopenharmony_ci } 1308e41f4b71Sopenharmony_ci return this.CardNode!.getFrameNode()!; 1309e41f4b71Sopenharmony_ci } 1310e41f4b71Sopenharmony_ci 1311e41f4b71Sopenharmony_ci getNode(): BuilderNode<[]> | null { 1312e41f4b71Sopenharmony_ci return this.CardNode; 1313e41f4b71Sopenharmony_ci } 1314e41f4b71Sopenharmony_ci 1315e41f4b71Sopenharmony_ci setNode(node: BuilderNode<[]> | null) { 1316e41f4b71Sopenharmony_ci this.CardNode = node; 1317e41f4b71Sopenharmony_ci this.rebuild(); 1318e41f4b71Sopenharmony_ci } 1319e41f4b71Sopenharmony_ci 1320e41f4b71Sopenharmony_ci onRemove() { 1321e41f4b71Sopenharmony_ci this.isRemove = true; 1322e41f4b71Sopenharmony_ci this.rebuild(); 1323e41f4b71Sopenharmony_ci this.isRemove = false; 1324e41f4b71Sopenharmony_ci } 1325e41f4b71Sopenharmony_ci 1326e41f4b71Sopenharmony_ci init(uiContext: UIContext) { 1327e41f4b71Sopenharmony_ci this.CardNode = new BuilderNode(uiContext); 1328e41f4b71Sopenharmony_ci this.CardNode.build(this.wrapBuilder) 1329e41f4b71Sopenharmony_ci } 1330e41f4b71Sopenharmony_ci} 1331e41f4b71Sopenharmony_ci 1332e41f4b71Sopenharmony_cilet myNode: MyNodeController | undefined; 1333e41f4b71Sopenharmony_ci 1334e41f4b71Sopenharmony_ciexport const createMyNode = 1335e41f4b71Sopenharmony_ci (uiContext: UIContext) => { 1336e41f4b71Sopenharmony_ci myNode = new MyNodeController(false); 1337e41f4b71Sopenharmony_ci myNode.init(uiContext); 1338e41f4b71Sopenharmony_ci } 1339e41f4b71Sopenharmony_ci 1340e41f4b71Sopenharmony_ciexport const getMyNode = (): MyNodeController | undefined => { 1341e41f4b71Sopenharmony_ci return myNode; 1342e41f4b71Sopenharmony_ci} 1343e41f4b71Sopenharmony_ci``` 1344e41f4b71Sopenharmony_ci 1345e41f4b71Sopenharmony_ci 1346e41f4b71Sopenharmony_ci 1347e41f4b71Sopenharmony_ci### Using with BindSheet 1348e41f4b71Sopenharmony_ci 1349e41f4b71Sopenharmony_ciTo achieve a seamless transition to a sheet ([bindSheet](../reference/apis-arkui/arkui-ts/ts-universal-attributes-sheet-transition.md#bindsheet)) with a shared element animation from the initial screen, set the mode in [SheetOptions](../reference/apis-arkui/arkui-ts/ts-universal-attributes-sheet-transition.md#sheetoptions) to **SheetMode.EMBEDDED**. This ensures that a new page can overlay the sheet, and upon returning, the sheet persists with its content intact. Concurrently, use a full modal transition with [bindContentCover](../reference/apis-arkui/arkui-ts/ts-universal-attributes-modal-transition.md#bindcontentcover) that appears without a transition effect. This page should only include the component that requires the shared element transition. Apply property animation to demonstrate the component's transition from the initial screen to the sheet, then close the page after the animation and migrate the component to the sheet. 1350e41f4b71Sopenharmony_ci 1351e41f4b71Sopenharmony_ciTo implement a shared element transition to a sheet when an image is clicked: 1352e41f4b71Sopenharmony_ci 1353e41f4b71Sopenharmony_ci- Mount both a sheet and a full-modal transition on the initial screen: Design the sheet as required, and place only the necessary components for the shared element transition on the full-modal page. Capture layout information to position it over the image on the initial screen. When the image is clicked, trigger both the sheet and full-modal pages to appear, with the full-modal set to **SheetMode.EMBEDDED** for the highest layer. 1354e41f4b71Sopenharmony_ci 1355e41f4b71Sopenharmony_ci- Place an invisible placeholder image on the sheet: This will be the final position for the image after the shared element transition. Use a [layout callback](../reference/apis-arkui/js-apis-arkui-inspector.md) to listen for when the placeholder image's layout is complete, then obtain its position and start the shared element transition with property animation from the full-modal page's image. 1356e41f4b71Sopenharmony_ci 1357e41f4b71Sopenharmony_ci- End the animation on the full-modal page: When the animation ends, trigger a callback to close the full-modal page and migrate the shared element image node to the sheet, replacing the placeholder. 1358e41f4b71Sopenharmony_ci 1359e41f4b71Sopenharmony_ci- Account for height differences: The sheet may have varying elevations, affecting its starting position compared to the full-modal, which is full-screen. Calculate and adjust for these height differences during the shared element transition, as demonstrated in the demo. 1360e41f4b71Sopenharmony_ci 1361e41f4b71Sopenharmony_ci- Enhance with additional animation: Optionally, add an animation to the initial image that transitions from transparent to visible to smooth the overall effect. 1362e41f4b71Sopenharmony_ci 1363e41f4b71Sopenharmony_ci``` 1364e41f4b71Sopenharmony_ci├──entry/src/main/ets // Code directory 1365e41f4b71Sopenharmony_ci│ ├──entryability 1366e41f4b71Sopenharmony_ci│ │ └──EntryAbility.ets // Entry point class 1367e41f4b71Sopenharmony_ci│ ├──NodeContainer 1368e41f4b71Sopenharmony_ci│ │ └──CustomComponent.ets // Custom placeholder node 1369e41f4b71Sopenharmony_ci│ ├──pages 1370e41f4b71Sopenharmony_ci│ │ └──Index.ets // Home page for the shared element transition 1371e41f4b71Sopenharmony_ci│ └──utils 1372e41f4b71Sopenharmony_ci│ ├──ComponentAttrUtils.ets // Component position acquisition 1373e41f4b71Sopenharmony_ci│ └──WindowUtils.ets // Window information 1374e41f4b71Sopenharmony_ci└──entry/src/main/resources // Resource files 1375e41f4b71Sopenharmony_ci``` 1376e41f4b71Sopenharmony_ci 1377e41f4b71Sopenharmony_ci```ts 1378e41f4b71Sopenharmony_ci// index.ets 1379e41f4b71Sopenharmony_ciimport { MyNodeController, createMyNode, getMyNode } from '../NodeContainer/CustomComponent'; 1380e41f4b71Sopenharmony_ciimport { ComponentAttrUtils, RectInfoInPx } from '../utils/ComponentAttrUtils'; 1381e41f4b71Sopenharmony_ciimport { WindowUtils } from '../utils/WindowUtils'; 1382e41f4b71Sopenharmony_ciimport { inspector } from '@kit.ArkUI' 1383e41f4b71Sopenharmony_ci 1384e41f4b71Sopenharmony_ciclass AnimationInfo { 1385e41f4b71Sopenharmony_ci scale: number = 0; 1386e41f4b71Sopenharmony_ci translateX: number = 0; 1387e41f4b71Sopenharmony_ci translateY: number = 0; 1388e41f4b71Sopenharmony_ci clipWidth: Dimension = 0; 1389e41f4b71Sopenharmony_ci clipHeight: Dimension = 0; 1390e41f4b71Sopenharmony_ci} 1391e41f4b71Sopenharmony_ci 1392e41f4b71Sopenharmony_ci@Entry 1393e41f4b71Sopenharmony_ci@Component 1394e41f4b71Sopenharmony_cistruct Index { 1395e41f4b71Sopenharmony_ci @State isShowSheet: boolean = false; 1396e41f4b71Sopenharmony_ci @State isShowImage: boolean = false; 1397e41f4b71Sopenharmony_ci @State isShowOverlay: boolean = false; 1398e41f4b71Sopenharmony_ci @State isAnimating: boolean = false; 1399e41f4b71Sopenharmony_ci @State isEnabled: boolean = true; 1400e41f4b71Sopenharmony_ci 1401e41f4b71Sopenharmony_ci @State scaleValue: number = 0; 1402e41f4b71Sopenharmony_ci @State translateX: number = 0; 1403e41f4b71Sopenharmony_ci @State translateY: number = 0; 1404e41f4b71Sopenharmony_ci @State clipWidth: Dimension = 0; 1405e41f4b71Sopenharmony_ci @State clipHeight: Dimension = 0; 1406e41f4b71Sopenharmony_ci @State radius: number = 0; 1407e41f4b71Sopenharmony_ci // Original image opacity 1408e41f4b71Sopenharmony_ci @State opacityDegree: number = 1; 1409e41f4b71Sopenharmony_ci 1410e41f4b71Sopenharmony_ci // Capture the original position information of the photo. 1411e41f4b71Sopenharmony_ci private originInfo: AnimationInfo = new AnimationInfo; 1412e41f4b71Sopenharmony_ci // Capture the photo's position information on the sheet. 1413e41f4b71Sopenharmony_ci private targetInfo: AnimationInfo = new AnimationInfo; 1414e41f4b71Sopenharmony_ci // Height of the sheet. 1415e41f4b71Sopenharmony_ci private bindSheetHeight: number = 450; 1416e41f4b71Sopenharmony_ci // Image corner radius on the sheet. 1417e41f4b71Sopenharmony_ci private sheetRadius: number = 20; 1418e41f4b71Sopenharmony_ci 1419e41f4b71Sopenharmony_ci // Set a layout listener for the image on the sheet. 1420e41f4b71Sopenharmony_ci listener:inspector.ComponentObserver = this.getUIContext().getUIInspector().createComponentObserver('target'); 1421e41f4b71Sopenharmony_ci aboutToAppear(): void { 1422e41f4b71Sopenharmony_ci // Set a callback for when the layout of the image on the sheet is complete. 1423e41f4b71Sopenharmony_ci let onLayoutComplete:()=>void=():void=>{ 1424e41f4b71Sopenharmony_ci // When the target image layout is complete, capture the layout information. 1425e41f4b71Sopenharmony_ci this.targetInfo = this.calculateData('target'); 1426e41f4b71Sopenharmony_ci // Trigger the shared element transition animation only when the sheet is properly laid out and there is no animation currently running. 1427e41f4b71Sopenharmony_ci if (this.targetInfo.scale != 0 && this.targetInfo.clipWidth != 0 && this.targetInfo.clipHeight != 0 && !this.isAnimating) { 1428e41f4b71Sopenharmony_ci this.isAnimating = true; 1429e41f4b71Sopenharmony_ci // Property animation for shared element transition animation of the modal 1430e41f4b71Sopenharmony_ci animateTo({ 1431e41f4b71Sopenharmony_ci duration: 1000, 1432e41f4b71Sopenharmony_ci curve: Curve.Friction, 1433e41f4b71Sopenharmony_ci onFinish: () => { 1434e41f4b71Sopenharmony_ci // The custom node on the modal transition page (overlay) is removed from the tree. 1435e41f4b71Sopenharmony_ci this.isShowOverlay = false; 1436e41f4b71Sopenharmony_ci // The custom node on the sheet is added to the tree, completing the node migration. 1437e41f4b71Sopenharmony_ci this.isShowImage = true; 1438e41f4b71Sopenharmony_ci } 1439e41f4b71Sopenharmony_ci }, () => { 1440e41f4b71Sopenharmony_ci this.scaleValue = this.targetInfo.scale; 1441e41f4b71Sopenharmony_ci this.translateX = this.targetInfo.translateX; 1442e41f4b71Sopenharmony_ci this.clipWidth = this.targetInfo.clipWidth; 1443e41f4b71Sopenharmony_ci this.clipHeight = this.targetInfo.clipHeight; 1444e41f4b71Sopenharmony_ci // Adjust for height differences caused by sheet height and scaling. 1445e41f4b71Sopenharmony_ci this.translateY = this.targetInfo.translateY + 1446e41f4b71Sopenharmony_ci (px2vp(WindowUtils.windowHeight_px) - this.bindSheetHeight 1447e41f4b71Sopenharmony_ci - px2vp(WindowUtils.navigationIndicatorHeight_px) - px2vp(WindowUtils.topAvoidAreaHeight_px)); 1448e41f4b71Sopenharmony_ci // Adjust for corner radius differences caused by scaling. 1449e41f4b71Sopenharmony_ci this.radius = this.sheetRadius / this.scaleValue 1450e41f4b71Sopenharmony_ci }) 1451e41f4b71Sopenharmony_ci // Animate the original image from transparent to fully visible. 1452e41f4b71Sopenharmony_ci animateTo({ 1453e41f4b71Sopenharmony_ci duration: 2000, 1454e41f4b71Sopenharmony_ci curve: Curve.Friction, 1455e41f4b71Sopenharmony_ci }, () => { 1456e41f4b71Sopenharmony_ci this.opacityDegree = 1; 1457e41f4b71Sopenharmony_ci }) 1458e41f4b71Sopenharmony_ci } 1459e41f4b71Sopenharmony_ci } 1460e41f4b71Sopenharmony_ci // Enable the layout listener. 1461e41f4b71Sopenharmony_ci this.listener.on('layout', onLayoutComplete) 1462e41f4b71Sopenharmony_ci } 1463e41f4b71Sopenharmony_ci 1464e41f4b71Sopenharmony_ci // Obtain the attributes of the component with the corresponding ID relative to the upper left corner of the window. 1465e41f4b71Sopenharmony_ci calculateData(id: string): AnimationInfo { 1466e41f4b71Sopenharmony_ci let itemInfo: RectInfoInPx = 1467e41f4b71Sopenharmony_ci ComponentAttrUtils.getRectInfoById(WindowUtils.window.getUIContext(), id); 1468e41f4b71Sopenharmony_ci // Calculate the ratio of the image's width and height to the window's width and height. 1469e41f4b71Sopenharmony_ci let widthScaleRatio = itemInfo.width / WindowUtils.windowWidth_px; 1470e41f4b71Sopenharmony_ci let heightScaleRatio = itemInfo.height / WindowUtils.windowHeight_px; 1471e41f4b71Sopenharmony_ci let isUseWidthScale = widthScaleRatio > heightScaleRatio; 1472e41f4b71Sopenharmony_ci let itemScale: number = isUseWidthScale ? widthScaleRatio : heightScaleRatio; 1473e41f4b71Sopenharmony_ci let itemTranslateX: number = 0; 1474e41f4b71Sopenharmony_ci let itemClipWidth: Dimension = 0; 1475e41f4b71Sopenharmony_ci let itemClipHeight: Dimension = 0; 1476e41f4b71Sopenharmony_ci let itemTranslateY: number = 0; 1477e41f4b71Sopenharmony_ci 1478e41f4b71Sopenharmony_ci if (isUseWidthScale) { 1479e41f4b71Sopenharmony_ci itemTranslateX = px2vp(itemInfo.left - (WindowUtils.windowWidth_px - itemInfo.width) / 2); 1480e41f4b71Sopenharmony_ci itemClipWidth = '100%'; 1481e41f4b71Sopenharmony_ci itemClipHeight = px2vp((itemInfo.height) / itemScale); 1482e41f4b71Sopenharmony_ci itemTranslateY = px2vp(itemInfo.top - ((vp2px(itemClipHeight) - vp2px(itemClipHeight) * itemScale) / 2)); 1483e41f4b71Sopenharmony_ci } else { 1484e41f4b71Sopenharmony_ci itemTranslateY = px2vp(itemInfo.top - (WindowUtils.windowHeight_px - itemInfo.height) / 2); 1485e41f4b71Sopenharmony_ci itemClipHeight = '100%'; 1486e41f4b71Sopenharmony_ci itemClipWidth = px2vp((itemInfo.width) / itemScale); 1487e41f4b71Sopenharmony_ci itemTranslateX = px2vp(itemInfo.left - (WindowUtils.windowWidth_px / 2 - itemInfo.width / 2)); 1488e41f4b71Sopenharmony_ci } 1489e41f4b71Sopenharmony_ci 1490e41f4b71Sopenharmony_ci return { 1491e41f4b71Sopenharmony_ci scale: itemScale, 1492e41f4b71Sopenharmony_ci translateX: itemTranslateX , 1493e41f4b71Sopenharmony_ci translateY: itemTranslateY, 1494e41f4b71Sopenharmony_ci clipWidth: itemClipWidth, 1495e41f4b71Sopenharmony_ci clipHeight: itemClipHeight, 1496e41f4b71Sopenharmony_ci } 1497e41f4b71Sopenharmony_ci } 1498e41f4b71Sopenharmony_ci 1499e41f4b71Sopenharmony_ci // Photo page. 1500e41f4b71Sopenharmony_ci build() { 1501e41f4b71Sopenharmony_ci Column() { 1502e41f4b71Sopenharmony_ci Text('Photo') 1503e41f4b71Sopenharmony_ci .textAlign(TextAlign.Start) 1504e41f4b71Sopenharmony_ci .width('100%') 1505e41f4b71Sopenharmony_ci .fontSize(30) 1506e41f4b71Sopenharmony_ci .padding(20) 1507e41f4b71Sopenharmony_ci Image($r("app.media.flower")) 1508e41f4b71Sopenharmony_ci .opacity(this.opacityDegree) 1509e41f4b71Sopenharmony_ci .width('90%') 1510e41f4b71Sopenharmony_ci .id('origin')// Mount the sheet page. 1511e41f4b71Sopenharmony_ci .enabled(this.isEnabled) 1512e41f4b71Sopenharmony_ci .onClick(() => { 1513e41f4b71Sopenharmony_ci // Obtain the position information of the original image, and move and scale the image on the modal page to this position. 1514e41f4b71Sopenharmony_ci this.originInfo = this.calculateData('origin'); 1515e41f4b71Sopenharmony_ci this.scaleValue = this.originInfo.scale; 1516e41f4b71Sopenharmony_ci this.translateX = this.originInfo.translateX; 1517e41f4b71Sopenharmony_ci this.translateY = this.originInfo.translateY; 1518e41f4b71Sopenharmony_ci this.clipWidth = this.originInfo.clipWidth; 1519e41f4b71Sopenharmony_ci this.clipHeight = this.originInfo.clipHeight; 1520e41f4b71Sopenharmony_ci this.radius = 0; 1521e41f4b71Sopenharmony_ci this.opacityDegree = 0; 1522e41f4b71Sopenharmony_ci // Start the sheet and modal pages. 1523e41f4b71Sopenharmony_ci this.isShowSheet = true; 1524e41f4b71Sopenharmony_ci this.isShowOverlay = true; 1525e41f4b71Sopenharmony_ci // Set the original image to be non-interactive and interrupt-resistant. 1526e41f4b71Sopenharmony_ci this.isEnabled = false; 1527e41f4b71Sopenharmony_ci }) 1528e41f4b71Sopenharmony_ci } 1529e41f4b71Sopenharmony_ci .width('100%') 1530e41f4b71Sopenharmony_ci .height('100%') 1531e41f4b71Sopenharmony_ci .padding({ top: 20 }) 1532e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Center) 1533e41f4b71Sopenharmony_ci .bindSheet(this.isShowSheet, this.mySheet(), { 1534e41f4b71Sopenharmony_ci // EMBEDDED mode allows other pages to be higher than the sheet page. 1535e41f4b71Sopenharmony_ci mode: SheetMode.EMBEDDED, 1536e41f4b71Sopenharmony_ci height: this.bindSheetHeight, 1537e41f4b71Sopenharmony_ci onDisappear: () => { 1538e41f4b71Sopenharmony_ci // Ensure that the state is correct when the sheet disappears. 1539e41f4b71Sopenharmony_ci this.isShowImage = false; 1540e41f4b71Sopenharmony_ci this.isShowSheet = false; 1541e41f4b71Sopenharmony_ci // Set the shared element transition animation to be triggerable again. 1542e41f4b71Sopenharmony_ci this.isAnimating = false; 1543e41f4b71Sopenharmony_ci // The original image becomes interactive again. 1544e41f4b71Sopenharmony_ci this.isEnabled = true; 1545e41f4b71Sopenharmony_ci } 1546e41f4b71Sopenharmony_ci }) // Mount the modal page as the implementation page for the shared element transition animation. 1547e41f4b71Sopenharmony_ci .bindContentCover(this.isShowOverlay, this.overlayNode(), { 1548e41f4b71Sopenharmony_ci // Set the modal page to have no transition. 1549e41f4b71Sopenharmony_ci transition: TransitionEffect.IDENTITY, 1550e41f4b71Sopenharmony_ci }) 1551e41f4b71Sopenharmony_ci } 1552e41f4b71Sopenharmony_ci 1553e41f4b71Sopenharmony_ci // Sheet page. 1554e41f4b71Sopenharmony_ci @Builder 1555e41f4b71Sopenharmony_ci mySheet() { 1556e41f4b71Sopenharmony_ci Column({space: 20}) { 1557e41f4b71Sopenharmony_ci Text('Sheet') 1558e41f4b71Sopenharmony_ci .fontSize(30) 1559e41f4b71Sopenharmony_ci Row({space: 40}) { 1560e41f4b71Sopenharmony_ci Column({space: 20}) { 1561e41f4b71Sopenharmony_ci ForEach([1, 2, 3, 4], () => { 1562e41f4b71Sopenharmony_ci Stack() 1563e41f4b71Sopenharmony_ci .backgroundColor(Color.Pink) 1564e41f4b71Sopenharmony_ci .borderRadius(20) 1565e41f4b71Sopenharmony_ci .width(60) 1566e41f4b71Sopenharmony_ci .height(60) 1567e41f4b71Sopenharmony_ci }) 1568e41f4b71Sopenharmony_ci } 1569e41f4b71Sopenharmony_ci Column() { 1570e41f4b71Sopenharmony_ci if (this.isShowImage) { 1571e41f4b71Sopenharmony_ci // Custom image node for the sheet page. 1572e41f4b71Sopenharmony_ci ImageNode() 1573e41f4b71Sopenharmony_ci } 1574e41f4b71Sopenharmony_ci else { 1575e41f4b71Sopenharmony_ci // For capturing layout and placeholder use, not actually displayed. 1576e41f4b71Sopenharmony_ci Image($r("app.media.flower")) 1577e41f4b71Sopenharmony_ci .visibility(Visibility.Hidden) 1578e41f4b71Sopenharmony_ci } 1579e41f4b71Sopenharmony_ci } 1580e41f4b71Sopenharmony_ci .height(300) 1581e41f4b71Sopenharmony_ci .width(200) 1582e41f4b71Sopenharmony_ci .borderRadius(20) 1583e41f4b71Sopenharmony_ci .clip(true) 1584e41f4b71Sopenharmony_ci .id('target') 1585e41f4b71Sopenharmony_ci } 1586e41f4b71Sopenharmony_ci .alignItems(VerticalAlign.Top) 1587e41f4b71Sopenharmony_ci } 1588e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Start) 1589e41f4b71Sopenharmony_ci .height('100%') 1590e41f4b71Sopenharmony_ci .width('100%') 1591e41f4b71Sopenharmony_ci .margin(40) 1592e41f4b71Sopenharmony_ci } 1593e41f4b71Sopenharmony_ci 1594e41f4b71Sopenharmony_ci @Builder 1595e41f4b71Sopenharmony_ci overlayNode() { 1596e41f4b71Sopenharmony_ci // Set alignContent to TopStart for Stack; otherwise, during height changes, both the snapshot and content will be repositioned with the height relayout. 1597e41f4b71Sopenharmony_ci Stack({ alignContent: Alignment.TopStart }) { 1598e41f4b71Sopenharmony_ci ImageNode() 1599e41f4b71Sopenharmony_ci } 1600e41f4b71Sopenharmony_ci .scale({ x: this.scaleValue, y: this.scaleValue, centerX: undefined, centerY: undefined}) 1601e41f4b71Sopenharmony_ci .translate({ x: this.translateX, y: this.translateY }) 1602e41f4b71Sopenharmony_ci .width(this.clipWidth) 1603e41f4b71Sopenharmony_ci .height(this.clipHeight) 1604e41f4b71Sopenharmony_ci .borderRadius(this.radius) 1605e41f4b71Sopenharmony_ci .clip(true) 1606e41f4b71Sopenharmony_ci } 1607e41f4b71Sopenharmony_ci} 1608e41f4b71Sopenharmony_ci 1609e41f4b71Sopenharmony_ci@Component 1610e41f4b71Sopenharmony_cistruct ImageNode { 1611e41f4b71Sopenharmony_ci @State myNodeController: MyNodeController | undefined = new MyNodeController(false); 1612e41f4b71Sopenharmony_ci 1613e41f4b71Sopenharmony_ci aboutToAppear(): void { 1614e41f4b71Sopenharmony_ci // Obtain the custom node. 1615e41f4b71Sopenharmony_ci let node = getMyNode(); 1616e41f4b71Sopenharmony_ci if (node == undefined) { 1617e41f4b71Sopenharmony_ci // Create a custom node. 1618e41f4b71Sopenharmony_ci createMyNode(this.getUIContext()); 1619e41f4b71Sopenharmony_ci } 1620e41f4b71Sopenharmony_ci this.myNodeController = getMyNode(); 1621e41f4b71Sopenharmony_ci } 1622e41f4b71Sopenharmony_ci 1623e41f4b71Sopenharmony_ci aboutToDisappear(): void { 1624e41f4b71Sopenharmony_ci if (this.myNodeController != undefined) { 1625e41f4b71Sopenharmony_ci // The node is removed from the tree. 1626e41f4b71Sopenharmony_ci this.myNodeController.onRemove(); 1627e41f4b71Sopenharmony_ci } 1628e41f4b71Sopenharmony_ci } 1629e41f4b71Sopenharmony_ci build() { 1630e41f4b71Sopenharmony_ci NodeContainer(this.myNodeController) 1631e41f4b71Sopenharmony_ci } 1632e41f4b71Sopenharmony_ci} 1633e41f4b71Sopenharmony_ci``` 1634e41f4b71Sopenharmony_ci 1635e41f4b71Sopenharmony_ci```ts 1636e41f4b71Sopenharmony_ci// CustomComponent.ets 1637e41f4b71Sopenharmony_ci// Custom placeholder node with cross-container migration capability 1638e41f4b71Sopenharmony_ciimport { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI'; 1639e41f4b71Sopenharmony_ci 1640e41f4b71Sopenharmony_ci@Builder 1641e41f4b71Sopenharmony_cifunction CardBuilder() { 1642e41f4b71Sopenharmony_ci Image($r("app.media.flower")) 1643e41f4b71Sopenharmony_ci // Prevent flickering of the image during the first load. 1644e41f4b71Sopenharmony_ci .syncLoad(true) 1645e41f4b71Sopenharmony_ci} 1646e41f4b71Sopenharmony_ci 1647e41f4b71Sopenharmony_ciexport class MyNodeController extends NodeController { 1648e41f4b71Sopenharmony_ci private CardNode: BuilderNode<[]> | null = null; 1649e41f4b71Sopenharmony_ci private wrapBuilder: WrappedBuilder<[]> = wrapBuilder(CardBuilder); 1650e41f4b71Sopenharmony_ci private needCreate: boolean = false; 1651e41f4b71Sopenharmony_ci private isRemove: boolean = false; 1652e41f4b71Sopenharmony_ci 1653e41f4b71Sopenharmony_ci constructor(create: boolean) { 1654e41f4b71Sopenharmony_ci super(); 1655e41f4b71Sopenharmony_ci this.needCreate = create; 1656e41f4b71Sopenharmony_ci } 1657e41f4b71Sopenharmony_ci 1658e41f4b71Sopenharmony_ci makeNode(uiContext: UIContext): FrameNode | null { 1659e41f4b71Sopenharmony_ci if(this.isRemove == true){ 1660e41f4b71Sopenharmony_ci return null; 1661e41f4b71Sopenharmony_ci } 1662e41f4b71Sopenharmony_ci if (this.needCreate && this.CardNode == null) { 1663e41f4b71Sopenharmony_ci this.CardNode = new BuilderNode(uiContext); 1664e41f4b71Sopenharmony_ci this.CardNode.build(this.wrapBuilder) 1665e41f4b71Sopenharmony_ci } 1666e41f4b71Sopenharmony_ci if (this.CardNode == null) { 1667e41f4b71Sopenharmony_ci return null; 1668e41f4b71Sopenharmony_ci } 1669e41f4b71Sopenharmony_ci return this.CardNode!.getFrameNode()!; 1670e41f4b71Sopenharmony_ci } 1671e41f4b71Sopenharmony_ci 1672e41f4b71Sopenharmony_ci getNode(): BuilderNode<[]> | null { 1673e41f4b71Sopenharmony_ci return this.CardNode; 1674e41f4b71Sopenharmony_ci } 1675e41f4b71Sopenharmony_ci 1676e41f4b71Sopenharmony_ci setNode(node: BuilderNode<[]> | null) { 1677e41f4b71Sopenharmony_ci this.CardNode = node; 1678e41f4b71Sopenharmony_ci this.rebuild(); 1679e41f4b71Sopenharmony_ci } 1680e41f4b71Sopenharmony_ci 1681e41f4b71Sopenharmony_ci onRemove() { 1682e41f4b71Sopenharmony_ci this.isRemove = true; 1683e41f4b71Sopenharmony_ci this.rebuild(); 1684e41f4b71Sopenharmony_ci this.isRemove = false; 1685e41f4b71Sopenharmony_ci } 1686e41f4b71Sopenharmony_ci 1687e41f4b71Sopenharmony_ci init(uiContext: UIContext) { 1688e41f4b71Sopenharmony_ci this.CardNode = new BuilderNode(uiContext); 1689e41f4b71Sopenharmony_ci this.CardNode.build(this.wrapBuilder) 1690e41f4b71Sopenharmony_ci } 1691e41f4b71Sopenharmony_ci} 1692e41f4b71Sopenharmony_ci 1693e41f4b71Sopenharmony_cilet myNode: MyNodeController | undefined; 1694e41f4b71Sopenharmony_ci 1695e41f4b71Sopenharmony_ciexport const createMyNode = 1696e41f4b71Sopenharmony_ci (uiContext: UIContext) => { 1697e41f4b71Sopenharmony_ci myNode = new MyNodeController(false); 1698e41f4b71Sopenharmony_ci myNode.init(uiContext); 1699e41f4b71Sopenharmony_ci } 1700e41f4b71Sopenharmony_ci 1701e41f4b71Sopenharmony_ciexport const getMyNode = (): MyNodeController | undefined => { 1702e41f4b71Sopenharmony_ci return myNode; 1703e41f4b71Sopenharmony_ci} 1704e41f4b71Sopenharmony_ci``` 1705e41f4b71Sopenharmony_ci 1706e41f4b71Sopenharmony_ci```ts 1707e41f4b71Sopenharmony_ci// ComponentAttrUtils.ets 1708e41f4b71Sopenharmony_ci// Obtain the position of the component relative to the window. 1709e41f4b71Sopenharmony_ciimport { componentUtils, UIContext } from '@kit.ArkUI'; 1710e41f4b71Sopenharmony_ciimport { JSON } from '@kit.ArkTS'; 1711e41f4b71Sopenharmony_ci 1712e41f4b71Sopenharmony_ciexport class ComponentAttrUtils { 1713e41f4b71Sopenharmony_ci // Obtain the position information of the component based on its ID. 1714e41f4b71Sopenharmony_ci public static getRectInfoById(context: UIContext, id: string): RectInfoInPx { 1715e41f4b71Sopenharmony_ci if (!context || !id) { 1716e41f4b71Sopenharmony_ci throw Error('object is empty'); 1717e41f4b71Sopenharmony_ci } 1718e41f4b71Sopenharmony_ci let componentInfo: componentUtils.ComponentInfo = context.getComponentUtils().getRectangleById(id); 1719e41f4b71Sopenharmony_ci 1720e41f4b71Sopenharmony_ci if (!componentInfo) { 1721e41f4b71Sopenharmony_ci throw Error('object is empty'); 1722e41f4b71Sopenharmony_ci } 1723e41f4b71Sopenharmony_ci 1724e41f4b71Sopenharmony_ci let rstRect: RectInfoInPx = new RectInfoInPx(); 1725e41f4b71Sopenharmony_ci const widthScaleGap = componentInfo.size.width * (1 - componentInfo.scale.x) / 2; 1726e41f4b71Sopenharmony_ci const heightScaleGap = componentInfo.size.height * (1 - componentInfo.scale.y) / 2; 1727e41f4b71Sopenharmony_ci rstRect.left = componentInfo.translate.x + componentInfo.windowOffset.x + widthScaleGap; 1728e41f4b71Sopenharmony_ci rstRect.top = componentInfo.translate.y + componentInfo.windowOffset.y + heightScaleGap; 1729e41f4b71Sopenharmony_ci rstRect.right = 1730e41f4b71Sopenharmony_ci componentInfo.translate.x + componentInfo.windowOffset.x + componentInfo.size.width - widthScaleGap; 1731e41f4b71Sopenharmony_ci rstRect.bottom = 1732e41f4b71Sopenharmony_ci componentInfo.translate.y + componentInfo.windowOffset.y + componentInfo.size.height - heightScaleGap; 1733e41f4b71Sopenharmony_ci rstRect.width = rstRect.right - rstRect.left; 1734e41f4b71Sopenharmony_ci rstRect.height = rstRect.bottom - rstRect.top; 1735e41f4b71Sopenharmony_ci return { 1736e41f4b71Sopenharmony_ci left: rstRect.left, 1737e41f4b71Sopenharmony_ci right: rstRect.right, 1738e41f4b71Sopenharmony_ci top: rstRect.top, 1739e41f4b71Sopenharmony_ci bottom: rstRect.bottom, 1740e41f4b71Sopenharmony_ci width: rstRect.width, 1741e41f4b71Sopenharmony_ci height: rstRect.height 1742e41f4b71Sopenharmony_ci } 1743e41f4b71Sopenharmony_ci } 1744e41f4b71Sopenharmony_ci} 1745e41f4b71Sopenharmony_ci 1746e41f4b71Sopenharmony_ciexport class RectInfoInPx { 1747e41f4b71Sopenharmony_ci left: number = 0; 1748e41f4b71Sopenharmony_ci top: number = 0; 1749e41f4b71Sopenharmony_ci right: number = 0; 1750e41f4b71Sopenharmony_ci bottom: number = 0; 1751e41f4b71Sopenharmony_ci width: number = 0; 1752e41f4b71Sopenharmony_ci height: number = 0; 1753e41f4b71Sopenharmony_ci} 1754e41f4b71Sopenharmony_ci 1755e41f4b71Sopenharmony_ciexport class RectJson { 1756e41f4b71Sopenharmony_ci $rect: Array<number> = []; 1757e41f4b71Sopenharmony_ci} 1758e41f4b71Sopenharmony_ci``` 1759e41f4b71Sopenharmony_ci 1760e41f4b71Sopenharmony_ci```ts 1761e41f4b71Sopenharmony_ci// WindowUtils.ets 1762e41f4b71Sopenharmony_ci// Window information 1763e41f4b71Sopenharmony_ciimport { window } from '@kit.ArkUI'; 1764e41f4b71Sopenharmony_ci 1765e41f4b71Sopenharmony_ciexport class WindowUtils { 1766e41f4b71Sopenharmony_ci public static window: window.Window; 1767e41f4b71Sopenharmony_ci public static windowWidth_px: number; 1768e41f4b71Sopenharmony_ci public static windowHeight_px: number; 1769e41f4b71Sopenharmony_ci public static topAvoidAreaHeight_px: number; 1770e41f4b71Sopenharmony_ci public static navigationIndicatorHeight_px: number; 1771e41f4b71Sopenharmony_ci} 1772e41f4b71Sopenharmony_ci``` 1773e41f4b71Sopenharmony_ci 1774e41f4b71Sopenharmony_ci```ts 1775e41f4b71Sopenharmony_ci// EntryAbility.ets 1776e41f4b71Sopenharmony_ci// Add capture of window width and height in onWindowStageCreate at the application entry. 1777e41f4b71Sopenharmony_ci 1778e41f4b71Sopenharmony_ciimport { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; 1779e41f4b71Sopenharmony_ciimport { hilog } from '@kit.PerformanceAnalysisKit'; 1780e41f4b71Sopenharmony_ciimport { display, window } from '@kit.ArkUI'; 1781e41f4b71Sopenharmony_ciimport { WindowUtils } from '../utils/WindowUtils'; 1782e41f4b71Sopenharmony_ci 1783e41f4b71Sopenharmony_ciconst TAG: string = 'EntryAbility'; 1784e41f4b71Sopenharmony_ci 1785e41f4b71Sopenharmony_ciexport default class EntryAbility extends UIAbility { 1786e41f4b71Sopenharmony_ci private currentBreakPoint: string = ''; 1787e41f4b71Sopenharmony_ci 1788e41f4b71Sopenharmony_ci onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 1789e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); 1790e41f4b71Sopenharmony_ci } 1791e41f4b71Sopenharmony_ci 1792e41f4b71Sopenharmony_ci onDestroy(): void { 1793e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); 1794e41f4b71Sopenharmony_ci } 1795e41f4b71Sopenharmony_ci 1796e41f4b71Sopenharmony_ci onWindowStageCreate(windowStage: window.WindowStage): void { 1797e41f4b71Sopenharmony_ci // Main window is created, set main page for this ability 1798e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); 1799e41f4b71Sopenharmony_ci 1800e41f4b71Sopenharmony_ci // Obtain the window width and height. 1801e41f4b71Sopenharmony_ci WindowUtils.window = windowStage.getMainWindowSync(); 1802e41f4b71Sopenharmony_ci WindowUtils.windowWidth_px = WindowUtils.window.getWindowProperties().windowRect.width; 1803e41f4b71Sopenharmony_ci WindowUtils.windowHeight_px = WindowUtils.window.getWindowProperties().windowRect.height; 1804e41f4b71Sopenharmony_ci 1805e41f4b71Sopenharmony_ci this.updateBreakpoint(WindowUtils.windowWidth_px); 1806e41f4b71Sopenharmony_ci 1807e41f4b71Sopenharmony_ci // Obtain the height of the upper avoid area (such as the status bar). 1808e41f4b71Sopenharmony_ci let avoidArea = WindowUtils.window.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); 1809e41f4b71Sopenharmony_ci WindowUtils.topAvoidAreaHeight_px = avoidArea.topRect.height; 1810e41f4b71Sopenharmony_ci 1811e41f4b71Sopenharmony_ci // Obtain the height of the navigation bar. 1812e41f4b71Sopenharmony_ci let navigationArea = WindowUtils.window.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); 1813e41f4b71Sopenharmony_ci WindowUtils.navigationIndicatorHeight_px = navigationArea.bottomRect.height; 1814e41f4b71Sopenharmony_ci 1815e41f4b71Sopenharmony_ci console.log(TAG, 'the width is ' + WindowUtils.windowWidth_px + ' ' + WindowUtils.windowHeight_px + ' ' + 1816e41f4b71Sopenharmony_ci WindowUtils.topAvoidAreaHeight_px + ' ' + WindowUtils.navigationIndicatorHeight_px); 1817e41f4b71Sopenharmony_ci 1818e41f4b71Sopenharmony_ci // Listen for changes in the window size, status bar height, and navigation bar height, and update accordingly. 1819e41f4b71Sopenharmony_ci try { 1820e41f4b71Sopenharmony_ci WindowUtils.window.on('windowSizeChange', (data) => { 1821e41f4b71Sopenharmony_ci console.log(TAG, 'on windowSizeChange, the width is ' + data.width + ', the height is ' + data.height); 1822e41f4b71Sopenharmony_ci WindowUtils.windowWidth_px = data.width; 1823e41f4b71Sopenharmony_ci WindowUtils.windowHeight_px = data.height; 1824e41f4b71Sopenharmony_ci this.updateBreakpoint(data.width); 1825e41f4b71Sopenharmony_ci AppStorage.setOrCreate('windowSizeChanged', Date.now()) 1826e41f4b71Sopenharmony_ci }) 1827e41f4b71Sopenharmony_ci 1828e41f4b71Sopenharmony_ci WindowUtils.window.on('avoidAreaChange', (data) => { 1829e41f4b71Sopenharmony_ci if (data.type == window.AvoidAreaType.TYPE_SYSTEM) { 1830e41f4b71Sopenharmony_ci let topRectHeight = data.area.topRect.height; 1831e41f4b71Sopenharmony_ci console.log(TAG, 'on avoidAreaChange, the top avoid area height is ' + topRectHeight); 1832e41f4b71Sopenharmony_ci WindowUtils.topAvoidAreaHeight_px = topRectHeight; 1833e41f4b71Sopenharmony_ci } else if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) { 1834e41f4b71Sopenharmony_ci let bottomRectHeight = data.area.bottomRect.height; 1835e41f4b71Sopenharmony_ci console.log(TAG, 'on avoidAreaChange, the navigation indicator height is ' + bottomRectHeight); 1836e41f4b71Sopenharmony_ci WindowUtils.navigationIndicatorHeight_px = bottomRectHeight; 1837e41f4b71Sopenharmony_ci } 1838e41f4b71Sopenharmony_ci }) 1839e41f4b71Sopenharmony_ci } catch (exception) { 1840e41f4b71Sopenharmony_ci console.log('register failed ' + JSON.stringify(exception)); 1841e41f4b71Sopenharmony_ci } 1842e41f4b71Sopenharmony_ci 1843e41f4b71Sopenharmony_ci windowStage.loadContent('pages/Index', (err) => { 1844e41f4b71Sopenharmony_ci if (err.code) { 1845e41f4b71Sopenharmony_ci hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); 1846e41f4b71Sopenharmony_ci return; 1847e41f4b71Sopenharmony_ci } 1848e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); 1849e41f4b71Sopenharmony_ci }); 1850e41f4b71Sopenharmony_ci } 1851e41f4b71Sopenharmony_ci 1852e41f4b71Sopenharmony_ci updateBreakpoint(width: number) { 1853e41f4b71Sopenharmony_ci let windowWidthVp = width / (display.getDefaultDisplaySync().densityDPI / 160); 1854e41f4b71Sopenharmony_ci let newBreakPoint: string = ''; 1855e41f4b71Sopenharmony_ci if (windowWidthVp < 400) { 1856e41f4b71Sopenharmony_ci newBreakPoint = 'xs'; 1857e41f4b71Sopenharmony_ci } else if (windowWidthVp < 600) { 1858e41f4b71Sopenharmony_ci newBreakPoint = 'sm'; 1859e41f4b71Sopenharmony_ci } else if (windowWidthVp < 800) { 1860e41f4b71Sopenharmony_ci newBreakPoint = 'md'; 1861e41f4b71Sopenharmony_ci } else { 1862e41f4b71Sopenharmony_ci newBreakPoint = 'lg'; 1863e41f4b71Sopenharmony_ci } 1864e41f4b71Sopenharmony_ci if (this.currentBreakPoint !== newBreakPoint) { 1865e41f4b71Sopenharmony_ci this.currentBreakPoint = newBreakPoint; 1866e41f4b71Sopenharmony_ci // Use the state variable to record the current breakpoint value. 1867e41f4b71Sopenharmony_ci AppStorage.setOrCreate('currentBreakpoint', this.currentBreakPoint); 1868e41f4b71Sopenharmony_ci } 1869e41f4b71Sopenharmony_ci } 1870e41f4b71Sopenharmony_ci 1871e41f4b71Sopenharmony_ci onWindowStageDestroy(): void { 1872e41f4b71Sopenharmony_ci // Main window is destroyed, release UI related resources 1873e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); 1874e41f4b71Sopenharmony_ci } 1875e41f4b71Sopenharmony_ci 1876e41f4b71Sopenharmony_ci onForeground(): void { 1877e41f4b71Sopenharmony_ci // Ability has brought to foreground 1878e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); 1879e41f4b71Sopenharmony_ci } 1880e41f4b71Sopenharmony_ci 1881e41f4b71Sopenharmony_ci onBackground(): void { 1882e41f4b71Sopenharmony_ci // Ability has back to background 1883e41f4b71Sopenharmony_ci hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); 1884e41f4b71Sopenharmony_ci } 1885e41f4b71Sopenharmony_ci} 1886e41f4b71Sopenharmony_ci``` 1887e41f4b71Sopenharmony_ci 1888e41f4b71Sopenharmony_ci 1889e41f4b71Sopenharmony_ci 1890e41f4b71Sopenharmony_ci## Using geometryTransition 1891e41f4b71Sopenharmony_ci 1892e41f4b71Sopenharmony_ci[geometryTransition](../reference/apis-arkui/arkui-ts/ts-transition-animation-geometrytransition.md) facilitates implicit shared element transitions within components, offering a smooth transition experience during view state changes. 1893e41f4b71Sopenharmony_ci 1894e41f4b71Sopenharmony_ciTo use **geometryTransition**, assign the same ID to both components that require the shared element transition. This sets up a seamless animation between them as one component disappears and the other appears. 1895e41f4b71Sopenharmony_ci 1896e41f4b71Sopenharmony_ciThis method is ideal for shared element transitions between two distinct objects. 1897e41f4b71Sopenharmony_ci 1898e41f4b71Sopenharmony_ci### Simple Use of geometryTransition 1899e41f4b71Sopenharmony_ci 1900e41f4b71Sopenharmony_ciBelow is a simple example of using **geometryTransition** to implement shared element transition for two elements on the same page: 1901e41f4b71Sopenharmony_ci 1902e41f4b71Sopenharmony_ci```ts 1903e41f4b71Sopenharmony_ciimport { curves } from '@kit.ArkUI'; 1904e41f4b71Sopenharmony_ci 1905e41f4b71Sopenharmony_ci@Entry 1906e41f4b71Sopenharmony_ci@Component 1907e41f4b71Sopenharmony_cistruct IfElseGeometryTransition { 1908e41f4b71Sopenharmony_ci @State isShow: boolean = false; 1909e41f4b71Sopenharmony_ci 1910e41f4b71Sopenharmony_ci build() { 1911e41f4b71Sopenharmony_ci Stack({ alignContent: Alignment.Center }) { 1912e41f4b71Sopenharmony_ci if (this.isShow) { 1913e41f4b71Sopenharmony_ci Image($r('app.media.spring')) 1914e41f4b71Sopenharmony_ci .autoResize(false) 1915e41f4b71Sopenharmony_ci .clip(true) 1916e41f4b71Sopenharmony_ci .width(200) 1917e41f4b71Sopenharmony_ci .height(200) 1918e41f4b71Sopenharmony_ci .borderRadius(100) 1919e41f4b71Sopenharmony_ci .geometryTransition("picture") 1920e41f4b71Sopenharmony_ci .transition(TransitionEffect.OPACITY) 1921e41f4b71Sopenharmony_ci // If a new transition is triggered during the animation, ghosting occurs when id is not specified. 1922e41f4b71Sopenharmony_ci // With id specified, the new spring image reuses the previous spring image node instead of creating a new node. Therefore, ghosting does not occur. 1923e41f4b71Sopenharmony_ci // id needs to be added to the first node under if and else. If there are multiple parallel nodes, id needs to be added for all of them. 1924e41f4b71Sopenharmony_ci .id('item1') 1925e41f4b71Sopenharmony_ci } else { 1926e41f4b71Sopenharmony_ci // geometryTransition is bound to a container. Therefore, a relative layout must be configured for the child components of the container. 1927e41f4b71Sopenharmony_ci // The multiple levels of containers here are used to demonstrate passing of relative layout constraints. 1928e41f4b71Sopenharmony_ci Column() { 1929e41f4b71Sopenharmony_ci Column() { 1930e41f4b71Sopenharmony_ci Image($r('app.media.sky')) 1931e41f4b71Sopenharmony_ci .size({ width: '100%', height: '100%' }) 1932e41f4b71Sopenharmony_ci } 1933e41f4b71Sopenharmony_ci .size({ width: '100%', height: '100%' }) 1934e41f4b71Sopenharmony_ci } 1935e41f4b71Sopenharmony_ci .width(100) 1936e41f4b71Sopenharmony_ci .height(100) 1937e41f4b71Sopenharmony_ci // geometryTransition synchronizes rounded corner settings, but only for the bound component, which is the container in this example. 1938e41f4b71Sopenharmony_ci // In other words, rounded corner settings of the container are synchronized, and those of the child components are not. 1939e41f4b71Sopenharmony_ci .borderRadius(50) 1940e41f4b71Sopenharmony_ci .clip(true) 1941e41f4b71Sopenharmony_ci .geometryTransition("picture") 1942e41f4b71Sopenharmony_ci // transition ensures that the component is not destroyed immediately when it exits. You can customize the transition effect. 1943e41f4b71Sopenharmony_ci .transition(TransitionEffect.OPACITY) 1944e41f4b71Sopenharmony_ci .position({ x: 40, y: 40 }) 1945e41f4b71Sopenharmony_ci .id('item2') 1946e41f4b71Sopenharmony_ci } 1947e41f4b71Sopenharmony_ci } 1948e41f4b71Sopenharmony_ci .onClick(() => { 1949e41f4b71Sopenharmony_ci animateTo({ 1950e41f4b71Sopenharmony_ci curve: curves.springMotion() 1951e41f4b71Sopenharmony_ci }, () => { 1952e41f4b71Sopenharmony_ci this.isShow = !this.isShow; 1953e41f4b71Sopenharmony_ci }) 1954e41f4b71Sopenharmony_ci }) 1955e41f4b71Sopenharmony_ci .size({ width: '100%', height: '100%' }) 1956e41f4b71Sopenharmony_ci } 1957e41f4b71Sopenharmony_ci} 1958e41f4b71Sopenharmony_ci``` 1959e41f4b71Sopenharmony_ci 1960e41f4b71Sopenharmony_ci 1961e41f4b71Sopenharmony_ci 1962e41f4b71Sopenharmony_ci### Combining geometryTransition with Modal Transition 1963e41f4b71Sopenharmony_ci 1964e41f4b71Sopenharmony_ciBy combining **geometryTransition** with a modal transition API, you can implement a shared element transition between two elements on different pages. The following example implements a demo where clicking a profile picture displays the corresponding profile page. 1965e41f4b71Sopenharmony_ci 1966e41f4b71Sopenharmony_ci```ts 1967e41f4b71Sopenharmony_ciclass PostData { 1968e41f4b71Sopenharmony_ci avatar: Resource = $r('app.media.flower'); 1969e41f4b71Sopenharmony_ci name: string = ''; 1970e41f4b71Sopenharmony_ci message: string = ''; 1971e41f4b71Sopenharmony_ci images: Resource[] = []; 1972e41f4b71Sopenharmony_ci} 1973e41f4b71Sopenharmony_ci 1974e41f4b71Sopenharmony_ci@Entry 1975e41f4b71Sopenharmony_ci@Component 1976e41f4b71Sopenharmony_cistruct Index { 1977e41f4b71Sopenharmony_ci @State isPersonalPageShow: boolean = false; 1978e41f4b71Sopenharmony_ci @State selectedIndex: number = 0; 1979e41f4b71Sopenharmony_ci @State alphaValue: number = 1; 1980e41f4b71Sopenharmony_ci 1981e41f4b71Sopenharmony_ci private allPostData: PostData[] = [ 1982e41f4b71Sopenharmony_ci { avatar: $r('app.media.flower'), name: 'Alice', message: 'It's sunny.', 1983e41f4b71Sopenharmony_ci images: [$r('app.media.spring'), $r('app.media.tree')] }, 1984e41f4b71Sopenharmony_ci { avatar: $r('app.media.sky'), name: 'Bob', message: 'Hello World', 1985e41f4b71Sopenharmony_ci images: [$r('app.media.island')] }, 1986e41f4b71Sopenharmony_ci { avatar: $r('app.media.tree'), name: 'Carl', message: 'Everything grows.', 1987e41f4b71Sopenharmony_ci images: [$r('app.media.flower'), $r('app.media.sky'), $r('app.media.spring')] }]; 1988e41f4b71Sopenharmony_ci 1989e41f4b71Sopenharmony_ci private onAvatarClicked(index: number): void { 1990e41f4b71Sopenharmony_ci this.selectedIndex = index; 1991e41f4b71Sopenharmony_ci animateTo({ 1992e41f4b71Sopenharmony_ci duration: 350, 1993e41f4b71Sopenharmony_ci curve: Curve.Friction 1994e41f4b71Sopenharmony_ci }, () => { 1995e41f4b71Sopenharmony_ci this.isPersonalPageShow = !this.isPersonalPageShow; 1996e41f4b71Sopenharmony_ci this.alphaValue = 0; 1997e41f4b71Sopenharmony_ci }); 1998e41f4b71Sopenharmony_ci } 1999e41f4b71Sopenharmony_ci 2000e41f4b71Sopenharmony_ci private onPersonalPageBack(index: number): void { 2001e41f4b71Sopenharmony_ci animateTo({ 2002e41f4b71Sopenharmony_ci duration: 350, 2003e41f4b71Sopenharmony_ci curve: Curve.Friction 2004e41f4b71Sopenharmony_ci }, () => { 2005e41f4b71Sopenharmony_ci this.isPersonalPageShow = !this.isPersonalPageShow; 2006e41f4b71Sopenharmony_ci this.alphaValue = 1; 2007e41f4b71Sopenharmony_ci }); 2008e41f4b71Sopenharmony_ci } 2009e41f4b71Sopenharmony_ci 2010e41f4b71Sopenharmony_ci @Builder 2011e41f4b71Sopenharmony_ci PersonalPageBuilder(index: number) { 2012e41f4b71Sopenharmony_ci Column({ space: 20 }) { 2013e41f4b71Sopenharmony_ci Image(this.allPostData[index].avatar) 2014e41f4b71Sopenharmony_ci .size({ width: 200, height: 200 }) 2015e41f4b71Sopenharmony_ci .borderRadius(100) 2016e41f4b71Sopenharmony_ci // Apply a shared element transition to the profile picture by its ID. 2017e41f4b71Sopenharmony_ci .geometryTransition(index.toString()) 2018e41f4b71Sopenharmony_ci .clip(true) 2019e41f4b71Sopenharmony_ci .transition(TransitionEffect.opacity(0.99)) 2020e41f4b71Sopenharmony_ci 2021e41f4b71Sopenharmony_ci Text(this.allPostData[index].name) 2022e41f4b71Sopenharmony_ci .font({ size: 30, weight: 600 }) 2023e41f4b71Sopenharmony_ci // Apply a transition effect to the text. 2024e41f4b71Sopenharmony_ci .transition(TransitionEffect.asymmetric( 2025e41f4b71Sopenharmony_ci TransitionEffect.OPACITY 2026e41f4b71Sopenharmony_ci .combine(TransitionEffect.translate({ y: 100 })), 2027e41f4b71Sopenharmony_ci TransitionEffect.OPACITY.animation({ duration: 0 }) 2028e41f4b71Sopenharmony_ci )) 2029e41f4b71Sopenharmony_ci 2030e41f4b71Sopenharmony_ci Text('Hello, this is' + this.allPostData[index].name) 2031e41f4b71Sopenharmony_ci // Apply a transition effect to the text. 2032e41f4b71Sopenharmony_ci .transition(TransitionEffect.asymmetric( 2033e41f4b71Sopenharmony_ci TransitionEffect.OPACITY 2034e41f4b71Sopenharmony_ci .combine(TransitionEffect.translate({ y: 100 })), 2035e41f4b71Sopenharmony_ci TransitionEffect.OPACITY.animation({ duration: 0 }) 2036e41f4b71Sopenharmony_ci )) 2037e41f4b71Sopenharmony_ci } 2038e41f4b71Sopenharmony_ci .padding({ top: 20 }) 2039e41f4b71Sopenharmony_ci .size({ width: 360, height: 780 }) 2040e41f4b71Sopenharmony_ci .backgroundColor(Color.White) 2041e41f4b71Sopenharmony_ci .onClick(() => { 2042e41f4b71Sopenharmony_ci this.onPersonalPageBack(index); 2043e41f4b71Sopenharmony_ci }) 2044e41f4b71Sopenharmony_ci .transition(TransitionEffect.asymmetric( 2045e41f4b71Sopenharmony_ci TransitionEffect.opacity(0.99), 2046e41f4b71Sopenharmony_ci TransitionEffect.OPACITY 2047e41f4b71Sopenharmony_ci )) 2048e41f4b71Sopenharmony_ci } 2049e41f4b71Sopenharmony_ci 2050e41f4b71Sopenharmony_ci build() { 2051e41f4b71Sopenharmony_ci Column({ space: 20 }) { 2052e41f4b71Sopenharmony_ci ForEach(this.allPostData, (postData: PostData, index: number) => { 2053e41f4b71Sopenharmony_ci Column() { 2054e41f4b71Sopenharmony_ci Post({ data: postData, index: index, onAvatarClicked: (index: number) => { this.onAvatarClicked(index) } }) 2055e41f4b71Sopenharmony_ci } 2056e41f4b71Sopenharmony_ci .width('100%') 2057e41f4b71Sopenharmony_ci }, (postData: PostData, index: number) => index.toString()) 2058e41f4b71Sopenharmony_ci } 2059e41f4b71Sopenharmony_ci .size({ width: '100%', height: '100%' }) 2060e41f4b71Sopenharmony_ci .backgroundColor('#40808080') 2061e41f4b71Sopenharmony_ci .bindContentCover(this.isPersonalPageShow, 2062e41f4b71Sopenharmony_ci this.PersonalPageBuilder(this.selectedIndex), { modalTransition: ModalTransition.NONE }) 2063e41f4b71Sopenharmony_ci .opacity(this.alphaValue) 2064e41f4b71Sopenharmony_ci } 2065e41f4b71Sopenharmony_ci} 2066e41f4b71Sopenharmony_ci 2067e41f4b71Sopenharmony_ci@Component 2068e41f4b71Sopenharmony_ciexport default struct Post { 2069e41f4b71Sopenharmony_ci @Prop data: PostData; 2070e41f4b71Sopenharmony_ci @Prop index: number; 2071e41f4b71Sopenharmony_ci 2072e41f4b71Sopenharmony_ci @State expandImageSize: number = 100; 2073e41f4b71Sopenharmony_ci @State avatarSize: number = 50; 2074e41f4b71Sopenharmony_ci 2075e41f4b71Sopenharmony_ci private onAvatarClicked: (index: number) => void = (index: number) => { }; 2076e41f4b71Sopenharmony_ci 2077e41f4b71Sopenharmony_ci build() { 2078e41f4b71Sopenharmony_ci Column({ space: 20 }) { 2079e41f4b71Sopenharmony_ci Row({ space: 10 }) { 2080e41f4b71Sopenharmony_ci Image(this.data.avatar) 2081e41f4b71Sopenharmony_ci .size({ width: this.avatarSize, height: this.avatarSize }) 2082e41f4b71Sopenharmony_ci .borderRadius(this.avatarSize / 2) 2083e41f4b71Sopenharmony_ci .clip(true) 2084e41f4b71Sopenharmony_ci .onClick(() => { 2085e41f4b71Sopenharmony_ci this.onAvatarClicked(this.index); 2086e41f4b71Sopenharmony_ci }) 2087e41f4b71Sopenharmony_ci // ID of the shared element transition bound to the profile picture. 2088e41f4b71Sopenharmony_ci .geometryTransition(this.index.toString(), {follow:true}) 2089e41f4b71Sopenharmony_ci .transition(TransitionEffect.OPACITY.animation({ duration: 350, curve: Curve.Friction })) 2090e41f4b71Sopenharmony_ci 2091e41f4b71Sopenharmony_ci Text(this.data.name) 2092e41f4b71Sopenharmony_ci } 2093e41f4b71Sopenharmony_ci .justifyContent(FlexAlign.Start) 2094e41f4b71Sopenharmony_ci 2095e41f4b71Sopenharmony_ci Text(this.data.message) 2096e41f4b71Sopenharmony_ci 2097e41f4b71Sopenharmony_ci Row({ space: 15 }) { 2098e41f4b71Sopenharmony_ci ForEach(this.data.images, (imageResource: Resource, index: number) => { 2099e41f4b71Sopenharmony_ci Image(imageResource) 2100e41f4b71Sopenharmony_ci .size({ width: 100, height: 100 }) 2101e41f4b71Sopenharmony_ci }, (imageResource: Resource, index: number) => index.toString()) 2102e41f4b71Sopenharmony_ci } 2103e41f4b71Sopenharmony_ci } 2104e41f4b71Sopenharmony_ci .backgroundColor(Color.White) 2105e41f4b71Sopenharmony_ci .size({ width: '100%', height: 250 }) 2106e41f4b71Sopenharmony_ci .alignItems(HorizontalAlign.Start) 2107e41f4b71Sopenharmony_ci .padding({ left: 10, top: 10 }) 2108e41f4b71Sopenharmony_ci } 2109e41f4b71Sopenharmony_ci} 2110e41f4b71Sopenharmony_ci``` 2111e41f4b71Sopenharmony_ci 2112e41f4b71Sopenharmony_ciAfter a profile picture on the home page is clicked, the corresponding profile page is displayed in a modal, and there is a shared element transition between the profile pictures on the two pages. 2113e41f4b71Sopenharmony_ci 2114e41f4b71Sopenharmony_ci 2115e41f4b71Sopenharmony_ci 2116