名詞說明
- 狀態欄:StatusBar,手機上方顯示電量、時間的橫條
- 導航欄:NavigationBar,手機下方顯示虛擬按鍵的橫條
- 標題欄:ActionBar,應用頂部顯示標題的橫條
- 全面屏:界面內容佔屏幕面積超80%以上的屏幕叫做全面屏,想要達到這個屏佔比,基本都是沒有導航欄的
狀態欄和導航欄自定義原理
通過SDK接口可以讓狀態欄和導航欄浮動並透明,不佔佈局空間
由於狀態欄和導航欄是透明的,我們可以自己定義兩個View填充在下面,再給View設置自定義背景,這樣就完成了自定義導航欄
但是這樣做有另一個難題,由於全面屏是不存在導航欄的,所以我們必須對全面屏進行適配
由於AndroidSDK沒有提供判斷全面屏的接口,我們需要自己找方案去實現
全面屏判斷原理
傳統屏幕是有導航欄的,由於導航欄本身也是一個控件,所以Window佈局下面,一定有一個和導航欄等高等寬的View
而全面屏沒有導航欄,則不會存在這樣的一個View,根據這個差異,我們去解析Window佈局結構,就能知道手機到底是不是全面屏
但是這樣做也有一個難題,就是佈局剛創建時,所有View的高度都是0,只有等佈局渲染完畢時,才能拿到正確的高度
也就是說,全面屏判斷不能在Activity.onCreate時立刻執行,必須等到佈局渲染完畢時,才能進行
由於AndroidSDK又沒有提供佈局渲染完畢的回調,所以我們又需要自己找方案去實現,不得不說,AndroidSDK在細節上確實還不夠完美
幸好我們有一個View.post()方法可以利用,這個方法的功能和Handler.post()一致,但是它會等到控件渲染完畢再執行,正好就滿足了我們的需求
代碼實現和功能封裝
通過以上分析,我們基本已經確定,功能是可以實現的,但是代碼會比較多且繁瑣
爲了以後使用方便,我們會對功能進行封裝,最好是以後通過單行代碼就可以解決適配問題
//CommonActivity.java
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.util.LinkedList;
import java.util.List;
public class CommonActivity extends AppCompatActivity {
//由於導航欄適配工作不能立刻執行
//所以我們需要先通過Runnable把它封裝起來,到了合適時候再執行
Runnable controlBarsAdapter;
//記錄有無導航欄
//現在的非全面屏手機也可以隱藏導航欄,我們的代碼並不僅適配全面屏,也適合傳統手機
Boolean hasNavigationBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//總是豎屏顯示,由於橫屏的寬高是不一樣的,橫屏代碼也需要適當調整
//這裏不想讓代碼變複雜,所以乾脆禁用橫屏,讓邏輯清晰點
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
//通過解析Window佈局,判斷有無導航欄
public void detectNavigationBar() {
int navigationBarHeight = Utils.navigationBarHeight(this);
int screenWidth = Utils.getScreenSize(this).getWidth();
//獲取Window下的全部節點,如果存在View和導航欄等高等寬,則說明存在導航欄
List<View> nodes = Utils.allWindowNode(this);
nodes = Utils.filter(nodes, node -> {
if (node.getClass() != NavigationBarPlaceholder.class)
if (node.getMeasuredHeight() == navigationBarHeight)
if (node.getMeasuredWidth() == screenWidth)
return true;
return false;
});
hasNavigationBar = nodes.size() > 0;
//適配狀態欄和導航欄
//因爲適配狀態欄和導航欄需要先知道手機是否存在導航欄,所以需要到此再執行
if (controlBarsAdapter != null) controlBarsAdapter.run();
}
//自定義狀態欄和導航欄
//讓狀態欄和導航欄浮動並透明,不佔佈局空間
//這樣就可以自己定義兩個View,分別放在statuBar和navigationBar的位置,來模擬自定義的狀態欄和導航欄
//默認使用R.id.v_top作爲statuBar佔位View,使用R.id.v_bottom作爲navigationBar佔位View
public void adaptControlBars() {
//將導航欄適配工作存在Runnable中,延遲到佈局加載完畢再調用
controlBarsAdapter = () -> {
//隱藏標題欄,標題欄會影響狀態欄浮動
//更好的方法是設置無標題欄的主題,隱藏標題欄會看到一瞬間的閃爍,不是很自然
//如果設置了無標題欄的主題,一定要註釋這一行,因爲ActionBar==null
super.getSupportActionBar().hide();
//讓狀態欄和導航欄浮動,不佔佈局空間
super.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
//設置狀態欄和導航欄透明
super.getWindow().setStatusBarColor(Utils.TRANSPARENT);
super.getWindow().setNavigationBarColor(Utils.TRANSPARENT);
//顯示狀態欄佔位View
View statuPlaceholder = Utils.getRootView(this).findViewById(R.id.v_top);
if (statuPlaceholder != null) {
int statuBarHeight = Utils.statuBarHeight(this);
Utils.size(statuPlaceholder, null, statuBarHeight);
}
//顯示導航欄佔位View
View navigationBarPlaceholder = Utils.getRootView(this).findViewById(R.id.v_bottom);
if (navigationBarPlaceholder != null && hasNavigationBar) {
int navigationBarHeight = Utils.navigationBarHeight(this);
Utils.size(navigationBarPlaceholder, null, navigationBarHeight);
}
//去除全面屏底部黑邊
//由於安卓系統限制了最大寬高比,全面屏一般會超出這個範圍
//超出部分沒有內容,就會顯示爲黑色,在手機底部產生黑邊效果
if (!hasNavigationBar) {
LinkedList<View> list = Utils.allWindowNode(this);
//正常屏幕的第一個節點和第三個節點都是全屏高的
//全面屏由於存在黑邊,第三個節點高度會比第一個節點小
//只要將第三個節點調至全屏大小,黑白就會消失
Utils.size(list.get(2), null, list.get(0).getMeasuredHeight());
}
};
}
}
//NavigationBarPlaceholder.java
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
//自定義一個View,用於自動執行View.post代碼,檢測是否有導航欄
public class NavigationBarPlaceholder extends View {
CommonActivity ctx;
public NavigationBarPlaceholder(Context context, AttributeSet attrSet) {
super(context, attrSet);
init(context, attrSet);
}
@Override
protected void onDraw(Canvas canvas) {
}
private void init(Context context, AttributeSet attrSet) {
this.ctx = (CommonActivity) context;
//View.post會等佈局加載完畢再執行,這時可以正確取得各控件的大小
//通過各控件的大小,可以判斷出手機是否是導航欄
post(() -> {
ctx.detectNavigationBar();
});
}
}
//MainActivity.java
import android.os.Bundle;
public class MainActivity extends CommonActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
super.adaptControlBars();
}
}
//activity_main.xml
<?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:background="#FFFFFF"
android:orientation="vertical">
<View
android:id="@+id/v_top"
android:layout_width="match_parent"
android:layout_height="0px"
android:background="#FF00FF" />
<View
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1" />
<com.easing.test.screen_adapt.NavigationBarPlaceholder
android:id="@+id/v_bottom"
android:layout_width="match_parent"
android:layout_height="0px"
android:background="#FF00FF" />
</LinearLayout>
//Utils.java
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.util.Size;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
//工具類
public class Utils {
public static final int TRANSPARENT = 0x00000000;
//獲取手機狀態欄高度
public static int statuBarHeight(Context context) {
try {
Class c = Class.forName("com.android.internal.R$dimen");
Object obj = c.newInstance();
Field field = c.getField("status_bar_height");
int x = Integer.parseInt(field.get(obj).toString());
return context.getResources().getDimensionPixelSize(x);
} catch (Exception e) {
return -1;
}
}
//獲取手機導航欄高度
public static int navigationBarHeight(Context context) {
Resources resources = context.getResources();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
//獲取屏幕大小
public static Size getScreenSize(Context context) {
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(dm);
return new Size(dm.widthPixels, dm.heightPixels);
}
//調整控件大小
public static void size(View v, Integer w, Integer h) {
ViewGroup.LayoutParams params = v.getLayoutParams();
if (params == null)
params = new ViewGroup.LayoutParams(0, 0);
if (w != null)
params.width = w.intValue();
if (h != null)
params.height = h.intValue();
v.setLayoutParams(params);
}
//獲取Activity的根節點View
public static View getRootView(Activity activity) {
return activity.findViewById(android.R.id.content);
}
//獲取Window下全部子控件
public static LinkedList<View> allWindowNode(Activity ctx) {
LinkedList<View> list = allViewNode(ctx.getWindow().getDecorView(), true);
return list;
}
//獲取View下全部子控件
public static LinkedList<View> allViewNode(View view, boolean recursive) {
LinkedList<View> children = new LinkedList();
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
children.add(child);
if (recursive)
children.addAll(allViewNode(child, recursive));
}
}
return children;
}
//過濾數據
public static <T> List<T> filter(List<T> datas, Predication<T> predication) {
List<T> filtered = new ArrayList();
for (T data : datas)
if (predication.predicate(data))
filtered.add(data);
return filtered;
}
public interface Predication<T> {
boolean predicate(T v);
}
}
使用方法
- 讓自己的Activity繼承CommonActivity
- 在佈局中使用View作爲StatusBar的填充View,並將id設置爲R.id.v_top
- 在佈局中使用NavigationBarPlaceholder作爲NavigationBar的填充View,並將id設置爲R.id.v_bottom
- 在Activity的onCreate中調用super.adaptControlBars()方法,CommonActivity會自動完成剩下的工作