关于Android的屏幕适配

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容