支持不同像素密度

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();

getScaled前缀开头的ViewConfiguration中的方法保证会返回以像素为单位的值,无论当前像素密度如何,这些值都能正确显示。

推荐使用矢量图形

创建多个不同密度版本的图像的替代方法是只创建一个矢量图形。矢量图形使用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资源,请将其大小设置为1.33 * mdpi。例如,mdpi屏幕的100x100像素图像对于tvdpi来说是133x133像素。

要为不同的密度创建替代位图可绘制对象,请遵循六个主要密度之间的3:4:6:8:12:16缩放比例。例如,如果您有一个位图可绘制对象,其中等密度屏幕的像素为48x48像素,则大小为:

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

提示:如果您有一些不想让系统缩放的可绘制资源(例如,当您在运行时自己对图像进行一些调整时),请将它们放在具有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目录中的更高密度启动器图标。

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

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

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

本节介绍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 Test Lab访问 Google 数据中心中的设备。