HarmonyOS 鸿蒙Next中Navigation一镜到底动画闪动

HarmonyOS 鸿蒙Next中Navigation一镜到底动画闪动

Navigation一镜到底动画闪动

NavDestination间一镜到底动画效果差,有闪动。

// 基础页
@ComponentV2
export struct EditorPage {
  @Local isShowSheet: boolean = false;
  build() {
    Column() {
      Button('点击弹出弹窗')
        .onClick(() => {
          this.isShowSheet = !this.isShowSheet;
        })
        .bindSheet(this.isShowSheet, sheetPageBuilder(), {
          preferType: SheetType.CENTER,
          detents: [SheetSize.LARGE],
          height: '100%',
          showClose: false,
          dragBar: false,
          maskColor: '#c6b1b0b0',
          onWillDismiss: () => {
            this.isShowSheet = false;
          }
        })
    }
  }
}

// 承载组件
@ComponentV2
struct sheetBuilder {
  @Provider('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.navPathStack, { name: 'DestinationPageOne' }) {
    }
  }
}

// 模态框构建
@Builder
function sheetPageBuilder() {
  sheetBuilder()
}

// 目标页面一
@ComponentV2
struct DestinationPageOne {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Flex({ direction:FlexDirection.Column, justifyContent: FlexAlign.End }) {
        Row() {
        }
        .width('100%').height(150).backgroundColor(Color.Pink)
        .geometryTransition('ABC', { follow: true })
        .onTouch((e: TouchEvent) => {
          if (e.type === TouchType.Up) {
            this.showBodyDesView();
          }
        })
      }
      .width('100%').height('100%').backgroundColor(Color.Gray)
    }
    .hideTitleBar(true)
    .hideBackButton(true)
  }

  private showBodyDesView(): void {
    this.getUIContext().animateTo({
      curve: curves.interpolatingSpring(0, 1, 342, 38)
    }, () => {
      this.navPathStack.pushPath({ name: "DestinationPageTwo" }, false)
    })
  }
}

@Builder
export function DestinationPageOneBuilder() {
  DestinationPageOne()
}

// 目标页面二
@ComponentV2
struct DestinationPageTwo {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Row() {
        Text('第二弹窗')
      }
      .width('100%').height('100%').backgroundColor(Color.Pink)
      .geometryTransition('ABC')
      .onClick(() => {
        this.onArrowPopNavStack()
      })
    }
    .hideTitleBar(true)
    .hideBackButton(true)
  }

  private onArrowPopNavStack(): void {
    this.getUIContext().animateTo({
      curve: curves.interpolatingSpring(0, 1, 342, 38)
    }, () => {
      this.navPathStack.pop(false);
    })
  }
}

@Builder
export function DestinationPageTwoBuilder() {
  DestinationPageTwo()
}

请问大家是怎么解决的。


更多关于HarmonyOS 鸿蒙Next中Navigation一镜到底动画闪动的实战教程也可以访问 https://www.itying.com/category-93-b0.html

8 回复

开发者您好,

动画闪动是因为第二个页面有白色背景导致感官上闪动,您这边可以设置为.mode(NavDestinationMode.DIALOG)避免闪动的问题,另外您具体希望实现什么样的一镜到底的效果,是否可以提供相关视频方便我们根据您的期待实现效果视频进行修改。

// 目标页面二
@ComponentV2
struct DestinationPageTwo {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Row() {
        Text('第二弹窗')
      }
      .width('100%').height('100%').backgroundColor(Color.Pink)
      .geometryTransition('ABC')
      .onClick(() => {
        this.onArrowPopNavStack()
      })
    }
    .mode(NavDestinationMode.DIALOG)
    .hideTitleBar(true)
    .hideBackButton(true)
  }

  private onArrowPopNavStack(): void {
    this.getUIContext().animateTo({
      curve: curves.interpolatingSpring(0, 1, 342, 38)
    }, () => {
      this.navPathStack.pop(false);
    })
  }
}

更多关于HarmonyOS 鸿蒙Next中Navigation一镜到底动画闪动的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


开发者你好,还请参考下此demo:

import { curves } from '@kit.ArkUI'; // 基础页

@Entry
@ComponentV2
export struct EditorPage {
  @Local isShowSheet: boolean = false;

  build() {
    Column() {
      Button('点击弹出弹窗')
        .onClick(() => {
          this.isShowSheet = !this.isShowSheet;
        })
        .bindSheet(this.isShowSheet, sheetPageBuilder(), {
          preferType: SheetType.CENTER,
          detents: [SheetSize.LARGE],
          height: '100%',
          showClose: false,
          dragBar: false,
          maskColor: '#c6b1b0b0',
          onWillDismiss: () => {
            this.isShowSheet = false;
          }
        })
    }
  }
}

// 承载组件
@ComponentV2
struct sheetBuilder {
  @Provider('pageMain')
  navPathStack: NavPathStack = new NavPathStack();

  aboutToAppear():
    void {
    this
      .navPathStack
      .replacePath({ name: 'DestinationPageOne' }, false);
  }

  build() {
    Navigation(this.navPathStack) {
    }.backgroundColor(Color.Gray).hideTitleBar(true).hideToolBar(true).hideNavBar(true)
  }
} // 模态框构建

@Builder
function sheetPageBuilder() {
  sheetBuilder()
}

// 目标页面一
@ComponentV2
struct DestinationPageOne {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.End }) {
        Row() {
        }
        .width('100%')
        .height(150)
        .backgroundColor(Color.Pink)
        .geometryTransition('ABC', { follow: true })
        .onTouch((e: TouchEvent) => {
          if (e.type === TouchType.Up) {
            this.showBodyDesView();
          }
        })
      }.width('100%').height('100%').backgroundColor(Color.Gray)
    }.hideTitleBar(true).hideBackButton(true).hideToolBar(true)
  }

  private showBodyDesView(): void {
    this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 342, 38) }, () => {
      this.navPathStack.pushPath({ name: "DestinationPageTwo" }, false)
    })
  }
}

@Builder
export function DestinationPageOneBuilder() {
  DestinationPageOne()
}

// 目标页面二
@ComponentV2
struct DestinationPageTwo {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Row() {
        Text('第二弹窗')
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Pink)
      .geometryTransition('ABC')
      .onClick(() => {
        this.onArrowPopNavStack()
      })
    }.backgroundColor(Color.Gray).hideTitleBar(true).hideBackButton(true).hideToolBar(true)
  }

  private onArrowPopNavStack(): void {
    this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 342, 38) }, () => {
      this.navPathStack.pop(false);
    })
  }
}

@Builder
export function DestinationPageTwoBuilder() {
  DestinationPageTwo()
}

routerMap.json配置如下:

{
  "routerMap": [

    {
      "name": "DestinationPageOne",
      "pageSourceFile": "src/main/ets/pages/EditorPage.ets",
      "buildFunction": "DestinationPageOneBuilder"
    },
    {
      "name": "DestinationPageTwo",
      "pageSourceFile": "src/main/ets/pages/EditorPage.ets",
      "buildFunction": "DestinationPageTwoBuilder"
    }
  ]
}

加上了mode,但效果变化无变化。感觉是在bindSheet中用navigation一镜到底的动画会导致两个元素的上下文断裂,就过渡不了。

开发者你好,还请参考下以下实现方案,该场景通过NavigationSystemTransitionType.SLIDE_BOTTOM(https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navdestination#systemtransition14)效果或者NavDestinationMode.DIALOG实现即可。之前设置Dialog不生效是由于pushPath的时候第二个参数传入了false,关闭了系统自带的转场动画。修改代码如下:

// 基础页
@Entry
@ComponentV2
export struct EditorPage {
  @Local isShowSheet: boolean = false;
  build() {
    Column() {
      Button('点击弹出弹窗')
        .onClick(() => {
          this.isShowSheet = !this.isShowSheet;
        })
        .bindSheet(this.isShowSheet, sheetPageBuilder(), {
          preferType: SheetType.CENTER,
          detents: [SheetSize.LARGE],
          height: '100%',
          showClose: false,
          dragBar: false,
          maskColor: '#c6b1b0b0',
          onWillDismiss: () => {
            this.isShowSheet = false;
          }
        })
    }
  }
}

// 承载组件
@ComponentV2
struct sheetBuilder {
  @Provider('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.navPathStack, { name: 'DestinationPageOne' }) {
    }
  }
}

// 模态框构建
@Builder
function sheetPageBuilder() {
  sheetBuilder()
}

// 目标页面一
@ComponentV2
struct DestinationPageOne {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Flex({  direction:FlexDirection.Column, justifyContent: FlexAlign.End }) {
        Row() {
        }
        .width('100%').height(150).backgroundColor(Color.Pink)
        .onClick(() => {
          this.showBodyDesView();
        })
      }
      .width('100%').height('100%').backgroundColor(Color.Gray)
    }
    .hideTitleBar(true)
    .hideBackButton(true)
    .systemTransition(NavigationSystemTransitionType.SLIDE_BOTTOM)

  }

  private showBodyDesView(): void {
    this.navPathStack.pushPath({ name: "DestinationPageTwo" })
  }
}

@Builder
export function DestinationPageOneBuilder() {
  DestinationPageOne()
}

// 目标页面二
@ComponentV2
struct DestinationPageTwo {
  @Consumer('pageMain') navPathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Row() {
        Text('第二弹窗')
      }
      .width('100%').height('100%').backgroundColor(Color.Pink)
      .onClick(() => {
        this.onArrowPopNavStack()
      })
    }
    .hideTitleBar(true)
    .hideBackButton(true)
    .systemTransition(NavigationSystemTransitionType.SLIDE_BOTTOM)
  }

  private onArrowPopNavStack(): void {
    this.navPathStack.pop(true);
  }
}

@Builder
export function DestinationPageTwoBuilder() {
  DestinationPageTwo()
}

router_map.json:

{
  "routerMap": [
    {
      "name": "DestinationPageOne",
      "pageSourceFile": "src/main/ets/pages/EditorPage.ets",
      "buildFunction": "DestinationPageOneBuilder"
    },
    {
      "name": "DestinationPageTwo",
      "pageSourceFile": "src/main/ets/pages/EditorPage.ets",
      "buildFunction": "DestinationPageTwoBuilder"
    }
  ]
}

这个是目标效果,但是现在是尝试了各种方法都不行

最好提供一下完整的代码,基于已知的代码,下面是一个优化后的例子,统一动画容器、对齐系统时序、固定布局约束、兜底页面过渡:

// 全局 NavPathStack 实例
const globalNavPathStack: NavPathStack = new NavPathStack();

// 基础页(触发 Sheet 弹出)
@Entry
@ComponentV2
export struct EditorPage {
  // 局部状态:控制 Sheet 显示/隐藏
  @Local isShowSheet: boolean = false;

  build() {
    Column() {
      Button('点击弹出弹窗')
        .fontSize(18)
        .width(200)
        .height(50)
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .borderRadius(8)
        .onClick(() => {
          this.isShowSheet = !this.isShowSheet;
        })
        // 绑定 Sheet 弹窗,优化配置减少层级冲突
        .bindSheet(this.isShowSheet, sheetPageBuilder(), {
          preferType: SheetType.CENTER,
          detents: [SheetSize.LARGE],
          height: '99.9%', // 避免 100% 高度直接渲染,减少与 Navigation 层级抢占
          showClose: false,
          dragBar: false,
          maskColor: '#c6b1b0b0',
          onWillDismiss: () => {
            this.isShowSheet = false;
          }
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor(Color.White); // 固定基础页背景,避免初始渲染闪动
  }
}

// Navigation 承载组件(Sheet 内容核心)
@ComponentV2
struct sheetBuilder {
  // 提供全局唯一 NavPathStack 实例给子组件
  @Provider('pageMain') navPathStack: NavPathStack = globalNavPathStack;

  build() {
    Navigation(this.navPathStack, { name: 'DestinationPageOne' }) {
      // Navigation 内容区域(此处为空,保持原有业务逻辑)
    }
    .hideTitleBar(true) // 隐藏标题栏,减少额外渲染节点
    .hideBackButton(true) // 隐藏默认返回按钮,自定义交互
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Gray); // 兜底背景,避免过渡留白
  }
}

// Sheet 内容构建器
@Builder
function sheetPageBuilder() {
  sheetBuilder()
}

// 目标页面一(来源页面,配置动画 follow: true)
@ComponentV2
struct DestinationPageOne {
  // 消费全局 NavPathStack 实例
  @Consumer('pageMain') navPathStack: NavPathStack = globalNavPathStack;

  build() {
    NavDestination() {
      // 关键优化3:统一父容器包裹动画内容,绑定 geometryTransition 标识
      Flex({
        direction: FlexDirection.Column,
        justifyContent: FlexAlign.End,
        alignItems: ItemAlign.Center
      }) {
        Row() {
          // 可添加自定义内容,保持原有布局结构
        }
        .width('100%')
        .height(150)
        .backgroundColor(Color.Pink)
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Gray)
      // 关键优化4:仅来源页面配置 follow: true,锚定动画起点
      .geometryTransition('ABC', { follow: true })
      .onTouch((e: TouchEvent) => {
        if (e.type === TouchType.Up) {
          this.showBodyDesView(); // 触发跳转到页面二
        }
      })
    }
    .hideTitleBar(true)
    .hideBackButton(true)
    .width('100%')
    .height('100%');
  }

  // 跳转逻辑:移除多余 animateTo,避免双重动画冲突
  private showBodyDesView(): void {
    this.navPathStack.pushPath({ name: "DestinationPageTwo" }, false);
  }
}

// 页面一构建器(保持原有代码结构)
@Builder
export function DestinationPageOneBuilder() {
  DestinationPageOne()
}

// 目标页面二(目标页面,对应相同动画标识)
@ComponentV2
struct DestinationPageTwo {
  // 消费全局 NavPathStack 实例
  @Consumer('pageMain') navPathStack: NavPathStack = globalNavPathStack;

  build() {
    NavDestination() {
      // 关键优化5:统一父容器包裹,绑定相同 geometryTransition 标识(无 follow)
      Flex({
        direction: FlexDirection.Column,
        justifyContent: FlexAlign.Center,
        alignItems: ItemAlign.Center
      }) {
        Text('第二弹窗')
          .fontSize(24)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center)
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Pink)
      // 关键优化6:目标页面仅绑定标识,不配置 follow,避免双向动画冲突
      .geometryTransition('ABC')
      .onClick(() => {
        this.onArrowPopNavStack(); // 触发返回页面一
      })
    }
    .hideTitleBar(true)
    .hideBackButton(true)
    .width('100%')
    .height('100%');
  }

  // 返回逻辑:移除多余 animateTo,对齐系统动画时序
  private onArrowPopNavStack(): void {
    this.navPathStack.pop(false);
  }
}

// 页面二构建器(保持原有代码结构)
@Builder
export function DestinationPageTwoBuilder() {
  DestinationPageTwo()
}

鸿蒙Next中Navigation一镜到底动画闪动问题,通常与UI渲染机制或动画过渡配置有关。可能涉及页面切换时组件状态未及时同步、动画帧率不稳定或内存回收导致的渲染中断。可检查Navigation组件的动画参数设置,确保页面切换动画的持续时间和曲线函数配置正确。同时,需排查自定义组件在动画过程中的生命周期回调,避免在动画执行期间进行耗时操作或UI重绘。

从代码和描述来看,Navigation一镜到底动画闪动的问题,核心在于geometryTransitionNavDestination切换的时序冲突。你使用了animateTo包裹pushPathpop操作,这会导致动画引擎与Navigation的默认切换动画产生竞争,从而引发视觉上的闪烁或跳帧。

关键问题分析:

  1. 动画嵌套冲突animateTo本身是一个动画过程,而NavPathStackpushPathpop操作内部也默认带有页面切换动画。两者同时执行,尤其是geometryTransition试图在组件树切换过程中保持几何属性连续,极易导致渲染帧的不稳定。
  2. geometryTransition 配置DestinationPageOneRow组件的geometryTransition('ABC', { follow: true })follow: true表示该组件会跟随共享ID的组件(此处为DestinationPageTwo中的Row)。但在NavDestination切换时,源页面和目标页面的组件存在短暂的共存与销毁/创建过程,若时机不当,follow行为可能无法平滑衔接。

直接解决思路:

  • 移除嵌套的animateTo:Navigation的切换本身可以配置动画。更常见的做法是直接调用pushPathpop,而不包裹在animateTo中。系统Navigation的默认动画与geometryTransition配合通常更稳定。将showBodyDesViewonArrowPopNavStack修改为:

    private showBodyDesView(): void {
      this.navPathStack.pushPath({ name: "DestinationPageTwo" }, false);
    }
    private onArrowPopNavStack(): void {
      this.navPathStack.pop();
    }
    

    移除animateTo包裹,让Navigation管理切换动画。pushPath的第二个参数enableTransition已设置为false(你代码中已是),但通常与geometryTransition配合时,使用默认的true(可省略)可能效果更好,因为系统动画会与共享元素过渡协同。

  • 检查geometryTransition的ID作用域:确保共享ID('ABC')在转场双方页面中唯一且对应。你的代码中两个页面都使用了'ABC',这本身是正确的。但需注意,若存在多个geometryTransition,ID必须严格匹配。

  • 简化页面结构进行测试:为了排除干扰,可以暂时将DestinationPageOne中复杂的Flex布局简化为一个与DestinationPageTwoRow样式完全一致的Row,并仅应用geometryTransition,观察闪动是否消失。这有助于确认是否是布局计算差异导致的动画断层。

代码调整示例(重点修改部分):

// DestinationPageOne 修改后
private showBodyDesView(): void {
  // 直接push,移除animateTo包裹
  this.navPathStack.pushPath({ name: "DestinationPageTwo" });
}

// DestinationPageTwo 修改后
private onArrowPopNavStack(): void {
  // 直接pop,移除animateTo包裹
  this.navPathStack.pop();
}

补充建议:

  • 确保NavDestination内的根组件或指定转场组件尺寸、位置稳定,避免在转场前后因布局变化导致跳跃。
  • 如果问题依旧,可以尝试在Navigation组件或NavDestination上调整动画参数,但通常优先考虑上述的结构性调整。

通过移除animateTo的嵌套,让geometryTransition在Navigation的原生页面过渡中独立工作,是解决此类闪动问题的首选方法。

回到顶部