阅读 1319

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,运行程序,我们会发现屏幕底部出现一个黑条。

如何适配

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, has no meta data! ");
        }
        applicationInfo.metaData.putString("android.max_aspect", "2.1");
    }
复制代码

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

查看适配之后的截图:

android-developers.googleblog.com/2017/03/upd…

图片资源适配

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

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

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

换图片

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

优化布局

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

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

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

全面屏高度问题适配

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

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;
}
复制代码

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

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

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: -------------------------
复制代码

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: -------------------------
复制代码

问题出现了,可以发现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的时候就已经加入了,不过是被隐藏了,通过查阅源码我们可以看到。

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

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

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

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: --------------------------
复制代码

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

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

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

可以发现又出问题了,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);
}
复制代码

正常的显示效果如下:

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

没有虚拟导航栏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: --------------------------
复制代码
关注下面的标签,发现更多相似文章
评论