支持不同的像素密度

Android 设备不仅具有不同的屏幕尺寸(手机、平板电脑、电视等),而且具有不同像素尺寸的屏幕。一台设备的每英寸像素数可能为 160,而另一台设备在同一空间内可以容纳 480 像素。如果不考虑像素密度的这些变化,系统可能会缩放图片,导致图片模糊不清,或者图片可能以错误的尺寸显示。

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

请观看以下视频,简要了解这些技巧。

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

使用密度无关像素

避免使用像素来定义距离或尺寸。使用像素定义尺寸会带来问题,因为不同的屏幕具有不同的像素密度,因此同样数量的像素在不同设备上对应不同的物理尺寸。

一张图片,其中展示了两个密度不同的示例设备显示屏
图 1:尺寸相同的两个屏幕可以具有不同的像素数。

如需在密度不同的屏幕上保持界面的可见尺寸,请使用密度无关像素 (dp) 作为衡量单位来设计界面。1 dp 是一个虚拟像素单位,大致等于中密度屏幕(160 dpi,即“基准”密度)上的 1 个像素。对于每种密度,Android 会将此值转换为相应的实际像素数。

请考虑图 1 中的两种设备。宽度为 100 像素的视图在左侧设备上看起来要大得多。定义为 100 dp 宽的视图在两个屏幕上看起来大小相同。

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

例如,如要指定两个视图之间的间距,请使用 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)

注意:切勿通过硬编码此等式来计算像素。请改用 TypedValue.applyDimension(),它会为您将多种类型的尺寸(dp、sp 等)转换为像素。

假设在某个应用中,用户的手指移动至少 16 个像素之后,系统可以识别出滚动或滑动手势。在基准屏幕上,用户的手指必须移动 16 pixels / 160 dpi(等于 1/10 英寸,即 2.5 毫米),然后系统才会识别该手势。

在高密度显示屏 (240 dpi) 设备上,用户的手指必须移动 16 pixels / 240 dpi,相当于 1/15 英寸(即 1.7 毫米)。此距离短得多,因此应用对用户来说似乎更灵敏。

若要解决此问题,请在代码中以 dp 为单位表示手势阈值,然后将其转换为实际像素。例如:

Kotlin

// The gesture threshold expressed in dp
private const val GESTURE_THRESHOLD_DP = 16.0f

private var gestureThreshold: Int = 0

// Convert the dps to pixels, based on density scale
gestureThreshold = TypedValue.applyDimension(
  COMPLEX_UNIT_DIP,
  GESTURE_THRESHOLD_DP + 0.5f,
  resources.displayMetrics).toInt()

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

Java

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

// Convert the dps to pixels, based on density scale
int gestureThreshold = (int) TypedValue.applyDimension(
  COMPLEX_UNIT_DIP,
  GESTURE_THRESHOLD_DP + 0.5f,
  getResources().getDisplayMetrics());

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

DisplayMetrics.density 字段指定用于根据当前像素密度将 dp 单位转换为像素的缩放比例。在中密度屏幕上,DisplayMetrics.density 等于 1.0,在高密度屏幕上,等于 1.5。在超高密度屏幕上,它等于 2.0,在低密度屏幕上,等于 0.75。TypedValue.applyDimension() 使用此数字获取当前屏幕的实际像素数。

使用预缩放的配置值

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

Kotlin

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

Java

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

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

首选矢量图形

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

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

您可以使用 Android Studio 的 Vector Asset Studio 将 SVG 转换为矢量可绘制对象,如下所示:

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

    展示如何在 Android Studio 中导入 SVG 的图片
    图 2:使用 Android Studio 导入 SVG。

    您可能会注意到 Asset Studio 窗口中出现一些错误,指出矢量可绘制对象不支持文件的某些属性。这不会阻止您导入文件;不受支持的属性会被忽略。

  4. 点击 Next

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

    由于一个矢量可绘制对象可用于所有像素密度,因此该文件位于您的默认 drawables 目录中,如以下层次结构所示。您无需使用密度专用目录。

    res/
      drawable/
        ic_android_launcher.xml
    

如需详细了解如何创建矢量图形,请参阅矢量可绘制对象文档。

提供备用位图

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

显示不同密度大小的位图的相对尺寸的图片
图 3:不同密度级别中位图的相对尺寸。

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

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

密度限定符 说明
ldpi 适用于低密度 (ldpi) 屏幕(约 120dpi)的资源。
mdpi 适用于中密度 (mdpi) 屏幕(约 160dpi)的资源。这是基准密度。
hdpi 适用于高密度 (hdpi) 屏幕(约 240dpi)的资源。
xhdpi 适用于超高密度 (xhdpi) 屏幕(约 320dpi)的资源。
xxhdpi 适用于超超高密度 (xxhdpi) 屏幕(约 480dpi)的资源。
xxxhdpi 适用于超超超高密度 (xxxhdpi) 屏幕(约 640dpi)的资源。
nodpi 适用于所有密度的资源。这些是与密度无关的资源。无论当前屏幕的密度如何,系统都不会缩放以此限定符标记的资源。
tvdpi 适用于密度介于 mdpi 和 hdpi 之间的屏幕(约约 213 dpi)的资源。这不属于“主要”密度组。它主要用于电视,而大多数应用都不需要它。对于大多数应用而言,提供 mdpi 和 hdpi 资源便已足够,系统将视情况对其进行缩放。如果发现有必要提供 tvdpi 资源,请将其大小调整为 1.33 * mdpi。例如,mdpi 屏幕的 100x100 像素图像对应于 tvdpi 的 133x133 像素。

如需为不同的密度创建备用可绘制位图资源,请遵循六种主要密度之间的 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 目录中,而不是放在 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

在之前的 xxhdpi 设备示例中,您可以在 mipmap-xxxhdpi 目录中提供更高密度的启动器图标。

如需了解图标设计准则,请参阅系统图标

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

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

本部分介绍了 Android 如何在不同像素密度下对位图执行缩放,以及如何进一步控制位图在不同密度下的绘制方式。除非您的应用会操控图形,或者您在不同像素密度上运行时遇到问题,否则您可以忽略此部分。

为了更好地了解在运行时操作图形时如何支持多种密度,您需要了解系统如何有助于确保位图的适当缩放。这是通过以下方法完成的:

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

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

    如果您请求预缩放的资源的尺寸,系统将返回表示缩放后尺寸的值。例如,针对 mdpi 屏幕设计的 50x50 像素位图在 hdpi 屏幕上会扩展为 75x75 像素(如果没有适用于 hdpi 的备用资源),并且系统会这样报告大小。

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

    res/drawable-nodpi/icon.png

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

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

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

    例如,假设某个设备配备了 480x800 的 WVGA 高密度屏幕,其尺寸与传统 HVGA 屏幕大致相同,但它运行的应用停用了预缩放。在这种情况下,系统会在应用查询屏幕尺寸并报告 320x533(像素密度的近似 mdpi 转换值)时“依赖于”应用。

    然后,当应用执行绘制操作时,例如使 (10,10) 到 (100,100) 的矩形失效,系统会通过将坐标缩放到适当的量来转换坐标,实际上会使 (15,15) 到 (150,150) 的区域无效。如果您的应用直接操控缩放后的位图,这种差异可能会导致意外行为,但为了确保最佳的应用性能,这种差异被视为一种合理的权衡。如果遇到这种情况,请参阅将 dp 单位转换为像素单位

    通常情况下,不会停用预缩放。支持多个屏幕的最佳方法是遵循本页介绍的基本技巧。

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

针对所有像素密度测试

请在具有不同像素密度的多台设备上测试您的应用,以便确保界面正确缩放。尽可能在实体设备上进行测试;如果您无法访问具有各种不同像素密度的实体设备,请使用 Android 模拟器

如果您想在实体设备上进行测试,但不想购买设备,则可以使用 Firebase Test Lab 访问 Google 数据中心的设备。