鸿蒙学习实战之路-应用沉浸式效果全攻略

概述

最近好多小伙伴问我:“西兰花,我做的鸿蒙应用总是感觉界面很割裂,状态栏和导航栏颜色跟我的内容搭不上,咋办呢?” 哎,这不就是传说中的「沉浸式效果」没做好嘛!

今天这篇,我就手把手带你搞定鸿蒙应用的沉浸式布局——让你的界面从"东拼西凑"变身"浑然一体",全程不踩坑!

先给大家看个直观的对比:

界面元素示意图

看到没?典型的手机界面分三块:顶部状态栏(显示时间、信号那些)、中间应用内容区、底部导航区(导航条或三键导航)。其中状态栏和导航区就是咱们常说的「避让区」,中间的才是「安全区」。

开发沉浸式效果其实就是解决两个问题:

  1. UI元素避让:别让你的按钮、文字跑到导航区(会被遮挡或误触),也别和状态栏信息重叠
  2. 视觉风格统一:让状态栏和导航区的颜色、样式和你的应用内容和谐统一,别显得突兀

针对这两个问题,鸿蒙提供了两种实现方案:

咱们一个个来拆解!

窗口全屏布局方案

这个方案就像把你的应用变成一张"全屏海报"——整个屏幕都是你的画布,连状态栏和导航区的位置都能用。但相应地,你得自己操心哪些地方不能放重要内容。

应用扩展布局,全屏显示,不隐藏避让区

这种场景适合需要在状态栏或导航区附近放内容,但又不想被遮挡的情况。比如很多视频APP的顶部标题栏会和状态栏融合。

步骤1:设置窗口全屏

首先,咱们得在Ability里告诉系统:“我的应用要占满整个屏幕!”

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from "@kit.AbilityKit";
import { window } from "@kit.ArkUI";
import { BusinessError } from "@kit.BasicServicesKit";
export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent("pages/Index", (err, data) => {
      if (err.code) {
        return;
      }
      let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口
      // 1. 设置窗口全屏
      let isLayoutFullScreen = true;
      windowClass
        .setWindowLayoutFullScreen(isLayoutFullScreen)
        .then(() => {
          console.info("成功设置窗口全屏模式~ ٩(๑❛ᴗ❛๑)۶");
        })
        .catch((err: BusinessError) => {
          console.error(`设置全屏失败,错误码:${err.code},错误信息:${err.message}`);
        });
      // 进行后续步骤2-3中的操作
    });
  }
}
步骤2:获取避让区域高度

设置全屏后,系统不会自动避让状态栏和导航区了,所以咱们得自己拿到这些区域的尺寸,好调整布局。

// EntryAbility.ets
// 2. 获取布局避让遮挡的区域
let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR; // 先获取导航条避让区
let avoidArea = windowClass.getWindowAvoidArea(type);
let bottomRectHeight = avoidArea.bottomRect.height; // 获取导航区域的高度
AppStorage.setOrCreate("bottomRectHeight", bottomRectHeight); // 存到全局存储,方便UI使用

type = window.AvoidAreaType.TYPE_SYSTEM; // 再获取状态栏避让区
avoidArea = windowClass.getWindowAvoidArea(type);
let topRectHeight = avoidArea.topRect.height; // 获取状态栏区域高度
AppStorage.setOrCreate("topRectHeight", topRectHeight); // 同样存到全局
步骤3:监听避让区域变化

手机可能会旋转、折叠屏可能会展开/折叠,这些情况都会改变避让区的大小。所以咱们得加个监听器,动态更新这些值。

// EntryAbility.ets
// 3. 注册监听函数,动态获取避让区域数据
windowClass.on("avoidAreaChange", (data) => {
  if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
    let topRectHeight = data.area.topRect.height;
    AppStorage.setOrCreate("topRectHeight", topRectHeight);
  } else if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
    let bottomRectHeight = data.area.bottomRect.height;
    AppStorage.setOrCreate("bottomRectHeight", bottomRectHeight);
  }
});
步骤4:在UI中应用避让

现在咱们已经拿到了状态栏和导航区的高度,接下来就可以在页面布局中使用这些值,让内容避开这些区域了。

// Index.ets
@Entry
@Component
struct Index {
  @StorageProp('bottomRectHeight')
  bottomRectHeight: number = 0;
  @StorageProp('topRectHeight')
  topRectHeight: number = 0;
  
  build() {
    Column() {
      Row() {
        Text('顶部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#2786d9')
      
      Row() {
        Text('主要内容 2').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 3').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 4').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 5').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('底部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#96dffa')
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#d5d5d5')
    .justifyContent(FlexAlign.SpaceBetween)
    // 关键!设置上下内边距,让内容避开状态栏和导航区
    .padding({
      top: this.getUIContext().px2vp(this.topRectHeight),
      bottom: this.getUIContext().px2vp(this.bottomRectHeight)
    })
  }
}

🥦 西兰花警告
避让区域的高度可能为0(比如某些全面屏手机隐藏了状态栏),所以别直接用这些值做除法或其他运算,最好加个默认值判断!

看看效果对比:

布局避让效果

未避让效果

应用扩展布局,隐藏避让区

这种场景适合游戏、视频播放器等需要完全沉浸式体验的应用——直接把状态栏和导航区都藏起来!

隐藏避让区示意图

步骤1:设置窗口全屏

这一步和之前一样:

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from "@kit.AbilityKit";
import { window } from "@kit.ArkUI";
import { BusinessError } from "@kit.BasicServicesKit";
export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent("pages/Index", (err, data) => {
      if (err.code) {
        return;
      }
      let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口
      // 1. 设置窗口全屏
      let isLayoutFullScreen = true;
      windowClass
        .setWindowLayoutFullScreen(isLayoutFullScreen)
        .then(() => {
          console.info("成功设置窗口全屏模式~ ٩(๑❛ᴗ❛๑)۶");
        })
        .catch((err: BusinessError) => {
          console.error(`设置全屏失败,错误码:${err.code},错误信息:${err.message}`);
        });
      // 进行后续步骤2中的状态栏和导航区域的隐藏操作
    });
  }
}
步骤2:隐藏状态栏和导航区

接下来,咱们调用接口把状态栏和导航区都隐藏掉:

// EntryAbility.ets
// 2. 设置状态栏隐藏
windowClass
  .setSpecificSystemBarEnabled("status", false)
  .then(() => {
    console.info("状态栏已隐藏~ (∩_∩)");
  })
  .catch((err: BusinessError) => {
    console.error(`隐藏状态栏失败,错误码:${err.code},错误信息:${err.message}`);
  });

// 3. 设置导航区域隐藏
windowClass
  .setSpecificSystemBarEnabled("navigationIndicator", false)
  .then(() => {
    console.info("导航区已隐藏~ (∩_∩)");
  })
  .catch((err: BusinessError) => {
    console.error(`隐藏导航区失败,错误码:${err.code},错误信息:${err.message}`);
  });
步骤3:UI布局(无需避让)

因为已经把避让区都隐藏了,所以咱们的UI布局就不用再考虑避让的问题了,直接铺满整个屏幕就行:

// Index.ets
@Entry()
@Component
struct Index {
  build() {
    Row() {
      Column() {
        Row() {
          Text('顶部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
        }.backgroundColor('#2786d9')
        
        Row() {
          Text('主要内容 2').fontSize(30)
        }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
        
        Row() {
          Text('主要内容 3').fontSize(30)
        }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
        
        Row() {
          Text('主要内容 4').fontSize(30)
        }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
        
        Row() {
          Text('主要内容 5').fontSize(30)
        }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
        
        Row() {
          Text('底部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
        }.backgroundColor('#96dffa')
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor('#d5d5d5')
    }
  }
}

🥦 西兰花小贴士
虽然隐藏了导航区,但用户还是可以通过从底部上滑唤出导航条的,所以别在屏幕最底部放太重要的可点击元素哦!

组件安全区方案

如果你的应用不需要在状态栏或导航区附近放特殊内容,只是想让背景色延伸过去,那组件安全区方案会更简单——系统会自动帮你处理UI的避让!

状态栏和导航区域颜色相同的情况

这种情况最简单,直接设置窗口背景色和应用的背景色一致就行:

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from "@kit.AbilityKit";
import { window } from "@kit.ArkUI";
export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent("pages/Index", (err) => {
      if (err.code) {
        return;
      }
      // 设置全窗颜色和应用元素颜色一致
      windowStage.getMainWindowSync().setWindowBackgroundColor("#d5d5d5");
    });
  }
}

然后UI布局就按照正常方式写,不用管避让的问题:

// Index.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('顶部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#2786d9')
      
      Row() {
        Text('主要内容 2').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 3').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 4').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 5').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('底部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#96dffa')
    }
    .width('100%').height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#d5d5d5')
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

效果如下:

同色状态栏导航区域效果

状态栏和导航区域颜色不同的情况

如果你的应用顶部是蓝色,底部是绿色,那上面的方法就不行了。这时候咱们可以用expandSafeArea属性,让特定的组件背景延伸到避让区:

// Index.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('顶部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#2786d9')
      // 设置顶部绘制延伸到状态栏
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
      
      Row() {
        Text('主要内容 2').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 3').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 4').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 5').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('底部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#96dffa')
      // 设置底部绘制延伸到导航区域
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .width('100%').height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#d5d5d5')
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

效果如下:

不同色状态栏导航区域效果

expandSafeArea属性的工作原理

好多小伙伴可能好奇这个属性到底是怎么工作的,我给大家简单解释一下:

  1. 布局阶段:系统先按照安全区的范围布局UI元素(确保内容不会被遮挡)
  2. 绘制阶段:查看设置了expandSafeArea的组件边界是否和安全区边界相交
  3. 如果相交,就扩大该组件的绘制区域,让它覆盖到状态栏或导航区

🥦 西兰花警告
使用expandSafeArea的组件不能设置固定宽高(百分比可以),否则可能无法正确延伸哦!

常见场景的沉浸式实现

背景图和视频场景

如果你的应用有全屏背景图或视频,想要让它们延伸到状态栏和导航区,可以直接给图片或视频组件设置expandSafeArea

// Index.ets
@Entry
@Component
struct SafeAreaExample1 {
  build() {
    Stack() {
      Image($r('app.media.bg'))
        .height('100%').width('100%')
        // 图片组件的绘制区域扩展至状态栏和导航区域
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }.height('100%').width('100%')
  }
}

效果如下:

背景图沉浸式效果

🥦 西兰花小贴士
Video组件使用expandSafeArea时,只有背景会延伸,视频内容区域不会扩展哦!

滚动类场景

滚动类组件(如Scroll、List)实现沉浸式有两种方法:

方法一:给滚动容器设置expandSafeArea

// Index.ets
@Entry
@Component
struct ScrollExample {
  scroller: Scroller = new Scroller()
  private arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  
  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      Scroll(this.scroller) {
        Column() {
          ForEach(this.arr, (item: number) => {
            Stack() {
              Text('滚动内容 ' + item.toString()).fontSize(30)
            }
            .width('80%').padding(20).borderRadius(15).backgroundColor(Color.White).margin({ top:30, bottom:30 })
          }, (item: string) => item)
        }.width('100%').backgroundColor('rgb(213,213,213)')
      }.backgroundColor('rgb(213,213,213)')
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }.width('100%').height('100%')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }
}

效果如下:

滚动容器沉浸式效果

方法二:设置滚动容器的裁剪属性

// Index.ets
@Entry
@Component
struct ScrollExample {
  scroller: Scroller = new Scroller()
  private arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  
  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      Scroll(this.scroller) {
        Column() {
          ForEach(this.arr, (item: number) => {
            Stack() {
              Text('滚动内容 ' + item.toString()).fontSize(30)
            }
            .width('80%').padding(20).borderRadius(15).backgroundColor(Color.White).margin({ top:30, bottom:30 })
          }, (item: string) => item)
        }.width('100%').backgroundColor('rgb(213,213,213)')
      }.backgroundColor('rgb(213,213,213)')
        .clipContent(ContentClipMode.SAFE_AREA) // 将裁剪区域扩展至避让区
    }.width('100%').height('100%')
  }
}

效果如下:

滚动容器裁剪沉浸式效果

底部页签场景

如果你的应用有底部页签(比如微信、支付宝),想要让页签的背景延伸到导航区,可以直接使用Navigation或Tabs组件——它们默认就支持这种效果!

如果是自定义页签,也可以给底部元素设置expandSafeArea

// Index.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('顶部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#2786d9')
      // 设置顶部绘制延伸到状态栏
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
      
      Row() {
        Text('主要内容 2').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 3').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 4').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('主要内容 5').fontSize(30)
      }.backgroundColor(Color.White).padding(20).borderRadius(15).width('80%')
      
      Row() {
        Text('底部内容').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }.backgroundColor('#96dffa')
      // 设置底部绘制延伸到导航区域
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .width('100%').height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#d5d5d5')
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

效果如下:

页签沉浸式效果对比

图文场景

如果你的应用是图文展示类(比如新闻、小说),顶部是图片,底部是文字,想要让图片延伸到状态栏,文字背景延伸到导航区,可以分别给这两个组件设置expandSafeArea

// Index.ets
@Entry
@Component
struct Index {
  build() {
    Swiper() {
      Column() {
        Image($r('app.media.start'))
          .height('50%').width('100%')
          // 设置图片延伸到状态栏
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
        
        Column() {
          Text('HarmonyOS 第一课')
            .fontSize(32)
            .margin(30)
          Text('通过循序渐进的学习路径,无经验和有经验的开发者都可以掌握ArkTS语言声明式开发范式,体验更简洁、更友好的HarmonyOS应用开发旅程。')
            .fontSize(20).margin(20)
        }.height('50%').width('100%')
        .backgroundColor(Color.White)
        // 设置文本内容区背景延伸到导航栏
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
      }
    }
    .width('100%')
    .height('100%')
    // 关闭Swiper组件默认的裁切效果以便子节点可以绘制在Swiper外
    .clip(false)
  }
}

效果如下:

图文场景沉浸式效果

总结

好啦,关于鸿蒙应用的沉浸式效果,咱们就讲完啦!总结一下两种方案的适用场景:

方案 适用场景 优点 缺点
窗口全屏布局 需要在状态栏/导航区附近放内容,或完全隐藏避让区 灵活性高,完全控制界面 需要自己处理避让逻辑
组件安全区方案 只需要背景延伸,内容在安全区即可 简单,系统自动处理避让 灵活性较低,内容无法进入避让区

🥦 西兰花最后叨叨

  • 沉浸式效果不是必须的,但做好了能极大提升用户体验
  • 实现时要考虑不同设备(手机、平板、折叠屏)的适配
  • 别为了追求视觉效果牺牲可用性,重要内容一定要放在安全区里

希望这篇文章能帮到正在开发鸿蒙应用的你!如果还有疑问,欢迎在评论区留言,咱们一起交流~ ٩(๑>◡<๑)۶

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐