多點在看,就是真愛!
目前市面上的劉海屏和水滴屏手機越來越多了,顏值方面是因人而異,有的人覺得很好看,也有人覺得丑爆了,我個人覺得是還可以。但是作為移動開發(fā)者來說,這并不是一件好事,越來越多異形屏手機的出現(xiàn)意味著我們需要投入大量精力在適配上(就不提之后會出的折疊屏手機了)。本文總結(jié)了當(dāng)下主流手機的劉海屏適配方案,鑒于目前Android碎片化的情況,想要覆蓋所有的機型是不可能的,但是能適配一些是一些,總比什么都不做要好。
所謂劉海屏,指的是手機屏幕正上方由于追求極致邊框而采用的一種手機解決方案。因形似劉海兒而得名——來自百度百科,水滴屏也是類似,為了簡單起見,下文就統(tǒng)稱這兩種為劉海屏了。
這里先上一張官方的圖
從圖中可以看出,劉海區(qū)域是鑲嵌在狀態(tài)欄內(nèi)部的,劉海區(qū)域的高度一般是不超過狀態(tài)欄高度的。因此,當(dāng)我們的應(yīng)用布局需要占據(jù)狀態(tài)欄來顯示時,就需要考慮到劉海區(qū)域是否會遮擋住頁面上的控件或者背景,這就是為什么將狀態(tài)欄區(qū)域稱為危險區(qū)域。如果應(yīng)用不需要占據(jù)狀態(tài)欄顯示,全部顯示在安全區(qū)域內(nèi),那么恭喜你,不需要做任何適配處理??偨Y(jié)來說,只有當(dāng)應(yīng)用需要全屏顯示時才需要進(jìn)行適配。
全屏顯示無非就是兩種情況:第一種是我們常說的沉浸式狀態(tài)欄,也就是狀態(tài)欄透明,頁面的布局延伸到狀態(tài)欄顯示,這種情況下狀態(tài)欄依然可見;第二種是類似應(yīng)用的閃屏頁風(fēng)格,頁面全屏顯示,狀態(tài)欄不可見。這兩種情況下如果不進(jìn)行適配處理都會產(chǎn)生一些問題。
先來看第一種情況,沉浸式風(fēng)格。需要將狀態(tài)欄設(shè)置為透明,需要注意只有在Android 4.4(API Level 19)以上才支持設(shè)置透明狀態(tài)欄。有兩種設(shè)置方法:
方法一:為Activity設(shè)置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);
// 透明狀態(tài)欄
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
}
頁面的布局很簡單,只包含一個按鈕,為了明顯,我為根布局設(shè)置了一個背景。
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>
運行之后發(fā)現(xiàn)按鈕會被劉海區(qū)域所遮擋,如圖所示:
再說第二種情況,全屏風(fēng)格,狀態(tài)欄不可見。同樣有兩種設(shè)置方法:
方法一:為Activity設(shè)置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);
}
}
補充說明一點,現(xiàn)在的手機屏幕高寬比例越來越大,我們還需要額外做一下適配才能使應(yīng)用在所有手機上都能全屏顯示,具體方式有兩種:
方式一:在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,建議設(shè)置 ratio_float為2.2或者更大)
方式二:在AndroidManifest.xml中配置支持分屏,注意驗證分屏下界面兼容性
android:resizeableActivity='true'
也可以通過設(shè)置targetSdkVersion>=24(即Android 7.0),該屬性的值會默認(rèn)為true,就不需要在AndroidManifest.xml中配置了。
運行之后,我們發(fā)現(xiàn)狀態(tài)欄的部分留出了一條黑邊,看上起很奇怪,這顯然不是我們想要的效果。
上文中已經(jīng)展示了劉海屏中全屏顯示帶來的問題,那么如何去解決呢?
其實沉浸式狀態(tài)欄帶來的遮擋問題與劉海屏無關(guān),本質(zhì)上是由于設(shè)置了透明狀態(tài)欄導(dǎo)致布局延伸到了狀態(tài)欄中,就算是不具有劉海屏,一定程度上也會造成布局的遮擋。不過既然劉海屏是處在狀態(tài)欄當(dāng)中的,那么我們就把這種情況也包含在劉海屏的適配中。清楚了原因之后,解決起來就很簡單了,我們只需要讓控件或布局避開狀態(tài)欄顯示就可以了,具體的解決方法有三種。
方法一.利用fitsSystemWindows屬性
當(dāng)我們給最外層View設(shè)置了android:fitsSystemWindows='true'屬性后,當(dāng)設(shè)置了透明狀態(tài)欄或者透明導(dǎo)航欄后,就會自動給View添加paddingTop或paddingBottom屬性,這樣就在屏幕上預(yù)留出了狀態(tài)欄的高度,我們的布局就不會占用狀態(tài)欄來顯示了。
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>
方法二.根據(jù)狀態(tài)欄高度手動設(shè)置paddingTop
這種方法的實現(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;
}
}
方法三.在布局中添加一個和狀態(tài)欄高度相同的View
和前兩種方法原理類似,同樣是讓屏幕預(yù)留出狀態(tài)欄的高度,這里在根布局中添加了一個透明的View,高度和狀態(tài)欄高度相同。這種方法的好處是可以自定義填充狀態(tài)欄View的背景,更靈活地實現(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));
// 在根布局中添加一個狀態(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;
}
}
適配之后成功地將控件避開了狀態(tài)欄(危險區(qū)域),如下圖所示:
對于全屏顯示的情況,處理起來要相對麻煩一些,下面重點說一下這種情況下的適配方案。
谷歌官方從Android P開始給開發(fā)者提供了劉海屏相關(guān)的API,可以通過直接調(diào)用API來進(jìn)行劉海屏的適配處理。
通過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)全屏頂部顯示黑邊時,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);
}
}
}
}
});
}
}
這里我在測試時也發(fā)現(xiàn)了一個問題,就是如果是在style中設(shè)置了全屏模式,在適配之前,頂部狀態(tài)欄區(qū)域顯示一條黑邊,這時候調(diào)用getDisplayCutout()獲取DisplayCutout對象返回的結(jié)果是null,其實這也不難理解,因為這時候是看不出劉海區(qū)域的,但是這樣會導(dǎo)致在適配之前無法通過DisplayCutout判斷是否存在劉海屏,只能在適配后才能獲取到劉海區(qū)域信息,因此只能對于所有設(shè)備都添加適配代碼。
那么接下來如何進(jìn)行適配呢,Android P中增加了一個窗口布局參數(shù)屬性layoutInDisplayCutoutMode,該屬性有三個值可以取:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:默認(rèn)的布局模式,僅當(dāng)劉海區(qū)域完全包含在狀態(tài)欄之中時,才允許窗口延伸到劉海區(qū)域顯示,也就是說,如果沒有設(shè)置為全屏顯示模式,就允許窗口延伸到劉海區(qū)域,否則不允許。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:永遠(yuǎn)不允許窗口延伸到劉海區(qū)域。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:始終允許窗口延伸到屏幕短邊上的劉海區(qū)域,窗口永遠(yuǎn)不會延伸到屏幕長邊上的劉海區(qū)域。
還有一個LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS模式,目前已經(jīng)被LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES所取代,不允許使用了,這里就不提了。
這么看可能還是有些不理解,接下來我們在一個全屏顯示的頁面分別設(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)欄之中時,才允許窗口延伸到劉海區(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ū)域,不過一般市面上的手機劉海區(qū)域都是在短邊上的,我是沒見過劉海長在“腰”上的,因此利用這個模式就實現(xiàn)適配了。
通過之前沉浸式狀態(tài)欄的顯示效果可以看出,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT在此時是允許窗口布局延伸到劉海區(qū)域的,因此更證實了只有在全屏顯示的情況下該模式才不允許窗口布局延伸到劉海區(qū)域。
適配后效果如下,現(xiàn)在看起來就很舒服了:
我這里為了簡單沒有添加任何控件,實際開發(fā)中在全屏顯示后我們?nèi)匀恍枰紤]劉海區(qū)域是否會遮擋顯示的內(nèi)容和控件,同樣需要避開危險區(qū)域來顯示。做法和沉浸式狀態(tài)欄的適配相同,原理同樣是將布局下移,預(yù)留出狀態(tài)欄的高度,這里就不一一列舉了。
目前市面上的劉海屏手機可以說是琳瑯滿目,各大廠商都在追求極致的屏占比,推出的新機型也基本上都有劉海屏,針對Android P以下的手機,我們只能依照各個廠商提供的適配方案來進(jìn)行適配。我也查閱了網(wǎng)上的一些適配文章,主要還是針對目前主流的手機品牌,本文總結(jié)了華為、小米、Vivo和Oppo的適配方案,其他品牌的手機之后有時間的話可能會再考慮。
華為官方提供的適配文檔:華為劉海屏手機安卓O版本適配指導(dǎo)
文檔中提供了很多劉海屏相關(guān)的方法,這里就不一一列舉了,著重看一下我們需要用到的方法。
判斷是否有劉海屏
/**
* 判斷是否有劉海屏
*
* @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;
}
}
應(yīng)用頁面設(shè)置使用劉海區(qū)顯示
官方提供了兩種適配方案:
方案一.使用新增的meta-data屬性android.notch_support,在應(yīng)用的AndroidManifest.xml中增加meta-data屬性,此屬性不僅可以針對Application生效,也可以對Activity配置生效。
使用方式如下:
<meta-data android:name='android.notch_support' android:value='true'/>
可以在Application下添加,意味著該應(yīng)用的所有頁面,系統(tǒ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>
也可以針對指定的Activity添加,意味著可以針對單個頁面進(jìn)行劉海屏適配,設(shè)置了該屬性的Activity系統(tǒng)將不會做特殊處理。
<!-- 全屏顯示頁面 -->
<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)用窗口在劉海屏手機使用劉海區(qū)
* <p>
* 通過添加窗口FLAG的方式設(shè)置頁面使用劉海區(qū)顯示
*
* @param window 應(yīng)用頁面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');
}
}
官方提供的所有方法我已經(jīng)放到了工具類HwNotchUtils里,可以根據(jù)需求來使用。
小米官方提供的適配文檔:https://dev.mi.com/console/doc/detail?pId=1293
我們同樣看一下關(guān)鍵方法。
判斷是否有劉海屏
/**
* 判斷是否有劉海屏
*
* @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;
}
}
應(yīng)用頁面設(shè)置使用劉海區(qū)顯示
小米提供的適配方案同樣有兩種(meta-data和Flag),使用方法和華為類似。
方案一.Application級別的控制接口
在 Application 下增加一個 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ū)域
雖然官方文檔上說的是Application級別的,但是我覺得也可以針對某一個Activity來配置,不過由于手頭上的手機條件不滿足,我并沒有驗證,如果有小伙伴測試過的話可以反饋一下,我再修正一下這里的說法。
方案二.Window級別的控制接口
通過給Window添加Flag也可以實現(xiàn)將窗口布局延伸到狀態(tài)欄中顯示。
/*劉海屏全屏顯示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; // 橫屏配置
/**
* 設(shè)置應(yīng)用窗口在劉海屏手機使用劉海區(qū)
* <p>
* 通過添加窗口FLAG的方式設(shè)置頁面使用劉海區(qū)顯示
*
* @param window 應(yīng)用頁面window對象
*/
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ù)需求來使用。
這里說一下我的測試情況,我是用小米8測試的,系統(tǒng)版本已經(jīng)升到了Android P,利用小米官方提供的適配方法沒有效果,只能用谷歌官方針對Android P的適配方案,這一點小米的官方文檔也提到了。
至于Android P以下版本的小米手機,我并沒有測試,如果有哪位大佬測試過了發(fā)現(xiàn)有問題可以反饋一下。
Vivo官方提供的適配文檔:Vivo全面屏應(yīng)用適配指南
Oppo官方提供的適配文檔:Oppo凹形屏適配指南
這里把Vivo和Oppo放在一起說,官方提供的資料不像華為和小米那么詳細(xì),只是提供了判斷是否有劉海屏的方法。
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');
}
至于全屏顯示的適配方案,通過閱讀官方文檔和網(wǎng)上的其他適配文章,我個人總結(jié)一下就是這兩種品牌的手機在設(shè)置全屏顯示時都無需做任何處理(前提是適配了全面屏,上文中提到過如何配置),也就是不會產(chǎn)生黑邊,我們只需要避免布局中的內(nèi)容或控件不被劉海區(qū)域所遮擋就可以了。具體的做法和沉浸式狀態(tài)欄的適配相同,基本原理還是將窗口布局下移,預(yù)留出狀態(tài)欄的高度。
注:由于手頭沒有這兩種廠商的手機,因此并沒有驗證,這一點確實是我做得不夠嚴(yán)謹(jǐn),有好心的大佬驗證之后歡迎指正。
其實我本來也想列出魅族的適配方案的,但是實在是沒找到官方文檔。。。如果有知道的大佬可以提供一下,我后面會把適配方案補上。
適配時的基本邏輯就是先判斷手機的品牌,這里我利用了一個開源工具類項目AndroidUtilCode,提供了一個獲取手機Rom信息的工具類RomUtils,用起來很方便,然后判斷是否是劉海屏,針對劉海屏手機添加適配代碼。完整的適配代碼如下所示:
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ìn)行適配
if (RomUtils.isHuawei() && HwNotchUtils.hasNotch(this)) {
HwNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
} else if (RomUtils.isXiaomi() && XiaomiNotchUtils.hasNotch(this)) {
XiaomiNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
}
}
雖然文中介紹了很多適配的內(nèi)容,但其實在開發(fā)中需要我們適配劉海屏的情況并不多,只有兩種情況需要我們進(jìn)行考慮:
1.沉浸式狀態(tài)欄,窗口布局延伸到了狀態(tài)欄中,是否會遮擋必要的內(nèi)容或控件(處在危險區(qū)域)。適配方案就是將窗口布局下移,預(yù)留出狀態(tài)欄的空間。
2.全屏顯示模式,不做適配的話狀態(tài)欄會呈現(xiàn)一條黑邊。適配方案是首先判斷系統(tǒng)版本,是Android P及以上就按照官方的API來適配,否則根據(jù)手機廠商的適配方案進(jìn)行適配。鑒于目前市面上Android P還沒有普及,為了帶來更好的用戶體驗,我們還是需要多花一些精力來適配各個手機廠商的劉海屏手機。
最后提示一下,本文只列出了四個當(dāng)下主流手機廠商的適配方案,我自己驗證過的只有華為和小米(只驗證了Android P)的方案,對于Vivo和Oppo的一些結(jié)論我可能說得不對,歡迎大家指正。當(dāng)然,如果大家還需要其他廠商的適配方案,也歡迎提出,我會盡力補上。
相關(guān)的代碼和工具類我已經(jīng)上傳到了github,可以下載Demo來查看,大家一起交流
https://github.com/StephenZKCurry/NotchAdaptedTest
---------- END ----------
分享大前端、Java、跨平臺等技術(shù),
關(guān)注職業(yè)發(fā)展和行業(yè)動態(tài)。