最清晰的Android多屏幕适配方案
问题的引入
当您的Android应用即将发布的时候,如果你想让更多的用户去使用你的应用,摆在工程师面前的一个重要问题就是如何让你的应用能在各种各样的终端上运行,这里的各种各样首当其冲的就是不同的屏幕分辨率和尺寸。
屏幕适配主要从图片和距离(文字)进行下手。从以往的方式适配方式中,开发者可能会考虑各种各样的分辨率,比如480*800、1280*800、1920*1080等,为此在资源文件夹里面创建了一大堆子文件夹,那么有什么更好的方式吗?首先我们看下genymotion模拟其中一些流行的镜像的屏幕参数信息:
上面举了两个例子,其实看了好几个发现大部分的屏幕虽然分辨率各种各样,但是dpi最多的就三种:160dpi、240dpi、320dpi
所以为了屏幕适配,我们应该仅仅对dpi做适配即可,这里我们分别对160dpi、240dpi、320dpi进行适配即可满足市场上大部分的需求,以上是图片的解决方案。
同时,光看dpi也不行,也要看对应的屏幕最小宽度,这个最小宽度是和dp对应的,用下列代码既可:
Configuration config = getResources().getConfiguration(); int smallestScreenWidth = config.smallestScreenWidthDp; L.i("smallest width : "+ smallestScreenWidth);
这个获取出来的值,我们需要建立对应的values-sw{smallestWidth}dp文件夹进行适配,以上是文字和距离尺寸的适配。
下面将具体的距离说明,文中的例子原先是在1280*800的160dpi的屏幕上开发的,这个在这里被叫做base size,我们对这个项目的代码进行适配。
文字和尺寸的适配
我们这里需要将代码跑在一个1920*1200分辨率320dpi的平板上,发现所有的字体都变大了,看似1920*1200的分辨率比之前的1280*800要大一大圈,但是因为dpi也高,所以导致字体变大。
运行上面的获取smallestScreenWidth的代码后,发现值为600。(base size的平板电脑这个值是800)
首先在values文件夹中建立一个dimens.xml文件
继续在res中建立和values文件夹同级别的两个文件夹values-sw600dp-land和values-sw800dp-land,为了适应更多的屏幕,也加入了values-sw480dp-land (后缀是land是因为例子的项目是平板)
随后我们一个个的把原来写的layout文件找出来,找出里面原来写死的“数字”,比如宽度和字体大小之类的,一般来说单位是dp或者sp,将这些数字全部在values/dimens.xml中定义一个变量同时写回layout文件中对应的数字的地方,这里举个例子:
原来的代码是这样的:
现在代码成了这样:
dimens.xml中添加的内容:
然后你将values-sw600dp-land的里面的dimens.xml分别乘以0.75来获得:(因为600/800等于0.75)
values-sw480dp-land里面的dimens.xml分别乘以0.6来获得(因为480/800等于0.6)
values-sw800dp-land保持和values里面的一样,因为它是base size。
这样子以后我们再运行代码到1920*1200分辨率320dpi的平板上,发现这个时候字体还有空间宽高都和原来的base size的一模一样了,就像是原封不动的跑在base size平板上的感觉!
那么这个时候问题来了:
那么多的layout文件夹本身每个创建一个dimen变量就够累了,然后还要分别拷贝一份同时手动计算乘以0.6或者0.75来获得新的值,拿计算器一个个的计算,多大的工作量啊,况且以后要是要来个sw1028、sw320呢?再次算一次?
这里提供一个方法:
在代码里面新加一个带main的java类,事实上它是自动的帮你在原来的values/dimens.xml的基础上自动的帮你把每个dimen乘以你所需要的乘积然后将结果写回对应的dimens.xml里面,这大大的减轻了工作量,以后每次修改dimens.xml只要这样子即可:
- 在values/dimens.xml里面添加或修改变量
- 跑一边上述java类自动的生成别的dimens.xml
这里给出这个工具java类的代码:
public class DimenTool { public static void gen() { File file = new File("./app/src/main/res/values/dimens.xml"); BufferedReader reader = null; StringBuilder sw480 = new StringBuilder(); StringBuilder sw600 = new StringBuilder(); StringBuilder sw720 = new StringBuilder(); StringBuilder sw800 = new StringBuilder(); StringBuilder w820 = new StringBuilder(); try { System.out.println("生成不同分辨率:"); reader = new BufferedReader(new FileReader(file)); String tempString; int line = 1; // 一次读入一行,直到读入null为文件结束 while ((tempString = reader.readLine()) != null) { if (tempString.contains("</dimen>")) { //tempString = tempString.replaceAll(" ", ""); String start = tempString.substring(0, tempString.indexOf(">") + 1); String end = tempString.substring(tempString.lastIndexOf("<") - 2); int num = Integer.valueOf(tempString.substring(tempString.indexOf(">") + 1, tempString.indexOf("</dimen>") - 2)); sw480.append(start).append((int) Math.round(num * 0.6)).append(end).append("\n"); sw600.append(start).append((int) Math.round(num * 0.75)).append(end).append("\n"); sw720.append(start).append((int) Math.round(num * 0.9)).append(end).append("\n"); sw800.append(tempString).append("\n"); w820.append(tempString).append("\n"); } else { sw480.append(tempString).append("\n"); sw600.append(tempString).append("\n"); sw720.append(tempString).append("\n"); sw800.append(tempString).append("\n"); w820.append(tempString).append("\n"); } line++; } reader.close(); System.out.println("<!-- sw480 -->"); System.out.println(sw480); System.out.println("<!-- sw600 -->"); System.out.println(sw600); System.out.println("<!-- sw720 -->"); System.out.println(sw720); System.out.println("<!-- sw800 -->"); System.out.println(sw800); String sw480file = "./app/src/main/res/values-sw480dp-land/dimens.xml"; String sw600file = "./app/src/main/res/values-sw600dp-land/dimens.xml"; String sw720file = "./app/src/main/res/values-sw720dp-land/dimens.xml"; String sw800file = "./app/src/main/res/values-sw800dp-land/dimens.xml"; String w820file = "./app/src/main/res/values-w820dp/dimens.xml"; writeFile(sw480file, sw480.toString()); writeFile(sw600file, sw600.toString()); writeFile(sw720file, sw720.toString()); writeFile(sw800file, sw800.toString()); writeFile(w820file, w820.toString()); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e1) { e1.printStackTrace(); } } } } public static void writeFile(String file, String text) { PrintWriter out = null; try { out = new PrintWriter(new BufferedWriter(new FileWriter(file))); out.println(text); } catch (IOException e) { e.printStackTrace(); } out.close(); } public static void main(String[] args) { gen(); } }
图片的适配
文字和空间宽度适配后,大家可能发现部分的ImageView或者ImageButton部分还有些变大或者变小,有的变模糊了,这里需要美工提供多套图片,大家请看这张图:
这里说明了开发时应该图片以160dpi为基准,同时提供不同dpi的基于baseline的图片的放大或者缩小版本。那么每套图片放什么地方呢?
以上是Google官方给出的分类标准(虽然 Android 也支持低像素密度 (LDPI) 的屏幕,但无需为此费神,系统会自动将 HDPI 尺寸的图标缩小到 1/2 进行匹配。)
所以对于我们的例子中1920*1200分辨率320dpi的平板,我们应该让美工制作放大2x的版本的图片,同时将图片放到drawable-xhdpi文件夹中,原来的图片放到drawable-mdpi文件夹中。这里需要注意一下,对于drawable你可能会写很多的比如shape或者selector的xml形式的drawable,他们本身不是图片而是一个xml文件,但是他们都会去引用真实的drawable图片,对于这种xml最好是要放到无dpi影响的drawable文件夹中(无后缀)
这样子以来,我们再把代码跑到1920*1200分辨率320dpi的平板上,发现图片部分也OK了,适配完毕。
这里还需要提醒的一点,并不是每个地方的图片都需要提供多套图片这种方案来解决,因为这种方案会带来使apk的size变大的副作用。所以大家可以根据实际需求,若可以通过上一节的方式来修改imageView的尺寸大小来解决(而非用wrap_content来指定layout_width和layout_height)的话,就不需要用多套图片的方式。
应用启动图标的适配
对于高分辨率低dpi的设备,我们经常会发现在launcher中我们的应用的启动icon被拉伸的模糊了,严重影响了门面的形象。
这里我们也通过提供多套icon的方式来解决,下面列表给出了不同屏幕密度中推荐的icon的size大小
在Android4.2以上的版本中,提供了对mipmaps的支持,说简单点就是他能对bitmap进行缩放的时候减少一些性能的耗损。如果你用Andorid Studio开发Android程序会发现Android Studio自动帮你创建了几个mipmaps文件夹,你可将应用的启动图标放到不同的mipmaps文件夹中而不是上述的drawable文件夹中,比如:
这里你至少要提供一个xxxhdpi类型的启动图标,因为Android会帮你自动缩小图标到对应的别的分辨率上(放大是会变模糊的),这样子可以节省些apk size。