Android劉海屏適配解決方案--NotchTools

image
1、概述
劉海屏指的是手機螢幕上方由於追求極致邊框而採用的方案,表現為在頂部有塊黑色遮擋,長得像劉海,所以叫劉海屏。
目前google在Android P上已經對劉海屏的適配進行了統一,所以在targetApi >= 28上可以使用谷歌官方推薦的適配方案進行劉海屏適配。但是在Android O版本的劉海屏如何適配呢?這就是本文要重點闡述的內容了:
1、對國內四大廠商(華為、小米、OPPO、VIVO)對Android O 版本劉海屏的適配方案進行介紹;
2、提出對Android O 版本劉海屏的通用解決方案,包括全屏佔用劉海屏、全屏不佔用劉海屏兩種情況;
3、提出適配工具 ofollow,noindex">NotchTools 解決方案,讓你的應用簡單快捷的適配全面屏
2、適配與未適配的效果對比
因為相比普通常規手機而言,劉海屏頂部中間會突出一塊劉海區域,所以會在給Actiivty設定全屏Flag的時候有一些不同。本文所涉及到的劉海屏適配都是在給Activity的window設定SYSTEM_UI_FLAG_FULLSCREEN(全屏flag)前提下的,在顯示狀態列的情況下(不管是狀態列透明或者不透明),不是本文討論的核心,我們的所說的劉海屏適配只是針對全屏沉浸式(狀態列隱藏)的情況下。
在設定SYSTEM_UI_FLAG_FULLSCREEN了Flag後,國內廠商的劉海屏手機對於此表現的預設顯示效果都是有差異的,具體為:
1、華為手機預設是全屏但是不佔用劉海區域;
2、小米手機預設是全屏但是不佔用劉海區域;
3、oppo手機預設是全屏但是佔用劉海區域;
4、vivo手機預設是全屏但是不佔用劉海區域;
所以我們再全屏的情況下需要對四大廠商做下適配,不然有可能一個App在不同手機上表現不一致、或者會對UI做了截斷,影響使用體驗:

佔用劉海顯示的劉海屏

未佔用劉海顯示的劉海屏
3、適配方案
Android O的劉海屏適配方案可分為兩種情況:
3、1 全屏且佔用劉海屏的適配方案
對於需要全屏且佔用劉海屏顯示的情況,如沉浸式遊戲、沉浸式閱讀(需要把態欄隱藏),適配時可以採用如下步驟:
1、在Activity中使用setSystemUiVisibility設定全屏的一些標識;
2、根據不同廠商的適配規則(官網有提供)設定不同的flag(大都通過反射),來讓App全屏沉浸式顯示;
3、根據廠商提供的Api,獲取劉海的高度,來調節一些View的間距,達到適配目的。
3、2 全屏但不佔用劉海的適配方案
這種適配方案一般採用如下步驟:
1、去各大手機廠商官網找到對應的全屏但不佔用劉海的方案,目前只有小米、華為提供了具體方法來設定是否佔用劉海區域,oppo和vivo只提供了機型是否是劉海屏手機的方法,但未提供適配方案;
2、華為和小米都有具體方案來適配全屏不佔用劉海的情況,這裡主要對vivo和oppo進行適配。vivo手機其實是不用適配的,因為你會發現不管你怎麼設定,vivo手機永遠都是不佔用劉海區域。oppo手機的話預設是佔用劉海區域的,所以適配的話可以通過在頂部新增一個劉海區域高度相同的黑色view來下移整體佈局,達到適配不佔用劉海的case。
3.3 華為手機劉海屏適配方案

華為手機劉海屏適配方案
3.3.1 判斷華為手機是否為劉海屏手機
@RequiresApi(api = Build.VERSION_CODES.O) @Override public boolean isNotchScreen(Window window) { boolean isNotchScreen = false; try { ClassLoader cl = window.getContext().getClassLoader(); Class HwNotchSizeUtil =cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen"); isNotchScreen = (boolean) get.invoke(HwNotchSizeUtil); } catch (ClassNotFoundException e) { LogUtils.d(TAG, "hasNotchInScreen ClassNotFoundException"); } catch (NoSuchMethodException e) { LogUtils.d(TAG, "hasNotchInScreen NoSuchMethodException"); } catch (Exception e) { LogUtils.d(TAG, "hasNotchInScreen Exception"); } finally { return isNotchScreen; } }
3.3.2 獲取華為手機的劉海屏高度
@RequiresApi(api = Build.VERSION_CODES.O) @Override public int getNotchHeight(Window window) { int[] ret = new int[]{0, 0}; try { ClassLoader cl = window.getContext().getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("getNotchSize"); ret = (int[]) get.invoke(HwNotchSizeUtil); } catch (ClassNotFoundException e) { } catch (NoSuchMethodException e) { } catch (Exception e) { } finally { return ret[1]; } }
3.3.3 設定頁面在華為劉海屏手機使用劉海區
@TargetApi(Build.VERSION_CODES.KITKAT) 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"); } }
3.3.4 設定頁面在華為劉海屏手機不使用劉海區
@RequiresApi(api = Build.VERSION_CODES.KITKAT) public static void setNotFullScreenWindowLayoutInDisplayCutout (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("clearHwFlags", int.class); method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |InstantiationException | InvocationTargetException e) { Log.e("test", "hw clear notch screen flag api error"); } catch (Exception e) { Log.e("test", "other Exception"); } }
3.4 小米手機劉海屏適配方案
3.4.1 判斷小米手機是否為劉海屏手機
@RequiresApi(api = Build.VERSION_CODES.O) @Override public boolean isNotchScreen(Window window) { return "1".equals(SystemProperties.getInstance().get("ro.miui.notch")); }
3.4.2 獲取小米手機劉海屏高度
public static int getStatusBarHeight(Context context) { if (statusBarHeight != -1) { return statusBarHeight; } int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android"); if (resourceId > 0) { statusBarHeight = context.getResources().getDimensionPixelSize(resourceId); } if (statusBarHeight <= 0) { int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resId > 0) { statusBarHeight = context.getResources().getDimensionPixelSize(resId); } } return statusBarHeight; }
3.4.3 設定頁面在小米劉海屏手機使用劉海區
在 WindowManager.LayoutParams 增加 extraFlags 成員變數,用以宣告該 window 是否使用耳朵區,其中,extraFlags 有以下變數:
0x00000100 開啟配置 0x00000200 豎屏配置 0x00000400 橫屏配置
組合後表示 Window 的配置,如:
0x00000100 | 0x00000200 豎屏繪製到耳朵區 0x00000100 | 0x00000400 橫屏繪製到耳朵區 0x00000100 | 0x00000200 | 0x00000400 橫豎屏都繪製到耳朵區
控制 extraFlags 時注意只控制這幾位,不要影響其他位。可以用 Window 的 addExtraFlags 和 clearExtraFlags 來修改, 這兩個方法是 MIUI 增加的方法,需要反射呼叫。
設定頁面在小米劉海屏手機使用劉海區程式碼如下:
public void fullScreenUseStatus(Activity activity, OnNotchCallBack notchCallBack) { super.fullScreenUseStatus(activity, notchCallBack); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isNotchScreen(activity.getWindow())) { //開啟配置 int FLAG_NOTCH = 0x00000100 | 0x00000200 | 0x00000400; try { Method method = Window.class.getMethod("addExtraFlags", int.class); if (!method.isAccessible()) { method.setAccessible(true); } method.invoke(activity.getWindow(), FLAG_NOTCH); } catch (Exception e) { e.printStackTrace(); } } }
3.4.4 設定頁面在小米劉海屏手機不使用劉海區
@RequiresApi(api = Build.VERSION_CODES.O) @Override public void fullScreenDontUseStatus(Activity activity, OnNotchCallBack notchCallBack) { super.fullScreenDontUseStatus(activity, notchCallBack); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isNotchScreen(activity.getWindow())) { //開啟配置 int FLAG_NOTCH = 0x00000100 | 0x00000400; try { Method method = Window.class.getMethod("addExtraFlags", int.class); if (!method.isAccessible()) { method.setAccessible(true); } method.invoke(activity.getWindow(), FLAG_NOTCH); } catch (Exception e) { e.printStackTrace(); } } }
3.4.5 小米劉海屏手機上劉海屏高度與狀態列高度差異
由於 Notch 裝置的狀態列高度與正常機器不一樣,因此在需要使用狀態列高度時,不建議寫死一個值,而應該改為讀取系統的值。
以下是獲取當前裝置狀態列高度的方法:
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = context.getResources().getDimensionPixelSize(resourceId); }
以下是獲取當前裝置劉海高度的方法:
int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android"); if (resourceId > 0) { result = context.getResources().getDimensionPixelSize(resourceId); }
3.5 OPPO手機劉海屏適配方案

image
3.5.1 判斷OPPO手機是否為劉海屏手機
@RequiresApi(api = Build.VERSION_CODES.O) @Override public boolean isNotchScreen(Window window) { if (window == null) { return false; } return window.getContext().getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism"); }
3.5.2 獲取OPPO手機劉海屏高度
目前OPPO劉海屏適配的官網上並沒有給出獲取劉海高度的方法,也沒有給出佔用劉海區域、不佔用劉海區域的方法。
官網上給的圖上的劉海的固定高度是80px,這裡通過獲取狀態列高度的方法得到值也是80,大概猜測OPPO手機的劉海高度是和狀態列高度一樣的。
public int getNotchHeight(Window window) { int statusBarHeight = 0; int resourceId = window.getContext().getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { statusBarHeight = window.getContext().getResources().getDimensionPixelSize(resourceId); } return statusBarHeight ; }
3.5.3 設定頁面在OPPO劉海屏手機使用劉海區
OPPO手機並沒有像小米、華為手機一樣提供具體的方法設定劉海區域,但是OPPO手機在全屏狀態下預設是佔用劉海區域的,是完全沉浸式的,所以只需設定全屏Flag即可:
public static void setFullScreenWithSystemUi(final Window window, boolean setListener) { int systemUiVisibility = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { systemUiVisibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } window.getDecorView().setSystemUiVisibility(systemUiVisibility); if (setListener) { window.getDecorView().setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { if (visibility == 0) { setFullScreenWithSystemUi(window, false); } } }); } }
3.5.4 設定頁面在OPPO劉海屏手機不使用劉海區
因為OPPO手機預設是全屏佔用劉海區域的,所以如果想達到全屏且不佔用劉海區域的話,需要在Activty的頂部通過新增一個狀態列高度的黑色佈局,來下移整體佈局,從而視覺上看起來是已經適配了。
public void fullScreenDontUseStatus(Activity activity, OnNotchCallBack notchCallBack) { super.fullScreenDontUseStatus(activity, notchCallBack); if (isNotchScreen(activity.getWindow())) { if (notchCallBack != null && isNotchScreen(activity.getWindow())) { notchCallBack.onNeedAddNotchStatusBar(true); }
onNeedAddNotchStatusBar方法如下:
View view = new View(this); view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, NotchTools.getFullScreenTools().getNotchHeight(getWindow()))); view.setBackgroundColor(Color.BLACK); mBaseToolbarContainer.addView(view);
程式碼中mBaseToolbarContainer為你的layout佈局中最頂部的view,可以預設是個空佈局,這樣的話通過新增一個狀態列高度的View,從而使整體佈局下移,也就達到了全屏但是不佔用狀態列區域的目的。
3.6 VIVO手機劉海屏適配方案
3.6.1 判斷VIVO手機是否為劉海屏手機
@RequiresApi(api = Build.VERSION_CODES.O) @Override public boolean isNotchScreen(Window window) { if (window == null) { return false; } if (mClass == null) { ClassLoader classLoader = window.getContext().getClassLoader(); try { mClass = classLoader.loadClass("android.util.FtFeature"); mMethod = mClass.getMethod("isFeatureSupport", Integer.TYPE); return (boolean) mMethod.invoke(mClass, 0x00000020); } catch (ClassNotFoundException e) { return false; } catch (NoSuchMethodException e) { return false; } catch (IllegalAccessException e) { return false; } catch (InvocationTargetException e) { return false; } } else { if (mMethod == null) { try { mMethod = mClass.getMethod("isFeatureSupport", Integer.TYPE); } catch (NoSuchMethodException e) { return false; } try { return (boolean) mMethod.invoke(mClass, 0x00000020); } catch (IllegalAccessException e) { return false; } catch (InvocationTargetException e) { return false; } } } return false; }
3.6.2 設定頁面在VIVO劉海屏手機使用劉海區
vivo手機在全屏下,不管如何設定,都不會使用劉海區域,無法適配
3.6.3 設定頁面在VIVO劉海屏手機不使用劉海區
vivo手機在全屏下,不管如何設定,都不會使用劉海區域,無需適配
3.7 在旋轉螢幕時的適配
我們一般會在Activity的onCreate方法中對Activity進行劉海適配,但是在一些涉及到視訊播放的場景下,會有橫屏旋轉隱藏狀態列、豎屏時顯示狀態列的情況,大部分這些邏輯都是寫在底層視訊播放邏輯中的,所以我們在做涉及到有可能重設狀態列Flag的情況下,需要進行一些設定,具體為:
1、在Activity的onCreate中設定SYSTEM_UI_FLAG_FULLSCREEN,且完成劉海屏適配
2、在Activity的onWindowFocusChanged方法中,再次呼叫劉海屏適配方法,防止橫屏時重置了相關適配,然後再回到豎屏了因為flag的配置不對,導致顯示異常。
4 NotchTools 適配工具
基於前面的適配規則,簡單的對劉海屏全屏適配封裝了一個工具---- NotchTools 。NotchTools的初衷是儘可能簡單的進行劉海屏適配,其中包括全屏佔用劉海區域、全屏不佔用劉海屏區域兩種情況,對於透明狀態列的情況,沒有進行適配(原理是和全屏佔用劉海屏區域是一樣的), NotchTools 只處理全屏(隱藏狀態列)的情況。
特別說明:
對於使用劉海區域的情況,因為有時候需要對layout檔案的部分view進行下移劉海或者狀態列的高度達到適配的目的,但有的時候又不希望對佈局或view進行下移,所以NotchTools工具在全屏且佔用劉海區域的情況下未做下移處理,使用者可以在OnNotchCallBack的回撥中,獲得狀態列高度,然後自行完成下移操作,具體程式碼在FullScreenUseNotchActivity有給出使用方式。
4.1 如何使用
4.1.1 NotchTools適配全屏但不佔用劉海情況
使用方法為在Activity的onCreate方法中使用如下程式碼:
NotchTools.getFullScreenTools().fullScreenDontUseStatus(this, new OnNotchCallBack() { @Override public void onNotchPropertyCallback(NotchProperty notchProperty) { } @Override public void onNeedAddNotchStatusBar(boolean needAddNocth) { if (needAddNocth) { setFakeNotchView(); } } });
需要注意的是使用者需要在OnNotchCallBack回撥的onNeedAddNotchStatusBar中新增上述程式碼,不然會在Oppo手機上有異常。
4.1.2 NotchTools適配全屏且佔用劉海情況
同理,在需要處理的Activity的onCreate中使用如下程式碼:
NotchTools.getFullScreenTools().fullScreenUseStatus(this, new OnNotchCallBack() { @Override public void onNotchPropertyCallback(NotchProperty notchProperty) { int notchHeight = notchProperty.geNotchHeight(); RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mBackView.getLayoutParams(); layoutParams.topMargin += notchHeight; mBackView.setLayoutParams(layoutParams); } @Override public void onNeedAddNotchStatusBar(boolean needAddNocth) { } });
對於需要使用劉海區域的適配中,使用者需要在OnNotchCallBack回撥的onNotchPropertyCallback方法中獲取狀態列高度notchHeight,然後自行去為自己的佈局或者View做下移操作,這樣靈活性更好一點。
4.1.3 NotchTools額外說明
NotchTools中的Activity都繼承了BaseActivity,BaseActivity的程式碼如下:
public class BaseActivity extends AppCompatActivity { /** * 劉海容器 */ private FrameLayout mNotchContainer; /** * 主內容區 */ private FrameLayout mContentContainer; @Override public void setContentView(int layoutResID) { super.setContentView(R.layout.activity_base); mNotchContainer = findViewById(R.id.notch_container); mContentContainer = findViewById(R.id.content_container); onBindContentContainer(layoutResID); } private void onBindContentContainer(int layoutResID) { LayoutInflater.from(this).inflate(layoutResID, mContentContainer, true); } /** * 全屏SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN下劉海屏適配需要 */ protected void setFakeNotchView() { if (mNotchContainer == null) { mNotchContainer = findViewById(R.id.notch_container); } View view = new View(this); view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, NotchTools.getFullScreenTools().getNotchHeight(getWindow()))); view.setBackgroundColor(Color.BLACK); mNotchContainer.addView(view); } }
BaseActivity內部過載了setContentView(int layoutResID)方法,在layoutResID的上方添加了一個Framelayout,也就是劉海的父容器,預設情況下他是一個空佈局,但是在全屏不佔用劉海的情況下,為了適配OPPO手機(OPPO手機沒有具體方法來實現全屏不佔用劉海),所以需要在mNotchContainer中放入劉海高度的黑色View,來下移整體佈局,達到適配目的。
5 總結
本文只討論了Android O機型中適配劉海屏的原理、方法、和解決方案,並提供了原始碼供參考,對於Google提出的Android P上的通用解決方案,可能會在以後做更新。
題外話:對於Android O上的各大產商提供的適配方案,有的廠商在官網上明確說明了會在Android P上進行相容,也就是O的適配方案依然在未來的P機型上可行。但是有的廠商已經在官網明確說明了在未來的Android P上不會相容O的適配方案,所以,適配還是任重而道遠,等國內P手機上市的那天,再來一場適配吧。
廣告時間:歡迎大佬們關注小弟寫的公眾號:雜湊同學
順便騙個star: NotchTools 原始碼地址

image