HarmonyOS鸿蒙Next中怎么实现类似华为控制中心左右切换页面的叠层效果。

HarmonyOS鸿蒙Next中怎么实现类似华为控制中心左右切换页面的叠层效果。 开发新手,打算做个类似的效果,但不知道用什么组件或是其他方法实现对应的动画效果


更多关于HarmonyOS鸿蒙Next中怎么实现类似华为控制中心左右切换页面的叠层效果。的实战教程也可以访问 https://www.itying.com/category-93-b0.html

8 回复

主要涉及的技术点:

1、手势:通过 PanGesture 手势,让负一屏跟随手势显示和隐藏。

2、模糊处理:使用 backdropBlur 为组件添加背景模糊效果,支持自定义设置模糊半径和灰阶参数。半径越大越模糊

3、UI布局:整体采用Stack叠层显示,主页面、模糊背景页面、负一屏页面。

难点在于手势的控制,参考 如何处理PanGesture与其他手势冲突问题 的场景三。

为了演示,采用了屏幕截图,就一张图作为主页面,gif很失帧,已真机运行为准:

cke_467679.gif

demo源码:https://github.com/iHongRen/harmony-study-demo

下面主功能实现代码并不多,大部分是负一屏的卡片UI代码。

/**
 * @fileName : BlurDemo.ets
 * @author : @cxy
 * @date : 2026/4/17
 * @description : 负一屏实现
 */
import { display, window } from "@kit.ArkUI"

@Entry
@Component
export struct BlurDemo {
  @State private translateX: number = 0
  private startTime: number = 0
  private startTranslateX: number = 0
  private contentWidth: number = 0
  private maxBlur: number = 50
  private duration: number = 300
  private threshold: number = 0.5
  private panOption: PanGestureOptions = new PanGestureOptions({
    direction: PanDirection.Left | PanDirection.Right
  })

  // 判断是否为快速滑动
  isSwipeGesture(distance: number, duration: number): boolean {
    const speed = distance / duration;
    return speed > 0.5; // 速度阈值
  }

  blurRadius(): number {
    return Math.max(0,
      Math.min(this.maxBlur, (this.contentWidth + this.translateX) / this.contentWidth * this.maxBlur))
  }

  shouldShowBlur(): boolean {
    return this.translateX > -this.contentWidth
  }

  aboutToAppear(): void {
    const width = this.getUIContext().px2vp(display.getDefaultDisplaySync().width)
    this.contentWidth = width
    this.translateX = -width

    const context = this.getUIContext().getHostContext()
    if (context) {
      // 开启沉浸式全屏
      window.getLastWindow(context).then((win) => {
        win.setWindowLayoutFullScreen(true)
      })
    }
  }

  build() {
    Column() {
      Stack() {
        this.mainBuilder()
        this.blurBuilder()
        this.sideBuilder()
      }
      // 使用单个PanGesture,通过速度判断区分拖拽和滑动
      .gesture(
        PanGesture(this.panOption)
          .onActionStart(() => {
            this.startTranslateX = this.translateX
            this.startTime = Date.now()
          })
          .onActionUpdate((event: GestureEvent) => {
            if (event) {
              this.updateOffsetByGesture(event.offsetX)
            }
          })
          .onActionEnd((event: GestureEvent) => {
            const duration = Date.now() - this.startTime
            const distance = Math.abs(event.offsetX)

            // 根据速度判断是滑动还是拖拽
            if (this.isSwipeGesture(distance, duration)) {
              // 快速滑动,执行切换操作
              this.handleSwipeGesture(event)
            } else {
              // 慢速移动,保持拖拽位置并吸附到边界
              this.snapToEdge()
            }
          })
      )
    }
  }

  @Builder
  mainBuilder() {
    Image($r('app.media.phone_bg'))
      .width('100%')
      .height('100%')
  }

  @Builder
  blurBuilder() {
    Stack()
      .width('100%')
      .height('100%')
      .backdropBlur(this.blurRadius())
      .visibility(this.shouldShowBlur() ? Visibility.Visible : Visibility.Hidden)
  }

  @Builder
  sideBuilder() {
    List({ space: 20 }) {
      ListItem() {
        this.sectionHeader()
      }

      ListItem() {
        this.weatherCard()
      }

      ListItem() {
        this.memoCard()
      }

      ListItem() {
        this.healthCard()
      }

      ListItem() {
        this.newsCard()
      }
    }
    .padding({ left: 20, right: 20 })
    .contentStartOffset(50)
    .contentEndOffset(50)
    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
    .translate({ x: this.translateX })
  }

  @Builder
  sectionHeader() {
    Column({ space: 8 }) {
      Text('今日卡片')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#111111')

      Text('向右滑动打开负一屏,向左滑动隐藏它')
        .fontSize(14)
        .fontColor('#6e6e73')
        .lineHeight(20)
    }
    .width('100%')
  }

  @Builder
  weatherCard() {
    Column({ space: 12 }) {
      Row() {
        Column({ space: 6 }) {
          Text('天气提醒')
            .fontSize(18)
            .fontWeight(FontWeight.Medium)
            .fontColor('#1c1c1e')

          Text('今天多云,最高 21℃,适合出行')
            .fontSize(14)
            .fontColor('#6e6e73')
            .lineHeight(20)
        }

        Stack() {
          Text('☀')
            .fontSize(20)
            .fontColor(Color.White)
        }
        .width(42)
        .height(42)
        .borderRadius('50%')
        .backgroundColor('#FF9F0A')

      }
      .justifyContent(FlexAlign.SpaceBetween)

      this.cardDivider()

      Row({ space: 12 }) {
        this.smallInfoBlock('风速', '5 km/h', '#34C759')
        this.smallInfoBlock('湿度', '62%', '#5AC8FA')
      }
    }
    .width('100%')
    .padding({
      top: 22,
      bottom: 22,
      left: 18,
      right: 18
    })
    .backgroundColor('rgba(255,255,255,0.95)')
    .borderRadius(20)
    .shadow({
      color: 'rgba(0,0,0,0.08)',
      offsetY: 10,
      radius: 22
    })
  }

  @Builder
  memoCard() {
    Column({ space: 12 }) {
      Text('备忘录')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1c1c1e')

      Text('15:00 产品评审会议17:30 线上读书会')
        .fontSize(14)
        .fontColor('#6e6e73')
        .lineHeight(22)

      Row({ space: 10 }) {
        Text('今天')
          .fontSize(11)
          .fontWeight(FontWeight.Medium)
          .fontColor('#ffffff')
          .backgroundColor('#34C759')
          .padding({
            left: 10,
            right: 10,
            top: 4,
            bottom: 4
          })
          .borderRadius(12)

        Text('会议')
          .fontSize(11)
          .fontWeight(FontWeight.Medium)
          .fontColor('#ffffff')
          .backgroundColor('#007AFF')
          .padding({
            left: 10,
            right: 10,
            top: 4,
            bottom: 4
          })
          .borderRadius(12)
      }
    }
    .width('100%')
    .padding({
      top: 22,
      bottom: 22,
      left: 18,
      right: 18
    })
    .backgroundColor('rgba(255,255,255,0.95)')
    .borderRadius(20)
    .shadow({
      color: 'rgba(0,0,0,0.08)',
      offsetY: 10,
      radius: 22
    })
  }

  @Builder
  healthCard() {
    Column({ space: 14 }) {
      Text('健康目标')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1c1c1e')

      Row({ space: 14 }) {
        this.statBlock('步数', '8,432', '#34C759')
        this.statBlock('睡眠', '7.5h', '#5AC8FA')
      }
    }
    .width('100%')
    .padding({
      top: 22,
      bottom: 22,
      left: 18,
      right: 18
    })
    .backgroundColor('rgba(255,255,255,0.95)')
    .borderRadius(20)
    .shadow({
      color: 'rgba(0,0,0,0.08)',
      offsetY: 10,
      radius: 22
    })
  }

  @Builder
  newsCard() {
    Column({ space: 12 }) {
      Row() {
        Text('今日要闻')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#1c1c1e')

        Text('查看更多')
          .fontSize(13)
          .fontColor('#007AFF')
      }
      .justifyContent(FlexAlign.SpaceBetween)

      Text('直播带货热潮继续,关注购物节优惠与物流动态。')
        .fontSize(14)
        .fontColor('#6e6e73')
        .lineHeight(20)
    }
    .width('100%')
    .padding({
      top: 22,
      bottom: 22,
      left: 18,
      right: 18
    })
    .backgroundColor('rgba(255,255,255,0.95)')
    .borderRadius(20)
    .shadow({
      color: 'rgba(0,0,0,0.08)',
      offsetY: 10,
      radius: 22
    })
  }

  @Builder
  cardDivider() {
    Stack()
      .width('100%')
      .height(1)
      .backgroundColor('rgba(0,0,0,0.08)')
  }

  @Builder
  smallInfoBlock(title: string, value: string, color: string) {
    Column({ space: 6 }) {
      Text(title)
        .fontSize(12)
        .fontColor('#6e6e73')

      Text(value)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
    }
    .width('50%')
    .padding({
      top: 14,
      bottom: 14,
      left: 14,
      right: 14
    })
    .backgroundColor('rgba(255,255,255,0.95)')
    .borderRadius(18)
  }

  @Builder
  statBlock(title: string, value: string, color: string) {
    Column({ space: 6 }) {
      Text(title)
        .fontSize(12)
        .fontColor('#6e6e73')

      Text(value)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
    }
    .width('48%')
    .padding({
      top: 16,
      bottom: 16,
      left: 16,
      right: 16
    })
    .backgroundColor('rgba(255,255,255,0.95)')
    .borderRadius(18)
  }

  @Builder
  cardItem(title: string, subtitle: string, accentColor: string) {
    Column({ space: 10 }) {
      Row() {
        Stack()
          .width(8)
          .height(8)
          .borderRadius('50%')
          .backgroundColor(accentColor)

        Column({ space: 4 }) {
          Text(title)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1c1c1e')

          Text(subtitle)
            .fontSize(13)
            .fontColor('#8e8e93')
            .lineHeight(20)
        }
      }
      .width('100%')

      Stack()
        .width('100%')
        .height(1)
        .backgroundColor('rgba(0,0,0,0.06)')
    }
    .width('100%')
    .padding({
      top: 18,
      bottom: 18,
      left: 18,
      right: 18
    })
    .backgroundColor('rgba(255,255,255,0.9)')
    .borderRadius(22)
    .shadow({
      color: 'rgba(0,0,0,0.08)',
      offsetY: 10,
      radius: 24
    })
  }

  private handleSwipeGesture(event: GestureEvent) {
    // 计算滑动角度
    const angle = Math.atan2(event.offsetY, event.offsetX) * 180 / Math.PI

    if (Math.abs(angle) < 45) {
      // 向右快速滑动,打开负一屏
      if (this.translateX < 0) {
        this.getUIContext().animateTo({
          duration: this.duration,
          curve: Curve.Friction
        }, () => {
          this.translateX = 0
        })
      }
    } else if (Math.abs(angle) > 135) {
      // 向左快速滑动,关闭负一屏
      this.getUIContext().animateTo({
        duration: this.duration,
        curve: Curve.Friction
      }, () => {
        this.translateX = -this.contentWidth
      })
    }
  }

  private updateOffsetByGesture(offsetX: number) {
    const newOffset = this.startTranslateX + offsetX
    // 直接限制在有效范围内,使拖动更跟手
    this.translateX = Math.max(-this.contentWidth, Math.min(0, newOffset))
  }

  private snapToEdge() {
    const threshold = -this.contentWidth * this.threshold

    let targetOffset: number = this.translateX

    if (this.translateX > threshold) {
      // 打开负一屏,执行动画到0
      targetOffset = 0
    } else {
      // 关闭负一屏,执行动画到-contentWidth
      targetOffset = -this.contentWidth
    }

    this.getUIContext().animateTo({
      duration: this.duration,
      curve: Curve.Friction
    }, () => {
      this.translateX = targetOffset
    })
  }
}

更多关于HarmonyOS鸿蒙Next中怎么实现类似华为控制中心左右切换页面的叠层效果。的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


结合了沉浸光感,一起做上去效果更好

import { HdsAnimationMode, HdsNavigation, HdsNavigationTitleMode, HdsTabs, HdsTabsController, HdsNavigationMenuContentOptions, ScrollEffectType, hdsMaterial, } from '@kit.UIDesignKit';
import { SymbolGlyphModifier } from "@kit.ArkUI";

@Entry
@Component
struct Index {
  private scrollerForScroll: Scroller = new Scroller();
  private controller: HdsTabsController = new HdsTabsController();
  @State currentSwiperIndex: number = 0;

  aboutToAppear() {
    this.controller.applyHideAnimation(HdsAnimationMode.CLICK_ANIMATION);
    this.controller.applyShowAnimation(HdsAnimationMode.CLICK_ANIMATION);
  }

  private menus: HdsNavigationMenuContentOptions = {
    value: [{
      content: {
        label: 'menu1',
        icon: $r('sys.symbol.square_and_pencil'),
      }
    }, {
      content: {
        label: 'menu2',
        icon: $r('sys.symbol.star')
      },
    },{
      content: {
        label: 'menu3',
        icon: $r('sys.symbol.more')
      },
    }
    ],
  };

  // 计算卡片缩放
  private getScale(index: number): number {
    return index === this.currentSwiperIndex ? 1.0 : 0.85;
  }
  // 计算卡片水平偏移
  private getTranslateX(index: number): number {
    if (index === this.currentSwiperIndex) return 0;
    return index > this.currentSwiperIndex ? 25 : -25;
  }
  // 透明度
  private getOpacity(index: number): number {
    return index === this.currentSwiperIndex ? 1 : 0.82;
  }
  // 层级
  private getZIndex(index: number): number {
    return index === this.currentSwiperIndex ? 99 : 10;
  }
  // 卡片背景色
  private getCardBg(index: number): ResourceColor {
    const colorList: ResourceColor[] = ['#2D3748', '#3C4A60', '#4A3F5C'];
    return colorList[index % colorList.length];
  }

  @Builder
  SwiperCard(index: number) {
    Stack() {
      Text(`控制中心卡片 ${index + 1}`)
        .fontSize(28)
        .fontColor(Color.White)
    }
    .width('92%')
    .height(220)
    .borderRadius(24)
    .backgroundColor(this.getCardBg(index))
    .scale({ x: this.getScale(index), y: this.getScale(index) })
    .translate({ x: this.getTranslateX(index) })
    .opacity(this.getOpacity(index))
    .zIndex(this.getZIndex(index))
  }

  build() {
    HdsNavigation() {
      HdsTabs({ controller: this.controller }) {
        ForEach(MENU_CONFIG, (item: MenuItem) => {
          TabContent() {
            Stack() {
              Scroll(this.scrollerForScroll) {
                Column() {
                  // 嵌入:华为控制中心 叠层左右切换
                  Swiper() {
                    this.SwiperCard(0)
                    this.SwiperCard(1)
                    this.SwiperCard(2)
                  }
                  .width('100%')
                  .height(260)
                  .indicator(false)
                  .loop(false)
                  .itemSpace(8)
                  .onChange((idx: number) => {
                    this.currentSwiperIndex = idx;
                  })

                  // 原有占位内容
                  Text('内容区域')
                    .fontSize(16)
                    .margin({ top: 20 })
                }
                .width('100%')
              }
              .height('100%')
            }
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
          }
          .tabBar(new BottomTabBarStyle({
            normal: item.symbolGlyph, selected: item.symbolGlyph1
          }, item.label))
        })
      }
      .barOverlap(true)
      .vertical(false)
      .barPosition(BarPosition.End)
      .barFloatingStyle({
        barBottomMargin: 28,
        systemMaterialEffect:  {
          materialType: hdsMaterial.MaterialType.ADAPTIVE,
          materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
        }
      })
    }
    .mode(NavigationMode.Stack)
    .titleBar({
      content: {
        title: {
          mainTitle: 'MainTitle',
        },
        menu: this.menus,
      },
      style: {
        scrollEffectOpts: {
          enableScrollEffect: false,
          scrollEffectType: ScrollEffectType.GRADIENT_BLUR,
        },
        systemMaterialEffect: {
          materialType: hdsMaterial.MaterialType.ADAPTIVE,
          materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
        },
      },
      avoidLayoutSafeArea: false,
      enableComponentSafeArea: false,
    })
    .bindToScrollable([this.scrollerForScroll])
    .hideBackButton(false)
    .titleMode(HdsNavigationTitleMode.MINI)
    .ignoreLayoutSafeArea([LayoutSafeAreaType.SYSTEM], [LayoutSafeAreaEdge.TOP, LayoutSafeAreaEdge.BOTTOM])
    .backgroundColor(Color.Transparent)
  }
}

interface MenuItem {
  symbolGlyph: SymbolGlyphModifier,
  symbolGlyph1: SymbolGlyphModifier,
  label: string,
  defaultBgColor: ResourceColor,
  hoverBgColor: ResourceColor,
  pressBgColor: ResourceColor,
};

const MENU_CONFIG: MenuItem[] = [
  {
    symbolGlyph: new SymbolGlyphModifier($r('sys.symbol.alarm_fill_1')).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
      .fontColor([$r('sys.color.ohos_id_color_bottom_tab_icon_off'),
        $r('sys.color.ohos_id_color_bottom_tab_icon_auxcolor_off02')]),
    symbolGlyph1: new SymbolGlyphModifier($r('sys.symbol.alarm_fill_1')).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
      .fontColor([$r('sys.color.ohos_id_color_activated'), $r('sys.color.ohos_id_color_primary_contrary')]),
    label: '闹钟',
    defaultBgColor: Color.Transparent,
    hoverBgColor: $r('sys.color.ohos_id_color_hover'),
    pressBgColor: $r('sys.color.ohos_id_color_click_effect')
  },
  {
    symbolGlyph: new SymbolGlyphModifier($r('sys.symbol.worldclock_fill_2')).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
      .fontColor([$r('sys.color.ohos_id_color_bottom_tab_icon_off'),
        $r('sys.color.ohos_id_color_bottom_tab_icon_auxcolor_off02')]),
    symbolGlyph1: new SymbolGlyphModifier($r('sys.symbol.worldclock_fill_2')).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
      .fontColor([$r('sys.color.ohos_id_color_activated'), $r('sys.color.ohos_id_color_primary_contrary')]),
    label: '时钟',
    defaultBgColor: Color.Transparent,
    hoverBgColor: $r('sys.color.ohos_id_color_hover'),
    pressBgColor: $r('sys.color.ohos_id_color_click_effect')
  },
  {
    symbolGlyph: new SymbolGlyphModifier($r('sys.symbol.stopwatch_2')).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
      .fontColor([$r('sys.color.ohos_id_color_bottom_tab_icon_off'),
        $r('sys.color.ohos_id_color_bottom_tab_icon_auxcolor_off02')]),
    symbolGlyph1: new SymbolGlyphModifier($r('sys.symbol.stopwatch_2')).renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)
      .fontColor([$r('sys.color.ohos_id_color_activated'), $r('sys.color.ohos_id_color_primary_contrary')]),
    label: '秒表',
    defaultBgColor: Color.Transparent,
    hoverBgColor: $r('sys.color.ohos_id_color_hover'),
    pressBgColor: $r('sys.color.ohos_id_color_click_effect')
  }
];

感觉你这个效果就是Swiper轮播或者Tabs组件

借用了点AI之力做了一个相对满意的版本,给大家参考。通过调整上层背景颜色,还能实现透明效果,可以在内容组件间的间隙看到下层内容。但我不太清楚会不会造成比较大的开销

@Entry
@Component
struct ProfessionalTransition {
  @State screenWidth: number = 375;
  // 修改 1: 初始位置在左侧负值处
  @State offsetX: number = -375;
  @State progress: number = 0;

  private lastX: number = -375;

  // 前50%完成所有视觉变化
  private getVisualProgress(): number {
    return Math.min(this.progress * 2, 1);
  }

  private getBottomScale(): number {
    const visualProgress = this.getVisualProgress();
    // 从 1.0 缩小到 0.85
    return 1 - (visualProgress * 0.15);
  }

  private getBottomOpacity(): number {
    const visualProgress = this.getVisualProgress();
    return 1 - visualProgress;
  }

  private getBottomBlur(): number {
    const visualProgress = this.getVisualProgress();
    return visualProgress * 5;
  }

  build() {
    Stack() {
      // --- 底层页面 (Page A) ---
      // 当 TopPanel 从左向右滑入时,底层页面产生缩放、淡出和模糊
      Column() {
        Column(){
          Text("BOTTOM PAGE")
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .scale({ x: this.getBottomScale(), y: this.getBottomScale() })
        }
        .backgroundColor(Color.Transparent)
        .opacity(this.getBottomOpacity())
        .blur(this.getBottomBlur())
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#1A1A1B')
      .justifyContent(FlexAlign.Center)


      // --- 顶层页面 (Page B) ---
      Column() {
        Text("Top Panel").fontSize(24).fontColor(Color.White).margin(40)

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceEvenly }) {
          ForEach([1, 2, 3, 4, 5, 6], (item: number) => {
            Column()
              .width('40%')
              .height(120)
              .borderRadius(24)
              .backgroundColor('rgba(255, 255, 255, 0.15)')
              .margin({ bottom: 20 })
              .shadow({ radius: 10, color: 'rgba(0,0,0,0.2)' })
          })
        }
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#2C2C2E') // 给顶层增加背景色以遮盖底层
      .offset({ x: this.offsetX, y: 0 })
      // 增加阴影增强层级感(右侧阴影)
      .shadow({ radius: 20, color: 'rgba(0,0,0,0.3)', offsetX: 10 })

    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
    .onAreaChange((oldValue: Area, newValue: Area) => {
      this.screenWidth = newValue.width as number;
      // 修改 2: 初始化时保持在左侧
      if (this.offsetX < 0) {
        this.offsetX = -this.screenWidth;
        this.lastX = -this.screenWidth;
      }
    })
    .gesture(
      PanGesture({ direction: PanDirection.Horizontal })
        .onActionStart(() => {
          this.lastX = this.offsetX;
        })
        .onActionUpdate((event: GestureEvent) => {
          // 修改 3: 目标计算
          let targetX = this.lastX + event.offsetX;
          // 限制范围在 [-screenWidth, 0]
          this.offsetX = Math.min(0, Math.max(-this.screenWidth, targetX));
          // 修改 4: 进度计算 (从 -375 到 0 映射为 0 到 1)
          this.progress = (this.offsetX + this.screenWidth) / this.screenWidth;
        })
        .onActionEnd((event: GestureEvent) => {
          const velocity = event.velocityX;
          const currentX = this.offsetX;
          let finalX = 0;

          // 修改 5: 速度逻辑判定
          if (velocity > 800) {
            // 快速向右划:进入 TopPanel
            finalX = 0;
          } else if (velocity < -800) {
            // 快速向左划:回到 BottomPage
            finalX = -this.screenWidth;
          } else {
            // 位置判定
            finalX = currentX > -this.screenWidth / 2 ? 0 : -this.screenWidth;
          }

          let distance = Math.abs(finalX - currentX);
          let absVelocity = Math.abs(velocity);

          let duration = 350;
          if (absVelocity > 0) {
            duration = (distance / absVelocity) * 1000;
          }
          duration = Math.min(Math.max(duration, 150), 500);

          animateTo({
            duration: duration,
            curve: Curve.EaseOut,
            onFinish: () => {
              this.progress = finalX === 0 ? 1 : 0;
            }
          }, () => {
            this.offsetX = finalX;
            this.progress = (finalX === 0) ? 1 : 0;
          })
        })
    )
  }
}

cke_1428.jpeg

pageTransition就可以实现。

在HarmonyOS Next中,使用Swiper组件并设置cachedCount为前后各1,通过customAnimation自定义过渡动画:利用animateTo对非当前页面施加Z轴位移、缩放及透明度变换(如缩小0.8、透明度0.7),即可实现类似控制中心的叠层切换效果。详细动画参数可参考TransitionEffect

使用 Stack 组件叠加多个页面元素,再结合 PanGesture 手势与 属性动画 即可实现这类叠层切换效果。将当前页、下一页等不同层级放入 Stack,通过监听水平拖拽手势实时更新各层的 translate(位移)、scale(缩放)与 opacity(透明度)等属性,并在手势结束或页面切换时使用 animateTo 驱动属性变化,就能形成层叠滑动与渐隐的视觉效果。页面切换逻辑建议维护一个页面索引栈,保证拖拽方向与卡片堆叠层次的一致性。

回到顶部