Android APP全面屏适配技术要点

全面屏的概念

为什么先要解释一下全面屏,因为这个词在现在来讲就是一个伪命题。全面屏字面意思就是手机的正面全部都是屏幕,100%的屏占比。但是现在推出所谓“全面屏”手机的厂商没有一个能达到全面的。

那么下面来说一下Android开发领域对全面屏的理解和定义吧。

一般手机的屏幕纵横比为16:9,如1080x1920、1440x2560等,其比值为1.77,在全面屏手机出现之前,Android中默认的最大屏幕纵横比(maximum aspect ratio)为1.86,即能够兼容16:9的屏幕。

一些手机厂商为了追求更大的屏幕空间以及更极致的用户体验,于是提高了屏幕纵横比,17:9、19:10、18:9、18.5:9的手机开始进入市场,这些手机的屏幕纵横比大大超过了1.86,这些手机被称为全面屏手机。

为何需要适配

我们将targetSdkVersion的值改为小于等于23,运行程序,我们会发现屏幕底部出现一个黑条。

image

如何适配

targetSdkVersion<=23,更大的屏幕纵横比

在Galaxy S8发布之后,Android官方提供了适配方案,即提高App所支持的最大屏幕纵横比,实现很简单,在AndroidManifest.xml中可做如下配置:

<meta-data android:name="android.max_aspect"
    android:value="ratio_float"/>

其中ratio_float为浮点数,官方建议为2.1或更大,因为18.5:9=2.055555555……,如果日后出现纵横比更大的手机,此值将会更大。

<meta-data android:name="android.max_aspect" 
    android:value="2.1" />

max_aspect值也可以在Java代码中动态地设置,通过下面的方法即可实现:

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 ");
        }
        applicationInfo.metaData.putString("android.max_aspect", "2.1");
    }

如果targetSdkVersion的值的值大于23,那么应该不用设置max_aspect即可。

查看适配之后的截图:

image

https://android-developers.googleblog.com/2017/03/update-your-app-to-take-advantage-of.html

图片资源适配

我们看一下启动页,在16:9屏幕中适配的图片,到了18:9的屏幕中就会被拉伸了。

16:9屏幕中显示 18:9屏幕中显示
image
image

解决这个问题无非就是两种方法,换图片或者是换布局

换图片

不能依赖单一厂商的解决方案,只能从Android系统属性出发。考虑到目前大部分全面屏手机只是在高度上拉长,且大多为6.0英寸左右,像素密度对比xxhdpi并没有多大区别,那我们可以在项目中增加一组资源drawable-xxhdpi-2160x1080 、drawable-long 这样解决图片的拉伸问题,当然这样的方法肯定是不太好的,会增加app的容量。这里就不演示了。

优化布局

当然最好的方法还是用相对布局采用XML的方式,或者.9图的解决方案。

我总结的就是少量多切,尽量减少尺寸对布局的影响。比如这里,使用正方形的切图,让他居中显示,无论屏幕纵横比如何,都不会拉伸这个图片,拉伸的只是背景而已。

image
<ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="fitCenter"
        android:src="@drawable/bz002"/>
适配前 适配后
image
image

全面屏高度问题适配

首先解释一下window,decorview,rootview这几个概念

image

Window官方文档:Window

public abstract class Window. Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc.

The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.

翻译一下:每一个 Activity 都持有一个 Window 对象,但是 Window 是一个抽象类,这里 Android 为 Window 提供了唯一的实现类 PhoneWindow。也就是说 Activity 中的 window 实例就是一个 PhoneWindow 对象。

但是 PhoneWindow 终究是 Window,它并不具备多少 View 相关的能力。不过 PhoneWindow 中持有一个 Android 中非常重要的一个 View 对象 DecorView.

现在的关系就很明确了,每一个 Activity 持有一个 PhoneWindow 的对象,而一个 PhoneWindow 对象持有一个 DecorView 的实例,所以 Activity 中 View 相关的操作其实大都是通过 DecorView 来完成

DecorView就可以理解为手机的内屏,就是那块玻璃,可以发光的屏幕。

这里通过代码,打印出我们页面中的高度的各项数据

int decorviewHeight = decorView.getHeight();
int screenHeight = FullScreenManager.getScreenHeight();
int nativeBarHeight = FullScreenManager.getNativeBarHeight();
int contentViewHeight = rootView.getHeight();
int navigationBarHeight1 = FullScreenManager.getNavigationBarHeight();

Log.d("shijiacheng","=======================================");
Log.d("shijiacheng","DecorView height: " + decorviewHeight + " px");
Log.d("shijiacheng","Screen height: " + screenHeight + " px");
Log.d("shijiacheng","NativeBar height: " + nativeBarHeight + " px");
Log.d("shijiacheng","ContentView height: " + contentViewHeight + " px");
Log.d("shijiacheng","NavigationBar height: " + navigationBarHeight + " px");
Log.d("shijiacheng","---------------------------------------");

获取decorView的高度

final View decorView = getWindow().getDecorView();
int decorviewHeight = decorView.getHeight();

获得屏幕高度

/**
 * 获得屏幕高度
 * @return
 */
public static int getScreenHeight() {
    Resources resource = AppContext.getInstance().getResources();
    DisplayMetrics displayMetrics = resource.getDisplayMetrics();
    return displayMetrics.heightPixels;
}

获取状态栏的高度

/**
 * 获取状态栏的高度
 *
 * @return
 */
public static int getNativeBarHeight() {
    Resources resource = AppContext.getInstance().getResources();
    int result = 0;
    int resourceId = resource.getIdentifier("status_bar_height", 
            "dimen", "android");
    if (resourceId > 0) {
        result = resource.getDimensionPixelSize(resourceId);
    }
    return result;
}

获取contentView的高度

LinearLayout contentView = findViewById(R.id.root);
int contentViewHeight = contentView.getHeight();

获取NavigationBar的高度

public static int getNavigationBarHeight() {
    Resources resources =  AppContext.getInstance().getResources();
    int resourceId = resources.getIdentifier("navigation_bar_height","dimen", "android");
    int height = resources.getDimensionPixelSize(resourceId);
    return height;
}

为了更加直观的展示各个数据,这里我们使用布局的方式将各个数据展示出来,布局代码比较简单,这里就不展示了。

image

先展示一下正常的屏幕高度的各项数据

10-08 09:52:03.636 23818-23818/? D/shijiacheng: =========================
10-08 09:52:03.637 23818-23818/? D/shijiacheng: DecorView height: 1280 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: Screen height: 1280 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: NativeBar height: 50 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: ContentView height: 1230 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: NavigationBar height: 96 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: -------------------------
image

DecorView = Screen height = NativeBar height + ContentView height

看一下小米mix全面屏的情况

2018-10-08 09:54:15.640 /? D/shijiacheng: =========================
2018-10-08 09:54:15.640 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 09:54:15.641 /? D/shijiacheng: RootView height: 2094 px
2018-10-08 09:54:15.641 /? D/shijiacheng: Screen height: 2030 px
2018-10-08 09:54:15.641 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 09:54:15.641 /? D/shijiacheng: ContentView height: 2094 px
2018-10-08 09:54:15.641 /? D/shijiacheng: NavigationBar height: 130 px
2018-10-08 09:54:15.641 /? D/shijiacheng: -------------------------
image

问题出现了,可以发现contentView的高度比screen屏幕的高度还要大,不禁要怀疑,我们的获取屏幕高度的方法在全面屏下计算错误了。

问题1:获取屏幕高度方法计算不准确

我们一直都是使用如下方法进行屏幕高度测量的:

public static int getScreenHeight() {
    Resources resource = AppContext.getInstance().getResources();
    DisplayMetrics displayMetrics = resource.getDisplayMetrics();
    return displayMetrics.heightPixels;
}

但是这个方法却是一个十分古老的方法,没有与时俱进,虽然说在普通屏幕上这种方法没有问题,但是在全面屏手机上来说,这种方法就不灵了。

下面我们就来研究一下获取屏幕尺寸的方法的演进。

获取屏幕宽高

获取屏幕的宽高是我们开发中经常遇到的问题,而且相信大家都已经非常熟悉,最常用的为以下两种:

public static int getScreenHeight1(Activity activity) {
    return activity.getWindowManager().getDefaultDisplay().getHeight();
}
public static int getScreenHeight2(Activity activity) {
    DisplayMetrics displayMetrics = new DisplayMetrics();
    activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    return displayMetrics.heightPixels;
}

其实以上两种方式是一样的,只不过第二种是把信息封装到 DesplayMetrics中,再从DesplayMetrics得到数据。

在 Android 3.2(Api 13) 之后又提供了如下的一个方法,将数据封装到Point中,然后返回宽度高度信息。

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
public static int getScreenHeight3(Activity activity) {
    Point point = new Point();
    activity.getWindowManager().getDefaultDisplay().getSize(point);
    return point.y;
}

在 Android 4.2(Api17) 之后提供了如下方法,与第三种类似也是将数据封装到Point中,然后返回款高度信息。

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static int getScreenHeight4(Activity activity) {
    Point realSize = new Point();
    activity.getWindowManager().getDefaultDisplay().getRealSize(realSize);
    return realSize.y;
}

其实getRealSize这个方法在Android Api15的时候就已经加入了,不过是被隐藏了,通过查阅源码我们可以看到。

image

Android Api15 Display.java源码中getRealSize()方法被标记为@hide

image

因此,我们可以重写获取高度的方法,适配所有机型,所有系统。

适配所有屏幕的获取屏幕尺寸的方法

public static int[] getScreenSize(Context context) {
        int[] size = new int[2];

        WindowManager w = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display d = w.getDefaultDisplay();
        DisplayMetrics metrics = new DisplayMetrics();
        d.getMetrics(metrics);
        // since SDK_INT = 1;
        int widthPixels = metrics.widthPixels;
        int heightPixels = metrics.heightPixels;

        // includes window decorations (statusbar bar/menu bar)
        if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 17)
            try {
                widthPixels = (Integer) Display.class.getMethod("getRawWidth").invoke(d);
                heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(d);
            } catch (Exception ignored) {
            }
        // includes window decorations (statusbar bar/menu bar)
        if (Build.VERSION.SDK_INT >= 17)
            try {
                Point realSize = new Point();
                Display.class.getMethod("getRealSize", Point.class).invoke(d, realSize);
                widthPixels = realSize.x;
                heightPixels = realSize.y;
            } catch (Exception ignored) {
            }
        size[0] = widthPixels;
        size[1] = heightPixels;
        return size;
    }

使用新的获取高度的方法,重新运行程序,运行结果已经正常显示了。

2018-10-08 13:19:32.389 /? D/shijiacheng: ==========================
2018-10-08 13:19:32.390 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: Screen height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 13:19:32.390 /? D/shijiacheng: ContentView height: 2094 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NavigationBar height: 130 px
2018-10-08 13:19:32.390 /? D/shijiacheng: --------------------------
image

问题2:小米mix切为经典导航键模式下的计算问题

我们在MIUI设置中将全面屏导航样式修改为“经典导航键”样式。

image

重新运行程序,运行结果如下:

image

可以发现又出问题了,DecorView = Screen height > NativeBar height + ContentView height

这里不难发现,Screen height将底部虚拟导航栏的高度也算进里面了。

很多情况下,我们都用如下方法获取导航栏的高度:

public static int getNavigationBarHeight() {
        Resources resources =  AppContext.getInstance().getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height",
            "dimen", "android");
        int height = resources.getDimensionPixelSize(resourceId);
        return height;
    }

这种方法得到的导航栏的高度数值是没问题的,但是在全面屏的手机上,即使隐藏了导航栏,也是可以获取到导航栏的高度的。通过上面的logcat日志可以看到,即使没有导航栏,导航栏的高度的计算也是有值的。

适配小米mix虚拟导航栏

小米mix的机型中,我们可以“force_fsg_nav_bar”来判断小米手机是否开启了全面屏手势。

public static int getHeightOfNavigationBar(Context context) {
        //如果小米手机开启了全面屏手势隐藏了导航栏则返回 0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            if (Settings.Global.getInt(context.getContentResolver(), 
                "force_fsg_nav_bar", 0) != 0) {
                return 0;
            }
        }
        int realHeight = getScreenSize(context)[1];

        Display d = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
        .getDefaultDisplay();
        DisplayMetrics displayMetrics = new DisplayMetrics();
        d.getMetrics(displayMetrics);

        int displayHeight = displayMetrics.heightPixels;

        return realHeight - displayHeight;
    }

因此可以通过这个方法来判断是否显示了底部导航栏,并且可以计算导航栏的高度。

int navigationBarHeight = FullScreenManager.getHeightOfNavigationBar(MainActivity.this);
if (navigationBarHeight > 0){
    container_navigationview.setVisibility(View.VISIBLE);
}else {
    container_navigationview.setVisibility(View.GONE);
}

正常的显示效果如下:

有虚拟导航栏 没有虚拟导航栏
image
image

没有虚拟导航栏Log

2018-10-08 13:19:32.389 /? D/shijiacheng: ==========================
2018-10-08 13:19:32.390 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: Screen height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 13:19:32.390 /? D/shijiacheng: ContentView height: 2094 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NavigationBar height: 0 px
2018-10-08 13:19:32.390 /? D/shijiacheng: --------------------------

有虚拟导航栏Log

2018-10-08 13:38:03.229 /? D/shijiacheng: ==========================
2018-10-08 13:38:03.230 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 13:38:03.230 /? D/shijiacheng: Screen height: 2160 px
2018-10-08 13:38:03.230 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 13:38:03.230 /? D/shijiacheng: ContentView height: 1964 px
2018-10-08 13:38:03.230 /? D/shijiacheng: NavigationBar height: 130 px
2018-10-08 13:38:03.230 /? D/shijiacheng: --------------------------
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容