Android黑科技Android 新技术学习

剖析Android屏幕适配及各方案

2018-08-29  本文已影响153人  小白兔兽型大发

  最近陆陆续续被一些android屏幕适配的文章刷屏了,我发现有些问题在看别人写的文章时候还是比较不容易理解的,在这里我们把这些东西捋一捋,来讲讲android屏幕适配的原理,还有之前用过的屏幕适配框架,以及目前比较火的适配框架,欢迎探讨...

为什么要适配?


  目前,很多厂商都推出了自己的全面屏手机,例如小米mix系列,蓝绿兄弟的 find-x,nex等等,不仅用了很多新的技术,而且体验方面也是流畅到没朋友(吃鸡必备,视角广),遥想当年用htc卡的不行也是美滋滋,在惊叹科技进步的同时,也不由感. 慨. 万.千.啊!

  好了我们言归正传..

  安卓屏幕适配从很久以前到现在一直是开发从业人员比较头疼的问题,源于google亲爸爸对他的定位--开源,所以任何厂商都可以对这个系统进行定制及修改,这就导致了国内各个型号分辨率,各种尺寸手机层出不穷,碎片化非常严重,再到现在全面屏即将成为街机,除了以往16比9 的手机之外,又有了18比9等其他屏占比手机,所以我们在开发的过程当中,需要进行屏幕上的适配调整.

image

   对于这张图,很多人都不陌生,这是2014年表示安卓手机碎片化的一张图,仅仅是2014年的,更别说4年后的今天了 (没找到2018年的图),所以,对于安卓这个打败了塞班划时代的系统,让我们又爱又恨.一方面我们可以在不重复造轮子的情况下,在巨人的肩膀上登高望远,一方面又受困于整个市场没有一个统一的规范,所以这张图很清晰的告诉大家这就是我们所要面临的适配问题

一些概念


  碎片虽然多,但也不是一个一个进行适配的,我们只需找出碎片中的共性,然后开发一些通用的手法,让每一个机型都达到我们的要求,当然,我们要进行屏幕适配,有几个概念要了解一下

Tips:以上面两个概念举个例子,看张图


QQ20180827-170339@2x.png

我们可以看到相同分辨率的手机,尺寸不一样,密度肯定也不一样,但是dpi同为420,说明ppi在一个区间之内 ,统一给到了dpi为420,这样会方便我们适配,也就是说我们写在项目中的dp,在两台分辨率一致,尺寸不一致的手机上,表现是一致的.

小结:通常情况下我们用写dp这个单位去做适配,确实可以解决大部分问题,那如果有一台手机分辨率为1080x1920,dpi为440,并不是480,那么相同的dp所求出来的px,440就比480小(上面俩公式),如果你的控件写的是dp,那么肯定会一个长,一个短,所以接下来的几种方案都是为了解决这个问题而做的

各种适配方案


1.宽高限定符适配

  当我们平常用as去开发的时候,在资源文件res下面,有一个vaules文件夹,我们写的各种宽度跟高度,通常会写在这个文件夹中的一个叫dimens.xml文件中,然后在布局中引用,当然,默认是所有分辨率的手机都会引用这个文件夹,限定符适配方案就是在这里,新建市面上所有分辨率的的vaules文件夹,那么不同分辨率的手机会寻找它所在分辨率所在的文件夹中的dimens文件,并引用,例如:

├── src/main  
│   ├── res  
│   ├── ├──values  
│   ├── ├──values-800x480  
│   ├── ├──values-860x540  
│   ├── ├──values-1024x600  
│   ├── ├──values-1024x768  
│   ├── ├──...  
│   ├── ├──values-2560x1440

通常情况下,我们会将1dp=1px的480x320分辨率的文件夹作为基准,那么我们所定义的vaules-480x320中的dimens文件肯定以1px为单位逐次递增:(以宽度举例)

    <dimen name="x1">1px</dimen>
    <dimen name="x2">2px</dimen>
    <dimen name="x3">3px</dimen>
    <dimen name="x4">4px</dimen>
    <dimen name="x5">5px</dimen>
    ...

那么如果是800x480分辨率的dimens 就应该为(宽度)
480(现在的)/320(基准的)=1.5 那么对应的dimens为:

    <dimen name="x1">1.5px</dimen>
    <dimen name="x2">3px</dimen>
    <dimen name="x3">4.5px</dimen>
    <dimen name="x4">6px</dimen>
    <dimen name="x5">7.5px</dimen>
    ...

同理:如果是1920x1080

    <dimen name="x1">3px</dimen>
    <dimen name="x2">6px</dimen>
    <dimen name="x3">9px</dimen>
    <dimen name="x4">12px</dimen>
    <dimen name="x5">15px</dimen>
    ...

好,那如果设计图是按照1080x1920设计的,UI小姐姐在其中一张图上标了一个100x100的按钮,那么我们只需要在这个1080x1920分辨率的dimens文件中找到100的值,并引用就可以了.

2.smallestWidth适配
  顾名思义,最小宽度适配,也属于限定符适配的一种,只不过是按照手机宽,高的最小值为基准进行适配,不同的是,我们需要在项目中新建这个样子的文件夹:

├── src/main  
│   ├── res  
│   ├── ├──values  
│   ├── ├──values-sw320dp  
│   ├── ├──values-sw360dp 
│   ├── ├──values-sw400dp  
│   ├── ├──values-sw410dp  
│   ├── ├──...  

文件夹中的数字320dp,360dp等就是我们所说的屏幕宽高最小值所代表的dp值,比如宽度为1080px的手机,dpi为480,那么根据第前面两个公式
  density=480/160=3
  dp=1080/3=360dp
那么宽度为360dp的手机都会从sw360dp这个文件中去读取dimens数值,其他同理;那每一个文件夹中的dimens文件应该怎么写呢?通过文件夹名称我们就会发现,我们是用dp去做单位的,它主要进行了两个步骤
1.将我们的手机屏幕宽度根据dpi转换成了dp
2.然后计算设计图宽度每一像素占多少dp
举个例子:
假如我们设计图宽度为750px(ios通用设计宽度),如果屏幕是360dp,1px占多少dp怎么算?
0.48=360/750

<resources>
<dimen name="base_dpi">360dp</dimen>
<dimen name="qb_px_0">0.00dp</dimen>
<dimen name="qb_px_1">0.48dp</dimen>
<dimen name="qb_px_2">0.96dp</dimen>
<dimen name="qb_px_3">1.44dp</dimen>
<dimen name="qb_px_4">1.92dp</dimen>
...
<resources>

这样我们算出的dp跟设计图所标注的px完全是一个比例,也就是说设计图写的是多少像素,我们在项目里直接@dimen/qb_px_像素值
简单到没朋友...
当然生成这些文件也是有工具的,工具戳这里,用ide直接import就可以
使用:

├── DimenTypes.class 
 //适配Android 3.2以上   大部分手机的sw值集中在  300-460之间
     DP_sw__300(300),  // values-sw300
     DP_sw__310(310),
     DP_sw__320(320),
     DP_sw__360(360);
    // 想生成多少自己以此类推
1.换成自己要用的文件夹
├── DimenGenerator.class 
1.首先根据设计图尺寸修改
/**
     * 设计稿尺寸(将自己设计师的设计稿的宽度填入)
     */
    private static final int DESIGN_WIDTH = 750;

    /**
     * 设计稿的高度  (将自己设计师的设计稿的高度填入)
     */
    private static final int DESIGN_HEIGHT = 2150;
2.修改输出路径,执行main方法
public static void main(String[] args) {
        int smallest = DESIGN_WIDTH>DESIGN_HEIGHT? DESIGN_HEIGHT:DESIGN_WIDTH;  //     求得最小宽度
        DimenTypes[] values = DimenTypes.values();
        for (DimenTypes value : values) {
            MakeUtils.makeAll(smallest, value, "/Users/gaox/Desktop/smallestwidth/dimens_sw");
        }

    }

3.今日头条适配方案

  通过上面两个方案,很明显我们可以发现,适配的核心,就是把设计图中所标注的px,根据手机dpi不同,分别换算成相应的px(宽高限定符),和相应的dp值(smallestwidth),也就是说我们写在layout中的dp(px),会根据手机dpi不同而改变,那假如现在有一个控件,我就想在布局里写死,我心情不好,就想这么写:

             <Button
                  android:layout_width="100dp"
                  android:layout_height="100dp"
                  android:text="任性"
                  android:textColor="@color/black"/>

那么能不能有办法进行适配一下呢?那我们就要说一下今日头条团队的适配方案了,他们把我们的适配思路转换了一下

  之前我们说过手机的dpi是固定的,那么根据公式density=dpi/160我们的density是可以算出的,你也可以理解为固定的,我们平常写在布局中的dp也可以根据公式dp=px/density算出来,button中的100dp也许你就是这么算的,但是注意,如果我们想要用头条的方案去适配:

 density=dpi/160这个公式要替换成
 density=当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp)

也就是说这个density不是通过dpi计算出来的,而是我们根据设计图自己算的,那么算出这个density的最终目的就是告诉你1dp等于多少px,没错,跟smallestWidth刚好完全相反!

   这里不得不说android的一个机制,不管我们写在布局中的单位是px,dp,sp等等什么也好最终都会转换为px,原因在这里:

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;
        case COMPLEX_UNIT_SP:
            return value * metrics.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;
    }

我们可以发现,写在项目中的dp会乘以density最终转换成px,在不动任何代码的情况下,可以在app初始化的时候,比如说application中获取屏幕的宽度,计算出density,然后修改系统的density为你算的就可以了,方法如下(解决了字体显示不正确等issues):

private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;

private static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application) {
        final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

        if (sNoncompatDensity == 0) {
            sNoncompatDensity = appDisplayMetrics.density;
            sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration newConfig) {
                    if (newConfig != null && newConfig.fontScale > 0) {
                        sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {

                }
            });
        }

        final float targetDensity = appDisplayMetrics.widthPixels / 360;
        final float targetScaledDensity=targetDensity*(sNoncompatScaledDensity/sNoncompatDensity);
        final int targetDensityDpi = (int) (160 * targetDensity);

        appDisplayMetrics.density=targetDensity;
        appDisplayMetrics.scaledDensity = targetScaledDensity;
        appDisplayMetrics.densityDpi = targetDensityDpi;

        final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        activityDisplayMetrics.density =  targetDensity;
        activityDisplayMetrics.scaledDensity=targetScaledDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;
    }

有两个问题需要特别说明一下:
①以上举例是以宽度进行density计算的,却不是一定要以宽度算,我们要根据页面的实际情况选取是按照宽度计算还是按照高度计算,通常一个不支持上下滑动的页面,内容又比较短,我们便要在高度上让其维持在一定比例显示,所以要按高度算,只是把上面方法,宽度改成高度就行了
②不仅仅能在application中进行全局修改,该方案也支持在每一个activity中修改,这样会更加灵活,哪怕每一个页面的设计尺寸都不一样,都能进行合理适配,最关键的是几乎没有工作量

①宽1080px,设计图为360dp,控件宽:100dp
0.2777=1080/360*100/1080
②宽720px,设计图为360dp,空间宽:100dp
0.2777=720/360*100/720

这套方案,我个人是比较推荐的,不管是新项目,还是老项目,只要你想用,代码几乎不用任何修改,全是系统的api,侵入性非常之低,使用上又非常灵活,如果哪天在使用过程中觉得哪里有问题,更换其他更好的适配方案又非常安全,删掉方法就行了,不过我们修改density,修改的是本项目尺寸按照设计图来计算,一些三方库并不是按照我们的这个设计图尺寸设计的,这样难免会有问题,比如一些dialog,toast,popwindow等库显示不正确,那么我们便需要调整,我们可以调整当前activity不用,或者调整成跟三方库一致的尺寸,也有大佬针对这些问题专门做了调整,传送门在这:大佬的头条适配究极方案,使用起来更简单.

上一篇 下一篇

猜你喜欢

热点阅读