Android P 劉海屏適配全攻略

玉剛說發表於2018-06-08

本文由 玉剛說寫作平臺 提供寫作贊助

原作者:四月葡萄

版權宣告:本文版權歸微信公眾號 玉剛說 所有,未經許可,不得以任何形式轉載

1.前言

先吐槽一下,劉海屏真醜。然而作為苦逼的開發者,還是要去適配劉海屏的。好了,吐槽完畢,進入正題。

劉海啦啦.jpeg
這裡主要是介紹一下Android P中劉海屏的適配以及Android P之前的適配。為什麼要分開呢?因為Android P之前官方還沒提供API來進行適配,都是由各家廠商來提供適配方案的。

2.Android P中的劉海屏適配

2.1 Google對劉海屏的支援介紹

Google將劉海屏命名為螢幕缺口了,這一小節內容摘自Android官方介紹: 螢幕缺口支援

Android P 支援最新的全面屏以及為攝像頭和揚聲器預留空間的凹口螢幕。 通過全新的 DisplayCutout 類,可以確定非功能區域的位置和形狀,這些區域不應顯示內容。 要確定這些凹口螢幕區域是否存在及其位置,請使用 getDisplayCutout() 函式。

全新的視窗布局屬性 layoutInDisplayCutoutMode 讓您的應用可以為裝置凹口螢幕周圍的內容進行佈局。 您可以將此屬性設為下列值之一:

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER

您可以按如下方法在任何執行 Android P 的裝置或模擬器上模擬螢幕缺口:

  1. 啟用開發者選項。
  2. Developer options(開發者選項) 螢幕中,向下滾動至 Drawing(繪圖) 部分並選擇 Simulate a display with a cutout(模擬具有凹口的螢幕)
  3. 選擇凹口螢幕的大小。

通過使用模擬器測試凹口螢幕.png
注:我們建議您通過使用執行 Android P 的裝置或模擬器測試凹口螢幕周圍的內容顯示。

2.2 Android P提供提供的劉海屏適配方案

  1. 對於有狀態列的頁面,不會受到劉海屏特性的影響,因為劉海屏包含在狀態列中了;
  2. 全屏顯示的頁面,系統劉海屏方案會對應用介面做下移處理,避開劉海區顯示,這時會看到劉海區域變成一條黑邊,完全看不到劉海了;
  3. 已經適配Android P應用的全屏頁面可以通過谷歌提供的適配方案使用劉海區,真正做到全屏顯示。

2.3 Android P中支援的凹口螢幕型別

目前Android支援了三類凹口螢幕型別:邊角螢幕凹口(斜劉海)雙螢幕凹口(劉海+鬍子)長型螢幕凹口(劉海),如下圖所示:

邊角螢幕凹口(斜劉海).png

雙螢幕凹口(劉海+鬍子).png

長型螢幕凹口(劉海).png

目前的手機主要還是長型螢幕凹口,即劉海屏。其他斜劉海和鬍子手機應該還沒有實物吧?反正是亮瞎了狗眼了。

2.4 劉海屏佈局及安全區域說明

佈局區域.png

2.5 Android P中凹口螢幕相關介面

注意,以下介面都是要Build.VERSION.SDK_INT >= 28才能呼叫到。

2.5.1 DisplayCutout類介面

主要用於獲取凹口位置和安全區域的位置等。主要介面如下所示:

方法 介面說明
getBoundingRects() 返回Rects的列表,每個Rects都是螢幕上非功能區域的邊界矩形。
getSafeInsetLeft () 返回安全區域距離螢幕左邊的距離,單位是px。
getSafeInsetRight () 返回安全區域距離螢幕右邊的距離,單位是px。
getSafeInsetTop () 返回安全區域距離螢幕頂部的距離,單位是px。
getSafeInsetBottom() 返回安全區域距離螢幕底部的距離,單位是px。

來看下例子。 這裡將開發者選項中的模擬具有凹口的螢幕選項改為雙螢幕凹口,即這裡應當有兩個劉海,然後,直接上程式碼:

public class NotchActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //開局就一張背景圖
        setContentView(R.layout.notch);

        getNotchParams();
    }

    @TargetApi(28)
    public void getNotchParams() {
        final View decorView = getWindow().getDecorView();

        decorView.post(new Runnable() {
            @Override
            public void run() {
                DisplayCutout displayCutout = decorView.getRootWindowInsets().getDisplayCutout();
                Log.e("TAG", "安全區域距離螢幕左邊的距離 SafeInsetLeft:" + displayCutout.getSafeInsetLeft());
                Log.e("TAG", "安全區域距離螢幕右部的距離 SafeInsetRight:" + displayCutout.getSafeInsetRight());
                Log.e("TAG", "安全區域距離螢幕頂部的距離 SafeInsetTop:" + displayCutout.getSafeInsetTop());
                Log.e("TAG", "安全區域距離螢幕底部的距離 SafeInsetBottom:" + displayCutout.getSafeInsetBottom());
                
                List<Rect> rects = displayCutout.getBoundingRects();
                if (rects == null || rects.size() == 0) {
                    Log.e("TAG", "不是劉海屏");
                } else {
                    Log.e("TAG", "劉海屏數量:" + rects.size());
                    for (Rect rect : rects) {
                        Log.e("TAG", "劉海屏區域:" + rect);
                    }
                }
            }
        });
    }
}
複製程式碼

輸出結果為:

06-04 21:57:10.120 5698-5698/? E/TAG: 安全區域距離螢幕左邊的距離 SafeInsetLeft:0
06-04 21:57:10.120 5698-5698/? E/TAG: 安全區域距離螢幕右部的距離 SafeInsetRight:0
06-04 21:57:10.120 5698-5698/? E/TAG: 安全區域距離螢幕頂部的距離 SafeInsetTop:112
06-04 21:57:10.120 5698-5698/? E/TAG: 安全區域距離螢幕底部的距離 SafeInsetBottom:112
06-04 21:57:10.120 5698-5698/? E/TAG: 劉海屏數量:2
06-04 21:57:10.120 5698-5698/? E/TAG: 劉海屏區域:Rect(468, 0 - 972, 112)
06-04 21:57:10.120 5698-5698/? E/TAG: 劉海屏區域:Rect(468, 2448 - 972, 2560)
複製程式碼

可以看到,即距離頂部和底部各112px的區域就是安全區域了。

2.5.2 設定凹口螢幕顯示模式

使用例子:

    WindowManager.LayoutParams lp = getWindow().getAttributes();
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
    getWindow().setAttributes(lp);
複製程式碼

Android P中新增了一個佈局引數屬性layoutInDisplayCutoutMode,包含了三種不同的模式,如下所示:

模式 模式說明
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 只有當DisplayCutout完全包含在系統欄中時,才允許視窗延伸到DisplayCutout區域。 否則,視窗布局不與DisplayCutout區域重疊。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 該視窗決不允許與DisplayCutout區域重疊。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 該視窗始終允許延伸到螢幕短邊上的DisplayCutout區域。

下面我們來寫個Demo看下這三種模式的顯示效果: Demo很簡單,就是顯示一張背景圖,相關背景佈局就不貼了,來看下主要的程式碼:

public class NotchActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //去掉標題
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        //開局就一張背景圖
        setContentView(R.layout.notch);

        //全屏顯示
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

        WindowManager.LayoutParams lp = getWindow().getAttributes();
        
        //下面圖1
        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
        //下面圖2
//        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        //下面圖3
//        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
        getWindow().setAttributes(lp);
    }
}
複製程式碼

這裡設定為全屏的顯示效果,三種模式的結果如下圖所示:

LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER.png
圖一可以看到上面有黑邊。

LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES.png
圖二明顯看到有劉海。

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT.png
圖三同樣是黑邊。

可以看到:

  1. LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式會讓螢幕到延申劉海區域中。
  2. LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER模式不會讓螢幕到延申劉海區域中,會留出一片黑色區域。
  3. LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT模式在全屏顯示下跟LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER一樣。

我們再來看看LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT模式在沉浸式狀態列下的效果,程式碼如下:

public class NotchActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //去掉標題
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        //開局就一張背景圖
        setContentView(R.layout.notch);

        //全屏顯示
//        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

        //沉浸式狀態列
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

        WindowManager.LayoutParams lp = getWindow().getAttributes();

        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;

        getWindow().setAttributes(lp);

    }
}
複製程式碼

如下圖所示:

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT.png

可以看到:

當劉海區域完全在系統的狀態列時,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT的顯示效果與LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES一致。

所以,當我們進行劉海屏的適配時,請根據實際情況去使用不同的layoutInDisplayCutoutMode

2.6 那麼劉海屏該如何適配呢?

2.6.1 如果頁面存在狀態列

  • 那麼很簡單,不用適配,因為劉海區域會包含在狀態列中了。
  • 如果不想看到劉海區域,可以使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER將劉海區域變成一條黑色邊。

2.6.2 如果頁面是全屏顯示

  • 不適配的話將會留出一條黑色邊。
  • 要做到真正全屏的話,那麼就先要獲取到劉海的區域(危險區域),內容部分(操作按鈕等)應當避開危險區域,保證在安全區域中展示。橫屏的話兩邊都需要注意避開劉海(危險區域)。

3.Android P之前的劉海屏適配

上面是Android P才有的解決方案,在P之前呢,上面的程式碼通通都沒用。然而我們偉大的國產廠商在Android P之前(基本都是Android O)就用上了高檔大氣上檔次的劉海屏,所以,這也造就了各大廠商在Android P之前的解決方案百花齊放。下面,我們來看下主流廠商:華為、vivo、OPPO、小米等所提供的方案。

注:相關的程式碼都已封裝好,可以直接拷貝使用。

3.1 華為

3.1.1 使用劉海區顯示

使用新增的meta-data屬性android.notch_support。 在應用的AndroidManifest.xml中增加meta-data屬性,此屬性不僅可以針對Application生效,也可以對Activity配置生效。 如下所示:

<meta-data android:name="android.notch_support" android:value="true"/>
複製程式碼
  • Application生效,意味著該應用的所有頁面,系統都不會做豎屏場景的特殊下移或者是橫屏場景的右移特殊處理。
  • Activity生效,意味著可以針對單個頁面進行劉海屏適配,設定了該屬性的Activity系統將不會做特殊處理。

實際上還有一種程式碼實現的方式,不過程式碼比較多,這裡就不貼了,有興趣的話可以在文末的連結中點進去看看。

3.1.2 是否有劉海屏

通過以下程式碼即可知道華為手機上是否有劉海屏了,true為有劉海,false則沒有。

    public static boolean hasNotchAtHuawei(Context context) {
        boolean ret = false;
        try {
            ClassLoader classLoader = context.getClassLoader();
            Class HwNotchSizeUtil = classLoader.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
            ret = (boolean) get.invoke(HwNotchSizeUtil);
        } catch (ClassNotFoundException e) {
            Log.e("Notch", "hasNotchAtHuawei ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("Notch", "hasNotchAtHuawei NoSuchMethodException");
        } catch (Exception e) {
            Log.e("Notch", "hasNotchAtHuawei Exception");
        } finally {
            return ret;
        }
    }
複製程式碼

3.1.3 劉海尺寸

華為提供了介面獲取劉海的尺寸,如下:

    //獲取劉海尺寸:width、height
    //int[0]值為劉海寬度 int[1]值為劉海高度
    public static int[] getNotchSizeAtHuawei(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("getNotchSize");
            ret = (int[]) get.invoke(HwNotchSizeUtil);
        } catch (ClassNotFoundException e) {
            Log.e("Notch", "getNotchSizeAtHuawei ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("Notch", "getNotchSizeAtHuawei NoSuchMethodException");
        } catch (Exception e) {
            Log.e("Notch", "getNotchSizeAtHuawei Exception");
        } finally {
            return ret;
        }
    }
複製程式碼

3.2 vivo

vivo在設定--顯示與亮度--第三方應用顯示比例中可以切換是否全屏顯示還是安全區域顯示。

3.2.1 是否有劉海屏

    public static final int VIVO_NOTCH = 0x00000020;//是否有劉海
    public static final int VIVO_FILLET = 0x00000008;//是否有圓角

    public static boolean hasNotchAtVoio(Context context) {
        boolean ret = false;
        try {
            ClassLoader classLoader = context.getClassLoader();
            Class FtFeature = classLoader.loadClass("android.util.FtFeature");
            Method method = FtFeature.getMethod("isFeatureSupport", int.class);
            ret = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
        } catch (ClassNotFoundException e) {
            Log.e("Notch", "hasNotchAtVoio ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("Notch", "hasNotchAtVoio NoSuchMethodException");
        } catch (Exception e) {
            Log.e("Notch", "hasNotchAtVoio Exception");
        } finally {
            return ret;
        }
    }
複製程式碼

3.2.2 劉海尺寸

vivo不提供介面獲取劉海尺寸,目前vivo的劉海寬為100dp,高為27dp。

vivo機型.png

3.3 OPPO

OPPO目前在設定 -- 顯示 -- 應用全屏顯示 -- 凹形區域顯示控制,裡面有關閉凹形區域開關。

3.3.1 是否有劉海屏

    public static boolean hasNotchInScreenAtOPPO(Context context) {
        return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }
複製程式碼

3.3.2 劉海尺寸

OPPO不提供介面獲取劉海尺寸,目前其有劉海屏的機型尺寸規格都是統一的。不排除以後機型會有變化。 其螢幕寬度為1080px,高度為2280px。劉海區域則都是寬度為324px, 高度為80px。

oppo機型.png

3.4 小米

3.4.1 是否有劉海屏

系統增加了 property ro.miui.notch,值為1時則是 Notch 屏手機。

手頭上沒有小米8的手機,暫時沒法驗證,這裡就不貼程式碼了,免得誤導大家。後面測試過再放出來。

3.4.2 劉海尺寸

小米的狀態列高度會略高於劉海屏的高度,因此可以通過獲取狀態列的高度來間接避開劉海屏,獲取狀態列的高度程式碼如下:

    public static int getStatusBarHeight(Context context) {
        int statusBarHeight = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        }
        return statusBarHeight;
    }
複製程式碼

其他手機也可以通過這個方法來間接避開劉海屏,但是有可能有些手機的劉海屏高度會高於狀態列的高度,所以這個方法獲取到的結果並不一定安全。

3.5 其他廠商

如果要適配其他廠商的劉海屏,可以去找下他們的開發者文件,一般都會有提供的,這裡就不詳述了。

4 參考資料:

1.Android P 功能和 API

2.華為劉海屏手機安卓O版本適配指導

3.vivo全面屏應用適配指南

4.OPPO凹形屏適配說明

5.MIUI Notch 屏適配說明

Android P 劉海屏適配全攻略
歡迎關注微信公眾號

相關文章