1. 程式人生 > >全面屏/劉海屏及虛擬鍵適配--總結版

全面屏/劉海屏及虛擬鍵適配--總結版

什麼是劉海屏?

隨著iPhone X釋出,劉海屏手機大行其道 ,Google Android P版本的釋出,也引入了劉海屏的概念 即將釋出的Android p也提供了對劉海屏的支援。 華為P20 pro, vivo X21,OPPO R15 華為nova 3e,紅米note6等手機廠商也紛紛推出自己的劉海屏手機app也要提前做好適配。

螢幕的正上方居中位置(下圖黑色區域)會被挖掉一個孔,螢幕被挖掉的區域無法正常顯示內容,這種型別的螢幕就是劉海屏,也有其他叫法:挖孔屏、凹凸屏等等,這裡統一按劉海屏命名。

如果我們的的app沒有適配android p的劉海屏,那麼在顯示的時候變會出現問題, 

1)如沒有狀態列,全屏顯示的App,那麼在Android P版本中顯示如下 

後果:導航欄中title被遮擋

2)如果有狀態列,全屏顯示的App,那麼在Android P版本中顯示如下,你會發現UI出現了黑邊,頁碼也看不清了 

顯示內容下移,頭部出現黑條,底部出現遮擋

以上都是基於標準的劉海屏設計出現的情況,還有一些廠商自定義了劉海屏,即劉海屏的高度大於了狀態列,那麼就會產生類似的問題,如下 

1)狀態列背景高度寫死的問題 

如何適配劉海屏?

由於Android p正式版今日剛釋出, 當前市面上的Android 劉海屏手機還不能用Android 官方提供的方案來解決,那怎麼辦呢?還好幾個廠商自己給出了適配方案。

華為劉海屏適配方案 - 如 P20 pro

華為給出的文件最為詳細,P20 pro預裝系統對未做劉海屏適配處理的app有一定處理,處理邏輯如下

可見,會被華為系統做偏移處理的有2種情況:

1.未設定meta-data值,頁面橫屏狀態

2.未設定meta-data值,頁面豎屏狀態,不顯示狀態列

這2種情況都會出現後果二。如果你的app中頁面沒有這兩種情況,例如都是豎屏且顯示狀態列,你就可以淡定地不做處理。

現在我們知道原因了就可以對症下藥了,這裡給出我推薦的解決方案(官方給出的解決方案不止一種,可以根據自己的需要採用) 分為4步:

1.配置meta-data

<meta-data android:name="android.notch_support" android:value="true"/>

①對Application生效,意味著該應用的所有頁面,系統都不會做豎屏場景的特殊下移或者是橫屏場景的右移特殊處理:

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

2.檢測是否存在劉海屏

public static boolean hasNotchInScreen(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;

    }

}

3.獲取劉海屏的引數

public static int[] getNotchSize(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("test", "getNotchSize ClassNotFoundException");

    } catch (NoSuchMethodException e) {

        Log.e("test", "getNotchSize NoSuchMethodException");

    } catch (Exception e) {

        Log.e("test", "getNotchSize Exception");

    } finally {

        return ret;

    }

}

4. UI適配

沒錯,第1步僅僅是告訴EMUI系統不要瞎操作你的頁面,真正適配的活還要你自己幹。

①判斷是否劉海屏,程式碼上面給出了

②如果是劉海屏手機需要應用自己調整佈局避開劉海區,佈局原則:保證重要的文字、圖片和視訊資訊、可點選的控制元件和圖示還有應用彈窗等等佈局建議顯示在狀態列區域以下(安全區域);不重要,遮擋不會出現問題的佈局可以延伸到狀態列區域(危險區域)顯示,按照這種佈局原則修改,可以一次修改就能適配所有的劉海屏手機:

vivo & OPPO劉海屏適配

vivo 和 OPPO官網僅僅給出了適配指導,沒有給出具體方案,簡單總結為:

如有是具有劉海屏的手機,豎屏顯示狀態列,橫屏不要在危險區顯示重要資訊或者設定點選事件。

那怎麼知道是不是劉海屏手機呢?

OPPO判斷方法:

public static boolean hasNotchInOppo(Context context){

 return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");

 }

vivo的判斷方法:

public static final int NOTCH_IN_SCREEN_VOIO=0x00000020;//是否有凹槽

public static final int ROUNDED_IN_SCREEN_VOIO=0x00000008;//是否有圓角

public static boolean hasNotchInScreenAtVoio(Context context){

    boolean ret = false;

    try {

        ClassLoader cl = context.getClassLoader();

        Class FtFeature = cl.loadClass("com.util.FtFeature");

        Method get = FtFeature.getMethod("isFeatureSupport",int.class);

        ret = (boolean) get.invoke(FtFeature,NOTCH_IN_SCREEN_VOIO);



    } 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; }

}

google官方對劉海屏做的支援

首選要在build.gradle設定支援 , 其實只要注意如下兩個屬性就好,只有如下設定,才能正確使用Android P版本,其他的build.gradle的配置可以視自己的as版本自定義即可

compileSdkVersion ‘android-P’

targetSdkVersion ‘P’

在AS建立一個Android P(API 28)的模擬器,在設定中開啟劉海屏

預設的Android P的模擬器是關閉劉海屏的,首先看下如何開啟Android P的模擬器中的劉海屏,

1)啟用開發者選項和除錯 

-> 開啟 Settings 應用。 

-> 選擇 System。 

-> 滾動到底部,然後選擇 About phone。 

-> 滾動到底部,點按 Build number 7 次。 

-> 返回上一螢幕,在底部附近可找到 Developer options

2)在 Developer options 螢幕中,向下滾動至 Drawing 部分並選擇 Simulate a display with a cutout

3)選擇螢幕缺口的大小

如圖所示,Google提供了四種劉海屏選擇方案 

一般選第四種

這裡需要注意一個問題:使用Android P模擬器的時候,這裡的模擬凹口屏的四個選項其實實質只改變了劉海屏的位置和寬度,高度(系統劉海屏不會超過狀態列),但是在真機中,這四個選項有可能對應不同的操作

Google預設的適配劉海屏策略是這樣的 

1) 如果應用未適配劉海屏,需要系統對全屏顯示的應用介面做特殊移動處理(豎屏下移處理,橫屏右移處),因此此處出現了黑邊的現象;如果應用頁面佈局不能做到自適應,就會出現佈局問題;如果應用佈局能夠做到自適應,也會有黑邊無法全屏顯示的體驗問題 

2)如果有狀態列的App,則不受劉海屏的影響(有狀態肯定不是全屏,那麼就不會有下移的風險)

google從Android P開始為劉海屏提供支援,目前提供了一個類和三種模式:

所以我們可用這個類判斷是否有劉海的存在以及劉海的位置

DisplayCutout cutout = mContext.getDisplayCutout();

三種模式

A new window layout attribute, layoutInDisplayCutoutMode, allows your app to lay out its content around a device's cutouts. You can set this attribute to one of the following values:

  • 第一種模式:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT :全屏視窗不會使用到劉海區域,非全屏視窗可正常使用劉海區域 //  僅僅當系統提供的bar完全包含了劉海區時才允許window擴充套件到劉海區,否則window不會和劉海區重疊,預設情況下,

    第二種模式:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES :允許window擴充套件到劉海區(原文說的是短邊的劉海區, 目前有劉海的手機都在短邊,所以就不糾結了)

    第三種模式:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER: 不允許window擴充套件到劉海區,視窗不允許和劉海屏重疊

public class AndroidPDemoActivity extends Activity{

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        //給Activity設定為無title,全屏顯示,這段程式碼在無劉海屏顯示效果如下

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);

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

        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;

        this.getWindow().setAttributes(lp);

        setContentView(R.layout.activity_android_p_demo_layout);

    }

}

上述程式碼換成LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, 可以看出顯示效果和DEFAULT是一致的,LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER是不允許使用劉海屏區域,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT全屏視窗不允許使用劉海屏區域

來看LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

可以看出LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES已經允許全屏app使用劉海屏了,只不過狀態列那邊是白色。可以看出LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES已經允許全屏app使用劉海屏了,只不過狀態列那邊是白色。

那我們要如何修復這個問題?採用沉浸式佈局即可,即設定DecorView為SYSTEM_UI_FLAG_FULLSCREEN模式。

那何為沉浸式佈局?這是Android 4.4 (API Level 19)引入一個新的概念,即真正的全屏模式:SystemUI(StatusBar和NavigationBar)也都被隱藏

佈局的時候,你總不能在劉海屏區域內部放控制元件吧,所以這個時候我們就要計算狀態列高度了,在佈局時候,要刻意避開這個區域

直接上程式碼: 

//主activity

public class AndroidPDemoActivity extends Activity{

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        DisplayCutoutDemo displayCutoutDemo = new DisplayCutoutDemo(this);

        displayCutoutDemo.openFullScreenModel();

        setContentView(R.layout.activity_android_p_demo_layout);

        displayCutoutDemo.controlView();

    }

}
/**

* 功能描述: 劉海屏控制

*/

public class DisplayCutoutDemo {

    private Activity mAc;

    public DisplayCutoutDemo(Activity ac) {

        mAc = ac;
    }

    //在使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES的時候,狀態列會顯示為白色,這和主內容區域顏色衝突,

    //所以我們要開啟沉浸式佈局模式,即真正的全屏模式,以實現狀態和主體內容背景一致

    public void openFullScreenModel(){

        mAc.requestWindowFeature(Window.FEATURE_NO_TITLE);

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

        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;

        mAc.getWindow().setAttributes(lp);

        View decorView = mAc.getWindow().getDecorView();

        int systemUiVisibility = decorView.getSystemUiVisibility();

        int flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

                | View.SYSTEM_UI_FLAG_FULLSCREEN;

        systemUiVisibility |= flags;

        mAc.getWindow().getDecorView().setSystemUiVisibility(systemUiVisibility);

    }

    //獲取狀態列高度

    public int getStatusBarHeight(Context context) {

        int result = 0;

        int resourceId = context.getResources().getIdentifier("status_bar_height","dimen", "android");

        if (resourceId > 0) {

            result = context.getResources().getDimensionPixelSize(resourceId);
        }

        Log.d("hwj","**getStatusBarHeight**" + result);

        return result;
    }

    public void controlView(){

        View decorView = mAc.getWindow().getDecorView();

        if(decorView != null){

            Log.d("hwj", "**controlView**" + android.os.Build.VERSION.SDK_INT);

            Log.d("hwj", "**controlView**" + android.os.Build.VERSION_CODES.P);

            WindowInsets windowInsets = decorView.getRootWindowInsets();

            if(windowInsets != null){

                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {

                    DisplayCutout displayCutout = windowInsets.getDisplayCutout();

                    //getBoundingRects返回List<Rect>,沒一個list表示一個不可顯示的區域,即劉海屏,可以遍歷這個list中的Rect,

                    //即可以獲得每一個劉海屏的座標位置,當然你也可以用類似getSafeInsetBottom的api

                    Log.d("hwj", "**controlView**" + displayCutout.getBoundingRects());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetBottom());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetLeft());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetRight());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetTop());

                }
            }
        }
    }
}

<!--佈局檔案-->

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"

    android:background="@android:color/holo_blue_bright"

    tools:context="com.hwj.android.learning.AndroidPDemoActivity">



    <!--假設我們已經通過DisplayCutoutDemo.java的controlView方法提前

     獲得了劉海屏的座標,則這裡坐邊距就可以設定了-->

    <!--在你實際使用android p模擬器的時候,你會發現android.os.Build.VERSION_CODES.P的值居然等於10000

     顧你是無法通過模擬器去實際使用DisplayCutoutDemo類的-->

    <Button

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_marginLeft="800px"/>

    <TextView

        android:id="@+id/android_p_demo_tv"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="Hello World!"/>

</LinearLayout>

這裡有一個需要關注的問題,在使用Android P模擬器的時,android.os.Build.VERSION_CODES.P的值居然等於10000,這說明在實際使用過程中你是無法通過模擬器去實際使用DisplayCutoutDemo類的,也就是無法去獲取劉海屏的具體座標

究其原因,我猜測是Android P版本還未正式釋出,這只是一個debug版本,待正式釋出,這個bug應該就會修復掉了。 

但是隻要我們掌握了適配原理,那就不用擔心了

但是我們應該遵循的一個原則就是:不要在劉海屏那一欄顯示內容,那一塊我們稱為非安全區域,儘量在安全區域去繪製UI

綜述劉海屏的適配: 

1)設定LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式 

2)設定沉浸式佈局模式 

3)計算狀態列高度,進行佈局;如果有特殊UI要求,則可以使用DisplayCutoutDemo類去獲取劉海屏的座標,完成UI

參考連結:

小技巧:

1. //去除title  

2. requestWindowFeature(Window.FEATURE_NO_TITLE);  

3. //去掉Activity上面的狀態列  getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);  

4. //去掉虛擬按鍵全屏顯示  

5. getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);  

在AndroidMainfest.xml中application中顯式宣告支援的最大螢幕高寬比(maximum aspect ratio),目前全面屏螢幕比例,將value設定為2.1(或者更大)即可適配一眾全面屏手機

<meta-data

            android:name="android.max_aspect"

            android:value="2.1" />

函虛擬鍵全屏方法--在onWindowFocusChanged中呼叫如下函式:

/**

     * 隱藏虛擬按鍵,並且全屏

     */

    protected void hideBottomUIMenu() {

        //隱藏虛擬按鍵,並且全屏

        if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) { // lower api

            View v = this.getWindow().getDecorView();

            v.setSystemUiVisibility(View.GONE);

        } else if (Build.VERSION.SDK_INT >= 19) {

            //for new api versions.

            View decorView = getWindow().getDecorView();

            int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN;

            decorView.setSystemUiVisibility(uiOptions);

        }

    }