HarmonyOS鸿蒙Next中半模态转场会触发两次下拉动画

HarmonyOS鸿蒙Next中半模态转场会触发两次下拉动画

// 半模态内容构建器
@Builder
myBuilder() {
  Column() {
    HealthTipsArticle({
      currentTip: this.currentTip,
    });
    // HealthTipsArticle2();
  }
  .width('100%');
}

//这是我构建半模态的区域

.onClick(() => {
  this.currentTip = tip;
  this.isShowTip = true;
})
.bindSheet($$this.isShowTip, this.myBuilder(), {
  title: { title: '健康小tips' }, // 修复title配置错误
  radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) },
});

我在另外一个组件中使用半模态转场可以完成正常下拉,但是在这个界面半模态关闭或者下拉的时候就会产生两次下拉的动画,就好像我开了两个半模态界面叠加一样,点击下拉或者关闭的时候,前面那个半模态界面会下拉,然后紧跟着后面那个半模态再下拉。求大佬帮忙看看代码哪里出了问题,感谢感谢!


更多关于HarmonyOS鸿蒙Next中半模态转场会触发两次下拉动画的实战教程也可以访问 https://www.itying.com/category-93-b0.html

12 回复

半模态转场的show参数若使用双向绑定(如@State@Link),可能存在多个触发源(如系统自带关闭按钮与手动修改show状态同时响应),导致动画重复执行

半模态内部的滑动组件(如列表)未正确处理触摸事件,导致滑动操作同时触发了半模态的转场动画和组件内动画。

几个参考方案

//统一状态控制
//避免直接依赖系统默认关闭逻辑,手动控制show状态变更

// 使用单一状态控制
[@State](/user/State) showSheet: boolean = false;

// 关闭时通过状态控制
onDismiss() {
  this.showSheet = false;
}
//拦截滑动事件冲突
//通过手势组件分离滑动事件作用域:

Column() {
  List() {
    // 列表内容
  }
  .onTouch((event) => {
    // 拦截列表滑动事件,避免冒泡到半模态转场
  })
}
.gesture(
  GestureGroup(GestureMode.Exclusive,
    PanGesture({ direction: PanDirection.Vertical })
      .onActionStart(() => {
        // 处理半模态转场手势
      })
  )
)

更多关于HarmonyOS鸿蒙Next中半模态转场会触发两次下拉动画的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


我用楼主的代码测试了一下没有出现问题,楼主的使用真机吗,我是用真机跑的,楼主说的现象我遇到过一次是因为半模态绑定在当前组件导致的

Button('半模态开关').onClick(() => {
  this.isShowTip = true;
})
  .bindSheet($$this.isShowTip, this.myBuilder(), {
    title: { title: '健康小tips' }, // 修复title配置错误
    radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) },
  });

cke_752.png

有可能是,我绑定是Column组件,

Column() {
    // 图片区域
    Image(tip.image)
      .width('100%')
      .height(180)
      .borderRadius({ topLeft: 8, topRight: 8 })
      .objectFit(ImageFit.Cover);

    // 文字区域
    Column({ space: 4 }) {
      Text(tip.title)
        .fontSize(17)
        .fontWeight(FontWeight.Bold);

      Text(tip.summary)
        .fontSize(14)
        .fontColor('#666666')
        .maxLines(2);
    }
    .padding(12)
    .width('100%');
  }
  .backgroundColor($r('sys.color.white'))
  .borderRadius(8)
  .onClick(() => {
    this.isShowTip = true;
    this.currentTip = tip;
  })
  .bindSheet($$this.isShowTip, this.myBuilder(), {
    title: { title: '健康小tips' }, // 修复title配置错误
    // detents: [SheetSize.MEDIUM, SheetSize.LARGE, 200],
    radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) },
  })

这个是较为完整的代码,

换一下你的绑定区域,换一个在这个页面不会消失且常驻的组件,主要是你的这种场景还可能和版本有关,

你好,其他人的建议你可以试试,可能是状态变量的问题,也可能是其他没有呈现出来的问题,你可以对照官网文档看看,同步试一下官网提供的使用了半模态的示例,比如这个,可以下载下来跟你的代码对照一下:一键拨号

谢谢,我看看文档,

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17,

楼主好,你的isShowTip状态变量在关闭时可能被重复修改,半模态关闭时没有正确同步状态,导致二次触发

你可以改用条件渲染方式控制半模态:

if (this.isShowTip) {

  // 通过条件渲染触发半模态
  .bindSheet($$this.isShowTip, this.myBuilder(), {
    title: { title: '健康小tips' },
    radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) }
  })
}

添加关闭回调处理:

.onClick(() => {
  this.currentTip = tip;
  this.isShowTip = !this.isShowTip; // 切换状态
})
.bindSheet($$this.isShowTip, this.myBuilder(), {
  // ...原有配置
  onDismiss: () => {
    this.isShowTip = false // 这里同步关闭状态
  }
})

我测试了下你的代码,没有问题,如果出现两次,说明你触发了两次打开弹框的代码,你仔细检查下,或者提供更完整的代码。

// 半模态内容构建器
@Builder
myBuilder() {
  Column() {
    HealthTipsArticle({
      currentTip: this.currentTip,
    });
  }
  .width('100%');
}

build() {
  // 用Refresh组件包裹整个列表,实现下拉刷新
  Refresh({
    refreshing: $$this.refreshing, // 双向绑定刷新状态
    builder: this.refreshBuilder() // 自定义刷新指示器
  }) {
    Scroll() {
      Column() {
        List() {
          if (this.searchKeyword) {
            // 搜索有结果
            if (this.filteredTips.length > 0) {
              ForEach(
                this.filteredTips,
                (tip: HealthTip) =>  {
                  ListItem() {
                    Column() {
                      // 图片区域
                      Image(tip.image)
                        .width('100%')
                        .height(180)
                        .borderRadius({ topLeft: 8, topRight: 8 })
                        .objectFit(ImageFit.Cover);

                      // 文字区域
                      Column({ space: 4 }) {
                        Text(tip.title)
                          .fontSize(17)
                          .fontWeight(FontWeight.Bold);

                        Text(tip.summary)
                          .fontSize(14)
                          .fontColor('#666666')
                          .maxLines(2);
                      }
                      .padding(12)
                      .width('100%');
                    }
                    .backgroundColor($r('sys.color.white'))
                    .borderRadius(8)
                    .onClick(() => {
                      this.currentTip = tip;
                      this.isShowTip = true;
                    })
                    .bindSheet($$this.isShowTip, this.myBuilder(), {
                      title: { title: '健康小tips' }, // 修复title配置错误
                      radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) },
                    });
                  }
                  .padding({ left: 16, right: 16, bottom: 12 });
                },
                (tip:HealthTip) => `tip_${tip.id}`
              );
            } else {
              // 搜索无结果
              ListItem() {
                // 使用Column容器包裹文本,通过容器属性实现居中
                Column() {
                  Text(`未找到包含"${this.searchKeyword}"的健康常识`)
                    .fontSize(16)
                    .fontColor('#999999')
                    .padding(20);
                }
                .width('100%') // 让容器占满ListItem宽度
                .justifyContent(FlexAlign.Center) // 垂直方向居中
                .alignItems(HorizontalAlign.Center); // 水平方向居中
              }
              // 可选:给ListItem添加统一的内边距,控制整体间距
              .padding({ left: 16, right: 16, top: 8, bottom: 8 });
            }
          } else {
            ForEach(this.randomTips, (tip: HealthTip) => {
              ListItem() {
                Column() {
                  // 图片区域
                  Image(tip.image)
                    .width('100%')
                    .height(180)
                    .borderRadius({ topLeft: 8, topRight: 8 })
                    .objectFit(ImageFit.Cover);

                  // 文字区域
                  Column({ space: 4 }) {
                    Text(tip.title)
                      .fontSize(17)
                      .fontWeight(FontWeight.Bold);

                    Text(tip.summary)
                      .fontSize(14)
                      .fontColor('#666666')
                      .maxLines(2);
                  }
                  .padding(12)
                  .width('100%');
                }
                .backgroundColor($r('sys.color.white'))
                .borderRadius(8)
                .onClick(() => {
                  this.isShowTip = true;
                  this.currentTip = tip;
                })
                .bindSheet($$this.isShowTip, this.myBuilder(), {
                  title: { title: '健康小tips' }, // 修复title配置错误
                  // detents: [SheetSize.MEDIUM, SheetSize.LARGE, 200],
                  radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) },
                })
              }
              .padding({ left: 16, right: 16, bottom: 12 });
            });
          }
        }
        .backgroundColor('#f5f5f5');

        Blank(100)
      }
      .width('100%');
    }
    .width('100%')
    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true });
  }
  // 下拉偏移量跟踪(控制进度条)
  .onOffsetChange((offset: number) => {
    this.refreshOffset = offset;
  })
  // 刷新状态变化跟踪
  .onStateChange((state: RefreshStatus) => {
    this.refreshState = state;
  })
  // 触发刷新时的逻辑
  .onRefreshing(() => {
    // 模拟网络请求延迟(实际项目可替换为接口请求)
    setTimeout(() => {
      this.refreshRandomTips(); // 刷新随机数据
      this.refreshing = false; // 结束刷新状态
    }, 1000); // 1秒刷新动画
  })
  .width('100%')
  .height('100%')
  .backgroundColor('#f5f5f5');
}

半模态转场触发两次下拉动画是鸿蒙Next已知问题。该现象由转场动画与下拉手势冲突导致,系统在转场过程中错误地重复触发了下拉动画。目前华为官方已识别此问题,预计在后续版本更新中修复。

根据你提供的代码片段,问题很可能出在 bindSheet 的调用方式上。从你的描述“好像开了两个半模态界面叠加”和代码来看,核心原因是 bindSheet 被多次绑定到了同一个响应式变量 $$this.isShowTip

问题分析

  1. 重复绑定bindSheet 是一个状态驱动的模态框绑定方法。在你的 .onClick 回调中,每次点击都会执行 bindSheet(...)。如果这个点击事件被多次触发(例如,按钮被快速点击,或者组件因状态更新而多次重建),就会导致同一个 $$this.isShowTip 变量被绑定多个半模态实例。
  2. 动画叠加:当 isShowTip 变为 false 时,所有被绑定的半模态实例都会同时执行关闭动画。由于它们几乎是同时被创建和触发的,你就会看到“两次下拉动画”的叠加效果,即第一个实例动画还未结束,第二个实例的动画紧接着开始。

解决方案

bindSheet 的调用移出 .onClick 回调,放到组件的 build() 方法或装饰器 @Builder 中,确保它只被声明和绑定一次。

这是修复后的代码结构示例:

// 在您的组件内部
@State isShowTip: boolean = false;
private currentTip: TipType; // 假设的类型

// 半模态内容构建器(保持不变)
@Builder
myBuilder() {
  Column() {
    HealthTipsArticle({
      currentTip: this.currentTip,
    });
  }
  .width('100%');
}

// 在build方法中,一次性绑定半模态
build() {
  Column() {
    // ... 你的其他UI组件 ...

    // 你的点击触发按钮或组件
    Button('显示提示')
      .onClick(() => {
        this.currentTip = tip; // 设置当前提示
        this.isShowTip = true; // 仅控制状态变量
      })
  }
  // 关键:将bindSheet放在这里,它只会在build时绑定一次。
  .bindSheet($$this.isShowTip, this.myBuilder(), {
    title: { title: '健康小tips' },
    radius: { topStart: LengthMetrics.vp(25), topEnd: LengthMetrics.vp(25) },
  });
}

关键修改点

  • 分离逻辑onClick 回调内只负责更新数据this.currentTip)和改变控制状态this.isShowTip = true)。
  • 单次绑定bindSheet 方法被移到 build() 方法中与UI一起声明。这样,无论点击多少次,半模态只与 $$this.isShowTip 绑定一次。状态变化会驱动同一个模态框的显示与隐藏,而不会创建多个实例。

额外检查项

如果按照以上修改后问题依旧,请检查:

  1. 组件重建:确保包含此代码的组件本身没有因为父组件的状态变化而被意外地多次重建。这可能导致 bindSheet 被重新执行。
  2. 事件冒泡:确认触发 .onClick 的元素没有嵌套在多个可点击组件内,导致事件被多次触发。

通过将状态控制与模态框绑定分离,可以确保半模态转场动画只由单一状态驱动,从而解决动画触发两次的问题。

回到顶部