支持不同的像素密度

Android 设备不仅拥有不同的屏幕尺寸(手机、平板电脑、电视等),还拥有不同像素密度的屏幕。一个设备可能每英寸有 160 个像素,而另一个设备在同样的尺寸内可容纳 480 个像素。如果您不考虑这些像素密度的差异,系统可能会缩放您的图像,导致图像变得模糊,或者图像显示的尺寸不正确。

本页面介绍了如何通过使用与分辨率无关的度量单位,并为每种像素密度提供替代的位图资源,来设计支持不同像素密度的应用程序。

观看以下视频,了解这些技术的概览。

有关设计图标资源的更多信息,请参阅 Material Design 图标指南

使用密度无关像素

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

An image showing two example device displays with different densities
图 1:两个相同尺寸的屏幕可能拥有不同数量的像素。

为了在不同密度的屏幕上保持 UI 的可见尺寸,请使用密度无关像素 (dp) 作为度量单位来设计您的 UI。1 dp 是一个虚拟像素单位,大约等于中密度屏幕(160 dpi,即“基准”密度)上的一个像素。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 像素 / 160 dpi,即 1/10 英寸(或 2.5 毫米),该手势才能被识别。

在高密度显示屏(240 dpi)的设备上,用户的手指必须移动 16 像素 / 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 的 Vector Drawable 格式。

您可以使用 Android Studio 的 Vector Asset Studio 将 SVG 转换为 Vector Drawable,操作步骤如下:

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

    An image showing how to import SVGs in Android Studio
    图 2:使用 Android Studio 导入 SVG。

    您可能会注意到 Asset Studio 窗口中出现一些错误,提示 Vector Drawable 不支持文件的某些属性。这不会阻止您导入文件;不受支持的属性将被忽略。

  4. 点击 Next

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

    由于一个 Vector Drawable 可用于所有像素密度,该文件应放在您的默认 drawable 目录中,如下层级所示。您无需使用特定于密度的目录。

    res/
      drawable/
        ic_android_launcher.xml
    

有关创建矢量图形的更多信息,请阅读 Vector Drawable 文档。

提供替代位图

为了在不同像素密度的设备上提供良好的图形质量,请为您的应用程序中的每个位图提供多个版本——每种密度桶 (density bucket) 一个,并配以相应的分辨率。否则,Android 必须缩放您的位图以使其在每个屏幕上占据相同的可见空间,从而导致模糊等缩放伪影。

An image showing relative sizes for bitmaps at different density sizes
图 3:不同密度桶中位图的相对大小。

您的应用程序可以使用几种密度桶。表 1 描述了可用的不同配置限定符及其适用的屏幕类型。

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

密度限定符 描述
ldpi 用于低密度 (ldpi) 屏幕 (~120 dpi) 的资源。
mdpi 用于中密度 (mdpi) 屏幕 (~160 dpi) 的资源。这是基准密度。
hdpi 用于高密度 (hdpi) 屏幕 (~240 dpi) 的资源。
xhdpi 用于超高密度 (xhdpi) 屏幕 (~320 dpi) 的资源。
xxhdpi 用于超超高密度 (xxhdpi) 屏幕 (~480 dpi) 的资源。
xxxhdpi 用于超超超高密度 (xxxhdpi) 屏幕 (~640 dpi) 的资源。
nodpi 用于所有密度的资源。这些是密度无关资源。无论当前屏幕的密度如何,系统都不会缩放标有此限定符的资源。
tvdpi 用于介于 mdpi 和 hdpi 之间的屏幕资源;约为 ~213 dpi。这不被视为“主要”密度组。它主要针对电视,大多数应用程序不需要它——提供 mdpi 和 hdpi 资源对大多数应用程序来说已经足够,系统会根据需要进行缩放。如果您发现必须提供 tvdpi 资源,请按 mdpi 的 1.33 倍缩放。例如,mdpi 屏幕上的 100x100 像素图像对应 tvdpi 屏幕为 133x133 像素。

要为不同密度创建替代位图 Drawable,请遵循六种主要密度之间 3:4:6:8:12:16 的缩放比例。例如,如果您有一个适用于中密度屏幕的 48x48 像素位图 Drawable,各密度对应的尺寸为:

  • 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 选择合适的位图。如果您没有为该密度提供特定的资源,系统会找到最接近的匹配项并将其缩放以适应屏幕。

提示:如果您有一些不希望系统缩放的 Drawable 资源(例如,当您在运行时自行对图像进行某些调整时),请将它们放在带有 nodpi 配置限定符的目录中。带有此限定符的资源被视为与密度无关,系统不会缩放它们。

有关其他配置限定符以及 Android 如何为当前屏幕配置选择合适资源的更多信息,请参阅 应用资源概览

将应用图标放入 mipmap 目录

与其他位图资源一样,您需要提供特定于密度的应用图标版本。但是,一些应用启动器会将您的应用图标显示得比设备密度桶所要求的大小大 25%。

例如,如果设备的密度桶是 xxhdpi,而您提供的最大应用图标位于 drawable-xxhdpi 中,应用启动器会放大此图标,这会使其看起来不那么清晰。

为避免这种情况,请将所有应用图标放入 mipmap 目录,而不是 drawable 目录。与 drawable 目录不同,即使您构建针对特定密度的 APK,所有 mipmap 目录中的资源都会保留在 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. 资源预缩放,例如位图 Drawable

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

    如果您请求预缩放资源的尺寸,系统会返回缩放后的尺寸值。例如,一个为 mdpi 屏幕设计的 50x50 像素位图在 hdpi 屏幕上会被缩放为 75x75 像素(如果没有 hdpi 的替代资源),系统也会按此报告其尺寸。

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

    res/drawable-nodpi/icon.png

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

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

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

    例如,假设设备有一个 WVGA 高密度屏幕(480x800),其物理尺寸与传统的 HVGA 屏幕大致相同,但它运行的是一个禁用了预缩放的应用程序。在这种情况下,当应用程序查询屏幕尺寸时,系统会向其“撒谎”并报告 320x533(即像素密度对应的 mdpi 转换值)。

    然后,当应用程序执行绘图操作(例如使从 (10,10) 到 (100, 100) 的矩形无效)时,系统会通过相应缩放坐标来转换坐标,并实际使 (15,15) 到 (150, 150) 的区域无效。如果您直接操作缩放后的位图,这种差异可能会导致意外行为,但这被认为是确保最佳应用程序性能的合理折衷。如果您遇到这种情况,请阅读 将 dp 单位转换为像素单位

    通常,您不应该禁用预缩放。支持多屏幕的最佳方式是遵循本页面描述的基本技术。

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

在所有像素密度上进行测试

在具有不同像素密度的多台设备上测试您的应用程序,以确保您的 UI 正确缩放。尽可能在物理设备上测试;如果您无法获取所有不同像素密度的物理设备,请使用 Android 模拟器

如果您想在物理设备上测试但不想购买设备,可以使用 Firebase Test Lab 来访问 Google 数据中心内的设备。