前言:做Android已经一段时间了,可是当别人问到我Android中的屏幕适配的时候,感觉自己有一种似懂非懂的感觉,这就有点尴尬了~~哈哈!还有就是ui跑过来问你要切什么样子的图的时候,总要解释半天,让别人感觉你好不专业啊,所以为了更好的理解android的屏幕适配,还是打算写一篇博客来总结一下,就当笔记了哈~~也欢迎大牛来指点指点,拜谢啦~~
本文我将结合android官方说明跟一些大牛的博客进行一些总结性的说明….
各单位之间的转换,大家可以参考android api中的TypeValue类的applyDimension方法:
/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters unit and value
* are as in {@link #TYPE_DIMENSION}.
*
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion --
* supplies display density and scaling information.
*
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
可以根据公式反推dp转换成px之类的公式。
介绍完单位后,我们开看看怎么获取android中的密度系数(density)、和屏幕密度(densityDpi)以及其它的一些参数:
(我这里使用的是三星galaxy s6做的测试)
WindowManager wm= (WindowManager) getSystemService(WINDOW_SERVICE);
DisplayMetrics outMtrics=new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMtrics);
int widthPixels=outMtrics.widthPixels;
int heightPixels = outMtrics.heightPixels;
float xdpi = outMtrics.xdpi;
float ydpi = outMtrics.ydpi;
float density = outMtrics.density;
int densityDpi = outMtrics.densityDpi;
float scaledDensity = outMtrics.scaledDensity;
StringBuilder sb=new StringBuilder();
sb.append("widthPixels:").append(widthPixels).append("\n").
append("heightPixels:").append(heightPixels).append("\n").
append("xdpi:").append(xdpi).append("\n").
append("ydpi:").append(ydpi).append("\n").
append("densityDpi:").append(densityDpi).append("\n").
append("scaledDensity:").append(scaledDensity).append("\n").
append("size:").append((Math.sqrt(Math.pow(widthPixels,2)+Math.pow(heightPixels,2)))/densityDpi).append("\n").
append("density:").append(density).append("\n");
Log.e("TAG",sb.toString());
打印结果:
03-24 02:41:58.024 4746-4746/com.yasin.annotationdemo E/TAG: widthPixels:1440//屏幕宽度(px)
heightPixels:2560//屏幕的高度(px)
xdpi:640.0 x轴每英寸所占的像素大小
ydpi:640.0
densityDpi:640//屏幕的密度大小(以每英寸所占的像素大小)
scaledDensity:4.0(文字密度缩放系数)
size:4.589389937671455 (根据屏幕宽高算出的手机大小,单位为英寸)
density:4.0(手机密度系数)
根据打印的这些参数,唯一让我们疑惑的是s6明明是5.1英寸的屏幕,怎么算出来的值为4.58英寸呢? 我们要知道手机厂商说的5.1寸可能包含了一些其他的成份在里面(屏幕的边框等因素),所以大家也不必要纠结这个值。
如果你硬是要获取一个离手机物理尺寸最接近的值,你可以用另外一个方法(包含了手机的statubar跟menubar):
private void getRealSize() {
WindowManager w = this.getWindowManager();
Display d = w.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
d.getMetrics(metrics);
// since SDK_INT = 1;
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels;
// includes window decorations (statusbar bar/menu bar)
if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 17)
try {
widthPixels = (Integer) Display.class.getMethod("getRawWidth").invoke(d);
heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(d);
} catch (Exception ignored) {
}
// includes window decorations (statusbar bar/menu bar)
if (Build.VERSION.SDK_INT >= 17)
try {
Point realSize = new Point();
Display.class.getMethod("getRealSize", Point.class).invoke(d, realSize);
widthPixels = realSize.x;
heightPixels = realSize.y;
} catch (Exception ignored) {
}
double size=(Math.sqrt(Math.pow(widthPixels, 2) + Math.pow(heightPixels, 2))) / metrics.densityDpi;
Log.e("TAG", "size"+size);
}
注意了我们这里说的手机多少寸是说的对角线的长度:
double size=(Math.sqrt(Math.pow(widthPixels, 2) + Math.pow(heightPixels, 2))) / metrics.densityDpi;
所以下次别人告诉你手机分辨率然后告诉你密度,问你我的手机是多寸,或者告诉你手机多少寸,问你手机的密度是多少的时候,可千万不要说不知道哦~~!!
好啦~看了一堆单位、系数,下面进入我们今天的主题(Android屏幕适配)…
Android 可在各种具有不同屏幕尺寸和密度的设备上运行。对于应用,Android 系统在不同设备中提供一致的开发环境,可以处理大多数工作,将每个应用的用户界面调整为适应其显示的屏幕。 同时,系统提供 API,可用于控制应用适用于特定屏幕尺寸和密度的 UI,以针对不同屏幕配置优化 UI 设计。 例如,您可能想要不同于手机 UI 的平板电脑 UI。
术语和概念
屏幕尺寸:
按屏幕对角测量的实际物理尺寸。
为简便起见,Android 将所有实际屏幕尺寸分组为四种通用尺寸:小、 正常、大和超大。
屏幕密度:
屏幕物理区域中的像素量;通常称为 dpi(每英寸 点数)。例如, 与“正常”或“高”密度屏幕相比,“低”密度屏幕在给定物理区域的像素较少。
为简便起见,Android 将所有屏幕密度分组为六种通用密度: 低、中、高、超高、超超高和超超超高。
方向:
从用户视角看屏幕的方向,即横屏还是 竖屏,分别表示屏幕的纵横比是宽还是高。请注意, 不仅不同的设备默认以不同的方向操作,而且 方向在运行时可随着用户旋转设备而改变。
分辨率:
屏幕上物理像素的总数。添加对多种屏幕的支持时, 应用不会直接使用分辨率;而只应关注通用尺寸和密度组指定的屏幕 尺寸及密度。
密度无关像素 (dp):
在定义 UI 布局时应使用的虚拟像素单位,用于以密度无关方式表示布局维度 或位置。
密度无关像素等于 160 dpi 屏幕上的一个物理像素,这是 系统为“中”密度屏幕假设的基线密度。在运行时,系统 根据使用中屏幕的实际密度按需要以透明方式处理 dp 单位的任何缩放 。dp 单位转换为屏幕像素很简单: px = dp * (dpi / 160)。 例如,在 240 dpi 屏幕上,1 dp 等于 1.5 物理像素。在定义应用的 UI 时应始终使用 dp 单位 ,以确保在不同密度的屏幕上正常显示 UI。
1、为简化您为多种屏幕设计用户界面的方式,Android 将实际屏幕尺寸和密度的范围 分为:
四种通用尺寸:小、正常、 大 和超大
超大屏幕至少为 960dp x 720dp
大屏幕至少为 640dp x 480dp
正常屏幕至少为 470dp x 320dp
小屏幕至少为 426dp x 320dp
注意: 这些最小屏幕尺寸在 Android 3.0 之前未正确定义,因此某些设备在正常屏幕与大屏幕之间变换时可能会出现分类错误的情况。 这些尺寸还基于屏幕的物理分辨率,因此设备之间可能不同 — 例如,具有系统状态栏的 1024x720 平板电脑因系统状态栏要占用空间,所以可供 应用使用的空间要小一点。
以我们的三星Galaxy s6(2560*1440)手机来说,我们的手机密度系数前面打印为:
density:4.0
所以转换成dp的话为:640dp*360dp(忽略一些其它因素的话应该是属于大屏幕的范围)。
2、六种通用的密度:
那么如何支持多种屏幕?
https://developer.android.google.cn/guide/practices/screens_support.html
大家直接上官网文档查看哈~~我这里就不重复啦~~~~~大致就是在res下建立各种分辨率的layout文件夹,从而来适配不同的屏幕。
但是在我们正常的项目中是只支持一些常用的手机都差不多啦,因为如果要适配平板跟穿戴设备的话,一般都是从新设计一套ui,不可能跟手机共用一套ui的,所以此时小伙伴就可以根据前面文章所写的内容跟官方文档说明进行适配了。
那么对于普通项目中的一套ui,我们该怎么进行屏幕适配呢?
官方文档中同样也为我们平时的书写提了一些建议:
很久很久以前你可能就已经这么做了,我就不啰嗦了,重点说一下(为不同屏幕密度提供替代位图可绘制对象)。
同样,面试的时候肯定你也会这么说:“要ui切几套不同的图片,放在相应的drawable目录中。“是的~确实是这样的,但是我们又应该怎么放呢?
其实吧,官方都为我们设计好了,比如我们新建一个android项目,as自动为什么生成了如下文件夹:
比如:我们此时的ui设计标准为mdpi,我们设计的icon_launcher的尺寸为48*48,那么其它文件夹中摆放的图片尺寸应该是为:
小伙伴不相信的话,可以自己打开文件看看哦~~
好啦!如果这个时候ui跑过来问你:“小伙子,我需要提供给你多少尺寸的图片呢?“。
你可以问:“你的ui设计标准是多少呢?“
ui可能这样回答:“我用的是iphone6为基准的。“
~~于是你屁颠屁颠的去网上一查(iphone6的尺寸为:1334×750、4.7英寸,根据前面我们提供的公式可以算出密度为:325dpi,可以判断是属于xxdpi这个范围的)~~
所以我需要以iphone6手机为基准去切图,比如在iphone6一个icon的大小为144*144,那么其它的文件夹相应icon为:
192x192 (4.0x) 用于超超超高密度
其实吧,我们也用不到这么多icon,做到hdpi就差不多了,至少我们现在的项目是这么做的。
好啦~介绍完切图,我们来验证一下我们的程序是不是这么加载的。
以我们的测试机(三星s6为例子):
我们直接加载我们项目中的ic_launcher.png文件,然后打印它的大小:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="com.yasin.annotationdemo.MainActivity"
android:orientation="vertical"
>
<TextView
android:id="@+id/id_tv_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<ImageView
android:id="@+id/id_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
/>
LinearLayout>
mImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mImageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mTextView2.setText("width: " + mImageView.getWidth() + " height: " + mImageView.getHeight());
}
});
我们让textview打印imageview的大小:
忽略其它的信息哈,我们看到程序显示为192*192的图片。
于是我们看看xxxdpi中的图片看是不是这个尺寸:
可以看到,确实是加载的这个图片,我们复制一个尺寸为192*192的图片到xxdpi文件夹中,然后改名为ic_launcher1.png,让imageview去加载,然后看到打印为:
从原图192*192变为了(256*256)扩大了1.3333倍,我们现在的手机密度为640,而xxdpi中对应的是480,所以640/480=1.3333,刚好是我们的缩放比例。
我们再次移动至xdpi文件夹(320dpi)中,现实的宽、高应该也是按照640/320=2,所以显示的大小应该为:384*384,我们来运行下我们的代码:
好啦~ 最后我们新建一个文件夹mipmap,我们直接放在默认图片文件夹中,图片大小照样为192*192,然后运行代码:
可以看到是以640/160=4倍的比例缩放的,所以192*4=768。
我们看看官方说的系统加载drawable文件的说明:
根据当前屏幕的密度,系统将使用您的应用中提供的任何尺寸或 密度特定资源,并且不加缩放而显示它们。如果没有可用于正确密度 的资源,系统将加载默认资源,并按需要向上或向下扩展,以 匹配当前屏幕的密度。系统假设默认资源( 没有配置限定符的目录中的资源)针对基线屏幕密度 (mdpi) 而设计, 除非它们加载自密度特定的资源目录。因此,系统 会执行预缩放,以将位图调整至适应当前屏幕 密度的大小。
如果您请求预缩放的资源的尺寸,系统将返回 代表缩放后尺寸的值。例如,针对 mdpi 屏幕以 50x50 像素 设计的位图在 hdpi 屏幕上将扩展至 75x75 像素(如果没有 用于 hdpi 的备用资源),并且系统会这样报告大小。
那么,我们是不是可以不让系统进行缩放,加载原图呢??可以!
然后运行代码:
系统没有进行任何缩放,原图加载的。。
知道了系统加载图片的规则后,但是有的时候有些公司为了控制apk的大小,一般都只放一套图,根据现在主流的手机的话大多都位于xx-dpi这个范围中,xxxdpi的手机也占大部分,但是大多数的人还是用不起4k屏的手机,哈哈哈~~ 所以目前来说,设计一套图的话,最好是放在xx-dpi文件夹中,这样就算我们的手机密度小于xxdpi中的密度,也只是会进行一个缩小的操作,图片不会失真,内存占用也不会太大,毕竟大多数进行的是缩小操作,至于那种4k手机是吧,就不考虑这么一点点性能问题啦~~~
好啦~~说完怎么摆放我们的图片后,我们终于是要讲咋基于一套ui进行手机适配了。。
一、res文件夹中建很多values文件,对应不同的屏幕尺寸
什么意思呢?很简单!!
比如我们ui设计的基准是750*1334(iphone6)我们有一张图片的大小为100px*100px,那么我们在1440*2560(三星s6)中显示的即为:192px*192px,按照一个比例进行缩放大小。所以我们的value文件夹为:
我们来看一看我们的效果图:
首先创建一个布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width=@dimen/x375
android:layout_height=@dimen/x375
android:src="#ff00"/>
LinearLayout>
宽高都为ui设计稿上标记的那样,直接设为屏幕的一半:
可以看到,两台模拟器上完美适配了~~~~
当然,我们不可能手动的建这么多文件夹,我就直接贴上大神写的java代码了:
GenerateValueFiles.java
package com.yasin.annotationdemo;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
public class GenerateValueFiles {
private int baseW;
private int baseH;
private String dirStr = "./res";
private final static String WTemplate = "{1}px \n";
private final static String HTemplate = "{1}px \n";
/**
* {0}-HEIGHT
*/
private final static String VALUE_TEMPLATE = "values-{0}x{1}";
private static final String SUPPORT_DIMESION = "320,480;480,800;480,854;540,960;600,1024;720,1184;720,1196;720,1280;768,1024;800,1280;1080,1812;1080,1920;1440,2560;";
private String supportStr = SUPPORT_DIMESION;
public GenerateValueFiles(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;
* @return
*/
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("\n");
sbForWidth.append("" );
float cellw = w * 1.0f / baseW;
System.out.println("width : " + w + "," + baseW + "," + cellw);
for (int i = 1; i < baseW; i++) {
sbForWidth.append(WTemplate.replace("{0}", i + "").replace("{1}",
change(cellw * i) + ""));
}
sbForWidth.append(WTemplate.replace("{0}", baseW + "").replace("{1}",
w + ""));
sbForWidth.append("");
StringBuffer sbForHeight = new StringBuffer();
sbForHeight.append("\n");
sbForHeight.append("" );
float cellh = h *1.0f/ baseH;
System.out.println("height : "+ h + "," + baseH + "," + cellh);
for (int i = 1; i < baseH; i++) {
sbForHeight.append(HTemplate.replace("{0}", i + "").replace("{1}",
change(cellh * i) + ""));
}
sbForHeight.append(HTemplate.replace("{0}", baseH + "").replace("{1}",
h + ""));
sbForHeight.append("");
File fileDir = new File(dirStr + File.separator
+ VALUE_TEMPLATE.replace("{0}", h + "")//
.replace("{1}", w + ""));
fileDir.mkdir();
File layxFile = new File(fileDir.getAbsolutePath(), "lay_x.xml");
File layyFile = new File(fileDir.getAbsolutePath(), "lay_y.xml");
try {
PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
pw.print(sbForWidth.toString());
pw.close();
pw = new PrintWriter(new FileOutputStream(layyFile));
pw.print(sbForHeight.toString());
pw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static float change(float a) {
int temp = (int) (a * 100);
return temp / 100f;
}
public static void main(String[] args) {
int baseW = 750;
int baseH = 1334;
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 GenerateValueFiles(baseW, baseH, addition).generate();
}
}
二、使用百分比布局进行屏幕适配
百分比布局的内容大家可以参考我的另外一篇博客:
http://blog.csdn.net/vv_bug/article/details/54958200
好啦~~!! 我们来总结下两种适配的优缺点:
1、value文件适配
优点:
缺点:
2、百分比布局适配
优点:
缺点:
所以综上所述,个人觉得value文件夹方式适配可能更符合我们实际项目开发。
小伙伴如果还有其它的适配方式,还记得分享一下哈~~拜谢啦!!
文章引用:
https://developer.android.google.cn/guide/practices/screens_support.html
http://blog.csdn.net/guolin_blog/article/details/50727753
http://blog.csdn.net/lmj623565791/article/details/45460089