原文地址:http://android.xsoftlab.net/training/multiscreen/index.html
引言
Android运行于数以百计不同尺寸的设备上。范围小到手持移动电话,大到电视设备。因此,在设计APP时应当兼顾到尽可能多的屏幕尺寸。这样才能照顾到较多的潜在用户。
但是仅仅考虑不同的设备类型还不够。每一种尺寸为用户提供了不同的可能性与挑战,所以为了使用户感到满意,应用程序需要做的不单单是支持多样的屏幕:它还必须对每种屏幕结构将用户体验优化到最佳。
这节课将会学习如何实现针对屏幕结构优化的用户界面。
Note: 这节课与相关示例程序均使用的是support library。
支持不同的屏幕尺寸
这节课将会学习通过以下方式来支持不同的屏幕尺寸:
- 确保布局可以灵活的调整尺寸。
- 对不同的屏幕结构提供适当的UI布局。
- 确保在正确的屏幕中使用了正确的布局。
- 提供可以正常缩放的位图。
使用”wrap_content”及”match_parent”
为了使布局可以灵活的适配不同的屏幕尺寸,应当对某些View组件的width,height属性使用”wrap_content”或”match_parent”。如果使用了”wrap_content”,那么View的高宽会被设置为View内容所需的最小尺寸。然而”match_parent”会使View的高宽扩展到父布局的尺寸大小。
通过使用”wrap_content”或”match_parent”可以使View高宽扩展到View所需要的大小或者扩展到父布局的可用空间:
<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”的。这可以使布局正确的适配不同的屏幕尺寸及方向。
下图是布局在垂直及水平方向的示例。注意View的尺寸会自动适配屏幕的高宽:
使用RelativeLayout
你可以使用LinearLayout结合”wrap_content”或”match_parent”构造相对复杂的布局。然而,LinearLayout不能够精确的控制子View的相对关系。在LinearLayout中View只能简单的被线性排列。如果需要调整View间的相对关系,一种较好的解决方式就是使用RelativeLayout,它允许指定View间的相对关系。下面的示例中,你可以指定一个View靠着另一个View的左边,而另一个View的右边则靠着屏幕的右边。
<?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>
下图是该布局在QVGA屏幕中的显示效果:
下图是该布局在大屏幕中的显示效果:
要注意虽然这些View的尺寸发生了改变,但是其它之间的相对关系还是保留了下来。
使用尺寸限定符
上面我们学习了如何利用灵活布局或者相对布局来匹配不同的屏幕,然而这对于匹配任何屏幕来说还不够好。因此,应用程序不单单只是实现灵活的布局,还应该对不同的屏幕配置提供相应的布局。可以通过configuration qualifiers中所描述的内容学习具体细节,它可以使程序在运行时根据当前的屏幕配置来自动选择对应的资源。
比如说,很多应用程序针对于大屏幕实现了”two pane”的模式。平板与电视大到足以同时显示两个面板,但是移动电话只能同时显示其中一个。所以,要实现这种布局,项目中应当含有以下文件:
- 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标识符。这个布局会在屏幕类型为large时被采用(比如,7英寸的平板或者更大的设备)。其它布局则会被小型设备所采用。
使用最小宽度标识符
开发者会遇到的困难之一就是在3.2之前Android的设备只有”large”屏幕尺寸,这包括了Dell Streak、Galaxy Tab以及常规的7英寸平板。然而,很多应用程序希望可以在这个范围下不同尺寸的设备中展示不同的布局,比如5英寸的设备或者7英寸的设备,甚至是所有的”large”设备都想考虑在内。这就是为什么Android会3.2的版本中引入”最小宽度(Smallest-width)”标识符的原因。
最小宽度限定符允许将最小宽度为给定的dp宽度的设备作为目标。比如说,经典的7英寸平板的最小宽度为600dp,如果希望可以在这块屏幕上同时放置两个面板的话,可以直接使用上面部分中所介绍的双面板布局。不过这里则不是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之前并不会有什么效果,因为3.2之前的系统识别不出sw600dp这种尺寸标识符,所以最好还是保留large标识符,所以会有一个名为res/layout-large/main.xml的布局文件,其中的内容与res/layout-sw600dp/main.xml保持一致。下面的部分将会介绍一种技术来避免重复的布局文件。
使用布局别称
最小宽度限定符只在Android 3.2上开始可用。因此,开发者还应当继续使用抽象尺寸标志(small, normal, large及xlarge)来兼容较早的版本。所以,如果希望在移动电话中显示单面板UI,在其它较大的屏幕中采用多面板UI,那么项目中应该含有以下文件:
- res/layout/main.xml:单面板布局
- res/layout-large:多面板布局
- res/layout-sw600dp:多面板布局
这后面两个文件是完全相同的,因为其中一个是用来匹配Android 3.2的设备的,而另一个是用来匹配较早版本的设备的。
为了避免存在这种重复的文件,可以使用别名文件技术。比如,你可以定义如下布局文件:
- 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_twopanes的别名设置为了main而已。一旦这些文件包含了large 或sw600dp,那么所有的系统则不会再专门区分版本。
使用方向标识符
有些布局在垂直及水平方向上均表现良好。但在新闻阅读示例APP中,针对每一种屏幕尺寸与方向均专门定义了布局:
- small screen, portrait: 单面板,带Logo
- small screen, landscape : 单面板,带Logo
- 7” tablet, portrait : 单面板,带ActionBar
- 7” tablet, landscape : 多面板,带ActionBar
- 10” tablet, portrait : 多窄面板,带ActionBar
- 10” tablet, landscape : 多面板,带ActionBar
- TV, landscape : 多面板,带ActionBar
上面所有的布局文件都被放置在res/layout/目录下。为了使每一种布局与相关的屏幕配置产生关联,App使用布局别名的方式来匹配每一项配置:
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倍)。
然后通过draw9patch工具将该图片打开,该工具位于tools/目录下,它可以用来标记哪块区域可以被拉伸。拉伸标记位于图片的左边和顶部。你也可以通过在右边及底部绘点的方式来定义内容区域,如下图所示:
注意边上那些黑色的像素点。左边和顶部的点指明了图像可以被拉伸的区域,右边和顶部的点指明了内容区域。
最后还要注意.9.png的扩展名。必须使用该扩展名,因为这是框架将其与普通图片区分的一种方式。
当在使用这张图片作为背景时,框架会将图片拉伸以适应按钮的尺寸,如下图所示: