在Android开发中,由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,适配成本越来越高。
先来介绍下基本概念
1、分辨率:
分辨率就是手机屏幕的像素点数,“宽×高”,安卓手机屏幕常见的分辨率有480×800、720×1280、1080×1920等。
2、屏幕大小:
屏幕大小是手机对角线的物理尺寸,以英寸(inch)为单位。比如5寸手机,对角线的尺寸:5寸×2.54厘米/寸=12.7厘米。
3、px(Pixel):
设备真实像素,同一像素值在不同分辨率的手机上展示不同,分辨率越大,展示区域越小
4、dpi(Dots Per Inch):
每英寸面积内的像素点数,计算方式如下。
比如我的手机,华为mate9: dpi = √﹙1080 * 1080 + 1920 * 1920﹚/ 5.9 = 373
比如 Google Pixel2 : dpi = √﹙1080 * 1080 + 1920 * 1920﹚/ 5.0 = 440
4.5寸手机:dpi = √﹙1080 * 1080 + 1920 * 1920﹚/ 4.5 = 440
5、density:
屏幕密度,density = dpi/160
6、dp(Device Independent Pixels):
设备独立像素,每英寸160px的设备上,1dp = 1px;以此类推,每英寸320px的设备上,1dp = 2px。
dp = px / density
dp = px * (160 / dpi) => 1dp = 2px * ( 160 / 320)
7、dp和px转换关系
- px = density * dp;
- density = dpi / 160;
- px = (dpi / 160) * dp;
系统屏幕密度:
- ldpi文件夹下对应的密度为120dpi,对应的分辨率为240*320
- mdpi文件夹下对应的密度为160dpi,对应的分辨率为320*480
- hdpi文件夹下对应的密度为240dpi,对应的分辨率为480*800
- xhdpi文件夹下对应的密度为320dpi,对应的分辨率为720*1280
- xxhdpi文件夹下对应的密度为480dpi,对应的分辨率为1080*1920
通过dp加上自适应布局和weight比例布局可以基本解决不同手机上适配的问题,这基本是最原始的Android适配方案。
我原以为这样就够了,但是这只能保证我们写出来的界面适配绝大部分手机,部分手机仍然需要单独适配,为什么dp只解决了90%的适配问题?因为并不是所有的1080P的手机dpi都是160的整数倍,比如Google 的Pixel2(1920 * 1080)的dpi是420,也就是说,在Pixel2中,1dp=2.625px,这样会导致相同分辨率的手机中,一个100dp * 100dp的控件,在一般的1080P手机上(dpi是480)展示是300px,而Pixel 2 中只有262.5px,这样控件的实际大小会有所不同。
图一是1080P、480dpi的手机,图二是1080P、420dpi的手机,设置图片宽度为360dp
图一占据px: 360dp * ( 480dpi / 160) = 1080 px 刚好满屏
图二占据px: 360dp * ( 420dpi / 160) = 945 px 未满屏
通过DisplayMetrix来转换dp、px
/**
* dp转换成px
* px = density * dp
*/
private int dp2px(Context context,float dpValue){
float scale=context.getResources().getDisplayMetrics().density;
return (int)(dpValue*scale+0.5f);
}
/**
* px转换成dp
*/
private int px2dp(Context context,float pxValue){
float scale=context.getResources().getDisplayMetrics().density;
return (int)(pxValue/scale+0.5f);
}
/**
* sp转换成px
*/
private int sp2px(Context context,float spValue){
float fontScale=context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue*fontScale+0.5f);
}
/**
* px转换成sp
*/
private int px2sp(Context context,float pxValue){
float fontScale=context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue/fontScale+0.5f);
}
利用TypeValue来转换dp、px
原理同上,也是通过DisplayMetrix.density 来进行dp转换, 通过DisplyMetrix.scaledDensity来进行sp转换
private int dp2px(Context context,int dpValue){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue,
context.getResources().getDisplayMetrics());
}
private int sp2px(Context context,int spValue){
return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue,
context.getResources().getDisplayMetrics());
}
TypedValue#applyDimension()
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; //同上,px = density * dp
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity; //文字的sp
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;
}
获取手机屏幕大小
华为Mate 9的屏幕尺寸是5.9英寸,分辨率是1920×1080像素。
经过计算并不是160的整数倍,373/160 = 2.3 , 通过系统调用发现density = 3.0,不知道为啥? 也许是手机厂商设置的480?。
至于1920 和1808是手机的虚拟导航键的问题,展示了虚拟导航键,size就变化了。
Log.e(tag, "getWindowManager().getDefaultDisplay().getRealMetrics(display)/n")
val display = DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(display)
Log.d(tag, display.toString())
Log.e(tag, "getWindowManager().getDefaultDisplay().getMetrics(display)/n")
windowManager.defaultDisplay.getMetrics(display)
Log.d(tag, display.toString())
Log.e(tag, "getWindowManager().getDefaultDisplay().getRealSize(size)")
val size = Point()
windowManager.defaultDisplay.getRealSize(size)
Log.d(tag, size.toString())
Log.e(tag, "getWindowManager().getDefaultDisplay().getSize(size)")
windowManager.defaultDisplay.getSize(size)
Log.d(tag, size.toString())
Log.e(tag, "getResources().getDisplayMetrics()")
val metrix = resources.displayMetrics
Log.d(tag, metrix.toString())