场景描述

透明页面也可以叫做弹窗页面,实际开发场景中经常有一个页面覆盖在另一个页面上的效果,例如:评论弹窗页面、广告弹窗页面等。

场景:评论弹窗页面

功能点:

  • 弹窗页面拉起。
  • 评论页面状态持久化保存。
  • 带参页面拉起。

方案一:使用router+subWindow实现

router路由无法更改页面模式,所以无法直接实现透明页面,需要借助拉起子窗口的方案实现透明页面的效果。思路如下:

  1. 获取窗口实例。
  2. 拉起一个子窗口并加载对应页面。
  3. 设置子窗口背景透明。
  4. 定义子窗口的关闭方案。

核心代码

在Ability中获取windowStage实例。

onWindowStageCreate(windowStage: window.WindowStage): void {
  // Main window is created, set main page for this ability
  windowStage.loadContent('pages/Index', (err) => {
  // 这里需要注意为了确保windowStage实例获取成功,我们最好在loadContent回调中回去,能保证页面加载成功的时候一定能讲windowStage实例存到AppStorage对象中
  AppStorage.setOrCreate("windowStage", windowStage);
});
}

创建一个子窗口作为页面载体,并加载RouterOpacityPage页面。

private windowClass: window.WindowStage | null = null
aboutToAppear(): void {
  this.windowClass = AppStorage.get("windowStage") as window.WindowStage;
}
 
build() {
  ...
  Button("使用router路由")
    .onClick(() => {
      this.windowClass?.createSubWindow("routerOpacityPage", (err, win) => {
        win.setUIContent('pages/RouterOpacityPage');
        win.showWindow();
      })
    })
  ...
}

加载页面后,这时候出现的新页面发现并不是透明的,那么我们把页面跟容器设置背景颜色为透明,也没有效果,根因是窗口默认是不透明的,需要设置窗口背景色。

@Entry
@Component
struct RouterOpacityPage {
  aboutToAppear(): void {
    // 设置当前窗口背景透明
    window.findWindow("routerOpacityPage").setWindowBackgroundColor("#00000000");
  }
 
  build() {
    ...
  }
}

需要注意的是,子窗口无法与主窗口事件交互,并且默认的手势返回也无法销毁,所以需要自己监听页面的返回手势来销毁子窗口来实现回到原页面的效果。

onBackPress(): boolean | void {
  // 这里解释下为什么需要用显示动画,因为窗口消失的时候无法对窗口添加动画,在转场动画中动画结束回调不生效,所以只能通过显示动画来控制组件显影然后在结束回调同销毁窗口
  animateTo({
  duration: 300, onFinish: () => {
    window.findWindow("routerOpacityPage").destroyWindow().then((res) => {
      console.log("destroyWindow success");
    }).catch(() => {
      console.log("destroyWindow fail");
    })
  }
}, () => {
  this.opacityValue = 0;
})
return true;
}

RouterOpacityPage 完整代码如下:

import { window } from '@kit.ArkUI'
 
@Entry
@Component
struct RouterOpacityPage {
  @State opacityValue: number = 1;
 
  aboutToAppear(): void {
    // 设置当前窗口背景透明
    window.findWindow("routerOpacityPage").setWindowBackgroundColor("#00000000");
  }
 
  onBackPress(): boolean | void {
    // 这里解释下为什么需要用显示动画,因为窗口消失的时候无法对窗口添加动画,在转场动画中动画结束回调不生效,所以只能通过显示动画来控制组件显影然后在结束回调同销毁窗口
    animateTo({
      duration: 300, onFinish: () => {
        window.findWindow("routerOpacityPage").destroyWindow().then((res) => {
          console.log("destroyWindow success");
        }).catch(() => {
          console.log("destroyWindow fail");
        })
      }
    }, () => {
      this.opacityValue = 0;
    })
    return true;
  }
 
  build() {
    Column() {
      Column() {
        Text("页面2").fontSize(50).fontWeight(FontWeight.Bold)
      }
      .backgroundColor(Color.White)
      .borderRadius(20)
      .width("80%")
      .height("60%")
      .justifyContent(FlexAlign.Center)
    }
    .opacity(this.opacityValue)
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
    .backgroundColor("#60000000")
    .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
  }
}

以上使用subWindow的方案实现了一个简单的透明页面效果,实际场景中可能还涉及到页面的持久化与参数传递。

页面持久化方案

上面代码中,我们在退出页面的时候使用的window.destroyWindow()方法,会导致整个窗口实例销毁,无法保存页面中的状态,这里我们需要使用window.minimize()方法来隐藏子窗口,而不是销毁子窗口,相关代码如下:

import { window } from '@kit.ArkUI';
import CommentComponent from '../component/CommentComponent';
 
@Entry
@Component
struct OpacityPage {
  @State opacityValue: number = 1;
  @State initialIndex: number = 0;
 
  onBackPress(): boolean | void {
    this.closeSubWindow();
    return true;
  }
 
  onPageShow(): void {
    this.opacityValue = 1;
  }
 
  closeSubWindow() {
    animateTo({
      duration: 300, onFinish: () => {
        // 当转场动画结束的时候执行窗口隐藏效果,注意这里不能使用destroyWindow销毁当前窗口,因为窗口销毁会导致page状态消失
        window.findWindow("OpacityPage").minimize().then((res) => {
          console.log("minimizeWindow success");
        }).catch(() => {
          console.log("minimizeWindow fail");
        })
      }
    }, () => {
      this.opacityValue = 0;
    })
  }
 
  build() {
    Column() {
      Column() {
        CommentComponent({ initialIndex: this.initialIndex })
      }
      .backgroundColor(Color.White)
      .borderRadius(20)
      .width("80%")
      .height("60%")
      .justifyContent(FlexAlign.Center)
      .onClick(() => {})
    }
    .opacity(this.opacityValue)
    .animation({ duration: 300 })
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
    .backgroundColor("#60000000")
    .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
    .onClick(() => {
      this.closeSubWindow()
    })
  }
}

实现效果如下所示:

页面参数传递方案

因为窗口之前没有提供数据传递的API,所以无法直接传递页面参数;但是每个窗口都有自己的UIContext,可以通过UIContext获取其他窗口的router路由栈,并进行参数传递操作,但是因为该方案会造成不必要是内存消耗,影响性能,并且实现起来较复杂这里只提供思路,不做具体实现,相关功能在navigation路由中实现。

方案二:使用DIALOG类型NavDestination实现【推荐】

使用navigation作为路由框架时,实现透明页面只需要设置页面的NavDestinationMode属性为DIALOG模式思路如下:

  1. 使用navigation作为页面跟容器。
  2. 跳转NavDestination页面并设置其mode属性为NavDestinationMode.DIALOG。
  3. 添加自定义转场动画。这里使用的组件转场,可根据实际需要替换为navigation的自定义转场。

核心代码

使用navigation作为跟页面容器。

import { window } from '@kit.ArkUI';
 
@Entry
@Component
struct Index {
  pageInfos: NavPathStack = new NavPathStack();
 
  build() {
    Navigation(this.pageInfos) {
      Column({ space: 8 }) {
        Button("使用navigation路由")
          .onClick(() => {
            this.pageInfos.pushPath({ name: 'RouterOpacityPage2' });
          })
      }
      .height('100%')
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .height('100%')
    .width('100%')
    .hideTitleBar(true)
    .hideBackButton(true)
    .hideToolBar(true)
  }
}

子页面设置当前的页面模式为DIALOG模式。

@Builder
export function RouterOpacityPage2Builder(name: string, param: Object) {
  RouterOpacityPage2();
}
 
@Component
export struct RouterOpacityPage2 {
  @State opacityValue: number = 1;
  pageInfos: NavPathStack = new NavPathStack();
 
  build() {
    NavDestination() {
      Column() {
        Column() {
          Text("页面2").fontSize(50).fontWeight(FontWeight.Bold)
        }
        .width('80%')
        .height('60%')
        .backgroundColor(Color.White)
        .borderRadius(20)
      }
      .width("100%")
      .height("100%")
      .backgroundColor("#60000000")
      .justifyContent(FlexAlign.Center)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
      .opacity(this.opacityValue)
      // 这里的动画可以使用navigation的自定义转场来实现,这里主要针对透明页面效果,动画效果不深入探讨实现
      .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
    }
    // 这里设置当前页面模式为DIALOG模式,默认情况下DIALOG模式就是透明页面
    .mode(NavDestinationMode.DIALOG)
    .hideTitleBar(true)
    .onBackPressed(() => {
      // 与第一种实现方式一样,这里也是用显示动画实现消失动画,具体场景也可以根据自己需要替换为navigation的自定义转场动画实现
      animateTo({
        duration: 300, onFinish: () => {
          this.pageInfos.pop();
        }
      }, () => {
        this.opacityValue = 0;
      })
      return true;
    })
    .onReady((context: NavDestinationContext) => {
      this.pageInfos = context.pathStack;
    })
  }
}

同样的上面实现了一个最简单案例,但是我们实际开发过程中会涉及到参数传递与持久化状态的问题,navigation的参数传递就简单多了,我们在使用NavPathStack.pushPath跳转的时候就传递参数即可,代码如下:

Button("使用navigation路由带参数")
  .onClick(() => {
    this.pageInfos.pushPath({ name: 'RouterOpacityPage2', param: 10 });
  })

RouterOpacityPage2页面接受参数代码如下:

在NavDestination.onShow生命周期中获取路由栈里面的参数信息即可。

@Component
export struct RouterOpacityPage2 {
  @State initialIndex: number = 0;
  pageInfos: NavPathStack = new NavPathStack();
 
  build() {
    NavDestination() {
      ...
    }
    .onShown(() => {
      if (this.pageInfos.getParamByName("RouterOpacityPage2")[0]) {
        this.initialIndex = this.pageInfos.getParamByName("RouterOpacityPage2")[0] as number;
      } else  {
        this.initialIndex = 0;
      }
    })
  }
}

页面持久化方案是不销毁透明页面(RouterOpacityPage2)在路由栈中的信息,即返回首页(HomePage)的时候不要使用NavPathStack.pop方法让页面出栈,而是找到NavPathStack中首页(HomePage)的路由信息使用NavPathStack.push回到首页,这样透明页面(RouterOpacityPage2)在路由栈中的信息不会消失,我们在RouterOpacityPage2中的操作就可以持久化的保存下来,再次打开的时候就会回到我们上次关闭时的状态,相关实现代码如下:

Button("使用navigation带参数持久化")
  .onClick(() => {
    // 实现页面持久化需要使用navigation单例路由模式,当前暂无相关接口直接实现需要手动实现
    let homeIndex = this.pageInfos.getIndexByName("RouterOpacityPage2");
    if (homeIndex.length == 0) {
      this.pageInfos.pushPath({ name: 'RouterOpacityPage2', param: 10 }, false);
      return;
    }
    // 找到路由栈中RouterOpacityPage2的index使用moveIndexToTop接口移动到顶层让其显示
    this.pageInfos.moveIndexToTop(homeIndex.pop(), false)
  })

@Component
export struct RouterOpacityPage2 {
  @State opacityValue: number = 1;
  @State initialIndex: number = 0;
  pageInfos: NavPathStack = new NavPathStack();
 
  build() {
    NavDestination() {
      ...
    }
    .onBackPressed(() => {
      animateTo({
        duration: 300, onFinish: () => {
          if (this.initialIndex) {
            // 实现页面持久化需要使用navigation单例路由模式,当前暂无相关接口直接实现需要手动实现
            let homeIndex = this.pageInfos.getIndexByName("HomePage");
            if (homeIndex.length == 0) {
              this.pageInfos.pushPath({ name: "HomePage" }, false);
              return;
            }
            this.pageInfos.moveIndexToTop(homeIndex.pop(), false);
          } else {
            this.pageInfos.pop();
          }
        }
      }, () => {
        this.opacityValue = 0;
      })
      return true;
    })
  }
}

注意:上面的方式使用时RouterOpacityPage2会一直存在路由栈中,为避免不必要的内存消耗可以根据需要在不需要持久保存的时候对路由栈进行pop出栈处理。

实现效果如下:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐