对于Android的屏幕适配,似乎所有的Android开发人员都知道这么一条准则:使用dp而不是使用px。但是当面对着所有的标注都是以px为单位的设计图时,应该怎样将其转换为dp,使得在(几乎)所有的屏幕上都能显示出相同的效果?最近的我却有点茫然,至今仍未找到理想的答案。下面是我关于这个问题的一些思考与资料搜索,整理一下写出来,期望能梳理出一点头绪。
px = dp * (dpi / 160)
关于这条式子其实直到不久之前我的理解都还是错误的,我一直把它当作是一条关于px和dp这两个单位的转换公式,如同1cm=10mm。所以对于“在dpi=160的屏幕上,1px=1dp”这句话我十分的不解:那就是说,在dpi=320的屏幕上,1px=2dp即1dp = 1/2 px?1dp对应1/2个像素点?这不就是与dp的定义中的“屏幕密度越大,1dp对应的像素点越多”相悖了吗?
其实正确的理解应该是:这是一条px与dp之间的关系表达式。如同y=ax,当a确定的时候,由x的值可以得到y的值,反之亦然。这样上面的疑问就能解答了:在dpi=320的屏幕上,当设计图上的一个标注的px值为1时,对应的dp值应为0.5(1 = 0.5 * (320 / 160))。
物理dpi与系统dpi
dpi计算公式根据上面的dpi的计算公式,以我手头上的华为荣耀6plus测试机(1920 * 1080,5.5'')为例,其dpi应该约等于400,即在这部手机上1dp对应着2.5(400/160)个像素点。
我们可以将手机屏幕信息和一个长度为100dp的Button所占的像素打印出来验证一下。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_screen"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.eichinn.practice.ScreenActivit">
<Button
android:id="@+id/btn"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="100dp"/>
</RelativeLayout>
public class ScreenActivity extends AppCompatActivity {
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screen);
Log.i("tag", "widthPixels: " + getRealWight(this));
Log.i("tag", "heightPixels: " + getRealHeight(this));
Log.i("tag", "generalizedDpi: " + getGeneralizedDpi(this));
btn = (Button) findViewById(R.id.btn);
btn.post(new Runnable() {
@Override
public void run() {
Log.i("tag", "100dp == " + btn.getWidth() + "px");
}
});
}
public static float getGeneralizedDpi(Activity activity) {
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
return dm.density;
}
public static int getRealWight(Activity activity) {
WindowManager wm = activity.getWindowManager();
Display display = wm.getDefaultDisplay();
int screenWidth = 0;
if (Build.VERSION.SDK_INT >= 17) {
Point size = new Point();
display.getRealSize(size);
screenWidth = size.x;
} else if (Build.VERSION.SDK_INT >= 14) {
try {
screenWidth = (Integer) Display.class.getMethod("getRawWidth").invoke(display);
} catch (Exception e) {
DisplayMetrics dm = new DisplayMetrics();
display.getMetrics(dm);
screenWidth = dm.widthPixels;
}
}
return screenWidth;
}
public static int getRealHeight(Activity activity) {
WindowManager wm = activity.getWindowManager();
Display display = wm.getDefaultDisplay();
int screenHeight = 0;
if (Build.VERSION.SDK_INT >= 17) {
Point size = new Point();
display.getRealSize(size);
screenHeight = size.y;
} else if (Build.VERSION.SDK_INT >= 14) {
try {
screenHeight = (Integer) Display.class.getMethod("getRawHeight").invoke(display);
} catch (Exception e) {
DisplayMetrics dm = new DisplayMetrics();
display.getMetrics(dm);
screenHeight = dm.heightPixels;
}
}
return screenHeight;
}
I/tag: widthPixels: 1080
I/tag: heightPixels: 1920
I/tag: generalizedDpi: 480.0
I/tag: 100dp == 300px
呃。。。说好的400呢,怎么变成480.0了???
网上找到一个我比较能接受的说法就是:为简便起见,Android 将所有屏幕密度分组为以下六种通用密度:
- ldpi ~120dpi
- mdpi ~160dpi
- hdpi ~240dpi
- xhdpi ~320dpi
- xxhdpi ~480dpi
-
xxxhdpi ~640dpi
每种通用密度都涵盖一个实际密度范围(图中下半部分)。
于是,上面计算出来的400由于落在了xxhdpi的范围内,所以其对应的通用密度就是480。也就是说上面打印出来的其实是手机屏幕对应的通用密度。
而在Android系统中使用(如进行dp与px的转换)的就是这些通用密度。我个人更喜欢称之为“系统dpi”,对应的上面根据实际屏幕参数计算的就叫“物理dpi”。
顺带提一下,这个系统dpi其实是保持在Android系统的一个配置文件(/system/build.prop)里面,这个文件其中有一行:ro.sf.lcd_density=480,这个就是系统dpi。也就是说我们其实可以修改这个配置(需要root权限,修改后重启生效)为物理dpi,但其实没有这个必要就是了。
如何适配屏幕
回到最初的问题:当面对着所有的标注都是以px为单位的设计图时,应该怎样将其转换为dp,使得在(几乎)所有的屏幕上都能显示出相同的效果?
根据前面几个小节,我目前能想到的答案就是:
- 在xml布局文件中,就用设计图上的标注值跟设计图的系统dpi进行转换。例如:设计图是以iPhone 6s(1334 * 750 4.7'' 326ppi)来做的,那它的系统dpi应该就是320,则设计图上的标注转换成dp就是除以2。即设计图上标的是10px,转换成dp就是5dp。
- 在java文件中,就使用以下方法去转换
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
至于其它单位(dp、sp等)转换成px则可以使用系统的TypedValue.applyDimension方法。
但就算按照这种方法来做适配,仍然达不到理想的效果。例如,设计图上有一个ImageView的高度占了屏幕高度的一半即1334 /2 = 667(px),转换成dp就是667 / 2 = 333.5(dp)。在1080 * 1920,系统dpi为3的屏幕上所占的宽度就是333.5 * 3 + 0.5 = 1001(px)。而这个屏幕的一半应该是1920 / 2 = 960(px),误差为1001 - 960 = 41(px)。
百分比布局
使用百分比布局可以避免以上问题,关于百分比布局的使用可以参考这篇文章。
但是我在使用百分比布局的过程中也碰到了一些问题
- 目前百分比布局只有PercentRelativeLayout、PercentFrameLayout,即只支持RelativeLayout与FrameLayout,没有PercentLinearLayout(当然LinearLayout有类似的layout_weight属性)。
- 百分比布局的属性只支持宽高和margin(layout_widthPercent, layout_heightPercent, layout_marginPercent及其它margin属性),也就是说,不能用来设置字体的大小和其它大小。
- 百分比布局的属性是相对于父布局而言的,在复杂、嵌套层次比较多的界面,计算百分比很麻烦,一旦设计图有修改就更麻烦了。
- 一些特殊的场景很难用百分比布局去实现。
一体、どうすればいいの?
网友评论