Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

宇智波搬_發表於2019-03-26

作者 : 快樂丸 轉載自 :www.jianshu.com/p/8ead0701d…

前言

目前市面上的劉海屏和水滴屏手機越來越多了,顏值方面是因人而異,有的人覺得很好看,也有人覺得醜爆了,我個人覺得是還可以。但是作為移動開發者來說,這並不是一件好事,越來越多異形屏手機的出現意味著我們需要投入大量精力在適配上(就不提之後會出的摺疊屏手機了)。本文總結了當下主流手機的劉海屏適配方案,鑑於目前Android碎片化的情況,想要覆蓋所有的機型是不可能的,但是能適配一些是一些,總比什麼都不做要好。

所謂劉海屏,指的是手機螢幕正上方由於追求極致邊框而採用的一種手機解決方案。因形似劉海兒而得名——來自百度百科,水滴屏也是類似,為了簡單起見,下文就統稱這兩種為劉海屏了。

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

什麼時候需要適配

這裡先上一張官方的圖

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

從圖中可以看出,劉海區域是鑲嵌在狀態列內部的,劉海區域的高度一般是不超過狀態列高度的。因此,當我們的應用佈局需要佔據狀態列來顯示時,就需要考慮到劉海區域是否會遮擋住頁面上的控制元件或者背景,這就是為什麼將狀態列區域稱為危險區域。如果應用不需要佔據狀態列顯示,全部顯示在安全區域內,那麼恭喜你,不需要做任何適配處理。總結來說,只有當應用需要全屏顯示時才需要進行適配。 全屏顯示無非就是兩種情況:第一種是我們常說的沉浸式狀態列,也就是狀態列透明,頁面的佈局延伸到狀態列顯示,這種情況下狀態列依然可見;第二種是類似應用的閃屏頁風格,頁面全屏顯示,狀態列不可見。這兩種情況下如果不進行適配處理都會產生一些問題。 先來看第一種情況,沉浸式風格。需要將狀態列設定為透明,需要注意只有在Android 4.4(API Level 19)以上才支援設定透明狀態列。有兩種設定方法: 方法一:為Activity設定style,新增一個屬性:

<item name="android:windowTranslucentStatus">true</item>
複製程式碼

方法二:在Activity的onCreate()中為Window新增Flag

public class ImmersiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明狀態列
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }
}
複製程式碼

頁面的佈局很簡單,只包含一個按鈕,為了明顯,我為根佈局設定了一個背景。 activity_immersive.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:orientation="vertical">

    <Button
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />

</LinearLayout>
複製程式碼

執行之後發現按鈕會被劉海區域所遮擋,如圖所示:

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

再說第二種情況,全屏風格,狀態列不可見。同樣有兩種設定方法: 方法一:為Activity設定style,新增屬性:

<item name="android:windowFullscreen">true</item>
<!-- 這裡為了簡單,直接從style中指定一個背景 -->
<item name="android:windowBackground">@mipmap/bg</item>
複製程式碼

方法二:在Activity的OnCreate()中新增程式碼:

public class FullScreenActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 全屏顯示
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
    }
}
複製程式碼

補充說明一點,現在的手機螢幕高寬比例越來越大,我們還需要額外做一下適配才能使應用在所有手機上都能全屏顯示,具體方式有兩種: 方式一:在AndroidManifest.xml中配置支援最大高寬比

<meta-data android:name="android.max_aspect"  
    android:value="ratio_float" />
複製程式碼

或者

 android:maxAspectRatio="ratio_float" (API LEVEL 26)
複製程式碼

說明:以上兩種介面可以二選一,ratio_float = 螢幕高 / 螢幕寬 (如oppo新機型螢幕解析度為2280 x 1080, ratio_float = 2280 / 1080 = 2.11,建議設定 ratio_float為2.2或者更大) 方式二:在AndroidManifest.xml中配置支援分屏,注意驗證分屏下介面相容性

android:resizeableActivity="true"  
複製程式碼

也可以通過設定targetSdkVersion>=24(即Android 7.0),該屬性的值會預設為true,就不需要在AndroidManifest.xml中配置了。 執行之後,我們發現狀態列的部分留出了一條黑邊,看上起很奇怪,這顯然不是我們想要的效果。

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

如何適配

上文中已經展示了劉海屏中全屏顯示帶來的問題,那麼如何去解決呢?

1.沉浸式狀態列的適配

其實沉浸式狀態列帶來的遮擋問題與劉海屏無關,本質上是由於設定了透明狀態列導致佈局延伸到了狀態列中,就算是不具有劉海屏,一定程度上也會造成佈局的遮擋。不過既然劉海屏是處在狀態列當中的,那麼我們就把這種情況也包含在劉海屏的適配中。清楚了原因之後,解決起來就很簡單了,我們只需要讓控制元件或佈局避開狀態列顯示就可以了,具體的解決方法有三種。 方法一.利用fitsSystemWindows屬性 當我們給最外層View設定了android:fitsSystemWindows="true"屬性後,當設定了透明狀態列或者透明導航欄後,就會自動給View新增paddingTop或paddingBottom屬性,這樣就在螢幕上預留出了狀態列的高度,我們的佈局就不會佔用狀態列來顯示了。 activity_immersive.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <Button
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />

</LinearLayout>
複製程式碼

方法二.根據狀態列高度手動設定paddingTop 這種方法的實現本質上和設定fitsSystemWindows是一樣的,首先獲取狀態列高度,然後設定根佈局的paddingTop等於狀態列高度就可以了,程式碼如下:

public class ImmersiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明狀態列
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        LinearLayout llRoot = findViewById(R.id.ll_root);
        // 設定根佈局的paddingTop
        llRoot.setPadding(0, getStatusBarHeight(this), 0, 0);
    }

    /**
     * 獲取狀態列高度
     *
     * @param context
     * @return
     */
    public 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;
    }
}
複製程式碼

方法三.在佈局中新增一個和狀態列高度相同的View 和前兩種方法原理類似,同樣是讓螢幕預留出狀態列的高度,這裡在根佈局中新增了一個透明的View,高度和狀態列高度相同。這種方法的好處是可以自定義填充狀態列View的背景,更靈活地實現我們想要的效果。

public class ImmersiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明狀態列
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        LinearLayout llRoot = findViewById(R.id.ll_root);
        View statusBarView = new View(this);
        statusBarView.setBackgroundColor(Color.TRANSPARENT);
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                getStatusBarHeight(this));
        // 在根佈局中新增一個狀態列高度的View
        llRoot.addView(statusBarView, 0, lp);
    }

    /**
     * 獲取狀態列高度
     *
     * @param context
     * @return
     */
    public 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;
    }
}
複製程式碼

適配之後成功地將控制元件避開了狀態列(危險區域),如下圖所示:

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

2.全屏顯示的適配

對於全屏顯示的情況,處理起來要相對麻煩一些,下面重點說一下這種情況下的適配方案。

2.1.Android P及以上

谷歌官方從Android P開始給開發者提供了劉海屏相關的API,可以通過直接呼叫API來進行劉海屏的適配處理。 通過DisplayCutout類可以獲得安全區域的範圍以及劉海區域(官方的叫法是缺口)的資訊,需要注意只有API Level在28及以上才可以呼叫。

/**
 * 獲得劉海區域資訊
 */
@TargetApi(28)
public void getNotchParams() {
    final View decorView = getWindow().getDecorView();
    if (decorView != null) {
        decorView.post(new Runnable() {
            @Override
            public void run() {
                WindowInsets windowInsets = decorView.getRootWindowInsets();
                if (windowInsets != null) {
                    // 當全屏頂部顯示黑邊時,getDisplayCutout()返回為null
                    DisplayCutout displayCutout = windowInsets.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);
                        }
                    }
                }
            }
        });
    }
}
複製程式碼

這裡我在測試時也發現了一個問題,就是如果是在style中設定了全屏模式,在適配之前,頂部狀態列區域顯示一條黑邊,這時候呼叫getDisplayCutout()獲取DisplayCutout物件返回的結果是null,其實這也不難理解,因為這時候是看不出劉海區域的,但是這樣會導致在適配之前無法通過DisplayCutout判斷是否存在劉海屏,只能在適配後才能獲取到劉海區域資訊,因此只能對於所有裝置都新增適配程式碼。 那麼接下來如何進行適配呢,Android P中增加了一個視窗布局引數屬性layoutInDisplayCutoutMode,該屬性有三個值可以取:

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:預設的佈局模式,僅當劉海區域完全包含在狀態列之中時,才允許視窗延伸到劉海區域顯示,也就是說,如果沒有設定為全屏顯示模式,就允許視窗延伸到劉海區域,否則不允許。 LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:永遠不允許視窗延伸到劉海區域。 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:始終允許視窗延伸到螢幕短邊上的劉海區域,視窗永遠不會延伸到螢幕長邊上的劉海區域。

還有一個LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS模式,目前已經被LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES所取代,不允許使用了,這裡就不提了。 這麼看可能還是有些不理解,接下來我們在一個全屏顯示的頁面分別設定三種佈局模式,看看有什麼區別。

public class FullScreenActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            WindowManager.LayoutParams lp = getWindow().getAttributes();
            // 僅當缺口區域完全包含在狀態列之中時,才允許視窗延伸到劉海區域顯示
//            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
            // 永遠不允許視窗延伸到劉海區域
//            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
            // 始終允許視窗延伸到螢幕短邊上的劉海區域
            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            getWindow().setAttributes(lp);
        }
    }
}
複製程式碼

三種模式下的顯示效果如下圖所示:

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT

image

LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER

image

LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

可以看出,當在全屏顯示情況下,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT和LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER的效果是一樣的,都是在狀態列顯示一條黑邊,也就是不允許視窗布局延伸到劉海區域,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES則允許視窗布局延伸到了劉海區域,這裡需要注意是短邊劉海區域,不過一般市面上的手機劉海區域都是在短邊上的,我是沒見過劉海長在“腰”上的,因此利用這個模式就實現適配了。 通過之前沉浸式狀態列的顯示效果可以看出,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT在此時是允許視窗布局延伸到劉海區域的,因此更證實了只有在全屏顯示的情況下該模式才不允許視窗布局延伸到劉海區域。 適配後效果如下,現在看起來就很舒服了:

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

我這裡為了簡單沒有新增任何控制元件,實際開發中在全屏顯示後我們仍然需要考慮劉海區域是否會遮擋顯示的內容和控制元件,同樣需要避開危險區域來顯示。做法和沉浸式狀態列的適配相同,原理同樣是將佈局下移,預留出狀態列的高度,這裡就不一一列舉了。

2.2 Android P以下

目前市面上的劉海屏手機可以說是琳琅滿目,各大廠商都在追求極致的屏佔比,推出的新機型也基本上都有劉海屏,針對Android P以下的手機,我們只能依照各個廠商提供的適配方案來進行適配。我也查閱了網上的一些適配文章,主要還是針對目前主流的手機品牌,本文總結了華為、小米、Vivo和Oppo的適配方案,其他品牌的手機之後有時間的話可能會再考慮。

華為適配方案

華為官方提供的適配文件:華為劉海屏手機安卓O版本適配指導 文件中提供了很多劉海屏相關的方法,這裡就不一一列舉了,著重看一下我們需要用到的方法。 判斷是否有劉海屏

/**
 * 判斷是否有劉海屏
 *
 * @param context
 * @return true:有劉海屏;false:沒有劉海屏
 */
public static boolean hasNotch(Context context) {
    boolean ret = false;
    try {
        ClassLoader cl = context.getClassLoader();
        Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
        Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
        ret = (boolean) get.invoke(HwNotchSizeUtil);
    } catch (ClassNotFoundException e) {
        Log.e("test", "hasNotchInScreen ClassNotFoundException");
    } catch (NoSuchMethodException e) {
        Log.e("test", "hasNotchInScreen NoSuchMethodException");
    } catch (Exception e) {
        Log.e("test", "hasNotchInScreen Exception");
    } finally {
        return ret;
    }
}
複製程式碼

應用頁面設定使用劉海區顯示 官方提供了兩種適配方案: 方案一.使用新增的meta-data屬性android.notch_support,在應用的AndroidManifest.xml中增加meta-data屬性,此屬性不僅可以針對Application生效,也可以對Activity配置生效。 使用方式如下:

<meta-data android:name="android.notch_support" android:value="true"/>
複製程式碼

可以在Application下新增,意味著該應用的所有頁面,系統都不會做豎屏場景的特殊下移或者是橫屏場景的右移特殊處理。

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <meta-data
        android:name="android.notch_support"
        android:value="true" />
     ...
</application>
複製程式碼

也可以針對指定的Activity新增,意味著可以針對單個頁面進行劉海屏適配,設定了該屬性的Activity系統將不會做特殊處理。

<!-- 全屏顯示頁面 -->
<activity
    android:name=".ui.FullScreenActivity"
    android:screenOrientation="portrait"
    android:theme="@style/FullScreenTheme">
    <meta-data
        android:name="android.notch_support"
        android:value="true" />
</activity>
複製程式碼

方案二.使用給window新增新增的FLAG_NOTCH_SUPPORT 程式碼如下:

/**
 * 設定應用視窗在劉海屏手機使用劉海區
 * <p>
 * 通過新增視窗FLAG的方式設定頁面使用劉海區顯示
 *
 * @param window 應用頁面window物件
 */
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
    if (window == null) {
        return;
    }
    WindowManager.LayoutParams layoutParams = window.getAttributes();
    try {
        Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
        Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
        Object layoutParamsExObj = con.newInstance(layoutParams);
        Method method = layoutParamsExCls.getMethod("addHwFlags", int.class);
        method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
    } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException
            | InvocationTargetException e) {
        Log.e("test", "hw add notch screen flag api error");
    } catch (Exception e) {
        Log.e("test", "other Exception");
    }
}
複製程式碼

官方提供的所有方法我已經放到了工具類HwNotchUtils裡,可以根據需求來使用。

小米適配方案

小米官方提供的適配文件:dev.mi.com/console/doc…

我們同樣看一下關鍵方法。 判斷是否有劉海屏

/**
 * 判斷是否有劉海屏
 *
 * @param context
 * @return true:有劉海屏;false:沒有劉海屏
 */
public static boolean hasNotch(Context context) {
    boolean ret = false;
    try {
        ClassLoader cl = context.getClassLoader();
        Class SystemProperties = cl.loadClass("android.os.SystemProperties");
        Method get = SystemProperties.getMethod("getInt", String.class, int.class);
        ret = (Integer) get.invoke(SystemProperties, "ro.miui.notch", 0) == 1;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        return ret;
    }
}
複製程式碼

應用頁面設定使用劉海區顯示 小米提供的適配方案同樣有兩種(meta-data和Flag),使用方法和華為類似。 方案一.Application級別的控制介面 在 Application 下增加一個 meta-data,用以宣告該應用視窗是否可以延伸到狀態列。

<meta-data
    android:name="notch.config"
    android:value="portrait|landscape"/>
複製程式碼

其中,value的值可以是以下四種

"none" 橫豎屏都不繪製耳朵區

"portrait" 豎屏繪製到耳朵區

"landscape" 橫屏繪製到耳朵區

"portrait|landscape" 橫豎屏都繪製到耳朵區
複製程式碼

這裡的耳朵區指的就是劉海區兩側的狀態列區域

雖然官方文件上說的是Application級別的,但是我覺得也可以針對某一個Activity來配置,不過由於手頭上的手機條件不滿足,我並沒有驗證,如果有小夥伴測試過的話可以反饋一下,我再修正一下這裡的說法。 方案二.Window級別的控制介面 通過給Window新增Flag也可以實現將視窗布局延伸到狀態列中顯示。

/*劉海屏全屏顯示FLAG*/
public static final int FLAG_NOTCH_SUPPORT = 0x00000100; // 開啟配置
public static final int FLAG_NOTCH_PORTRAIT = 0x00000200; // 豎屏配置
public static final int FLAG_NOTCH_HORIZONTAL = 0x00000400; // 橫屏配置

/**
 * 設定應用視窗在劉海屏手機使用劉海區
 * <p>
 * 通過新增視窗FLAG的方式設定頁面使用劉海區顯示
 *
 * @param window 應用頁面window物件
 */
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
    // 豎屏繪製到耳朵區
    int flag = FLAG_NOTCH_SUPPORT | FLAG_NOTCH_PORTRAIT;
    try {
        Method method = Window.class.getMethod("addExtraFlags",
                int.class);
        method.invoke(window, flag);
    } catch (Exception e) {
        Log.e("test", "addExtraFlags not found.");
    }
}
複製程式碼

官方提供的所有方法我已經放到了工具類XiaomiNotchUtils裡,可以根據需求來使用。 這裡說一下我的測試情況,我是用小米8測試的,系統版本已經升到了Android P,利用小米官方提供的適配方法沒有效果,只能用谷歌官方針對Android P的適配方案,這一點小米的官方文件也提到了。

Android劉海屏適配全方案(華為、小米、Vivo、Oppo)

至於Android P以下版本的小米手機,我並沒有測試,如果有哪位大佬測試過了發現有問題可以反饋一下。

Vivo、Oppo適配方案

Vivo官方提供的適配文件:Vivo全面屏應用適配指南 Oppo官方提供的適配文件:Oppo凹形屏適配指南 這裡把Vivo和Oppo放在一起說,官方提供的資料不像華為和小米那麼詳細,只是提供了判斷是否有劉海屏的方法。 Vivo判斷是否有劉海屏

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

/**
 * 判斷是否有劉海屏
 *
 * @param context
 * @return true:有劉海屏;false:沒有劉海屏
 */
public static boolean hasNotch(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", "hasNotchAtVivo ClassNotFoundException");
    } catch (NoSuchMethodException e) {
        Log.e("Notch", "hasNotchAtVivo NoSuchMethodException");
    } catch (Exception e) {
        Log.e("Notch", "hasNotchAtVivo Exception");
    } finally {
        return ret;
    }
}
複製程式碼

Oppo判斷是否有劉海屏

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    // Android P利用官方提供的API適配
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    // 始終允許視窗延伸到螢幕短邊上的缺口區域
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
    getWindow().setAttributes(lp);
} else {
    // Android P以下根據手機廠商的適配方案進行適配
    if (RomUtils.isHuawei() && HwNotchUtils.hasNotch(this)) {
        HwNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
    } else if (RomUtils.isXiaomi() && XiaomiNotchUtils.hasNotch(this)) {
        XiaomiNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
    }
}
}
複製程式碼

至於全屏顯示的適配方案,通過閱讀官方文件和網上的其他適配文章,我個人總結一下就是這兩種品牌的手機在設定全屏顯示時都無需做任何處理(前提是適配了全面屏,上文中提到過如何配置),也就是不會產生黑邊,我們只需要避免佈局中的內容或控制元件不被劉海區域所遮擋就可以了。具體的做法和沉浸式狀態列的適配相同,基本原理還是將視窗布局下移,預留出狀態列的高度。 注:由於手頭沒有這兩種廠商的手機,因此並沒有驗證,這一點確實是我做得不夠嚴謹,有好心的大佬驗證之後歡迎指正。 其實我本來也想列出魅族的適配方案的,但是實在是沒找到官方文件。。。如果有知道的大佬可以提供一下,我後面會把適配方案補上。 適配時的基本邏輯就是先判斷手機的品牌,這裡我利用了一個開源工具類專案AndroidUtilCode,提供了一個獲取手機Rom資訊的工具類RomUtils,用起來很方便,然後判斷是否是劉海屏,針對劉海屏手機新增適配程式碼。完整的適配程式碼如下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    // Android P利用官方提供的API適配
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    // 始終允許視窗延伸到螢幕短邊上的缺口區域
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
    getWindow().setAttributes(lp);
} else {
    // Android P以下根據手機廠商的適配方案進行適配
    if (RomUtils.isHuawei() && HwNotchUtils.hasNotch(this)) {
        HwNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
    } else if (RomUtils.isXiaomi() && XiaomiNotchUtils.hasNotch(this)) {
        XiaomiNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
    }
}
複製程式碼

總結

雖然文中介紹了很多適配的內容,但其實在開發中需要我們適配劉海屏的情況並不多,只有兩種情況需要我們進行考慮: 1.沉浸式狀態列,視窗布局延伸到了狀態列中,是否會遮擋必要的內容或控制元件(處在危險區域)。適配方案就是將視窗布局下移,預留出狀態列的空間。 2.全屏顯示模式,不做適配的話狀態列會呈現一條黑邊。適配方案是首先判斷系統版本,是Android P及以上就按照官方的API來適配,否則根據手機廠商的適配方案進行適配。鑑於目前市面上Android P還沒有普及,為了帶來更好的使用者體驗,我們還是需要多花一些精力來適配各個手機廠商的劉海屏手機。 最後提示一下,本文只列出了四個當下主流手機廠商的適配方案,我自己驗證過的只有華為和小米(只驗證了Android P)的方案,對於Vivo和Oppo的一些結論我可能說得不對,歡迎大家指正。當然,如果大家還需要其他廠商的適配方案,也歡迎提出,我會盡力補上。 相關的程式碼和工具類我已經上傳到了github,可以下載Demo來檢視,大家一起交流

github.com/StephenZKCu…

相關文章