支持不同的像素密度

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,即 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 的矢量 drawable 格式。

您可以使用 Android Studio 的Vector Asset Studio 将 SVG 转换为矢量 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 窗口中注意到一些错误,表明矢量 drawable 不支持文件中的某些属性。这不会阻止您导入文件;不支持的属性将被忽略。

  4. 点击 Next

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

    由于一个矢量 drawable 可以在所有像素密度上使用,此文件会放在您的默认 drawable 目录中,如以下层级所示。您无需使用密度特定目录。

    res/
      drawable/
        ic_android_launcher.xml
    

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

提供替代位图

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

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

将生成的图像文件放在 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",或通过编程方式为 BitmapinScaled 设置为 "false" 来禁用尺寸和图片的预缩放。在这种情况下,系统会在绘制时自动缩放任何绝对像素坐标和像素尺寸值。这样做是为了确保像素定义的屏幕元素仍以与在基准像素密度 (mdpi) 下可以显示的物理尺寸大致相同的尺寸显示。系统对应用透明地处理此缩放,并向应用报告缩放后的像素尺寸,而不是物理像素尺寸。

    例如,假设一台设备具有 WVGA 高密度屏幕,分辨率为 480x800,尺寸与传统的 HVGA 屏幕大致相同——但它正在运行一个已禁用预缩放的应用。在这种情况下,当应用查询屏幕尺寸时,系统会“欺骗”应用,报告 320x533,这是像素密度对应的近似 mdpi 转换尺寸。

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

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

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

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

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

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