安卓:日常开发屏幕适配总结
Android 屏幕适配方案
Android多分辨率适配框架
Android 屏幕适配之dimens(上线项目中实际应用)
先读完上面的文章再继续读本篇文章最好不过了
这篇文章是结合自己的开发总结的,能适应大部分需求,就是因为安卓屏幕尺寸太多了,所以只能说适应大部分的需求
术语和概念
屏幕尺寸(screen size): 屏幕的对角线测量。
为了方便,Android把所有的屏幕尺寸分为了4个广义的大小:小、正常、大、更大
屏幕密度(screen density): 屏幕占据的物理区域所含像素的个数,通常被称为dpi(dots per inch)即每英寸的像素点数
分辨率(resolution): 屏幕上物理像素的点数
例如,有一个240px*400px的屏幕,可以理解为在这个屏幕上横着有400条线,每条线上有240个像素点
像素(px): 屏幕上的点
注意:由于JPG容易失真, 在Android开发中尽量避免使用.jpg图片, 应该使用.png图片, 它采用了从LZ77派生的无损数据压缩算法.
图片相关
屏幕:尺寸5.1,分辨率1920X1080
DPI:(1920<sup>2</sup> + 1080<sup>2</sup>) / 5.1 = 2202 / 5.1 = 431
mdpi 120dpi ~ 160dpi
hdpi 160dpi ~ 240dpi
xhdpi 240dpi ~ 320dpi
xxhdpi 320dpi ~ 480dpi
xxxhdpi 480dpi ~ 640dpi
在设计图标时:
对于五种主流像素密度(mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)
应按照(2:3:4:6:8)的比例进行缩放,(1x, 1.5x, 2x, 3x, 4x)
例如:
尺寸为100x100px的图标,设置ImageView
为wrap_content
,把图片放进xxhdpi中在不同像素密度的手机上显示:
手机像素密度/文件夹像素密度
在xxh
手机上显示为100*3/3=100px
在xhdpi
手机上显示100*2/3=66px
依此类推
图片占用的内存:
宽*高*4*(手机像素密度/文件夹像素密度)的平方/1024/1024(MB)
比如:在xxhdpi手机上100*100px的图片占用的内存为:
100*100*4/1024/1024 = 0.038MB
像素适配
- 主流的分辨率
我已经集成到了我们的程序中,当然对于特殊的,你可以通过参数指定。关于屏幕分辨率信息,可以通过该网站查询:http://screensiz.es/phone - 下载jar
先下载autolayout.jar - 执行命令
java -jar xx.jar width height width,height_width,height
例如我在开发中基准屏幕为1080*1920
, 其他的要适配480*800,720*1280,1440*2560
等
就可以写成java -jar autolayout.jar 1080 1920 480,800_720,1280_1080,1776_1080,1812_1440,1921_1440,2560
最后生成res文件夹
res
对于dimens文件这里我想说一下,其实一个文件夹下面一个dimens文件即可,没必要弄两个(lay_x和lay_y),删掉多余的文件夹以及文件夹里的y坐标
里面只保留
lay_x.xml
文件分析:
在适配过程中比较常见的问题是虚拟按键的问题,这里重点说一下:有些手机有虚拟按键,例如华为的很多手机都有,有些手机没有,有虚拟按键的手机在适配过程中会出现一些问题,下面以华为 honor V8为例说一下这个问题
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
import java.lang.reflect.Method;
/**
* @author lqx
*/
public class PxUtil {
/**
* @param context
* @return 获取屏幕原始尺寸高度,包括虚拟功能键高度
*/
public static String getTotalHeight(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics displayMetrics = new DisplayMetrics();
@SuppressWarnings("rawtypes")
Class c;
try {
c = Class.forName("android.view.Display");
@SuppressWarnings("unchecked")
Method method = c.getMethod("getRealMetrics", DisplayMetrics.class);
method.invoke(display, displayMetrics);
} catch (Exception e) {
e.printStackTrace();
}
return "包含虚拟按键的分辨率:"+displayMetrics.widthPixels+"x"+displayMetrics.heightPixels;
}
/**
* @param context
* @return 获取屏幕内容高度不包括虚拟按键
*/
public static String getScreenHeight(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
if (wm != null) {
wm.getDefaultDisplay().getMetrics(outMetrics);
}
return "不包含虚拟按键的分辨率:"+outMetrics.widthPixels+"x"+outMetrics.heightPixels;
}
}
通过这两个方法我们可以得到手机的分辨率高度和手机去除虚拟按键的高度,两者相减就是手机的虚拟按键的高度
调用后得到的结果是总高度 : 2560 内容高度 : 2408 虚拟按键 : 152
如果想要适配该机型,其实也很简单,只需要把原来的values-2560x1440文件夹复制一份重新名为values-2408x1440即可,在使用dimens适配过程中,若遇有类似虚拟按键问题可照此处理,亲测完美适配!
减少dimens文件除了删除文件夹中的Y坐标,还有一个方法:对于一个主流的分辨率只要留虚拟按键高度最高的那组dimens文件即可,什么意思呢?比如说1920x1080分辨率,有多款机型都是这个分辨率,只不过是虚拟按键高度不同,你可能需要创建1788x1080,1812x1080,1776x1080...
等多套dimens文件,其实大可不必,只需要1776x1080这一套就够了(保留去除虚拟按键之后最低的那一套),因为系统找不到对应尺寸的dimens文件,会使用比它略小的分辨率的dimens文件,如此一来我们的dimens文件会大大减少的。
心得
-
分辨率的访问顺序:
按宽高的变化,变化小的先访问。(乘积大的先访问) 如: -
720x1280
-
710x1280
-
700x1280
-
710x800
-
700x800
-
720x1280
-
720x1270
-
710x1280
权重和margin padding
当一个布局平分的时候尽量用权重,每个权重里面的小布局可以用margin和padding来设置,或者直接写死了大小,这些配置的数值不能太大,否则会有显示不好的结果.一般的在100px左右就可以
分析布局
打开Android Device Monitor
执行3步
AndroidDeviceMonitor.png
分析具体某个控件
小红点分析.png区域控件的各参数意义:
eg:(1)TextView[155,1772][212,1829]
对应意义:
(序号)控件名[左上角x坐标,左上角y坐标][右下角x坐标,右下角y坐标]
从而能得出这个控件的宽 = 212-155 = 57 高 = 1829-1772 = 57
依次类推
最后结合上面的像素适配那就是绝配的存在
谨记:控件写死的情况下尽量不要超过100像素,否则显示效果不佳
生成自己定制的jar
上面介绍的是能生成x和y的适配,我在开发中只用到了x所以接下来自己定制,由于项目中可能还会用到赋值的时候所以我会在代码中说明
直接上代码,其中内置了几种常用的分辨率
480,800;720,1280;1080,1920;1440,2560
其他的需要根据自己的需求添加,我设置的参考基准是1080*1920
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
/**
* @author lqx
*/
public class CreatValue {
/**
* 参考基准值的X作为标准
*/
private int baseW;
private int baseH;
private String dirStr = "./res";
/**
* 生成X方向的模板
*/
private final static String W_TEMPLATE = "<dimen name=\"x{0}\">{1}px</dimen>\n";
/**
* {height}:最后生成values-1920x1080中的1920
* {width}: 最后生成values-1920x1080中的1080
*/
private final static String VALUE_TEMPLATE = "values-{height}x{width}";
private static final String SUPPORT_DIMESION = "480,800;720,1280;1080,1920;1440,2560;";
private String supportStr = SUPPORT_DIMESION;
public CreatValue(int baseX, int baseY, String supportStr) {
this.baseW = baseX;
this.baseH = baseY;
if (!this.supportStr.contains(baseX + "," + baseY)) {
this.supportStr += baseX + "," + baseY + ";";
}
this.supportStr += validateInput(supportStr);
System.out.println(supportStr);
File dir = new File(dirStr);
if (!dir.exists()) {
dir.mkdir();
}
System.out.println(dir.getAbsoluteFile());
}
/**
* @param supportStr w,h_...w,h;
*/
private String validateInput(String supportStr) {
StringBuffer sb = new StringBuffer();
String[] vals = supportStr.split("_");
int w = -1;
int h = -1;
String[] wh;
for (String val : vals) {
try {
if (val == null || val.trim().length() == 0){
continue;
}
wh = val.split(",");
w = Integer.parseInt(wh[0]);
h = Integer.parseInt(wh[1]);
} catch (Exception e) {
System.out.println("skip invalidate params : w,h = " + val);
continue;
}
sb.append(w + "," + h + ";");
}
return sb.toString();
}
public void generate() {
String[] vals = supportStr.split(";");
for (String val : vals) {
String[] wh = val.split(",");
generateXmlFile(Integer.parseInt(wh[0]), Integer.parseInt(wh[1]));
}
}
private void generateXmlFile(int w, int h) {
StringBuffer sbForWidth = new StringBuffer();
sbForWidth.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
sbForWidth.append("<resources>\n");
float cellw = w * 1.0f / baseW;
System.out.println("width : " + w + "," + baseW + "," + cellw);
for (int i = 0; i < baseW; i++) {
sbForWidth.append("\t"+W_TEMPLATE.replace("{0}", i + "").replace("{1}",
change(cellw * i) + ""));
}
sbForWidth.append("\t"+W_TEMPLATE.replace("{0}", baseW + "").replace("{1}",
w + ""));
/**-----------------------------开始----------------------------------------*/
//下面是生成负数部分,因为有时候会用到margin为负数
// for (int i = -1; i > -baseW; i--) {
// sbForWidth.append("\t"+W_TEMPLATE.replace("{0}", "f"+i*-1).replace("{1}",
// change(cellw * i) + ""));
// }
// sbForWidth.append("\t"+W_TEMPLATE.replace("{0}", "f"+baseW).replace("{1}",
// -w + ""));
//生成负数部分到这个结束
/**-----------------------------结束----------------------------------------*/
sbForWidth.append("</resources>");
File fileDir = new File(dirStr + File.separator
+ VALUE_TEMPLATE.replace("{height}", h + "")//
.replace("{width}", w + ""));
fileDir.mkdir();
File layxFile = new File(fileDir.getAbsolutePath(), "lay_x.xml");
try {
PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
pw.print(sbForWidth.toString());
pw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
private static float change(float a) {
int temp = (int) (a * 100);
return temp / 100f;
}
public static void main(String[] args) {
int baseW = 1080;
int baseH = 1920;
String addition = "";
try {
if (args.length >= 3) {
baseW = Integer.parseInt(args[0]);
baseH = Integer.parseInt(args[1]);
addition = args[2];
} else if (args.length >= 2) {
baseW = Integer.parseInt(args[0]);
baseH = Integer.parseInt(args[1]);
} else if (args.length >= 1) {
addition = args[0];
}
} catch (NumberFormatException e) {
System.err.println("right input params : java -jar xxx.jar width height w,h_w,h_..._w,h;");
e.printStackTrace();
System.exit(-1);
}
new CreatValue(baseW, baseH, addition).generate();
}
}
注意: 上面的类中有一段生成负值的代码 如果开发中有需要直接打开就可以
- 生成自己方便的jar
- 在空白文件夹中创建
类名.java
,也就是:CreatValue.java
,把上面的代码复制到文件中
- 用命令编译生成
CreatValue.class
文件,按住shift
+右击鼠标,找到命令行
或者`Powershell窗口
这样就说明程序没问题
- 在目录下新建一个
manifest.mf
文件,文件里写如下内容:
Main-Class: CreatValue
注意: CreatValue后面要加上一个换行才可以否则会出现错误
然后运行如下一行命令:
jar cvfm autolayout.jar manifest.mf CreatValue.class CreatValue.java
其中:autolayout.jar的名字随意就可以,之所以将CreatValue.java打包进jar是为了以后好修改程序
目录下会生成一个autolayout.jar,是个压缩文件。到此时Jar包已经生成,目录如下图:
其中META-INF文件夹下的内容如下:
MANIFEST.MF的内容如下:
image.png- 如何运行该Jar包呢?
1>如果基准值1080*1920
和需要生产的像素文件夹等不需要修改,直接双击就可以运行程序了,然后会在目录下产生一个res
文件夹
2>在命令行运行:
其中:需要设定的基准值是720*1280,还需要生成的像素文件夹有300*400,500*600
那么命令如下:
java -jar autolayout.jar 720 1280 300,400_500,600
输出结果:
PS E:\Users\Documents\生成不同分辨率的values> java -jar autolayout.jar 720 1280 300,400_500,600
300,400_500,600
E:\Users\Documents\生成不同分辨率的values\.\res
width : 480,720,0.6666667
width : 720,720,1.0
width : 1080,720,1.5
width : 1440,720,2.0
width : 300,720,0.41666666
width : 500,720,0.6944444
除了生成原来的内定分辨率还增加了300*400,500*600
,添加成功
到此就结束了,记着需要负值的时候就直接打开注释就可以
附:
jar命令的简介(可运行”jar”命令来查看各个参数用法):
-c 创建一个jar包
-t 显示jar中的内容列表
-x 解压jar包
-u 添加文件到jar包中
-f 指定jar包的文件名
-v 生成详细的报造,并输出至标准设备
-m 指定manifest.mf文件.(manifest.mf文件中可以对jar包及其中的内容作一些一设置)
-0 产生jar包时不对其中的内容进行压缩处理
-M 不产生所有文件的清单文件(Manifest.mf)。这个参数与忽略掉-m参数的设置
-i 为指定的jar文件创建索引文件
-C 表示转到相应的目录下执行jar命令,相当于cd到那个目录,然后不带-C执行jar命令
参考 Java程序在命令行下编译运行打Jar包
通过删除上面部分代码做成一个工具类,复制代码放到项目中,没次增删分辨率之后右击运行下main()就可以了
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
/**
* 生成不同dimension的工具类
* @author lqx
*/
public class CreatValue {
/**
* 参考基准值的X作为标准
*/
private int baseW;
/**
* 此处的Y值只是用来生存文件夹的
*/
private int baseH;
private String dirStr = "./res";
/**
* 生成X方向的模板
*/
private final static String W_TEMPLATE = "<dimen name=\"x{0}\">{1}px</dimen>\n";
/**
* {height}:最后生成values-1920x1080中的1920
* {width}: 最后生成values-1920x1080中的1080
*/
private final static String VALUE_TEMPLATE = "values-{height}x{width}";
private static final String SUPPORT_DIMESION = "480,800;720,1280;1080,1920;1440,2560";
private String supportStr = SUPPORT_DIMESION;
private CreatValue(int baseX, int baseY) {
this.baseW = baseX;
this.baseH = baseY;
if (!this.supportStr.contains(baseX + "," + baseY)) {
this.supportStr += baseX + "," + baseY + ";";
}
System.out.println(supportStr);
File dir = new File(dirStr);
if (!dir.exists()) {
dir.mkdir();
}
System.out.println(dir.getAbsoluteFile());
}
private void generate() {
String[] vals = supportStr.split(";");
for (String val : vals) {
String[] wh = val.split(",");
generateXmlFile(Integer.parseInt(wh[0]), Integer.parseInt(wh[1]));
}
}
private void generateXmlFile(int w, int h) {
StringBuffer sbForWidth = new StringBuffer();
sbForWidth.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
sbForWidth.append("<resources>\n");
float cellw = w * 1.0f / baseW;
System.out.println("width : " + w + "," + baseW + "," + cellw);
for (int i = 0; i < baseW; i++) {
sbForWidth.append("\t" + W_TEMPLATE.replace("{0}", i + "").replace("{1}",
change(cellw * i) + ""));
}
sbForWidth.append("\t" + W_TEMPLATE.replace("{0}", baseW + "").replace("{1}",
w + ""));
/**-----------------------------开始----------------------------------------*/
//下面是生成负数部分,因为有时候会用到margin为负数,需要的话直接打开注释就可以
// for (int i = -1; i > -baseW; i--) {
// sbForWidth.append("\t" + W_TEMPLATE.replace("{0}", "f" + i * -1).replace("{1}",
// change(cellw * i) + ""));
// }
// sbForWidth.append("\t" + W_TEMPLATE.replace("{0}", "f" + baseW).replace("{1}",
// -w + ""));
//生成负数部分到这个结束
/**-----------------------------结束----------------------------------------*/
sbForWidth.append("</resources>");
File fileDir = new File(dirStr + File.separator
+ VALUE_TEMPLATE.replace("{height}", h + "")//
.replace("{width}", w + ""));
fileDir.mkdir();
File layxFile = new File(fileDir.getAbsolutePath(), "lay_x.xml");
try {
PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
pw.print(sbForWidth.toString());
pw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
private static float change(float a) {
int temp = (int) (a * 100);
return temp / 100f;
}
/**
* CreatValue方法的参数是基准值
* @param args
*/
public static void main(String[] args) {
new CreatValue(1080, 1920).generate();
}
}