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