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