伴鱼技术团队

Technology changes the world

伴鱼开放平台 上线了! 源于实践的解决方案,助力企业成就未来!

屏幕适配介绍及AndroidAutoSize使用指南

背景介绍

因为Android设备的种类繁多,屏幕的尺寸也是五花八门,结果同一个设计方案在不同的设备上的显示效果就会有所差异。所以,就需要对不同的设备做适配,以获取在不同尺寸的设备上有相同显示效果的能力。

引入一个公式

  • 像素:px,物理单位,一般系统设备上显示的尺寸如:1280X768用的就是像素单位;
  • 设备独立像素:dp,一般以dp为尺寸单位的控件;
  • 像素密度:dpi,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值。

这里要注意的是,dpi都是软件意义上的单位,在特定的机型上它是系统软件上的一个配置项,并不是一成不变的,是可以修改的。下面要说的“今日头条方案”就是基于修改这个值来实现的。

Android系统上一块屏幕上像素的个数和DP的数量之间有个对应关系:

Count(px) = Count(dp) * (dpi/160)

屏幕适配方案都是基于对这个公式的处理,因为处理方法的不同,就有了不同的适配方案。

方案选择

对于下面的公式的处理,介绍几种不同的适配方案。两个限定符方案,dimension文件后的限定符是系统可识别的,系统会根据当前的屏幕配置选择对应的dimension文件绘制View。

Count(px) = Count(dp) * (dpi/160)

  • 宽高限定符适配方案:面向像素px,采用系统定义值。对不同的物理像素尺寸指定不同的像素宽度定义,例如同一个View在800x480的屏幕上的宽度为view_width=100px,在1024x600上的宽度为view_width=125px。如下表,需要在res目录下维护一组尺寸定义文件,对同一个尺寸变量给出相应的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ├── src/main
    │ ├── res
    │ ├── ├──values
    │ ├── ├──values-800x480
    │ ├── ├──values-860x540
    │ ├── ├──values-1024x600
    │ ├── ├──values-1024x768
    │ ├── ├──...
    │ ├── ├──values-2560x1440
  • smallWidth限定符适配方案:设备独立像素dp,采用系统定义值。对不同的dp尺寸指定不同的dp宽度定义,例如同一个View在宽度320dp的屏幕上的宽度为view_width=100dp,在宽度480dp的屏幕上的宽度为view_width=150dp。如下表,需要在res目录下维护一组尺寸定义文件,对同一个尺寸变量给出相应的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ├── src/main
    │ ├── res
    │ ├── ├──values
    │ ├── ├──values-sw320dp
    │ ├── ├──values-sw360dp
    │ ├── ├──values-sw400dp
    │ ├── ├──values-sw411dp
    │ ├── ├──values-sw480dp
    │ ├── ├──...
    │ ├── ├──values-sw600dp
    │ ├── ├──values-sw640dp
  • 今日头条适配方案:与上面两个方案相比,并不是对公式中的某个单位量进行适配。而是通过直接修改系统定义,来达到使 单位量Count(dp) 统一到某个数值的目的。Count(px)是物理数量,是不可以改变的,如果我们想要统一Count(dp)的值,就需要修改dpi的值来实现。

我们对比了几种常用的实现方案,选择所谓的“今日头条方案”作为适配实施方案。

屏幕适配方案对比

参考文章今日头条屏幕适配方案终极版正式发布!中的说明,今日头条的适配方案并不是完美的适配方案,和其他两种方案相比各有优缺点,我们选择这个方案的主要是因为它的灵活性高、扩展性好。

AndroidAutoSize使用指南

开源项目地址

AndroidAutoSize是一个开源项目,是基于上述“今日头条适配方案”,封装的一个方便易用可自动适配,也支持定制化拓展的开源适配项目:GitHub项目链接

AndroidAutoSize基本操作

AndroidAutoSize最基本的操作是利用ActivityLifecycleCallbacks在页面开始绘制之前,修改DisplayMetrics对象的成员变量值,在之后的绘制过程中,取得的系统配置是修改之后的值,就可以达到屏幕适配的目的。

可以这么做的原因是DisplayMetrics对象是公共的,谁都可以修改。也是因为这个原因,有可能遇到在页面绘制过程中或者在触发式操作之后需要更新页面时DisplayMetrics对象已经被修改而发生页面适配失败的现象。

适配的宽度和高度标准在manifest文件中通过meta-data定义,标准的宽度和高度是以dp为单位的, 标准值可以根据设计需求确定

1
2
3
4
5
6
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>

AndroidAutoSize集成

AndroidAutoSize库引入
1
api 'me.jessyan:autosize:1.1.2'
AndroidAutoSize初始化配置

AutoSizeConfig类是AndroidAutoSize的配置入口。这个类是单例的,使用 AutoSizeConfig.getInstance() 获取配置对象。建议在Application对象的

``` 方法中进行配置。
1
2
3
4
5
6
7
8
9
10
11
12

* 标准高度和标准宽度的配置:尺寸标准根据设计而定,只需要根据默认适配方向设置其中一个即可。

如果不在manifest中配置标准尺寸,也可以在AutoSizeConfig初始化时设置:

``` xml
<meta-data
android:name="design_width_in_dp"
android:value="375" />
<meta-data
android:name="design_height_in_dp"
android:value="667" />

1
2
3
AutoSizeConfig.getInstance()
.setDesignWidthInDp(375)
.setDesignHeightInDp(667);
  • 适配策略配置:AndroidAutoSize提供了默认的适配策略,如果需要可以设置定制的适配策略替代默认策略,CustomAdaptStrategy是AutoAdaptStrategy的实现类,实现逻辑参考DefaultAutoAdaptStrategy:

    1
    2
    3
    AutoSizeConfig.getInstance()
    .setAutoAdaptStrategy(new CustomAdaptStrategy())
    .setExcludeFontScale(true);
纵向适配问题

AndroidAutoSize默认使用的是宽度(横向)适配,高度(纵向)适配时Activity需要实现CustomAdapt。当然,默认使用的适配方方向是在AutoSizeConfig对象中的AutoAdaptStrategy对象内指定的,也可以指定默认为高度(纵向)适配。这里用纵向/横向的说法,是为了说明使用过程中的适配与屏幕旋转方向无关。

1
2
3
4
public interface CustomAdapt {
boolean isBaseOnWidth(); // 指定适配方向
float getSizeInDp(); // 适配方向上的DP数值
}
1
2
3
4
5
6
7
8
9
@Override
public boolean isBaseOnWidth() {
return false;
}

@Override
public float getSizeInDp() {
return 640;
}
横竖屏旋转的处理

AndroidAutoSize对AutoAdaptStrategy 对象的使用是通过包装类调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WrapperAutoAdaptStrategy implements AutoAdaptStrategy {
private final AutoAdaptStrategy mAutoAdaptStrategy;

public WrapperAutoAdaptStrategy(AutoAdaptStrategy autoAdaptStrategy) {
mAutoAdaptStrategy = autoAdaptStrategy;
}

@Override
public void applyAdapt(Object target, Activity activity) {
onAdaptListener onAdaptListener = AutoSizeConfig.getInstance().getOnAdaptListener();
if (onAdaptListener != null){
onAdaptListener.onAdaptBefore(target, activity);
}
if (mAutoAdaptStrategy != null) {
mAutoAdaptStrategy.applyAdapt(target, activity);
}
if (onAdaptListener != null){
onAdaptListener.onAdaptAfter(target, activity);
}
}
}

包装类在调用适配策略之前和之后都调用了 AutoSizeConfig.getInstance().getOnAdaptListener() ,这个回调对象是通过 AutoSizeConfig.getInstance().setOnAdaptListener(onAdaptListener) 设置,切换横竖屏是需要在 onAdaptBefore 中交换屏幕的宽和高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AutoSizeConfig.getInstance() //屏幕适配监听器
.setOnAdaptListener(new onAdaptListener() {
@Override
public void onAdaptBefore(Object target, Activity activity) {
//使用以下代码, 可以解决横竖屏切换时的屏幕适配问题
//使用以下代码, 可支持 Android 的分屏或缩放模式, 但前提是在分屏或缩放模式下当用户改变您 App 的窗口大小时系统会重绘当前的页面, 经测试在某些机型, 某些情况下系统不会重绘当前页面, ScreenUtils.getScreenSize(activity) 的参数一定要不要传 Application!!!
AutoSizeConfig.getInstance().setScreenWidth(ScreenUtils.getScreenSize(activity)[0]);
AutoSizeConfig.getInstance().setScreenHeight(ScreenUtils.getScreenSize(activity)[1]);
AutoSizeLog.d(String.format(Locale.ENGLISH, "%s onAdaptBefore!", target.getClass().getName()));
}

@Override
public void onAdaptAfter(Object target, Activity activity) {
AutoSizeLog.d(String.format(Locale.ENGLISH, "%s onAdaptAfter!", target.getClass().getName()));
}
})

默认适配策略

上面讲到,AndroidAutoSize是通过ActivityLifecycleCallbacks决定触发修改系统参数时机的。触发的修改系统参数的操作是通过AutoAdaptStrategy实现。

1
2
3
4
5
6
7
8
9
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// TODO 此处略去一部分

//Activity 中的 setContentView(View) 一定要在 super.onCreate(Bundle); 之后执行
if (mAutoAdaptStrategy != null) {
mAutoAdaptStrategy.applyAdapt(activity, activity);
}
}

DefaultAutoAdaptStrategy是默认的适配策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 屏幕适配逻辑策略默认实现类, 可通过 {@link AutoSizeConfig#init(Application, boolean,
*/
public class DefaultAutoAdaptStrategy implements AutoAdaptStrategy {
@Override
public void applyAdapt(Object target, Activity activity) {

//检查是否开启了外部三方库的适配模式, 只要不主动调用 ExternalAdaptManager 的方法, 下面的代码就不会执行
if (AutoSizeConfig.getInstance().getExternalAdaptManager().isRun()) {
if (AutoSizeConfig.getInstance().getExternalAdaptManager().isCancelAdapt(target.getClass())) {
AutoSizeLog.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
} else {
ExternalAdaptInfo info = AutoSizeConfig.getInstance().getExternalAdaptManager()
.getExternalAdaptInfoOfActivity(target.getClass());
if (info != null) {
AutoSizeLog.d(String.format(Locale.ENGLISH, "%s used %s for adaptation!", target.getClass().getName(), ExternalAdaptInfo.class.getName()));
AutoSize.autoConvertDensityOfExternalAdaptInfo(activity, info);
return;
}
}
}

//如果 target 实现 CancelAdapt 接口表示放弃适配, 所有的适配效果都将失效
if (target instanceof CancelAdapt) {
AutoSizeLog.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
}

//如果 target 实现 CustomAdapt 接口表示该 target 想自定义一些用于适配的参数, 从而改变最终的适配效果
if (target instanceof CustomAdapt) {
AutoSizeLog.d(String.format(Locale.ENGLISH, "%s implemented by %s!", target.getClass().getName(), CustomAdapt.class.getName()));
AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);
} else {
AutoSizeLog.d(String.format(Locale.ENGLISH, "%s used the global configuration.", target.getClass().getName()));
AutoSize.autoConvertDensityOfGlobal(activity);
}
}
}
  • 默认适配方案中,如果在页面中什么都不做,适配过程通过最后一条语句 AutoSize.autoConvertDensityOfGlobal(activity) 实现,AutoSizeConfig是单例的,对象中的所有配置都是全局的,默认是宽度适配。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 使用 AndroidAutoSize 初始化时设置的默认适配参数进行适配 (AndroidManifest 的 Meta 属性)
    *
    * @param activity {@link Activity}
    */
    static void autoConvertDensityOfGlobal(Activity activity) {
    int sizeInDp;
    if (AutoSizeConfig.getInstance().isBaseOnWidth()) {
    sizeInDp = AutoSizeConfig.getInstance().getDesignWidthInDp();
    } else {
    sizeInDp = AutoSizeConfig.getInstance().getDesignHeightInDp();
    }
    autoConvertDensity(activity, sizeInDp, AutoSizeConfig.getInstance().isBaseOnWidth());
    }
  • 默认适配策略中,如果某个页面不需要适配只需要实现CancelAdapt接口就可以;

  • 默认适配策略中,如果某个页面需要做高度适配,需要实现CustomAdapt接口,并指定标准高度值;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * 使用 {@link Activity} 或 Fragment 的自定义参数进行适配
    *
    * @param activity {@link Activity}
    * @param customAdapt {@link Activity} 或 Fragment 需实现 {@link CustomAdapt}
    */
    static void autoConvertDensityOfCustomAdapt(Activity activity, CustomAdapt customAdapt) {
    Preconditions.checkNotNull(customAdapt, "customAdapt == null");
    float sizeInDp = customAdapt.getSizeInDp();

    //如果 CustomAdapt#getSizeInDp() 返回 0, 则使用在 AndroidManifest 上填写的设计图尺寸
    if (sizeInDp <= 0) {
    if (customAdapt.isBaseOnWidth()) {
    sizeInDp = AutoSizeConfig.getInstance().getDesignWidthInDp();
    } else {
    sizeInDp = AutoSizeConfig.getInstance().getDesignHeightInDp();
    }
    }
    autoConvertDensity(activity, sizeInDp, customAdapt.isBaseOnWidth());
    }

其他适配设置

单次适配设置

从上面的内容可以看到,对整个页面的适配是在页面绘制之前通过调用 AutoSize.autoConvertDensity(acivity, sizeInDp, isBaseOnWidth) 实现修改系统参数。那么,如果我们需要对某个页面的部分元素进行特殊的尺寸适配,那么也可以通过直接调用这个方法。

在默认策略情况下,这也是对实现 CustomAdapt 的一种替代方案,实现CancelAdapt接口然后直接调用这个方法进行适配。

热插拔

AndroidAutoSize屏幕适配方案可以支持随时关闭和开启, AutoSizeConfig.getInstance()可以调用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 重新开始框架的运行
* 框架具有 热插拔 特性, 支持在项目运行中动态停止和重新启动适配功能
*/
public void restart() {
Preconditions.checkNotNull(mActivityLifecycleCallbacks, "Please call the AutoSizeConfig#init() first");
synchronized (AutoSizeConfig.class) {
if (isStop) {
mApplication.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
isStop = false;
}
}
}

/**
* 停止框架的运行
* 框架具有 热插拔 特性, 支持在项目运行中动态停止和重新启动适配功能
*/
public void stop(Activity activity) {
Preconditions.checkNotNull(mActivityLifecycleCallbacks, "Please call the AutoSizeConfig#init() first");
synchronized (AutoSizeConfig.class) {
if (!isStop) {
mApplication.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
AutoSize.cancelAdapt(activity);
isStop = true;
}
}
}

填坑指南

  • 具体内容参考:JessYan: 今日头条屏幕适配方案常见问题汇总,提问前必看AutoSizeCompat
  • 在屏幕适配时,所有View宽度或高度之和要考虑和design_width_in_dp/design_height_in_dp的关系。
  • 热插拔是开启和关闭页面生命周期回调中的适配逻辑,对 单次适配设置 的调用是无效的,建议使用时进行封装处理。
  • 接口 CancelAdaptCustomAdapt 的使用是在 DefaultAutoAdaptStrategy 中定义的,如果设置了自定义的 AutoAdaptStrategy 要注意这两个接口的实现处理。

友情提示

当页面中有弹出框或者请求网络API后变更的UI时,AutoSizeCompat是非常有用的解决方法。

参考文章

欢迎关注我的其它发布渠道