Android屏幕适配总结终极方案

屏幕适配

屏幕适配的概念

碎片化既是 Android 的优势和弱点,也是开发者们头疼的问题,同时也为 Android 的全球影响力提供了基础。Android 设备的外形和尺寸各不相同,性能水平和屏幕尺寸也都大不相同。此外,有许多不同版本的 Android 在任何时候都同时处于活动状态,增加了另一层的碎片化。这意味着,开发各种Android设备的应用程序将是极具挑战性和耗时的。

今年,我们看到了之前报告中出现的趋势的延续;更多的设备和更多的设备之间的区别。今年我们注意到的一件大事是品牌的碎片化,今年有超过1000个品牌被发现,我们在2012年第一次发布这个报告时没有看到。三星仍然是市场领头羊的,在全球安卓设备市场的份额从43%降至37.8%(尽管市场仍在继续增长)。从消费者的角度来看,Android 生态系统的强大之处一直是能够挑选出一款适合你的规格的设备,因为有那么多的设备可以选择。这种趋势只会持续下去,今年的Android设备和品牌比以往任何时候都多。

从以上两段话能看出:

  • Android碎片化不能避免
  • Android碎片化只会越来越严重

资料来源:opensignal.com

屏幕适配的重要性

作为一名 Android 开发者,有一个逾越不过的问题,就是屏幕适配。谈屏幕适配其实是一件非常让人蛋疼的事情,究其原因就是Android 的开放性。由于 Google 这种搞一个东西就开放源代码的“习惯”造成了 Android 系统的开放性,以至于随随便便一个厂商就可以随心所欲定制自家系统,任意修改成他们想要的样子。这使整个 Android 的碎片化非常严重。

严重到什么程度呢?下面给出几张图,大家感受一下。

1、设备的平台

Android 碎片化.png

这是对过去几个月下载OpenSignal应用程序的Android设备进行可视化的最佳方式。这张图表显示了Android开发者面临的挑战;超过24000个不同的设备可以立即使用他们的应用,这使得优化成为一个真正的挑战。在两年内,Android的碎片化比我们在2013年看到的11868年翻了一倍多。

2、品牌的碎片化

Android手机厂商占比.png

与第一个图表类似,这表明市场在制造商方面存在分歧,三星再次证明了自己的主导地位。2012年,我们看到三星拥有47.5%的市场份额,而这一差距可以通过竞争对手的崛起来解释,上面的图表显示了1294个不同的品牌。

3、Android 操作系统碎片

Android 操作系统的碎片化.png

设备碎片化并不是开发者在为 Android 开发时面临的唯一挑战;操作系统本身非常分散。然而,在过去的一年里,我们看到了碎片化的轻微减少,在过去的12个月里,占主导地位的API版本(在这个例子中是KitKat)的市场份额有所上升。

4、Android与iOS系统版本分布比对


Android系统与iOS系统不同版本占比.png

与iOS相比,所有类型的安卓系统都有不同的表现。这两个饼图清楚地显示了两个竞争操作系统之间的API碎片的差异。

5、屏幕大小碎片

Android

Android屏幕分辨率分布.png

任何应用程序成功的关键都是让UI正确,而Android在这方面给开发者带来了两个特别的挑战。首先,品牌倾向于在系统UI上产生自己的变体(三星的Touchwhizz和HTC的感觉是两个这样的例子),这可以改变各种默认元素的外观。其次,没有其他的智能手机/平板电脑平台拥有如此大的屏幕尺寸。今年的一个重大变化是包含了巨大的Slate 21——这绝对让我们今年看到的所有其他Android设备相形见绌(以及所有的iOS设备)。

要想知道在过去的几年里,平均屏幕尺寸(以及CPU、RAM和NFC流行率)发生了怎样的变化,请参阅本文附带的博客文章

iOS


iOS屏幕分辨率分布.png

设计和编码布局在所有这些屏幕上都能很好地工作,对于任何开发人员来说都是极具挑战性的。下图中所示的iOS生态系统与Android有很好的对比,因为在相当小的维度上设计要容易得多。

6、品牌扩散

Android手机厂商年度数量.png

这张图表显示了过去几年设备品牌数量的巨大增长,有1294个不同的品牌对我们调查的682000台设备负责。自2012年以来,我们的样品中所看到的品牌数量几乎达到了六倍。

7、Android 全球市场分布

Android手机品牌全球分布情况.png

这张地图显示了我们拥有重要数据的每个国家的领先制造商,并基于2015年下载OpenSignal应用的所有设备。正如预期的那样,三星是世界上许多国家的主要制造商,在墨西哥、印度和中国也有明显的例外。

调查数据只截止到2015年:

  • 2012年,支持Android的设备共有3997种。
  • 2013年,支持Android的设备共有11868种。
  • 2014年,支持Android的设备共有18796种。

虽然数据只显示到2015年,但是这些数据已经足够表明屏幕适配的重要性了。

针对这么多的手机,我们不可能做到面面俱到,当今市场上最流行哪些呢?根据我在 screensiz.es 上看到的数据(截止到2017年7月19日),当前全球手机屏幕尺寸、操作系统、品牌以及市场占有率的调查数据如下:

screenSize.es.png

根据【友盟+】2016年手机生态发展报告H1 数据来看:

国产手机品牌比例分布

国产手机品牌占比.png

国内Android设备屏幕分辨率分布

Android设备分辨率占比.png

国内Android手机系统版本分布

Android手机系统国内占比.png

现在应该很清楚为什么我们要对 Android 的屏幕进行适配了吧?这么多的屏幕尺寸,怎么能保证我们自己开发的程序能美观的显示到不同尺寸、不同分辨率的设备上呢?显而易见,这肯定需要我们自己去处理,而如何处理这些问题,这就涉及到屏幕适配,也就是今天要讲的。

从上图可以看出,主流的分辨率是前六种:1280×720、1920×1080、854×480、960×540、800×480、1184×720。而真实开发中,我们要做的事就是适配当前市场上绝大多数的 Android 屏幕就可以了。

知道了屏幕适配的重要性之后,接下来开始进入正题。

必须要了解的几个概念

  • 屏幕尺寸、屏幕分辨率、屏幕像素密度
  • 屏幕尺寸:屏幕对角线长度,单位是英寸,我们常说的多少多少寸,比如4.7存手机、5.7存手机,指的就是这个。
  • 屏幕分辨率:如 1920×1080,是指在手机屏幕的像素点的个数,单位是px,1px = 1 像素点,一般是纵向像素 × 横向像素,意味着高有 1920 个像素点,宽有 1080 个像素点。
  • 屏幕像素密度:是指每英寸上的像素点数,单位是 dpi(dotper inch)。像素密度和屏幕尺寸和屏幕分辨率有关,它是由对角线的像素点数除以屏幕的大小得到的,关系如下:
屏幕像素密度算法.png

单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。

  • dp、dip、dpi、sp、px
  • dp:是Android 特有的,意为密度无关像素,Google 发布的 BASELINE(基准线)为 160,以此为基准。
  • dip:Density Independent Pixels,同dp一个意思,目前废弃了,一般都写dp。
  • dpi:即为屏幕像素密度的单位
  • sp:Scale-IndependentPixels的缩写,可以根据文字大小首选项自动进行缩放。Google推荐我们使用12sp以上的大小,通常可以使用12sp,14sp,18sp,22sp,为避免精度损失,建议最好不要使用奇数和小数。
  • px:就是我们常说的像素
  • mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi
像素密度范围对应表.png
Google各种尺寸对应密度表.png

了解了以上这些“纸上谈兵”的概念之后,接下来进入真实解决问题的环节。但请记住,只有了解了这些概念,你才能去解决这些问题,所以不要轻视这些概念性的东西。

一般都是采用以下几种解决方案:

布局适配

这种适配方案基本上不怎么使用了,因为实在是太耗费资源,试想一下。多写一套布局,而大部分的代码都是相同的,纯粹是为了适配而做的这些事情,无疑增加了开发者的负担以及使程序变得更冗余和庞大,我个人认为,实在是得不偿失。

权重适配

这是 LinearLayout 的特有属性:weight,意味权重,我们可以让界面布局按照我们设定的比例来显示。比如,现在有个需求,要求界面上有两个控件,一个占屏幕宽的1/3,另一个占屏幕宽的2/3。

权重适配宽度为0的时候.png
  • 文本1的weight=1,宽度为0dp
  • 文本2的weight=2,宽度为0dp

而显示效果就是文本1的宽度占了1/(1+2),文本2的宽度占了1/(1+2),如上图所示,这样就很好的完成了需求。
但我希望你能明白权重的计算规则,权重的意思是控件的大小等于自身大小加上占剩余空间的比例。注意是剩余空间的比例,什么意思呢?

先看另外一种效果再来讲讲原因:

权重适配宽度为match_parent.png

与之前相比,我只改了一个属性,就是把文本1和文本2这两个TextView的宽度改成了match_parent。
然而结果却变了个样,本该显示1/3的文本1却占了2/3,而文本2却变成了1/3。

再来看一下刚才那句话,什么叫剩余空间呢?

当前的LinearLayout的orientatioin为横向,两个TextView的weight分别是1和2。
先分析第一种情况,就是当宽度为0dp的时候。
假设说屏幕的宽度为L,现在该屏幕下有两个控件,两个控件的宽度为0,那剩余空间就等于L-(0+0)=L。
文本1的weight=1,那么它的宽度:
自身宽度 + 所占剩余空间的比例 = 真正的宽度
0 + L * 1/(1+2) = 1/3 L

同理,文本2的宽度为:

自身宽度 + 所占剩余空间的比例 = 真正的宽度 0 + L * 2/(1+2) = 2/3 L

因此当宽度设置为0dp的时候,显示效果就是1:2的关系。

再来讨论当宽度设置为match_parent的时候的情况,就是当控件的宽度为 L 的时候。
现在屏幕的宽度为L,现在这两个控件的宽度都为L,那剩余空间就等于L-(L+L) = -L。
文本1的weight=1,那么它的宽度:
自身宽度 + 所占剩余空间的比例 = 真正的宽度
L + -L * 1/(1+2) = 2/3 L

同理,文本2的宽度为:
自身宽度 + 所占剩余空间的比例 = 真正的宽度 L + -L * 2/(1+2) = 1/3 L

因此当宽度设置为0dp的时候,显示效果就是2:1的关系。
这就是权重的计算规则,很多人面试都遇到过。

这是横向的只测试了宽的权重,其实高也是一样,这里不做讲解。

注意:权重只有LinearLayout才有,RelativeLayout没有这个属性,虽然 Google 推荐使用RelativeLayout而不是LinearLayout。

这里多提一句为什么Google推荐使用RelativeLayout而不是LinearLayout。

在 Android 中,系统对View进行测量、布局和绘制时,都是通过对 View树的遍历来进行操作的。如果一个View 树的高度太高,就会严重影响测量、布局和绘制的速度,因此我们如果不想被影响的话,第一个方法就是要降低View树的高度,Google也在其API文档中建议View树的高度不宜超过10层。

真实项目中,大部分的根布局几乎都是RelativeLayout,因为要实现相对位置的控制,所以比较方便一些。
而最初的时候创建一个 xml 布局,Google是用 LinearLayout作为默认根布局的,而现在已经使用 RelativeLayout 作为 xml 文件默认的根布局了,原因就是希望通过扁平的 RelativeLayout来降低通过 LinearLayout 嵌套所产生布局树的高度,从而提高 UI 渲染的效率。

代码适配

有一些情况下,我们需要去动态的设置控件的大小或者是控件的位置,比如dialog或者popupwindow的偏移量或者是显示的位置等等,这个时候在xml布局里就显得有点乏力,我们可以根据当前屏幕的大小属性来设置合适的数值。
比如

    //获取屏幕高宽
        DisplayMetrics metric = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metric);
        int windowsHeight= metric.heightPixels;
        int windowsWight= metric.widthPixels;
        //动态改变布局
        LinearLayout production_factory = (LinearLayout)findViewById(R.id.production_factory);
        ViewGroup.LayoutParams params = production_factory.getLayoutParams();
        params.height= windowsHeight / 2;
        production_factory.setLayoutParams(params);

图片适配 + 多套切图的解决办法

1、图片适配

图片适配什么意思?为什么要有图片适配? 切多套图,正确的图片放入正确的文件夹下面

首先看一张图

Google的ic_launcher.png

在默认的res文件夹下面,发现有这么多mipmap文件夹,现在看到里面只是放了一个ic_launcher图片,就是应用的启动图标,为什么要有这么多张?

点开之后发现每张图片的分辨率和所占用的大小都不一致

mipmap-mdpi           48x48            2.21k
mipmap-hdpi           72x72            3.42k
mipmap-xhdpi          96x96            4.84k
mipmap-xxhdpi        144x144           7.72k
mipmap-xxxhdpi       192x192           10.49k

这是因为每个手机的屏幕密度都不一样,当程序运行到手机上时,系统会根据当前手机所对应的屏幕密度去找相应文件夹下面的图片。比如当我们启动一个屏幕密度为mdpi的手机时,加载的其实就是分辨率48x48大小为2.21k的ic_launcher图片,其他以此类推。我们需要按照规则将不同的图片放到相对应的文件夹下面,这样做的好处就是节省内存。下面做一个实验来验证这一结论。

图片地址
这是一张1080x1920分辨率、1.4MB的图片,我们把它放到mipmap-xxhdpi的文件夹下面,接下来启动一个1920x1080分辨率的模拟器

然后xml布局里将其设置为背景,然后运行。

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@mipmap/img_welcome"/>

</FrameLayout>

打开Android Studio的内存检测,检查耗费的内存,如图

img_default_xxhdpi.png

可以看到,这个1.4MB的图片运行到这个手机上所消耗的内存是8.95MB。

接下来,咱们把这个图片移动到mipmap-xhdpi文件夹下面。运行后,监测内存情况如下:

img_default_xhdpi.png

可以看到,这个1.4MB的图片运行到这个手机上所消耗的内存是18.83MB。
继续放进mipmap-hdpi文件夹下面时,情况如下:

img_default_hdpi.png

可以看到,这个1.4MB的图片运行到这个手机上所消耗的内存是32.67MB。
继续放进mipmap-mdpi文件夹下面时,情况如下:

img_default_mdpi.png

可以看到,这个1.4MB的图片运行到这个手机上所消耗的内存是72.23MB。

为什么都是同一张图片,什么都没改,只是换了个文件夹而已,它就有这么大的区别呢?
这张图片是1.4Mb,我们启动的模拟器参数如下:

    屏幕尺寸:4.95"
    分辨率:1920*1080
    系统版本:Android6.0

然后我有用了Android5.1和Android7.1的系统分别做了测试:

三种模拟器.png

测试统计结果如下:

同一张图片不同分系统手机占用内存情况.png

根据前面所讲的屏幕像素密度计算公式可以得出该模拟器的屏幕像素密度是445,这就意味着它对应的xxhdpi,它会优先去加载此文件夹下面的图片。

因此当我们把图片放到xxhdpi文件夹下面时,系统第一优先找的是这个文件夹,这是有好处的,好处就是节省内存,从上图也可以看出,的确如此。这就是为什么我们要在对应的文件夹下面放对应分辨率图片的好处,就是占用内存小。这就是好处,至于为什么会出现这么大的差别,我们这样来分析。

还记得前面讲过,mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi的比例是2:3:4:6:8的概念吗?

当前图片1.4Mb,放到相应文件夹下面时占用内存不同

首先这张图片理应消耗的内存是8.97Mb-1.4Mb=7.6Mb (在手机里占用的内存-图片自身的大小)

得出该图片大约为8Mb,分析其他几种情况:


当放到mdpi下时,按照2:3:4:6:8的比例,mdpi跟xxhdpi的比例是2:6,我们的模拟器是xxhdpi的,在去mdpi文件夹里找图片时会自动转换,转换的比例就是2:6。也就是1:3的关系,既然如此,宽是3倍,高是3倍,自然一张图就变成了9倍。

xhdpi是4:6,也就是1.5x1.5=2.25 所以8Mb x 2.25 = 18Mb左右,差不多也跟上面一样。

再分析hdpi的情况,跟xxhdpi的比例是3:6,也就是1:2,2 x 2=4,也就是扩大了4倍。 所以8Mb x 4=32Mb,也符合上面的情况。

虽然在Android7.+系统下每个阶段差不多比Android5.+和6.+多出了4M左右的内存,但是不影响结果。

总结:由此可见,一张图片放到正确的文件夹里面是多么的重要,由此你是否明白Google为什么造了那么多套ic_launcher的图片了吧。

那我们开发的图片到底是放在mipmap目录还是放在drawable目录下呢?

官方介绍:

Mipmapping for drawables

Using a mipmap as the source for your bitmap or drawable is a simple way to provide a quality image and various image scales, which can be particularly useful if you expect your image to be scaled during an animation.

Android 4.2 (API level 17) added support for mipmaps in the Bitmap class—Android swaps the mip images in your Bitmap when you've supplied a mipmap source and have enabled setHasMipMap(). Now in Android 4.3, you can enable mipmaps for a BitmapDrawable object as well, by providing a mipmap asset and setting the android:mipMap attribute in a bitmap resource file or by calling hasMipMap().

应用场景:

If you know that you are going to draw this bitmap at less than 50% of its original size, you may be able to obtain a higher quality by turning this property on. Note that if the renderer respects this hint it might have to allocate extra memory to hold the mipmap levels for this bitmap.

一个应用实例:

Nexus 6

ScreenThe Nexus 6 boasts an impressive 5.96” Quad HD screen display at a resolution of 2560 x 1440 (493 ppi). This translates to ~ 730 x 410 dp (density independent pixels).

Check your assets

It has a quantized density of 560 dpi, which falls in between the xxhdpi and xxxhdpi primary density buckets. For the Nexus 6, the platform will scale down xxxhdpi assets, but if those aren’t available, then it will scale up xxhdpi assets.

Provide at least an xxxhdpi app icon because devices can display large app icons on the launcher. It’s best practice to place your app icons in mipmap- folders (not the drawable- folders) because they are used at resolutions different from the device’s current density. For example, an xxxhdpi app icon can be used on the launcher for an xxhdpi device.

res/
   mipmap-mdpi/
      ic_launcher.png
   mipmap-hdpi/
      ic_launcher.png
   mipmap-xhdpi/
      ic_launcher.png  
   mipmap-xxhdpi/
      ic_launcher.png
   mipmap-xxxhdpi/   
      ic_launcher.png  # App icon used on Nexus 6 device launcher

Choosing to add xxxhdpi versions for the rest of your assets will provide a sharper visual experience on the Nexus 6, but does increase apk size, so you should make an appropriate decision for your app.

res/
   drawable-mdpi/
      ic_sunny.png
   drawable-hdpi/
      ic_sunny.png
   drawable-xhdpi/   
      ic_sunny.png
   drawable-xxhdpi/  # Fall back to these if xxxhdpi versions aren’t available
      ic_sunny.png 
   drawable-xxxhdpi/ # Higher resolution assets for Nexus 6
      ic_sunny.png

总结: mipmap 只是放 ic_launcher ,其他的还是应该像以前一样放在 drawable Stack Overflow链接

但是用mipmap系统会在缩放上提供一定的性能优化。

2、 使用.9图

Android里有9patch图的概念,也就是.9图,能够自动拉伸你指定的地方,这其实是一种格式特殊的png文件,能指明可以拉伸以及不可拉伸的区域,同时还可以把显示内容区域的位置标示清楚。

create_9_patch.gif

.9图的有两条线必须画,就是左边和上边,否则会报错。
上图的红色区域就是图片会被拉伸的区域,其他地方不会发生任何变化,这个时候,一个简单的.9图就制作完成了。

9_patch_black.png

我们控制这张图片哪些区域可以缩放,缩放的区域就是两条黑边的交集,即中间位置的红框。

3、使用 Vector Asset

create_vector.gif

Android Support 25中BottomNavigationView与ViewPager结合实现material Tab标准效果

多套切图的解决办法

我们需要提供备用位图(符合屏幕尺寸的图片资源)
由于 Android 可在各种屏幕密度的设备上运行,因此我们提供的位图资源应该始终可以满足各类密度的要求:

密度类型                  代表的分辨率(px)   系统密度(dpi)
低密度(ldpi)               240x320             120
中密度(mdpi)               320x480             160
高密度(hdpi)               480x800             240
超高密度(xhdpi)            720x1280            320
超超高密度(xxhdpi)         1080x1920           480

根据以下尺寸范围针对各密度生成相应的图片。

比如说,如果我们为 xhdpi 设备生成了 200x200 px尺寸的图片,就应该按照相应比例地为 hdpi、mdpi 和 ldpi 设备分别生成 150x150、100x100 和 75x75 尺寸的图片

即一套分辨率=一套位图资源

接下来将生成的图片文件放在 res/ 下的相应子目录中(mdpi、hdpi、xhdpi、xxhdpi),系统就会根据运行您应用的设备的屏幕密度自动选择合适的图片

最后通过引用 @mipmap/id,系统都能根据相应屏幕的 屏幕密度(dpi)自动选取合适的位图。

注意:

如果是.9图或者是不需要多个分辨率的图片,放在drawable文件夹即可对应分辨率的图片要正确的放在合适的文件夹,否则会造成图片拉伸等问题。

更好地方案解决“图片资源”适配问题

上述方案是常见的一种方案,这固然是一种解决办法,但缺点很明显:

1.每套分辨率出一套图,为美工或者设计增加了许多工作量
2.对Android工程文件的apk包变的很大

那么,有没有一种方法:

保证屏幕密度适配
可以最小占用设计资源
使得apk包不变大(只使用一套分辨率的图片资源)

下面我们就来介绍这个方法:

只需选择唯一一套分辨率规格的图片资源
Google官方给出的高清设计图尺寸有两种方案,一种是以mdpi设计,然后对应放大得到更高分辨率的图片,另外一种则是以高分辨率作为设计大小,然后按照倍数对应缩小到小分辨率的图片。
推荐使用第二种方法,因为小分辨率在生成高分辨率图片的时候,会出现像素丢失。
而分辨率可以以1280*720或者是1920*1080作为主要分辨率进行设计。

首先来理解下Android 加载资源过程

Android SDK会根据屏幕密度自动选择对应的资源文件进行渲染加载(自动渲染)

比如说,SDK检测到你手机的分辨率是320x480(dpi=160),会优先到mipmap-mdpi文件夹下找对应的图片资源;

但假设你只在xhdpi文件夹下有对应的图片资源文件(mdpi文件夹是空的),那么SDK会去xhdpi文件夹找到相应的图片资源文件,然后将原有大像素的图片自动缩放成小像素的图片,于是大像素的图片照样可以在小像素分辨率的手机上正常显示。

所以理论上来说只需要提供一种分辨率规格的图片资源就可以了。

那么应该提供哪种分辨率规格呢?

如果只提供ldpi规格的图片,对于大分辨率(xdpi、xxdpi)的手机如果把图片放大就会不清晰

所以需要提供一套你需要支持的最大dpi分辨率规格的图片资源,这样即使用户的手机分辨率很小,这样图片缩小依然很清晰。

xhdpi应该是首选

原因如下:

xhdpi分辨率以内的手机需求量最旺盛

目前市面上最普遍的手机的分辨率还多集中在720x1080范围内(xhdpi),所以目前来看xhdpi规格的图片资源成为了首选。

而且很多公司为了保持App不同版本的体验交互一致,可能会以iPhone手机为基础进行设计,包括后期的切图之类的。

iPhone主流的屏幕dpi约等于320, 刚好属于xhdpi,所以选择xhdpi作为唯一一套dpi图片资源,可以让设计师不用专门为Android端切图,直接把iPhone的那一套切好的图片资源放入mipmap-xhdpi文件夹里就好,这样大大减少的设计师的工作量!

dp + dimens 适配

我们总是在xml布局里写dp,dp到底是什么意思呢?

这是Android开发中特有的一种度量,称作屏幕无关像素,它不表示任何具体的长度或者像素点,这个值只有在具体屏幕密度的手机上,才会被转换为具体的像素值。

它跟px不一样,咱们在xml里面写上px,那无论运行到任何设备上,就是固定的px,不会发生什么变化,但是dp就不一样了。

比如拿以下几种来说明一下。

分辨率(px)      系统密度(dpi)
  240x320             120
  320x480             160
  480x800             240
  720x1280            320
  1080x1920           480

1dp转换的px是多少呢?

其实就跟dpi有关,而基准线是160dpi,这就意味着1dp在320x480这款手机上就是1px,在480x800是1.5px,720x1280上是2px,1080x1920上是3px。

这就是dp。

比如现在有这么个需求,要求一个控件的宽度占屏幕的一半,我们用dp怎么来实现呢?

首先进行计算,比如320x480,它的宽是320个px,那我们就要写160px,用dp来表示就是160dp。所以我们可以在xml布局里写160dp就可以了,同样的方式试一试其他的手机:

比如240x320,你写160dp在这种手机上转换的px就是 160dp*120/160 = 120px,所以在这种手机上160dp代表的就是120px,刚好是240的一半,也能适配。
 
再来试一试480x800,想在这种手机上也占一半,必须是240px,而咱们写的是160dp,它转换的px就是 160dp*240/160=240px,刚好也能符合需求。

其实Google出dp这个东西本身就能达到适配的概念,你看,咱们写了一个160dp,跑到这几种手机上都能达到需求,这不就是适配了吗?

但是我之所以提出来了,就说明dp并不能保证完美适配,比如咱们试试720x1280。
720的一半是360px,而咱们写的160dp,转换的px是160dp*320/160=320px,明显就不是一半,差了40px。也就是说咱们的代码运行到这种手机上的时候就没有适配好,没有达到需求。

再试试1080x1920的,同样的计算,160dp*480/160=480,而1080的一半是540,480跟540差了60px,也不行,没有达到需求。

由此可见,当在240x320,320x480,480x800上面的时候写160dp是正常的,可以满足需求。而在720x1280,1080x1920上面却不能完美适配。

问题抛出来了,该怎么解决这个问题呢?

比如720p和1080p,我们通过计算,720/2=360,而根据dpi的比例320/160=2,所以我们要想再720p的手机上占宽度的一半,dp值应该是360px/2=180dp。

反过来算一下,如果我们写180dp,那在720p上转换的像素是180dpx320/160=360px,是720的一半,换到1080p上,180dpx480/160=540px,刚好是1080的一半,那也就是说,咱们写 180dp就可以实现了需求,那咱们就可以在xml布局里这么写:

<TextView
     android:layout_width="180dp"
     android:layout_height="wrap_content" />

可是,这么一写又有问题出现了,在240x320,320x480,480x800上面又坏了,这可怎么办呢?

千呼万唤始出来,dimens+dp

Android 的values文件夹下面可以创建一个dimens的文件。这个dimens的作用就跟mipmap和drawable的引用方式一样,可以通过@dimens/xxx的方式来引用。我们的思路就是创建多套dimens,然后xml布局引用dimens,当运行到不同的手机上时,系统会自动去寻找相应的dimens,去加载我们提前准备好的dp值,图文教程:

首先,系统默认的values下面就有个dimens文件,点击values下面的dimens,然后创建一个dimen,如下图所示,我在此创建了一个名为item_width 的dimen,值为160dp,他能适配 240x320,320x480,480x800 三种分辨率:

dimens_default.png

同时再打开values-1280x720文件夹下面的dimens,创建一个相同的名为 item_width,但是值为180dp,如图:

dimens_1280x720.png

同样的步骤,在values-1920x1080文件夹下面的dimens里也创建一个同样的180dp的dimen:

dimens_1920x1080.png

做好了这些之后,接下来我们需要在xml布局里引用,还记得之前的TextView的width写的多少吧?

之前写的是160dp,是一个固定值,而现在就不能这么写了,我们需要写成“@dimen/item_width”,这个什么意思呢?其实这里只是一个引用,当我们点击想看看具体是多少的值的时候,会发现这样的情况,如图:

dimen_item_width.png

这个意思就是,当前我们写的“@dimen/item_width”引用了三个地方,问我们想去查看哪一个文件夹下面的dimens。

做好了这一切之后,这个TextView的值就是一个变的,下次再运行到手机上的时候,720p手机上运行时TextView的宽度就是180dp,1080p手机上也是180dp,其他的手机就是160dp(因为我们没有创建其他分辨率的文件夹,所以会走默认的values)。
这就达到了指定机型的屏幕适配。

百分比适配 张鸿洋

虽然上述的dimens+dp能达到适配的目的,但是项目中如果要适配市场上常见的机型时,我们只能一个个的去计算,然后写上我们计算好的dp值,而且ui妹子在效果图上标记的都是px,我们还要根据这个再计算转换,很麻烦,有没有那种一劳永逸的?

参考了hongyang的博客之后,有这么一套适配方案,如下:

思路:把任何设备的手机宽度像素均分为320份,高度像素均分为480份,使用我们写好的程序自动生成资源values-***×***文件夹,里面包含lay_x.xml和lay_y.xml,分别对应宽度和高度的像素。

ay_x.xml(宽):

<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="x1">1.0px</dimen>
<dimen name="x2">2.0px</dimen>
<dimen name="x3">3.0px</dimen>
<dimen name="x4">4.0px</dimen>
...
<dimen name="x318">318.0px</dimen>
<dimen name="x319">319.0px</dimen>
<dimen name="x320">320px</dimen>
</resources>

然后lay_y.xml(高):

<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="y1">1.0px</dimen>
<dimen name="y2">2.0px</dimen>
<dimen name="y3">3.0px</dimen>
<dimen name="y4">4.0px</dimen>
...
<dimen name="y480">480px</dimen>
</resources>

我们直接在dimens里面写上具体的px值,而不是dp值,这样给定一个写死的值,在手机上运行的时候就是我们所写上的px值。可能到这里各位又有疑问了,这样的话怎么做到适配的呢?别着急,我们假设手机屏幕的宽度都是320某单位,那么我们将一个屏幕宽度的总像素数平均分成320份,每一份对应具体的像素就可以了。

我们可以创建多个values文件夹,多到覆盖市面上绝大多数流行的手机机型,并且用相应的比例计算好px值,由于现在是以320x480为基准的,所以如果我们要编写其他的values,要进行如下计算,比如我们以1920x1080为例:

由于基准是320x480,所以1080/320=3.375px,1920/480=4px,所以相应文件应该是这样的

lay_x.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="x1">3.375px</dimen>
<dimen name="x2">6.65px</dimen>
<dimen name="x3">10.125px</dimen>
...
<dimen name="x320">1080px</dimen>
</resources>

lay_y.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="y1">4px</dimen>
<dimen name="y2">8px</dimen>
<dimen name="y3">12px</dimen>
<dimen name="y4">16px</dimen>
...
<dimen name="y480">1920px</dimen>
</resources>

我们可以按照这种计算的方式补全当今市面上流行的机型。

但是我们怎么可能会去做这么蠢的事呢?肯定有工具类之类的东西!在此,再次感谢hongyang提供的工具类。

代码应该很好懂,我们将一个屏幕宽度分为320份,高度480份,然后按照实际像素对每一个单位进行复制,放在对应values-widthxheight文件夹下面的lax.xml和lay.xml里面,这样就可以统一所有你想要的分辨率的单位了,无论在什么分辨率下,x320都是代表屏幕宽度,y480都是代表屏幕高度。

当执行了上述代码之后,会在本地生成很多个values文件夹,我们把这些values拷贝到项目的res下面,如下所示:

hongyang_percent_values.png

注意:

分辨率为480x320的资源文件应放在res/values-480x320文件夹中;同理分辨率为1920x1080的资源文件应放在res/values-1920x1080文件夹中。(其中values-480x320是分辨率限定符)

必须在默认values里面也创建对应默认 lay_ x.xml 和 lay_y.xml 文件,如下

lay_x.xml

<?xml version="1.0" encoding="utf-8">
<resources>
<dimen name="x1">1.0dp</dimen>
<dimen name="x2">2.0dp</dimen>
...
</resources>

因为对于没有生成对应分辨率文件的手机,会使用默认values文件夹,如果默认values文件夹没有(即没有对应的分辨率、没有对应dimen)就会报错,从而无法进行屏幕适配。
(注意对应单位改为dp,而不同于上面的px。因为不知道机型的分辨率,所以默认分辨率文件只好默认为x1=1dp以保证尽量兼容(又回到dp老方法了),这也是这个解决方案的一个弊端)

而我们怎么用呢?

工作中只需要根据UI妹子给出的某一个分辨率设计图的尺寸,然后找到对应像素数的单位,然后设置给控件就可以了,如下:

<TextView
     android:text="@string/hello_world"
     android:layout_width="@dimen/x160"
     android:layout_height="@dimen/y160"/>

最后:本文非原创,大部分资料来源于大神的博客,这里只是作为一个归纳,希望更多的同学能够学习到屏幕适配的知识,也算是自己学习的一个笔记记录。感谢下面的大神:

Android 屏幕适配方案 - 张鸿洋

Android 屏幕适配(上)- 临风眠

Android 屏幕适配(下)- 临风眠

Android Support 25中BottomNavigationView与ViewPager结合实现material Tab标准效果 - M星空

vector drawable - 比目鱼26

Android统计报告 - opensignal.com

友盟手机统计报告

推荐阅读更多精彩内容