前言
2017年9月,拜腾的横空出世,打破了车载主机界一直以来的沉寂,各大媒体也是不吝词藻的对它的超长中控屏进行了大肆的报道。这个时候作为同为车机供应者的诸位友商心里却不那么的平静,恨不得在发布会现场带上一把尺子。要知道,咱们国人的借鉴能力,那在世界上都是有一号的,很快的各大厂商也纷纷开始推广起了自己的超长屏。
谁的锅?
所谓超长屏,即在中控主机上既能显示汽车仪表,同时又可以显示地图导航的一块屏幕,对于应用的显示是可以做全屏展示和半屏展示切换的,即App从3840720分辨率到1280720分辨率的切换。做这块长屏适配的时候,我们本以为只要做好长屏切换短屏的时候UI界面的适配就可以高枕无忧,没想到在实机调试的时候,我们却被一个很尴尬的问题困了许久,首先让我们来看一下在3840*720屏幕上我们的显示效果。
从图片中可以看到,我们的应用并没有能够充满屏幕而只是占据了左边很窄的一个部分,这的确是一个非常离奇的现象,因为我们的布局使用的是填充结构,也就是match_parent,按逻辑来说应该会是拉伸至屏幕整个宽度。
因为用手机和夜神模拟器模拟器(3840*720分辨率)我们的应用都是有进行调试过的,于是乎,我们开始怀疑是主机厂的FrameWork定制层出了问题,因为前文提到过主机厂是可以动态控制我们应用3840与1280的显示空间的。
论证自己的假设
为了证明自己的“清白”,我们特意在新的版本中增加了对rootView的Log打印信息,方法如下:
private void printRootViewWidthAndHeight(){
fmParent.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
fmParent.getViewTreeObserver().removeOnPreDrawListener(this);
int width = fmParent.getWidth();
int height = fmParent.getHeight();
Logger.t(TAG).d("ParentView_width_is: "+width+" height_is: "+height);
return true;
}
});
}
打印结果果然不出我们所料,parentView的宽度只有1339左右,parentView是我们Activity的根布局View,跟布局只有1339,里面的子布局只显示在这个区域空间也就能够解释了。从层级上来说,parentView的上层是DecorView,而DecorView只是将ActionBar与content包含在内的一个布局,它限制宽度的可能性不大。我们都知道每个Activity都会持有一个Window,而在安卓中,Window只有唯一的一个实现类PhoneWindow ,所以每个Activity都会持有一个PhoneWindow,下一个重点怀疑对象就是这个PhoneWindow。
private void printScreenWidthAndHeightByWindowManager(){
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
int width = windowManager.getDefaultDisplay().getWidth();
int height = windowManager.getDefaultDisplay().getHeight();
Logger.t(TAG).d("Screen_width_is: "+width+" height_is: "+height);
}
PhoneWindow打印结果也依旧只有1339,难道说我们在代码中手动设置了Window宽度?重新复查代码后并没有发现修改Window宽度相关代码。没办法,只有从源码入手了。我们深入WindowMangager的getWidth方法。
深入源码
/**
* @deprecated Use {@link #getSize(Point)} instead.
*/
@Deprecated
public int getWidth() {
synchronized (this) {
updateCachedAppSizeIfNeededLocked();
return mCachedAppWidthCompat;
}
}
从Display源码的getWidth中可以看到,mCachedAppWidthCompat就是屏幕的宽度,那么mCachedAppWidthCompat是在何处给予的赋值呢?我们来看一下updateCachedAppSizeIfNeededLocked方法。
private void updateCachedAppSizeIfNeededLocked() {
long now = SystemClock.uptimeMillis();
if (now > mLastCachedAppSizeUpdate + CACHED_APP_SIZE_DURATION_MILLIS) {
updateDisplayInfoLocked();
mDisplayInfo.getAppMetrics(mTempMetrics, getDisplayAdjustments());
mCachedAppWidthCompat = mTempMetrics.widthPixels;
mCachedAppHeightCompat = mTempMetrics.heightPixels;
mLastCachedAppSizeUpdate = now;
}
}
从updateCachedAppSizeIfNeededLocked函数中可以看到,当超过缓存时间的时候,updateCachedAppSizeIfNeededLocked会从mTempMetrics中取出屏幕宽度作为新的宽度缓存对象,让我买来看一下mTempMetrics是什么。
// Temporary display metrics structure used for compatibility mode.
private final DisplayMetrics mTempMetrics = new DisplayMetrics();
通过查看源码我们发现mTempMetrics 其实就是DisplayMetrics 。看来要搞懂屏幕宽度的获取机制,首先要找到DisplayMetrics 的widthPixels赋值原理。深入DisplayMetrics,我们发现在DisplayMetrics内部并没有widthPixels的初始化或计算公式,widthPixels的赋值来源于外部赋值。
public void setTo(DisplayMetrics o) {
if (this == o) {
return;
}
//从DisplayMetrics获取widthPixels
widthPixels = o.widthPixels;
heightPixels = o.heightPixels;
density = o.density;
densityDpi = o.densityDpi;
scaledDensity = o.scaledDensity;
xdpi = o.xdpi;
ydpi = o.ydpi;
noncompatWidthPixels = o.noncompatWidthPixels;
noncompatHeightPixels = o.noncompatHeightPixels;
noncompatDensity = o.noncompatDensity;
noncompatDensityDpi = o.noncompatDensityDpi;
noncompatScaledDensity = o.noncompatScaledDensity;
noncompatXdpi = o.noncompatXdpi;
noncompatYdpi = o.noncompatYdpi;
}
通过对setTo函数的溯源,最终找到了一个名为updateSystemConfiguration的方法,从该方法中可以看到还是对DisplayMetrics的外部赋值,调查似乎进入了盒子效应,盒子的里面是盒子,里面的盒子里还是盒子。
/**
* Update the system resources configuration if they have previously
* been initialized.
*
* @hide
*/
public static void updateSystemConfiguration(Configuration config, DisplayMetrics metrics,
CompatibilityInfo compat) {
if (mSystem != null) {
mSystem.updateConfiguration(config, metrics, compat);
}
}
柳暗花明
在调查DisplayMetrics的过程中,安卓手机刘海屏适配的问题吸引了我的注意,自IPhoneX推出了刘海屏之后,安卓各位友商的刘海屏可谓是纷至沓来,颇有些累死安卓开发不偿命的架势。未做适配的应用运行在刘海屏上的效果和我们应用在车机上的运行效果是很相似的,都是设置了match_parent之后不能够填充满屏幕。本着谷歌不会对不完美显示置之不理的思想,我去查阅了谷歌官方对刘海屏的适配,终于发现了max_aspect这个属性,具体设置方法如下:
<meta-data android:name="android.max_aspect"
android:value="ratio_float"/>
max_aspect也可以通过代码设置,如下:
public void setMaxAspect() {
ApplicationInfo applicationInfo = null;
try {
applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if(applicationInfo == null){
throw new IllegalArgumentException(" get application info = null, has no meta data! ");
}
applicationInfo.metaData.putString("android.max_aspect", "5.3");
}
通过查阅资料,在不设置max_aspect属性的情况下,安卓系统会取默认值,即1.86,那么这个值到底是什么呢?这个值其实是屏幕分辨率宽高的比值,在全面屏量产以前,安卓手机的分辨率多为16:9,换算成浮点型的值大概为1.777,小于最大的宽高比1.86,所以能够正常显示。而我们的车载主机的分辨率为3840*720,宽长比达到了令人发指的5.33,远大于系统默认值1.86。因为没有设置max_aspect,系统在显示应用的时候会采取默认的宽长比来显示,让我们来回看一下出问题的应用显示效果,因为高度很小,只有720,所以宽度在限定了宽高比之后只能显示1339的大小。
了解了max_aspect的原理之后,显示不全的问题也就迎刃而解了,我们重设max_aspect的值为5.3(3840/720) ,再看一下效果。
可以看到显示不全的问题已完美解决。但是对于max_aspect我还有一个疑问,既然在全面屏以前最大是16:9只有1.77左右,为什么谷歌要设置默认值为1.86呢?难道谷歌早已看穿了一切?我觉得不会这么巧合。结合DisplayMetrics又对max_aspect进行了一番研究,我们发现在获取宽高的时候,虚拟导航菜单栏也是不会计算到宽高之中的,也就是说如果3840*720的屏幕上如果底部有一个100左右的导航栏,那么应用实际高度只有600左右,如果max_aspect赋值为5.3的话,根据600的宽高比换算出的宽度依旧不能充满屏幕,需要比5.3还要大一点的值才能够适配有虚拟按键的情况。
总结
很多曾经深信不疑的系统方法不一定真的无懈可击,就像我们最开始从WindowManager获取屏幕宽高,一味的盲目相信,很可能造成非常惨重的代价。
示例代码
参考文献
(1)https://android-developers.googleblog.com/2017/03/update-your-app-to-take-advantage-of.html
(2)https://blog.csdn.net/qq_33523706/article/details/79241585
(3)https://blog.csdn.net/reboot123/article/details/23710827
(4)https://blog.csdn.net/weelyy/article/details/79284332
(5)https://www.jianshu.com/p/b7594ae3900f