这是一篇关于Android屏幕适配的介绍,首先声明,不建议使用px作为单位,原因大家都知道。 ###图片镇楼
看到这两张图相信大家都不陌生了,没错,这就是苦逼的适配,在2012年,OpenSignalMaps发布了第一份Android碎片化报告,统计数据表明,
2012年,支持Android的设备共有3997种。
2013年,支持Android的设备共有11868种。
2014年,支持Android的设备共有18796种。
···
2017年...?
首先需要介绍一些概念:
原文地址
- px(pixels)像素,屏幕上的点,不同设备显示效果相同,例如,HVGA代表320×480像素。
- in(英寸)屏幕的物理尺寸, 每英寸等于2.54厘米。
- pt(point)标准长度单位, 1pt=1/72英寸,用于印刷业,UI设计师会用,iOS字体单位,Android开发不涉及。
- dpi(dots per inch) 打印分辨率,每英寸所能打印的点数,即打印精度; 每英寸点数,即每英寸包含像素个数。
- ppi (pixels per inch)图像分辨率, 像素密度,在图像中, 每英寸所包含的像素数目。
- density(屏幕密度), density和dpi的关系为 density = dpi/160。density这个概念方便理解不同dpi的倍数关系。
- dp(也即dip,device independent pixels)设备独立像素,Android特有的单位,与密度无关的像素,基于屏幕密度的抽象单位,在320x480分辨率,同时每英寸160点(dpi = 160)的显示器上,1dp = 1px。
- sp(scaled pixels)放大像素,与刻度无关的像素,字体单位,可以根据用户的字体大小首选项进行缩放。sp和dp一样,是android开发里特有的单位, 查看TextView的源码可知 Android 默认使用 sp 作为字号单位。 为什么要把sp和dp代替px?最简单的原因是他们不会因为ppi的变化而变化,在相同物理尺寸和不同ppi/dpi下,他们呈现的高度大小是相同。也就是说更接近物理呈现,而px则不行。
换算关系:
px = dp * (dpi / 160),原来这里的dpi是归一化后的dpi。
则dp = px / (ppi / 160)
ppi = √(长度像素数² + 宽度像素数²) / 屏幕对角线英寸数
dp*ppi/160 = px。比如1dp x 320ppi/160 = 2px。
dependencies {
...
implementation 'com.github.sing1:UIAdapter:last_version'
}
或者使用后面的代码自己生成,然后考到项目中,或者下载library中的代码拷贝,这个库适配的机型有:
240x320 120dpi | 240x400 120dpi | 240x432 120dpi | 320x480 160dpi | 480x640 240dpi |
---|---|---|---|---|
480x800 120dpi | 480x800 160dpi | 480x800 240dpi | 480x854 120dpi | 480x854 160dpi |
480x854 240dpi | 640x960 320dpi | 600x1024 160dpi | 600x1024 240dpi | 600x1024 120dpi |
720x1280 160dpi | 720x1280 240dpi | 720x1280 320dpi | 768x1280 320dpi | 768x1024 160dpi |
768x1280 160dpi | 768x1280 240dpi | 800x1280 320dpi | 800x1280 160dpi | 800x1280 213dpi |
1080x1920 420dpi | 1080x1920 480dpi | 1200x1920 320dpi | 1440x2560 560dpi | 1152x1536 240dpi |
1152x1920 240dpi | 1200x1920 240dpi | 1536x2048 320dpi | 1536x2560 320dpi | 1600x2560 320dpi |
1080x2400 440dpi | 1080x2252 440dpi | 1080x2296 440dpi |
这个其中包括了横屏和竖屏,下面是生成文件的代码:
// 竖屏文件的输出路劲
private final static String rootPathPort = "/Users/Sing/Desktop/res/values-port-{0}dpi-{1}x{2}/";
// 横屏文件的输出路劲
private final static String rootPathLand = "/Users/Sing/Desktop/res/values-land-{0}dpi-{1}x{2}/";
// 水平方向(X)的值
private final static String templateX = " <dimen name=\"dp_{0}_x\">{1}dp</dimen>\n";
// 垂直方向(Y)的值
private final static String templateY = " <dimen name=\"dp_{0}_y\">{1}dp</dimen>\n";
// 默认模板信息,及参照尺寸,这里用的是1280x720 320dpi
private static int defaultDpi = 320;
private static int defaultWidth = 720;
private static int defaultHeight = 1280;
private static double defaultScale = 2.0;
// 将double类型的数据保留两位小数
public static String twoDemal(double num) {
DecimalFormat dFormat = new DecimalFormat("##0.00");
String result = dFormat.format(num);
return result;
}
// 传入宽、高、DPI生成
public static void makeString(int w, int h, int dpi) {
makeValue(w,h,dpi,0);// 竖屏X
makeValue(w,h,dpi,1);// 竖屏Y
makeValue(w,h,dpi,2);// 横屏X
makeValue(w,h,dpi,3);// 横屏Y
}
// type 0-3 分别为 竖屏X、竖屏Y、横屏X、横屏Y
private static void makeValue(int w, int h, int dpi, int type) {
double scale = ((double) dpi / 160); // 缩放比例
double totalWidthDp = defaultWidth / defaultScale; // 机型水平方向总共有多少个DP
double totalHeightDp = defaultHeight / defaultScale; // 机型垂直方向总共有多少个DP
double portWidthValue = (double) w / scale / totalWidthDp; // 竖屏情况下,参考模板水平方向(X) 每1dp对应的适配机型的值
double portHeightValue = (double) h / scale / totalHeightDp;// 竖屏情况下,参考模板垂直方向(Y) 每1dp对应的适配机型的值
double landWidthValue = ((double) h / w) * portWidthValue;// 横屏情况下,参考模板水平方向(X) 每1dp对应的适配机型的值
double landHeightValue = ((double) w / h) * portHeightValue;// 横屏情况下,参考模板垂直方向(Y) 每1dp对应的适配机型的值
String rootPath = "";// 实际输出的路径
File layFile = null;
String fileName = "";
StringBuffer sb = new StringBuffer();
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");// 文件头
sb.append("<resources>\n");// 文件头
if (type == 0) { // 竖屏 X
for (int i = 1; i <= 200; i++) {
sb.append(templateX.replace("{0}", i + "").replace("{1}", twoDemal(portWidthValue * i) + ""));
}
rootPath = rootPathPort;
fileName = "dimens_x.xml";
} else if (type == 1) {// 竖屏 Y
for (int i = 1; i <= 200; i++) {
sb.append(templateY.replace("{0}", i + "").replace("{1}", twoDemal(portHeightValue * i) + ""));
}
rootPath = rootPathPort;
fileName = "dimens_y.xml";
} else if (type == 2) {// 横屏 X
for (int i = 1; i <= 200; i++) {
sb.append(templateX.replace("{0}", i + "").replace("{1}", twoDemal(landWidthValue * i) + ""));
}
fileName = "dimens_x.xml";
rootPath = rootPathLand;
} else if (type == 3) {// 横屏 Y
for (int i = 1; i <= 200; i++) {
sb.append(templateY.replace("{0}", i + "").replace("{1}", twoDemal(landHeightValue * i) + ""));
}
rootPath = rootPathLand;
fileName = "dimens_y.xml";
}
sb.append("</resources>");// 文件尾
String path = rootPath.replace("{0}", dpi + "").replace("{1}", h + "").replace("{2}", w + "");
File rootFile = new File(path);
if (!rootFile.exists()) {
rootFile.mkdirs();
}
layFile = new File(path + fileName);
try {
PrintWriter pw = new PrintWriter(new FileOutputStream(layFile));
pw.print(sb.toString());
pw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
// 入口在这里,想生成什么机型就传入什么机型的信息,
public static void main(String[] args) {
makeString(720, 1280, 320);
makeString(1080, 1920, 480);
// 这里可以输出你想要适配的机型
}
传入相应的机型后输出效果图如下:
使用生成的代码进行适配:
从图中可以看出,在1280x720和1920x1080的手机上,不管是横屏还是竖屏,控件的宽度和高度分别占据屏幕的1/2和1/4的的大小,由此说明,生成的文件计算的还是准确的,但是竖屏是个接近正方形的在横屏上却变成了长方形,是哪里出问题了?
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="@dimen/dp_180_x"
android:layout_height="@dimen/dp_160_y"
android:background="#123123"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="16sp" />
</LinearLayout>
是因为控件的长度和高度是按手机的长宽计算的,所以适配的时候有时高度需要按照宽度来作为参考物,这个需要了解一下。
这个是第一版,如果有BUG的话欢迎指出!