全屏模态转场+隐式共享元素实现一镜到底

用全屏+隐式共享元素实现一镜到底的效果

HarmonyOS
2024-05-26 11:06:46
浏览
收藏 0
回答 1
待解决
回答 1
按赞同
/
按时间
jackie510

使用的核心API

模态转场

核心代码解释

import curves from '@ohos.curves'; 
import { TabIndex } from './TabIndex'; 
  
const ITEM_HEIGHT: number = 80; 
  
@Entry 
@Component 
struct Index { 
  @State isEditPageShow: boolean = false; 
  // 共享元素转场的id,转场时设置相应的值,转场结束后设置为空字符串 
  @State geometryId: string = ''; 
  @State editPageBorderRadius: number = 0; 
  // 每次按返回键返回主页时,增加backCount通知卡片将scale值置为1 
  @State backCount: number = 0; 
  // 维护一个列表,保存了对应index的卡片的位移 
  @State cardTranslateList: number[] = [0, 0, 0, 0, 0]; 
  // 标题的位移 
  @State titleTranslate: number = 0; 
  // 控制蒙板的上下树,在点击卡片和返回主页动画之外,需要保持蒙板下树,否则可能会一直绘制全屏大小的Column引起性能问题 
  @State isMaskEnabled: boolean = false; 
  @State maskAlpha: number = 0; 
  private itemArray: string[] = ['Item1', 'Item2', 'Item3', 'Item4', 'Item5']; 
  itemHeight: number = 80; 
  itemBorderRadius: number = 38; 
  // 动画计数,用于处理接续场景 
  animationCount: number = 0; 
  // 由于点击卡片的动画完成后需要将geometryId置空,在返回时需要用到这个变量记录上一次点击的卡片的id 
  formerId: string = ''; 
  
  // 点击卡片弹出编辑页 
  private onCardClicked(itemName: string, index: number): void { 
    // 将动画计数+1并记录动画计数 
    let animationCount = ++this.animationCount; 
  
    // 通过itemName将点击的卡片和弹出的编辑页进行绑定,实现从点击卡片位置弹出编辑页 
    this.geometryId = itemName; 
    this.formerId = itemName; 
  
    // 动画开始前设置编辑页圆角为38,这样卡片圆角就会从20动到38 
    this.editPageBorderRadius = 38; 
  
    animateTo({ 
      duration: 350, 
      curve: Curve.Friction, 
      onFinish: () => { 
        // 如果该动画后没有后续的接续动画,则设置geometryId为空字符串 
        if (animationCount === this.animationCount) { 
          this.geometryId = ''; 
          this.editPageBorderRadius = 0; 
        } 
      } 
    }, () => { 
      this.isEditPageShow = true; 
    }) 
    this.doTranslateAnimation(index); 
  
    this.isMaskEnabled = true; 
    animateTo({ 
      duration: 200, 
      curve: Curve.Sharp, 
      onFinish: () => { 
        // 如果后续无接续动画,则将蒙板下树,避免一直绘制造成性能问题 
        if (animationCount === this.animationCount) { 
          this.isMaskEnabled = false; 
        } 
      } 
    }, () => { 
      this.maskAlpha = 0.2; 
    }) 
  } 
  
  private doTranslateAnimation(index: number): void { 
    // 点击卡片打开编辑页时 
    if (this.isEditPageShow) { 
      animateTo({ 
        duration: 350, 
        curve: Curve.Friction 
      }, () => { 
        for (let i = 0; i < this.cardTranslateList.length; i++) { 
          if (i < index) { 
            this.cardTranslateList[i] = -ITEM_HEIGHT; 
          } else if (i > index) { 
            this.cardTranslateList[i] = ITEM_HEIGHT; 
          } 
        } 
        this.titleTranslate = -ITEM_HEIGHT; 
      }); 
    } else { // 返回主页时 
      animateTo({ 
        duration: 350, 
        curve: Curve.Friction 
      }, () => { 
        for (let i = 0; i < this.cardTranslateList.length; i++) { 
          this.cardTranslateList[i] = 0; 
        } 
        this.titleTranslate = 0; 
      }); 
    } 
  } 
  
  // 点击编辑页的返回箭头返回 
  private onBackClicked(): void { 
    // 将动画计数+1并记录动画计数 
    let animationCount = ++this.animationCount; 
  
    this.geometryId = this.formerId; 
  
    // 返回时需要将圆角从0变成38,就会有38动到20的圆角一镜到底效果 
    this.editPageBorderRadius = 38; 
  
    // 增加计数,卡片子组件将scale设回1 
    this.backCount++; 
  
    animateTo({ 
      duration: 350, 
      curve: Curve.Friction, 
      onFinish: () => { 
        // 如果该动画后没有后续的接续动画,则设置geometryId为空字符串 
        if (animationCount === this.animationCount) { 
          this.geometryId = ''; 
        } 
      } 
    }, () => { 
      this.isEditPageShow = false; 
    }) 
    this.doTranslateAnimation(0); 
  
    this.isMaskEnabled = true; 
    animateTo({ 
      duration: 350, 
      curve: Curve.Sharp, 
      onFinish: () => { 
        // 如果后续无接续动画,则将蒙板下树,避免一直绘制造成性能问题 
        if (animationCount === this.animationCount) { 
          this.isMaskEnabled = false; 
        } 
      } 
    }, () => { 
      this.maskAlpha = 0; 
    }) 
  } 
  
  // 点击卡片需要弹出的编辑页 
  @Builder 
  EditPageBuilder() { 
    Column() { 
      TabIndex() 
       .margin({ top: 39 }); 
    } 
    .size({ width: 384, height: 792 }) 
    // 加背景色将首页挡掉 
    .backgroundColor('#ffffffff') 
    .geometryTransition(this.geometryId) 
    // 添加编辑页出现和消失的透明度动画 
    .transition(TransitionEffect.asymmetric( 
      TransitionEffect.OPACITY.animation({ curve: Curve.Sharp, duration: 100 }), 
      TransitionEffect.OPACITY.animation({ curve: Curve.Sharp, duration: 150, delay: 200 }) 
    )) 
    // 要实现圆角的一镜到底,需要对编辑页加clip 
    .clip(true) 
    .borderRadius(this.editPageBorderRadius) 
  } 
  
  build() { 
    Stack() { 
      Column({ space: 20 }) { 
        // 给标题栏套一层父控件并设置clip为true,否则标题栏会位移到状态栏上面 
        Row() { 
          Text('This is list page') 
            .fontColor(Color.Black) 
            .fontSize(30) 
            .width('100%')// 由于在EntryAbility里设置了沉浸式,此处加上margin避让状态栏。此处让标题在39vp的基础上再向下偏移一些 
            .translate({ y: this.titleTranslate }) 
        } 
        .clip(true) 
        .margin({ top: 50 }) 
  
        List() { 
          ForEach(this.itemArray, (itemName: string, index: number) => { 
            ListItem() { 
              Card({ 
                itemName: itemName, 
                index: index, 
                backCount: this.backCount, 
                onItemClickListener: (itemName: string, index: number) => { 
                  this.onCardClicked(itemName, index); 
                } 
              }) 
            } 
            .margin(10) 
            .translate({ y: this.cardTranslateList[index] }) 
          }) 
        } 
        .alignListItem(ListItemAlign.Center) 
        .size({ width: '100%', height: '100%' }) 
      } 
      .width('100%') 
      .height('100%') 
      .backgroundColor('#30808080') 
      // bindContentCover模态转场弹出编辑页 
      .bindContentCover(this.isEditPageShow, this.EditPageBuilder(), { modalTransition: ModalTransition.NONE, 
        // 添加onDisappear以实现侧滑退出的逻辑,否则侧滑后会有问题 
        onDisappear: () => { 
          this.onBackClicked(); 
        } 
      }) 
  
      // 新增全屏的黑色蒙板 
      if (this.isMaskEnabled) { 
        Column() 
          .width('100%') 
          .height('100%') 
          .backgroundColor('#000000') 
          .opacity(this.maskAlpha) 
          .enabled(false) 
          .focusable(false) 
      } 
    } 
    .width('100%') 
    .height('100%') 
  } 
} 
  
@Component 
struct Card { 
  @Prop itemName: string; 
  // 同步父控件的值,变化时将scale设为1 
  @Prop @Watch('onBack2List') backCount: number; 
  @Prop index: number; 
  @State cardScale: number = 1.0; 
  // 当离手时,通知父控件做返回逻辑 
  onItemClickListener?: (itemName: string, index: number) => void; 
  
  private onBack2List(): void { 
    animateTo({ 
      duration: 350, 
      curve: Curve.Friction 
    }, () => { 
      this.cardScale = 1.0; 
    }); 
  } 
  
  build() { 
    Column() { 
      Text(this.itemName) 
        .height(ITEM_HEIGHT) 
        .textAlign(TextAlign.Center) 
    } 
    .size({ width: '30%', height: ITEM_HEIGHT }) 
    .scale({ x: this.cardScale, y: this.cardScale }) 
    .backgroundColor(Color.White) 
    // 改下值换个卡片的显示效果 
    .borderRadius(20) 
    .geometryTransition(this.itemName) 
    .onTouch((event?: TouchEvent) => { 
      if (event?.type == TouchType.Down) { 
        // 下压时,做0.95的scale动画 
        animateTo({ 
          curve: curves.interpolatingSpring(0.5, 1, 350, 35) 
        }, () => { 
          this.cardScale = 0.95; 
        }); 
      } else if ((event as TouchEvent).type === TouchType.Up) { 
        // 离手时,通知父控件返回 
        this.onItemClickListener?.(this.itemName, this.index); 
      } else if ((event as TouchEvent).type === TouchType.Cancel) { 
        // 其他场景让控件恢复 
        animateTo({ 
          curve: curves.interpolatingSpring(0.5, 1, 350, 35) 
        }, () => { 
          this.cardScale = 1.0; 
        }); 
      } 
    }) 
  } 
} 
  
import { TabTitleBar } from "@ohos.arkui.advanced.TabTitleBar" 
import promptAction from '@ohos.promptAction' 
  
class tabItem { 
  title: ResourceStr; 
  icon?: ResourceStr; 
  
  constructor(title: ResourceStr, icon?: ResourceStr) { 
    this.title = title 
    this.icon = icon 
  } 
} 
  
class menuItem { 
  value: ResourceStr; 
  isEnabled?: boolean; 
  action?: () => void; 
  
  constructor(value: ResourceStr, isEnabled?: boolean, action?: () => void) { 
    this.value = value; 
    this.isEnabled = isEnabled; 
    this.action = action; 
  } 
} 
  
@Entry 
@Component 
export struct TabIndex { 
  @Builder 
  componentBuilder() { 
    Text("#1ABC9C\nTURQUOISE") 
      .fontWeight(FontWeight.Bold) 
      .fontSize(14) 
      .width("100%") 
      .textAlign(TextAlign.Center) 
      .fontColor("#CCFFFFFF") 
      .backgroundColor("#1ABC9C") 
    Text("#16A085\nGREEN SEA") 
      .fontWeight(FontWeight.Bold) 
      .fontSize(14) 
      .width("100%") 
      .textAlign(TextAlign.Center) 
      .fontColor("#CCFFFFFF") 
      .backgroundColor("#16A085") 
    Text("#2ECC71\nEMERALD") 
      .fontWeight(FontWeight.Bold) 
      .fontSize(14) 
      .width("100%") 
      .textAlign(TextAlign.Center) 
      .fontColor("#CCFFFFFF") 
      .backgroundColor("#2ECC71") 
    Text("#27AE60\nNEPHRITIS") 
      .fontWeight(FontWeight.Bold) 
      .fontSize(14) 
      .width("100%") 
      .textAlign(TextAlign.Center) 
      .fontColor("#CCFFFFFF") 
      .backgroundColor("#27AE60") 
    Text("#3498DB\nPETER RIVER") 
      .fontWeight(FontWeight.Bold) 
      .fontSize(14) 
      .width("100%") 
      .textAlign(TextAlign.Center) 
      .fontColor("#CCFFFFFF") 
      .backgroundColor("#3498DB") 
  } 
  
  private readonly tabItems: Array<tabItem> = [new tabItem('日'), new tabItem('周'), new tabItem('月')] 
  
  build() { 
    Navigation() { 
      Tabs({ barPosition: BarPosition.Start }) { 
        TabContent() { 
          Column() { 
            Tabs() { 
            } 
          } 
          .backgroundColor(Color.Blue) 
          .width('100%') 
        } 
        .tabBar('日') 
  
        TabContent() { 
          Column() { 
            Tabs() { 
            } 
          } 
          .backgroundColor(Color.Orange) 
          .width('100%') 
        } 
        .tabBar('周') 
  
        TabContent() { 
          Column() { 
            Tabs() { 
            } 
          } 
          .backgroundColor(Color.Green) 
          .width('100%') 
        } 
        .tabBar('月') 
      } 
      .scrollable(false) 
    } 
    .titleMode(NavigationTitleMode.Mini) 
  } 
  
}

实现效果

适配版本信息

IDE:DevEco Studio 4.1.1.300

SDK:4.1.2.1

分享
微博
QQ
微信
回复
2024-05-27 10:52:02
相关问题
模态转场实现弹框样式的页面
366浏览 • 1回复 待解决
模态转场如何控制固定高度
515浏览 • 1回复 待解决
如何实现动画转场效果
367浏览 • 1回复 待解决
有谁知道ArkTS支持跳转吗?
541浏览 • 1回复 待解决
应用怎么实现模态效果
682浏览 • 1回复 待解决
CustomDialog如何实现模态详情页效果
501浏览 • 1回复 待解决
ArkTS中怎么完全屏蔽具体实现
471浏览 • 1回复 待解决
鸿蒙系统异构组网到底实现了没有?
6897浏览 • 1回复 待解决
如何实现多线程数据共享
612浏览 • 1回复 待解决
HAP和HSP之间如何实现数据共享
468浏览 • 1回复 待解决
实现开放测试的具体流程
764浏览 • 1回复 待解决