Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

支持不同的像素密度

Android 设备(手机、平板电脑、电视等)不仅有不同的屏幕尺寸,而且其屏幕也有不同的像素尺寸。也就是说,有可能一部设备的屏幕为每平方英寸 160 像素,而另一部设备的屏幕在相同的空间内可以容纳 480 像素。如果您不考虑像素密度的这些差异,系统可能会缩放图片(导致图片变模糊),或者图片可能会以完全错误的尺寸显示。

本页向您介绍如何设计应用来支持不同的像素密度,那就是使用分辨率无关度量单位,并针对每种像素密度提供备用位图资源。

如需简要了解这些技巧,请观看下面的视频。

如需详细了解如何设计实际图标素材资源,请参阅 Material Design 图标指南

使用密度无关像素

您必须避免的第一个陷阱是使用像素来定义距离或尺寸。使用像素来定义尺寸会带来问题,因为不同的屏幕具有不同的像素密度,所以同样数量的像素在不同的设备上可能对应于不同的物理尺寸。

图 1. 尺寸相同的两个屏幕可能具有不同数量的像素

要在密度不同的屏幕上保留界面的可见尺寸,您必须使用密度无关像素 (dp) 作为度量单位来设计界面。dp 是一个虚拟像素单位,1 dp 约等于中密度屏幕(160dpi;“基准”密度)上的 1 像素。对于其他每个密度,Android 会将此值转换为相应的实际像素数。

例如,考虑图 1 中的两部设备。如果将某个视图定义为“100px”宽,那么它在左侧设备上看起来要大得多。因此,您必须改用“100dp”来确保它在两个屏幕上看起来大小相同。

不过,在定义文本大小时,您应改用可缩放像素 (sp) 作为单位(但切勿将 sp 用于布局尺寸)。默认情况下,sp 单位与 dp 大小相同,但它会根据用户的首选文本大小来调整大小。

例如,当您指定两个视图的间距时,请使用 dp

    <Button android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/clickme"
        android:layout_marginTop="20dp" />
    

当指定文本大小时,请一律使用 sp

    <TextView android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp" />
    

将 dp 单位转换为像素单位

在某些情况下,您需要以 dp 表示尺寸,然后将其转换为像素。dp 单位转换为屏幕像素很简单:

px = dp * (dpi / 160)

假设在某一应用中,用户的手指至少移动 16 像素之后,系统会识别出滚动或滑动手势,那么在基准屏幕上,用户的手指必须至少移动 16 pixels / 160 dpi,相当于 1 英寸的 1/10(2.5 毫米),相应手势才能被识别;而在配备高密度显示屏 (240dpi) 的设备上,用户的手指必须至少移动 16 pixels / 240 dpi,相当于 1 英寸的 1/15(1.7 毫米)。此距离短得多,因此用户会感觉应用在该设备上更灵敏。

要解决此问题,手势阈值必须在代码中以 dp 表示,然后再转换为实际像素。例如:

Kotlin

    // The gesture threshold expressed in dp
    private const val GESTURE_THRESHOLD_DP = 16.0f
    ...
    private var mGestureThreshold: Int = 0
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Get the screen's density scale
        val scale: Float = resources.displayMetrics.density
        // Convert the dps to pixels, based on density scale
        mGestureThreshold = (GESTURE_THRESHOLD_DP * scale + 0.5f).toInt()

        // Use mGestureThreshold as a distance in pixels...
    }
    

Java

    // The gesture threshold expressed in dp
    private static final float GESTURE_THRESHOLD_DP = 16.0f;

    // Get the screen's density scale
    final float scale = getResources().getDisplayMetrics().density;
    // Convert the dps to pixels, based on density scale
    mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);

    // Use mGestureThreshold as a distance in pixels...
    

DisplayMetrics.density 字段根据当前像素密度指定要将 dp 单位转换为像素而必须使用的缩放系数。对于中密度屏幕,DisplayMetrics.density 等于 1.0;对于高密度屏幕,它等于 1.5;对于超高密度屏幕,它等于 2.0;对于低密度屏幕,它等于 0.75。此数字是一个系数,用其乘以 dp 单位,即可得出当前屏幕的实际像素数。

使用预缩放的配置值

您可以使用 ViewConfiguration 类来获得 Android 系统常用的距离、速度和时间。例如,框架用作滚动阈值的距离(以像素为单位)可通过 getScaledTouchSlop() 获得:

Kotlin

    private val GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).scaledTouchSlop
    

Java

    private static final int GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).getScaledTouchSlop();
    

ViewConfiguration 中以 getScaled 前缀开头的方法必定返回以像素为单位的值,无论当前像素密度是多少,该值都会正确显示。

提供备用位图

要在像素密度不同的设备上提供良好的图形质量,您应该以相应的分辨率在应用中提供每个位图的多个版本(针对每个密度级别提供一个版本)。否则,Android 系统必须缩放位图,使其在每个屏幕上占据相同的可见空间,从而导致缩放失真,如模糊。

图 2. 不同密度大小的位图的相对尺寸

您的应用中有多个密度级别可供使用。表 1 描述了可用的不同配置限定符以及它们适用的屏幕类型。

表 1. 适用于不同像素密度的配置限定符。

密度限定符 说明
ldpi 适用于低密度 (ldpi) 屏幕 (~ 120dpi) 的资源。
mdpi 适用于中密度 (mdpi) 屏幕 (~ 160dpi) 的资源(这是基准密度)。
hdpi 适用于高密度 (hdpi) 屏幕 (~ 240dpi) 的资源。
xhdpi 适用于加高 (xhdpi) 密度屏幕 (~ 320dpi) 的资源。
xxhdpi 适用于超超高密度 (xxhdpi) 屏幕 (~ 480dpi) 的资源。
xxxhdpi 适用于超超超高密度 (xxxhdpi) 屏幕 (~ 640dpi) 的资源。
nodpi 适用于所有密度的资源。这些是与密度无关的资源。无论当前屏幕的密度是多少,系统都不会缩放以此限定符标记的资源。
tvdpi 适用于密度介于 mdpi 和 hdpi 之间的屏幕(约 213dpi)的资源。这不属于“主要”密度组。它主要用于电视,而大多数应用都不需要它。对于大多数应用而言,提供 mdpi 和 hdpi 资源便已足够,系统将视情况对其进行缩放。如果您发现有必要提供 tvdpi 资源,应按一个系数来确定其大小,即 1.33*mdpi。例如,如果某张图片在 mdpi 屏幕上的大小为 100px x 100px,那么它在 tvdpi 屏幕上的大小应该为 133px x 133px。

要针对不同的密度创建备用可绘制位图资源,您应遵循六种主要密度之间的 3:4:6:8:12:16 缩放比。例如,如果您有一个可绘制位图资源,它在中密度屏幕上的大小为 48x48 像素,那么它在其他各种密度的屏幕上的大小应该为:

  • 36x36 (0.75x) - 低密度 (ldpi)
  • 48x48(1.0x 基准)- 中密度 (mdpi)
  • 72x72 (1.5x) - 高密度 (hdpi)
  • 96x96 (2.0x) - 超高密度 (xhdpi)
  • 144x144 (3.0x) - 超超高密度 (xxhdpi)
  • 192x192 (4.0x) - 超超超高密度 (xxxhdpi)

然后,将生成的图片文件放在 res/ 下的相应子目录中,系统将根据运行应用的设备的像素密度自动选取正确的文件:

    res/
      drawable-xxxhdpi/
        awesome-image.png
      drawable-xxhdpi/
        awesome-image.png
      drawable-xhdpi/
        awesome-image.png
      drawable-hdpi/
        awesome-image.png
      drawable-mdpi/
        awesome-image.png
    

之后,每当您引用 @drawable/awesomeimage 时,系统都会根据屏幕的 dpi 选择适当的位图。如果您没有为某个密度提供特定于密度的资源,那么系统会选取下一个最佳匹配项并对其进行缩放以适合屏幕。

提示:如果您有一些系统绝不能缩放(或许是因为您在运行时自行对图片进行一些调整)的可绘制资源,则应将这些资源放在带有 nodpi 配置限定符的目录中。带有此限定符的资源被视为与密度无关,系统将不会对它们进行缩放。

如需详细了解其他配置限定符以及 Android 如何根据当前屏幕配置选择适当的资源,请参阅提供资源

将应用图标放在 mipmap 目录中

与其他所有位图资源一样,对于应用图标,您也需要提供特定于密度的版本。不过,某些应用启动器显示的应用图标会比设备的密度级别所要求的大差不多 25%。

例如,如果设备的密度级别为 xxhdpi 且您提供的最大应用图标在 drawable-xxhdpi 中,那么启动器应用会放大此图标,这会使其看起来不太清晰。因此,您应在 mipmap-xxxhdpi 目录中提供一个密度更高的启动器图标,而后启动器便可改用 xxxhdpi 资源。

由于应用图标可能会像这样放大,因此您应将所有应用图标都放在 mipmap 目录中,而不是放在 drawable 目录中。与 drawable 目录不同,所有 mipmap 目录都会保留在 APK 中,即使您构建特定于密度的 APK 也是如此。这样,启动器应用便可选取要显示在主屏幕上的最佳分辨率图标。

    res/
      mipmap-xxxhdpi/
        launcher-icon.png
      mipmap-xxhdpi/
        launcher-icon.png
      mipmap-xhdpi/
        launcher-icon.png
      mipmap-hdpi/
        launcher-icon.png
      mipmap-mdpi/
        launcher-icon.png
    

有关图标设计指南,请参阅图标的素材指南

如需构建应用图标方面的帮助,请参阅使用 Image Asset Studio 创建应用图标

改用矢量图形

除了创建多个特定于密度的图片版本之外,另一种方法是仅创建一个矢量图形。在借助矢量图形创建图片时,使用 XML 定义路径和颜色,而不是使用像素位图。因此,矢量图形可以缩放到任何尺寸而不会出现缩放失真,不过它们通常最适合图标等插图,而不太适合照片。

矢量图形通常以 SVG(可缩放矢量图形)文件的形式提供,但 Android 不支持此格式,因此您必须将 SVG 文件转换为 Android 的矢量图格式。

您可以在 Android Studio 中使用 Vector Asset Studio 轻松地将 SVG 转换为矢量图,具体步骤如下:

  1. Project 窗口中,右键点击 res 目录,然后依次选择 New > Vector Asset
  2. 选择 Local file (SVG, PSD)
  3. 找到要导入的文件并进行任何调整。

    图 3. 使用 Android Studio 导入 SVG 文件

    您可能会注意到 Asset Studio 窗口中出现了一些错误,指出文件的某些属性不受矢量图支持。但这不会阻止您导入,只是会忽略不受支持的属性。

  4. 点击 Next

  5. 在下一个屏幕上,确认您希望文件在项目中所在的源集,然后点击 Finish

    因为可以对所有像素密度使用一个矢量图,所以此文件位于默认的 drawable 目录中(您不需要使用特定于密度的目录):

        res/
          drawable/
            ic_android_launcher.xml
        

如需详细了解如何创建矢量图形,请阅读矢量图文档。

针对不常见的密度问题给出的建议

本部分将详细说明 Android 系统如何在像素密度不同的屏幕上对位图执行缩放,以及您如何进一步控制位图在像素密度不同的屏幕上的绘制方式。除非您的应用操控图形,或者您的应用在像素密度不同的设备上运行时遇到了问题,否则您可以忽略本部分。

为了更好地了解如何在运行时操控图形期间支持多种密度,您应该先了解,系统会通过以下方式帮助确保正确缩放位图:

  1. 预缩放资源(如可绘制位图资源)

    根据当前屏幕的密度,系统会使用您的应用中特定于密度的任何资源。如果没有针对相应密度的资源可用,系统会加载默认资源,并根据需要将其放大或缩小。系统假定默认资源(即没有配置限定符的目录中的资源)是针对基准像素密度 (mdpi) 设计的,并且会调整这些位图的大小,使其大小适合当前像素密度。

    如果您请求预缩放的资源的尺寸,系统将返回表示缩放后尺寸的值。例如,假设针对 mdpi 屏幕设计了一个 50x50 像素的位图,它在 hdpi 屏幕上会放大到 75x75 像素(如果没有针对 hdpi 的备用资源),那么系统会将尺寸报告为 75x75 像素。

    在某些情况下,您可能不希望 Android 系统预缩放资源。要避免预缩放,最简单的方法就是将资源放在带有 nodpi 配置限定符的资源目录中。例如:

    res/drawable-nodpi/icon.png

    当系统使用此文件夹中的 icon.png 位图时,不会根据当前设备密度对其进行缩放。

  2. 自动缩放像素尺寸和坐标

    您可以在清单中将 android:anyDensity 设为 "false" 来停用尺寸和图片预缩放,也可以针对 BitmapinScaled 设为 "false" 来以编程方式停用预缩放。在这种情况下,系统会在绘图时自动缩放所有绝对像素坐标和像素尺寸值。这样做是为了确保按像素定义的屏幕元素的显示尺寸与其在基准像素密度 (mdpi) 屏幕上的物理尺寸大致相同。系统将以对应用透明的方式处理此缩放,并向应用报告缩放后的像素尺寸,而不是物理像素尺寸。

    例如,假设某个设备配备 WVGA 高密度屏幕,该屏幕的尺寸为 480x800,与传统 HVGA 屏幕差不多一样大,但在该设备上运行的某个应用停用了预缩放。在这种情况下,当该应用查询屏幕尺寸时,系统会对其“撒谎”,将屏幕尺寸报告为 320x533(针对像素密度为 mdpi 的屏幕转换后得到的近似尺寸)。随后,当该应用执行绘图操作时,例如使 (10,10) 到 (100,100) 的矩形无效,系统会将坐标缩放适当的量来对其进行转换,因而实际上会使 (15,15) 到 (150,150) 的区域无效。如果您的应用直接操控缩放后的位图,此差异可能会导致意外行为,但为了尽可能保持良好的应用性能,这种处理方式被视为一种合理的权衡。如果您遇到这种情况,请阅读将 dp 单位转换为像素单位

    通常,不应停用预缩放。要支持多种屏幕,最好的方法就是遵循本文档中介绍的基本技巧。

如果您的应用操控位图或以其他某种方式直接与屏幕上的像素交互,您可能需要采取其他措施来支持不同的像素密度。例如,如果您通过计算手指滑过的像素数来响应触摸手势,那么需要使用适当的密度无关像素值,而不是实际像素,但您可以轻松地在 dp 和 px 值之间转换

针对所有像素密度测试

务必在像素密度不同的多部设备上测试您的应用,这样您就可以确保界面正确缩放。在物理设备上进行测试很简单,但如果您无法访问具有各种不同像素密度的物理设备,那么也可以使用 Android 模拟器

如果您希望在物理设备上进行测试,但又不想购买设备,那么可以使用 Firebase 测试实验室访问 Google 数据中心的设备。