1 Android屏幕适配的度量单位和相关概念
建议在阅读本文章之前,可以先阅读快乐李同学写的文章《Android屏幕适配的度量单位和相关概念》,这篇文章包含了阅读本文的一些基础知识,推荐阅读。
2 Android屏幕适配的解决方案
2.1 Android屏幕适配前言
Android屏幕适配是一个亘古不变的难题,在百度或者Google搜索相关的关键词,我们总能找到各个年代所流行的Android屏幕适配方法。但是由于时效性的原因,2021年的今天出现了新的Android屏幕适配方法,并淘汰了部分以前旧的Android屏幕适配方法。于是,今天博主就来详细地总结一下,把迄今为止所有的Android屏幕适配方法都罗列出来,供大家参考和对比。
早在2014年,支持的Android设备就有18796种了,而不像2021年的今天iOS设备的类型可能都没超过100种,下面这张图片所显示的内容足以充分说明当今Android系统碎片化问题的严重性,因为该图片中的每一个矩形都代表着一种Android设备。
就像Google Android开发者官网的《设备兼容性概览》文章所提到,这么多种类的Android设备都有着不同的屏幕尺寸和像素密度,尽管系统可通过基本的缩放和调整大小功能使界面适应不同屏幕,但您应做出进一步优化,以确保界面能够在各类屏幕上美观地呈现。
以下的思维导图便是本文所要讲解内容的总览,也是结合Google Android开发者官网的相关Android屏幕适配文档后,总结的最全面的Android屏幕适配方案了。
2.2 适配不同屏幕尺寸的Android设备
Android 设备的形状和尺寸多种多样,因此应用的布局需要十分灵活。也就是说,布局应该从容应对不同的屏幕尺寸和方向,而不是为布局定义刚性尺寸,假定屏幕尺寸和宽高比是一定的。
通过支持尽可能多的屏幕,您的应用可以在各种不同设备上运行,这样您就可以使用单个 APK 将其提供给最多的用户。此外,如果能够使您的应用灵活适应不同的屏幕尺寸,可确保您的应用可以处理设备上的窗口配置更改,例如,当用户启用多窗口模式时发生的窗口配置更改。
本页将向您介绍如何采用以下技巧支持不同的屏幕尺寸:
- 使用允许布局调整大小的视图尺寸
- 根据屏幕配置创建备用界面布局
- 提供可以随视图一起拉伸的位图
但是,请注意,适应不同的屏幕尺寸并不一定会使您的应用与所有 Android 设备类型兼容。您应该采取其他措施支持 Android Wear、Android TV、Android Auto 和 Chrome 操作系统设备。
有关针对不同屏幕构建界面的设计指南,请参阅自适应界面的 Material 指南。
2.2.1 灵活的布局
无论您首先要支持什么硬件配置文件,都需要创建一个能够灵活应对屏幕尺寸变化(即便是微小变化)的布局。
2.2.1.1 使用ConstraintLayout
如需创建适用于不同屏幕尺寸的自适应布局,最佳做法是将 ConstraintLayout
用作界面中的基本布局。使用 ConstraintLayout
,您可以根据布局中视图之间的空间关系指定每个视图的位置和大小。通过这种方式,当屏幕尺寸改变时,所有视图都可以一起移动和拉伸。
如需使用 ConstraintLayout
构建布局,最简单的方法是使用 Android Studio 中的布局编辑器。借助该工具,您可以将新视图拖动到布局中,将其约束条件附加到父视图和其他同级视图以及修改视图的属性,完全不必手动修改任何 XML(请参见图 1)。
如需了解详情,请参阅使用 ConstraintLayout 构建自适应界面。
但是,ConstraintLayout
并不能解决所有布局场景(特别是动态加载的列表,对于此类布局,应使用 RecyclerView),但无论您使用何种布局,都应该避免对布局尺寸进行硬编码(请参见下一部分)。
2.2.1.2 使用允许布局UI组件动态调整大小的视图属性,避免硬编码的布局尺寸(如100dp)
2.2.1.2.1 优先用wrap_content和match_parent
为了确保布局能够灵活地适应不同的屏幕尺寸,您应该对大多数视图组件的宽度和高度使用 "wrap_content"
和 "match_parent"
,而不是硬编码的尺寸,例如100dp。
-
"wrap_content"
指示视图将其尺寸设为适配该视图中相应内容所需的尺寸。 -
"match_parent"
使视图在父视图中尽可能地展开。
例如用match_parent和wrap_content修饰的一个TextView,其代码如下:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lorem_ipsum" />
虽然此视图的实际布局取决于其父视图和任何同级视图中的其他属性,但是此 TextView
想要将其宽度设为填充所有可用空间 (match_parent
),并将其高度设为正好是文本长度所需的空间 (wrap_content
)。这样可以使此视图适应不同的屏幕尺寸和不同的文本长度。
下图显示了当屏幕宽度随着设备屏幕方向而发生变化时,如何使用 "match_parent"
调整文本视图的宽度。
2.2.1.2.2 使用搭配layout_weight的LinearLayout
如果您使用的是 LinearLayout
,则也可以按布局权重展开子视图,以便每个视图按自身权重值所占的比例填充剩余的空间。
2.2.1.2.3 多层嵌套的LinearLayout可用ConstraintLayout或RelativeLayout优化
但是,在嵌套的 LinearLayout
中使用权重将要求系统执行多次布局遍历以确定每个视图的尺寸,这会降低界面性能。幸运的是,ConstraintLayout
几乎能够构建 LinearLayout
所能构建的所有布局,而不会影响性能,因此您应该尝试将布局转换为 ConstraintLayout。然后,即可使用约束链来定义加权布局。
注意:使用
ConstraintLayout
时,不得使用match_parent
。而应将尺寸设为0dp
以启用一种称为“匹配约束”的特殊行为,该行为通常与match_parent
的预期行为相同。如需了解详情,请参阅如何调整 ConstraintLayout 中的视图尺寸。
2.2.2 备用布局
虽然您的布局应始终通过拉伸其视图内部和周围的空间来应对不同的屏幕尺寸,但这可能无法针对每种屏幕尺寸提供最佳用户体验。例如,您为手机设计的界面或许无法在平板电脑上提供良好的体验。因此,您的应用还应提供备用布局资源,以针对特定屏幕尺寸优化界面设计。
您可以通过创建额外的 res/layout/
目录提供特定于屏幕的布局(针对需要不同布局的每种屏幕配置创建一个目录),然后将屏幕配置限定符附加到 layout
目录名称(例如,对于可用宽度为 600dp 的屏幕,附加限定符为 layout-w600dp
)。
这些配置限定符表示应用界面可用的可见屏幕空间。从应用中选择布局时,系统会考虑所有系统装饰(例如导航栏)和窗口配置更改(例如,当用户启用多窗口模式时)。
如需在 Android Studio(使用 3.0 或更高版本)中创建备用布局,请按以下步骤操作:
- 打开默认布局,然后点击工具栏中的 Orientation for Preview 图标。
- 在下拉列表中,点击以创建一个建议的变体(如 Create Landscape Variant),或点击 Create Other。
- 如果您选择 Create Other,系统将显示 Select Resource Directory。在此窗口中,在左侧选择一个屏幕限定符,然后将其添加到 Chosen qualifiers 列表中。添加限定符之后,点击 OK。(有关屏幕尺寸限定符的信息,请参阅下面几部分。)
此时系统会在相应的布局目录中创建一个重复的布局文件,以便您可以开始自定义该屏幕变体的布局。
2.2.2.1 最小宽度限定符(Smallest-Width)
使用“最小宽度”屏幕尺寸限定符,您可以为具有最小宽度(以密度无关像素 dp 或 dip 为度量单位)的屏幕提供备用布局。
通过将屏幕尺寸描述为密度无关像素的度量值,Android 允许您创建专为非常具体的屏幕尺寸而设计的布局,同时让您不必对不同的像素密度有任何担心。
例如,您可以创建一个名为 main_activity
且针对手机和平板电脑进行了优化的布局,方法是在目录中创建该文件的不同版本,如下所示:
res/layout/main_activity.xml # For handsets (smaller than 600dp available width)
res/layout-sw600dp/main_activity.xml # For 7” tablets (600dp wide and bigger)
最小宽度限定符指定屏幕两侧的最小尺寸,而不考虑设备当前的屏幕方向,因此这是一种指定布局可用的整体屏幕尺寸的简单方法。
下面是其他最小宽度值与典型屏幕尺寸的对应关系:
- 320dp:典型手机屏幕(240x320 ldpi、320x480 mdpi、480x800 hdpi 等)。
- 480dp:约为 5 英寸的大手机屏幕 (480x800 mdpi)。
- 600dp:7 英寸平板电脑 (600x1024 mdpi)。
- 720dp:10 英寸平板电脑(720x1280 mdpi、800x1280 mdpi 等)。
下图提供了一个更详细的视图,说明了不同屏幕 dp 宽度与不同屏幕尺寸和方向的一般对应关系。
请记住,最小宽度限定符的所有数值都是密度无关像素,因为重要的是系统考虑像素密度(而不是原始像素分辨率)之后可用的屏幕空间量。
注意:您使用这些限定符指定的尺寸不是实际屏幕尺寸,而是 Activity 窗口可用的宽度或高度(以 dp 为单位)。Android 系统可能会将部分屏幕用于系统界面(如屏幕底部的系统栏或顶部的状态栏),因此部分屏幕可能不可供您的布局使用。如果您的应用在多窗口模式下使用,则它只能使用该窗口的尺寸。对该窗口进行大小调整时,它会使用新窗口尺寸触发配置更改,以便系统可以选择适当的布局文件。因此,在声明尺寸时,您应具体说明 Activity 需要的尺寸。在声明为布局提供的空间时,系统会考虑系统界面使用的所有空间。
2.2.2.2 可用宽度限定符
您可能希望根据当前可用的宽度或高度来更改布局,而不是根据屏幕的最小宽度来更改布局。例如,如果您有一个双窗格布局,您可能希望在屏幕宽度至少为 600dp 时使用该布局,但屏幕宽度可能会根据设备的屏幕方向是横向还是纵向而发生变化。在这种情况下,您应使用“可用宽度”限定符,如下所示:
res/layout/main_activity.xml # For handsets (smaller than 600dp available width)
res/layout-w600dp/main_activity.xml # For 7” tablets or any screen with 600dp available width (possibly landscape handsets)
如果您关心可用高度,则可以使用“可用高度”限定符来执行相同的操作。例如,对于屏幕高度至少为 600dp 的屏幕,请使用限定符 layout-h600dp
。
2.2.2.3 屏幕方向限定符
虽然您可能只需将“最小宽度”和“可用宽度”限定符结合使用即可支持所有尺寸变化,但是您可能还希望当用户在纵向与横向之间切换屏幕方向时改变用户体验。
为此,您可以将 port
或 land
限定符添加到资源目录名称中。只需确保这些限定符在其他尺寸限定符后面即可。例如:
res/layout/main_activity.xml # For handsets
res/layout-land/main_activity.xml # For handsets in landscape
res/layout-sw600dp/main_activity.xml # For 7” tablets
res/layout-sw600dp-land/main_activity.xml # For 7” tablets in landscape
如需详细了解所有屏幕配置限定符,请参阅提供资源指南中的表 2。
2.2.2.4 Fragment将界面UI组件模块化
在针对多种屏幕尺寸设计应用时,您希望确保不会在 Activity 之间不必要地重复界面行为。因此,您应该使用 Fragment 将界面逻辑提取到单独的组件中。然后,您可以组合 Fragment 以便在大屏幕设备上运行时创建多窗格布局,或者在手机上运行时将 Fragment 放置在单独的 Activity 中。
例如,平板电脑上的一款新闻应用可能在左侧显示报道列表,而在右侧显示一篇完整的报道。在左侧选择一篇报道时,会更新右侧的报道视图。但是,在手机上,这两个组件应显示在单独的屏幕上。从列表中选择一篇报道时,会改变整个屏幕以显示这篇报道。
如需了解详情,请参阅使用 Fragment 构建动态界面。
2.2.2.5 使用旧尺寸限定符支持Android3.1及之前的设备
如果您的应用支持 Android 3.1(API 级别 12)或更低版本,则除上面的最小/可用宽度限定符之外,您还需要使用旧尺寸限定符。
在上面的示例中,如果要在较大的设备上显示双窗格布局,那么您需要使用“large”配置限定符来支持 3.1 及更低版本。因此,要在这些旧版本上实现此类布局,您可能需要创建以下文件:
res/layout/main_activity.xml # For handsets (smaller than 640dp x 480dp)
res/layout-large/main_activity.xml # For small tablets (640dp x 480dp and bigger)
res/layout-xlarge/main_activity.xml # For large tablets (960dp x 720dp and bigger)
如果同时支持低于和高于 Android 3.2 的版本,您必须同时对布局使用最小宽度限定符和“large”限定符。因此,您应创建一个名为 res/layout-large/main.xml
的文件,该文件可能与 res/layout-sw600dp/main.xml
完全相同。
为避免同一文件出现这种重复,您可以使用别名文件。例如,您可以定义以下布局:
res/layout/main.xml # single-pane layout
res/layout/main_twopanes.xml # two-pane layout
并添加以下两个文件:
-
res/values-large/layout.xml:
<resources> <item name="main" type="layout">@layout/main_twopanes</item> </resources>
-
res/values-sw600dp/layout.xml:
<resources> <item name="main" type="layout">@layout/main_twopanes</item> </resources>
这两个文件的内容完全相同,但它们实际上并未定义布局,而只是将 main
设置为 main_twopanes
的别名。由于这些文件具有 large
和 sw600dp
选择器,因此它们适用于任何 Android 版本的平板电脑和电视(低于 3.2 版本的平板电脑和电视与 large
匹配,高于 3.2 版本者将与 sw600dp
匹配)。
2.2.3 可拉伸的九宫格位图
如果您在改变尺寸的视图中将位图用作背景,您会注意到,当视图根据屏幕尺寸或视图中的内容增大或缩小时,Android 会缩放您的图片。这通常会导致明显的模糊或其他缩放失真。解决方案是使用九宫格位图,这种特殊格式的 PNG 文件会指示哪些区域可以拉伸,哪些区域不可以拉伸。
九宫格位图基本上是一种标准的 PNG 文件,但带有额外的 1 像素边框,指示应拉伸哪些像素(并且带有 .9.png
扩展名,而不只是 .png
)。如图 5 中所示,左边缘和上边缘的黑线之间的交点是可以拉伸的位图区域。
或者,您也可以定义内容在视图内应进入的安全区域,方法是以同样的方式在右边缘和下边缘添加线条。
将九宫格作为背景应用于视图时,框架会正确拉伸图片以适应按钮的尺寸。
如需根据位图创建九宫格图片方面的帮助,请参阅创建可调整大小的位图。
2.3 适配不同像素密度ppi的Android设备
Android 设备不仅有不同的屏幕尺寸(手机、平板电脑、电视等),而且其屏幕也有不同的像素尺寸。也就是说,有可能一部设备的屏幕为每平方英寸 160 像素,而另一部设备的屏幕为每平方英寸 480 像素。如果您不考虑像素密度的这些差异,系统可能会缩放图片(导致图片变模糊),或者图片可能会以完全错误的尺寸显示。
本页向您介绍如何设计应用来支持不同的像素密度,那就是使用分辨率无关度量单位,并针对每种像素密度提供备用位图资源。
2.3.1 使用密度无关像素dp/dip
密度无关像素是一个基于屏幕物理密度的度量单位,在160dpi的屏幕中1dp大约等于1px。当在更高密度的屏幕上运行时,用于绘制1dp的像素数量会被一个适合屏幕dpi的density因子放大,例如在320dpi的屏幕中1dp大约等于2px。而在低密度屏幕上,1dp的像素数量会减少。
也就是说,dp与px的比值与屏幕物理密度成正相关,但不一定成正比。度量单位dp可以在布局中适当地调整UI组件的大小,以适合不同的屏幕密度。换句话说,它为您在不同设备上的UI元素的真实大小提供了一致性。
为什么说dp与px的比值与屏幕物理密度成正相关,而不是成正比呢?
这主要是因为Android开发者可以在代码中指定浮点数的dp或者px,但当在设备屏幕上按照对应的dp或px显示内容时要进行四舍五入取整,因为设备屏幕都是一个个像素点构成的,因此它们的关系是成正相关。
举个例子,一个宽高为11dp*11dp的ImageView,在density为1.0的设备屏幕上显示的实际像素为11px*11px,但在density为1.5的设备屏幕上显示的实际像素为17px*17px,而不是16.5px*16.5px。因为一个1px*1px的像素点只能是要么显示,要么不显示,所以Android系统中会将16.5px*16.5px四舍五入取整为17px*17px。
Android官方文档《支持不同的像素密度》对于屏幕适配的观点是:必须避免的第一个陷阱是使用像素px来定义距离或尺寸。使用像素来定义尺寸会带来问题,因为不同的屏幕具有不同的像素密度,所以同样数量的像素在不同的设备上可能对应于不同的物理尺寸。
例如下方同样是4.0英寸的两部手机,左边那台手机的分辨率很低,是320px*180px,右边那台是960px*540px。如果将显示字母a图片的ImageView宽高都设置为100px,那么左边手机显示的字母a图片很大,而右边手机显示的字母a图片很小。
只有将显示字母a图片的ImageView宽高都设置为100dp,才能出现下面的效果,即该字母a图片在两台分辨率不同的手机看起来实际的物理宽高差不多一致,而不是一大一小。
要在密度不同的屏幕上保持一个UI组件显示出相同的实际物理宽高,您必须使用密度无关像素 (dp) 作为度量单位来设计界面。dp是一个虚拟像素单位,1dp约等于在在基准密度160dpi屏幕上的1px。对于其他每个密度,Android 会将此值转换为相应的实际像素数。
2.3.2 提供备用位图
要在像素密度不同的设备上提供良好的图形质量,您应该以相应的分辨率在应用中提供每个位图的多个版本(针对每个密度级别提供一个版本)。否则,Android 系统必须缩放位图,使其在每个屏幕上占据相同的可见空间,从而导致缩放失真,如模糊。
您的应用中有多个密度级别可供使用。下表描述了可用的不同配置限定符以及它们适用的屏幕类型。
表 1. 适用于不同像素密度的配置限定符。
密度限定符 | 说明 |
---|---|
ldpi | 适用于低密度 (ldpi) 屏幕 (~ 120dpi) 的资源。 |
mdpi | 适用于中密度 (mdpi) 屏幕 (~ 160dpi) 的资源(这是基准密度)。 |
hdpi | 适用于高密度 (hdpi) 屏幕 (~ 240dpi) 的资源。 |
xhdpi | 适用于加高 (xhdpi) 密度屏幕 (~ 320dpi) 的资源。 |
xxhdpi | 适用于超超高密度 (xxhdpi) 屏幕 (~ 480dpi) 的资源。 |
xxxhdpi | 适用于超超超高密度 (xxxhdpi) 屏幕 (~ 640dpi) 的资源。 |
nodpi | 适用于所有密度的资源。这些是与密度无关的资源。无论当前屏幕的密度是多少,系统都不会缩放以此限定符标记的资源。 |
tvdpi | 适用于密度介于 mdpi 和 hdpi 之间的屏幕(约 213dpi)的资源。这不属于“主要”密度组。它主要用于电视,而大多数应用都不需要它。对于大多数应用而言,提供 mdpi 和 hdpi 资源便已足够,系统将视情况对其进行缩放。如果您发现有必要提供 tvdpi 资源,应按一个系数来确定其大小,即 1.33*mdpi。例如,如果某张图片在 mdpi 屏幕上的大小为 100px x 100px,那么它在 tvdpi 屏幕上的大小应该为 133px x 133px。 |
要针对不同的密度创建备用可绘制位图资源,您应遵循六种主要密度之间的 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/
awesome-image.png
drawable-nodpi/
awesome-image.png
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/awesome-image
时,系统便会根据屏幕 dpi 选择相应的位图。如果您没有为某个密度提供特定于密度的资源,那么系统会选取下一个最佳匹配项并对其进行缩放以适合屏幕。
举个例子,drawable、drawable-mdpi、drawable-xhdpi文件夹下都有对应分辨率图片资源image1.png:
-
那么xhdpi的设备直接在drawable-xhdpi文件夹下找到该图片,接着显示到屏幕上;
-
而hdpi的设备先在drawable-hdpi文件夹下没找到该图片,再去drawable-xhdpi文件夹下找到该图片,并将其缩小到hdpi对应的分辨率,接着显示到屏幕上;
-
而xxhdpi的设备先在drawable-xxhdpi文件夹下没找到该图片,再去drawable-xxxhdpi文件夹下也没找到该图片,最后只能退而求其次去drawable-xhdpi文件夹下找到该图片,并将其放大到xxhdpi对应的分辨率(注意这个放大过程会失真),接着显示到屏幕上。
再举个例子,只有drawable文件夹下有图片资源image2.png,那么那么xhdpi的设备会按照drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi、drawable-hdpi、drawable-mdpi、drawable文件夹从前到后的优先级顺序去查找这个图片资源,最终是在drawable文件夹中找到该图片资源,并将其放大或缩小到xhdpi对应的分辨率(放大会失真,缩小则不会),接着显示到屏幕上。
提示:如果您有一些系统应该永远不会缩放(或许是因为您在运行时亲自对图像做一些调整)的可绘制对象资源,则应将它们放在有
nodpi
配置限定符的目录中。使用此限定符的资源被视为与密度无关,系统不会缩放它们。
如需详细了解其他配置限定符以及 Android 如何根据当前屏幕配置选择适当的资源,请参阅提供资源。
2.3.2.1 位图错放密度限定符文件夹的后果
如果有一张1MB大小、3500px*2500px的image.jpg图片,那么它在APP中加载会占用多少内存呢?是不是也是占用1MB的内存?
首先我们要知道默认情况下,一个像素占用4个字节,因为Android默认采用ARGB来表示颜色,例如#FFFF7A25
表示完全不透明的橙色。接着我们来看看计算APP中加载图片所占用内存的公式:
APP中加载图片所占用内存=横向像素数*纵向像素数*每个像素占用的内存(默认为4)
因此一张1MB大小、3500px*2500px的image.jpg图片,它在APP中所占用内存为3500*2500*4/1024/1024≈33.3786MB,这么一张大分辨率的图片足以让APP抛出一个OOM内存溢出的异常。
那么为什么一张实际占用空间大小为1MB的图片在APP中加载图片所占用内存为33.3786MB呢,这主要是因为该图片是应用了相应压缩算法的jpg图片,而在Android中使用Bitmap时需要该图片的原始数据,所以要按照上述的公式进行计算。
如果把该图片仅存放于drawable-mdpi
文件夹下,而屏幕密度对应为xhdpi的设备在APP中加载该图片会占用约133.5144MB的内存,是原来理论值33.3786MB的四倍,这就更容易导致内存溢出了。这主要是因为Android系统会把drawable-mdpi
文件夹下的该图片的宽和高各自扩大2倍,以保证该图片能在屏幕密度对应为xhdpi的设备上正常显示。
再举另一个例子,如果把该图片仅存放且正确地存放于drawable-hxdpi
文件夹下,而屏幕密度对应为mdpi的设备在APP中加载该图片会占用约8.3447MB的内存,是原来理论值33.3786MB的四分之一。但是Google仍然不推荐我们只存放xhdpi分辨率下的该图片,而是应该提供mdpi和hdpi分辨率下备用位图,这样屏幕密度对应为mdpi的设备就能直接加载drawable-mdpi
文件夹下的该图片,就不用再从drawable-xhdpi
文件夹中取出该图片再进行缩小了,因为这个缩小过程也是会消耗系统资源的,这也是为什么Google要求我们为mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi屏幕密度设备提供备用位图的原因。
具体的比例关系如下图所示(NX表示表示宽和高是mdpi的N倍,总内存就是N*N倍):
这也是为什么Android要求备用位图要放在对应密度限定符的文件夹中,而位图错放密度限定符的文件夹可能导致OOM内存溢出、消耗不必要的系统资源等问题。
那么在将仅存在于drawable-xhdpi
文件夹下的某图片缩小到适配mdpi屏幕分辨率设备的过程中,一定要将此图片完整地加载进入进内存再进行缩小的过程吗?
当然不是,可以使用BitmapFactory.decode()
传入一个Options对象,进行某些设置即可读取到该图片的宽高和类型,而不需要读取整个图片到内存中。读取了图片的宽高属性后,可以按照想要的比例进行缩放读取。我的理解是如果你设置的压缩比是2(横向纵向像素各缩小2倍,总压缩比4倍),在BitmapFactory进行decode的时候知道了你的压缩比,它会选择性的读取某些像素点(具体算法未知,可以简单的想成比如原图是100px*100px,现在我们获得到了它的输入流,只是平均地从输入流中读取50px*50px的像素)。这样就实现了大图片不加载到内存先压缩的过程。
这种做法可以用在本地图片的显示处理,同时也可以用在加载网络图片的时候。都是为了避免OOM,同时实现对大图片进行压缩。
2.3.3 mipmap目录存放应用图标
与其他所有位图资源一样,对于应用图标,您也需要提供特定于密度的版本。不过,某些应用启动器显示的应用图标会比设备的密度级别所要求的大差不多 25%。
例如,如果设备的密度存储分区为 xxhdpi,而您提供的最大应用图标密度级别为 drawable-xxhdpi
,则启动器应用会放大此图标,这会导致它看起来不太清晰。因此,您应在 mipmap-xxxhdpi
目录中提供一个密度更高的启动器图标,而后启动器便可改用 xxxhdpi 资源。
由于应用图标可能会像这样放大,因此您应将所有应用图标都放在 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
有关图标设计指南,请参阅图标的素材指南。如需构建应用图标方面的帮助,请参阅使用 Image Asset Studio 创建应用图标。
2.4 适配以dp为单位的不同最小宽度的Android设备
2.4.1 以px为度量单位的基于屏幕分辨率限定符的百分比布局(2015年已废弃方案)
苦于Android屏幕碎片化的问题,最早在2015年5月,张鸿洋在其文章《Android 屏幕适配方案》中提出了以px为度量单位的基于屏幕分辨率限定符的百分比布局的Android屏幕适配方案,后文中该方案会简称为“屏幕分辨率限定符Android屏幕适配方案”。其原理主要是在 res 文件夹下创建各种屏幕分辨率对应的 values-xxx 文件夹,然后根据一个基准分辨率,生成各种分辨率对应的 dimens.xml 文件。
例如基准分辨率为 1280x720,将宽度分成 720 份,取值为 1px~720px,将高度分成 1280 份,取值为 1px~1280px。假设设计图上的一个控件的宽度为 720px,那么布局中就写 android:layout_width="@dimen/x720" ,当运行程序的时候,系统会根据设备的分辨率去寻找对应的 dimens.xml 文件。例如运行在分辨率为 1280x720 的设备上,系统会自动找到对应的 values-1280x720 文件夹下的 lay_x.xml 文件,由上图可知 x720 对应的值为720.px,可铺满该屏幕宽度。运行在分辨率为 1920x1080 的设备上,系统会自动找到对应的 values-1920x1080 文件夹下的 lay_x.xml 文件,由上图可知 x720 对应的值为 1080.0px,可铺满该屏幕宽度。这样就达到了屏幕适配的要求。
但是这种Android屏幕适配方案存在着很大的缺陷:
- 屏幕分辨率限定符适配是以屏幕分辨率为基准的,Android 设备分辨率一大堆,而且还要考虑部分带有虚拟导航栏的Android设备,因为带和不带虚拟导航栏的Android设备的APP可用屏幕分辨率是不同的,这样就需要大量的 dimens.xml 文件;
- 屏幕分辨率限定符适配采用的是 px 单位,而Google在Android开发者官方文档《支持不同的像素密度》中对于屏幕适配的观点是:必须避免的第一个陷阱是使用像素px来定义距离或尺寸;
- 屏幕分辨率限定符适配需要设备分辨率与 values-xx 文件夹完全匹配才能达到适配,如果数值有一点点差距都无法匹配,因此这也导致需要大量的dimens.xml 文件。
也正是由于有以上的缺陷,后续在2018年4月简书博主Wildma提出了以dp为度量单位的基于最小宽度限定符的百分比布局的Android屏幕适配方案。
2.4.2 以dp为度量单位的基于最小宽度限定符的百分比布局(2018年方案)
基于屏幕分辨率限定符Android屏幕适配方案的灵感,在2018年4月简书博主Wildma发表了文章《一种非常好用的Android屏幕适配—smallestWidth 限定符适配》,提出了以dp为度量单位的基于最小宽度限定符的百分比布局的Android屏幕适配方案,后文中该方案会简称为“最小宽度限定符Android屏幕适配方案”。
该适配方案的原理便是以设计图最小宽度(单位为 dp)作为基准值,利用插件生成所有设备对应的 dimens.xml 文件,再根据设计图标注,标注多少 dp,布局中就写多少dp,格式为@dimen/dp_XX,最后系统都是根据限定符去寻找对应的 dimens.xml 文件。
例如程序运行在最小宽度为 360dp 的设备上,系统会自动找到对应的 values-sw360dp 文件夹下的 dimens.xml 文件。区别就在于屏幕分辨率限定符适配是拿 px 值等比例缩放,而最小宽度限定符Android屏幕适配方案是拿 dp 值来等比缩放而已。需要注意的是“最小宽度”是不区分方向的,即无论是宽度还是高度,哪一边小就认为哪一边是“最小宽度”。
相比于屏幕分辨率限定符Android屏幕适配方案,最小宽度限定符Android屏幕适配方案有以下优势:
- 屏幕分辨率限定符适配是以屏幕分辨率为基准的,Android 设备分辨率一大堆,而且还要考虑部分带有虚拟导航栏的Android设备,因为带和不带虚拟导航栏的Android设备的APP可用屏幕分辨率是不同的,这样就需要大量的 dimens.xml 文件。但是无论手机屏幕的像素是多少、密度是多少,90%的手机的最小宽度都为360dp,所以采用最小宽度限定符Android屏幕适配方案只需要少量 dimens.xml 文件即可;
- 屏幕分辨率限定符适配采用的是 px 单位,而最小宽度限定符Android屏幕适配方案采用的单位是 dp 和 sp,dp 和 sp 是 google 推荐使用的计量单位。又由于很多应用要求字体大小随系统改变,所以字体单位使用 sp 也更灵活;
- 屏幕分辨率限定符适配需要设备分辨率与 values-xx 文件夹完全匹配才能达到适配,而最小宽度限定符Android屏幕适配方案寻找 dimens.xml 文件的原理是从大往小找,例如设备的最小宽度为 360dp,就会先去找 values-360dp,发现没有则会向下找 values-320dp,如果还是没有才找默认的 values 下的 demens.xml 文件,所以即使没有完全匹配也能达到不错的适配效果。
但是最小宽度限定符Android屏幕适配方案也不是万能的,它也有一个严重的缺陷:
布局UI组件宽高属性的描述符不具有通用性。绝大部分的Android项目中,布局UI组件宽高属性的描述符都是"XXdp",而该方案用的是"@dimen/dp_XX",那么之前使用"XXdp"描述符的Android老项目需要一一进行修改,既要考虑"XXdp"描述符和"@dimen/dp_XX"描述符转换的问题。另外如果引入一些第三方的自定义View,那么这些第三方的自定义View的宽高属性可能无法被此方案进行统一控制。
但总的来说,最小宽度限定符Android屏幕适配方案也非常优秀,如果想要体验这个项目,可访问该适配方案的GitHub项目链接地址:ScreenAdaptation。
2.4.3 使用ConstraintLayout的比例约束功能(2018年至今最优方案)
早在2016年,Google便推出了支持Android2.3及以上版本的约束布局(ConstraintLayout) V1.0,它的出现主要是为了解决布局嵌套过多的问题,以灵活的方式定位和调整小部件。也是从 Android Studio 2.3 起,新建的Activity默认所带的模板xml文件便是使用ConstraintLayout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
但真正引起Android开发者激动的是在2018年发布的V1.1稳定版的ConstraintLayout,新版本引入了按照比例约束控件位置和尺寸的新功能,能够更好地适配屏幕大小不同的机型。而这个功能便是相比于最小宽度限定符Android屏幕适配方案更优的选择。布局UI控件指定"app:layout_constraintWidth_percent"和"app:layout_constraintHeight_percent"属性,来约束布局UI控件的宽高在可用空间中的百分比比例。因此,使用几行 XML 代码就可以使 Button 或 TextView 展开并以百分比填充屏幕:
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintWidth_percent="0.7"
android:text="70% WIDTH" />
有些人考虑到了嵌套布局带来的风险,所以用一个RelativeLayout来装下所有的控件。那么问题来了,既然用RelativeLayout可以解决问题,为什么还要使用ConstraintLayout呢?因为ConstraintLayout使用起来比RelativeLayout更灵活,性能更出色!还有一点就是ConstraintLayout可以按照比例约束控件位置和尺寸,能够更好地适配屏幕大小不同的机型。
相比于多层嵌套的LinearLayout和RelativeLayout,为什么优选ConstraintLayout?
在开发过程中经常能遇到一些复杂的UI,可能会出现布局嵌套过多的问题,嵌套得越多,设备绘制视图所需的时间和计算功耗也就越多。
有些人考虑到了嵌套布局带来的风险,所以用一个RelativeLayout来装下所有的控件。那么问题来了,既然用RelativeLayout可以解决问题,为什么还要使用ConstraintLayout呢?因为ConstraintLayout使用起来比RelativeLayout更灵活,性能更出色!还有一点就是ConstraintLayout可以按照比例约束控件位置和尺寸,能够更好地适配屏幕大小不同的机型。
2.5 适配刘海屏(Display Cutouts)设备
刘海屏是指某些设备显示屏上的一个区域延伸到显示面,这样既能为用户提供全面屏体验,又能为设备正面的重要传感器留出空间。Android 在搭载 Android 9(API 级别 28)及更高版本的设备上正式支持刘海屏。请注意,设备制造商也可以选择在搭载 Android 8.1 或更低版本的设备上支持刘海屏。
本主题介绍如何实现对带刘海屏的设备的支持,包括如何处理“刘海区域”,即显示面上包含刘海的无边框矩形。
2.5.1 刘海屏设备所具备的特质
为了确保一致性和应用兼容性,搭载 Android 9 的设备必须确保以下刘海行为:
- 一条边缘最多只能包含一个刘海;
- 一台设备不能有两个以上的刘海;
- 设备的两条较长边缘上不能有刘海;
- 在未设置特殊标志的竖屏模式下,状态栏的高度必须至少与刘海的高度持平;
- 默认情况下,在全屏模式或横屏模式下,整个刘海区域必须显示黑边。
2.5.2 APP处理刘海区域的方案
如果不希望您的内容与刘海区域重叠,请确保您的内容不与状态栏和导航栏重叠,这样做一般就足够了。如果您要将内容呈现到刘海区域中,则可以使用 WindowInsets.getDisplayCutout() 来检索 DisplayCutout 对象,该对象包含每个刘海区域的安全边衬区和边界框。您可以使用这些 API 来检查您的内容是否与刘海区域重叠,以便根据需要重新放置。
注意:要在多个 API 级别管理刘海实现,您还可以使用 AndroidX 库(可通过 SDK 管理器获得)中的 DisplayCutoutCompat。
Android 还允许您控制是否在刘海区域内显示内容。窗口布局属性 layoutInDisplayCutoutMode 控制您的内容如何呈现在刘海区域中。您可以将 layoutInDisplayCutoutMode
设为以下某个值:
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT - 这是默认行为,如上所述。在竖屏模式下,内容会呈现到刘海区域中;但在横屏模式下,内容会显示黑边。
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - 在竖屏模式和横屏模式下,内容都会呈现到刘海区域中。
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - 内容从不呈现到刘海区域中。
您可以通过编程或在 Activity 中设置样式来设置刘海模式。以下示例定义了一种样式,您可以使用它将LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
属性应用到 Activity。
<style name="ActivityTheme">
<item name="android:windowLayoutInDisplayCutoutMode">
shortEdges <!-- default, shortEdges, never -->
</item>
</style>
下面几部分更详细地介绍了不同的刘海模式。
2.5.2.1 默认方案
默认情况下,在未设置特殊标志的竖屏模式下,在带刘海屏的设备上,状态栏的大小会调整为至少与刘海一样高,而您的内容会显示在下方区域。在横屏模式或全屏模式下,您的应用窗口会显示黑边,因此您的任何内容都不会显示在刘海区域中。
2.5.2.2 将内容呈现在短边刘海区域中
对于某些内容(如视频、照片、地图和游戏),呈现在刘海区域中是一种很好的方法,这样能够为用户提供沉浸感更强的全面屏体验。如果设置了 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,则在竖屏模式和横屏模式下,内容都会延伸到显示屏的短边上的刘海区域,而不管系统栏处于隐藏还是可见状态。请注意,窗口无法延伸到屏幕的长边上的刘海区域。使用此模式时,请确保没有重要内容与刘海区域重叠。
请注意,Android 可能不允许内容视图与系统栏重叠。要替换此行为并强制内容延伸到刘海区域,请通过 View.setSystemUiVisibility(int) 方法将以下任一标志应用于视图可见性:
- SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- SYSTEM_UI_FLAG_LAYOUT_STABLE
下面是一些 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
示例:
请注意,边角处的刘海可等同于在短边上,因此适用同样的行为:
2.5.2.3 从不将内容呈现在刘海区域中
如果设置了 LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER,则不允许窗口与刘海区域重叠。
此模式应该用于暂时设置 View.SYSTEM_UI_FLAG_FULLSCREEN 或 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 的窗口,以避免在设置或清除了该标志时执行另一种窗口布局。
请查看下面的 LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
示例:
2.5.3 适配刘海屏的最佳方案
使用刘海屏时,请务必考虑以下几点:
-
不要让刘海区域遮盖任何重要的文本、控件或其他信息。
-
不要将任何需要精细轻触识别的交互式元素放置或延伸到刘海区域。刘海区域中的轻触灵敏度可能要比其他区域低一些。
-
避免对状态栏高度进行硬编码,因为这样做可能会导致内容重叠或被切断。如有可能,请使用 WindowInsetsCompat 检索状态栏高度,并确定要对您的内容应用的适当内边距。
-
不要假定应用会占据整个窗口,而应使用 View.getLocationInWindow() 来确认应用的位置。不要使用 View.getLocationOnScreen()。
-
务必妥善处理进入或退出全屏模式。请阅读这篇 Android 开发者博文。
-
对于竖屏模式下的默认刘海行为,如果刘海区域位于顶部边缘,并且窗口未设置 FLAG_FULLSCREEN 或 View.SYSTEM_UI_FLAG_FULLSCREEN,则窗口可以延伸到刘海区域。同样,如果刘海区域位于底部边缘,并且窗口未设置 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,则窗口可以延伸到刘海区域。在全屏模式或横屏模式下,窗口的布局方式应确保其不与刘海区域重叠。
-
如果您的应用需要进入和退出全屏模式,请使用
shortEdges
或never
刘海模式。默认刘海行为可导致应用中的内容在全屏模式转换过程中上下移动,如下图所示:
-
在全屏模式下,在使用窗口坐标与屏幕坐标时应保持谨慎,因为在显示黑边的情况下,您的应用不会占据整个屏幕。由于显示黑边,因此根据屏幕原点得到的坐标与根据窗口原点得到的坐标不再相同。您可以根据需要使用 getLocationOnScreen() 将屏幕坐标转换为视图坐标。下图展示了内容显示黑边时这两种坐标有何不同:
处理MotionEvent
时,请使用 MotionEvent.getX() 和 MotionEvent.getY() 来避免类似的坐标问题。不要使用 MotionEvent.getRawX() 或 MotionEvent.getRawY()。
2.5.4 测试APP在刘海屏设备的呈现效果
请务必测试应用的所有屏幕和体验。如有可能,在具有不同类型刘海屏的设备上进行测试。如果您没有带刘海屏的设备,可以在搭载 Android 9 的任意设备或模拟器上模拟一些常见的刘海配置,具体操作步骤如下:
- 启用开发者选项。
- 在开发者选项屏幕中,向下滚动到绘制部分,然后选择模拟刘海屏。
- 选择刘海类型。
本文参考文献:
Android开发者-文档-指南-设备兼容性-设备兼容性概览
Android开发者-文档-指南-设备兼容性-支持不同的屏幕尺寸
Android开发者-文档-指南-设备兼容性-支持不同的像素密度
【Android屏幕适配】浅析px、dp、ppi、dpi、sp
iOS开发中使用的单位pt与ps中的pt是不是同一个概念?个人觉得不是。望高手解答…?