🎯 浅谈适配沉浸式页面

⭐⭐⭐⭐

📢 前言

沉浸式模式通常指让应用的界面更加专注于内容,不希望用户被无关元素干扰。在移动端应用中,全屏窗口元素包括状态栏、应用界面和导航栏

📌 见解

1️⃣ 使页面和避让区域的色调统一,为用户提供更好的视觉体验

2️⃣ 最大程度利用屏幕可视区域,使页面获得更大的布局空间

3️⃣ 提供完全沉浸的体验,让用户沉浸其中,不被其他事物所干扰

⚠️ 使用场景

  • 顶部或底部背景延伸

  • 顶部图片延伸

  • 拱洞列表底部延伸

  • 全屏沉浸式

  • 挖空区避让

  • 深色背景喜爱状态栏颜色适配

🧩 拆解

🧱 顶部或底部背景延伸

// 方案一:使用expandSafeArea属性扩展背景组件安全区域
Column()
  .height('100%')
  .width('100%')
  .justifyContent(FlexAlign.Center)
  .backgroundColor('#F1F3F5')
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

// 方案二:使用Window.setWindowLayoutFullScreen()方法设置窗口全屏模式  

// 1、定义相关关键字和接口
const statusBarType = window.AvoidAreaType.TYPE_SYSTEM
const navBarType = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR

interface AvoidArea {
  topRectHeight: number
  bottomRectHeight: number
}

// 2、初始化状态
context = this.getUIContext()?.getHostContext() as common.UIAbilityContext
  private windowClass = this.context.windowStage.getMainWindowSync()
  @State avoidArea: AvoidArea = { topRectHeight: 0, bottomRectHeight: 0 }

 // 3、设置状态栏和导航栏避让区域 && 监听不同设备避让区域的变化
 setAvoidArea() {
  const statusBarArea = this.windowClass.getWindowAvoidArea(statusBarType)
    this.avoidArea.topRectHeight = statusBarArea.topRect.height
  const navBarArea = this.windowClass.getWindowAvoidArea(navBarType)
    this.avoidArea.bottomRectHeight = navBarArea.bottomRect.height
}

onAvoidAreaChange = (data: window.AvoidAreaOptions) => {
  if (data.type === statusBarType) {
    this.avoidArea.topRectHeight = data.area.topRect.height;
  } else if (data.type === navBarType) {
    this.avoidArea.bottomRectHeight = data.area.bottomRect.height;
  }
} 

// 4、生命周期中调用
aboutToAppear(): void {
  this.windowClass.setWindowLayoutFullScreen(true)
  this.setAvoidArea()
  this.windowClass.on('avoidAreaChange', this.onAvoidAreaChange)
}

aboutToDisappear(): void {
  this.windowClass.setWindowLayoutFullScreen(false)
  this.windowClass.off('avoidAreaChange', this.onAvoidAreaChange)
}

// 5、视图使用
Column()
  .height('100%')
  .width('100%')
  .justifyContent(FlexAlign.Center)
  .backgroundColor('#F1F3F5')
  .padding({
    top: this.avoidArea.topRectHeight,
    bottom: this.avoidArea.bottomRectHeight
   })

🧱 顶部图片延伸场景

 Scroll() {
  Column() {
    Swiper(this.swiperController) {
       ForEach(['bakc'], (image: string) => {
          Image($r(`app.media.${image}`))
            .width('100%')
            .height('100%')
       }, (image: Resource) => JSON.stringify(image))
      }
      .height(200)

      Text('汉堡黄🍔')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      x  .height(1000)
    }
  }
  .height('100%')
  .width('100%')
  .backgroundColor('#F1F3F5')
  // TODO: 使用延伸属性扩展出去
  // .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  // 设置如案例一设置顶部安全区边距
  .padding({  top: this.avoidArea.topRectHeight + 'px' })

🧱 滚动列表底部延伸场景

Column() {
  List({ space: 20 }) {
    ListItem() {
      Column() {
        Text('1')
      }
      .height(1000)
    }
    .width('100%')
    .backgroundColor(Color.Green)

     ListItem() {
      Column() {
        Text('2')
      }
      .width('100%')
      .backgroundColor(Color.Gray)
      .height(1000)
    }
  }
  .layoutWeight(1)
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.width('100%')
.height('100%')

🧱 全屏沉浸式场景

aboutToAppear(): void {
  // 在自己业务设置以下代码
  this.windowClass.setWindowSystemBarEnable([])
}

aboutToDisappear(): void {
  this.windowClass.setWindowSystemBarEnable(['status', 'navigation'])
  }

🧱 挖空区避让

/**
 * 通过比较挖孔区域距屏幕左侧边缘和右侧边缘的距离,判断挖孔区域在屏幕左侧、右侧还是中间。
 * 挖孔在中间无需计算两侧按钮左右边距,默认为0。
 * 挖孔在左边,则左侧按钮左边距 = 挖孔距屏幕左侧边缘的距离 + 挖孔区域的宽度。
 * 挖孔在右侧,则右侧按钮右边距 = 挖孔距屏幕右侧边缘的距离 + 挖孔区域的宽度。
 */

// 1、为了方便测试src/main/module.json5
"abilities": [
  {
    "orientation": "auto_rotation", // 支持自动旋转
  }
]
// 2、视图使用
aboutToAppear(): void {
  // 在自己业务设置以下代码
  this.windowClass.setWindowSystemBarEnable([])
}

aboutToDisappear(): void {
  this.windowClass.setWindowSystemBarEnable(['status', 'navigation'])
  }

完整代码

// 1、为了方便测试src/main/module.json5
//"abilities": [
//  {
//    "orientation": "auto_rotation", // 支持自动旋转
//  }
//]

import { common } from '@kit.AbilityKit';
import { display, window } from '@kit.ArkUI';

const statusBarType = window.AvoidAreaType.TYPE_SYSTEM
const navBarType = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR

interface AvoidArea {
  topRectHeight: number
  bottomRectHeight: number
}

interface StatusBarMargin {
  left: number
  right: number
}


@Entry
@Component
struct Index {
  context = this.getUIContext()?.getHostContext() as common.UIAbilityContext
  private windowClass = this.context.windowStage.getMainWindowSync()
  private displayClass = display.getDefaultDisplaySync()
  @State avoidArea: AvoidArea = { topRectHeight: 0, bottomRectHeight: 0 }
  private swiperController: SwiperController = new SwiperController()
  @State statusBarMargin: StatusBarMargin = { left: 0, right: 0 }

  async setStatusBarMargin(windowWidth: number) {
    const cutoutInfo = await this.getCutoutInfo()
    this.statusBarMargin = this.getStatusBarMargin(cutoutInfo, windowWidth)
  }

  async getCutoutInfo() {
    const res = await this.displayClass.getCutoutInfo()
    return res.boundingRects
  }

  getStatusBarMargin(cutoutInfo: display.Rect[], windowWidth: number): StatusBarMargin {
    if (!cutoutInfo || cutoutInfo.length === 0) {
      return { left: 0, right: 0 }
    }
    const cutoutRect = cutoutInfo[0];
    const cutoutLeftGap = cutoutRect.left
    const cutoutWidth = cutoutRect.width
    const cutoutRightGap = windowWidth - cutoutLeftGap - cutoutWidth;
    if (Math.abs(cutoutLeftGap - cutoutRightGap) <= 10) {
      return { left: 0, right: 0 }
    }
    if (cutoutLeftGap < cutoutRightGap) {
      return { left: cutoutLeftGap + cutoutWidth, right: 0 }
    }
    return { left: 0, right: cutoutRightGap + cutoutWidth }
  }

  aboutToAppear(): void {
    // TODO: 测试组件延伸属性需要将下一段代码注释掉
    this.windowClass.setWindowLayoutFullScreen(true)
    this.setAvoidArea()
    this.windowClass.on('avoidAreaChange', this.onAvoidAreaChange)
    this.windowClass.setWindowSystemBarEnable([])
    this.windowClass.on('windowSizeChange',(data) => {
      this.setStatusBarMargin(display.getDefaultDisplaySync().width)
    })
  }

  aboutToDisappear(): void {
    this.windowClass.setWindowLayoutFullScreen(false)
    this.windowClass.off('avoidAreaChange', this.onAvoidAreaChange)
    this.windowClass.setWindowSystemBarEnable(['status', 'navigation'])
  }

  setAvoidArea() {
    const statusBarArea = this.windowClass.getWindowAvoidArea(statusBarType)
    this.avoidArea.topRectHeight = statusBarArea.topRect.height
    const navBarArea = this.windowClass.getWindowAvoidArea(navBarType)
    this.avoidArea.bottomRectHeight = navBarArea.bottomRect.height
  }

  onAvoidAreaChange = (data: window.AvoidAreaOptions) => {
    if (data.type === statusBarType) {
      this.avoidArea.topRectHeight = data.area.topRect.height
    } else if (data.type === navBarType) {
      this.avoidArea.bottomRectHeight = data.area.bottomRect.height
    }
  }

  build() {
    // TODO: 🧱 **滚动列表底部延伸场景** demo
    // Column() {
    //   List({ space: 20 }) {
    //     ListItem() {
    //       Column() {
    //         Text('1')
    //       }
    //       .height(1000)
    //     }
    //     .width('100%')
    //     .backgroundColor(Color.Green)
    //
    //     ListItem() {
    //       Column() {
    //         Text('2')
    //       }
    //       .width('100%')
    //       .backgroundColor(Color.Gray)
    //       .height(1000)
    //     }
    //   }
    //   .layoutWeight(1)
    //   .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    // }
    // .width('100%')
    // .height('100%')

    // TODO: 🧱 **顶部图片延伸场景 || 顶部或底部背景延伸** demo
    // Scroll() {
    //   Column() {
    //     Swiper(this.swiperController) {
    //       ForEach(['bakc'], (image: string) => {
    //         Image($r(`app.media.${image}`))
    //           .width('100%')
    //           .height('100%')
    //       }, (image: Resource) => JSON.stringify(image))
    //     }
    //     .height(200)
    //
    //     Text('汉堡黄🍔')
    //       .fontSize(20)
    //       .fontWeight(FontWeight.Bold)
    //       .height(1000)
    //   }
    // }
    // .height('100%')
    // .width('100%')
    // .backgroundColor('#F1F3F5')
    // // .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    // // .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
    // .padding({
    //   top: this.avoidArea.topRectHeight + 'px',
    //   bottom: this.avoidArea.bottomRectHeight + 'px'
    // })

    // TODO: 🧱 **挖空区避让** demo
    Row() {
      Text('我叫汉堡黄')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Gray)
    .margin({ left: this.statusBarMargin.left + 'px', right: this.statusBarMargin.right + 'px' })
  }
}

📝 沉浸式设计通过消除视觉边界、强化内容聚焦、动态适配系统特性(如深色模式、多窗口形态),为用户提供更流畅、舒适的体验,同时为开发者提供标准化的技术方案(如全屏接口、避让区监听),降低多设备适配成本

🌸🌼🌺

Logo

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

更多推荐