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