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