在安卓开发中,我们除了需要适配不同手机的手机分辨率外,还需要适配手机上的虚拟状态栏和导航栏的高度,这其中又不乏有一些手机的手机屏幕格外的与众不同,比如今天我们需要了解的“刘海屏”。什么是“刘海屏”?屏幕的正上方居中位置(下图黑色区域)会被挖掉一个孔,屏幕被挖掉的区域无法正常显示内容,这种类型的屏幕就是刘海屏,也有其他叫法:挖孔屏、凹凸屏等等,这里统一按刘海屏命名。
目前网上也有很多适配安卓手机“刘海屏”的教程:安卓适配“刘海屏”手机。
这里我们就来讨论一下,如何在实际的安卓开发过程中,适配“刘海屏”手机屏幕。
首先我们的项目一定会有一个统一封装的TittleView或者封装的一个带头部布局的TitleBaseActivity,这个TitleBaseActivity里面有对头部布局的方法封装,需要显示头部布局的Activity就可以继承这个TitleBaseActivity,并且在布局文件里面添加头部布局文件,这里需要注意的是,我们的TitleBaseActivity是一个抽象类,继承与BaseActivity,里面主要做了findViewById的操作,然后实现返回按钮的事件监听。
直接贴上代码:
public abstract class AbsTitleFenJiActivity extends AbsFenJActivity {
protected TipView mTipView;
protected View mViewReadDot;
protected View mViewStatusBar;
protected AppCompatImageView mImgRight;
protected ConstraintLayout mClTitleRight;
protected AppCompatTextView mHeadSaveBtn;
protected AppCompatImageButton mHeadBackBtn;
protected AppCompatTextView mHeadTitleView;
@Override
public void initViews(Bundle savedInstanceState) {
mTipView = findView(R.id.tip_view);
mViewStatusBar = findView(R.id.view_status_bar);
mHeadBackBtn = findView(R.id.ibtn_head_back);
mHeadTitleView = findView(R.id.tv_head_title);
mImgRight = findView(R.id.img_title_right);
mHeadSaveBtn = findView(R.id.btn_save);
mViewReadDot = findView(R.id.view_red_dot);
mClTitleRight = findView(R.id.cl_title_right);
mHeadTitleView.setText(getTitleString());
mHeadSaveBtn.setTextColor(getRightTextColor());
mHeadBackBtn.setVisibility(getBackViewVisibility());
resetStatusBarHeight(mViewStatusBar);
if (getTitleRightIcon() > 0) {
mImgRight.setVisibility(View.VISIBLE);
mHeadSaveBtn.setVisibility(View.VISIBLE);
mImgRight.setImageResource(getTitleRightIcon());
} else if(ObjectUtils.isEmpty(getTitleRightString()) && getTitleRightImageView() > 0 ){ //如果右侧设置了图标,隐藏文字
mHeadSaveBtn.setVisibility(View.INVISIBLE);
mImgRight.setVisibility(View.VISIBLE);
mImgRight.setImageResource(getTitleRightImageView());
}else {
mImgRight.setVisibility(View.GONE);
mHeadSaveBtn.setText(getTitleRightString());
}
}
protected void resetStatusBarHeight(View view) {
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) view.getLayoutParams();
layoutParams.height = getStatusBarHeight();
}
@Override
public void initListeners() {
mHeadBackBtn.setOnClickListener(view -> finish());
}
/**
* 重设置标题
* @param titleName
*/
protected void resetTitle(String titleName) {
mHeadTitleView.setText(titleName);
}
protected abstract String getTitleString();
protected abstract String getTitleRightString();
protected abstract int getTitleRightImageView();
protected int getBackViewVisibility() {
return View.VISIBLE;
}
protected int getRightTextColor() {
return getColor_(R.color.black);
}
protected int getTitleRightIcon() {
return -1;
}
}
可以发现,我们这个TitleBaseActivity封装了三个头部文件左、中、右三个区域的代码实现方法,需要子类实现,如果子类没有需要,可以返回默认值,就不显示。
现在再回到适配“刘海屏”手机屏幕的问题,我们可以通过对统一封装好的TitleBaseActivity里面去判断手机屏幕是否是“刘海屏”手机屏幕,是刘海屏手机屏幕则获取具体的“刘海”高度。然后如果能正常显示的刘海可以不显示出来,不正常的刘海(一般是高度过高)我们就需要增加我们获取的头部导航栏的高度,来适配手机“刘海”的高度。这里我个人的猜想就是刘海的高度和头部导航栏的高度不相同的时候就会出现需要适配的问题,否则我们通过获取状态栏高度来做沉浸式是完全可以适配“刘海屏”的。就相当于刘海区域只是突出到了状态栏的高度。但是如果刘海的高度高出了状态栏的高度,也就是当我们获取手机头部状态栏高度的时候,获取的其实就是刘海的高度,高于实际的状态栏的高度。
按网上的教程写了一个获取手机状态栏高度和判断手机是否是刘海屏手机的工具类,测试发现工具类并不准确。屏幕适配工具类直接贴代码如下:
/**
* 安卓手机适配底部导航栏高度的工具类
* @author guotianhui
*/
public class PhoneNaigatBarUtils {
private static PhoneNaigatBarUtils instance = null;
private PhoneNaigatBarUtils(){}
public static PhoneNaigatBarUtils getInstance(){
if(instance ==null){
synchronized (PhoneNaigatBarUtils.class){
if(instance == null){
instance = new PhoneNaigatBarUtils();
}
}
}
return instance;
}
/**
* 适配方法
*/
public int adaptAcitivityViewHeightWithBar(Context context,Activity activity){
if(checkDeviceHasNavigationBar(context)){ //如果手机有底部导航栏,去掉底部导航栏的高度
return (getScreenHeight(activity) - getNavigationBarHeight(activity));
}else{
return 0;
}
}
public int adaptAcitivityHeightWithNavigationBar(Context context,Activity activity){
if(checkDeviceHasNavigationBar(context)){ //如果手机有底部导航栏,去掉底部导航栏的高度
return (getScreenHeight(activity) - (getNavigationBarHeight(activity) - getStatusBarHeight(activity)-20));
}else{
return 0;
}
}
private int getNavigationBarHeight(Activity activity) {
Resources resources = activity.getResources();
int resourceId=resources.getIdentifier("navigation_bar_height","dimen","android");
return resources.getDimensionPixelSize(resourceId);
}
public int getStatusBarHeight(Activity activity) {
Resources resources = activity.getResources();
int resourceId = resources.getIdentifier("status_bar_height","dimen","android");
return resources.getDimensionPixelSize(resourceId);
}
/**
*获取是否存在NavigationBar
*/
private boolean checkDeviceHasNavigationBar(Context context) {
boolean hasNavigationBar = false;
Resources rs = context.getResources();
int id = rs.getIdentifier("config_showNavigationBar", "bool", "android");
if (id > 0) {
hasNavigationBar = rs.getBoolean(id);
}
try {
Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
Method m = systemPropertiesClass.getMethod("get", String.class);
String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
hasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
hasNavigationBar = true;
}
} catch (Exception ignored) {
}
return hasNavigationBar;
}
public int getScreenHeight(Context context) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
return dm.heightPixels;
}
/**
* 判断手机是否是刘海屏
* @param context
* @return
*/
public boolean hasNotchInScreenHuawei(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;
}
}
public boolean hasNotchInScreenAtVoio(Context context){
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class FtFeature = cl.loadClass("com.util.FtFeature");
Method get = FtFeature.getMethod("isFeatureSupport",int.class);
ret = (boolean) get.invoke(FtFeature,0x00000020);//是否有凹槽
} 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; }
}
public boolean hasNotchInOppo(Context context){
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
}
测试代码如下:
val naigatBarUtils = PhoneNaigatBarUtils.getInstance();
val statusBarHeight = naigatBarUtils.getStatusBarHeight(this@TestActivity)
Log.e(">>>>>>>>>>>>", "statusBarHeight:$statusBarHeight")
val hasNotchInScreenHuawei = naigatBarUtils.hasNotchInScreenHuawei(context)
Log.e(">>>>>>>>>>>>", "hasNotchInScreenHuawei:$hasNotchInScreenHuawei")
val hasNotchInScreenAtVoio = naigatBarUtils.hasNotchInScreenAtVoio(context)
Log.e(">>>>>>>>>>>>", "hasNotchInScreenAtVoio:$hasNotchInScreenAtVoio")
val hasNotchInOppo = naigatBarUtils.hasNotchInOppo(context)
Log.e(">>>>>>>>>>>>", "hasNotchInOppo:$hasNotchInOppo")
测试代码运行在Vivo手机8.1系统,测试得到结果如下图:
结果显示:获取Vovi手机是否是“刘海屏”的代码出现了ClassNotFoundException 错误,说明网上说的判断工具类并不是适用。
目前问题也不是很严重,就是发现小米手机8.1系统会有问题。
然后查看小米社区8.1刘海屏问题,反应是状态栏高度和刘海不齐平的问题,然后查看小米官网刘海屏幕适配,官网建议是使用8.1的刘海屏用户,升级系统到MIUI系统10.
所以,适配刘海屏其实没有那么复杂,只要状态栏高度和刘海的高度齐平,我们做沉浸式拿到手机状态栏的高度之后,其实就会在手机的正中央出现一小块刘海区域,其他的并不会有影响。小米8.0系统有问题,升级系统应该就可以解决。