1# Shared Element Transition 2 3 4Shared 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. It is typically used with the modal transition for combined effects. When used with transition and attribute animations, it can make its way into a wider range of use cases. 5 6 7## Implementation with transition and Attribute Animation 8 9This 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, attribute 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. 10 111. 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. 12 13 ```ts 14 class Tmp{ 15 set(item:CradData):CradData{ 16 return item 17 } 18 } 19 // Build two pages for the normal and expanded states of the same component, which are then used based on the declared state variables. 20 @Component 21 export struct MyExtendView { 22 // Declare the isExpand variable to be synced with the parent component. 23 @Link isExpand: boolean; 24 @State cardList: Array<CardData> = xxxx; 25 26 build() { 27 List() { 28 // Customize the expanded component as required. 29 if (this.isExpand) { 30 Text('expand') 31 .transition(TransitionEffect.translate(y:300).animation({ curve: curves.springMotion(0.6, 0.8) })) 32 } 33 34 ForEach(this.cardList, (item: CradData) => { 35 let Item:Tmp = new Tmp() 36 let Imp:Tmp = Item.set(item) 37 let Mc:Record<string,Tmp> = {'cardData':Imp} 38 MyCard(Mc) 39 }) 40 } 41 .width(this.isExpand ? 200 : 500) // Define the attributes of the expanded component. 42 .animation({ curve: curves.springMotion()}) // Bind an animation to component attributes. 43 } 44 } 45 ``` 46 472. 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. 48 49 ```ts 50 class Tmp{ 51 isExpand: boolean = false; 52 set(){ 53 this.isExpand = !this.isExpand; 54 } 55 } 56 let Exp:Record<string,boolean> = {'isExpand': false} 57 @State isExpand: boolean = false 58 59 ... 60 List() { 61 // Control the appearance or disappearance of sibling components through the isExpand variable, and configure the enter/exit transition. 62 if (!this.isExpand) { 63 Text ('Collapse') 64 .transition(TransitionEffect.translate(y:300).animation({ curve: curves.springMotion(0.6, 0.9) })) 65 } 66 67 MyExtendView(Exp) 68 .onClick(() => { 69 let Epd:Tmp = new Tmp() 70 Epd.set() 71 }) 72 73 // Control the appearance or disappearance of sibling components through the isExpand variable, and configure the enter/exit transition. 74 if (this.isExpand) { 75 Text ('Expand') 76 .transition(TransitionEffect.translate(y:300).animation({ curve: curves.springMotion() })) 77 } 78 } 79 ... 80 ``` 81 82 83Below is the complete sample code and effect. 84 85 86 87```ts 88// utils.ets 89import curves from '@ohos.curves'; 90 91// Build two pages for the normal and expanded states of the same component, which are then used based on the declared state variables. 92@Component 93export struct share_transition_expand { 94 // Declare the isExpand variable to be synced with the parent component. 95 // Expand the component. 96 @State isExpand: boolean = false; 97 // Currently expanded component. 98 @State curIndex: number = 0; 99 @State listArray: Array<number> = [1, 2, 3, 4, 5, 6]; 100 build() { 101 Column() { 102 List() { 103 ForEach(this.listArray, (item:number, index?:number|undefined) => { 104 // Customize the expanded component as required. 105 if (!this.isExpand || this.curIndex == index) { 106 ListItem() { 107 Column() { 108 Row() { 109 Row() 110 .backgroundColor(Color.Pink) 111 .borderRadius(20) 112 .width(80) 113 .height(80) 114 115 Column() { 116 Text ('Click to expand Item' + item) 117 .fontSize(20) 118 Text ('Shared element transition') 119 .fontSize(12) 120 .fontColor(0x909399) 121 } 122 .alignItems(HorizontalAlign.Start) 123 .justifyContent(FlexAlign.SpaceAround) 124 .margin({ left: 10 }) 125 .height(80) 126 } 127 .width('90%') 128 .height(100) 129 130 if (this.isExpand) { 131 Row() { 132 Text('Expanded state') 133 .fontSize(28) 134 .fontColor(0x909399) 135 .textAlign(TextAlign.Center) 136 .transition(TransitionEffect.OPACITY.animation({ curve: curves.springMotion(0.6, 0.9) })) 137 } 138 .width('90%') 139 .justifyContent(FlexAlign.Center) 140 } 141 } 142 .onClick(() => { 143 // Define the animation parameters for expanding and collapsing. 144 animateTo({ curve: curves.springMotion(0.6, 0.9) }, () => { 145 if(index){ 146 this.curIndex = index; 147 } 148 this.isExpand = !this.isExpand; 149 }) 150 }) 151 .width('90%') 152 .height(this.isExpand && this.curIndex == index ? '85%' : 100) // Define the attributes of the expanded component as required. 153 .alignItems(HorizontalAlign.Center) 154 .borderRadius(10) 155 .margin({ top: 15 }) 156 .backgroundColor(Color.White) 157 .shadow({ radius: 20, color: 0x909399, offsetX: 20, offsetY: 10 }) 158 // Control the appearance or disappearance of sibling components through the isExpand variable, and configure the enter/exit transition. 159 .transition(TransitionEffect.scale({ x: 0, y: 0 }).animation({ curve: curves.springMotion(0.3, 1.5) })) 160 } 161 .zIndex(this.curIndex == index ? 1 : 0) 162 } 163 }) 164 } 165 .height('100%') 166 .alignListItem(ListItemAlign.Center) 167 } 168 .width('100%') 169 .height('100%') 170 .justifyContent(FlexAlign.Start) 171 } 172} 173``` 174 175 176 177```ts 178// Index.ets 179import { share_transition_expand } from './utils'; 180@Entry 181@Component 182struct ShareTransitionDemo { 183 @State isExpand: boolean = false; 184 @State Tmp:Record<string,boolean> = { 'isExpand': false } 185 private scroller: Scroller = new Scroller(); 186 build() { 187 Scroll(this.scroller) { 188 Column() { 189 Text('Sibling nodes appear and disappear.') 190 .fontWeight(FontWeight.Bold) 191 .fontSize(30) 192 .fontColor(Color.Black) 193 .margin(10) 194 195 share_transition_expand(this.Tmp) 196 197 } 198 .width('100%') 199 .height('100%') 200 .justifyContent(FlexAlign.Start) 201 } 202 } 203} 204``` 205 206 207 208 209 210 211## Implementation with transition and zIndex 212 213This example implements a shared element transition for the scenario where, as a component is expanded, it is displayed on the top of the container while sibling components in the same container stay. This is achieved with the use of **zIndex**. Specifically: 214 215- Build two pages for the normal and expanded states of the same component, which are then used based on the declared state variables. 216 217- Change the display level of components through the **zIndex** attribute. Set this attribute to **1** for the component in the expanded state and retain the default value **0** for other sibling components. In this way, the component in the expanded state will be displayed over the sibling components. 218 219- With the **translate** attribute, move the component to the top of the parent container when it is expanded. 220 221- Use a placeholder container so that the location of the sibling components remains unchanged. The outer container is placed as a placeholder, and the internal container changes the size. 222 223Below is the complete sample code and effect. 224 225 226```ts 227// utils.ets 228import curves from '@ohos.curves'; 229 230// Build two pages for the normal and expanded states of the same component, which are then used based on the declared state variables. 231@Component 232export struct share_zIndex_expand { 233 // Declare the isExpand variable to be synced with the parent component. 234 @State isExpand: boolean = false; 235 @State curIndex: number = 0; 236 @State listArray: Array<number> = [1, 2, 3, 4, 5, 6]; 237 private parentScroller: Scroller = new Scroller(); // Upper-layer scroller 238 239 build() { 240 Column() { 241 List() { 242 ForEach(this.listArray, (item:number, index?:number|undefined) => { 243 // Customize the expanded component as required. 244 if (!this.isExpand || this.curIndex == index) { 245 ListItem() { 246 Column() { 247 Row() { 248 Row() 249 .backgroundColor(Color.Pink) 250 .borderRadius(20) 251 .width(80) 252 .height(80) 253 254 Column() { 255 Text ('Click to expand Item' + item) 256 .fontSize(20) 257 Text ('Shared element transition') 258 .fontSize(12) 259 .fontColor(0x909399) 260 } 261 .alignItems(HorizontalAlign.Start) 262 .justifyContent(FlexAlign.SpaceAround) 263 .margin({ left: 10 }) 264 .height(80) 265 } 266 .width('90%') 267 .height(100) 268 269 if (this.isExpand && this.curIndex == index) { 270 Row() { 271 Text('Expanded state') 272 .fontSize(28) 273 .fontColor(0x909399) 274 .textAlign(TextAlign.Center) 275 .transition(TransitionEffect.OPACITY.animation({ curve: curves.springMotion(0.6, 0.9) })) 276 } 277 .width('90%') 278 .justifyContent(FlexAlign.Center) 279 } 280 } 281 .width('90%') 282 .height(this.isExpand && this.curIndex == index ? 750 : 100) 283 .alignItems(HorizontalAlign.Center) 284 .borderRadius(10) 285 .margin({ top: 15 }) 286 .backgroundColor(Color.White) 287 .shadow({ radius: 20, color: 0x909399, offsetX: 20, offsetY: 10 }) 288 } 289 .onClick(() => { 290 // Define the animation parameters for expanding and collapsing. 291 animateTo({ curve: curves.springMotion(0.6, 0.9) }, () => { 292 if(index){ 293 this.curIndex = index; 294 } 295 this.isExpand = !this.isExpand; 296 }) 297 }) 298 .zIndex(this.curIndex == index? 1: 0) // When the current list item is selected, its zIndex attribute is set to 1, and it is displayed over other sibling components whose zIndex is 0. 299 .translate({ // Move the list item to the top of the parent container through translate. 300 y: this.isExpand && this.curIndex == index ? -60 - this.parentScroller.currentOffset()['yOffset'] : 0 301 }) 302 } 303 }) 304 } 305 .clip(false) 306 .height('100%') // Fixed size of the placeholder container. 307 .alignListItem(ListItemAlign.Center) 308 } 309 .zIndex(1) 310 .width('100%') 311 .height('100%') 312 .justifyContent(FlexAlign.Start) 313 } 314} 315``` 316 317 318```ts 319// Index.ets 320import { share_zIndex_expand } from './utils' 321@Entry 322@Component 323struct ShareZIndexDemo { 324 @State isExpand: boolean = false; 325 @State curIndex: number = 0; 326 private scroller: Scroller = new Scroller(); 327 @State Sze:Record<string,boolean|number|Scroller> = { 'isExpand': this.isExpand, 'curIndex': this.curIndex, 'parentScroller': this.scroller } 328 329 build() { 330 Scroll(this.scroller) { 331 Column() { 332 Text ('zIndex changes z-axis') 333 .fontWeight(FontWeight.Bold) 334 .fontSize(30) 335 .fontColor(Color.Black) 336 .zIndex(0) 337 .margin(10) 338 339 share_zIndex_expand(this.Sze) 340 } 341 .width('100%') 342 .height('100%') 343 .justifyContent(FlexAlign.Start) 344 } 345 } 346} 347``` 348 349 350 351 352## Implementation with geometryTransition 353 354This example implements a shared element transition with [geometryTransition](../reference/arkui-ts/ts-transition-animation-geometrytransition.md), which is used for implicit shared element transitions during component switching. 355 356Below is the complete sample code and effect for using **geometryTransition** and the **if/else** syntax to implement a shared element transition: 357 358 359```ts 360@Entry 361@Component 362struct IfElseGeometryTransition { 363 @State isShow: boolean = false; 364 365 build() { 366 Stack({ alignContent: Alignment.Center }) { 367 if (this.isShow) { 368 Image($r('app.media.test')) 369 .autoResize(false) 370 .clip(true) 371 .width(300) 372 .height(400) 373 .offset({ y: 100 }) 374 .geometryTransition("picture") 375 .transition(TransitionEffect.OPACITY) 376 } else { 377 // geometryTransition is bound to a container. Therefore, a relative layout must be configured for the child components of the container. 378 // The multiple levels of containers here are used to demonstrate passing of relative layout constraints. 379 Column() { 380 Column() { 381 Image($r('app.media.icon')) 382 .width('100%').height('100%') 383 }.width('100%').height('100%') 384 } 385 .width(80) 386 .height(80) 387 // geometryTransition synchronizes rounded corner settings, but only for the bound component, which is the container in this example. 388 // In other words, rounded corner settings of the container are synchronized, and those of the child components are not. 389 .borderRadius(20) 390 .clip(true) 391 .geometryTransition("picture") 392 // transition ensures that the component is not destructed immediately when it exits. You can customize the transition effect. 393 .transition(TransitionEffect.OPACITY) 394 } 395 } 396 .onClick(() => { 397 animateTo({ duration: 1000 }, () => { 398 this.isShow = !this.isShow; 399 }) 400 }) 401 } 402} 403``` 404 405 406 407Below is the sample code and effect for using **geometryTransition** and a modal transition API to implement a shared element transition: 408 409 410```ts 411import curves from '@ohos.curves'; 412 413@Entry 414@Component 415struct GeometryTransitionDemo { 416 // Define the state variable used to control modal transition. 417 @State isPresent: boolean = false; 418 419 // Use @Builder to build the modal. 420 @Builder 421 MyBuilder() { 422 Column() { 423 Text(this.isPresent ? 'Page 2' : 'Page 1') 424 .fontWeight(FontWeight.Bold) 425 .fontSize(30) 426 .fontColor(Color.Black) 427 .margin(20) 428 429 Row() { 430 Text('Shared component 1') 431 .fontWeight(FontWeight.Bold) 432 .fontSize(20) 433 .fontColor(Color.White) 434 } 435 .justifyContent(FlexAlign.Center) 436 .borderRadius(10) 437 .backgroundColor(0xf56c6c) 438 .width('100%') 439 .aspectRatio(1) 440 .margin({ bottom: 20 }) 441 // New shared element, <Row, whose ID is share1. 442 .geometryTransition('share1') 443 444 Column() { 445 Text ('Expanded page') 446 .textAlign(TextAlign.Center) 447 .fontSize(15) 448 .fontColor(this.isPresent ? Color.White : Color.Transparent) 449 .margin(20) 450 451 Text('Click anywhere to return') 452 .textAlign(TextAlign.Center) 453 .fontSize(15) 454 .fontColor(this.isPresent ? Color.White : Color.Transparent) 455 } 456 .width('100%') 457 .transition(TransitionEffect.OPACITY.animation({ curve: curves.springMotion(0.6, 1.2) })) 458 459 } 460 .width('100%') 461 .height('100%') 462 .justifyContent(FlexAlign.Start) 463 .transition(TransitionEffect.opacity(0.99)) 464 .backgroundColor(this.isPresent ? 0x909399 : Color.Transparent) 465 .clip(true) 466 .onClick(() => { 467 animateTo({ duration: 1000 }, () => { 468 this.isPresent = !this.isPresent; 469 }) 470 }) 471 } 472 473 build() { 474 Column() { 475 Text('Page 1') 476 .fontWeight(FontWeight.Bold) 477 .fontSize(30) 478 .fontColor(Color.Black) 479 .margin(20) 480 481 Row() { 482 Text('Shared component 1') 483 .fontWeight(FontWeight.Bold) 484 .fontSize(20) 485 .fontColor(Color.White) 486 } 487 .justifyContent(FlexAlign.Center) 488 .borderRadius(10) 489 .backgroundColor(0xf56c6c) 490 .width(150) 491 .height(150) 492 .margin(20) 493 // Modal transition component 494 .bindContentCover(this.isPresent, this.MyBuilder, ModalTransition.NONE) 495 // The <Row> component is assigned the ID share1 and configured to have the shared element effect. 496 .geometryTransition('share1') 497 .onClick(() => { 498 animateTo({ curve: curves.springMotion(0.6, 1.2) }, () => { 499 // Change the state variable in the closure to display the modal. 500 this.isPresent = !this.isPresent; 501 }) 502 }) 503 504 Text('Component 2') 505 .fontWeight(FontWeight.Bold) 506 .fontSize(20) 507 .fontColor(Color.White) 508 .textAlign(TextAlign.Center) 509 .borderRadius(10) 510 .backgroundColor(0x67C23A) 511 .width(150) 512 .height(150) 513 .margin(20) 514 } 515 .width('100%') 516 .height('100%') 517 .justifyContent(FlexAlign.Start) 518 .backgroundColor(Color.White) 519 } 520} 521``` 522 523 524 525 526 527## Implementation with Attribute Animation 528 529 530```ts 531import curves from '@ohos.curves'; 532class itTmp{ 533 $rect:Array<number> = [] 534} 535@Entry 536@Component 537struct AutoAchieveShareTransitionDemo { 538 private items: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; 539 540 // Specify whether the component is expanded. 541 @State expand: boolean = false; 542 543 // Attributes related to the shared element. 544 @State rect_top: number = 0; // Position of the shared element. 545 @State rect_bottom: number = 0; 546 @State rect_left: number = 0; 547 @State rect_right: number = 0; 548 549 // Attributes related to the newly created element. 550 @State item: string = ''; // Record the expanded element. 551 @State cardHeight: number = 300; // Widget height. 552 @State cardOpacity: number = 1; // Widget opacity. 553 @State layoutHeight: number = 300; // Height of the expanded page. 554 @State layoutWidth: string = '90%'; // Width of the expanded page. 555 @State layoutOffset: number = 0; // Offset of the expanded page. 556 @State layoutOpacity: number = 0; // Opacity of the expanded page. 557 558 // In the callback invoked when the transition is complete. 559 @State count: number = 0; 560 561 build() { 562 Stack() { 563 Scroll() { 564 Column({ space: 20 }) { 565 ForEach(this.items, (item:string, index?:number|undefined) => { 566 Row() { 567 Column() { 568 Text('Shared element ' + item) 569 .fontSize(30) 570 .fontColor(Color.Black) 571 .fontWeight(FontWeight.Bolder) 572 Text ('Expand widget') 573 .fontSize(20) 574 .fontColor(0x909399) 575 } 576 .width('100%') 577 .height('100%') 578 .justifyContent(FlexAlign.Center) 579 } 580 .width('90%') 581 .height(this.cardHeight) 582 .padding(20) 583 .backgroundColor(Color.Pink) 584 .borderRadius(10) 585 .shadow({ radius: 10, color: 0x909399, offsetX: 10, offsetY: 10 }) 586 .opacity(this.expand && this.item == item ? 0 : 1) 587 // Set a unique ID and obtain the attribute information of the component corresponding to the ID. 588 .id(item) 589 .onClick(() => { 590 // Obtain the position and size of the corresponding component. 591 let strJson = getInspectorByKey(item); 592 let rect:itTmp = JSON.parse(strJson); 593 let rectInfo:Array<object> = JSON.parse('[' + rect.$rect + ']'); 594 let rect_left:string = JSON.parse('[' + rectInfo[0] + ']')[0]; 595 let rect_top:string = JSON.parse('[' + rectInfo[0] + ']')[1]; 596 let rect_right:string = JSON.parse('[' + rectInfo[1] + ']')[0]; 597 let rect_bottom:string = JSON.parse('[' + rectInfo[1] + ']')[1]; 598 let rect_value:Record<string,string> = { 599 "left": rect_left, "top": rect_top, "right": rect_right, "bottom": rect_bottom 600 }; 601 602 // Set the location, content, and status of the shared element. 603 this.rect_top = Number(rect_top); 604 this.item = item; 605 this.expand = true; 606 this.count += 1; 607 608 animateTo({ curve: curves.springMotion() }, () => { 609 this.layoutHeight = 2772 / 3.5; 610 this.layoutWidth = '100%'; 611 this.layoutOffset = -((Number(rect_top) - 136) / 3.5); 612 }) 613 }) 614 }) 615 } 616 .width('100%') 617 .margin({ top: 20 }) 618 } 619 .height('100%') 620 621 // Create an element that is the same as the component based on the obtained component information. 622 if (this.expand) { 623 Column() { 624 // Share element. 625 Row() { 626 Column() { 627 Text('Shared element ' + this.item) 628 .fontSize(30) 629 .fontColor(Color.Black) 630 .fontWeight(FontWeight.Bolder) 631 Text ('Expand widget') 632 .fontSize(20) 633 .fontColor(0x909399) 634 } 635 .width('100%') 636 .height('100%') 637 .justifyContent(FlexAlign.Center) 638 } 639 .width('100%') 640 .height(this.cardHeight) 641 .padding(20) 642 .backgroundColor(Color.Pink) 643 644 // New element. 645 Text('Expanded page\n\nExpanded page\n\nExpanded page\n\nExpanded page\n\nExpanded page\n\nExpanded page\n\nExpanded page\n\nExpanded page') 646 .fontSize(20) 647 .fontColor(0xcccccc) 648 .margin({ top: 20 }) 649 .width(100) 650 651 } 652 .borderRadius(this.layoutWidth == '100%' ? 0 : 10) 653 .shadow({ radius: 10, color: 0x909399, offsetX: 10, offsetY: 10 }) 654 .width(this.layoutWidth) 655 .height(this.layoutHeight) 656 .clip(true) 657 .backgroundColor(Color.White) 658 // Work out the absolute position of the new element. 659 .position({ 660 x: this.layoutWidth == '90%' ? '5%' : 0, 661 y: (this.rect_top - 136) / 3.5 662 }) 663 .translate({ 664 y: this.layoutOffset 665 }) 666 .onClick(() => { 667 this.count -= 1; 668 669 animateTo({ 670 curve: curves.springMotion(), 671 onFinish: (() => { 672 if (this.count == 0) { 673 this.expand = false; 674 } 675 }) 676 }, () => { 677 this.layoutHeight = this.cardHeight; 678 this.layoutWidth = '90%'; 679 this.layoutOffset = 0; 680 }) 681 }) 682 } 683 } 684 } 685} 686``` 687 688 689 690 691