文章

沉浸式效果

沉浸式效果

沉浸式效果

沉浸式效果

文档中心

概述

典型应用全屏窗口UI元素包括状态栏、应用界面和底部导航条,其中状态栏和导航条,通常在沉浸式布局下称为避让区;避让区之外的区域称为安全区。开发应用沉浸式效果主要指通过调整状态栏、应用界面和导航条的显示效果来减少状态栏导航条等系统界面的突兀感,从而使用户获得最佳的UI体验。

图1** **界面元素示意图
1731312743890-27be63d7-a45e-4982-955f-9e160a6f7834.png

开发应用沉浸式效果主要要考虑如下几个设计要素:

  • UI元素避让处理:导航条底部区域可以响应点击事件,除此之外的可交互UI元素和应用关键信息不建议放到导航条区域。状态栏显示系统信息,如果与界面元素有冲突,需要考虑避让状态栏。
  • 沉浸式效果处理:将状态栏和导航条颜色与界面元素颜色相匹配,不出现明显的突兀感。

针对上面的设计要求,可以通过如下两种方式实现应用沉浸式效果:

  • 窗口全屏布局方案:调整布局系统为全屏布局,界面元素延伸到状态栏和导航条区域实现沉浸式效果。当不隐藏避让区时,可通过接口查询状态栏和导航条区域进行可交互元素避让处理,并设置状态栏或导航条的颜色等属性与界面元素匹配。当隐藏避让区时,通过对应接口设置全屏布局即可。
  • 组件安全区方案:布局系统保持安全区内布局,然后通过接口延伸绘制内容(如背景色,背景图)到状态栏和导航条区域实现沉浸式效果。

该方案下,界面元素仅做绘制延伸,无法单独布局到状态栏和导航条区域,针对需要单独布局UI元素到状态栏和导航条区域的场景建议使用窗口全屏布局方案处理。

窗口全屏布局方案

窗口全屏布局方案主要涉及以下应用扩展布局,全屏显示,不隐藏避让区应用扩展布局,隐藏避让区两个应用场景。

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

可以通过调用窗口强制全屏布局接口setWindowLayoutFullScreen()实现界面元素延伸到状态栏和导航条;然后通过接口getWindowAvoidArea()on(‘avoidAreaChange’)获取并动态监听避让区域的变更信息,页面布局根据避让区域信息进行动态调整;设置状态栏或导航条的颜色等属性与界面元素进行匹配。

  1. 调用setWindowLayoutFullScreen()接口设置窗口全屏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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('Succeeded in setting the window layout to full-screen mode.');
      }).catch((err: BusinessError) => {
        console.error('Failed to set the window layout to full-screen mode. Cause:' + JSON.stringify(err));
      });
      // 进行后续步骤2-3中的操作
    });
  }
}
  1. 使用getWindowAvoidArea()接口获取当前布局遮挡区域(例如状态栏、导航条)。
1
2
3
4
5
6
7
8
9
10
11
12
// EntryAbility.ets
// 2. 获取布局避让遮挡的区域
let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR; // 以导航条避让为例
let avoidArea = windowClass.getWindowAvoidArea(type);
let bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航条区域的高度
AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);


type = window.AvoidAreaType.TYPE_SYSTEM; // 以状态栏避让为例
avoidArea = windowClass.getWindowAvoidArea(type);
let topRectHeight = avoidArea.topRect.height; // 获取状态栏区域高度
AppStorage.setOrCreate('topRectHeight', topRectHeight);
  1. 注册监听函数,动态获取避让区域的实时数据。
1
2
3
4
5
6
7
8
9
10
11
// 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);
  }
});
  1. 布局中的UI元素需要避让状态栏和导航条,否则可能产生UI元素重叠等情况。

如下例子中,对控件顶部设置padding(具体数值与状态栏高度一致),实现对状态栏的避让;对底部设置padding(具体数值与底部导航条区域高度一致),实现对底部导航条的避让。如果去掉顶部和底部的padding设置,即不避让状态栏和导航条,UI元素就会发生重叠。具体可见下文步骤中图2和图3的效果对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Index.ets
@Entry
@Component
struct Index {
  @StorageProp('bottomRectHeight')
  bottomRectHeight: number = 0;
  @StorageProp('topRectHeight')
  topRectHeight: number = 0;


  build() {
    Row() {
      Column() {
        Row() {
          Text('DEMO-ROW1').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('DEMO-ROW2').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('DEMO-ROW3').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('DEMO-ROW4').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('DEMO-ROW5').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('DEMO-ROW6').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)
      }
      .width('100%')
        .height('100%')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor('#008000')
        // top数值与状态栏区域高度保持一致;bottom数值与导航条区域高度保持一致
        .padding({ top: px2vp(this.topRectHeight), bottom: px2vp(this.bottomRectHeight) })
    }
  }
}
  1. 根据实际的UI界面显示或相关UI元素背景颜色等,还可以按需设置状态栏的文字颜色、背景色或设置导航条的显示或隐藏,以使UI界面效果呈现和谐。状态栏默认是透明的,透传的是应用界面的背景色。

此例中UI颜色主要有两种,比较简单,故未对状态栏文字颜色、背景色进行设置。

  1. 图2** 布局避让状态栏和导航条
    1731312743894-3fd2ce37-038e-4897-b72c-f47a1bc3775a.jpeg
    图3** **布局未避让状态栏和导航条,UI元素重叠
    1731312743942-10e38d29-615e-42a0-a389-de4fb5e24fba.jpeg

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

此场景下导航条会自动隐藏,适用于游戏、电影等应用场景。可以通过从底部上滑唤出导航条。

1731312743867-4c2dadda-cdae-478a-9a90-29b28532b092.png

  1. 调用setWindowLayoutFullScreen()接口设置窗口全屏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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('Succeeded in setting the window layout to full-screen mode.');
      }).catch((err: BusinessError) => {
        console.error(`Failed to set the window layout to full-screen mode. Code is ${err.code}, message is ${err.message}`);
      });
      // 进行后续步骤2中的状态栏和导航条的隐藏操作
    });
  }
}
  1. 调用setSpecificSystemBarEnabled()接口设置状态栏和导航条的具体显示/隐藏状态,此场景下将其设置为隐藏。
1
2
3
4
5
6
7
8
9
10
11
12
13
// EntryAbility.ets
// 2. 设置状态栏隐藏
windowClass.setSpecificSystemBarEnabled('status', false).then(() => {
  console.info('Succeeded in setting the status bar to be invisible.');
}).catch((err: BusinessError) => {
  console.error(`Failed to set the status bar to be invisible. Code is ${err.code}, message is ${err.message}`);
});
// 2. 设置导航条隐藏
windowClass.setSpecificSystemBarEnabled('navigationIndicator', false).then(() => {
  console.info('Succeeded in setting the navigation indicator to be invisible.');
}).catch((err: BusinessError) => {
  console.error(`Failed to set the navigation indicator to be invisible. Code is ${err.code}, message is ${err.message}`);
});
  1. 在界面中无需进行导航条避让操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Index.ets
@Entry()
@Component
struct Index {
  build() {
    Row() {
      Column() {
        Row() {
          Text('ROW1').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('ROW2').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('ROW3').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('ROW4').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('ROW5').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)


        Row() {
          Text('ROW6').fontSize(40)
        }.backgroundColor(Color.Orange).padding(20)
      }
      .width('100%')
        .height('100%')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor('#008000')
    }
  }
}

组件安全区方案

应用未使用setWindowLayoutFullScreen()接口设置窗口全屏布局时,默认使能组件安全区布局。

应用在默认情况下窗口背景绘制范围是全屏,但UI元素被限制在安全区内(自动排除状态栏和导航条)进行布局,来避免界面元素被状态栏和导航条遮盖。

图4** **界面元素自动避让状态栏和导航条示意图
1731312743960-d5c19321-7c5b-4757-92fa-30b82e41d1b0.png

针对状态栏和导航条颜色与界面元素颜色不匹配问题,可以通过如下两种方式实现沉浸式效果:

  • 状态栏和导航条颜色相同场景,可以通过设置窗口的背景色来实现沉浸式效果。窗口背景色可通过setWindowBackgroundColor()进行设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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, data) => {
      if (err.code) {
        return;
      }


      // 设置全窗颜色和应用元素颜色一致
      windowStage.getMainWindowSync().setWindowBackgroundColor('#008000');
    });
  }
}

界面状态栏和导航条颜色相同场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// xxx.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('ROW1').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW2').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW3').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW4').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW5').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW6').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)
    }
    .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor('#008000')
  }
}

1731312744447-2aadee3f-576c-4dcc-8d32-d32d4f5be0aa.png

  • 状态栏和导航条颜色不同时,可以使用expandSafeArea属性扩展安全区域属性进行调整。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// xxx.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('Top Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor('#F08080')
        // 设置顶部绘制延伸到状态栏
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])


      Row() {
        Text('ROW2').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW3').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW4').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW5').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('Bottom Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor(Color.Orange)
        // 设置底部绘制延伸到导航条
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .width('100%').height('100%').alignItems(HorizontalAlign.Center)
      .backgroundColor('#008000')
      .justifyContent(FlexAlign.SpaceBetween)
  }
}

1731312744441-de9583bd-6292-40a8-b48c-57886738c8fd.png

扩展安全区域属性原理

  • 布局阶段按照安全区范围大小进行UI元素布局。
  • 布局完成后查看设置了expandSafeArea的组件边界(不包括margin)是否和安全区边界相交。
  • 如果设置了expandSafeArea的组件和安全区边界相交,根据expandSafeArea传递的属性则进一步扩大组件绘制区域大小覆盖状态栏、导航条这些非安全区域。
  • 上述过程仅改变组件自身绘制大小,不进行二次布局,不影响子节点和兄弟节点的大小和位置。
  • 子节点可以单独设置该属性,只需要自身边界和安全区域重合就可以延伸自身大小至非安全区域内,需要确保父组件未设置clip等裁切属性。
  • 配置expandSafeArea属性组件进行绘制扩展时,需要关注组件不能配置固定宽高尺寸,百分比除外。

背景图和视频场景

设置背景图、视频控件大小为安全区域大小并配置expandSafeArea属性。

1
2
3
4
5
6
7
8
9
10
11
12
// xxx.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%')
  }
}

1731312744576-82d2824e-3173-4563-9145-6ce00b9f80ac.png

滚动类场景

要求需要List滚动类组件滚动过程中元素可以和导航条重合,滚动至底部时,元素在导航条上面需要避让。

1731312744634-9e21995b-8797-4d0e-9af0-04765c65d7a5.png

由于expandSafeArea不改变子节点布局,因此,List等滚动类组件可以调用expandSafeArea,延伸List组件视图窗口大小而不改变ListItem内在布局。实现ListItem在滑动过程中显示在导航条下,但滚动至最后一个时显示在导航条上,示意图如下:

图5** **未适配时列表下方被导航条遮盖

1731312744526-7fda10e5-1657-4cfe-8e2f-96510255e3ad.png

图6** **List配置expandSafeArea属性后的效果

1731312744805-abbaf0c9-a3ab-4594-8560-da6a6c869beb.png1731312744892-d7837225-3126-4bb6-9f05-7ad75ab8d041.png

仅扩展底部导航条。

  1. 配置窗口整体底色。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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, data) => {
      if (err.code) {
        return;
      }


      windowStage.getMainWindowSync().setWindowBackgroundColor('#DCDCDC'); // 配置窗口整体底色
    });
  }
}
  1. 界面代码展示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// xxx.ets
@Entry
@Component
struct ListExample {
  private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


  build() {
    Column() {
      List({ space: 20, initialIndex: 0 }) {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text('' + item)
              .width('100%')
              .height(100)
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .borderRadius(10)
              .backgroundColor(0xFFFFFF)
          }
        }, (item:number) => item.toString())
      }
      .listDirection(Axis.Vertical) // 排列方向
        .scrollBar(BarState.Off)
        .friction(0.6)
        .divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
        .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
        .width('90%')
        // List组件的视窗范围扩展至导航条。
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }
    .width('100%')
      .height('100%')
      .padding({ top: 15 })
  }
}

底部页签场景

要求页签背景色能够延伸到导航条区域,但页签内部可操作元素需要在导航条之上。

1731312744984-ec470679-a582-40eb-973b-bd04a0960233.png

针对底部的页签部分,Navigation组件和Tabs组件默认实现了页签的延伸处理,开发者只需要保证Navigation和Tabs组件的底部边界和底部导航条重合即可。若开发者显式调用expandSafeArea接口,则安全区效果由expandSafeArea参数指定。

如果未使用上述组件而是采用自定义方式实现页签的场景,可以针对底部元素设置expandSafeArea属性实现底部元素的背景扩展。

图7** **顶部和底部UI元素未设置和设置expandSafeArea属性效果对比
1731312745111-058282e3-3890-4c29-b986-e1a9f56e5d53.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// xxx.ets
@Entry
@Component
struct VideoCreateComponent {
  build() {
    Column() {
      Row() {
        Text('Top Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor('#F08080')
        // 设置顶部绘制延伸到状态栏
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])


      Row() {
        Text('ROW2').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW3').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW4').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('ROW5').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)


      Row() {
        Text('Bottom Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor(Color.Orange)
        // 设置底部绘制延伸到导航条
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .width('100%').height('100%').alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor(Color.Green)
  }
}

图文场景

当状态栏元素和底部导航条元素不同时,无法单纯通过窗口背景色或者背景图组件延伸实现,此时需要对顶部元素和底部元素分别配置expandSafeArea属性,顶部元素配置expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.TOP]),底部元素配置expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.BOTTOM])。

1731312745201-f63b98b5-83e3-4589-9bb2-ed64294676a4.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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)
  }
}
本文由作者按照 CC BY 4.0 进行授权