支持不同的像素密度

Android 设备不仅具有不同的屏幕尺寸(手机、平板电脑、电视等),而且屏幕的像素大小也不同。 一些设备的每英寸像素为 160 个,而另一些设备则在相同的空间内包含 480 个像素。 如果您不考虑这些像素密度的差异,系统可能会缩放您的图像,导致图像变得模糊,或者图像可能以错误的大小显示。

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

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

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

使用与密度无关的像素

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

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

为了在不同密度屏幕上保持 UI 的可见大小,请使用 *密度无关像素* (dp) 作为您的测量单位来设计您的 UI。 一个 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,这等于十分之一英寸(或 2.5 毫米),然后才会识别出手势。

在具有高密度显示屏(240 dpi)的设备上,用户的手指必须移动 16 像素 / 240 dpi,这等于十五分之一英寸(或 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...

The 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 的 矢量资源工作室 将 SVG 转换为矢量可绘制对象,方法如下:

  1. 在**项目**窗口中,右键单击**res**目录,然后选择**新建 > 矢量资源**。
  2. 选择**本地文件(SVG、PSD)**。
  3. 找到要导入的文件,并进行任何调整。

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

    您可能会注意到**资源工作室**窗口中出现一些错误,表明矢量可绘制对象不支持文件的一些属性。这并不会阻止您导入文件;不支持的属性将被忽略。

  4. 单击**下一步**。

  5. 在下一个屏幕上,确认要在项目中放置文件的 源代码集,然后单击**完成**。

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

    res/
      drawable/
        ic_android_launcher.xml
    

有关创建矢量图形的更多信息,请阅读 矢量可绘制对象 文档。

提供替代位图

为了在具有不同像素密度的设备上提供良好的图形质量,请在您的应用程序中提供每个位图的多个版本——每个密度桶一个,以相应的分辨率。否则,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 像素。

要为不同的密度创建替代位图可绘制对象,请遵循六个主要密度之间的 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 目录中提供更高密度的启动器图标。

有关图标设计指南,请参见 系统图标

有关构建应用程序图标的帮助,请参见 使用图像资源工作室创建应用程序图标

有关不常见密度问题的建议

本节介绍 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) 相同的物理大小显示。系统对应用程序透明地处理此缩放,并向应用程序报告缩放后的像素尺寸,而不是物理像素尺寸。

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

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

    通常,您不会禁用预缩放。支持多个屏幕的最佳方法是遵循本页中描述的基本技术。

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

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

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

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