对于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"?>    
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属性),也就是说,不能用来设置字体的大小和其它大小。
  • 百分比布局的属性是相对于父布局而言的,在复杂、嵌套层次比较多的界面,计算百分比很麻烦,一旦设计图有修改就更麻烦了。
  • 一些特殊的场景很难用百分比布局去实现。

一体、どうすればいいの?

更多相关文章

  1. Android(安卓)自动化测试之Monkey参数介绍及其停止办法
  2. Android(安卓)Studio开发学习(一)—— 布局
  3. 戏说Android(安卓)view 工作流程《上》
  4. Android集结号
  5. 布局Layouts之LinearLayout线性布局
  6. 在android设计中,如何在有限的界面上做布局
  7. Android(安卓)- 支持不同的设备 - 支持不同的屏幕
  8. android 自定义输入法研究
  9. Android用户界面布局

随机推荐

  1. 使用原生 js 完成一个购物车页面和 ES6
  2. 程序员翻车时的 30 种常见反应!
  3. MySQL命令操作
  4. 一分钟上手Docker容器
  5. Spring Cloud微服务Sentinel+Apollo限流
  6. 写了10年JAVA代码,为何还是给人一种乱糟糟
  7. Python入门课程零基础到精通——基本数学
  8. 漫画:聊一聊MVC、MVP、MVVM?
  9. 漫画:架构师是吧?什么是哈希轮?
  10. Sprint Boot如何基于Redis发布订阅实现异