前言
目前市面上的劉海屏和水滴屏手機(jī)越來(lái)越多了,顏值方面是因人而異,有的人覺(jué)得很好看,,也有人覺(jué)得丑爆了,,我個(gè)人覺(jué)得是還可以。但是作為移動(dòng)開(kāi)發(fā)者來(lái)說(shuō),,這并不是一件好事,,越來(lái)越多異形屏手機(jī)的出現(xiàn)意味著我們需要投入大量精力在適配上(就不提之后會(huì)出的折疊屏手機(jī)了)。本文總結(jié)了當(dāng)下主流手機(jī)的劉海屏適配方案,,鑒于目前Android碎片化的情況,,想要覆蓋所有的機(jī)型是不可能的,但是能適配一些是一些,,總比什么都不做要好,。
所謂劉海屏,指的是手機(jī)屏幕正上方由于追求極致邊框而采用的一種手機(jī)解決方案,。因形似劉海兒而得名——來(lái)自百度百科,,水滴屏也是類似,為了簡(jiǎn)單起見(jiàn),,下文就統(tǒng)稱這兩種為劉海屏了,。
什么時(shí)候需要適配
這里先上一張官方的圖
從圖中可以看出,劉海區(qū)域是鑲嵌在狀態(tài)欄內(nèi)部的,,劉海區(qū)域的高度一般是不超過(guò)狀態(tài)欄高度的,。因此,當(dāng)我們的應(yīng)用布局需要占據(jù)狀態(tài)欄來(lái)顯示時(shí),,就需要考慮到劉海區(qū)域是否會(huì)遮擋住頁(yè)面上的控件或者背景,,這就是為什么將狀態(tài)欄區(qū)域稱為危險(xiǎn)區(qū)域。如果應(yīng)用不需要占據(jù)狀態(tài)欄顯示,,全部顯示在安全區(qū)域內(nèi),那么恭喜你,,不需要做任何適配處理,。總結(jié)來(lái)說(shuō),,只有當(dāng)應(yīng)用需要全屏顯示時(shí)才需要進(jìn)行適配,。
全屏顯示無(wú)非就是兩種情況:第一種是我們常說(shuō)的沉浸式狀態(tài)欄,也就是狀態(tài)欄透明,,頁(yè)面的布局延伸到狀態(tài)欄顯示,,這種情況下?tīng)顟B(tài)欄依然可見(jiàn);第二種是類似應(yīng)用的閃屏頁(yè)風(fēng)格,,頁(yè)面全屏顯示,,狀態(tài)欄不可見(jiàn)。這兩種情況下如果不進(jìn)行適配處理都會(huì)產(chǎn)生一些問(wèn)題。
先來(lái)看第一種情況,,沉浸式風(fēng)格,。需要將狀態(tài)欄設(shè)置為透明,需要注意只有在Android 4.4(API Level 19)以上才支持設(shè)置透明狀態(tài)欄,。有兩種設(shè)置方法:
方法一:為Activity設(shè)置style,,添加一個(gè)屬性:
<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);
// 透明狀態(tài)欄
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
}
頁(yè)面的布局很簡(jiǎn)單,只包含一個(gè)按鈕,,為了明顯,,我為根布局設(shè)置了一個(gè)背景。
activity_immersive.xml
<?xml version='1.0' encoding='utf-8'?>
<LinearLayout xmlns:android='http://schemas./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>
運(yùn)行之后發(fā)現(xiàn)按鈕會(huì)被劉海區(qū)域所遮擋,,如圖所示:
再說(shuō)第二種情況,,全屏風(fēng)格,狀態(tài)欄不可見(jiàn),。同樣有兩種設(shè)置方法:
方法一:為Activity設(shè)置style,,添加屬性:
<item name='android:windowFullscreen'>true</item>
<!-- 這里為了簡(jiǎn)單,直接從style中指定一個(gè)背景 -->
<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);
}
}
補(bǔ)充說(shuō)明一點(diǎn),,現(xiàn)在的手機(jī)屏幕高寬比例越來(lái)越大,,我們還需要額外做一下適配才能使應(yīng)用在所有手機(jī)上都能全屏顯示,具體方式有兩種:
方式一:在AndroidManifest.xml中配置支持最大高寬比
<meta-data android:name='android.max_aspect'
android:value='ratio_float' />
或者
android:maxAspectRatio='ratio_float' (API LEVEL 26)
說(shuō)明:以上兩種接口可以二選一,,ratio_float = 屏幕高 / 屏幕寬 (如oppo新機(jī)型屏幕分辨率為2280 x 1080,, ratio_float = 2280 / 1080 = 2.11,建議設(shè)置 ratio_float為2.2或者更大)
方式二:在AndroidManifest.xml中配置支持分屏,,注意驗(yàn)證分屏下界面兼容性
android:resizeableActivity='true'
也可以通過(guò)設(shè)置targetSdkVersion>=24(即Android 7.0),該屬性的值會(huì)默認(rèn)為true,,就不需要在AndroidManifest.xml中配置了。
運(yùn)行之后,,我們發(fā)現(xiàn)狀態(tài)欄的部分留出了一條黑邊,,看上起很奇怪,這顯然不是我們想要的效果,。
如何適配
上文中已經(jīng)展示了劉海屏中全屏顯示帶來(lái)的問(wèn)題,,那么如何去解決呢?
1.沉浸式狀態(tài)欄的適配
其實(shí)沉浸式狀態(tài)欄帶來(lái)的遮擋問(wèn)題與劉海屏無(wú)關(guān),,本質(zhì)上是由于設(shè)置了透明狀態(tài)欄導(dǎo)致布局延伸到了狀態(tài)欄中,,就算是不具有劉海屏,一定程度上也會(huì)造成布局的遮擋,。不過(guò)既然劉海屏是處在狀態(tài)欄當(dāng)中的,,那么我們就把這種情況也包含在劉海屏的適配中。清楚了原因之后,,解決起來(lái)就很簡(jiǎn)單了,,我們只需要讓控件或布局避開(kāi)狀態(tài)欄顯示就可以了,,具體的解決方法有三種。
方法一.利用fitsSystemWindows屬性
當(dāng)我們給最外層View設(shè)置了android:fitsSystemWindows='true'屬性后,,當(dāng)設(shè)置了透明狀態(tài)欄或者透明導(dǎo)航欄后,,就會(huì)自動(dòng)給View添加paddingTop或paddingBottom屬性,這樣就在屏幕上預(yù)留出了狀態(tài)欄的高度,,我們的布局就不會(huì)占用狀態(tài)欄來(lái)顯示了,。
activity_immersive.xml
<?xml version='1.0' encoding='utf-8'?>
<LinearLayout xmlns:android='http://schemas./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>
方法二.根據(jù)狀態(tài)欄高度手動(dòng)設(shè)置paddingTop
這種方法的實(shí)現(xiàn)本質(zhì)上和設(shè)置fitsSystemWindows是一樣的,首先獲取狀態(tài)欄高度,,然后設(shè)置根布局的paddingTop等于狀態(tài)欄高度就可以了,,代碼如下:
public class ImmersiveActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_immersive);
// 透明狀態(tài)欄
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
LinearLayout llRoot = findViewById(R.id.ll_root);
// 設(shè)置根布局的paddingTop
llRoot.setPadding(0, getStatusBarHeight(this), 0, 0);
}
/**
* 獲取狀態(tài)欄高度
*
* @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;
}
}
方法三.在布局中添加一個(gè)和狀態(tài)欄高度相同的View
和前兩種方法原理類似,同樣是讓屏幕預(yù)留出狀態(tài)欄的高度,,這里在根布局中添加了一個(gè)透明的View,,高度和狀態(tài)欄高度相同。這種方法的好處是可以自定義填充狀態(tài)欄View的背景,,更靈活地實(shí)現(xiàn)我們想要的效果,。
public class ImmersiveActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_immersive);
// 透明狀態(tài)欄
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));
// 在根布局中添加一個(gè)狀態(tài)欄高度的View
llRoot.addView(statusBarView, 0, lp);
}
/**
* 獲取狀態(tài)欄高度
*
* @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;
}
}
適配之后成功地將控件避開(kāi)了狀態(tài)欄(危險(xiǎn)區(qū)域),如下圖所示:
2.全屏顯示的適配
對(duì)于全屏顯示的情況,,處理起來(lái)要相對(duì)麻煩一些,,下面重點(diǎn)說(shuō)一下這種情況下的適配方案。
2.1.Android P及以上
谷歌官方從Android P開(kāi)始給開(kāi)發(fā)者提供了劉海屏相關(guān)的API,,可以通過(guò)直接調(diào)用API來(lái)進(jìn)行劉海屏的適配處理,。
通過(guò)DisplayCutout類可以獲得安全區(qū)域的范圍以及劉海區(qū)域(官方的叫法是缺口)的信息,需要注意只有API Level在28及以上才可以調(diào)用,。
/**
* 獲得劉海區(qū)域信息
*/
@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) {
// 當(dāng)全屏頂部顯示黑邊時(shí),,getDisplayCutout()返回為null
DisplayCutout displayCutout = windowInsets.getDisplayCutout();
Log.e('TAG', '安全區(qū)域距離屏幕左邊的距離 SafeInsetLeft:' + displayCutout.getSafeInsetLeft());
Log.e('TAG', '安全區(qū)域距離屏幕右部的距離 SafeInsetRight:' + displayCutout.getSafeInsetRight());
Log.e('TAG', '安全區(qū)域距離屏幕頂部的距離 SafeInsetTop:' + displayCutout.getSafeInsetTop());
Log.e('TAG', '安全區(qū)域距離屏幕底部的距離 SafeInsetBottom:' + displayCutout.getSafeInsetBottom());
// 獲得劉海區(qū)域
List<Rect> rects = displayCutout.getBoundingRects();
if (rects == null || rects.size() == 0) {
Log.e('TAG', '不是劉海屏');
} else {
Log.e('TAG', '劉海屏數(shù)量:' + rects.size());
for (Rect rect : rects) {
Log.e('TAG', '劉海屏區(qū)域:' + rect);
}
}
}
}
});
}
}
這里我在測(cè)試時(shí)也發(fā)現(xiàn)了一個(gè)問(wèn)題,就是如果是在style中設(shè)置了全屏模式,,在適配之前,,頂部狀態(tài)欄區(qū)域顯示一條黑邊,這時(shí)候調(diào)用getDisplayCutout()獲取DisplayCutout對(duì)象返回的結(jié)果是null,,其實(shí)這也不難理解,,因?yàn)檫@時(shí)候是看不出劉海區(qū)域的,但是這樣會(huì)導(dǎo)致在適配之前無(wú)法通過(guò)DisplayCutout判斷是否存在劉海屏,,只能在適配后才能獲取到劉海區(qū)域信息,因此只能對(duì)于所有設(shè)備都添加適配代碼,。
那么接下來(lái)如何進(jìn)行適配呢,,Android P中增加了一個(gè)窗口布局參數(shù)屬性layoutInDisplayCutoutMode,該屬性有三個(gè)值可以?。?/p>
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:默認(rèn)的布局模式,,僅當(dāng)劉海區(qū)域完全包含在狀態(tài)欄之中時(shí),,才允許窗口延伸到劉海區(qū)域顯示,也就是說(shuō),,如果沒(méi)有設(shè)置為全屏顯示模式,,就允許窗口延伸到劉海區(qū)域,否則不允許,。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:永遠(yuǎn)不允許窗口延伸到劉海區(qū)域,。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:始終允許窗口延伸到屏幕短邊上的劉海區(qū)域,窗口永遠(yuǎn)不會(huì)延伸到屏幕長(zhǎng)邊上的劉海區(qū)域,。
還有一個(gè)LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS模式,,目前已經(jīng)被LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES所取代,不允許使用了,,這里就不提了,。
這么看可能還是有些不理解,接下來(lái)我們?cè)谝粋€(gè)全屏顯示的頁(yè)面分別設(shè)置三種布局模式,,看看有什么區(qū)別,。
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();
// 僅當(dāng)缺口區(qū)域完全包含在狀態(tài)欄之中時(shí),才允許窗口延伸到劉海區(qū)域顯示
// lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
// 永遠(yuǎn)不允許窗口延伸到劉海區(qū)域
// lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
// 始終允許窗口延伸到屏幕短邊上的劉海區(qū)域
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(lp);
}
}
}
三種模式下的顯示效果如下圖所示:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
可以看出,,當(dāng)在全屏顯示情況下,,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT和LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER的效果是一樣的,都是在狀態(tài)欄顯示一條黑邊,,也就是不允許窗口布局延伸到劉海區(qū)域,,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES則允許窗口布局延伸到了劉海區(qū)域,這里需要注意是短邊劉海區(qū)域,,不過(guò)一般市面上的手機(jī)劉海區(qū)域都是在短邊上的,,我是沒(méi)見(jiàn)過(guò)劉海長(zhǎng)在“腰”上的,因此利用這個(gè)模式就實(shí)現(xiàn)適配了,。
通過(guò)之前沉浸式狀態(tài)欄的顯示效果可以看出,,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT在此時(shí)是允許窗口布局延伸到劉海區(qū)域的,因此更證實(shí)了只有在全屏顯示的情況下該模式才不允許窗口布局延伸到劉海區(qū)域,。
適配后效果如下,,現(xiàn)在看起來(lái)就很舒服了:
我這里為了簡(jiǎn)單沒(méi)有添加任何控件,實(shí)際開(kāi)發(fā)中在全屏顯示后我們?nèi)匀恍枰紤]劉海區(qū)域是否會(huì)遮擋顯示的內(nèi)容和控件,,同樣需要避開(kāi)危險(xiǎn)區(qū)域來(lái)顯示,。做法和沉浸式狀態(tài)欄的適配相同,原理同樣是將布局下移,,預(yù)留出狀態(tài)欄的高度,,這里就不一一列舉了。
2.2 Android P以下
目前市面上的劉海屏手機(jī)可以說(shuō)是琳瑯滿目,,各大廠商都在追求極致的屏占比,,推出的新機(jī)型也基本上都有劉海屏,,針對(duì)Android P以下的手機(jī),我們只能依照各個(gè)廠商提供的適配方案來(lái)進(jìn)行適配,。我也查閱了網(wǎng)上的一些適配文章,,主要還是針對(duì)目前主流的手機(jī)品牌,本文總結(jié)了華為,、小米,、Vivo和Oppo的適配方案,其他品牌的手機(jī)之后有時(shí)間的話可能會(huì)再考慮,。
華為適配方案
華為官方提供的適配文檔:華為劉海屏手機(jī)安卓O版本適配指導(dǎo)
文檔中提供了很多劉海屏相關(guān)的方法,,這里就不一一列舉了,著重看一下我們需要用到的方法,。
判斷是否有劉海屏
/**
* 判斷是否有劉海屏
*
* @param context
* @return true:有劉海屏,;false:沒(méi)有劉海屏
*/
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;
}
}
應(yīng)用頁(yè)面設(shè)置使用劉海區(qū)顯示
官方提供了兩種適配方案:
方案一.使用新增的meta-data屬性android.notch_support,在應(yīng)用的AndroidManifest.xml中增加meta-data屬性,,此屬性不僅可以針對(duì)Application生效,,也可以對(duì)Activity配置生效。
使用方式如下:
<meta-data android:name='android.notch_support' android:value='true'/>
可以在Application下添加,,意味著該應(yīng)用的所有頁(yè)面,,系統(tǒng)都不會(huì)做豎屏場(chǎng)景的特殊下移或者是橫屏場(chǎng)景的右移特殊處理。
<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>
也可以針對(duì)指定的Activity添加,,意味著可以針對(duì)單個(gè)頁(yè)面進(jìn)行劉海屏適配,,設(shè)置了該屬性的Activity系統(tǒng)將不會(huì)做特殊處理。
<!-- 全屏顯示頁(yè)面 -->
<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
代碼如下:
/**
* 設(shè)置應(yīng)用窗口在劉海屏手機(jī)使用劉海區(qū)
* <p>
* 通過(guò)添加窗口FLAG的方式設(shè)置頁(yè)面使用劉海區(qū)顯示
*
* @param window 應(yīng)用頁(yè)面window對(duì)象
*/
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');
}
}
官方提供的所有方法我已經(jīng)放到了工具類HwNotchUtils里,,可以根據(jù)需求來(lái)使用,。
小米適配方案
小米官方提供的適配文檔:https://dev.mi.com/console/doc/detail?pId=1293
我們同樣看一下關(guān)鍵方法。
判斷是否有劉海屏
/**
* 判斷是否有劉海屏
*
* @param context
* @return true:有劉海屏,;false:沒(méi)有劉海屏
*/
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;
}
}
應(yīng)用頁(yè)面設(shè)置使用劉海區(qū)顯示
小米提供的適配方案同樣有兩種(meta-data和Flag),,使用方法和華為類似。
方案一.Application級(jí)別的控制接口
在 Application 下增加一個(gè) meta-data,,用以聲明該應(yīng)用窗口是否可以延伸到狀態(tài)欄,。
<meta-data
android:name='notch.config'
android:value='portrait|landscape'/>
其中,value的值可以是以下四種
'none' 橫豎屏都不繪制耳朵區(qū)
'portrait' 豎屏繪制到耳朵區(qū)
'landscape' 橫屏繪制到耳朵區(qū)
'portrait|landscape' 橫豎屏都繪制到耳朵區(qū)
這里的耳朵區(qū)指的就是劉海區(qū)兩側(cè)的狀態(tài)欄區(qū)域
雖然官方文檔上說(shuō)的是Application級(jí)別的,,但是我覺(jué)得也可以針對(duì)某一個(gè)Activity來(lái)配置,,不過(guò)由于手頭上的手機(jī)條件不滿足,我并沒(méi)有驗(yàn)證,,如果有小伙伴測(cè)試過(guò)的話可以反饋一下,,我再修正一下這里的說(shuō)法。
方案二.Window級(jí)別的控制接口
通過(guò)給Window添加Flag也可以實(shí)現(xiàn)將窗口布局延伸到狀態(tài)欄中顯示,。
/*劉海屏全屏顯示FLAG*/
public static final int FLAG_NOTCH_SUPPORT = 0x00000100; // 開(kāi)啟配置
public static final int FLAG_NOTCH_PORTRAIT = 0x00000200; // 豎屏配置
public static final int FLAG_NOTCH_HORIZONTAL = 0x00000400; // 橫屏配置
/**
* 設(shè)置應(yīng)用窗口在劉海屏手機(jī)使用劉海區(qū)
* <p>
* 通過(guò)添加窗口FLAG的方式設(shè)置頁(yè)面使用劉海區(qū)顯示
*
* @param window 應(yīng)用頁(yè)面window對(duì)象
*/
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
// 豎屏繪制到耳朵區(qū)
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.');
}
}
官方提供的所有方法我已經(jīng)放到了工具類XiaomiNotchUtils里,,可以根據(jù)需求來(lái)使用。
這里說(shuō)一下我的測(cè)試情況,,我是用小米8測(cè)試的,,系統(tǒng)版本已經(jīng)升到了Android P,利用小米官方提供的適配方法沒(méi)有效果,,只能用谷歌官方針對(duì)Android P的適配方案,,這一點(diǎn)小米的官方文檔也提到了。
至于Android P以下版本的小米手機(jī),,我并沒(méi)有測(cè)試,,如果有哪位大佬測(cè)試過(guò)了發(fā)現(xiàn)有問(wèn)題可以反饋一下。
Vivo,、Oppo適配方案
Vivo官方提供的適配文檔:Vivo全面屏應(yīng)用適配指南
Oppo官方提供的適配文檔:Oppo凹形屏適配指南
這里把Vivo和Oppo放在一起說(shuō),,官方提供的資料不像華為和小米那么詳細(xì),只是提供了判斷是否有劉海屏的方法,。
Vivo判斷是否有劉海屏
public static final int VIVO_NOTCH = 0x00000020; // 是否有劉海
public static final int VIVO_FILLET = 0x00000008; // 是否有圓角
/**
* 判斷是否有劉海屏
*
* @param context
* @return true:有劉海屏,;false:沒(méi)有劉海屏
*/
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:沒(méi)有劉海屏
*/
public static boolean hasNotch(Context context) {
return context.getPackageManager().hasSystemFeature('com.oppo.feature.screen.heteromorphism');
}
至于全屏顯示的適配方案,,通過(guò)閱讀官方文檔和網(wǎng)上的其他適配文章,,我個(gè)人總結(jié)一下就是這兩種品牌的手機(jī)在設(shè)置全屏顯示時(shí)都無(wú)需做任何處理(前提是適配了全面屏,上文中提到過(guò)如何配置),,也就是不會(huì)產(chǎn)生黑邊,,我們只需要避免布局中的內(nèi)容或控件不被劉海區(qū)域所遮擋就可以了。具體的做法和沉浸式狀態(tài)欄的適配相同,,基本原理還是將窗口布局下移,,預(yù)留出狀態(tài)欄的高度。
注:由于手頭沒(méi)有這兩種廠商的手機(jī),,因此并沒(méi)有驗(yàn)證,,這一點(diǎn)確實(shí)是我做得不夠嚴(yán)謹(jǐn),有好心的大佬驗(yàn)證之后歡迎指正,。
其實(shí)我本來(lái)也想列出魅族的適配方案的,,但是實(shí)在是沒(méi)找到官方文檔。,。,。如果有知道的大佬可以提供一下,我后面會(huì)把適配方案補(bǔ)上,。
適配時(shí)的基本邏輯就是先判斷手機(jī)的品牌,,這里我利用了一個(gè)開(kāi)源工具類項(xiàng)目AndroidUtilCode,提供了一個(gè)獲取手機(jī)Rom信息的工具類RomUtils,,用起來(lái)很方便,,然后判斷是否是劉海屏,,針對(duì)劉海屏手機(jī)添加適配代碼。完整的適配代碼如下所示:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Android P利用官方提供的API適配
WindowManager.LayoutParams lp = getWindow().getAttributes();
// 始終允許窗口延伸到屏幕短邊上的缺口區(qū)域
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(lp);
} else {
// Android P以下根據(jù)手機(jī)廠商的適配方案進(jìn)行適配
if (RomUtils.isHuawei() && HwNotchUtils.hasNotch(this)) {
HwNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
} else if (RomUtils.isXiaomi() && XiaomiNotchUtils.hasNotch(this)) {
XiaomiNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
}
}
總結(jié)
雖然文中介紹了很多適配的內(nèi)容,,但其實(shí)在開(kāi)發(fā)中需要我們適配劉海屏的情況并不多,,只有兩種情況需要我們進(jìn)行考慮:
1.沉浸式狀態(tài)欄,窗口布局延伸到了狀態(tài)欄中,,是否會(huì)遮擋必要的內(nèi)容或控件(處在危險(xiǎn)區(qū)域),。適配方案就是將窗口布局下移,預(yù)留出狀態(tài)欄的空間,。
2.全屏顯示模式,,不做適配的話狀態(tài)欄會(huì)呈現(xiàn)一條黑邊。適配方案是首先判斷系統(tǒng)版本,,是Android P及以上就按照官方的API來(lái)適配,,否則根據(jù)手機(jī)廠商的適配方案進(jìn)行適配。鑒于目前市面上Android P還沒(méi)有普及,,為了帶來(lái)更好的用戶體驗(yàn),,我們還是需要多花一些精力來(lái)適配各個(gè)手機(jī)廠商的劉海屏手機(jī)。
最后提示一下,,本文只列出了四個(gè)當(dāng)下主流手機(jī)廠商的適配方案,,我自己驗(yàn)證過(guò)的只有華為和小米(只驗(yàn)證了Android P)的方案,對(duì)于Vivo和Oppo的一些結(jié)論我可能說(shuō)得不對(duì),,歡迎大家指正,。當(dāng)然,如果大家還需要其他廠商的適配方案,,也歡迎提出,,我會(huì)盡力補(bǔ)上。
相關(guān)的代碼和工具類我已經(jīng)上傳到了github,,可以下載Demo來(lái)查看,,大家一起交流