一、概念
DPI名称(范围值) | density密度(1dp显示多少px) | 常见分辨率 |
ldpi ≤ 120dpi | 0.75px = 120dpi / 160 | QVGA = 240*320 |
mdpi ≤ 160dpi(基准) | 1px = 160dpi / 160 | HVGA = 320*480 |
hdpi ≤ 240dpi | 1.5px = 240dpi / 160 | WVGA = 480*800 FWVGA = 480*854 |
xhdpi ≤ 320dpi | 2px = 320dpi / 160 | 720P = 720*1280 |
xxhdpi ≤ 480dpi | 3px = 480dpi / 160 | 1080P = 1080*1920 |
xxxhdpi ≤ 640dpi | 4px = 640dpi / 160 | 4K = 2460*3840 |
1.1 屏幕像素密度 PPI、DPI
PPI(Pixels Per Inch)屏幕每英寸容纳多少个像素点,DPI(Dots Per Inch)这个“点”是根据屏幕物理概念产生的一个软件概念,在不同行业有不同理解,印刷行业每英寸打印多少个墨点,鼠标移动一英寸光标移动多少像素点,在Android中被用来表示屏幕每英寸显示多少个像素点,概念上可以当作没有区别。
由于不同设备的屏幕尺寸和分辨率各不相同且毫无规律,导致 DPI 的值非常混乱,从而影响 dp 适配。


1.2 密度无关像素 dp
density-independent pixel,是 Android 特有的单位,只与DPI有关,保证了在不同屏幕像素密度的设备上显示相同的效果。以DPI=160为基准1dp=1px,用设备实际DPI值/160=density密度,即1dp=多少px。用屏幕宽或高的px/density=屏幕宽或高最大dp长度。
-
dp = px / density
-
density = dpi / 160
-
dpi = √(宽² + 高²) / 屏幕尺寸
1.3 独立比例像素 sp
scale-independent pixel,是 Android 特有的单位,用于设置字体大小。
二、AndroidStudio目录说明
Android会根据手机屏幕分辨率选择对应文件夹中的资源,如果没有对应的文件夹,则从最高分辨率的文件夹依次往低处寻找,找到高分辨率的就压缩后显示,找到低分辨率的就放大显示。
mipmap只用来放桌面应用图标(Manifest中application标签配置的icon项),drawable用来存放图片资源,values用来针对不同分辨力编写对应资源文件。

三、品牌机型分辨率
分辨率 | 小米 | 红米 | Pixel | 三星 | OPPO | VIVO | 荣耀 | 一加 | IQOO | |
720P | 720*1280 | |||||||||
720*1520 | 7/8 | |||||||||
720*1600 | 9A | |||||||||
720*1650 | 12C | |||||||||
1080P | 1080*1920 | 5/6 | 1/2 | |||||||
1080*2160 | Mix2 | 3 | ||||||||
1080*2220 | 4 | |||||||||
1080*2340 | 9/10、Mix3 | 9、K20P、Note9 | 5/6/7 | S22/23 | X60 | 7 | ||||
1080*2376 | X50/70 | |||||||||
1080*2388 | Z7 | |||||||||
1080*2400 | 12/13、Mix4、CV1/2 | 10、K30P/40P、Note10/11/12 | S21 | X5、Reno8 | X80、S9/10/12/15/16 | 60/70/80 | ||||
1080*2412 | Reno9 | Ace1 | ||||||||
1.5K | 1220*2712 | K50至尊版/K60至尊版 | X90 | Ace2 | ||||||
1240*2772 | X6 | |||||||||
1260*2800 | ||||||||||
2K | 1440*2560 | |||||||||
1440*2960 | S8/9 | |||||||||
1440*3040 | S10 | |||||||||
1440*3200 | 11/13P/14P | K50P/K60/K60P | S20 | 11 | ||||||
1440*3216 | 9P/10P/11 |
四、屏幕适配
市面上的机型,不同分辨率*不同屏幕尺寸=无限多种DPI组合。当美工给的图以px为单位使用4.1的dimen适配、以dp为单位使用4.2的字节跳动density适配、更推荐的是smallestWidth限定符适配。
dimen 宽高适配 | 通过穷举市面上所有 Android 手机的屏幕像素尺寸来实现适配,通过比例换算来为不同分辨率的屏幕分别准备一套 dimens 文件,应用在运行时再去引用和当前设备完全匹配的 dimens 文件,以此来实现屏幕适配。 | 优点:? |
缺点:缺点是容错率低,只有精准匹配分辨率才能适配,否则只能引用默认dimens文件夹,显示效果就会有很大出入。 | ||
smallestWidth 最小宽度限定 | 根据屏幕最短的那个边(不考虑屏幕方向)适配。适配原理和宽高限定一样,也是通过比例换算来为不同尺寸屏幕分别准备一套 dimens 文件,应用在运行时再去引用和当前设备最匹配的那个。 | 优点:容错率高,没有找到对应dimens文件会向下寻找接近的。在 320 ~ 460 dp 之间每 10 dp 就提供一套 dimens 文件就足够使用了,想要囊括更多设备的话也可以再缩短步长,基本不用担心最终效果会与设计稿偏差太多,且不会影响到三方库。 |
缺点:需要生成多套 dimens 文件,增大了 apk 体积。 | ||
density 字节跳动密度适配 | 基于系统将 dp 转换为 px 的公式 px = dp * density 来实现适配,通过在运行时动态修改 density 值的大小,使得修改后计算出的屏幕宽度就等于设计稿的宽度,从而使得在不同屏幕尺寸下我们都可以直接使用设计稿给出的 dp 值,且无需准备多套 dimens 文件。 | 优点:可以直接使用设计稿中的 dp 值,无需准备多套 dimens 文件进行映射,因此不会增大 apk 体积,且在三种方案中 UI 还原度最高,其它两种方案都需要精准命中屏幕尺寸后才能达到此方案的还原度。 |
缺点:由于此方案会影响到应用全局,对于已经迭代了很久的项目来说,中途引入此方案大概率会影响到现有的适配方案;即使是新项目,又需要考虑到此方案对于三方库的影响,不能由于主项目的变动导致三方库自身界面变形。 |
4.1 dimen宽高适配


public class DimenUtils {
private final static String rootPath = "C:/Users/Administrator/Desktop/layoutroot/values-{0}x{1}/"; //注意将Administrator替换为实际用户名
private final static float dw = 300f; //屏幕横向分为多少等份
private final static float dh = 500f; //屏幕纵向分为多少等份
private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n"; //xml中横向条目的内容:<dimen name="x1">2.4px</dimen>
private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n"; //xml中纵向条目的内容:<dimen name="y1">2.56px</dimen>
public static void main(String[] args) { //AndroidStudio运行失败的话选择 Run 'DimenUtils.main()' with Coverage
//720P
makeString(720, 1280);
// makeString(720, 1520);
// makeString(720, 1600);
// makeString(720, 1650);
//1080P
// makeString(1080, 1920);
// makeString(1080, 2160);
// makeString(1080, 2220);
makeString(1080, 2340);
// makeString(1080, 2376);
makeString(1080, 2400);
// makeString(1080, 2412);
//1.5K
// makeString(1220, 2712);
// makeString(1240, 2772);
// makeString(1260, 2800);
//2K
// makeString(1440, 2560);
// makeString(1440, 2960);
// makeString(1440, 3040);
makeString(1440, 3200);
}
public static void makeString(int w, int h) {
StringBuffer sb = new StringBuffer();
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
sb.append("<resources>");
float cellw = w / dw;
for (int i = 1; i < dw; i++) {
sb.append(WTemplate.replace("{0}", i + "").replace("{1}",
change(cellw * i) + ""));
}
sb.append(WTemplate.replace("{0}", "300").replace("{1}", w + ""));
sb.append("</resources>");
StringBuffer sb2 = new StringBuffer();
sb2.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
sb2.append("<resources>");
float cellh = h / dh;
for (int i = 1; i < dh; i++) {
sb2.append(HTemplate.replace("{0}", i + "").replace("{1}",
change(cellh * i) + ""));
}
sb2.append(HTemplate.replace("{0}", "500").replace("{1}", h + ""));
sb2.append("</resources>");
String path = rootPath.replace("{0}", h + "").replace("{1}", w + "");
File rootFile = new File(path);
if (!rootFile.exists()) {
rootFile.mkdirs();
}
File layxFile = new File(path + "lay_x.xml");
File layyFile = new File(path + "lay_y.xml");
try {
PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
pw.print(sb.toString());
pw.close();
pw = new PrintWriter(new FileOutputStream(layyFile));
pw.print(sb2.toString());
pw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static float change(float a) {
int temp = (int) (a * 100);
return temp / 100f;
}
}
//将生成的文件夹直接拖进 AndroidStudio 中的 res 中,使用的时候就 @dimen/ 后跟方向和份数。
<TextView
android:layout_width="@dimen/x100"
android:layout_height="@dimen/y250"/>
4.2 smallestWidth 限定符适配
大部分手机的宽度dp值集中在320-450之间,大部分1080P的手机应该都是360dp,390dp,411dp。可以在这个基础上,参考Android studio中的Virtual Device Configuration。
public class JavaTest {
private static final int DESIGN_WIDTH = 375; //设计稿宽度(将自己设计师的设计稿的宽度填入)
private static final int DESIGN_HEIGHT = 667; //设计稿高度(将自己设计师的设计稿的高度填入)
public static void main(String[] args) {
int smallest = DESIGN_WIDTH > DESIGN_HEIGHT ? DESIGN_HEIGHT : DESIGN_WIDTH; // 求得最小宽度
DimenTypes[] values = DimenTypes.values();
for (DimenTypes value : values) {
File file = new File("");
MakeUtils.makeAll(smallest, value, file.getAbsolutePath());
}
}
private enum DimenTypes {
//适配Android3.2+,大部分手机的sw值集中在300~460,想生成多少自己以此类推
DP_sw__300(300), // values-sw300
DP_sw__310(310),
DP_sw__320(320),
DP_sw__330(330),
DP_sw__340(340),
DP_sw__350(350),
DP_sw__360(360),
DP_sw__370(370),
DP_sw__380(380),
DP_sw__390(390),
DP_sw__400(400),
DP_sw__410(410),
DP_sw__420(420),
DP_sw__430(430),
DP_sw__440(440),
DP_sw__450(450),
DP_sw__460(460),
DP_sw__470(470),
DP_sw__480(480),
DP_sw__490(490);
private int swWidthDp;
DimenTypes(int swWidthDp) {
this.swWidthDp = swWidthDp;
}
public int getSwWidthDp() {
return swWidthDp;
}
public void setSwWidthDp(int swWidthDp) {
this.swWidthDp = swWidthDp;
}
}
private static class MakeUtils {
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
private static final String XML_RESOURCE_START = "<resources>\r\n";
private static final String XML_RESOURCE_END = "</resources>\r\n";
private static final String XML_DIMEN_TEMPLETE = "<dimen name=\"qb_%1$spx_%2$d\">%3$.2fdp</dimen>\r\n";
private static final String XML_BASE_DPI = "<dimen name=\"base_dpi\">%ddp</dimen>\r\n";
private static final int MAX_SIZE = 720;
private static final String XML_NAME = "dimens.xml"; //生成的文件名
public static float px2dip(float pxValue, int sw, int designWidth) {
float dpValue = (pxValue / (float) designWidth) * sw;
BigDecimal bigDecimal = new BigDecimal(dpValue);
float finDp = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).floatValue();
return finDp;
}
/**
* 生成所有的尺寸数据
*/
private static String makeAllDimens(DimenTypes type, int designWidth) {
float dpValue;
String temp;
StringBuilder sb = new StringBuilder();
try {
sb.append(XML_HEADER);
sb.append(XML_RESOURCE_START);
//备份生成的相关信息
temp = String.format(XML_BASE_DPI, type.getSwWidthDp());
sb.append(temp);
for (int i = 0; i <= MAX_SIZE; i++) {
dpValue = px2dip((float) i, type.getSwWidthDp(), designWidth);
temp = String.format(XML_DIMEN_TEMPLETE, "", i, dpValue);
sb.append(temp);
}
sb.append(XML_RESOURCE_END);
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}
/**
* 生成的目标文件夹,只需传宽进来就行
* @param type 枚举类型
* @param buildDir 生成的目标文件夹
*/
public static void makeAll(int designWidth, DimenTypes type, String buildDir) {
try {
//生成规则
final String folderName;
if (type.getSwWidthDp() > 0) {
//适配Android 3.2+
folderName = "values-sw" + type.getSwWidthDp() + "dp";
} else {
return;
}
//生成目标目录
File file = new File(buildDir + File.separator + folderName);
if (!file.exists()) {
file.mkdirs();
}
//生成values文件
FileOutputStream fos = new FileOutputStream(file.getAbsolutePath() + File.separator + XML_NAME);
fos.write(makeAllDimens(type, designWidth).getBytes());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4.3 density 字节跳动密度适配
4.3.1 问题原因
由于不同设备的屏幕尺寸和分辨率各不相同且毫无规律,导致 DPI 的值非常混乱从而影响 dp 适配。例如 UI 图按照屏幕宽度为 360dp 设计的,那么在一个 5 寸 1080*1920 的设备上屏幕的宽度为 392.7dp 是比设计图要宽的,这种情况下使用 dp 也无法统一显示效果。
4.3.2 解决思路
通常情况下只需要以宽或高一个维度去适配,如界面是上下滑动的只需要保证所有设备宽度显示一致,又如界面不支持滑动则需要保证所有设备高度显示一致(避免有的设备显示不全)。考虑现在基本都是以 dp 为单位,从公式 dp = px / density 中可以看出,如果 UI 图宽度为 360dp 想要保证所有设备计算得出的 px 正好是屏幕宽度上的像素点的话,只能通过修改 density 的值。
源码中得知 dp 和 px 的转换都是通过 DisplayMetric 来计算的,density 是 DisplayMetric 中的成员变量,该类的实例通过 Resource.getDisplayMetrics() 获得,而 Resource 又是通过 Context 获得的。其它成员变量 densityDpi 对应 dpi,scaledDensity 对应字体缩放因子(正常情况下等于 density,调节系统字体大小后会改变该值)。
4.3.3 代码(Kotlin)
object DensityUtils {
private var noncompatDensity = 0F
private var noncompatScaledDensity = 0F
private fun setCustomDensity(activity: Activity, application: Application) {
val appDisplayMetrics = application.resources.displayMetrics
if (noncompatDensity == 0F) {
noncompatDensity = appDisplayMetrics.density
noncompatScaledDensity = appDisplayMetrics.scaledDensity
application.registerComponentCallbacks(object : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {
if (newConfig.fontScale > 0 ) {
noncompatScaledDensity = application.resources.displayMetrics.scaledDensity
}
}
override fun onLowMemory() {}
})
}
val targetDensity = appDisplayMetrics.widthPixels / 360F
val targetScaledDensity = targetDensity * (noncompatScaledDensity / noncompatDensity)
val targetDensityDpi = (targetDensity * 160).toInt()
appDisplayMetrics.density = targetDensity
appDisplayMetrics.scaledDensity = targetScaledDensity
appDisplayMetrics.densityDpi = targetDensityDpi
val activityDisplayMetrics = activity.resources.displayMetrics
activityDisplayMetrics.density = targetDensity
activityDisplayMetrics.scaledDensity = targetScaledDensity
activityDisplayMetrics.densityDpi = targetDensityDpi
}
}
4.3.4 代码(Java)
public class DensityUtils {
private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;
private static void setCustomDensity(@NonNull Activity activity, @NonNull Application application) {
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sNoncompatDensity == 0) {
sNoncompatDensity = appDisplayMetrics.density;
sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {}
});
}
final float targetDensity = appDisplayMetrics.widthPixels / 360F;
final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
final int targetDensityDpi = (int)(targetDensity * 160);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
}