鸿蒙应用开发-实用技巧-Navigation组件导航

379 阅读4分钟

鸿蒙NEXT配图.webp

1. 前言

本文不会介绍基础使用,若完全不了解,可先看 官方文档, 不会有比此介绍更加详细的了.

本文主要当下(beta5/Api12)使用时出现的问题以及一些实用技巧.

直接上干货

2. NavPathStack#.popToName

  • beta5/api12

2.1 不能夸页面栈带参返回

Navigation的实际功能开发的时候,会有一种页面栈操作,即:

sequenceDiagram
A页面 -> B页面: 启动 B 页面
B页面 -> C页面: 启动 C 页面
C页面 --> A页面: 返回 A 页面

但是目前(api12)不支持从C页面携带返回参数直接给到A页面

// C PAGE
// 略 ...
@Provide({ allowOverride: 'pageStack' }) pageStack: NavPathStack = new NavPathStack()

Button('RETURN A').onClick(() => {
    this.pageStack.popToName('BPage', {
      arg1: 'THIS IS THE RETURN ARG, ORIGIN C PAGE',
      arg2: '页面回到A页面, 但是pop参数仅返回B页面'
    })
})

// 略 ...
//  A页面接收返回参数
this.pageStack.pushDestinationByName('BPage', {}, pop => {
  console.error('PAGE(A), call pop >>>', JSON.stringify(pop))
}, true)

// B页面接收返回参数
this.pageStack.pushDestinationByName('CPage', {}, pop => {
  console.error('PAGE(B), call pop >>>', JSON.stringify(pop))
}, true)

点击后Debugger 截屏2024-09-27 14.20.59.png

虽然页面回到了A, 但是返回参数在 B页面的pop回调中被触发了, A页面没有任何反应.

针对此问题向华为提交了 issue, 但是得到的答复令我无语..

截屏2024-09-27 13.55.29.png

2.2 解决方案

既然华为不认为是问题,那就一定有解决方法.直接上代码

namespace route {
    export function pop(stack: NavPathStack, resultJsonStr?: string, animated?: boolean): NavPathInfo | undefined {
      if (resultJsonStr && resultJsonStr.length > 0) {
        try {
          return stack.pop(JSON.parse(resultJsonStr) as Record<string, Object>, animated)
        } catch (e) {
          console.error('popToName, JSON Parse ERROR: ', JSON.stringify(e))
          // Q: 为什么要传递空对象
          // A: unknown时pop接收方将不会被触发pop回调方法
          return stack.pop({}, animated)
        }
      } else {
        return stack.pop({}, animated)
      }
    }

    /**
     * 跳转路由到之前的指定页面
     *
     * 如果多实例页面可能会有问题, 暂未验证过
     */
    export function popToName(stack: NavPathStack, name: string, resultJsonStr?: string,
      animated?: boolean): Promise<NavPathInfo | undefined> {
      return new Promise(resolve => {
        if (stack.size() == 0) {
          resolve(pop(stack, resultJsonStr, animated))
          return
        }
        const previousIndexArray = stack.getIndexByName(name)
        if (!previousIndexArray || previousIndexArray.length == 0) {
          // 可能传入的参数错误
          resolve(pop(stack, resultJsonStr, animated))
          return
        }
        let startIndex = previousIndexArray.length > 0 ? previousIndexArray[previousIndexArray.length - 1] : 0
        // Q: 为什么 + 2
        // A: 1. getIndexByName 只能获取目标页之前的 index 数组, 例如: [0, 1, 2] 下标2是目标页面, 但是获取到只会是 [0, 1];
        //    2. 要想跳转并且pop带参数, 目标页面index的下一个index 携带 pop参数, 目标页才能触发 pop 回调;
        startIndex += 2
        const len = stack.size()
        if (startIndex < len) {
          let removeArray: number[] = []
          for (let i = startIndex; i < len; i++) {
            if (i != 0) {
              removeArray.push(i)
            }
          }
          stack.removeByIndexes(removeArray)
        }
        resolve(pop(stack, resultJsonStr, animated))
      })
    }
}

调用方法

// C页面
@Provide({ allowOverride: 'pageStack' }) pageStack: NavPathStack = new NavPathStack()

Button('RETURN TOP PAGE(RIGHT)').onClick(() => {
  route.popToName(this.pageStack, 'APage', JSON.stringify({
    arg1: 'THIS IS THE RETURN ARG, ORIGIN C PAGE',
    arg2: '页面回到A页面, pop参数可以带回A页面'
  }))
})

截屏2024-09-27 15.03.29.png

3. NavDestinationMode.DIALOG

  • beta5/api12

3.1 下级页面无动画

当启动顺序如下时: Navigation -> NavDestination(DIALOG) -> NavDestination(STANDARD)

NavDestination(STANDARD) 的页面启动时无转场动画,直接出现在屏幕中

3.2 解决方案

自定义转场

这是我修改后的代码, 官方提供的运行时有一些问题

// CustomNavigationUtils.ts
import { curves } from '@kit.ArkUI';

// 自定义接口,用来保存某个页面相关的转场动画回调和参数
export interface AnimateCallback {
  finish: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
  start: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
  onFinish: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
  interactive: ((operation: NavigationOperation) => void | undefined) | undefined;
  timeout: (number | undefined) | undefined;
}

export interface NavParam {
  name: string,
  startCallback?: (operation: boolean, isExit: boolean) => void,
  endCallback?: (operation: boolean, isExit: boolean) => void,
  onFinish?: (operation: boolean, isExit: boolean) => void,
  interactiveCallback?: (operation: NavigationOperation) => void,
  timeout?: number
}

const customTransitionMap: Map<string, AnimateCallback> = new Map();

export default class CustomTransition {
  static delegate = new CustomTransition();
  interactive: boolean = false;
  proxy: NavigationTransitionProxy | undefined = undefined;
  private animationId: number = 0;
  operation: NavigationOperation = NavigationOperation.PUSH

  static S() {
    return CustomTransition.delegate;
  }

  /* 注册某个页面的动画回调
   * name: 注册页面的唯一id
   * startCallback:用来设置动画开始时页面的状态
   * endCallback:用来设置动画结束时页面的状态
   * onFinish:用来执行动画结束后页面的其他操作
   * interactiveCallback: 注册的可交互转场的动效
   * timeout:转场结束的超时时间
   */
  registerNavParam($$: NavParam): void {
    if (customTransitionMap.has($$.name)) {
      let param = customTransitionMap.get($$.name);
      if (param != undefined) {
        param.start = $$.startCallback;
        param.finish = $$.endCallback;
        param.timeout = $$.timeout;
        param.onFinish = $$.onFinish;
        param.interactive = $$.interactiveCallback;
        return;
      }
    }
    let params: AnimateCallback = {
      timeout: $$.timeout,
      start: $$.startCallback,
      finish: $$.endCallback,
      onFinish: $$.onFinish,
      interactive: $$.interactiveCallback
    };
    customTransitionMap.set($$.name, params);
  }

  getAnimationId() {
    return Date.now();
  }

  unRegisterNavParam(name: string): void {
    customTransitionMap.delete(name);
  }

  fireInteractiveAnimation(id: string, operation: NavigationOperation) {
    let animation = customTransitionMap.get(id)?.interactive;
    if (!animation) {
      return;
    }
    animation(operation);
  }

  updateProgress(progress: number) {
    if (!this.proxy?.updateTransition) {
      return;
    }
    progress = this.operation == NavigationOperation.PUSH ? 1 - progress : progress;
    this.proxy?.updateTransition(progress);
  }

  cancelTransition() {
    if (this.proxy?.cancelTransition) {
      this.proxy.cancelTransition();
    }
  }

  recoverState() {
    if (!this.proxy?.from.navDestinationId || !this.proxy?.to.navDestinationId) {
      return;
    }
    let fromParam = customTransitionMap.get(this.proxy.from.navDestinationId);
    if (fromParam?.onFinish) {
      fromParam.onFinish(false, false);
    }
    let toParam = customTransitionMap.get(this.proxy?.to.navDestinationId);
    if (toParam?.onFinish) {
      toParam.onFinish(true, true);
    }
  }

  finishTransition() {
    this.proxy?.finishTransition();
  }

  finishInteractiveAnimation(rate: number) {
    if (this.operation == NavigationOperation.PUSH) {
      if (rate > 0.5) {
        if (this.proxy?.cancelTransition) {
          this.proxy.cancelTransition();
        }
      } else {
        this.proxy?.finishTransition();
      }
    } else {
      if (rate > 0.5) {
        this.proxy?.finishTransition();
      } else {
        if (this.proxy?.cancelTransition) {
          this.proxy.cancelTransition();
        }
      }
    }
  }

  getAnimateParam(name: string): AnimateCallback {
    let result: AnimateCallback = {
      start: customTransitionMap.get(name)?.start,
      finish: customTransitionMap.get(name)?.finish,
      timeout: customTransitionMap.get(name)?.timeout,
      onFinish: customTransitionMap.get(name)?.onFinish,
      interactive: customTransitionMap.get(name)?.interactive,
    };
    return result;
  }
}


export function CustomNavContentTransition(from: NavContentInfo, to: NavContentInfo,
  operation: NavigationOperation): NavigationAnimatedTransition | undefined {
  // 首页不进行自定义动画
  if (from.index === -1 || to.index === -1) {
    return undefined;
  }

  CustomTransition.S().operation = operation;
  if (CustomTransition.S().interactive) {
    let customAnimation: NavigationAnimatedTransition = {
      onTransitionEnd: (isSuccess: boolean) => {
        console.log("===== current transition is " + isSuccess);
        CustomTransition.S().recoverState();
        CustomTransition.S().proxy = undefined;
      },
      transition: (transitionProxy: NavigationTransitionProxy) => {
        CustomTransition.S().proxy = transitionProxy;
        let targetIndex: string | undefined = operation == NavigationOperation.PUSH ?
          (to.navDestinationId) : (from.navDestinationId);
        if (targetIndex) {
          CustomTransition.S().fireInteractiveAnimation(targetIndex, operation);
        }
      },
      isInteractive: CustomTransition.S().interactive
    }
    return customAnimation;
  }
  let customAnimation: NavigationAnimatedTransition = {
    onTransitionEnd: (isSuccess: boolean) => {
      console.log(`current transition result is ${isSuccess}`)
    },
    timeout: 3000,
    // 转场开始时系统调用该方法,并传入转场上下文代理对象
    transition: (transitionProxy: NavigationTransitionProxy) => {
      if (!from.navDestinationId || !to.navDestinationId) {
        return;
      }
      // 从封装类CustomTransition中根据子页面的序列获取对应的转场动画回调
      let fromParam: AnimateCallback = CustomTransition.S().getAnimateParam(from.navDestinationId);
      let toParam: AnimateCallback = CustomTransition.S().getAnimateParam(to.navDestinationId);
      if (operation == NavigationOperation.PUSH) {
        if (toParam.start) {
          toParam.start(true, false);
        }
        animateTo({
          curve: curves.springMotion(),
          duration: toParam.timeout || 300,
          onFinish: () => {
            transitionProxy.finishTransition();
          }
        }, () => {
          if (toParam.finish) {
            toParam.finish(true, false);
          }
        })
      } else {
        if (fromParam.start) {
          fromParam.start(true, true);
        }
        animateTo({
          curve: curves.springMotion(),
          duration: fromParam.timeout || 300,
          onFinish: () => {
            transitionProxy.finishTransition();
          }
        }, () => {
          if (fromParam.finish) {
            fromParam.finish(true, true);
          }
        })
      }
    }
  };
  return customAnimation;
}


/*
// EXAMPLE
CustomTransition.S().registerNavParam({
  name: this.navDestinationId,
  startCallback: (isPush: boolean, isExit: boolean) => {
    this.customTranslateX = isPush ? '100%' : 0
  },
  endCallback: (isPush: boolean, isExit: boolean) => {
    this.customTranslateX = isPush ? 0 : '100%'
  },
  onFinish: (isPush: boolean, isExit: boolean) => {
    this.customTranslateX = 0
  },
  interactiveCallback: (operation: NavigationOperation) => {
    if (operation == NavigationOperation.PUSH) {
      this.customTranslateX = '100%';
      animateTo({
        duration: 1000,
        onFinish: () => {
          this.customTranslateX = 0;
        }
      }, () => {
        this.customTranslateX = 0;
      })
    } else {
      this.customTranslateX = 0;
      animateTo({
        duration: 1000,
        onFinish: () => {
          this.customTranslateX = 0;
        }
      }, () => {
        this.customTranslateX = '100%';
      })
    }
  },
  timeout: 200
})

if (typeof this.navDestinationId === 'string') {
  CustomTransition.S().unRegisterNavParam(this.navDestinationId)
}
 */

3.3 使用方法

Navigation(this.weboxPageStack)
.customNavContentTransition(CustomNavContentTransition)
@Prop navDestinationId?: string = undefined

NavDestination()
.onReady((context: NavDestinationContext) => {
  this.navDestinationId = context.navDestinationId;
  if (typeof this.navDestinationId === 'string') {
    CustomTransition.S().registerNavParam({
      name: this.navDestinationId,
      startCallback: (isPush: boolean, isExit: boolean) => {
        this.customTranslateY = isPush && !isExit ? '100%' : 0
      },
      endCallback: (isPush: boolean, isExit: boolean) => {
        this.customTranslateY = isPush && !isExit ? 0 : '100%'
      },
    })
  })
.onDisAppear(() => {
  if (typeof this.navDestinationId === 'string') {
    CustomTransition.S().unRegisterNavParam(this.navDestinationId)
  }
})

撰写不易,请给个赞👍吧