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