安卓 4 入门指南(四)

原文:Beginning Android 4

协议:CC BY-NC-SA 4.0

二十四、定义和使用样式

有时,您会在布局元素中发现一些带有神秘样式属性的代码。例如,在关于线程的章节中,出现了以下ProgressBar:

<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   >   <ProgressBarandroid:id="@+id/progress"     style="?android:attr/progressBarStyleHorizontal"     android:layout_width="fill_parent"     android:layout_height="wrap_content" /> </LinearLayout>

神奇的style属性将我们的ProgressBar从一个普通的圆形变成了一个单杠。

本章简要探讨了样式的概念,包括如何创建样式以及如何将样式应用到自己的小部件中。

款式:DIY 干爽

样式的目的是封装一组您打算重复使用、有条件使用或与您的布局保持分离的属性。主要用例是“不要重复自己”(DRY)——如果你有一堆看起来一样的小部件,使用一个样式来使用“看起来一样”的单一定义,而不是从一个小部件复制到另一个小部件。

如果我们看一个例子,特别是Styles/NowStyled示例项目,这一段会更有意义。这是我们在前面章节中检查过的同一个项目,它有一个全屏按钮,显示活动启动或按钮被按下的日期和时间。在这个例子中,我们想要改变按钮表面的文本外观,这将通过使用一个样式来实现。

这个项目中的res/layout/main.xml文件与第二十章中的文件相同,但是增加了一个style属性:

<?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android"   android:id="@+id/button"   android:text=""   android:layout_width="fill_parent"   android:layout_height="fill_parent"   style="@style/bigred" />

**注意:**因为style属性是股票 XML 的一部分,因此不在android名称空间中,它没有得到android:前缀。

@style/bigred指向一个样式资源。样式资源是值资源,可以在项目中的res/values/目录中找到,或者在其他资源集中找到(例如,res/values-v11/用于仅在 API 级别 11 或更高级别上使用的值资源)。惯例是将样式资源保存在一个styles.xml文件中,比如下面来自NowStyled项目的:

<?xml version="1.0" encoding="utf-8"?> <resources>   <style name="bigred">     <item name="android:textSize">30sp</item>     <item name="android:textColor">#FFFF0000</item>   </style> </resources>

元素提供了样式的名称,这是我们从布局中引用样式时使用的名称。<style>元素的<item>子元素表示应用于任何样式的属性值——在我们的例子中,是我们的Button小部件。因此,我们的Button将有一个相对较大的字体(android:textSize设置为30sp),并且它的文本将显示为红色(android:textColor设置为#FFFF0000)。

项目中的其他地方不需要做任何更改——清单、活动的 Java 代码等等都不需要调整。只需定义样式并将其应用于小部件,就会得到如图 Figure 24–1 所示的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 24–1。**Styles/now style 示例应用

风格的要素

应用样式时需要考虑四个问题:

  • 你把样式属性放在哪里表示你想应用一个样式?
  • 哪些属性可以通过样式来定义?
  • 你如何继承以前定义的风格(你自己的或者来自 Android 的)?
  • 样式定义中的属性可以有哪些值?
在哪里应用样式

style属性可以应用于一个小部件,它只影响那个小部件。

style属性也可以应用于一个容器,它只影响那个容器。但是,这样做不会自动设置其子级的样式。例如,假设res/layout/main.xml看起来像这样:

<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="fill_parent"     android:layout_height="fill_parent" `    style=“@style/bigred”


`

尽管有style属性,最终的 UI 不会有红色大字体的Button文本。样式只影响容器,不影响容器的内容。

你也可以将一个样式应用到一个活动或一个应用中,在这种情况下,它被称为一个主题,这将在本章的后面介绍。

可用的属性

在设计小部件或容器的样式时,您可以在样式本身中应用该小部件或容器的任何属性。因此,如果它出现在 Android JavaDocs 的“XML 属性”或“继承的 XML 属性”部分,您可以将它放在一个样式中。

注意,Android 会忽略无效的样式。因此,如果我们如上所示将bigred样式应用于LinearLayout,一切都会运行良好,只是没有可见的结果。尽管LinearLayout没有android:textSizeandroid:textColor属性,但不会出现编译时故障或运行时异常。

此外,布局指令,如android:layout_width,可以放在一个样式中。

传承一种风格

您还可以通过在<style>元素上指定一个parent属性来表明您想要从另一个样式继承样式属性。例如,看看这个样式资源(你会在第二十八章中再次看到,其中涵盖了使用片段框架的 UI 设计):

<?xml version="1.0" encoding="utf-8"?> <resources>   <style name="activated" parent="android:Theme.Holo">     <item name="android:background">?android:attr/activatedBackgroundIndicator</item>   </style> </resources>

在这里,我们表明我们希望从 Android 内部继承Theme.Holo风格。因此,除了指定我们自己的所有属性定义,我们还指定我们想要来自Theme.Holo的所有属性定义。

在许多情况下,这是不必要的。如果您没有指定父对象,那么您的属性定义将被混合到应用于小部件或容器的任何默认样式中。

可能的值

通常,你在样式中赋予属性的值将是某个常量,比如30sp#FFFF0000。但是,有时您可能希望执行一点间接操作,从您继承的主题中应用一些其他属性值。在这种情况下,您需要使用有点神秘的?android:attr/语法,以及一些相关的魔法咒语。

例如,让我们再来看看这个样式资源:

<?xml version="1.0" encoding="utf-8"?> <resources>   <style name="activated" parent="android:Theme.Holo">     <item name="android:background">?android:attr/activatedBackgroundIndicator</item>   </style> </resources>

这里,我们指出android:background的值不是某个常数值,甚至不是对可提取资源的引用(例如@drawable/my_background)。相反,我们从我们继承的主题中引用了一些其他属性的值——??。无论主题如何定义为activatedBackgroundIndicator都是我们的背景。

有时这适用于整体风格。例如,让我们再来看看ProgressBar:

<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   >   <ProgressBarandroid:id="@+id/progress"     style="?android:attr/progressBarStyleHorizontal"     android:layout_width="fill_parent"     android:layout_height="wrap_content" /> </LinearLayout>

这里,我们的样式属性——不是样式资源——指向主题提供的属性(progressBarStyleHorizontal)。如果你翻翻 Android 的源代码,你会发现这被定义为一个样式资源,确切地说是@android:style/Widget.ProgressBar.Horizontal。因此,我们对 Android 说,我们希望我们的ProgressBar通过?android:attr/progressBarStyleHorizontal的间接方式被命名为@android:style/Widget.ProgressBar.Horizontal

Android 风格系统的这一部分仍然很少被记录,即使是最新发布的 Android 4.0 冰淇淋三明治——整个继承主题是三个简短的段落。谷歌自己建议你看看列出各种风格的 Android 源代码,看看有什么是可能的。

这是继承风格变得重要的一个地方。在本节显示的第一个例子中,我们从Theme.Holo继承,因为我们特别想要来自Theme.HoloactivatedBackgroundIndicator值。该值可能不存在于其他样式中,或者它可能没有我们想要的值。

主题:任何其他名称的风格…

主题是通过<activity><application>元素上的android:theme属性应用于活动或应用的样式。如果你正在应用的主题是你自己的,简单地引用它为@style/…,就像你在一个小部件的style属性中一样。但是,如果你应用的主题来自 Android,通常你会使用一个以@android:style/为前缀的值,比如@android:style/Theme.Dialog@android:style/Theme.Light

在一个主题中,你的重点不是设计小部件,而是设计活动本身。比如下面是@android:style/Theme.NoTitleBar.Fullscreen的定义:

`

`

它指定活动应该接管整个屏幕,移除 Android 1.x 和 2.x 设备上的状态栏(android:windowFullscreen设置为true),以及 Android 3.x 和 4.x 设备上的动作栏。它还指定内容覆盖图——环绕活动内容视图的布局——应该设置为 nothing ( android:windowContentOverlay设置为@null),具有移除标题栏的效果。

主题还可能指定应用于特定小部件的其他样式。例如,我们在根主题(Theme)中看到以下内容:

<item name="progressBarStyleHorizontal">@android:style/Widget.ProgressBar![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U002.jpg) .Horizontal</item>

这里,progressBarStyleHorizontal是指向@android:style/ Widget.ProgressBar.Horizontal的。这就是我们如何能够在我们的ProgressBar小部件中引用?android:attr/progressBarStyleHorizontal,并且我们可以创建我们自己的主题,重新定义progressBarStyleHorizontal以指向一些其他的样式(例如,如果我们想要改变用于实际进度条图像本身的圆角矩形)。

二十五、处理多种屏幕尺寸

在 Android 1.0 发布后的第一年左右,所有生产的 Android 设备都有相同的屏幕分辨率(HVGA,320×480 像素)和尺寸(大约 3.5 英寸,或 9 厘米)。然而,从 2009 年末开始,设备开始出现各种不同的屏幕尺寸和分辨率,从微小的 QVGA (240×320)屏幕到更大的 WVGA (480×800)屏幕。2010 年末,平板电脑和谷歌电视设备出现,提供了更多的屏幕尺寸,随着蜂巢和冰淇淋三明治的发布,平板电脑和更大的屏幕尺寸爆炸式增长。

当然,用户会希望你的应用在所有这些屏幕上都能正常工作,也许会利用更大的屏幕尺寸来增加更大的价值。为此,Android 1.6 增加了新的功能,以帮助更好地支持这些不同的屏幕尺寸和分辨率,这些功能在后续的 Android 版本中得到了扩展。随着 Android 3.0 的发布,可选的片段系统作为一种处理不同屏幕尺寸的更强大——尽管更复杂——的方式被引入。Android 文档广泛介绍了使用传统方法和片段方法处理多种屏幕尺寸的机制。我们鼓励你在阅读本章(以及第二十八章)的同时阅读该文档,以充分理解如何最好地应对,或者利用多种屏幕尺寸。

这一章将处理更多的理论和抽象的设计思想,用一些章节讨论屏幕尺寸的选择和理论。然后,我们将深入探讨如何让一个相当简单的应用很好地处理多种屏幕尺寸。这一章将避免增加片段的复杂性,但是不要害怕:我们将回到第二十八章的主题和片段。

采取默认

让我们假设你一开始完全忽略了屏幕尺寸和分辨率的问题。会发生什么?

如果你的应用是为 Android 1.5 或更低版本编译的,Android 会认为你的应用在传统的屏幕尺寸和分辨率下看起来很好。Android 将自动执行以下操作:

  • 如果你的应用安装在屏幕更大的设备上,Android 将在兼容模式下运行你的应用,根据实际屏幕大小缩放一切。因此,假设你有一个 24 像素的方形 PNG 文件,Android 在一个标准物理尺寸但具有 WVGA 分辨率的设备上安装并运行你的应用(所谓的高密度屏幕)。Android 可能会在显示 PNG 文件时将其缩放为 36 像素,因此它在屏幕上占据相同的可视空间。有利的一面是,Android 会自动处理这个问题;不利的一面是,位图缩放算法会使图像有点模糊。
  • 如果你的应用安装在屏幕较小的设备上,Android 会阻止你的应用运行。因此,QVGA 设备,如 HTC Tattoo,将无法获得您的应用,即使它在 Android 市场上可用。

为了举例说明这如何影响你的应用,Figure 25–1 展示了在 HTC 纹身上看到的Containers/Table示例应用,带有 QVGA 屏幕。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–1。 通过兼容模式在 QVGA 中的表样

如果您的应用是为 Android 1.6 或更高版本编译的,Android 会假设您可以正确处理所有屏幕尺寸,因此不会在兼容模式下运行您的应用。考虑到后续版本的巨大改进,尤其是 Android 2.2、3.0 和 4.0,很少有开发者会将 1.6 之前的版本作为目标。这意味着你几乎总是会自己处理屏幕尺寸管理。在后面的部分中,您将看到如何对其进行定制。

整体合一

在 Android 中处理多种屏幕尺寸的最简单的方法是设计你的用户界面(UI ),使其自动根据屏幕尺寸进行缩放,而不需要任何特定尺寸的代码或资源。换句话说,“它只是工作。”

然而,这意味着你在 UI 中使用的一切都可以被 Android 优雅地缩放,一切都将适合,即使是在 QVGA 屏幕上。

以下部分提供了实现这种一体化解决方案的一些提示。

考虑规则,而不是立场

一些开发人员,也许是那些来自 UI 开发拖放学校的开发人员,首先考虑的是小部件的位置。他们认为他们希望特定的部件在特定的固定位置具有特定的固定大小。他们对 Android 布局管理器(容器)感到失望,并倾向于用他们习惯的方式来设计 ui。

这种方法很少能很好地工作,即使是在桌面上,这可以在不能很好地处理窗口大小调整的应用中看到。同样,这种方法也不适用于移动设备,尤其是 Android,因为它们的屏幕尺寸和分辨率差异很大。

不要想立场,要想规则。你需要教会 Android 关于小部件的大小和位置的“商业规则”,然后 Android 将根据设备屏幕在分辨率方面实际支持的内容来解释这些规则。

最简单的规则是android:layout_widthandroid:layout_heightfill_parentwrap_content值。它们不指定具体的尺寸,而是适应可用的空间。

最容易指定规则的环境是RelativeLayout。虽然表面上很复杂,RelativeLayout做得很好,让你控制你的布局,同时仍然适应其他屏幕尺寸。例如,您可以执行以下操作:

  • 明确地将窗口小部件锚定在屏幕的底部或右侧,而不是希望它们会因为其他布局而出现在那里
  • 控制连接的小部件之间的距离(例如,字段的标签应该在字段的左侧),而不必依赖填充或边距

指定规则的最好方法是创建自己的布局类。例如,假设您正在创建一系列实现纸牌游戏的应用。您可能希望有一个布局类,它知道关于扑克牌的以下内容:它们如何重叠,面朝上还是面朝下,处理不同数量的牌应该有多大,等等。虽然你可以用一个RelativeLayout来实现你想要的外观,但是实现一个PlayingCardLayout或者一个HandOfCardsLayout或者更明确地为你的应用定制的东西可能会更好。不幸的是,创建自定义布局类目前还没有被记录下来。

考虑物理尺寸

Android 提供了大量可用的尺寸测量单位。最流行的是 pixel ( px),因为它很容易让人理解这个概念。毕竟,每个 Android 设备都有一个每个方向都有一定数量像素的屏幕。

但是,随着屏幕密度的变化,像素开始变得麻烦。随着给定屏幕尺寸中像素数量的增加,像素实际上会缩小。传统 Android 设备上的 32 像素图标可能对手指友好,但在高密度设备上(比如手机外形的 WVGA),32 像素对于手指来说可能有点小。

如果你有某种本质上可缩放的东西(例如,Button),你可以考虑使用毫米(mm)或英寸(in)作为度量单位。无论屏幕分辨率还是屏幕尺寸,10 毫米就是 10 毫米。这样,您可以确保小部件的大小适合手指,而不管可能需要多少像素。

避免“真实”像素

在某些情况下,使用毫米表示尺寸没有意义。在这种情况下,您可能需要考虑使用其他度量单位,同时避免使用“真实”像素。

Android 提供了以密度无关像素(dip)测量的尺寸。这些 1:1 映射到 160 dpi 屏幕(例如,经典的 HVGA Android 设备)的像素,并从那里缩放。例如,在 240 dpi 的设备(例如,手机大小的 WVGA 设备)上,该比率是 2:3,因此50dip = 50px在 160 dpi,而= 75px在 240 dpi。用户使用dip的好处是尺寸的实际大小保持不变,因此显然 160 dpi 的50dip和 240 dpi 的50dip没有区别。

Android 还提供按比例像素测量的尺寸(sp)。理论上,缩放像素是根据用户选择的字体大小进行缩放的(System.Settings中的FONT_SCALE值)。

选择可伸缩的抽屉

传统位图——PNG、JPG、BMP 和 GIF——本质上不可扩展,Android 4.0 也不支持最新的图像格式——WEBP。如果你不是在兼容模式下运行,Android 甚至不会尝试根据屏幕分辨率和尺寸来缩放这些内容。无论你提供的位图大小是多少,即使这会使图像在某些屏幕上变得过大或过小。

解决这个问题的一个方法是尽量避免静态位图,使用九补丁位图和 XML 定义的 drawables(如GradientDrawable)作为替代。九片位图是一种 PNG 文件,经过特殊编码,具有指示如何拉伸图像以占据更多空间的规则。XML 定义的 drawables 使用一种准 SVG XML 语言来定义形状、它们的笔画和填充等等。

量身定做,只为你(还有你,还有你,还有……)

有时,您会希望根据屏幕大小或密度拥有不同的外观或行为。Android 提供了一些技术,您可以使用这些技术根据应用运行的环境来切换资源或代码块。当这些技术与上一节描述的技术结合使用时,实现屏幕尺寸和密度独立是完全可能的,至少对于运行 Android 1.6 和更新版本的设备来说是如此。

增加<支撑屏>元素

主动支持不同屏幕尺寸的第一步是将<supports-screens>元素添加到AndroidManifest.xml文件中。这指定了应用明确支持和不支持的屏幕尺寸。它没有明确支持的那些将由自动兼容模式处理,如前所述。

下面是一个包含<supports-screens>元素的清单:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"   package="com.commonsware.android.eu4you"   android:versionCode="1"   android:versionName="1.0">   <supports-screens     android:largeScreens="true"     android:normalScreens="true"     android:smallScreens="true"     android:anyDensity="true"   />   <application android:label="@string/app_name"     android:icon="@drawable/cw">     <activity android:name=".EU4You"               android:label="@string/app_name">       <intent-filter>         <action android:name="android.intent.action.MAIN" />         <category android:name="android.intent.category.LAUNCHER" />       </intent-filter>     </activity>   </application> </manifest>

android:smallScreensandroid:normalScreensandroid:largeScreens属性是不言自明的:每一个属性都有一个布尔值,表明你的应用是明确支持那个尺寸的屏幕(true)还是需要兼容模式帮助(false)。Android 2.3 还为更大的平板电脑、电视等增加了android:xlargeScreens(剧院,有人吗?).

android:anyDensity属性表示您是否在计算中考虑了密度(true)或不考虑密度(false)。如果是false,Android 将会把你的所有尺寸(例如4px)都当作普通密度(160-dpi)的屏幕来处理。如果你的应用运行在更低或更高密度的屏幕上,Android 会相应地缩放你的尺寸。如果你指出android:anyDensity = "true",你就是在告诉 Android 不要这么做,让你承担使用密度无关单位的责任,比如dipmmin

资源和资源集

基于屏幕大小或密度切换不同事物的主要方法是创建资源集。通过创建特定于不同设备特性的资源集,您可以教会 Android 如何渲染每一个资源集,然后 Android 会自动在这些资源集中进行切换。

默认缩放

默认情况下,Android 会缩放所有可提取的资源。那些本质上可伸缩的,如前所述,将会很好地伸缩。普通位图使用普通的缩放算法进行缩放,这可能会也可能不会给你很好的结果。这也可能会降低应用的速度。为了避免这种情况,您需要设置单独的包含不可缩放位图的资源集。

基于密度的集合

如果您希望基于不同的屏幕密度拥有不同的布局、尺寸等,您可以使用-ldpi-mdpi-hdpi-xhdpi资源集标签。例如,res/values-hdpi/dimens.xml将包含高密度设备中使用的尺寸。

请注意,在使用这些屏幕密度资源集时,Android 1.5 (API level 3)中有一个 bug。尽管所有的 Android 1.5 设备都是中等密度,但 Android 1.5 可能会意外地选择其他密度。如果您打算支持 Android 1.5 并使用屏幕密度资源集,您需要克隆您的-mdpi集的内容,克隆名为-mdpi-v3。这个基于版本的集合将在本节稍后详细描述。

基于大小的集合

同样,如果你希望根据屏幕大小拥有不同的资源集,Android 提供了-small-normal-large-xlarge资源集标签。创建res/layout-large-land/将指示在横向大屏幕(如 WVGA)上使用的布局。

基于版本的集合

可能会有早期版本的 Android 被新的资源集标签弄糊涂的时候。为了帮助解决这个问题,您可以向您的资源集添加一个版本标签,格式为-vN,其中N是一个 API 级别。因此,res/drawable-large-v4/表示这些 drawables 应该在 API 级别为 4 (Android 1.6)和更高的大屏幕上使用。

所以,如果你发现 Android 1.5 模拟器或设备正在抓取错误的资源集,可以考虑在它们的资源集名称中添加-v4来过滤掉它们。

找到你的尺码

如果需要根据屏幕大小或密度在 Java 代码中采取不同的动作,有几种选择。

如果您的资源集中有一些与众不同的东西,您可以基于此“嗅”出来,并在代码中相应地进行分支。例如,正如你将在本章后面的代码示例中看到的,你可以在一些布局中有额外的小部件(例如,res/layout-large/main.xml);简单地看看是否有一个额外的小部件存在,就可以知道你是否在运行一个大屏幕。

你也可以通过一个Configuration对象找到你的屏幕尺寸等级,通常由一个Activity通过getResources().getConfiguration()获得。一个Configuration对象有一个名为screenLayout的公共字段,它是一个位掩码,指示应用运行的屏幕类型。您可以测试您的屏幕是小、正常还是大,或者是长(其中“长”表示 16:9 或类似的宽高比,而不是 4:3)。例如,我们在这里测试我们是否在大屏幕上运行:

if (getResources().getConfiguration().screenLayout       & Configuration.SCREENLAYOUT_SIZE_LARGE)     ==Configuration.SCREENLAYOUT_SIZE_LARGE) {  // yes, we are large } else {  // no, we are not }

类似地,您可以使用DisplayMetrics类找出您的屏幕密度,或者您的屏幕尺寸中像素的确切数量。

没有什么比得上真实的东西

Android 模拟器将帮助你在不同尺寸的屏幕上测试你的应用。但是,这只能做到这一步,因为移动设备的 LCD 与台式机或笔记本电脑的 LCD 具有不同的特性,例如:

  • 移动设备 LCD 的密度可能比开发机器的密度高得多。
  • 鼠标允许比实际指尖更精确的触摸屏输入。

在可能的情况下,你将需要以新的和令人兴奋的方式使用模拟器,或者尝试使用具有不同屏幕分辨率的实际设备。

密度不同

摩托罗拉 DROID 有一个 240 dpi、3.7 英寸、480×854 像素的屏幕(FWVGA 显示器)。要模拟 DROID 屏幕,根据像素计算,需要占用 19 英寸、1280×1024 像素液晶显示器的三分之一,因为液晶显示器的密度比 DROID 低得多——约为 96 dpi。因此,当你为像 droid 这样的 FWVGA 显示器启动 Android 模拟器时,你会得到一个巨大的模拟器窗口。

对于在 FWVGA 环境中确定应用的整体外观来说,这仍然是非常好的。无论密度如何,窗口小部件仍将对齐,大小将具有相同的关系(例如,窗口小部件 A 可能是窗口小部件 B 的两倍高,并且无论密度如何都是如此),等等。

但是,请记住以下几点:

  • 在 19 英寸的 LCD 上看起来尺寸合适的东西,在相同分辨率的移动设备屏幕上可能会太小。
  • 在模拟器中,你可以用鼠标轻松点击的东西可能太小,用手指在物理上更小、更密集的屏幕上无法显示出来。
调整密度

默认情况下,仿真器以牺牲密度为代价来保持像素计数的准确性,这就是为什么您会得到真正大的仿真器窗口。不过,您可以选择让仿真器以牺牲像素数量为代价来保持密度的准确性。

最简单的方法是使用 Android 1.6 中引入的 Android AVD 管理器。这个工具的 Android 2.0 版本有一个启动选项对话框,当你通过开始按钮启动一个仿真器实例时会弹出这个对话框,如图 Figure 25–2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–2。??【启动选项】对话框

默认情况下,“按实际大小显示”复选框是未选中的,Android 会正常打开模拟器窗口。您可以选中该复选框,然后提供两位缩放信息:

  • 您希望模拟的设备的屏幕尺寸,以英寸为单位(例如,摩托罗拉 DROID 的屏幕尺寸为 3.7 英寸)
  • 显示器的 dpi(单击?按钮打开计算器,帮助您确定您的 dpi 值)

这为您提供了一个仿真窗口,它更准确地描述了您的用户界面在物理设备上的外观,至少在大小方面是这样的。但是,由于仿真程序使用的像素比设备少得多,因此字体可能难以阅读,图像可能有块状等等。

无情地利用形势

到目前为止,我们已经关注了如何确保你的布局在其他尺寸的屏幕上看起来不错。对于比标准尺寸更小的屏幕(例如 QVGA),这也许是你所能希望达到的。

然而,一旦你进入更大的屏幕,另一种可能性就出现了:使用不同的布局来利用额外的屏幕空间。当物理屏幕尺寸较大时(例如,戴尔 Streak Android 平板电脑上的 5 英寸液晶显示器,或三星 Galaxy Tab 上的 7 英寸液晶显示器),这一点特别有用,而不是简单地在相同的物理空间中拥有更多像素。

以下部分描述了一些利用额外空间的方法。

用按钮代替菜单

选项菜单选择需要两个物理动作:按下菜单按钮,然后点击适当的菜单选项。上下文菜单选择也需要两个物理动作:长时间点击小部件,然后点击菜单选项。上下文菜单具有实际上不可见的额外问题;例如,用户可能没有意识到你的ListView有一个上下文菜单。

你可以考虑增加你的用户界面来提供直接在屏幕上完成事情的方法,否则这些事情可能会隐藏在菜单上。这不仅减少了用户需要采取的步骤数量,而且使这些选项更加明显。

例如,假设您正在创建一个媒体播放器应用,并且希望提供手动播放列表管理。您有一个在ListView中显示播放列表中的歌曲的活动。在选项菜单上,您可以选择添加,将设备上的歌曲添加到播放列表中。在ListView的上下文菜单上,你有一个移除选项,加上上移和下移选项来重新排列列表中的歌曲。不过,对于大屏幕,您可能会考虑为这四个选项在 UI 中添加四个ImageButton小部件,只有当通过 D-pad 或轨迹球选择一行时,上下文菜单中的三个小部件才会启用。在普通或小屏幕上,你会坚持只使用菜单。

用一个简单的活动替换标签

您可能在 UI 中引入了一个TabHost来允许您在可用的屏幕空间中显示更多的小部件。只要您通过将小部件移动到一个单独的选项卡而节省的空间大于选项卡本身占用的空间,您就赢了。然而,拥有多个选项卡意味着需要更多的用户步骤来导航用户界面,特别是当用户需要频繁地在选项卡之间来回切换时。

如果你只有两个标签,考虑改变你的用户界面,提供一个大屏幕布局,去掉标签,把所有的小部件放在一个屏幕上(或者,等待第二十八章关于片段的讨论)。这使得用户无需一直切换标签就能看到所有内容。

如果你有三个或者更多的标签,你可能没有足够的屏幕空间来把这些标签的内容放在一个活动中。然而,你可以考虑对半分:让流行的小部件一直出现在活动中,让你的TabHost在(大约)半个屏幕上处理剩下的部分。

整合多个活动

最强大的技术是使用更大的屏幕来彻底消除活动转换。例如,如果您有一个ListActivity,单击一个项目会在一个单独的活动中显示该项目的详细信息,请考虑支持大屏幕布局,其中详细信息与ListView在同一个活动中(例如,在横向布局中,ListView在左边,在右边)。这消除了用户在查看另一组细节之前必须不断地按后退按钮来离开一组细节的情况。

您将在下一节中展示的示例代码中看到这种技术的应用。

例子:EU4You

为了研究如何使用前面几节中介绍的一些技术,让我们看一下ScreenSizes/EU4You示例应用。这个应用有一个活动(EU4You),其中包含一个ListView,上面有欧盟成员的名单和他们各自的旗帜。点击其中一个国家,就会出现这个国家的移动维基百科页面。

在本书的源代码中,您会发现这个应用的四个版本。我们从一个不知道屏幕大小的应用开始,慢慢地添加更多与屏幕相关的功能。

第一刀

首先,这是我们的AndroidManifest.xml文件,它看起来很像本章前面显示的那个:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"   package="com.commonsware.android.eu4you"   android:versionCode="1"   android:versionName="1.0">   <supports-screens     android:xlargeScreens="true"     android:largeScreens="true"     android:normalScreens="true"     android:smallScreens="true"     android:anyDensity="true"   />   <application android:label="@string/app_name"     android:icon="@drawable/cw">     <activity android:name=".EU4You"               android:label="@string/app_name">       <intent-filter>         <action android:name="android.intent.action.MAIN" />         <category android:name="android.intent.category.LAUNCHER" />       </intent-filter>     </activity>   </application> </manifest>

注意,我们已经包含了<supports-screens>元素,这表明我们确实支持所有的屏幕尺寸。如果我们不指定我们支持某些屏幕尺寸,这将阻止 Android 的大部分自动缩放。

我们的主要布局与尺寸无关,因为它只是一个全屏ListView:

<?xml version="1.0" encoding="utf-8"?> <ListView xmlns:android="http://schemas.android.com/apk/res/android"   android:id="@android:id/list"   android:layout_width="fill_parent"   android:layout_height="fill_parent" />

不过,我们的争吵最终将需要一些调整:

`<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  android:layout_width=“fill_parent”
  android:layout_height=“wrap_content”
  android:padding=“2dip”
  android:minHeight=“?android:attr/listPreferredItemHeight”


  
`

比如现在,我们的字体大小设置为20dip,不会因屏幕大小或密度而变化。

我们的EU4You活动有点冗长,主要是因为有很多欧盟成员,所以我们需要智能地显示行中的标志和文本:

`package com.commonsware.android.eu4you;

import android.app.ListActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;

public class EU4You extends ListActivity {
  static private ArrayList EU=new ArrayList();

static {
    EU.add(new Country(R.string.austria, R.drawable.austria,
                     R.string.austria_url));
    EU.add(new Country(R.string.belgium, R.drawable.belgium,
                     R.string.belgium_url)); EU.add(new Country(R.string.bulgaria, R.drawable.bulgaria,
                     R.string.bulgaria_url));
    EU.add(new Country(R.string.cyprus, R.drawable.cyprus,
                     R.string.cyprus_url));
    EU.add(new Country(R.string.czech_republic,
                     R.drawable.czech_republic,
                     R.string.czech_republic_url));
    EU.add(new Country(R.string.denmark, R.drawable.denmark,
                     R.string.denmark_url));
    EU.add(new Country(R.string.estonia, R.drawable.estonia,
                     R.string.estonia_url));
    EU.add(new Country(R.string.finland, R.drawable.finland,
                     R.string.finland_url));
    EU.add(new Country(R.string.france, R.drawable.france,
                     R.string.france_url));
    EU.add(new Country(R.string.germany, R.drawable.germany,
                     R.string.germany_url));
    EU.add(new Country(R.string.greece, R.drawable.greece,
                     R.string.greece_url));
    EU.add(new Country(R.string.hungary, R.drawable.hungary,
                     R.string.hungary_url));
    EU.add(new Country(R.string.ireland, R.drawable.ireland,
                     R.string.ireland_url));
    EU.add(new Country(R.string.italy, R.drawable.italy,
                     R.string.italy_url));
    EU.add(new Country(R.string.latvia, R.drawable.latvia,
                     R.string.latvia_url));
    EU.add(new Country(R.string.lithuania, R.drawable.lithuania,
                     R.string.lithuania_url));
    EU.add(new Country(R.string.luxembourg, R.drawable.luxembourg,
                     R.string.luxembourg_url));
    EU.add(new Country(R.string.malta, R.drawable.malta,
                     R.string.malta_url));
    EU.add(new Country(R.string.netherlands, R.drawable.netherlands,
                     R.string.netherlands_url));
    EU.add(new Country(R.string.poland, R.drawable.poland,
                     R.string.poland_url));
    EU.add(new Country(R.string.portugal, R.drawable.portugal,
                     R.string.portugal_url));
    EU.add(new Country(R.string.romania, R.drawable.romania,
                     R.string.romania_url));
    EU.add(new Country(R.string.slovakia, R.drawable.slovakia,
                     R.string.slovakia_url));
    EU.add(new Country(R.string.slovenia, R.drawable.slovenia,
                     R.string.slovenia_url));
    EU.add(new Country(R.string.spain, R.drawable.spain,
                     R.string.spain_url));
    EU.add(new Country(R.string.sweden, R.drawable.sweden,
                     R.string.sweden_url));
    EU.add(new Country(R.string.united_kingdom,
                     R.drawable.united_kingdom,
                     R.string.united_kingdom_url));
  }

@Override
  public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    setListAdapter(new CountryAdapter());
  }

@Override
  protected void onListItemClick(ListView l, View v,
                                int position, long id) {
    startActivity(new Intent(Intent.ACTION_VIEW,
                              Uri.parse(getString(EU.get(position).url))));
  }

static class Country {
    int name;
    int flag;
    int url;

Country(int name, int flag, int url) {
      this.name=name;
      this.flag=flag;
      this.url=url;
    }
  }

class CountryAdapter extends ArrayAdapter {
    CountryAdapter() {
      super(EU4You.this, R.layout.row, R.id.name, EU);
    }

@Override
    public View getView(int position, View convertView,
                       ViewGroup parent) {
      CountryWrapper wrapper=null;

if (convertView==null) {
        convertView=getLayoutInflater().inflate(R.layout.row, null);
        wrapper=new CountryWrapper(convertView);
        convertView.setTag(wrapper);
      }
      else {
        wrapper=(CountryWrapper)convertView.getTag();
      }

wrapper.populateFrom(getItem(position));

return(convertView);
    }
  }

class CountryWrapper {
    private TextView name=null;
    private ImageView flag=null;
    private View row=null;

CountryWrapper(View row) {
      this.row=row;     }

TextView getName() {
      if (name==null) {
        name=(TextView)row.findViewById(R.id.name);
      }

return(name);
    }

ImageView getFlag() {
      if (flag==null) {
        flag=(ImageView)row.findViewById(R.id.flag);
      }

return(flag);
    }

void populateFrom(Country nation) {
      getName().setText(nation.name);
      getFlag().setImageResource(nation.flag);
    }
  }
}`

图 25–3、25–4 和 25–5 分别显示了普通 HVGA 仿真器、WVGA 仿真器和 QVGA 屏幕中的活动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–3。 EU4You,原版,HVGA

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–4。 EU4You,原版,WVGA (800×480 像素)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–5。 EU4You,原版,QVGA

固定字体

首先要解决的问题是字体大小。正如你所看到的,对于固定的 20 像素大小,字体的范围从大到小,取决于屏幕的大小和密度。对于 WVGA 屏幕,字体可能很难阅读。

我们可以将维度作为一个资源(res/values/dimens.xml),并基于屏幕大小或密度拥有该资源的不同版本。然而,更简单的方法是只指定一个与密度无关的大小,如5mm,如ScreenSizes/EU4You_2项目所示:

`<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  android:layout_width=“fill_parent”
  android:layout_height=“wrap_content”
  android:padding=“2dip”
  android:minHeight=“?android:attr/listPreferredItemHeight”


  
`

图 25–6、25–7 和 25–8 分别显示了 HVGA、WVGA 和 QVGA 屏幕上的新活动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25-6。 EU4You,5mm 字体版本,HVGA

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–7。 EU4You,5mm 字体版本,WVGA (800×480 像素)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25-8。 EU4You,5mm 字体版本,QVGA

现在我们的字体大小一致,足够匹配旗帜。

固定图标

那么,那些图标呢?它们的大小也应该不同,因为它们对于所有三个模拟器都是相同的。

但是,Android 会自动缩放位图资源,即使是在<supports-screens>及其属性设置为true的情况下。从好的方面来说,这意味着您可能不需要对这些位图做任何事情。然而,您依赖于一个设备来进行扩展,这无疑会消耗 CPU 时间(因此会延长电池寿命)。此外,与开发机器上的图形工具相比,设备使用的缩放算法可能不是最佳的。

ScreenSizes/EU4You_3项目创建了res/drawable-ldpires/drawable-hdpi,分别放入更小和更大的旗帜。该项目还将res/drawable更名为res/drawable-mdpi。Android 将根据设备或模拟器的需要,使用合适的屏幕密度标志。

因为这种效果很微妙,不会在本书中很好地表现出来,所以没有提供截图。

利用空间

虽然该活动在纵向模式下在 WVGA 上看起来不错,但在横向模式下确实浪费了很多空间,如图 Figure 25–9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–9。 EU4You,风景 WVGA (800×480 像素)

我们可以更好地利用这个空间,让维基百科的内容在大屏幕横向模式下直接出现在主活动上;这省去了生成单独的浏览器活动。

要做到这一点,我们首先必须将main.xml布局克隆到包含WebView小部件的res/layout-large-land呈现中,如ScreenSizes/EU4You_4所示:

`<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  android:layout_width=“fill_parent”
  android:layout_height=“fill_parent”


  
`

然后,我们需要调整我们的活动来寻找那个WebView,如果找到就使用它,否则默认启动一个浏览器活动:

`@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

browser=(WebView)findViewById(R.id.browser);

setListAdapter(new CountryAdapter()); }

@Override
protected void onListItemClick(ListView l, View v,
                            int position, long id) {
  String url=getString(EU.get(position).url);

if (browser==null) {
    startActivity(new Intent(Intent.ACTION_VIEW,
                            Uri.parse(url)));
  }
  else {
    browser.loadUrl(url);
  }
}`

这为我们提供了一个更加节省空间的活动版本,如图 Figure 25–10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 25–10。 EU4You,风景 WVGA (800×480 像素),设置为正常密度,并显示嵌入式 WebView

如果用户点击维基百科页面上的一个链接,完整的浏览器就会打开,以便于浏览。我们可以重复这个练习,为大屏幕的活动添加更多的数据。

请注意,测试这个版本的活动,以查看这种行为,需要一些额外的模拟器工作。默认情况下,Android 将 WVGA 设备设置为高密度,这意味着 WVGA 在资源集方面并不算大,而是正常的。您将需要创建一个不同的模拟器 AVD,设置为正常(中等)密度,这将导致一个大的屏幕尺寸。

如果不是浏览器呢?

当然,EU4You确实有点作弊。第二个活动是浏览器(或者嵌入式表单中的WebView),而不是您自己创建的活动。如果第二个活动是您的某个活动,在一个布局中有许多小部件,并且您既想将它用作一个活动(对于较小的屏幕),又想将它嵌入到您的主活动 UI 中(对于较大的屏幕),那么事情会变得稍微复杂一些。

对于 Android 1.6 和更高版本,解决这个问题的最好方法是使用新的片段系统。虽然这是在 Android 3.0 中引入的,但 Android 兼容性库使片段在早期版本的 Android 中可用。片段的基本使用——包括另一个版本的EU4You样本——将在第二十八章中介绍。

二十六、关注平板电脑和更大的用户界面

2011 年 2 月,Android 3.0 和一种用户界面范式问世,这种界面范式采用了比早期 Android 版本设计的传统手机大得多的屏幕。快进到 2011 年 10 月,Android 4.0 冰淇淋三明治(ICS)已经发布,将 Android 3.0 的平板专用蜂巢 UI 系统与主流 Android 代码库统一起来。拥抱平板电脑,以及更大的设备,如电视、影院显示器等,这是自第一代手机问世之前的 Android 0.9 以来,Android 最大的一次变化。

在设计和构建 Android 应用时是否考虑平板设备是您自己的偏好,但了解平台适应大格式的方式将允许您设计代码,以便在未来轻松适应平板电脑,最重要的是,处理 API 的核心原则,无论您对平板电脑的感觉如何,都必须解决这些原则。这一章更侧重于平板设备的 API 的现状以及它们在 Android 中的位置。

为什么选择平板电脑?

原则上,Android 最初以手机为中心的 UI 可以在平板电脑上运行。毕竟,一些平板电脑已经搭载了 Android 2.2 支持,如三星 Galaxy Tab 和中兴 V9。显然,这些制造商认为当时的 Android 对于他们的平板设备来说已经足够强大了。

也就是说,随着你进入更大的平板电脑(例如,10 英寸对角线屏幕的摩托罗拉 XOOM),旧的 Android 手机用户界面开始变得沉闷。虽然应用可以通过扩展来使用更大的屏幕,但是默认的扩展方式只是把所有东西都放大,这经常会导致大量的空间浪费。手机上的电子邮件客户端可能会专门显示收件箱中的电子邮件列表,而平板电脑上的电子邮件客户端实际上应该显示电子邮件列表和其他内容,例如所选电子邮件的内容。我们有这个房间,所以不妨使用它。

同样,对菜单的依赖在手机上是合理的,但在平板电脑上就没那么有意义了。我们有足够的空间在屏幕上展示更多的功能。将它们隐藏在菜单中会使它们不容易被用户发现,并且需要额外的点击才能访问。

因此,“现代”Android 旨在保留 Android 用户体验的精髓,同时允许应用(相对)优雅地利用可用空间。

用户看到的内容

平板电脑屏幕看起来与传统手机上的 Android 2.x 屏幕略有不同,如图 Figure 26–1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 26–1。**Android 应用启动器,在模拟器上显示为平板电脑

有了这些额外的不动产,各种库存组件可以放置在更多不同的位置。在本例中,我们看到系统栏位于屏幕底部。系统栏的左端是返回、主页和最近任务的屏幕按钮(无需记住长按主页按钮即可达到相同的效果)。通知图标出现在系统栏的右侧,旁边还有时钟、信号和电池电量指示器(通知的概念将在第三十七章中介绍)。

未针对 Android 3.x/4.0 优化的应用的 UI 看起来大同小异,如图 Figure 26–2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 26–2。**Android 3.0 上的 fancy lists/动态样本项目

唯一实质性的区别是系统栏左边的新图标,它将打开一个 Android 2.x 选项菜单,如果应用有的话。

针对平板电脑优化的应用看起来会有些不同,如图 Figure 26–3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 26–3。 在安卓 4.0 上添加联系人

屏幕顶部是操作栏,占据了 Android 3.0 之前的应用使用菜单的空间。在图 26–3 中,完成选项作为菜单选项出现。其他需要注意的菜单行为是动作栏左端的<图标,如图 Figure 26–4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 26–4。<图标用于在动作层级中向上移动,在安卓 4.0 中显示

在这种情况下,点击< icon takes the user up in the hierarchy of actions in this application, going “up” from viewing a new contact to viewing the list of existing contacts, as shown in 图 26–5。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 26–5。 安卓 4.0 显示的可用联系人名单

我们的用户界面已经走过了几个 Android 版本的生命周期。在 Android 2.x 中,联系人 UI 将有一个包含联系人列表的活动,以及一个查看该联系人详细信息的单独活动。在 Android 3.0 中,这些被合并成一个活动。在 Android 4.0 中,我们又回到了每次操作一个活动的模式。操作栏的右侧包括一个“查找联系人”搜索图标(放大镜)和一个添加新联系人的图标。与之相邻的是代表任何其他可用选项菜单项和上下文菜单项的图标。

处理剩余的设备

当然,世界上所有的 Android 手机并没有因为 Android 4.0 的发布而消失。我们的目标是让您从一个代码库创建一个同时支持手机和平板电脑的应用。

你的以手机为中心的应用在平板电脑上运行也很好,尽管你可能希望做一些事情来利用更大的屏幕尺寸,正如上一章所讨论的那样。如果您想采用冰激凌三明治 UI 的一般外观,您需要在清单的<uses-sdk>元素中包含android:targetSdkVersion="14"。如果你以前为 Honeycomb 开发过,并且习惯于使用android:hardwareAccelerated="true"属性显式打开硬件加速,那么好消息是你不再需要在 Android 4.0 中显式设置这样的加速。硬件加速现在是默认的。从ScreenSizes/EU4You_5示例项目的AndroidManifest.xml文件中摘录的这段内容展示了 SDK 的变化:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"   package="com.commonsware.android.eu4you"   android:versionCode="1"   android:versionName="1.0">   <uses-permission android:name="android.permission.INTERNET" />   <supports-screens     android:largeScreens="true"     android:normalScreens="true"     android:smallScreens="true"     android:anyDensity="true"   />   <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="14" />   <application android:label="@string/app_name"     android:icon="@drawable/cw"     <activity android:name=".EU4You"               android:label="@string/app_name">       <intent-filter>         <action android:name="android.intent.action.MAIN" />         <category android:name="android.intent.category.LAUNCHER" />       </intent-filter>     </activity>   </application> </manifest>

最终的应用在旧设备上运行良好,但是没有其他变化,我们在摩托罗拉 XOOM 上得到如图 Figure 26–6 所示的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 26–6。??【EU4You】样本应用,在摩托罗拉 XOOM 上运行

如果你想利用冰淇淋三明治的一些新特性,你还需要考虑向后兼容性,以确保你在应用中实现的东西能在新老版本的 Android 上成功工作。这个主题将在本书的后面部分讨论。

如果您有需要特定于版本的资源,比如样式,您可以使用-v*NN*资源集后缀语法,其中 NN 表示您所针对的版本。例如,你可以有一个res/values/styles.xml和一个res/values-v14/styles.xml——后者将用于冰淇淋三明治,而前者将用于旧版本的 Android。但是首先,你需要探索所有你可以利用的平板 UI 特性,这是接下来几章的重点。

二十七、使用动作栏

让你的应用更好地融入最新最棒的 Android UI 的最简单的方法之一是启用动作栏,这在第二十六章中有介绍。让它变得“容易”的是,动作栏的大部分基本功能都是向后兼容的 Android 4.0 的设置不会导致应用在早期版本的 Android 上崩溃。

本章中显示的示例项目是Menus/ActionBar,它扩展了上一章中显示的Menus/Inflation项目。

启用动作栏

默认情况下,您的 Android 应用不会使用操作栏。事实上,它甚至不会显示在屏幕上。如果您希望动作栏出现在屏幕上,您需要在清单中的<uses-sdk>元素中包含android:targetSdkVersion="11"或更高版本,例如Menus/ActionBar项目的清单:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.inflation">     <application android:label="@string/app_name"                 android:icon="@drawable/cw"                 android:hardwareAccelerated="true">         <activity android:name=".InflationDemo" android:label="@string/app_name">             <intent-filter>                 <action android:name="android.intent.action.MAIN"/>                 <category android:name="android.intent.category.LAUNCHER"/>             </intent-filter>         </activity>     </application>     <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="11" />     <supports-screens android:xlargeScreens="true"                     android:largeScreens="true"                     android:normalScreens="true"                     android:smallScreens="true"                     android:anyDensity="true"/> </manifest>

这将使你的选项菜单出现在屏幕的右上角,在动作栏的菜单图标下,如第二十六章所示。此外,您的活动图标将出现在左上角,旁边是您的活动名称(来自清单中的android:label属性)。

虽然这给了你基本的现代外观和感觉——包括冰淇淋三明治主题的小部件——但它并没有真正改变用户体验。

将菜单项提升到操作栏

与动作栏集成的下一步是将某些选项菜单项从选项菜单的一部分提升到总是在动作栏上可见。这使得它们更容易被找到,并在用户需要使用它们的时候节省了时间。

为此,在您的菜单 XML 资源中,您可以将android:showAsAction属性添加到<item>元素中。值ifRoom意味着如果有空间,菜单项将出现在动作栏中,而值always意味着菜单项将总是被放在动作栏中。在其他条件相同的情况下,ifRoom是更好的选择,因为一旦蜂窝用户界面转移到手机上,它将更好地适应更小的屏幕。您也可以将其与withText值(例如ifRoom|withText)结合使用,使菜单项的标题出现在该项目的图标旁边(否则,只有图标出现在操作栏中)。

例如,Menus/ActionBar项目的options.xml菜单资源在前两个菜单项上有android:showAsAction:

`<?xml version="1.0" encoding="utf-8"?>

         `

第二个菜单项 Reset 用于重置列表的内容,它是一个普通的“带文本”操作栏按钮。第一个菜单项 Add 做了一点不同的事情,我们将在本章的后面讨论。第三个菜单项 About 没有android:showAsAction这一事实意味着它将保留在菜单中,即使动作栏中还有空间。

请注意,Java 代码没有改变——我们的InflationDemo活动的onCreateOptionsMenu()onOptionsItemSelected()不需要调整,因为菜单项仅通过菜单 XML 资源被提升到动作栏中。

响应标志

屏幕左上角的活动图标可点击。如果用户点击它,它会触发onOptionsItemSelected()…但不是您自己定义的选项菜单项。而是使用了android.R.id.home的神奇值。在Menus/ActionBar项目中,我们将它连接到用户选择 About options 菜单项时调用的相同代码——显示一个Toast:

`@Override
public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
    case R.id.add:
      add();
      return(true);

case R.id.reset:
      initAdapter();
      return(true);

case R.id.about:
    case android.R.id.home:
      Toast
        .makeText(this,
                  “Action Bar Sample App”,
                  Toast.LENGTH_LONG)
        .show();
      return(true);
  }

return(super.onOptionsItemSelected(item));
}`

然而,在一个包含多个活动的项目中,无论这意味着什么,点击图标都会将你带到应用的“主页”活动。

向操作栏添加自定义视图

除了简单地将选项菜单项转换为相当于工具栏按钮的内容之外,您还可以使用操作栏做更多的事情。您可以将自己的自定义用户界面添加到操作栏中。在Menus/ActionBar的例子中,我们将在动作栏本身用一个添加字段替换添加菜单选项和结果对话框。

然而,如下所述,实现起来有点棘手。

定义布局

要在动作栏中放置自定义的东西,我们需要以布局 XML 文件的形式定义“自定义的东西”是什么。幸运的是,我们已经有了一个用于向列表添加单词的布局 XML 文件——它是当点击 Add options 菜单项时,Menus/Inflation示例包装在自定义的AlertDialog中的文件。最初的布局是这样的:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:orientation="horizontal"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     >   <TextView       android:text="Word:"       android:layout_width="wrap_content"       android:layout_height="wrap_content"       />   <EditText       android:id="@+id/title"       android:layout_width="fill_parent"       android:layout_height="wrap_content"       android:layout_marginLeft="4dip"       /> </LinearLayout>

我们需要对这个布局做一些小的调整,以便将其用于操作栏:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:orientation="horizontal"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     >   <TextView       android:text="Word:"       android:layout_width="wrap_content"       android:layout_height="wrap_content"       android:textAppearance="@android:style/TextAppearance.Medium"       />   <EditText       android:id="@+id/title"       android:layout_width="fill_parent"       android:layout_height="wrap_content"       android:layout_marginLeft="4dip"       android:width="160sp"       android:inputType="text"       android:imeActionId="1337"       android:imeOptions="actionDone"       /> </LinearLayout>

具体来说,我们做了以下小调整:

  • 我们向表示添加标题的TextView添加了一个android:textAppearance属性。android:textAppearance属性允许我们一次性定义字体类型、大小、颜色和粗细(如粗体)。我们特别使用了一个神奇的值@android:style/TextAppearance.Medium,以便标题与我们提升到动作栏的另一个菜单项上的重置标签的样式相匹配。
  • 我们为EditText小部件指定了android:width="160sp",因为android:layout_width="fill_parent"在动作栏中被忽略了——否则,我们将占用栏的其余部分。
  • 我们在EditText小部件上指定了android:inputType="text",这将我们限制为单行文本。
  • 我们用EditText小部件上的android:imeActionIdandroid:imeOptions来控制软键盘的动作按钮,所以当用户按下软键盘上的回车键时,我们得到控制权。
将布局放入菜单

接下来,如果我们运行的是最新版本的 Android,比如冰激凌三明治或蜂巢,我们需要教会 Android 使用这种布局来添加选项菜单项。为此,我们在<item>元素上使用了android:actionLayout属性,引用了我们的布局资源(@layout/add),如本章前面所示。这个属性在早期版本的 Android 上会被忽略,所以可以安全使用。

如果我们什么也不做,我们将得到想要的 UI,如图 Figure 27–1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 27–1。 菜单/ActionBar 示例应用

然而,尽管用户可以输入一些东西,我们没有办法知道他们输入了什么,什么时候输入的,等等。

控制用户输入

给定我们放在EditText小部件上的软键盘设置,我们可以安排找出用户何时在软键盘或硬件键盘上按回车键。然而,要做到这一点,我们需要接触到EditText小部件本身。你可能认为它是在onCreate()中用户界面膨胀时添加的……但是你错了。

对于动作栏,onCreateOptionsMenu()onCreate()之后被调用,作为设置 UI 的一部分。在经典版本的安卓系统中,onCreateOptionsMenu()直到用户按下菜单键才会被调用。但是,由于一些选项菜单项可能会被提升到动作栏中,Android 现在会自动调用onCreateOptionsMenu()。在我们扩大我们的options.xml菜单资源后,EditText将会存在。

然而,获得EditText的最好方法是不要在活动中使用findViewById()。相反,我们应该在与添加选项相关的MenuItem上调用getActionView()。这将返回视图层次结构的根,它是从我们在菜单资源的android:actionLayout属性中定义的布局资源展开的。在这种情况下,那是来自res/layout/add.xmlLinearLayout,所以我们需要在它上面调用findViewById()来获得EditText:

`@Override
public boolean onCreateOptionsMenu(Menu menu) {
  new MenuInflater(this).inflate(R.menu.option, menu);

EditText add=(EditText)menu
                         .findItem(R.id.add)
                         .getActionView()
                         .findViewById(R.id.title);

add.setOnEditorActionListener(onSearch);

return(super.onCreateOptionsMenu(menu));
}`

然后,我们可以调用EditText上的setOnEditorActionListener(),注册一个OnEditorActionListener对象,当用户在硬键盘或软键盘上按 Enter 键时,该对象将获得控制权:

`private TextView.OnEditorActionListener onSearch=
  new TextView.OnEditorActionListener() {
  public boolean onEditorAction(TextView v, int actionId,
                               KeyEvent event) {
    if (event==null || event.getAction()==KeyEvent.ACTION_UP) {
      addWord(v);

InputMethodManager imm=(InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);       imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
    }

return(true);
  }
};`

这又调用了一个addWord()方法,提供了EditText,它通过ArrayAdapter将单词添加到列表中:

`private void addWord(TextView title) {
  ArrayAdapter adapter=(ArrayAdapter)getListAdapter();

adapter.add(title.getText().toString());
}`

显示AlertDialogadd()方法也可以使用相同的addWord()方法,尽管它不会在平板电脑上使用,因为添加菜单选项不再作为菜单选项存在:

`private void add() {
  final View addView=getLayoutInflater().inflate(R.layout.add, null);

new AlertDialog.Builder(this)
    .setTitle(“Add a Word”)
    .setView(addView)
    .setPositiveButton(“OK”,
                       new DialogInterface.OnClickListener() {
      public void onClick(DialogInterface dialog,
                           int whichButton) {
        addWord((TextView)addView.findViewById(R.id.title));
      }
    })
    .setNegativeButton(“Cancel”, null)
    .show();
}`

最终结果是,当用户在 Add 字段中键入内容并按下 Enter 键时,该单词将被添加到列表的底部。这比传统的电话 UI 节省了一些点击,因为用户不必打开选项菜单,不必点击选项菜单项,并且不必点击对话框上的按钮。

请注意,我们的OnEditorActionListener不仅仅是将单词添加到列表中:它隐藏了软键盘。如前一章所述,它使用InputMethodManager来实现这一点。

别忘了手机!

除了上一节中描述的自定义视图特性之外,本章中关于操作栏的所有内容都是自动向后兼容的。适用于冰淇淋三明治口味的 Android 版本的代码和资源将适用于未经修改的 Android 经典版本。

然而,如果你想使用自定义视图功能,你会遇到一个问题——getActionView()方法是 API 级的新方法,在旧版本的 Android 上不可用。这意味着您将需要编译至少 API 级别 11(例如,设置您的 Eclipse 目标或 Ant default.properties来引用android-11)或更高,并且您将需要采取措施来避免在旧设备上调用getActionView()。我们将在后面的章节中探讨如何实现这一壮举。

二十八、片段

2011 年 Android 开发者面临的最大变化可能是 Android 3.0 引入了片段系统,以及最近 Android 4.0 冰淇淋三明治将片段系统合并到主代码库。片段是一个可选层,可以放在活动和小部件之间,旨在帮助您重新配置活动,以支持大屏幕(例如平板电脑)和小屏幕(例如手机)。然而,片段系统也增加了额外的复杂性,这将需要 Android 开发者社区一些时间来适应。因此,使用片段的公共评论、博客帖子和示例应用就更少了,因为片段是在 Android 之后很久才被引入的。

本章涵盖了片段的基本用途,包括在 Android 3.0 之前的设备上支持片段。

引入片段

片段不是小部件,像ButtonEditText。片段不是容器,像LinearLayoutRelativeLayout。片段不是活动。

相反,片段集合了小部件和容器。然后可以将片段放入活动中——有时一个活动有几个片段,有时每个活动有一个片段。原因是 Android 屏幕尺寸的变化。

片段解决的问题

平板电脑的屏幕比手机大。电视的屏幕比平板电脑大。利用额外的屏幕空间是有意义的,正如第二十五章中所描述的那样,该章解释了如何处理多种屏幕尺寸。在那一章中,我们分析了一个EU4You示例应用,最终以一个活动结束,该活动将在一个不同的布局中为更大尺寸的屏幕加载,该布局具有一个嵌入的WebView小部件。该活动将检测小部件的存在,并使用它来加载与选定国家相关的网页内容,而不是启动一个单独的浏览器活动或只包含一个WebView的活动。

然而,第二十五章中概述的场景相当琐碎。想象一下,我们有一个包含 28 个小部件的TableLayout,而不是一个WebView。在更大尺寸的屏幕上,我们希望TableLayout和相邻的ListView在同一活动中;在较小的屏幕上,我们希望TableLayout在一个单独的活动中,因为没有足够的空间。为了使用早期的 Android 技术做到这一点,我们需要复制两个活动中所有的TableLayout处理逻辑,创建一个活动基类并希望两个活动都可以继承它,或者将TableLayout和它的内容变成一个定制的ViewGroup…或者做其他事情。这仅仅是针对一个这样的场景——在一个更大的应用中乘以许多活动,复杂性就会增加。

片段溶液

片段减少了复杂性,但没有消除。

对于片段,可以在多个活动中使用的用户界面的每个离散块(基于屏幕大小)都放在一个片段中。根据屏幕大小,正在讨论的活动决定了谁得到片段。

EU4You的例子中,我们有两个片段。一个片段代表国家列表。另一个片段表示该国家的详细信息(在我们的例子中,是一个WebView)。在大屏幕设备上,我们希望两个片段都在一个活动中,而在小屏幕设备上,我们将这些片段放在两个独立的活动中。这为大屏幕用户提供了与上一个版本的EU4You相同的好处:用更少的点击获得更多信息。然而,我们用片段演示的技术将更具可伸缩性,能够处理比简单的EU4YouWebView或【非】场景更复杂的 UI 模式。

在这种情况下,我们的整个 UI 都在片段中。那是不必要的。片段是一种选择加入的技术——你只需要它们用于你的 UI 中可能出现在不同场景的不同活动中的部分。事实上,您的活动根本不会改变(比如说,一个帮助屏幕)可能不会使用任何片段。

片段还为我们提供了其他一些额外的功能,包括:

  • 基于用户交互动态添加片段的能力:例如,Gmail 应用最初显示用户邮件文件夹的ListFragment。轻按一个文件夹会在屏幕上添加第二个ListFragment,显示该文件夹中的对话。点击一个对话会在屏幕上添加第三个Fragment,显示该对话中的信息。
  • 动态片段在屏幕上来回移动的动画功能:例如,当用户在 Gmail 中点击一个对话时,文件夹ListFragment从屏幕上滑向左边,对话ListFragment向左滑动并缩小以占据更少的空间,而消息Fragment从右边滑入。
  • 动态片段的自动后退按钮管理:例如,当用户在查看消息Fragment时按下后退,则Fragment滑向右侧,对话ListFragment向右滑动并扩展以填充更多屏幕,文件夹ListFragment从左侧滑回。所有这些都不必由开发人员来管理——只需通过FragmentTransaction添加动态片段,就可以让 Android 自动处理后退按钮,包括反转所有动画。
  • 向选项菜单添加选项的能力,因此也向动作栏添加选项:调用片段的onCreate()中的setHasOptionsMenu()来注册对此感兴趣,然后像在活动中一样覆盖片段中的onCreateOptionsMenu()onOptionsItemSelected()。片段还可以注册小部件来拥有上下文菜单,并像处理活动一样处理这些上下文菜单。
  • 向动作栏添加标签的能力:动作栏可以有标签,取代了TabHost,其中每个标签的内容都是一个片段。类似地,动作栏可以有一个导航模式,用一个Spinner在模式之间切换,其中每个模式由一个片段表示。

如果你可以使用任何运行 Honeycomb 或 Ice Cream Sandwich 的最新设备,启动 Gmail 应用来查看所有片段的功能。

Android 兼容性库

如果片段只适用于 Android 3.0 和更高版本,我们将回到起点,因为今天并不是所有的 Android 设备都运行 Android 3.0 和更高版本。

幸运的是,情况并非如此,因为 Google 已经发布了 Android 兼容性库(ACL),它可以通过 Android SDK 和 AVD 管理器获得(在这里,您可以安装其他 SDK 支持文件,创建和启动您的仿真器 AVD,等等)。ACL 允许您访问从 Android 1.6 开始的 Android 版本上的片段系统。因为绝大多数 Android 设备运行的是 1.6 或更高版本,这允许您在保持向后兼容性的同时开始使用片段。随着时间的推移,对于希望使用该库的应用,该库可能会添加其他特性来帮助实现向后兼容性。

本章中的材料着重于在使用分段时使用 ACL。一般来说,对片段使用 ACL 几乎等同于直接使用原生 Android 3.0 片段类。

由于 ACL 仅支持 Android 1.6 的版本,Android 1.5 设备将无法使用基于片段的应用。这在目前的 Android 设备中只占很小的一部分——在撰写本文时大约是 1%。

创建片段类

建立基于片段的应用的第一步是为每个片段创建片段类。正如您从Activity(或子类)继承活动一样,您从Fragment(或子类)继承片段。

在这里,我们将检查Fragments/EU4You_6示例项目和它定义的片段。

**注:**本书的约定将是使用“片段”作为通用名词,Fragment指代实际的Fragment类。

一般片段

除了从Fragment继承之外,片段唯一需要的就是覆盖onCreateView()。这将作为把片段放在屏幕上的一部分被调用。您需要返回一个代表片段主体的View。最有可能的是,您将通过一个 XML 布局文件来创建您的片段的 UI,并且onCreateView()将扩展该片段布局文件。

例如,下面是来自EU4You_6DetailsFragment,它将围绕我们的WebView显示给定国家的网页内容:

`import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;

public class DetailsFragment extends Fragment {
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    return(inflater**.inflate**(R.layout.details_fragment, container, false));
  }

public void loadUrl(String url) {
    ((WebView)(getView().findViewById(R.id.browser))).loadUrl(url);
  }
}`

注意,我们不是从android.app.Fragment继承,而是从android.support.v4.app.Fragment继承。后者是来自 ACL 的Fragment实现,因此它可以跨 Android 版本使用。

onCreateView()实现扩展了一个布局,碰巧其中有一个WebView:

<?xml version="1.0" encoding="utf-8"?> <WebView   xmlns:android="http://schemas.android.com/apk/res/android"   android:id="@+id/browser"   android:layout_width="fill_parent"   android:layout_height="fill_parent" />

它还公开了一个loadUrl()方法,宿主活动使用它来告诉片段是时候显示一些 web 内容了,并提供 URL 来做同样的事情。DetailsFragmentloadUrl()的实现使用getView()检索onCreateView()中创建的View,找到其中的WebView,将loadUrl()调用委托给WebView

Fragment上有无数其他可用的生命周期方法。更重要的包括活动的标准onCreate()onStart()onResume()onPause()onStop()onDestroy()方法的镜子。由于片段是带有小部件的片段,它将实现更多以前可能驻留在这些方法的活动中的业务逻辑。例如,在onPause()onStop()中,由于用户可能不会返回到您的应用,您可能希望将任何未保存的编辑保存到某个临时存储中。在DetailsFragment的例子中,这里没有真正合格的东西,所以那些生命周期方法被单独留下。

列表片段

一个肯定会流行的Fragment子类是ListFragment。这将一个ListView封装在一个Fragment中,旨在简化国家、邮件文件夹、邮件对话等列表的设置。类似于一个ListActivity,你所需要做的就是用你选择和配置的ListAdapter调用setListAdapter(),并且当用户点击列表中的一行时覆盖onListItemClick()来响应。

EU4You_6中,我们有一个代表可用国家列表的CountriesFragment。它初始化onActivityCreated()中的ListAdapter,这个函数在onCreate()结束保存片段的活动后被调用:

`@Override
public void onActivityCreated(Bundle state) {
  super
.onActivityCreated(state);

setListAdapter(new CountryAdapter());

if (state!=null) {
    int position=state**.getInt**(STATE_CHECKED, -1);

if (position>-1) {
      getListView().setItemChecked(position, true);
    }
  }
}`

处理提供给onCreate()Bundle的代码将在本章稍后解释。

CountryAdapter与之前的EU4You样本几乎相同,除了在Fragment上没有getLayoutInflater()方法,所以我们必须在LayoutInflater上使用静态from()方法,并通过getActivity()提供我们的活动:

`class CountryAdapter extends ArrayAdapter {
  CountryAdapter() {
    super(getActivity(), R.layout.row, R.id.name, EU);
  }

@Override
  public View getView(int position, View convertView,
                     ViewGroup parent) {
    CountryWrapper wrapper=null;

if (convertView==null) {
      convertView=LayoutInflater
                    .from(getActivity())
                    .inflate(R.layout.row, null);
      wrapper=new CountryWrapper(convertView);
      convertView.setTag(wrapper);
    }
    else {
      wrapper=(CountryWrapper)convertView**.getTag**();
    }

wrapper**.populateFrom**(getItem(position));

return(convertView);
  }
}`

同样,CountryWrapper与之前的EU4You样品没有任何不同:

`static class CountryWrapper {
  private TextView name=null;
  private ImageView flag=null;
  private View row=null;

CountryWrapper(View row) {
    this.row=row;
    name=(TextView)row**.findViewById**(R.id.name);
    flag=(ImageView)row**.findViewById**(R.id.flag);
  }

TextView getName() {
    return(name);
  }

ImageView getFlag() {
    return(flag);
  }

void populateFrom(Country nation) {
    getName().setText(nation.name);
    getFlag().setImageResource(nation.flag);   }
}`

国家列表也是一样的:

static {   EU**.add**(new **Country**(R.string.austria, R.drawable.austria,                     R.string.austria_url));   EU**.add**(new **Country**(R.string.belgium, R.drawable.belgium,                     R.string.belgium_url));   EU**.add**(new **Country**(R.string.bulgaria, R.drawable.bulgaria,                     R.string.bulgaria_url));   EU0**.add**(new **Country**(R.string.cyprus, R.drawable.cyprus,                     R.string.cyprus_url));   EU**.add**(new **Country**(R.string.czech_republic,                     R.drawable.czech_republic,                     R.string.czech_republic_url));   EU**.add**(new **Country**(R.string.denmark, R.drawable.denmark,                     R.string.denmark_url));   EU**.add**(new **Country**(R.string.estonia, R.drawable.estonia,                     R.string.estonia_url));   EU**.add**(new **Country**(R.string.finland, R.drawable.finland,                     R.string.finland_url));   EU**.add**(new **Country**(R.string.france, R.drawable.france,                     R.string.france_url));   EU**.add**(new **Country**(R.string.germany, R.drawable.germany,                     R.string.germany_url));   EU**.add**(new **Country**(R.string.greece, R.drawable.greece,                     R.string.greece_url));   EU**.add**(new **Country**(R.string.hungary, R.drawable.hungary,                     R.string.hungary_url));   EU**.add**(new **Country**(R.string.ireland, R.drawable.ireland,                     R.string.ireland_url));   EU**.add**(new **Country**(R.string.italy, R.drawable.italy,                     R.string.italy_url));   EU**.add**(new **Country**(R.string.latvia, R.drawable.latvia,                     R.string.latvia_url));   EU**.add**(new **Country**(R.string.lithuania, R.drawable.lithuania,                     R.string.lithuania_url));   EU**.add**(new **Country**(R.string.luxembourg, R.drawable.luxembourg,                     R.string.luxembourg_url));   EU**.add**(new **Country**(R.string.malta, R.drawable.malta,                     R.string.malta_url));   EU**.add**(new **Country**(R.string.netherlands, R.drawable.netherlands,                     R.string.netherlands_url));   EU**.add**(new **Country**(R.string.poland, R.drawable.poland,                     R.string.poland_url));   EU**.add**(new **Country**(R.string.portugal, R.drawable.portugal,                     R.string.portugal_url));   EU**.add**(new **Country**(R.string.romania, R.drawable.romania,                     R.string.romania_url));   EU**.add**(new **Country**(R.string.slovakia, R.drawable.slovakia,                     R.string.slovakia_url));   EU**.add**(new **Country**(R.string.slovenia, R.drawable.slovenia,                     R.string.slovenia_url));   EU**.add**(new **Country**(R.string.spain, R.drawable.spain,                     R.string.spain_url));   EU**.add**(new **Country**(R.string.sweden, R.drawable.sweden,                     R.string.sweden_url));   EU**.add**(new **Country**(R.string.united_kingdom,                     R.drawable.united_kingdom,                     R.string.united_kingdom_url)); }

…正如Country的定义一样,来自一个单独的公共类:

`public class Country {
  int name;
  int flag;
  int url;

Country(int name, int flag, int url) {
    this.name=name;
    this.flag=flag;
    this.url=url;
  }
}`

持续突出显示

当你使用像 Gmail 这样基于片段的应用时,有一件事会让你眼前一亮。当您点击列表中的一行,并且在同一活动中显示(或更新)另一个片段时,您点击的行会保持高亮显示。这与传统的使用ListView背道而驰,在传统的使用中,列表选择器只有在使用 D-pad、轨迹球或类似的定点设备时才会出现。目的是向用户显示相邻片段的上下文。

实际的实现与您预期的不同。这些ListView小部件实际上实现了CHOICE_MODE_SINGLE,通常使用行右侧的RadioButton来呈现。然而,在ListFragment中,单选ListFragment的典型样式是通过一个“激活的”背景。

EU4You_6中,这是通过我们的**Country**Adapter使用的行布局(res/layout/row.xml)来处理的:

`<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  android:layout_width=“fill_parent”
  android:layout_height=“wrap_content”
  android:padding=“2dip”
  android:minHeight=“?android:attr/listPreferredItemHeight”
  style=“@style/activated”


  <TextView android:id=“@+id/name”
    android:layout_width=“wrap_content”
    android:layout_height=“wrap_content”     android:layout_gravity=“center_vertical|right”
    android:textSize=“5mm”
  />
`

注意style属性,它指向一个activated样式。这被EU4You_6定义为本地风格,而不是操作系统提供的风格。事实上,它必须有两个样式的实现,因为“激活”的概念是 Android 3.0 的新功能,不能在以前的 Android 版本中使用。

因此,EU4You_6有一个向后兼容的空样式的res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?> <resources>   <style name="activated">   </style> </resources>

它还有res/values-v11/styles.xml-v11资源集后缀意味着这将只在 API Level 11 (Android 3.0)及更高版本上使用。这里,风格继承了标准的 Android 全息主题,并使用标准的激活背景色:

<?xml version="1.0" encoding="utf-8"?> <resources>   <style name="activated" parent="android:Theme.Holo">     <item name="android:background">?android:attr/activatedBackgroundIndicator</item>   </style> </resources>

CountriesFragment中,活动将通过enablePersistentSelection()方法让我们知道CountriesFragment是否出现在DetailsFragment旁边——因此需要单选模式:

public void **enablePersistentSelection**() {   **getListView**()**.setChoiceMode**(ListView.CHOICE_MODE_SINGLE); }

同样,在onListItemClick()CountriesFragment“检查”用户点击的行,从而启用持久高亮:

`@Override
public void onListItemClick(ListView l, View v, int position,
                           long id) {
  l**.setItemChecked**(position, true);

if (listener!=null) {
    listener**.onCountrySelected**(EU**.get**(position));
  }
}`

listener对象和对on**Country**Selected()的调用将在本章后面解释。

其他片段基类

ACL 还有另外一个子类Fragment : DialogFragment。这用于帮助协调模态Dialog和基于片段的 UI。

Android 3.0 本身还有两个子类Fragment,在本文撰写之时,ACL 中还没有:

  • PreferenceFragment:用于新型蜂巢式PreferenceActivity(包含在第三十一章中)
  • WebViewFragment:一个Fragment缠绕着一个WebView

片段、布局、活动和多种屏幕尺寸

拥有一些片段类及其伴随的布局当然很好,但是我们需要将它们与活动联系起来,并让它们出现在屏幕上。在这个过程中,我们必须考虑如何处理多种屏幕尺寸,就像我们对先前版本的EU4You示例采用的WebViewor-browser 方法一样。

在 Android 3.0 及更高版本中,任何活动都可以托管一个片段。但是,对于 ACL,需要从FragmentActivity继承才能使用片段。ACL 的这种限制肯定会带来挑战,特别是如果您打算将地图放入片段中,这是我们将在本书后面讨论的主题。其他活动基类造成的问题更少——例如,ListActivity将由ListFragment代替。

片段可以通过两种方式添加到活动中:

  • 您可以通过活动布局中的<fragment>元素来定义它们。这些片段是固定的,并且将在该活动实例的生存期内一直存在。
  • 您可以通过FragmentManagerFragmentTransaction即时添加它们。这为您提供了更多的灵活性,但也增加了复杂性。这种技术不在本书讨论范围之内。

处理多种屏幕尺寸的一个很大的限制是,对于任何配置更改,布局都需要有相同的起始片段。因此,活动的小屏幕版本和大屏幕版本可以有不同的片段组合,但是相同屏幕大小的纵向布局和横向布局必须定义相同的片段。否则,当屏幕旋转时,Android 就会出现问题,例如,试图使用不存在的片段。

我们还需要解决片段和活动之间的通信。活动定义了它们持有的片段,因此它们通常知道哪些类实现了这些片段,并可以直接调用这些片段上的方法。然而,这些片段只知道它们是由某个活动托管的,而这个活动可能因情况而异。因此,典型的模式是使用接口进行片段到活动的通信:

  • 定义一个方法的接口,该片段将在它的活动(或者由该活动提供的一些其他对象)上调用这些方法。
  • 当创建片段时,活动通过片段上的一些 setter 方法提供该接口的实现。
  • 片段根据需要使用该接口实现。

当我们完成EU4You_6活动和它们相应的布局时,我们会看到所有这些。

在早期版本的EU4You项目中,我们只有一个活动,也叫做EU4You。在EU4You_6,我们有两项活动:

  • EU4You:在所有屏幕尺寸下显示CountriesFragment的手柄,以及在更大屏幕上显示DetailsFragment
  • DetailsActivity:在较小的屏幕上显示DetailsFragment

虽然我们可以让EU4You在更小的屏幕上启动浏览器活动,而不是让DetailsActivity托管一个只有WebViewDetailsFragment,但后一种方法对于更多基于片段的应用来说更现实。

优优

首先,我们来看看EU4You活动的各个部分。

布局

对于普通屏幕设备,我们只想显示CountriesFragment。这是通过具有适当的<fragment>元素的res/layout/main.xml来实现的:

<?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android"   class="com.commonsware.android.eu4you.CountriesFragment"   android:id="@+id/countries"   android:layout_width="fill_parent"   android:layout_height="fill_parent" />

属性表明哪个 Java 类实现了这个片段。否则,这种布局是不起眼的。

请注意,片段不会像活动那样在清单文件中列出。

另一种布局

对于大屏幕设备,在横向模式下,我们希望将CountriesFragmentDetailsFragment并排显示。这样,用户可以点击一个国家并查看详细信息,而无需在活动之间来回切换。这也使我们能够更好地利用屏幕空间。

然而,有一个问题。如果我们想在我们的布局文件中预定义这两个片段,我们必须对使用相同的片段对横向和纵向模式——尽管我们不想在纵向模式下使用EU4You中的DetailsFragment(将列表垂直堆叠在WebView上看起来很奇怪,最好的情况也是如此)。作为一种变通方法,我们将对两个方向使用相同的布局文件,然后在 Java 代码中进行调整。另一个解决问题的方法是让布局文件只有CountriesFragment并使用FragmentManager和一个FragmentTransaction来添加到DetailsFragment中。不过,在这里,我们将使用其他技巧。

因此,在res/layout-large/(不是res/layout-large-land/)中,我们有这样的布局:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout   xmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="horizontal"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <fragment class="com.commonsware.android.eu4you.CountriesFragment"     android:id="@+id/countries"     android:layout_weight="30"     android:layout_width="0px"     android:layout_height="fill_parent"   />   <fragment class="com.commonsware.android.eu4you.DetailsFragment"     android:id="@+id/details"     android:layout_weight="70"     android:layout_width="0px"     android:layout_height="fill_parent"   /> </LinearLayout>

注意,我们负责片段的定位,所以这里我们使用一个水平的LinearLayout来包围两个<fragment>元素。

监听器接口

当用户在CountriesFragment中选择一个国家时,我们希望让我们的包含活动知道这一点。在这种情况下,碰巧唯一会主办CountriesFragment的活动是EU4You。然而,也许将来不会是这样。因此,我们应该通过一个监听器接口将从CountriesFragment到它的主机活动的通信抽象出来。

因此,EU4You_6项目有一个**Country**Listener接口:

`package com.commonsware.android.eu4you;

public interface Country Listener {
  void onCountrySelected(Country c);
}`

CountriesFragment持有由托管活动提供的**Country**Listener的实例:

public void **setCountryListener**(CountryListener listener) {   this.listener=listener; }

并且,当用户点击一个国家并触发onListItemClick()时,CountriesFragment调用接口上的on**Country**Selected()方法:

`@Override
public void onListItemClick(ListView l, View v, int position,
                           long id) {
  l**.setItemChecked**(position, true);

if (listener!=null) {
    listener**.onCountrySelected**(EU**.get**(position));
  }
}`

活动

这个EU4You活动时间不长,虽然有点棘手:

`package com.commonsware.android.eu4you;

import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.view.View;

public class EU4You extends FragmentActivity implements Country Listener {
  private boolean detailsInline=false;

@Override
  public void onCreate(Bundle savedInstanceState) {
    super**.onCreate**(savedInstanceState);
    setContentView(R.layout.main);

CountriesFragment countries
      =(CountriesFragment)getSupportFragmentManager()
                           .findFragmentById(R.id.countries);

countries**.setCountryListener**(this);

Fragment f=getSupportFragmentManager().findFragmentById(R.id.details);

detailsInline=(f!=null &&
                    (getResources().getConfiguration().orientation==
                     Configuration.ORIENTATION_LANDSCAPE));

if (detailsInline) {
      countries**.enablePersistentSelection**();     }
    else if (f!=null) {
      f.getView().setVisibility(View.GONE);
    }
  }

@Override
  public void onCountrySelected(Country c) {
    String url=getString(c.url);

if (detailsInline) {
      ((DetailsFragment)getSupportFragmentManager()
                           .findFragmentById(R.id.details))
                           .loadUrl(url);

}
    else {
      Intent i=new Intent(this, DetailsActivity.class);

i**.putExtra**(DetailsActivity.EXTRA_URL, url);
      startActivity(i);
    }
  }
}`

我们在onCreate()的任务是连接我们的片段。片段本身是由我们对setContentView()的调用创建的,膨胀了我们的布局和其中定义的片段。然而,除此之外,EU4You还做了以下事情:

  • 找到CountriesFragment并将自己注册为**Country**Listener,因为EU4You实现了那个接口。
  • 如果存在,查找DetailsFragment。如果它存在,并且我们处于横向模式,我们告诉CountriesFragment启用持续高亮,以提醒用户右侧正在加载什么细节。如果它存在,并且我们处于纵向模式,我们实际上不想要DetailsFragment,但是需要它与布局模式一致,所以我们将片段的内容标记为GONE。如果DetailsFragment不存在,我们不必做任何特别的事情。

findFragmentById()这样的呼叫的FragmentManager是通过getFragmentManager()完成的。然而,ACL 定义了一个单独的getSupportFragmentManager(),以确保您正在使用 ACL 的FragmentManager实现,并在更广泛的 Android 版本上工作。

另外,由于EU4You实现了**Country**Listener接口,所以它必须实现on**Country**Selected()。在这里,EU4You指出我们是否应该路由到DetailsFragment的内嵌版本。如果应该,那么on**Country**Selected()**Country**传递给DetailsFragment,因此它加载那个国家的网页。否则,我们启动DetailsActivity,额外提供 URL。

详细活动

DetailsActivity将用于DetailsFragment未在EU4You活动中显示的情况,包括以下情况:

  • 当设备具有正常屏幕尺寸,因此在布局中没有DetailsFragment
  • 当设备具有纵向尺寸的大屏幕,因此EU4You隐藏了它自己的DetailsFragment
布局

布局中只有我们的<fragment>元素,因为没有其他东西可以显示:

<?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android"   class="com.commonsware.android.eu4you.DetailsFragment"   android:id="@+id/details"   android:layout_width="fill_parent"   android:layout_height="fill_parent" />

活动

DetailsActivity简单地将来自Intent extra 的 URL 传递给DetailsFragment,告诉它要显示什么网页内容:

`package com.commonsware.android.eu4you;

import android.support.v4.app.FragmentActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

public class DetailsActivity extends FragmentActivity {
  public static final String EXTRA_URL=“com.commonsware.android.eu4you.EXTRA_URL”;

@Override
  public void onCreate(Bundle savedInstanceState) {
    super**.onCreate**(savedInstanceState);
    setContentView(R.layout.details);

DetailsFragment details
      =(DetailsFragment)getSupportFragmentManager()
                           .findFragmentById(R.id.details);

details**.loadUrl**(getIntent().getStringExtra(EXTRA_URL));
  }
}`

片段和配置变化

在第十九章中,我们回顾了活动如何处理配置变化,比如屏幕旋转。这如何转化为一个片段的世界?

和往常一样,有好消息,也有其他消息。

好消息是片段有可以覆盖的onSaveInstanceState()方法,行为很像它们的活动对应物。然后Bundle在很多地方都可以买到,比如onCreate()onActivityCreated(),尽管没有专门的onRestoreInstanceState()

另一个消息是,不仅片段缺少onRetainNonConfigurationInstance(),而且 ACL 的FragmentActivity不允许您扩展onRetainNonConfigurationInstance(),因为那是内部使用的。使用片段的直接 Android 实现的应用不会遇到这个问题。这个限制是很大的,开发人员社区仍然在共同寻找克服这个限制的方法。

为片段设计

片段的总体设计方法倾向于在片段中包含业务逻辑,活动充当片段间导航的编排层,以及片段所不具备的功能(例如,onRetainNonConfigurationInstance())。例如,Gmail 应用最初可能在每个活动(例如,文件夹的活动、对话列表的活动、单个对话的活动)中实现其大部分业务逻辑。如今,该应用可能是围绕将业务逻辑委托给片段而构建的,活动只是根据可用的屏幕大小来选择显示哪些片段。

自从 fragments 在 2011 年初首次亮相以来,这已经导致了现有应用的大量重组。例如,ListActivity可能已经从onListItemClick()发起了另一个活动。第一次重构会让片段的onListItemClick()发起一个活动。但是,该片段不知道用户请求的内容是否应该在另一个活动中显示——它可能会转到当前活动中的另一个片段。因此,该片段不应该盲目地调用startActivity(),而是应该调用其容器活动上的方法(或者,更有可能是由该活动实现的侦听器接口),告诉它点击事件,并让它决定正确的操作过程。

二十九、处理平台变更

自最初发布以来,Android 一直在快速发展,并将在未来几年继续发展。或许,随着时间的推移,变化的速度会下降一些。然而,就目前而言,你应该假设每 6 到 12 个月就会有重要的 Android 发布,并且可能的 Android 硬件阵容会不断变化。因此,虽然现在 Android 的重点是手机和平板电脑,但很快你就会看到 Android 上网本、Android 电视、Android 媒体播放器等等。

许多这些变化对您现有的代码几乎没有影响。但是,有些应用至少需要新一轮的测试,并且可能会根据测试结果对这些应用进行更改。

本章涵盖了随着 Android 的发展,未来可能会给你带来麻烦的几个问题,并提供了一些处理这些问题的建议。

让你兴奋的事情

Android 改变,不仅仅是在谷歌引入的方面,还包括设备制造商如何为他们自己的硬件调整 Android。如果您没有做好准备,本节将指出这些变化会影响您的应用的几个地方。

查看层次结构

Android 不是为处理任意复杂的视图层次而设计的。在这里,视图层次结构意味着容器包含容器,容器包含容器包含小部件。在后面的章节中描述的hierarchyviewer程序很好地描述了这样的视图层次结构。

Android 总是限制视图层次的深度。然而,在 Android 1.5 中,这个限制被降低了,所以一些在 Android 1.1 上运行良好的应用在新的 Android 中将会崩溃。当然,这对于开发人员来说是令人沮丧的,他们从来没有意识到视图层次深度的问题,然后被这种变化所困扰。

从中吸取的教训如下:

  • 保持你的视图层次浅。一旦进入两位数深度,就越有可能耗尽堆栈空间。
  • 如果您遇到一个StackOverflowException,并且堆栈跟踪看起来像是在绘制小部件的中间,那么您的视图层次结构可能太复杂了。
改变资源

Android 核心团队可能会随着 Android 升级而改变资源,这些可能会对您的应用产生意想不到的影响。例如,在 Android 1.5 中,Android 团队改变了股票Button背景,以允许更小的按钮。然而,隐式依赖于前一个更大的最小尺寸的应用最终会崩溃,需要一些 UI 调整。

类似地,应用可以重用 Android 内部的公共资源,比如图标。虽然这样做可以节省一些存储空间,但是这些资源中有许多是公共的,不被视为 SDK 的一部分。例如,硬件制造商可能会更改图标以适应一些可选的 UI 外观和感觉。依赖现有的总是看起来像他们做的有点危险。将这些资源从 Android 开源项目复制到您自己的代码库中会更好。

处理 API 变更

Android 核心团队在保持 API 稳定方面做得很好,当他们改变 API 时,支持一个弃用模型。在 Android 中,当一个特性被弃用时,这并不意味着这个特性正在消失,只是不鼓励继续使用它。当然,每次新的 Android 更新都会发布新的 API。通过 API 差异报告,API 的变更在每个版本中都有很好的文档记录。

不幸的是,Android Market——Android 应用的主要发布渠道——只允许你为每个应用上传一个 Android 包(APK)文件。因此,你需要一个 APK 文件来处理尽可能多的 Android 版本。很多时候,你的代码会“正常工作”,不需要修改。但是,其他时候,您将需要进行调整,特别是如果您希望在新版本上支持新的 API,同时又不破坏旧版本。让我们研究一些处理这些情况的技术。

最小、最大、目标和构建版本

Android 不遗余力地帮助你处理这样一个事实,即在任何时间点,市场上会有许多 Android 操作系统版本。不幸的是,Android 提供的工具给了我们一组有些混乱的重叠概念,比如目标和 SDK 版本。本节试图澄清这些概念。

目标与 SDK 版本与操作系统版本

目标的概念是在本书开头介绍的。定义 avd 时使用目标来确定这些 avd 支持什么类型的设备。创建新项目时也会使用目标,主要是为了确定将使用哪个版本的 SDK 构建工具来构建您的项目。

目标将 API 级别与该目标是否包括谷歌 API(例如,谷歌地图支持)的指示符相结合。

API 级别是表示 Android API 版本的整数。每个对 Android API 进行修改的 Android OS 版本都会触发一个新的 API 级别。以下是 API 级别:

  • 3:安卓 1.5r1,1.5r2,1.5r3
  • 4:安卓 1.6r1 和 1.6r2
  • 5 : Android 2.0
  • 6 : Android 2.0.1
  • 7 : Android 2.1.x
  • 8 : Android 2.2.x
  • 9:安卓 2.3,2.3.1,2.3.2
  • Android 2.3.3 和 2.3.4
  • 11 : Android 3.0.x
  • 12 : Android 3.1.x
  • 13 : Android 3.2
  • 14 : Android 4.0

谷歌维护着一个网页,根据对 Android Market 的请求,概述了目前使用的 Android 版本。

最低 SDK 版本

在您的AndroidManifest.xml文件中,您应该添加一个<uses-sdk>元素。该元素描述了您的应用如何与各种 SDK 版本相关联。

<uses-sdk>中最关键的属性是android:minSdkVersion。这表明您的应用支持的最低 API 级别。运行与较低 API 级别相关联的 Android 操作系统版本的设备将无法安装您的应用。如果您选择通过该分销商发布,您的应用甚至可能不会出现在 Android Market 列表中的那些设备上。

如果你跳过这个属性,Android 假设你的应用可以在所有的 Android API 版本上工作。那可能是真的,但是如果你没有测试过,就这样假设是相当危险的。因此,将android:minSdkVersion设置为您正在测试并且愿意支持的最低级别。

目标 SDK 版本

另一个<uses-sdk>属性是android:targetSdkVersion。这代表了您主要开发的 Android API 的版本。任何运行新版操作系统的 Android 设备都可以选择应用一些兼容性设置,这将有助于像你这样针对旧 API 的应用在新版操作系统上运行。

大多数情况下,您应该将它设置为当前的 Android API 版本,即您发布应用时的版本。

特别是对于冰激凌三明治,您需要指定一个目标1415来获得新的外观和感觉。

最高 SDK 版本

第三个<uses-sdk>属性是android:maxSdkVersion。任何运行比该 API 等级所指示的更新的 Android 操作系统的 Android 设备将被禁止运行您的应用。

从好的方面来说,这确保了您的应用不会在您没有测试过的 API 级别上使用,特别是如果您将它设置为截至您发布之日的当前 Android API 版本。

然而,请记住,您的应用将被过滤出这些新设备的 Android 市场。随着时间的推移,如果您不发布具有更高 SDK 最高版本的更新,这将限制您的应用的范围。

Android 核心团队建议您不要使用这个选项,而是依靠 Android 固有的向后兼容性——特别是利用您的android:targetSdkVersion值——来允许您的应用继续在新的 Android OS 版本上运行。

检测版本

如果您只是想基于版本在代码中采用不同的分支,最简单的方法就是检查android.os.Build.VERSION.SDK_INT。这个公共静态整数值将反映您在创建 avd 和在清单中指定 API 级别时使用的相同 API 级别。因此,你可以将这个值与android.os.Build.VERSION_CODES.DONUT进行比较,看看你运行的是 Android 1.6 还是更新版本。

包装 API

只要您尝试使用的 API 存在于您支持的所有 Android 版本中,只需分支就足够了。当 API 发生变化时,事情就会变得麻烦,比如当方法有新参数、新方法甚至新类时。您需要不管 Android 版本如何都可以工作的代码,同时还允许您利用新的可用 API。

挑战在于,如果你试图加载虚拟机代码,而这些代码引用了设备运行的 Android 版本中不存在的类、方法等,那么你的应用将会因VerifyError而崩溃。你需要针对包含你正在尝试使用的最新 API 的 Android 版本编译——你不能将代码加载到一个旧的 Android 设备上。

请注意,这里的关键词是“加载该代码”您不一定会因为应用中存在一个使用比现有 API 更新的类而遇到问题。只有当你执行的代码触发 Android 将那个类加载到你的运行进程中,你才会遇到VerifyError

记住这一点,有三个主要的技巧来处理这种情况,在下面的章节中概述。

检测类别

也许你需要做的就是禁用你的应用中的一些功能,这些功能会导致在给定的设备上不可能发生的事情。例如,假设您有一个使用片段特性的活动。您无法在 3.0 之前的设备上成功启动该活动。停止该活动可能只是禁用一个菜单选项或Button之类的事情。

要查看某个类(比方说,ListFragment)是否对您可用,可以调用Class.forName()。这将返回一个代表所请求的类的Class对象,或者抛出一个Exception,如果它不可用的话。您可以使用异常处理程序来禁用 UI 路径,这将导致您的应用尝试启动使用不可用类的活动。

反思

如果您需要对旧版本 Android 上不存在的类进行有限的访问,您可以使用一点反射。

例如,在关于旋转的章节中,我们使用了一系列允许用户选择联系人的示例应用。这依赖于一个ACTION_PICKIntent,使用特定的Uri作为联系人的内容提供者。在这些示例中,我们特别使用了ContactsContract,这是 Android 2.0 及更高版本中提供的修订版联系人 API。这意味着这些项目无法在旧版本的 Android 上运行。

然而,我们真正需要的是这个神奇的Uri值。如果我们能设计出一种方法,在不引起问题的情况下,为旧版本的 Android 获得正确的Uri,以及为新版本的 Android 获得正确的Uri,我们就能更好地向后兼容。

幸运的是,通过一些反射,这很容易做到:

`static {
  intsdk=new Integer(Build.VERSION.SDK).intValue();

if (sdk>=5) {
    try {
      Class clazz=Class**.forName**(“android.provider.ContactsContract$Contacts”);

CONTENT_URI=(Uri)clazz**.getField**(“CONTENT_URI”).get(clazz);
    }
    catch (Throwable t) {
      Log.e(“PickDemo”, “Exception when determining CONTENT_URI”, t);
    }
  }
  else {
    CONTENT_URI=android.provider.Contacts.People.CONTENT_URI;
  }
}`

在这里,我们通过查看Build.VERSION.SDK来检查设备的 API 级别(我们可以使用Build.VERSION.SDK_INT,但这是在 Android 1.6 之前添加的——这里显示的代码也适用于 Android 1.5)。如果我们在 Android 2.0 (API 级别5)或更高,我们使用Class.forName()来获得新的ContactsContract.Contacts类,然后使用反射来获得该类的CONTENT_URI静态数据成员。如果我们在旧版本的 Android 上,我们简单地使用旧的Contacts.People类发布的Uri

因为我们没有在代码中直接引用ContactsContract.Contacts,所以我们可以安全地执行它,即使是在旧版本的 Android 上。

条件类加载

反思是有用的,但对任何复杂的事物来说都是痛苦的。而且,它比直接调用代码要慢。

因此,最强大的技术是简单地组织您的代码,使您拥有使用较新 API 的常规类,但是您不在较旧的设备上加载这些类。我们将在本书的后面部分研究这种技术。

图案为冰淇淋三明治和蜂窝

随着 Honeycomb (Android 3.0)和现在的冰激凌三明治(Android 4.0)的出现,支持多个 Android 版本现在是一个重大挑战。在许多情况下,支持不同 UI 所需的 UI 更改需要您采取措施来确保您的应用仍然可以在旧版本的 Android 上成功运行。本节概述了处理向后兼容性领域的一些模式。

操作栏

正如在第二十七章中提到的,动作栏的许多基本特性将以向后兼容的方式工作。例如,指示选项菜单项可以显示在动作栏中只需要菜单资源 XML 中的一个属性,这个属性在旧版本的 Android 中将被忽略。支持 Honeycomb 的设备会将该项目放在操作栏中,而运行早期 Android 版本的设备不会。

然而,并不是动作栏的所有功能都是向后兼容的。在第二十七章的中的Menus/ActionBar示例应用中,我们添加了一个自定义的View到动作栏,允许人们在不处理菜单和对话框的情况下添加单词到我们的列表中。然而,这需要一些只在 API 级别11 (Android 3.0)和更高级别的代码。更高级的动作栏功能——超出了本书的范围——也有类似的需求。

您需要安排只在运行 API 级别11或更高的设备上使用那些动作栏方法。本章前面概述的条件类加载就是这样一种技术,也是在Menus/ActionBarBC示例应用中使用的技术。让我们来看看这是如何工作的。

检查 API 级别

我们最初的onCreateOptionsMenu()是这样的:

`@Override
public boolean onCreateOptionsMenu(Menu menu) {
  new MenuInflater(this).inflate(R.menu.option, menu);

EditText add=(EditText)menu
                         .findItem(R.id.add)
                         .getActionView()
                         .findViewById(R.id.title);

add**.setOnEditorActionListener**(onSearch);

return(super**.onCreateOptionsMenu**(menu));
}`

这很好,但是它将只在 API 级别11和更高的级别上起作用,因为getActionView()只从那个 API 级别开始存在。因此,在没有获得VerifyError的情况下,我们无法在旧版本的 Android 上运行这段代码,甚至无法加载这个类。

新版本的onCreateOptionsMenu()隐藏了违规代码,并检查 API 级别:

`@Override
public boolean onCreateOptionsMenu(Menu menu) {
  new MenuInflater(this).inflate(R.menu.option, menu);

EditText add=null;

if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB) {
    View v=ICSHCHelper**.getAddActionView**(menu);

if (v!=null) {
      add=(EditText)v**.findViewById**(R.id.title);
    }
  }

if (add!=null) {
    add**.setOnEditorActionListener**(onSearch);
  }

return(super**.onCreateOptionsMenu**(menu));
}`

我们只隐藏检索理论上放在动作栏中的View的代码。如果我们在一个旧版本的 Android 上,HONEYCOMB检查将失败,我们将以一个nullView结束,所以我们跳过将OnEditorActionListener添加到那个View内的EditText

这还有另一个好处:如果 Android 设备运行 API 级别11或更高,但没有空间容纳我们的自定义 APIView,它也能工作。Android 平板电脑将有一个动作栏和足够的空间,但未来支持蜂窝的手机可能会有一个动作栏,但缺乏足够的空间。在这种情况下,手机会保留添加选项菜单项,我们仍然会以一个nullView结束。这段代码处理这种情况;原始代码没有。

隔离冰淇淋三明治/蜂巢代码

我们的 Honeycomb 特定代码保存在一个单独的ICSHCHelper类中(ICS 用于冰激凌三明治,HC 用于 Honeycomb),该类仅用于 API 级别11(或更高)的设备:

`packagecom.commonsware.android.inflation;

importandroid.view.Menu;
importandroid.view.View;

classICSHCHelper {
  static View getAddActionView(Menu menu) {     return(menu.findItem(R.id.add).getActionView());
  }
}`

ICSHCHelper有一个单独的getAddActionView()静态方法,如果有添加动作栏条目的话,这个静态方法会找到它的View

因为我们不试图在这个类上执行任何代码,除了在HONEYCOMB检查中,在旧版本的 Android 上有这个类是安全的。Menus/ActionBarHC应用可以在 Android 1.6 及更高版本上运行。

编写平板电脑专用应用

理想情况下,您的 Android 应用可以在所有形式的设备上运行:手机、平板电脑等等。然而,你可能想创建一个不能在手机上使用的应用。理想情况下,你应该让你的应用远离小屏幕设备,这样用户才不会失望。

要做到这一点,你可以利用这样一个事实,即 Android 将扩大应用,但不会缩小应用。换句话说,如果你指定你的应用不支持一些更大的屏幕尺寸(例如,android:xlargeScreens="false"出现在你的AndroidManifest.xml文件的<supports-screens>元素中),Android 仍然允许你的应用在这样的屏幕上运行,并采取措施帮助你的应用在额外的屏幕空间上运行。但是,如果您指定您的应用不支持一些较小的屏幕尺寸(例如,android:smallScreens="false"出现在您的<supports-screens>元素中),Android 将不会运行您的应用,您将被过滤出此类设备的 Android 市场。

因此,如果您的应用只能在大屏幕设备上运行良好,请使用如下的<supports-screens>元素:

<supports-screens android:xlargeScreens="true"                  android:largeScreens="true"                  android:normalScreens="false"                  android:smallScreens="false"                  android:anyDensity="true"/>

三十、访问文件

虽然 Android 通过偏好和数据库提供结构化存储,但有时一个简单的文件就足够了。Android 提供了两种访问文件的模式:一种是应用预打包的文件,另一种是应用在设备上创建的文件。

你和你骑的马

假设您有一些静态数据希望随应用一起提供,比如拼写检查器的单词列表。最简单的部署方法是将文件放在res/raw目录中,这样它将作为打包过程的一部分作为原始资源放在 Android 应用 APK 文件中。

要访问这个文件,您需要给自己弄一个Resources对象。从一个活动中,这就像调用getResources()一样简单。一个Resources对象提供openRawResource()来获取你指定文件的一个InputStreamopenRawResource()期望打包的文件有一个整数标识符,而不是路径。这就像通过findViewById()访问小部件一样;例如,如果将一个名为words.xml的文件放在res/raw中,这个标识符在 Java 中可以作为R.raw.words来访问。

因为您只能获得一个InputStream,所以您无法修改这个文件。因此,它实际上只对静态参考数据有用。此外,由于它在用户安装应用包的更新版本之前不会改变,所以要么引用数据必须在可预见的将来有效,要么您必须提供一些更新数据的方法。最简单的处理方法是使用引用数据来引导一些其他可修改的存储形式(例如,数据库),但这会导致存储中数据的两个副本。另一种方法是保持参考数据不变,并将修改保存在文件或数据库中,然后在需要完整的信息时将它们合并在一起。例如,如果您的应用提供了一个 URL 文件,您可以拥有另一个文件来跟踪用户添加的 URL 或引用用户删除的 URL。

Files/Static示例项目中,您会发现对前面的列表框示例进行了修改,这次使用了静态 XML 文件,而不是 Java 中的硬连线数组。布局是相同的:

<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"   android:orientation="vertical"   android:layout_width="fill_parent"   android:layout_height="fill_parent" >   <TextView     android:id="@+id/selection"     android:layout_width="fill_parent"     android:layout_height="wrap_content"   />   <ListView     android:id="@android:id/list"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:drawSelectorOnTop="false"   /> </LinearLayout>

除了这个 XML 文件之外,您还需要一个 XML 文件,其中包含要在列表中显示的单词:

<words>   <word value="lorem" />   <word value="ipsum" />   <word value="dolor" />   <word value="sit" />   <word value="amet" />   <word value="consectetuer" />   <word value="adipiscing" />   <word value="elit" />   <word value="morbi" />   <word value="vel" />   <word value="ligula" />   <word value="vitae" />   <word value="arcu" />   <word value="aliquet" />   <word value="mollis" />   <word value="etiam" />   <word value="vel" />   <word value="erat" />   <word value="placerat" />   <word value="ante" />   <word value="porttitor" />   <word value="sodales" />   <word value="pellentesque" />   <word value="augue" />   <word value="purus" /> </words>

虽然这种 XML 结构并不完全是空间效率的模型,但对于演示来说已经足够了。

Java 代码现在必须读入 XML 文件,解析出单词,并将它们放在列表可以获取地方:

public class StaticFileDemo extends ListActivity {   TextView selection; `ArrayList items=new ArrayList();

@Override
  public void onCreate(Bundle icicle) {
    super**.onCreate**(icicle);
    setContentView(R.layout.main);
    selection=(TextView)findViewById(R.id.selection);

try {
      InputStream in=getResources().openRawResource(R.raw.words);
      DocumentBuilder builder=DocumentBuilderFactory
                               .newInstance()
                               .newDocumentBuilder();
      Document doc=builder**.parse**(in, null);
      NodeList words=doc**.getElementsByTagName**(“word”);

for (inti=0;i<words**.getLength**();i++) {
        items**.add**(((Element)words**.item**(i)).getAttribute(“value”));
      }

in**.close();
    }
    catch (Throwable t) {
      Toast
        .makeText(this, "Exception: "+t
.toString**(), Toast.LENGTH_LONG)
        .show();
    }

setListAdapter(new ArrayAdapter(this,
                                 android.R.layout.simple_list_item_1,
                                 items));
  }

public void onListItemClick(ListView parent, View v, int position,
                  long id) {
    selection**.setText**(items**.get**(position).toString());
  }
}`

**注意:**我们对openRawResource()的调用引用了前面描述的R.raw.words。从冰激凌三明治开始,更具体地说,SDK 和 ADT 版本 14 和 15,Google 已经禁止以这种方式引用一些资源字段,允许库项目只编译一次,然后跨应用重用。通常,这不值得一提。然而,在 Eclipse 中,随 SDK 14 发布的 ADT 插件错误地将我们的用法标记为错误,试图在switch语句中使用R.raw.words。在这个问题解决之前,您需要从命令行构建或者调整您的 ADT 插件级别。

分歧主要在onCreate()内部。我们为 XML 文件(getResources().openRawResource(R.raw.words))获取一个InputStream,然后使用内置的 XML 解析逻辑将文件解析成一个 DOM Document,挑选出单词元素,然后将值属性注入一个ArrayListArrayAdapter使用。

产生的活动看起来和以前一样,如 Figure 30–1 所示,因为单词列表是相同的,只是重新定位了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 30–1。**static file demo 示例应用

当然,还有更简单的方法将 XML 文件作为预打包文件提供给你,比如使用 XML 资源。这在第三十一章中有所涉及。然而,虽然这个例子使用了 XML,但是这个文件也可以是一个简单的每行一个单词的列表,或者是 Android 资源系统本身不处理的其他格式。

读取“n 个 writin”

读写您自己的特定于应用的数据文件几乎与您在桌面 Java 应用中所做的一样。关键是在活动或其他上下文中使用 openFileInput()和 openFileOutput()来分别获得 InputStream 和 OutputStream。从这一点来看,它与常规 Java I/O 逻辑没有太大区别:

  • 根据需要包装那些流,比如通过使用一个InputStreamReaderOutputStreamWriter用于基于文本的 I/O。
  • 读取或写入数据。
  • 完成后,使用close()释放流。

如果两个应用都试图通过openFileInput()读取一个notes.txt文件,那么每个应用都将访问自己的文件版本。如果您需要从许多地方访问一个文件,您可能希望创建一个内容提供者,这将在下一章中描述。

注意openFileInput()openFileOutput()不接受文件路径(如path/to/file.txt),只接受简单的文件名。

下面是世界上最简单的文本编辑器的布局,摘自Files/ReadWrite示例应用:

<?xml version="1.0" encoding="utf-8"?> <EditTextxmlns:android="http://schemas.android.com/apk/res/android"   android:id="@+id/editor"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:singleLine="false"   android:gravity="top"   />

我们这里只有一个很大的文本编辑小工具…这很无聊。

Java 只是稍微复杂一点:

`package com.commonsware.android.readwrite;

importandroid.app.Activity;
importandroid.os.Bundle;
importandroid.view.View;
importandroid.widget.Button;
importandroid.widget.EditText;
importandroid.widget.Toast;
importjava.io.BufferedReader;
importjava.io.File;
importjava.io.InputStream;
importjava.io.InputStreamReader;
importjava.io.OutputStream;
importjava.io.OutputStreamWriter;

public class ReadWriteFileDemo extends Activity {
  private final static String NOTES=“notes.txt”;
  privateEditText editor;

@Override
  public void onCreate(Bundle icicle) {
    super**.onCreate**(icicle);
    setContentView(R.layout.main);
    editor=(EditText)findViewById(R.id.editor);
  }

public void onResume() {
    super**.onResume**();

try {
      IputStream in=openFileInput(NOTES);

if (in!=null) {         InputStreamReader tmp=new InputStreamReader(in);
        BufferedReader reader=new BufferedReader(tmp);
        String str;
        StringBuilderbuf=new StringBuilder();

while ((str = reader**.readLine**()) != null) {
          buf**.append**(str+“\n”);
        }

in**.close**();
        editor**.setText**(buf**.toString**());
      }
    }
    catch (java.io.FileNotFoundException e) {
      // that’s OK, we probably haven’t created it yet
    }
    catch (Throwable t) {
      Toast
        .makeText(this, "Exception: "+t**.toString**(), Toast.LENGTH_LONG)
        .show();
    }
  }

public void onPause() {
    super**.onPause**();

try {
      OutputStreamWriter out=
          new OutputStreamWriter(openFileOutput(NOTES, 0));

out**.write**(editor**.getText**().toString());
      out.close();
    }
    catch (Throwable t) {
      Toast
        .makeText(this, "Exception: "+t.toString(), Toast.LENGTH_LONG)
        .show();
    }
  }
}`

首先,我们挂钩到onResume(),这样我们就可以控制我们的编辑器什么时候复活,从新启动还是被冻结。我们使用openFileInput()读入notes.txt,并将内容注入文本编辑器。如果没有找到文件,我们假设这是第一次运行活动(或者文件通过其他方式被删除),我们只是让编辑器为空。

接下来,我们挂钩到onPause(),这样当我们的活动被另一个活动隐藏或者被关闭时,我们就可以获得控制权,比如通过设备的 Back 按钮。这里,我们使用openFileOutput()打开notes.txt,将文本编辑器的内容注入其中。

最终结果是我们有了一个持久的记事本,如图图 30–2 和 30–3 所示。任何输入的内容都会保留,直到被删除,直到我们的活动被关闭(例如,通过后退按钮),手机被关闭,或者类似的情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**图 30–2。**read write file demo 示例应用,如同最初启动的

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 30–3。 同样的应用,输入一些文字后

使用应用本地文件的另一种方法是使用getFilesDir()。这将返回一个File对象,该对象指向板载闪存中应用可以存储文件的位置。这个目录是openFileInput()openFileOutput()工作的地方。然而,虽然openFileInput()openFileOutput()不支持子目录,但如果需要,可以使用getFilesDir()中的File来创建和导航子目录。

默认情况下,只有您的应用可以访问存储在此处的文件。设备上的其他应用无权读取该空间,更不用说写入了。然而,请记住,一些用户“root”他们的 Android 手机,获得超级用户权限。这些用户将能够读写他们想要的任何文件。因此,请不要认为应用本地文件对感兴趣的用户是安全的。

外部存储:巨大的经济空间

除了应用本地存储,您还可以访问外部存储。这可能是可移动媒体卡的形式,如 SD 卡或 microSD 卡,或者是附加的板载闪存,用作“外部存储”。

有利的一面是,外部存储往往比板载存储有更多的可用空间。机载存储可能相当有限;例如,最初的 T-Mobile G1 (HTC Dream)所有应用的总容量为 70MB。虽然新手机提供了更多的板载空间,但外部存储通常至少为 2GB,最大可达 32GB。

不利的一面是,如果愿意,所有应用都可以读写外部存储,因此这些文件不是很安全。此外,外部存储可以作为 USB 大容量存储设备安装在主机上,当它在此模式下使用时,Android 应用无法访问它。因此,在任何给定的时刻,外部存储器上的文件可能对您可用,也可能不可用。

往哪里写

如果您的应用有太大的文件,不能冒险放在应用本地文件区域,那么您可以使用getExternalFilesDir(),它可用于任何活动或其他Context。这为您提供了一个File对象,它指向在外部存储器上自动创建的目录,对于您的应用是唯一的。虽然不能抵御其他应用,但它有一个很大的优势:当您的应用被卸载时,这些文件会被自动删除,就像应用本地文件区域中的文件一样。

如果你有更多属于用户而不是你的应用的文件(例如,相机拍摄的照片、下载的 MP3 文件等。),更好的解决方案是使用Environment类上可用的getExternalStoragePublicDirectory()。这为您提供了一个File对象,该对象基于您传递给getExternalStoragePublicDirectory()的类型,指向为特定类型的文件留出的目录。例如,你可以分别请求DIRECTORY_MOVIESDIRECTORY_MUSICDIRECTORY_PICTURES来存储 MP4、MP3 或 JPEG 文件。卸载应用时,这些文件将被留下。

您还会在Environment上找到一个getExternalStorageDirectory()方法,指向外部存储的根目录。这不再是首选方法,之前描述的方法有助于更好地组织用户的文件。然而,如果你支持旧的 Android 设备,你可能需要使用getExternalStorageDirectory(),仅仅是因为新的选项可能对你不可用。

什么时候写

从 Android 1.6 开始,您还需要持有权限才能使用外部存储(例如,WRITE_EXTERNAL_STORAGE)。权限的概念将在后面的章节中介绍。

此外,如果用户将外部存储器作为 USB 存储设备安装,它可能会被占用。您可以使用getExternalStorageState()(在Environment上的一个静态方法)来确定外部存储器目前是否可用。

StrictMode:避免 Janky 代码

如果用户觉得你的应用响应迅速,他们就更可能喜欢你的应用。所谓“反应灵敏”,我们的意思是它对用户的操作做出快速而准确的反应,比如点击和滑动。

相反,如果用户认为你的 UI“笨拙”——对他们的请求反应迟钝——他们就不太可能对你的应用满意。例如,也许你的列表不能像用户希望的那样平滑滚动,或者点击一个按钮不能立即得到他们想要的结果。

虽然线程和AsyncTask之类的东西会有所帮助,但是在哪里应用它们并不总是显而易见的。使用 Traceview 或类似的 Android 工具进行全面的性能分析当然是可能的。然而,开发人员在主应用线程上做的一些标准的事情,有时很偶然,往往会导致缓慢:

  • 用于板载存储和外部存储(例如 SD 卡)的闪存 I/O
  • 网络输入输出

然而,即使在这里,也可能看不出您是在主应用线程上执行这些操作。当操作实际上是由您简单调用的 Android 代码完成时,情况尤其如此。

这就是StrictMode的用武之地。它的任务是帮助您确定您在主应用线程上做什么事情可能会导致不愉快的用户体验。

设置严格模式

制定一套政策。目前有两类策略:虚拟机策略和线程策略。VM 策略代表了与您的整个应用相关的糟糕的编码实践,特别是泄漏 SQLite Cursor对象和 kin。线程策略表示在主应用线程上执行时不好的事情,特别是闪存 I/O 和网络 I/O。

每个策略都规定了StrictMode应该注意什么(例如,闪存读取可以,但闪存写入不行)以及当您违反规则时StrictMode应该如何反应,例如

  • 将消息记录到 LogCat
  • 显示一个对话框
  • 崩溃你的应用(说真的!)

最简单的方法是从第一个活动的onCreate()调用StrictMode上的静态enableDefaults()方法。这将设置正常操作,通过简单地记录到 LogCat 来报告所有违规。但是,如果您愿意,您可以通过Builder对象设置自己的定制策略。

了解 StrictMode 的实际应用

Threads/ReadWriteStrict示例应用是本章前面显示的Files/ReadWrite示例应用的翻版。它所添加的只是一个定制的StrictMode线程策略:

StrictMode**.setThreadPolicy**(new StrictMode.ThreadPolicy**.Builder**()                            **.detectAll**()                            **.penaltyLog**()                            **.build**());

如果您运行该应用,用户将看不到任何区别。但是,您将在 LogCat 中看到一条调试级别的日志消息,其中包含以下堆栈跟踪:

12-28 17:19:40.009: DEBUG/StrictMode(480): StrictMode policy violation; ~duration=169![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U001.jpg)  ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2 12-28 17:19:40.009: DEBUG/StrictMode(480): at![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U001.jpg)  android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:745) 12-28 17:19:40.009: DEBUG/StrictMode(480): at![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U001.jpg)  dalvik.system.BlockGuard$WrappedFileSystem.open(BlockGuard.java:228) 12-28 17:19:40.009: DEBUG/StrictMode(480): at![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U001.jpg)  android.app.ContextImpl.openFileOutput(ContextImpl.java:410) 12-28 17:19:40.009: DEBUG/StrictMode(480): at![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U001.jpg)  android.content.ContextWrapper.openFileOutput(ContextWrapper.java:158) 12-28 17:19:40.009: DEBUG/StrictMode(480): at![images](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/begin-andr4/img/U001.jpg)  com.commonsware.android.readwrite.ReadWriteFileDemo.onPause(ReadWriteFileDemo.java:82) …

这里,StrictMode警告我们,我们试图在主应用线程(我们设置了StrictMode策略的线程)上进行闪存写入。理想情况下,我们会重写这个项目,以使用一个AsyncTask或写出数据的东西。

请只开发!

不要在生产代码中使用StrictMode。它是为构建、测试和调试应用而设计的。它不是为野外使用而设计的。

为了解决这个问题,你可以

  • 在准备生产构建时,只需注释掉或删除StrictMode安装代码
  • 需要时,使用某种生产标志跳过StrictMode设置代码
有条件的严格

StrictMode仅适用于 Android 2.3 及更高版本。因此,如果我们的代码中有它,即使是在开发模式中,当我们尝试在旧的模拟器或设备上测试时,它可能会干扰。正如我们在前面的章节中看到的,有一些技术可以解决这个问题,但是使用反射来配置StrictMode会非常痛苦。

因此,正确的方法是简单地组织您的代码,使您拥有使用较新 API 的常规类,但是您不要在较旧的设备上加载这些类。APIVersions/ReadWriteStrict项目演示了这一点,允许应用在可用的地方使用 Android 2.3 的StrictMode,在不可用的地方跳过它。

当我们在本节前面检查StrictMode时,我们在示例活动的onCreate()方法中配置了StrictMode。这是可行的,但是只在 Android 2.3 和更新的版本上。

为了让它在旧版本的 Android 上工作,我们使用了StrictWrapper:

`packagecom.commonsware.android.readwrite;

importandroid.os.Build;

abstract class StrictWrapper {
  static private StrictWrapper INSTANCE=null;

static public void init() {
    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.GINGERBREAD) {
      INSTANCE=new StrictForRealz();
    }
    else {
      INSTANCE=new NotAllThatStrict();
    }
  }

static class NotAllThatStrict extends StrictWrapper {
    // no methods needed
  }
}`

这个看起来很奇怪的类封装了我们处理StrictMode的“做我们或者不做我们”的逻辑。它包含一个init()方法,当被调用时,检查应用运行在哪个版本的 Android 上,并基于它创建一个StrictWrapper子类的单例实例——对于 Android 2.3 和更高版本为StrictForRealz,对于旧版本的 Android 为NotAllThatStrict。后一个类是StrictWrapper的静态内部类,什么都不做,反映出 Android 较新版本中没有StrictMode

StrictForRealz包含了StrictMode的初始化逻辑:

`packagecom.commonsware.android.readwrite;

importandroid.os.StrictMode;

classStrictForRealz extends StrictWrapper {
  StrictForRealz() {
    StrictMode**.setThreadPolicy**(new StrictMode.ThreadPolicy**.Builder**()
                               .detectAll()
                               .penaltyLog()
                               .build());
  }
}`

并且,我们活动的onCreate()方法调用StrictWrapper上的init(),以触发创建适当的对象:

`@Override
public void onCreate(Bundle icicle) {
  super**.onCreate**(icicle);
  setContentView(R.layout.main);

StrictWrapper**.init**();

editor=(EditText)findViewById(R.id.editor);
}`

当活动第一次启动时,StrictWrapperStrictForRealz都没有被加载到流程中。一旦我们到达onCreate()中的init()语句,Android 就会将StrictWrapper加载到这个过程中,但是这是安全的,因为它不会引用任何可能不存在的类。只有当我们安全地使用受支持的 Android 版本时,StrictWrapper上的init()方法才会执行涉及StrictForRealz的语句。因此,StrictForRealz只有在我们使用较新的 Android 版本时才会被加载到进程中,所以我们在StrictForRealz中使用StrictMode不会触发VerifyError

在这里,我们所需要的只是一点初始化。singleton 模式用于演示如果您愿意,您可以公开一个依赖于版本的 API 实现。简单地将 API 定义为抽象类(StrictWrapper)上的抽象方法,并在具体子类(StrictForRealzNotAllThatStrict)上拥有这些抽象方法的依赖于版本的具体实现。

Linux 文件系统:同步,你赢了

Android 建立在 Linux 内核之上,使用 Linux 文件系统来保存文件。传统上,Android 使用 YAFFS(另一种闪存文件系统),优化用于低功耗设备,将数据存储到闪存中。今天,许多设备仍在使用 YAFFS。

YAFFS 有一个大问题:一次只有一个进程可以写入文件系统。YAFFS 不提供文件级锁定,而是提供分区级锁定。这可能会成为一个瓶颈,特别是当 Android 设备的功能越来越强大,并且开始想要同时做更多的事情时,就像他们的台式机和笔记本兄弟一样。

Android 开始向 ext4 发展,ext 4 是另一个针对台式机/笔记本的 Linux 文件系统。您的应用不会直接感觉到差异。然而,ext4 做了相当多的缓冲,它会给没有考虑这种缓冲的应用带来问题。2008 年和 2009 年,当 ext4 开始流行时,Linux 应用开发人员一头扎进了这个领域。作为一个 Android 开发者,你现在需要考虑一下…你自己的文件存储。

如果你使用的是 SQLite 或者SharedPreferences,就不需要担心这个问题。Android(和 SQLite,如果你正在使用它)为你处理所有的缓冲问题。但是,如果您编写自己的文件,您可能希望在将数据刷新到磁盘时考虑一个额外的步骤。具体来说,您需要触发一个名为fsync()的 Linux 系统调用,它告诉文件系统确保所有缓冲区都被写入磁盘。

如果你在同步模式下使用java.io.RandomAccessFile,这一步也会为你处理,所以你不需要担心。然而,Java 开发人员倾向于使用FileOutputStream,它不会触发fsync(),即使您在流上调用close()。相反,你在FileOutputStream上调用getFD().sync()来触发fsync()。注意,这可能很耗时,所以只要可行,磁盘写应该在主应用线程之外完成,比如通过AsyncTask

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值