屏幕适配
前言
依赖库可访问我的Github:https://github.com/Mangosir/DimensMake
使用起来很方便:
-
在项目根目录下的build.gradle文件中添加如下代码
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
-
在moudle下的build.gradle文件添加如下依赖
dependencies { compile 'com.github.Mangosir:DimensMake:1.0.4' }
-
在工程中任何一个类添加如下代码,然后右键运行main方法即可
public static void main(String[] args){ //下面两种方法任选其一,具体参数信息见代码 DimensBuild.buildDP(); DimensBuild.buildPX(); }
说起屏幕适配或者说UI适配应该是Android开发中必碰到的一个问题,在项目开发早期,UI小妹(当然了,也可能是UI小哥了)在问完IOS要多少分辨率的图片后来问你要多少分辨率的,此刻你要怎么回答;在面试中,面试官问你在平时开发中是如何处理屏幕适配的问题的,这时候你要怎么应对;在开发过程中,碰到不同分辨率的手机,不同尺寸的手机出现的UI变形问题,你要怎么解决;今天我想用这篇文章来解决这些问题
适配缘由
做Android开发一定会碰到适配这个问题,在Android世界里,Android设备太多了,手机,平板,TV,手表等,光其中的手机这一项就有众多厂家发布的奇奇怪怪的手机,不仅分辨率各有不同,就是手机尺寸也是一言难尽,各种尺寸都有,更烦的是有的手机还在屏幕上搞个虚拟导航栏放在底部;厂家多也就算了,由于Android系统的开源,任何厂家,个人,OEM厂商,运营商都可以对Android进行定制,修改成他们想要的样子,这更加大了适配的难度(尤其是我在平时开发中经常会用到一些工程机);总之而言,Android的碎片化太严重了;不像ios适配机型有限,所以解决掉Android适配问题是做一个好的APP的第一步
相关概念
在开发过程中,UI给出的设计图上的所有元素尺寸都是以px作为单位的,但是我们在layout文件中定义相关View的长宽是用dp作为单位来指定其值的,定义文字大小是用sp作为单位,还可能会涉及到屏幕尺寸,屏幕分辨率,屏幕像素密度dpi
这里说到了四个单位:px dp sp dpi,还有屏幕尺寸及屏幕分辨率:
-
屏幕分辨率:指手机横纵方向上的像素点数,比如常见的720 * 1280 ,1080 * 1920,单位px,1px=一个像素点,
-
屏幕尺寸:不是屏幕宽度,也不是长度,而是屏幕的对角线长度,比如常见的5.0尺寸,5.5尺寸,单位是英寸,1英寸=2.54厘米
-
dpi:全称dots per ich,也就是每英寸的像素点数(其实不是真正有多少像素点数),或者说屏幕像素密度;它是一个软件上的概念,是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值;我们可能还接触过一个单位叫ppi,它也是像素密度,这个是物理上的概念,它是客观存在的不会改变。dpi是软件参考了物理像素密度后,人为指定的一个值,这样保证了某一个区间内的物理像素密度在软件上都使用同一个值。这样会有利于我们的UI适配;比如,几部相同分辨率不同尺寸的手机的ppi可能分别是是430,440,450,那么在Android系统中,可能dpi会全部指定为480;ppi与屏幕尺寸和屏幕分辨率有关,它们的关系如下
-
px:这个大家容易理解,也就是真实像素的单位,就跟s是时间秒的单位一样
-
sp:全称scale-independent pixel,也叫sip,即独立缩放像素,Android特有的用来描述文字大小的单位,建议使用12,14,18,20等偶数值,避免奇数和小数造成精度的丢失问题;为什么用这个不用dp呢?这是因为我们可以在手机系统设置中修改文字的大小,如果使用dp单位,文字大小不会被改变;而使用sp会随着修改而变化;具体原因可参考sp与dp的区别
-
dp:全称Density-independent pixel 也叫dip,即独立像素密度,它的一个特性就是与像素无关,或者说与终端上的实际物理像素点无关,是Android特有的用来描述View尺寸的单位,保证同一个分辨率的图片在不同分辨率的手机上保持同样的视觉大小(占用相识比例的空间)
比如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度,原因在下方
在Android中,规定以160dpi为基准,1dp=1px;320dpi下,1dp = 2px,计算公式:px = dp * (dpi / 160),屏幕密度越大,1dp对应 的像素点越多,如下表
dp | 1 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|---|
px | 1 | 1.5 | 2 | 3 | 3.5 | 4 |
dpi | 160 | 240 | 320 | 480 | 560 | 640 |
这里还有一个东西没有提,mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi等,它们是用来修饰Android中的mipmap(Eclipse中是drawable)文件夹及values文件夹,用来区分不同像素密度下的图片和dimen值,它们与dpi的关系如下
名称 | 密度区间 |
---|---|
mdpi | 120dpi~160dpi |
hdpi | 160dpi~240dpi |
xhdpi | 240dpi~320dpi |
xxhdpi | 320dpi~480dpi |
xxxhdpi | 480dpi~640dpi |
通常情况下每个目录对应的屏幕分辨率如下
名称 | 代表分辨率 |
---|---|
mdpi | 320x480 |
hdpi | 480x800 |
xhdpi | 720x1280 |
xxhdpi | 1080x1920 |
xxxhdpi | 1440x2960 |
但是这里要注意,同一个分辨率的dpi是可能不同的,比如1080x1920分辨率的dpi可以是420,也可以是480
适配方案
db适配
在设计大图的时候,最好是同一个图片分别设计几种分辨率放到对应目录,以免变形和降低内存开销
设计图标的时候同理,假如你的手机分辨率是1080x1920,手机屏幕密度是480,那1dp=3px;如果UI在设计过程中将这个图标设计为108x108px,那么转换为dp就是36x36dp,放在xxhdpi目录中,理论上来说在上表中对应的每个分辨率的Android手机里这个图标对应的大小都是36dp * 36dp;那么就需要为720x1280分辨率的手机设计一个72x72px的图标;为480x800分辨率的手机设计一个54x54的图标等,以此类推,即一套分辨率=一套位图资源,当然了这肯定会为UI设计师增加很多工作量,也会给APK增加大小
如果没有设计其它屏幕分辨率的图标,那么Android会将这个目录的图标进行压缩或者放大展示在其它分辨率的设备上;比如在720x1280的设备,就会将这个图标进行压缩,展示后的像素就会变成72x72px;在1440x2960设备上,就会将这个图标进行放大,展示后的实际像素有可能会达到144x144px,这样虽然能达到类似的展示效果,比如占用相似比例的空间,但是图标内容是会变形的,只是变形的程度不同
使用db作为尺寸单位可以基本解决不同手机上适配的问题,这可以说是最原始的Android适配方案
使用这种方案能解决掉绝大部分的适配问题,但是使用这种方案还是存在一些问题,比如并不是所有分辨率为1080x1920手机的的dpi都是480,还有420的,甚至更低都有,那么1dp对应的px就会有不同,为什么呢?上面在介绍dpi的时候说到,dpi跟分辨率和屏幕尺寸有关系,虽然都是同样的分辨率,但是尺寸有5.5的,也有5.0,还有4.7的,那么1dp对应的px肯定是不同的,那么一个设置为360x360dp的ImageView在480dpi的手机上宽度就会铺满屏幕,但是在420dpi的屏幕,1dp=2.625px,实际展示出来的宽度只有945px,这样宽度就不能铺满屏幕,在更低的dpi屏幕上,差别就更明显了
设计最大分辨率图片
Android系统会根据屏幕密度自动选择对应的资源目录下的文件进行渲染加载,比如你的手机分辨率是480x800,那就会优先去hdpi目录下加载图片,如果没有,那就去更高的dpi的目录比如xhdpi,xxhdpi去加载,然后将原图片进行压缩展示,但是因为原图片像素很大,尽管进行了压缩,还是能展示的很清晰
所以理论上来说只需要提供一种分辨率规格的图片资源就可以了,但是要提供尽量高分辨率的图片,现在的手机大部分的分辨率都是720x1280和1080x1920,所以提供一套xxhdpi规格的图片就可以了,这样就跟设计师说按照1080x1920分辨率设计就行了
这只是一种比较偷懒的方法,比较适合那种用户基数小,用户群体固定的APP
然后对layout文件中的ImageView的ScaleType属性的设置:
设置不同的ScaleType会得到不同的显示效果,一般情况下,设置为centerCrop能获得较好的适配效果。
屏幕分辨率限定符(宽高限定符)
什么叫屏幕分辨率限定符呢?
我们知道市面上不同分辨率的手机太多太多了,而这种方案就是尽可能多的将其列举出来,在res目录下面创建与各种分辨率一一对应的values-xxx 文件夹;然后选定一种基准分辨率,然后其它分辨率以该分辨率做参考,生成对应的dimens文件,如图:
其实分辨率方面1920x1080以及1280x720是应用适配占比最高,市场占有率有50%左右,至于这些数据从哪来,大家可以去友盟统计查看
dimens文件生成原理
我们选定一种分辨率作为基准分辨率,将宽高分成x份和y份,1/x份就是一个px;其它分辨率都以基准分辨率作为参考系,计算出一份等于多少px,最后编写对应的dimens文件
比如我这里以1280x720作为基准分辨率,那么
-
在values-1280x720目录下,创建lay_x和lay_y两个xml文件
-
高度既然是1280px,那就将所有分辨率的高度都统一分为1280份,定义dimen个数是从y1到y1280,在当前分辨率下,分别取值为1px-1280px,存放在lay_y文件下
-
宽度是720px,那就将所有分辨率的宽度统一划分为720份,定义dimen个数是从x1到x720,在当前分辨率下,分别取值为1px-720px,存放在lay_x文件下
values-1280x720目录下文件搞定了,那么对于1920x1080分辨率的values-1920x1080目录下的lay_x和lay_y文件怎么写呢?
-
lay_x文件里还是定义dimen个数是从x1到x720,那么x1的值是(1080/720) * 1 = 1.5px ,x2的值是(1080/720) * 2 = 3.0px,以此类推,x720的值是(1080/720) * 720 = 1080.0px
-
lay_y文件里也是定义dimen个数是从y1到y1280,那么y1的值是(1920/1280) * 1 = 1.5px,y2的值是(1920/1280) * 2 = 3.0px,以此类推,y1280的值是(1920/1280) * 1280 = 1920.0px
其它分辨率下的lay_x和lay_y文件都是如此计算,计算公式:(实际分辨率/基准分辨率)* X = px(X为份额)
最终文件内容如图:
使用样例1
这时候假如你拿到的UI设计图上的一个控件宽度是540px,而且设计图是基于1920x1080分辨率设计的;而且控件是占屏幕宽度50%,那你在layout中可以对控件这样写
android:layout_width="@dimen/x360"
至于这个360怎么得来的,就是根据上面的计算公式,(1080/720) * x = 540px,可以得出x = 360
这样当app运行时,系统就会自动根据当前手机分辨率去相应的values目录下去找对应的值
-
如果手机分辨率是1920x1080,那么系统就会去values-1920x1080目录下的lay_x文件找x360这个索引,获取到的值是540px,那控件就是如设计图一样占屏幕宽度的50%
-
如果手机分辨率是1280x720,那么系统就会去values-1280x720目录下的lay_x文件找x360这个索引,获取到的值是360px,控件还是如设计图一样占屏幕宽度的50%
这样在手机上的UI比例跟设计图一样,如此我们也就完成了UI适配的工作
注意:这里有两个前提条件就是控件的大小要确定,设计图的大小也要确定
使用样例2
比如设计图上的控件宽度还是540px,但是设计图是基于1280x720设计的,那控件就是占屏幕宽度75%,那在layout里就得这样写
android:layout_width="@dimen/x540"
540同样是根据上面的计算公式得出(720/720) * x = 540px,可以得出x = 540
那么在1920x1080分辨率的手机上,系统就会去values-1920x1080目录下的lay_x文件找x540这个索引,得到的值是810px,这样同样是占屏幕宽度75%
dimens文件生成脚本
刚接触的童鞋是不是一看要写这么多文件,而且文件里面的内容这么多,几百上千条dimen,要是手写那不得写到天昏地暗啊;不用担心,作为程序猿,怎么能做这么体力活呢?写个脚本就可以完成这些工作
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
/**
* Author: Mangoer
* Time: 2019/1/4 19:48
* Version:
* Desc:TODO()
*/
public class DimenPXThread implements Runnable{
private String path = "";
private static List<DimensPX> dimensData = new ArrayList<>();
static{
dimensData.add(new DimensPX(480,800,720,1280));
dimensData.add(new DimensPX(480,854,720,1280));
dimensData.add(new DimensPX(540,960,720,1280));
dimensData.add(new DimensPX(600,1024,720,1280));
dimensData.add(new DimensPX(768,1024,720,1280));
dimensData.add(new DimensPX(720,1184,720,1280));
dimensData.add(new DimensPX(720,1196,720,1280));
dimensData.add(new DimensPX(720,1280,720,1280));
dimensData.add(new DimensPX(768,1280,720,1280));
dimensData.add(new DimensPX(800,1280,720,1280));
dimensData.add(new DimensPX(1080,1776,720,1280));
dimensData.add(new DimensPX(1080,1794,720,1280));
dimensData.add(new DimensPX(1080,1812,720,1280));
dimensData.add(new DimensPX(1080,1920,720,1280));
dimensData.add(new DimensPX(1200,1920,720,1280));
dimensData.add(new DimensPX(1080,2016,720,1280));
dimensData.add(new DimensPX(1080,2040,720,1280));
dimensData.add(new DimensPX(1080,2160,720,1280));
dimensData.add(new DimensPX(1440,2560,720,1280));
}
public void setPath(String path) {
this.path = path;
}
@Override
public void run() {
if("".equals(path)) return;
StringBuffer sb = new StringBuffer();
for(int i=0; i<dimensData.size(); i++){
DimensPX dimensPX = dimensData.get(i);
String parentName = path + "values-"+dimensPX.getyPX() + "x" + dimensPX.getxPX();
File file = new File(parentName);
if(!file.exists()){
file.mkdirs();
}
/************************编写lay_x.xml文件*******************************/
File lay_x = new File(file, "lay_x.xml");
lay_x.delete();
writeFile(lay_x, sb, dimensPX,"x");
/**************************编写lay_y.xml文件********************************/
File lay_y = new File(file, "lay_y.xml");
lay_y.delete();
writeFile(lay_y, sb, dimensPX,"y");
}
}
private void writeFile(File lay, StringBuffer sb, DimensPX dimens, String type){
//切勿使用FileWriter写数据,它是默认使用ISO-8859-1 or gb2312,不是utf-8,并且没有setEncoding方法
BufferedWriter fw = null;
try {
fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(lay,true),"UTF-8"));
fw.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>" +"\n");
fw.write("<resources>" + "\n");
int bound;
if("x".equals(type)){
bound = dimens.getxBase();
}else{
bound = dimens.getyBase();
}
for(int k=1; k<bound+1; k++){
sb.setLength(0);
sb.append(" <dimen name=\""+type+k+"\">");
float px = 0.0f;
if("x".equals(type)){
px = ((float)dimens.getxPX()/dimens.getxBase()) * k;
}else{
px = ((float)dimens.getyPX()/dimens.getyBase()) * k;
}
sb.append(px+"px</dimen>" + "\n");
fw.write(sb.toString());
}
fw.write("</resources>");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
总结
这个方案有一个难以避免的问题,就是容错率不高:
列举出来的分辨率是有限的,而且是一对一的,手机是哪个分辨率就必须要找到对应的限定符;可是Android手机的分辨率太多了呀,这样没列举出来的分辨率就只能使用默认dimens了,不可避免的会导致UI变形;很恶心的是有的手机底部还有虚拟导航栏,这又得单独写一份dimens文件;还有平板的适配,也得增加dimens文件;大量的dimens.xml 文件能占到2M左右,增加了APK的体积
不过该方案放弃Google推荐的dp适配方案,直接采用实际像素来适配,能解决绝大部分的适配问题,且能适配的较好,而且也有较多的团队使用这种方案,还是比较成熟的,前提是列举相当齐全的分辨率限定符
最小宽度限定符
顾名思义根据屏幕宽高中最小尺寸的dp值去对应的values-swdp目录下的dimens文件中获取结果,所以也叫smallestWidth限定符适配 或者 sw< N >dp限定符适配
可以看到这里加了最小两个字,到底什么意思呢?我们知道手机屏幕是可以旋转的,这样宽高值就会互换,如果我们选定最小,那么该方案就不区分屏幕旋转了,只取宽高中最小的值
最终目录如图
该方案和上面的屏幕分辨率限定符方案原理都是一样的,都是写好相关的文件,运行时由系统去寻找对应的资源文件;只不过上面那个方案是根据当前手机分辨率去找匹配到的values-xxx目录,而该方案是根据宽高中最小dp值去匹配对应的values-swxxxdp目录
那这个最小dp值怎么算出来的呢?
计算公式:(手机宽高最小像素值/(DPI/160) ) = dp
用代码表达:
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics );
int heightP = dm.heightPixels;
int widthP = dm.widthPixels;
float density = dm.density;
float smallestWidthDP;
if(heightP < widthP ) {
smallestWidthDP = heightP / density;
}else {
smallestWidthDP = widthP / density;
}
比如手机分辨率是1920 x 1080,显然最小值是1080,其DPI是480,那么dp = 1080 / (480/160) = 360dp;接下来系统就会根据这个360dp去匹配values-sw360dp这个目录下的dimens文件;如果找不到对应的values目录,系统就会向下找离values-sw360dp最近的一个目录,比如values-sw320dp
dimens文件生成原理
这里同样还是要选取一个最小宽度作为基准值,假如我选取360dp作为最小宽度基准值,也就是说将屏幕宽度等分为360份,这样从第一份到第360份所占的dp值的计算公式如下:
dp = ( 实际最小宽度 / 最小宽度基准值 ) * X
这里的X是份额,我们是以360dp作为基准,那X就是从1到360
由此可知values-sw360dp目录下的dimens文件中内容如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">1.00000dp</dimen>
<dimen name="dp_2">2.00000dp</dimen>
<dimen name="dp_3">3.00000dp</dimen>
<!--中间太多 省略-->
<dimen name="dp_108">108.00000dp</dimen>
<dimen name="dp_358">358.00000dp</dimen>
<dimen name="dp_359">359.00000dp</dimen>
<dimen name="dp_360">360.00000dp</dimen>
</resources>
那么其它目录下的dimens内容也就容易计算了,比如values-sw420dp目录,如果系统匹配到这个目录,说明该手机的最小宽度是420dp,套用上面的公式,可以得出从第一份到第360份所占的dp值如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">1.16667dp</dimen>
<dimen name="dp_2">2.33334dp</dimen>
<dimen name="dp_3">3.50001dp</dimen>
<!--中间太多 省略-->
<dimen name="dp_358">417.66786dp</dimen>
<dimen name="dp_359">418.83453dp</dimen>
<dimen name="dp_360">420.00000dp</dimen>
</resources>
使用样例
假设你拿到的设计图是基于1920x1080的分辨率设计的,且DPI是480,设计图上的一个控件宽度是324x324px,控件所占宽度百分比是324/1080 = 30%,那我们应该在layout布局中给这个控件的宽高引用多少dimens合适呢?如下:
<ImageView
android:id="@+id/iv"
android:layout_width="@dimen/dp_108"
android:layout_height="@dimen/dp_108"
android:src="@mipmap/ic_launcher"
android:layout_centerInParent="true"/>
为什么是dp_108呢?我们来算一下
该设计图的最小宽度是1080px,那么最小宽度dp值根据开头的公式 px = px * (dpi / 160) 可以算出是 1080 / (480/160) = 360dp
控件宽度 = 324 / (480/160) = 108dp
套用【dimens文件生成原理】这一节的公式 dp = ( 实际最小宽度 / 最小宽度基准值 ) * X
这个公式里实际最小宽度是360dp,最小宽度基准值是360dp,dp值是108dp,只有X未知
所以X = 108dp / (360dp/360dp) = 108
如此系统就会去values-sw360dp目录下的dimens文件中查找引用dp_108
验证
既然开发完了,那我们就举例验证下
-
假如APP运行在一款1920x1080分辨率的手机上,DPI是400;可以得出该手机的最小宽度是1080px,dp值 = 1080 / (400/160) = 432,这样系统就会匹配到values-sw432dp目录下的dimens文件,该文件内容如下
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="dp_1">1.20000dp</dimen> <dimen name="dp_2">2.40000dp</dimen> <dimen name="dp_3">3.60000dp</dimen> <!--中间太多 省略--> <dimen name="dp_108">129.60000dp</dimen> <dimen name="dp_358">417.66786dp</dimen> <dimen name="dp_359">418.83453dp</dimen> <dimen name="dp_360">432.00000dp</dimen> </resources>
可以看到在这个设备上该控件的实际dp值是129.6dp,同样占屏幕宽度432dp的30%,与设计图的比例是一样的
-
假如APP运行在一款1280 * 720分辨率的手机上,DPI是320;可以得出该手机的最小宽度是720px,dp值 = 720 / (320/160) = 360dp,这样系统就会匹配到values-sw360dp目录下的dimens文件,该文件内容在上方以列出;可以得出这个设备上该控件的实际dp值是108dp,同样占屏幕宽度360dp的30%,与设计图的比例是一样的
-
假如APP运行在一款1920x1080分辨率的手机上,DPI是420;可以得出该手机的最小宽度是1080px,dp值 = 1080 / (420/160) = 411.428571dp,这样系统就会找到一个小于等于411.428571dp的目录,而values-sw411dp这个目录是最接近它的,所以就是用该目录下的dimens文件,内容如下
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="dp_1">1.14167dp</dimen> <dimen name="dp_2">2.28333dp</dimen> <dimen name="dp_3">3.42501dp</dimen> <!--中间太多 省略--> <dimen name="dp_108">123.30036dp</dimen> <dimen name="dp_358">408.71786dp</dimen> <dimen name="dp_359">409.85953dp</dimen> <dimen name="dp_360">411.00000dp</dimen> </resources>
可以看到在这个设备上该控件的实际dp值是123.30036dp,占屏幕宽度411.428571dp的29.97%,与设计图的比例差了一点点,原因就是虽然都是同样的分辨率,但是因为手机dpi的不同,导致算出来的最小宽度dp值可能出现小数这种情况,这样就没办法完全精准匹配values目录,只能匹配最近的目录,进而导致误差的出现;想优化这种情况只能是生成较全面的values目录,假如这里没有values-sw411dp目录,下面只有values-sw400dp目录,那误差就更大;不过1%的误差可以接受
关于字体
首先字体为什么官方推荐是使用sp作为单位呢?上面也说了,是为了响应用户在设置界面对手机全局字体大小的修改;这一点dp是做不到的,所以这里还是使用sp作为字体单位;那么我们就需要在dimens文件里添加字体相关的dimen引用,只不过单位是sp
那这个大小是多少呢?要知道同一个手机上默认情况下的1dp和1sp大小是一样的,不相信吗?请看
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="100sp"
android:layout_height="100sp"
android:text="Hello 1"/>
<Button
android:layout_width="100dp"
android:layout_height="100dp"
android:text="Hello 2"
android:layout_marginTop="100dp"/>
</RelativeLayout>
看预览效果
假如设计图是在【使用样例】这一节中的,某个文字的大小是36px,那sp值 = 36/(480/160) = 12sp,跟dp值计算方法一样;一般我们字体只需要设置12sp,14sp,16sp,18sp,20sp,22sp这些个就行了
在values-sw360dp目录下的dimens文件如下:
/**********字体适配***************/
<dimen name="sp_12">12.0sp</dimen>
<dimen name="sp_14">14.0sp</dimen>
<dimen name="sp_16">16.0sp</dimen>
<dimen name="sp_18">18.0sp</dimen>
<dimen name="sp_20">20.0sp</dimen>
<dimen name="sp_22">22.0sp</dimen>
<dimen name="sp_24">24.0sp</dimen>
在values-sw480dp目录下的dimens文件如下:
/**********字体适配***************/
<dimen name="sp_12">16.0sp</dimen>
<dimen name="sp_14">18.666668sp</dimen>
<dimen name="sp_16">21.333334sp</dimen>
<dimen name="sp_18">24.0sp</dimen>
<dimen name="sp_20">26.666668sp</dimen>
<dimen name="sp_22">29.333334sp</dimen>
<dimen name="sp_24">32.0sp</dimen>
dimens文件生成脚本
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
/**
* Author: Mangoer
* Time: 2019/1/4 19:50
* Version:
* Desc: TODO()
*/
public class DimenDPThread implements Runnable{
private String path = "";
private static int wBaseDp = 360;
private static List<DimensDP> dimensData = new ArrayList<>();
static{
dimensData.add(new DimensDP(240,wBaseDp));
dimensData.add(new DimensDP(320,wBaseDp));
dimensData.add(new DimensDP(360,wBaseDp));
dimensData.add(new DimensDP(384,wBaseDp));
dimensData.add(new DimensDP(392,wBaseDp));
dimensData.add(new DimensDP(400,wBaseDp));
dimensData.add(new DimensDP(410,wBaseDp));
dimensData.add(new DimensDP(411,wBaseDp));
dimensData.add(new DimensDP(420,wBaseDp));
dimensData.add(new DimensDP(430,wBaseDp));
dimensData.add(new DimensDP(432,wBaseDp));
dimensData.add(new DimensDP(440,wBaseDp));
dimensData.add(new DimensDP(480,wBaseDp));
dimensData.add(new DimensDP(533,wBaseDp));
dimensData.add(new DimensDP(592,wBaseDp));
//600dp以后的基本是平板适配,如果你的APP不需要适配平板,那后面的就不需要加了
dimensData.add(new DimensDP(600,wBaseDp));
dimensData.add(new DimensDP(640,wBaseDp));
dimensData.add(new DimensDP(662,wBaseDp));
dimensData.add(new DimensDP(720,wBaseDp));
dimensData.add(new DimensDP(768,wBaseDp));
dimensData.add(new DimensDP(800,wBaseDp));
dimensData.add(new DimensDP(811,wBaseDp));
}
public void setPath(String path) {
this.path = path;
}
public void setwBaseDp(int wBaseDp) {
this.wBaseDp = wBaseDp;
}
@Override
public void run() {
if("".equals(path)) return;
StringBuffer sb = new StringBuffer();
for(int i=0; i<dimensData.size(); i++){
DimensDP dimensDP = dimensData.get(i);
String parentName = path + "values-sw" + dimensDP.getSwDp() + "dp";
File file = new File(parentName);
if(!file.exists()){
file.mkdirs();
}
/************************编写dimens.xml文件*******************************/
File dim = new File(file, "dimens.xml");
dim.delete();
writeFile(dim, sb, dimensDP);
}
}
private void writeFile(File lay, StringBuffer sb, DimensDP dimens){
//切勿使用FileWriter写数据,它是默认使用ISO-8859-1 or gb2312,不是utf-8,并且没有setEncoding方法
BufferedWriter fw = null;
try {
fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(lay,true),"UTF-8"));
fw.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>" +"\n");
fw.write("<resources>" + "\n");
StringBuffer sp = new StringBuffer();
for(int k = 1; k<dimens.getwBaseDp()+1; k++){
sb.setLength(0);
sb.append(" <dimen name=\"dp_"+k+"\">");
float dp = ((float)dimens.getSwDp()/dimens.getwBaseDp()) * k;
sb.append(dp+"dp</dimen>" + "\n");
fw.write(sb.toString());
if (k >= 12 && k <=24 & k%2 == 0) {
sp.append(" <dimen name=\"sp_"+k+"\">");
float value = ((float)dimens.getSwDp()/dimens.getwBaseDp()) * k;
sp.append(value+"sp</dimen>" + "\n");
}
}
fw.write("\n");
fw.write(" /**********字体适配***************/" + "\n");
fw.write("\n");
fw.write(sp.toString());
fw.write("</resources>");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
总结
该方案比较成熟,可以看做是屏幕分辨率限定符方案的升级版,容错率更高,只要values目录合理,即使手机对应的最小宽度值没有完全对应的资源文件,它也能向下兼容,寻找最接近的values目录;并且该方案产生的文件要更少;其实可以看出两种方案的最终目的都是以跟设计图相同的百分比去适配各个手机的屏幕
疑问
有人可能问了,设计师给的图上标的都是px,而该方案都是dp,那每次写的时候都要去算下每个view的dp值吗?
不需要的,推荐几个工具:
- 将设计图上传到蓝湖,上面会标注出dp
- 使用像素大厨查看psd源文件,获取dp值
- 没办法只能自己对着设计图的px算
參考文章:
https://www.jianshu.com/p/1302ad5a4b04
https://www.jianshu.com/p/a4b8e4c5d9b0
https://www.jianshu.com/p/2aded8bb6ede