博客 / 詳情

返回

HarmonyOS鴻蒙開發 - 解決上下兩欄白邊 - 沉浸式效果

鴻蒙應用開發從入門到入行

第九天 - 解決上下兩欄白邊 - 沉浸式效果

預覽器上下兩欄白邊

  • 自從HarmonyOS升級到release版後,很多同學會問貓林老師:為什麼他的預覽器上下有白邊,為什麼明明根容器寫了寬高百分百但沒鋪滿。如下圖

    image-20241210141135933

白邊原因

  • 其實上面的白邊,稱之為狀態欄。上面會放手機wifi信號、電池電量等信息。一般情況下我們不需要把應用中具有交互效果的界面延伸到上面去,免得影響操作。
  • 同樣,下面的白邊稱之為導航欄,也即切換手機內應用的地方。會有一個小橫條方便你切換不同應用以及回到桌面。
  • 如下圖所示

    image-20241210142736603

  • 而HarmonyOS升級到release版本後,特意在預覽器裏把這上下兩欄給你留白空出來,就是為了方便讓開發者知道,自己的界面並沒佔用這兩個區域,所以一般情況下,如果你的應用整體背景顏色就是白色的,其實是無需處理的。

沉浸式效果介紹

  • 根據上面説的白邊情況,如果你的app背景色正好也是白色,那麼可以和上下白邊融為一體,顯得不那麼突兀。但如果你的app是別的顏色,那麼可能會有明顯的突兀感。
  • 舉個例子:大家經常用的美團。我們看看它目前的情況,以及假設有白邊的情況

    image-20241210143740406
    這是美團正常情況,會看到頂部是黃色,狀態欄也變為黃色,視覺效果上渾然一體

  • 以下假設狀態欄白色

    image-20241210144614483

    可以看到視覺效果上會比較突兀

  • 通過對比我們發現,確實在實際app開發過程中,狀態欄上可以不放任何界面元素,但是需要將狀態欄的顏色定義的與app背景色保持一致,才會視覺上顯得更好看,更融為一體。像這樣的效果,我們稱之為沉浸式效果
  • 通過上面的描述我們已經發現沉浸式效果能提供比較好的視覺效果,但如何實現呢?
  • 有三種方案都可以實現:

    • 通過設置Window背景色來實現
    • 通過調用窗口強制全屏佈局接口setWindowLayoutFullScreen() + padding避讓實現 (麻煩)
    • 直接使用擴展到避讓區功能

通過設置Window背景色實現沉浸式

  • 設置窗體背景色實現

    • 先看不設置的情況下,我們寫的一個寬高百分百,且背景顏色為紅色的界面,如下圖,可以看到狀態欄和整體背景色不一致,有明顯突兀感

      image-20241210150551860

    • 此時,我們可以設置窗體全局背景色也為紅色實現視覺沉浸,來到EntryAbility.ets,找到onWindowStageCreate生命週期函數,在windowStage.loadContent回調裏設置如下代碼即可

      windowStage.getMainWindowSync().setWindowBackgroundColor('#ff0000')
    • 注意:這裏只能給16進制顏色,且必須滿6位
    • 效果如下

      image-20241210151024558

      • 此時渾然一體
    • 這種方法雖然簡單,但有缺點:

      1. 預覽器依然會有白邊,只有模擬器或真機運行才能看到效果
      2. 它寫死了顏色,每個App裏不管是哪個頁面都是此顏色,假如你App裏多個頁面的主題顏色不一樣,會導致非常突兀,如下圖

      image-20241210151943780

使用setWindowLayoutFullScreen實現沉浸式

  • 這是Window提供的一個方法,可以設置讓App整屏(即覆蓋狀態欄與導航欄)實現整塊屏幕都可以佈局,但是大部分使用時必須配合避讓偏移,否則會有問題。至於什麼問題呢,我們往下看。
  • 首先來到EntryAbility.ets,繼續找到onWindowStageCreate生命週期函數,在windowStage.loadContent回調裏設置如下代碼即可

    windowStage.getMainWindow().then(w => {
        // 設置佔用全屏
        w.setWindowLayoutFullScreen(true)
    })
  • 這樣雖然實現了沉浸式效果,但也存在了問題,例如,我們第一頁中本來有Button,但是此時Button位置跑到原來的狀態欄去了,如下圖

    image-20241210152535258

  • 這樣的話,會導致原本不該佈局的區域也會存在我們的佈局元素。第一巨醜,第二用户也點擊不了。
  • 因此,我們使用這個方法實現沉浸式時,一般還要做讓頁面根容器padding避讓。也即讓我們佈局的組件,通過padding的方式挪動他們位置,避讓原本的狀態欄和導航欄。
  • 例:

    Column() {
        Button('去下一頁')
          .onClick(() => {
            router.pushUrl({
              url: 'pages/Second'
            })
          })
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Red)
      .padding({ top: 50, bottom: 50 })
  • 此時,我們發現寫的按鈕確實不會被劉海屏擋住了。但是細心的同學發現了,我們這裏寫死的50vp。不合理,有可能給少了,也有可能給多了。畢竟不同設備的狀態欄可能不一樣。所以如果我們使用這種方案還需要獲取屏幕的狀態欄與導航欄的高度。然後把高度存到本地存儲裏,方便所有頁面都可以使用並設置padding
  • 具體步驟:繼續來到onWindowStageCreate,填寫如下代碼

    onWindowStageCreate(windowStage: window.WindowStage): void {
        // Main window is created, set main page for this ability
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    
        windowStage.loadContent('pages/Index', (err) => {
          // windowStage.getMainWindowSync().setWindowBackgroundColor('#ff0000')
          if (err.code) {
            hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
            return;
          }
          
          // 以下是設置沉浸式,以及獲取設備導航條、狀態欄高度的代碼
          windowStage.getMainWindow().then(w => {
            // 設置沉浸式 
            w.setWindowLayoutFullScreen(true)
            // 獲取設備區域參數
            let avoidArea = w.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
            let bottomRectHeight = avoidArea.bottomRect.height; // 獲取到導航條區域的高度
            AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight); // 存到本地存儲
            let topRectHeight = avoidArea.topRect.height; // 獲取狀態欄區域高度
            AppStorage.setOrCreate('topRectHeight', topRectHeight); // 存到本地存儲
    
            // 當區域發生改變(例如豎屏變橫屏),重新獲取一次再保存
            w.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);
              }
            });
          })
          hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
        });
      }
  • 好了,上面每句代碼都有註釋,可以根據註釋去理解。當然咯,我知道你們沒興趣看代碼,所以需要用可以直接複製,反正這個代碼是固定的
  • 然後來到頁面裏,先取出本地存儲的值,且用@StorageProp裝飾器,設置狀態自動更新。

      @StorageProp('bottomRectHeight')
      bottomRectHeight: number = 0;
      @StorageProp('topRectHeight')
      topRectHeight: number = 0;
  • 然後把這兩個變量,設置給根容器的padding即可

      @StorageProp('bottomRectHeight')
      bottomRectHeight: number = 0;
      @StorageProp('topRectHeight')
      topRectHeight: number = 0;
    
      build() {
        Column() {
          Button('去下一頁')
            .onClick(() => {
              router.pushUrl({
                url: 'pages/Second'
              })
            })
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Red)
        .padding({ top: px2vp(this.topRectHeight), bottom: px2vp(this.bottomRectHeight) })
  • 同樣的,其他頁面也如此設置即可
  • 這樣,我們通過一系列操作。以及一段代碼實現了沉浸式效果。但大家也發現明顯的缺點

    1. 預覽器依然沒效果,需要真機或模擬器查看
    2. 代碼過多。好多沒耐心的同學看到這,可能都已經煩躁的想打人了
    3. 這樣子會讓所有頁面都被迫使用沉浸式,如果哪個頁面不需要沉浸式,還需要再此頁面的about裏禁用

      aboutToAppear(): void {
          window.getLastWindow(getContext())
            .then(win => {
              win.setWindowLayoutFullScreen(false)
            })
      }
  • 因此,我們還有最為簡單的一種方式,請往下繼續看

使用expandSafeArea設置沉浸式(推薦)

  • expandSafeArea是一個按需方式的沉浸式方案,它能完美起到哪個頁面需要沉浸式,就在哪個頁面使用即可,絕對不會讓整個App每個頁面都強制沉浸式。而且使用起來非常簡單,只需要在需要沉浸式的頁面的根容器裏設置即可,例

    Column() {
          Button('去下一頁')
            .onClick(() => {
              router.pushUrl({
                url: 'pages/Second'
              })
            })
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Red)
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  • 解釋:參數1固定,參數2是設置需要沉浸式的區域,SafeAreaEdge.TOP代表上面狀態欄沉浸式,SafeAreaEdge.BOTTOM代表下面導航欄沉浸式,此時效果如下

    image-20241210161011646

    • 沒錯,此時不需要啓動模擬器,預覽器也可以直接看到效果!
  • 當然,你也可以只設置讓頂部沉浸式,則第二個參數只要寫一個TOP即可,如下代碼

    Column() {
          // 生略裏面代碼
      }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Red)
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  • 效果如下

    image-20241210161130556

擴展思考:setWindowLayoutFullScreen是否很雞肋?

  • 同學們到目前為止,發現實現沉浸式我們用了三種方案。其中第一種設置Window背景色可以無視掉,這種方法我們基本不會用。而第二種方法能實現,但又比較麻煩,第三種是最容易也最推薦的方式。
  • 但此時請我們思考下:setWindowLayoutFullScreen是否真的一點應用場景都沒有?
  • 要想回答這個問題,我們可以從setWindowLayoutFullScreen的特點入手,大家還記得嗎?setWindowLayoutFullScreen最大的特點是讓app所有頁面都強制全屏(沉浸式),那麼大家仔細想想,有沒有哪種App是需要任意頁面都強制全屏的呢?

    • 沒錯,答案是遊戲!如下圖

    點擊放大

  • 像這樣的,如果以後是遊戲類App,我們必然需要使用setWindowLayoutFullScreen一次性設置所有頁面全屏
  • 因此,這個方法,大家也需要有點印象哦!萬一哪天要用到呢?
  • 請思考:還有沒有除了遊戲以外也可能要用到setWindowLayoutFullScreen的場景呢?把你的想法可以打在評論區
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.