支持不同屏幕尺寸

本课程将向您介绍如何通过下列方法支持不同的屏幕尺寸:

  • 确保您的布局能够根据屏幕适当地调整大小
  • 根据屏幕配置提供合适的 UI 布局
  • 确保对正确的屏幕应用正确的布局
  • 提供可正常缩放的位图

使用“wrap_content”和“match_parent”

为确保您的布局能够灵活地适应不同的屏幕尺寸,您应该为某些视图组件的宽度和高度使用 "wrap_content""match_parent"。 如果您使用 "wrap_content",视图的宽度或高度将设置为使内容适应该视图所需的最小尺寸,而 "match_parent" 会使组件通过扩展来匹配其父视图的尺寸。

通过使用 "wrap_content""match_parent" 尺寸值来替代硬编码尺寸,您的视图将相应地仅使用该视图所需空间,或者通过扩展填满可用空间。 例如:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout android:layout_width="match_parent" 
                  android:id="@+id/linearLayout1"  
                  android:gravity="center"
                  android:layout_height="50dp">
        <ImageView android:id="@+id/imageView1" 
                   android:layout_height="wrap_content"
                   android:layout_width="wrap_content"
                   android:src="@drawable/logo"
                   android:paddingRight="30dp"
                   android:layout_gravity="left"
                   android:layout_weight="0" />
        <View android:layout_height="wrap_content" 
              android:id="@+id/view1"
              android:layout_width="wrap_content"
              android:layout_weight="1" />
        <Button android:id="@+id/categorybutton"
                android:background="@drawable/button_bg"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:layout_width="120dp"
                style="@style/CategoryButtonStyle"/>
    </LinearLayout>

    <fragment android:id="@+id/headlines" 
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

请注意示例应用如何为组件尺寸使用 "wrap_content""match_parent",而非使用具体尺寸。 这可使布局正确适应不同的屏幕尺寸和屏幕方向。

例如,以下便是此布局在纵向模式和横向模式下的外观。 请注意,组件尺寸会自动适应宽度和高度:

图 1. 纵向模式(左侧)和横向模式(右侧)下的 News Reader 示例应用。

使用 RelativeLayout

您可以使用 LinearLayout 的嵌套实例和 "wrap_content" 尺寸与 "match_parent" 尺寸的组合来构建相当复杂的布局。不过,LinearLayout 不允许您精确控制子视图的空间关系;LinearLayout 中的视图只是排成一行。如果您需要将子视图方向改为直线以外的其他方向,更好的解决方案通常是使用 RelativeLayout,它允许您根据组件之间的空间关系指定布局。 例如,您可以在屏幕左侧布置一个子视图,在屏幕右侧布置另一个子视图。

例如:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/label"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Type here:"/>
    <EditText
        android:id="@+id/entry"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/label"/>
    <Button
        android:id="@+id/ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/entry"
        android:layout_alignParentRight="true"
        android:layout_marginLeft="10dp"
        android:text="OK" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/ok"
        android:layout_alignTop="@id/ok"
        android:text="Cancel" />
</RelativeLayout>

图 2 展示了此布局在 QVGA 屏幕上的显示外观。

图 2. QVGA 屏幕(小屏幕)上的屏幕截图。

图 3 展示了它在较大屏幕上的显示外观。

图 3. WSVGA 屏幕(大屏幕)上的屏幕截图。

请注意,尽管组件的尺寸发生了变化,但其空间关系如 RelativeLayout.LayoutParams 所指定得以保留。

使用尺寸限定符

您可以从前文介绍的那类灵活布局或相对布局中获得的好处是有限的。 尽管这些布局可以通过拉伸组件内部和周围的空间来适应不同的屏幕,却可能无法为每一种屏幕尺寸提供最佳用户体验。 因此,您的应用不仅应实现灵活布局,还应针对不同的屏幕配置提供多种备选布局。 您可以利用配置限定符来实现此目的,它允许运行组件根据当前设备配置(如针对不同屏幕尺寸的不同布局设计)自动选择合适的资源。

例如,许多应用都针对大屏幕实现了“双窗格”模式(应用可以在一个窗格中显示项目列表,在另一个窗格中显示项目内容)。 平板电脑和 TV 足够大,可在一个屏幕上同时容纳两个窗格,但手机屏幕只能独立显示它们。 因此,如需实现这些布局,您可以建立以下文件:

  • res/layout/main.xml,单窗格(默认)布局:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="match_parent" />
    </LinearLayout>
  • res/layout-large/main.xml,双窗格布局:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal">
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="400dp"
                  android:layout_marginRight="10dp"/>
        <fragment android:id="@+id/article"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.ArticleFragment"
                  android:layout_width="fill_parent" />
    </LinearLayout>

请注意第二个布局目录名称中的 large 限定符。在屏幕归类为大屏幕的设备(例如,7 英寸及更大尺寸的平板电脑)上,将选择此布局。 对于小型设备,将选择另一个布局(无限定符)。

使用最小宽度限定符

开发者在 3.2 以下版本 Android 设备上遇到的其中一个困难是“庞大”的屏幕尺寸容器,其中囊括了 Dell Streak、原版 Galaxy Tab 以及常规 7 英寸平板电脑。 不过,尽管这些设备都被视为“大”屏设备,许多应用可能仍需要为此类别中的不同设备(如为 5 英寸和 7 英寸设备)显示不同的布局。 正因如此,Android 在 Android 3.2 中引入了“最小宽度”限定符。

最小宽度限定符允许您将目标锁定在具有特定最小宽度(单位:dp)的屏幕。 例如,典型的 7 英寸平板电脑最小宽度为 600dp,因此,如果您希望您的 UI 在这些屏幕上显示两个窗格(但在较小屏幕上显示单个列表),您同样可以为单窗格布局和双窗格布局使用前文中的两种布局,但不使用 large 尺寸限定符,而是使用 sw600dp 为最小宽度是 600dp 的屏幕指定双窗格布局:

  • res/layout/main.xml,单窗格(默认)布局:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="match_parent" />
    </LinearLayout>
  • res/layout-sw600dp/main.xml,双窗格布局:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal">
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="400dp"
                  android:layout_marginRight="10dp"/>
        <fragment android:id="@+id/article"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.ArticleFragment"
                  android:layout_width="fill_parent" />
    </LinearLayout>

这意味着,最小宽度大于或等于 600dp 的设备将选择 layout-sw600dp/main.xml(双窗格)布局,而屏幕较小的设备将选择 layout/main.xml(单窗格)布局。

不过,这种方法在低于 3.2 版本的设备上不太奏效,因为它们无法将 sw600dp 识别为尺寸限定符,所以您仍需使用 large 限定符。因此,您应该建立一个与 res/layout-sw600dp/main.xml 完全相同的、名为 res/layout-large/main.xml 的文件。下文介绍的技巧可让您避免因此而产生重复的布局文件。

使用布局别名

最小宽度限定符仅在 Android 3.2 及更高版本上提供。因此,您仍应使用兼容早期版本的抽象尺寸容器(小、正常、大和超大)。 例如,如果您想让自己设计的 UI 在手机上显示单窗格 UI,但在 7 英寸平板电脑、TV 及其他大屏设备上显示多窗格 UI,则需要提供下列文件:

  • res/layout/main.xml: 单窗格布局
  • res/layout-large: 多窗格布局
  • res/layout-sw600dp: 多窗格布局

后两个文件完全相同,因为其中一个将由 Android 3.2 设备匹配,另一个是为了照顾使用早期版本 Android 的平板电脑和 TV 的需要。

为避免为平板电脑和 TV 产生相同的重复文件(以及由此带来的维护难题),您可以使用别名文件。 例如,您可以定义下列布局:

  • res/layout/main.xml,单窗格布局
  • res/layout/main_twopanes.xml,双窗格布局

并添加以下两个文件:

  • 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 的别名。由于这些文件具有 largesw600dp 选择器,因此它们适用于任何 Android 版本的平板电脑和电视(低于 3.2 版本的平板电脑和电视匹配 large,高于 3.2 版本者将匹配 sw600dp)。

使用屏幕方向限定符

某些布局在横向和纵向屏幕方向下都表现不错,但其中大多数布局均可通过调整做进一步优化。 在 News Reader 示例应用中,以下是布局在各种屏幕尺寸和屏幕方向下的行为:

  • 小屏幕,纵向:单窗格,带徽标
  • 小屏幕,横向:单窗格,带徽标
  • 7 英寸平板电脑,纵向:单窗格,带操作栏
  • 7 英寸平板电脑,横向:双窗格,宽,带操作栏
  • 10 英寸平板电脑,纵向:双窗格,窄,带操作栏
  • 10 英寸平板电脑,横向:双窗格,宽,带操作栏
  • TV,横向:双窗格,宽,带操作栏

因此,以上每一种布局都在 res/layout/ 目录下的某个 XML 文件中定义。如果之后需要将每一种布局分配给各种屏幕配置,应用会使用布局别名将它们与每一种配置进行匹配:

res/layout/onepane.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout/onepane_with_bar.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout android:layout_width="match_parent" 
                  android:id="@+id/linearLayout1"  
                  android:gravity="center"
                  android:layout_height="50dp">
        <ImageView android:id="@+id/imageView1" 
                   android:layout_height="wrap_content"
                   android:layout_width="wrap_content"
                   android:src="@drawable/logo"
                   android:paddingRight="30dp"
                   android:layout_gravity="left"
                   android:layout_weight="0" />
        <View android:layout_height="wrap_content" 
              android:id="@+id/view1"
              android:layout_width="wrap_content"
              android:layout_weight="1" />
        <Button android:id="@+id/categorybutton"
                android:background="@drawable/button_bg"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:layout_width="120dp"
                style="@style/CategoryButtonStyle"/>
    </LinearLayout>

    <fragment android:id="@+id/headlines" 
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout/twopanes.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

res/layout/twopanes_narrow.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="200dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

至此所有可能的布局均已定义,现在只需使用配置限定符将正确的布局映射到每一种配置。 现在您可以使用布局别名技巧来完成这项工作:

res/values/layouts.xml

<resources>
    <item name="main_layout" type="layout">@layout/onepane_with_bar</item>
    <bool name="has_two_panes">false</bool>
</resources>

res/values-sw600dp-land/layouts.xml

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

res/values-sw600dp-port/layouts.xml

<resources>
    <item name="main_layout" type="layout">@layout/onepane</item>
    <bool name="has_two_panes">false</bool>
</resources>

res/values-large-land/layouts.xml

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

res/values-large-port/layouts.xml

<resources>
    <item name="main_layout" type="layout">@layout/twopanes_narrow</item>
    <bool name="has_two_panes">true</bool>
</resources>

使用九宫格位图

支持不同屏幕尺寸通常意味着您的图像资源也必须能够适应不同的尺寸。 例如,按钮背景必须能够适应其所应用到的任何一种按钮形状。

如果您在可能改变尺寸的组件上使用简单图像,您很快会发现效果有些差强人意,因为运行组件会均匀地拉伸或缩小您的图像。 解决方案是使用九宫格位图,这种特殊格式的 PNG 文件会指示哪些区域可以拉伸,哪些区域不可以拉伸。

因此,在设计将用于尺寸可变组件的位图时,请一律使用九宫格位图。 如需将位图转换为九宫格位图,您可以先从普通图像着手(图 4,为了清晰起见,放大 4 倍显示)。

图 4.button.png

然后通过 SDK 的 draw9patch 实用程序(位于 tools/ 目录中)运行它,在该实用程序中,您可以通过沿左侧和顶部边框绘制像素来标记应拉伸的区域。 您还可以通过沿右侧和底部边框绘制像素来标记应容纳内容的区域,结果会得到图 5。

图 5.button.9.png

请注意边框沿线的黑色像素。顶部和左侧边框上的黑色像素指示可以拉伸图像的位置,右侧和底部边框上的黑色像素则指示应该放置内容的位置。

还请注意 .9.png 扩展。您必须使用此扩展,因为框架就是通过它来检测这是一幅九宫格图像,而不是普通的 PNG 图像。

当您对组件应用此背景时(通过设置 android:background="@drawable/button"),框架会正确拉伸图像来适应按钮的尺寸,如图 6 中的各种尺寸所示。

图 6. 一个使用各种尺寸 button.9.png 九宫格图像的按钮。