名词说明
- 状态栏: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 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 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
//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 allWindowNode(Activity ctx) {
LinkedList list = allViewNode(ctx.getWindow().getDecorView(), true);
return list;
}
//获取View下全部子控件
public static LinkedList allViewNode(View view, boolean recursive) {
LinkedList children = new LinkedList();
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?