每日一学——Android屏幕适配方案分析以及头条适配原理—基于头条适配方案的框架AndroidAutoSize (一)

荒废了几天继续我的每日一篇学习博客 go go go!

介绍下目前(我认为)主流的适配方案

前言:
首先,我们应该明白没有完美的适配方案,因为安卓设备真的是五花八门,宽高比差距太大,在保证页面View不变形的情况下,只能取宽度为基准,自适应的设置高度,这样造成的页面整体高度会超出屏幕高度,解决的方法就只能是设置页面滚动,避免页面内容显示不全;其次要是以高度为基准,很大情况下view会变形,比如图片什么的会被拉伸变形。

  1. 头条的适配方案优化版——AndroidAutoSize
  2. SmallestWidth 限定符适配方案
  3. 约束布局——ConstraintLayout;

以上是目前普遍使用的适配方案,还有一些适配方案也可以了解下,指不定你就接手了一个老项目,里面就是这些方案之一,了解一下不吃亏,不过一定程度上这些方案存在缺点和不足,所以慢慢的淡出实际开发,但是看你的需求还是有一定的可取之处。比如

  1. 对适配要求不是很高的:dp直接适配:dp是google推荐的适配UI尺寸单位,通过控件的自适应和权重可以基本解决大部分手机的适配,成本最低,但是对于一些复杂Ui和dpi差距很大的手机型号,这套适配方案就存在很大的不足了。
  2. dimens适配(称呼很多,暂时我就称呼为dimens适配吧):实际就是把你需要适配的所有手机的宽高像素列举出来,设定一个基准的分辨率值,计算出不同宽高手机的dimens值,不同宽高比去对应自身对应的dimens文件,精准高效的适配。但是这存在一个很大的缺点就是,目前国内安卓厂商五花八门,尺寸根本没有一个限定,很难穷举所有设备的分辨率。
  3. 鸿洋大神的AndroidAutoLayout 适配框架:这套适配在三五年前还是热门的,在手机宽高差距不那么天差地别,宽高比差距不是很明显的时代,这套适配能完美的通过规定的宽高比来同比缩放控件的大小,能达到完美复原设计UI图的效果。但是现在的手机这就明显不适用了,图片等会出现明显的拉伸变形。

头条方案适配的原理

这里准备白嫖下大佬的,自己也是跟着大佬学习了这个适配方案。
先上链接:https://juejin.im/post/5b7a29736fb9a019d53e7ee2
还得明白,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配。
此适配方案依赖density的计算,换算公式:

设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density

意思就是UI设计图标注的1dp占当前设备的多少像素。
借鉴下别人的描述:

densityDpi:设备的DPI,也就是上面那些常量值,是由手机厂商设置的。
density:缩放系数,这个下面会详细讲解。
scaledDensity:字体缩放系数,和density一样,不过可以受用户设置的字体大小的偏好进行

1、densityDpi:
dpi的计算:

dpi=(√(横向分辨率^2+纵向分辨率^2))/屏幕尺寸

emmmm,虽然我是学数学的,但是我还是想说这玩意看着就不是一眼出答案那种,还是需要计算一波

public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

public static int DENSITY_DEVICE = getDeviceDensity();

private static int getDeviceDensity() {
    return SystemProperties.getInt("qemu.sf.lcd_density",
            SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
}

代码可以看出来,densityDpi是通过系统自己的属性取值,如果qemu.sf.lcd_density 的值取不到,就取 ro.sf.lcd_density的,还取不到,就使用默认值,也就是 160 了。一般都是手机厂商设置好的,我们可以直接获取到设备的dpi

2、density

设备DPI除以(设计图)基准DPI,就可以得到缩放系数,跟上面“设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density”的计算是一样的

例如:你手机是640320分辨率的,你设计图自定义或者设计的尺寸就是640320的,那么在这个手机设备上,density就是1,相当于设计图尺寸在手机设备上的缩放系数是1;

3、scaledensity
与 density 一样的,也是缩放系数,默认情况和 density 的值是相等的。

scaledDensity = density;

下面开始说头条方案如何适配的:

Android中在布局文件中填写的是什么单位,最后都会被转化为 px,所以看看下面的typevalue的源码:

public static float applyDimension(int unit, float value, DisplayMetrics metrics) {
    switch (unit) {
    case COMPLEX_UNIT_PX:
        return value;// 像素直接返回
    case COMPLEX_UNIT_DIP:
        return value * metrics.density;// dp乘以density缩放系数
    case COMPLEX_UNIT_SP:
        return value * metrics.scaledDensity;// sp乘以scaledDensity缩放系数
    case COMPLEX_UNIT_PT:
        return value * metrics.xdpi * (1.0f/72);
    case COMPLEX_UNIT_IN:
        return value * metrics.xdpi;
    case COMPLEX_UNIT_MM:
        return value * metrics.xdpi * (1.0f/25.4f);
    }
    return 0;
}

是不是有点意思了,density、densityDpi、scaledensity这是属性我们可以修改,那么我们就可以设定一个基准值,确定设备的缩放系数,那不就基本按设计稿的UI适配了嘛

一般蓝湖上我都将设计调整为640*320dp的,那么通过这个尺寸你可以设定一个基准宽度,得到设备的缩放系数()

float newDensity = dm.widthPixels / 320;

直接看 DisplayMetrics 里面的属性。

/**
 * The absolute width of the available display size in pixels.
 */
public int widthPixels;
/**
 * The absolute height of the available display size in pixels.
 */
public int heightPixels;

得到newDensity,同样也要获取newScaledDensity得值,才能对字体进行适配

float newScaledDensity =  newDensity * (dm.density / dm.scaledDensity);

计算densityDpi

density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;

逆推计算:

int newDensityDpi =  (int) (targetDensity * DisplayMetrics.DENSITY_DEFAULT);

最后设置DisplayMetrics参数:

dm.density = newDensity;
dm.scaledDensity = newScaleDensity;
dm.densityDpi = newDensityDpi;

验证一下可行性:

假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致

验证设备 1

屏幕总宽度为 1080 px,根据今日头条的的公式求出 density,1080 / 375 = 2.88 (density)*
这个 50dp * 50dp 的 View,系统最后会将高宽都换算成 px,50dp * 2.88 = 144 px (根据公式 dp * density = px)
144 / 1080 = 0.133,View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放

某些设备总宽度为 1080 px,但是 DPI 可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 DPI 求出 density,是根据自己的公式求出的 density,所以这对今日头条的方案没有影响
上面只能确定在所有屏幕总宽度为 1080 px 的设备上能完成等比例适配,那我们再来试试其他分辨率的设备

验证设备 2

屏幕总宽度为 1440 px,根据今日头条的的公式求出 density,1440 / 375 = 3.84 (density)
这个 50dp * 50dp 的 View,系统最后会将高宽都换算成 px,50dp * 3.84 = 192 px (根据公式 dp * density = px)
192 / 1440 = 0.133,View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放
两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 0.133

以上是头条方案适配的原理,借鉴了大佬的不少东西,学习了不少。

以上头条的适配方案,总结其优缺点如下:

优点:

  1. 使用成本低,操作极其简单;
  2. 完全解耦,跟布局代码完全脱离,不影响你使用其他适配方案;
  3. 全局性——可适配三方库的控件和系统的控件(不止是是 Activity 和 Fragment,Dialog、Toast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益

缺点:

  1. 项目布局种使用的px都会作为dp进行换算;这点其实可以不用考虑,目前应该没有人直接在布局里面用px单位吧
  2. 全局性的局限性,因为这个适配是全局的,当然也就存在引入、依赖的第三方也会基于这套适配,可能会出现跟之前得适配冲突,造成翻车,这点就只能在你使用得地方activity、fragment或者···取消或者修改头条得适配了;

下面就得介绍AndroidAutoSize

首先附上大佬的github地址:https://github.com/JessYanCoding/AndroidAutoSize
再附上大佬的博客吧:https://juejin.im/post/5bce688e6fb9a05cf715d1c2

贴上整体结构:

├── external
│   ├── ExternalAdaptInfo.java
│   ├── ExternalAdaptManager.java
│── internal
│   ├── CancelAdapt.java
│   ├── CustomAdapt.java
│── unit
│   ├── Subunits.java
│   ├── UnitsManager.java
│── utils
│   ├── AutoSizeUtils.java
│   ├── LogUtils.java
│   ├── Preconditions.java
│   ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java

介绍下:

AndroidAutoSize 在使用上非常简单,只需要填写设计图尺寸这一步即可接入项目,但需要注意的是,AndroidAutoSize 有两种类型的布局单位可以选择,一个是 主单位 (dp、sp),一个是 副单位 (pt、in、mm),两种单位面向的应用场景都有不同,也都有各自的优缺点

附上dp主单位的小demo:https://github.com/JessYanCoding/AndroidAutoSize/tree/master/demo

使用(我这里简单介绍下dp和sp单位的适配):

1.先引入依赖:

implementation 'me.jessyan:autosize:1.1.2'
  1. AndroidManifest.xml中设置:(默认以宽度为维度适配,高度备选)
    切记:如果没有取消默认的宽度适配,你只在AndroidManifest配置了高度,是不能适配的,还会报错的。
<manifest>
    <application>            
        <meta-data
            android:name="design_width_in_dp"
            android:value="360"/>
        <meta-data
            android:name="design_height_in_dp"
            android:value="640"/>           
     </application>           
</manifest>

  1. 如果不选用默认的宽度适配,就需要取消默认的宽度适配了
    简单实现下全局高度适配吧
    我这里直接在application里面设置了:
  @Override
    public void onCreate() {
        super.onCreate();
        application = this;
        context = getApplicationContext();
        //默认使用宽度适配
        AutoSizeConfig.getInstance().setBaseOnWidth(false);
    }

如果你只是单个页面适配的话,你就往下看重写下方法取消就完事了,这个框架的使用基本就这三点就能ok了,轻松上手!!

如果只是单纯的适配,不涉及一些第三方适配的修改,上面的操作就够了,如果需要修改自定义就得看看大佬的描述了:https://github.com/JessYanCoding/AndroidAutoSize/blob/master/demo/src/main/java/me/jessyan/autosize/demo/BaseApplication.java#L55

自定义Activity(当跟全局配置的Androidmanifest里面的不一样时,需要我们单独适配):

public class CustomAdaptActivity extends AppCompatActivity implements CustomAdapt {

	 /**
     * 是否按照宽度进行等比例适配 (为了保证在高宽比不同的屏幕上也能正常适配, 所以只能在宽度和高度之中选择一个作为基准进行适配)
     *
     * @return {@code true} 为按照宽度进行适配, {@code false} 为按照高度进行适配
     */
    @Override
    public boolean isBaseOnWidth() {
        return false;
    }

	 /**
     * 这里使用 iPhone 的设计图, iPhone 的设计图尺寸为 750px * 1334px, 高换算成 dp 为 667 (1334px / 2 = 667dp)
     * <p>
     * 返回设计图上的设计尺寸, 单位 dp
     * {@link #getSizeInDp} 须配合 {@link #isBaseOnWidth()} 使用, 规则如下:
     * 如果 {@link #isBaseOnWidth()} 返回 {@code true}, {@link #getSizeInDp} 则应该返回设计图的总宽度
     * 如果 {@link #isBaseOnWidth()} 返回 {@code false}, {@link #getSizeInDp} 则应该返回设计图的总高度
     * 如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸, {@link #getSizeInDp} 则返回 {@code 0}
     *
     * @return 设计图上的设计尺寸, 单位 dp
     */
    @Override
    public float getSizeInDp() {
        return 667;
    }
}

取消当前activity的全局适配,实现CancelAdapt 这个接口类

public class CancelAdaptActivity extends AppCompatActivity implements CancelAdapt {

}

自定义frgment:

frgment的自定义适配跟activity是一样,只是需要在application的初始化时候设置下对Fragment的支持:

AutoSizeConfig.getInstance().setCustomFragment(true);

实现CustomAdapt:

public class CustomAdaptFragment extends Fragment implements CustomAdapt {

    @Override
    public boolean isBaseOnWidth() {
        return false;
    }

    @Override
    public float getSizeInDp() {
        return 667;
    }
}

取消frgmrent的适配,只需要实现CancelAdapt接口就行

public class CancelAdaptFragment extends Fragment implements CancelAdapt {

}

适配第三方库页面:

直接引用大佬的博客介绍吧:

在使用主单位时可以使用 ExternalAdaptManager 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (Activity、Fragment)
由于 AndroidAutoSize 要求需要自定义适配参数或取消适配的页面必须实现 CustomAdapt、CancelAdapt,这时问题就来了,三方库是通过远程依赖的,我们无法修改它的源码,这时我们怎么让三方库的页面也能实现自定义适配参数或取消适配呢?别急,这个需求 AndroidAutoSize 也已经为你考虑好了,当然不会让你将三方库下载到本地然后改源码!


通过 ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo) 将需要自定义的类和自定义适配参数添加进方法即可替代实现 CustomAdapt 的方式,这里 展示了使用方式,以及详细的注释


通过 ExternalAdaptManager#addCancelAdaptOfActivity(Class) 将需要取消适配的类添加进方法即可替代实现 CancelAdapt 的方式,这里 也展示了使用方式,以及详细的注释


需要注意的是 ExternalAdaptManager 的方法虽然可以添加任何类,但是只能支持 Activity、Fragment,并且 ExternalAdaptManager 是支持链式调用的,以便于持续添加多个页面
当然 ExternalAdaptManager 不仅可以对三方库的页面使用,也可以让自己项目中的 Activity、Fragment 不用实现 CustomAdapt、CancelAdapt 即可达到自定义适配参数和取消适配的功能

总结:

整个博客基本就是我自己理接以及整理大佬博客的学习记录,希望能帮助到你,点个赞吧老铁。

下一篇:每日一学——Android适配——SmallestWidth 限定符适配方案(二)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值