極力推薦文章:歡迎收藏
文章轉載網絡:
前言
目前市面上的劉海屏和水滴屏手機越來越多了,顏值方面是因人而異,有的人覺得很好看,也有人覺得丑爆了,我個人覺得是還可以。但是作為移動開發者來說,這並不是一件好事,越來越多異形屏手機的出現意味着我們需要投入大量精力在適配上(就不提之后會出的折疊屏手機了)。本文總結了當下主流手機的劉海屏適配方案,鑒於目前Android碎片化的情況,想要覆蓋所有的機型是不可能的,但是能適配一些是一些,總比什么都不做要好。
所謂劉海屏,指的是手機屏幕正上方由於追求極致邊框而采用的一種手機解決方案。因形似劉海兒而得名——來自百度百科,水滴屏也是類似,為了簡單起見,下文就統稱這兩種為劉海屏了。
什么時候需要適配
這里先上一張官方的圖
從圖中可以看出,劉海區域是鑲嵌在狀態欄內部的,劉海區域的高度一般是不超過狀態欄高度的。因此,當我們的應用布局需要占據狀態欄來顯示時,就需要考慮到劉海區域是否會遮擋住頁面上的控件或者背景,這就是為什么將狀態欄區域稱為危險區域。如果應用不需要占據狀態欄顯示,全部顯示在安全區域內,那么恭喜你,不需要做任何適配處理。總結來說,只有當應用需要全屏顯示時才需要進行適配。
<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>
運行之后發現按鈕會被劉海區域所遮擋,如圖所示:
image
再說第二種情況,全屏風格,狀態欄不可見。同樣有兩種設置方法:
<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);
}
}
補充說明一點,現在的手機屏幕高寬比例越來越大,我們還需要額外做一下適配才能使應用在所有手機上都能全屏顯示,具體方式有兩種:
<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或者更大)
android:resizeableActivity="true"
也可以通過設置targetSdkVersion>=24(即Android 7.0),該屬性的值會默認為true,就不需要在AndroidManifest.xml中配置了。
image
如何適配
上文中已經展示了劉海屏中全屏顯示帶來的問題,那么如何去解決呢?
1.沉浸式狀態欄的適配
其實沉浸式狀態欄帶來的遮擋問題與劉海屏無關,本質上是由於設置了透明狀態欄導致布局延伸到了狀態欄中,就算是不具有劉海屏,一定程度上也會造成布局的遮擋。不過既然劉海屏是處在狀態欄當中的,那么我們就把這種情況也包含在劉海屏的適配中。清楚了原因之后,解決起來就很簡單了,我們只需要讓控件或布局避開狀態欄顯示就可以了,具體的解決方法有三種。方法一.利用fitsSystemWindows屬性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
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
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;
}
}
適配之后成功地將控件避開了狀態欄(危險區域),如下圖所示:
image
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判斷是否存在劉海屏,只能在適配后才能獲取到劉海區域信息,因此只能對於所有設備都添加適配代碼。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);
}
}
}
三種模式下的顯示效果如下圖所示:
image
image
image
可以看出,當在全屏顯示情況下,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在此時是允許窗口布局延伸到劉海區域的,因此更證實了只有在全屏顯示的情況下該模式才不允許窗口布局延伸到劉海區域。
image
我這里為了簡單沒有添加任何控件,實際開發中在全屏顯示后我們仍然需要考慮劉海區域是否會遮擋顯示的內容和控件,同樣需要避開危險區域來顯示。做法和沉浸式狀態欄的適配相同,原理同樣是將布局下移,預留出狀態欄的高度,這里就不一一列舉了。
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里,可以根據需求來使用。
小米適配方案
小米官方提供的適配文檔:小米劉海屏水滴屏 Android O 適配判斷是否有劉海屏
/**
* 判斷是否有劉海屏
*
* @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;
}
}
應用頁面設置使用劉海區顯示方案一.Application級別的控制接口
<meta-data
android:name="notch.config"
android:value="portrait|landscape"/>
其中,value的值可以是以下四種
"none" 橫豎屏都不繪制耳朵區
"portrait" 豎屏繪制到耳朵區
"landscape" 橫屏繪制到耳朵區
"portrait|landscape" 橫豎屏都繪制到耳朵區
這里的耳朵區指的就是劉海區兩側的狀態欄區域
雖然官方文檔上說的是Application級別的,但是我覺得也可以針對某一個Activity來配置,不過由於手頭上的手機條件不滿足,我並沒有驗證,如果有小伙伴測試過的話可以反饋一下,我再修正一下這里的說法。方案二.Window級別的控制接口
/*劉海屏全屏顯示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里,可以根據需求來使用。
image
至於Android P以下版本的小米手機,我並沒有測試,如果有哪位大佬測試過了發現有問題可以反饋一下。
Vivo、Oppo適配方案
Vivo官方提供的適配文檔:Vivo全面屏應用適配指南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判斷是否有劉海屏
/**
* 判斷是否有劉海屏
*
* @param context
* @return true:有劉海屏;false:沒有劉海屏
*/
public static boolean hasNotch(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
至於全屏顯示的適配方案,通過閱讀官方文檔和網上的其他適配文章,我個人總結一下就是這兩種品牌的手機在設置全屏顯示時都無需做任何處理(前提是適配了全面屏,上文中提到過如何配置),也就是不會產生黑邊,我們只需要避免布局中的內容或控件不被劉海區域所遮擋就可以了。具體的做法和沉浸式狀態欄的適配相同,基本原理還是將窗口布局下移,預留出狀態欄的高度。注:由於手頭沒有這兩種廠商的手機,因此並沒有驗證,這一點確實是我做得不夠嚴謹,有好心的大佬驗證之后歡迎指正。
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());
}
}
還有一點需要注意的地方,如果目標頁是應用的啟動頁面,采用這種添加代碼的適配方案(我是在Activity的onCreate()中添加的),會先顯示出黑邊,然后變為全屏顯示,就像下圖這樣:
image
如果我們使用在AndroidManifest.xml中增加meta-data屬性的方案呢,上文也提到了,這種方案對於Android P版本的小米手機是沒有效果的,當然了,谷歌官方提供的適配方案也無法通過這種方式配置,因此我們無法避免通過代碼來適配。出現這種問題的原因是app在點擊圖標啟動后,會先執行創建進程、應用初始化等操作,雖然時間可能很短,但還是會耗時,這會讓用戶認為點擊應用圖標沒有反應,為了提升用戶體驗,在這期間系統會根據AndroidManifest.xml指定的主題來展示出一個Preview Window(預覽窗口),常見的應用啟動白屏/黑屏就是該原因導致的。1.設置android:windowIsTranslucent屬性
<item name="android:windowIsTranslucent">true</item>
設置該屬性表示該窗口是半透明的,這樣就不會在啟動頁還未加載之前顯示出預覽窗口,但是這樣會導致點擊了應用圖標后不會馬上顯示出應用程序窗口,相當於禁用了官方的優化方案,如果應用初始化進行了過多操作,會延遲幾秒才顯示應用窗口,用戶體驗非常不好。2.設置android:windowDisablePreview屬性
<item name="android:windowDisablePreview">true</item>
該屬性的作用很明顯,就是禁用預覽窗口,也就是說系統不會使用窗口的主題來顯示一個Preview Window,和windowIsTranslucent屬性的效果相同,該屬性同樣會影響用戶體驗。
總結
雖然文中介紹了很多適配的內容,但其實在開發中需要我們適配劉海屏的情況並不多,只有兩種情況需要我們進行考慮: