安卓初学者入门指南(二)
九、探索 Android 概念:核心 UI 小部件
在本书的第一部分中,您快速浏览了为您的 Android 应用创建新的 Android Studio 项目所需的步骤,并探索了默认 Android 项目的结构、文件和各个部分的用途。您深入研究了使用应用配色方案的 XML 配置创建自己的行为的初始步骤,并利用文本资源在应用的主活动中个性化欢迎消息。
在这一章中,我们将超越前面章节的表面变化,直接进入所有 Android 应用中可用的主要用户界面元素,以及那些将成为您为所有未来 Android 工作开发和部署的许多活动的主体的元素。您将学习如何部署、调整和控制作为 Android 开发人员可用的许多用户界面小部件,并开始打造更复杂的 Android 用户体验之旅。
一切从视野开始
在 Android 世界中,你可以在用户界面上显示的每一个主要元素——一个活动——都继承自一个叫做 View 的基类。任何源自视图的小部件,无论是文本框、按钮、选择列表还是其他什么,都为您提供了一系列源自这种视图谱系的常见行为和好处。这些常见的行为和属性包括以一致的方式设置和控制字体、颜色和其他样式特征。
除了这些共同的特征之外,由于所有小部件都有共同的视图遗产,所以还有一系列的方法和属性可用。接下来我们将讨论这些遗传特征。
从视图派生的关键方法
从 View 基类派生的任何小部件都继承了一系列方法和属性,这些方法和属性有助于管理基本状态管理、与其他小部件分组、布局中的父对象和子对象等等。您将看到的属性包括
-
findViewById():查找具有给定 ID 的小部件,广泛用于将 XML 中定义的小部件链接到 Java(和 Kotlin)中的控制逻辑。 -
getParent():查找父对象,无论是小工具还是容器。 -
getRootView():获取从活动对setContentView()的原始调用中提供的树根。 -
setEnabled(), isEnabled():设置并检查任何小部件的启用状态,例如,复选框、单选按钮等。 -
isClickable():报告该视图(如按钮)是否对点击或按压事件做出反应。 -
onClickListener():对于像按钮这样可以点击的视图,定义了一个回调,当相关的视图被点击时会被触发。回调实现包含您决定需要的任何逻辑。
从视图派生的关键属性和特性
除了 View 基类的核心方法,所有小部件还继承了一些关键属性。这些属性包括
-
android:contentDescription:这是一个与辅助工具可以使用的任何部件相关的文本值,其中部件的视觉方面对用户帮助很小或没有帮助。 -
android:visibility:决定小部件在第一次实例化时是可见还是不可见。 -
android:padding, android:paddingLeft, android:paddingRight, android:paddingTop, android:paddingBottom:在小工具的所有边上填充值的各种方法。注意小部件的填充也可以在运行时用
setPadding()方法设置。
介绍 Android 中的核心 UI 小部件
在构建 Android 应用时,您会一次又一次地使用一组核心 UI 小部件,因为它们提供了计算机、智能手机等用户在过去几十年中已经学会期待的许多常见用户界面体验。让我们一步一步地看这些核心部件的例子。
用文本视图标记事物
提供一个可读的文本标签可能是所有 UI 小部件中最基本的,你会在几乎所有发明的设计工具包中找到一个标签或静态文本等价物。Android 提供了TextView小部件来实现这个功能,允许您在活动 UI 的任何地方放置一个静态字符串(或者至少一开始是静态的,因为TextView的值可以通过编程来更改)。该字符串的文本完全由您决定,是提供相邻小部件的描述、标题、一些评论还是注释——选择权在您。
Android 提供了两种主要的方法来定义一个TextView和所有的 UI 小部件。第一种方法是完全通过使用 Java 代码定义您的TextView,设置像屏幕上的位置、大小、文本有效载荷等属性。任何以前有过开发重要用户界面经验的人都会告诉你这是一个很大的工作量,很容易出错,你会很快看到你的代码变得难以管理。但是有更好的方法,你已经用过了!
使用 Android 的其他 UI 设计方法要快得多,也容易得多,这是通过声明性 XML 实现的。在第三章中,您创建了自己的MyFirstApp应用,并负责TextView小部件中的文本。在第七章中,你进一步涉猎了控制TextView字符串的来源和其他一些装饰属性。您可以随时添加您认为有意义的任意数量的TextView小部件,或者直接通过在活动的 XML 定义文件中定义更多的<TextView>元素,或者使用图形化布局编辑器。
图形布局编辑器让您担心视觉样式,并在幕后自动生成必要的匹配 XML 来描述您的TextView小部件。让我们现在就来测试一下。即使你的MyFirstApp应用非常好,我们也不要把它放在眼里。在 Android Studio 中,通过File ➤ New... ➤ New Project菜单选项创建一个新项目。就像我们在第三章做的那样,给你的项目取一个有意义的名字,比如TextViewExample,选择空的活动模板。这将创建您的新项目,并默认在activity_main.xml文件中放置一个TextView小部件(就像您创建MyFirstApp应用时发生的一样)。您的activity_main.xml内容应该类似于清单 9-1 。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-1A fresh Android Studio application using the Empty Activity template
与其在这里编辑 XML,不如通过单击 Android Studio 视图最右侧的 Design 按钮来调用图形布局设计器。这应该会隐藏 XML 内容,而是为您的应用呈现等效的图形布局,如图 9-1 所示。
图 9-1
调用图形布局设计器
这个视图中有很多内容,但是最好的学习方法是依次试验每个领域。从 Palette 部分开始,您将看到一个小部件类型列表——Common、Text、Buttons 等等——在它的旁边是一个实际小部件的列表。在这个列表的顶部,你会看到TextView,如图 9-1 所示。单击并拖动它到你在屏幕中间看到的微型屏幕上,你应该会看到一个微小的浮动标签在移动,直到你松开鼠标键。去做吧,在任何你认为“正确”的地方去做。
第二个TextView现在已经就位,但是您将会看到一个红色的错误标志——单击它将会通知您还没有对新的TextView设置“约束”,因此如果您实际构建并运行这个应用,您已经放置它的位置将不会被保留。别慌!我们现在要解决这个问题,但是是在你的 XML 定义中,并且在这个过程中学习更多关于TextView小部件的知识。通过单击最右侧的代码按钮,切换回布局的代码视图。
您的activity_main.xml文件现在将类似于清单 9-2 ,添加了一个新的<TextView>元素和相关属性。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="176dp"
android:layout_marginLeft="176dp"
android:layout_marginBottom="252dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-2Your revised activity_main.xml file
我们已经在前面的章节中修改了<TextView>属性,现在我们要去逛逛了!让我们介绍一些新的属性来帮助您控制TextView和其他小部件:
-
Android:layout _ margin star
-
Android:layout _ margin
-
android:layout_marginLeft
-
android:layout_marginRight
-
android:layout_marginBottom
顾名思义,这些都有助于在小部件的不同边缘设置边距。
我们还受益于应用的默认布局方法,称为ConstraintLayout。我们将在接下来的章节中更详细地讨论布局,但是现在你可以把它们看作是帮助你在活动中放置小部件的方法。有些布局会照顾到你的很多设置,代价是稍微少了一些艺术自由,而另一些会给你全权委托,但让你需要做更多的工作。ConstraintLayout是前一种,试图尽可能帮助你得到好看的布局。它为所有小部件的布局带来的几个关键属性包括
-
app:layout _ constraint top _ toTopOf
-
app:layout _ constraint bottom _ tobottom of
-
app:layout _ constraint left _ toLeftmOf
-
app:layout _ constraint right _ toRightOf
还有许多layout_constraint*样式属性的组合,它们都是为了能够指定如何根据与另一个小部件的顶部、底部、左侧或右侧边缘的接近度和关系,以及它的中心、相关小部件中任何文本的位置等等来对齐和调整小部件的大小!
我们需要这些布局边距属性和约束属性为我们的新TextView提供可预测的行为,以粗体显示:
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:layout_marginStart="176dp"
android:layout_marginLeft="176dp"
android:layout_marginBottom="252dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
实际值相当容易理解。这三个不同的边距属性以像素为单位设置 TextView 周围的边距大小。布局约束属性将垂直和水平约束默认绑定到称为“父”的东西。在这种情况下,这意味着父活动本身。相反,您可以参考另一个小部件的android:id,让它作为约束小部件的指导因素。
有了这些修改,你应该保存你的activity_main.xml文件,然后运行你的TextViewExample应用,看看你的新标签是否在你指定的位置,如图 9-2 所示。
图 9-2
应用的更多 TextView 标签
为了完整地描述TextView部件,您应该知道有将近 100 种不同的属性可以控制TextView标签的行为、样式、大小、颜色等等。更多例子包括
-
android:hint:要显示的提示。
-
android:typeface:设置标签使用的字体(例如,monospace)。
-
android:textStyle:指示应该对文本应用粗体(bold)、斜体(italic)和两者(bold_italic)的什么组合。
-
android.textAppearance:一个综合属性,可以让您一次性组合文本颜色、字体、大小和样式!
-
android:textColor:使用常见的十六进制 RGB 符号来选择标签的文本颜色。比如#0000FF 就是蓝色。
您可以在Ch09/TextViewExample项目文件夹中找到这个例子的代码。
到目前为止,我们已经看到了 XML 在 Android 中可以做些什么,这是很有说明性的,也很有教育意义,但是开发应用的真正能力和灵活性来自于您选择的编程语言和 Android 的 XML 能力的结合。一旦我们转向更复杂的 UI 小部件,部署您的 Java(或 Kotlin)能力就变得既必要又可取,您将在接下来关于其他 UI 小部件的章节中看到这一点。
扣上完美的用户界面
按钮是任何 UI 开发的基础,可以追溯到模拟电视、收音机等电子设备上真实世界的模拟按钮(还记得那些吗?),还有汽车仪表盘。Android 提供了几种类型的按钮,其中最简单的是 android.widget 包中的Button小部件,它在按钮的“表面”包含简单的文本。让我们开始创建一个新的应用,它使用一个按钮和一些 Java 逻辑来跟踪和控制出现在按钮上的文本。您可以在Ch09/ButtonExample项目文件夹中查看这个例子。
首先,使用 Android Studio 新建项目向导创建一个新项目,并选择空的活动模板作为起点。将您的项目命名为ButtonExample(或您认为同样具有描述性的名称)。创建项目后,打开 activity_main.xml 文件并删除默认的 TextView 元素。切换到布局编辑器中的设计视图,从面板中选择Button小部件并将其放在屏幕上。按钮的确切位置并不重要,因为我们将在以后让它完全填充我们的应用活动。你应该会看到一个与图 9-3 相似的布局。
图 9-3
向 ButtonExample 的设计蓝图添加按钮
请注意,这两个错误通知您该按钮不受约束。这是因为当您选择使用 ConstraintLayout 布局来容纳其他子 UI 小部件时,这些小部件中的每一个都需要至少一个水平和一个垂直“约束”,或者关于它们应该如何相对于父布局或另一个小部件的顶部、底部、开始和结束进行定位的指令。要解决这个问题,请在每条边的中点使用两个圆中的一个,然后单击并拖动连接器到设计蓝图的边上。这会将按钮的水平约束设置为活动的侧边,减去任何边距。然后选择按钮上边缘或下边缘中心的圆,单击并拖动连接符到活动的上边缘或下边缘,以设置垂直约束。切换到您的activity_main.xml文件的代码视图,它应该类似于清单 9-3 中所示。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:layout_editor_absoluteX="161dp"
tools:layout_editor_absoluteY="341dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-3ButtonExample activity_main.xml file, showing Button definition
接下来,我们需要为我们的按钮提供一些魅力,让它真正做一些事情。因此,打开ButtonExample的MainActivity.java源文件,用清单 9-4 中的 Java 代码替换它的内容。
package org.beginningandroid.buttonexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button myButton;
Integer myInt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myButton = new Button(this);
myButton.setOnClickListener(this);
myInt = 0;
updateClickCounter();
setContentView(myButton);
}
@Override
public void onClick(View v) {
updateClickCounter();
}
private void updateClickCounter() {
myInt++;
myButton.setText(myInt.toString());
}
}
Listing 9-4Modified Java code for ButtonExample
将这些代码一部分一部分地分解将帮助你理解正在发生的事情,并且展示一些你将用于所有开发的通用方法,而不仅仅是一个点击按钮的应用!
前面清单中的前几行介绍了包名和任何所需的或必需的 Java 类导入:
package org.beginningandroid.buttonexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
已经根据您在新建项目向导中输入的名称为您设置了包名。默认情况下,androidx.appcompat.app.AppCompatActivity和android.os.Bundle的类库导入也包含在新建项目向导中。然后,我显式地添加了android.widget.Button类,因为这是一个预构建的类,它提供了我想要连接到我放置在布局中的按钮的所有公共逻辑和行为。我最后添加了android.view.View类,因为它提供了预构建的能力,可以为从 View 类派生的小部件上发生的事件定义监听器——这意味着我们在本章中讨论的每个小部件。特别是,如下面几段所示,我定义并使用了一个OnClick监听器,这样ButtonExample应用就可以被通知点击事件并触发我想要的逻辑。
通过对MainActivity的类定义的更改,您可以看到启用OnClick监听的第一步。我修改了新建项目向导提供的默认类声明,还添加了实现View.OnClickListener,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button myButton;
Integer myInt;
// more code here
}
对该类的这一修改使我们能够稍后提供OnClick逻辑 Android Studio 将通过一个警告提示您这样做,如果您还没有添加android.view.View导入,还会提示您添加。接下来我定义了两个方便的变量,一个是名为myButton的Button,它将被连接到 UI 小部件,另一个是名为 myInt 的整数,我用它作为计数器来跟踪按钮被点击的次数。
接下来,增强了onCreate()方法,让它在活动第一次运行时执行我们需要的设置任务。我们将在第十二章中深入探讨活动的四种主要生命周期方法,但是现在你可以相信这样一个事实,即onCreate()只在活动开始时被调用一次。在ButtonExample代码中,我们正在定义和实例化myButton对象,为myButton设置OnClick监听器,最初将myInt的值设置为0(零),然后调用私有方法updateClickCounter()。
两个非常简单的步骤组成了整个updateClickCounter()方法,如下所示:
private void updateClickCounter() {
myInt++;
myButton.setText(myInt.toString());
}
首先,myInt值增加 1。随后,调用 myButton 对象的.setText() helper 方法,并将myInt值的String表示传递给它,以便更新按钮小部件上显示的文本来反映新的myInt值。简而言之,我们增加点击计数器的值,并将其显示为按钮文本。
我们跳过的最后一段代码是onClick()方法,每当单击按钮时,我们定义的监听器都会调用该方法。这个方法中的逻辑简单地调用私有的updateClickCounter()方法,这意味着我们在onCreate()时间和任何后续的按钮点击中重用相同的逻辑。
继续运行你的代码——或者书中的Ch09/ButtonExample代码——你应该会看到一个按钮填充的应用,用一个递增计数器作为按钮标签,类似于图 9-4 所示。
图 9-4
正在运行的按钮示例应用
使用 ImageView 和 ImageButton 获取图片
如果你的 Android 应用仅仅由文本和几个按钮组成,事情会变得非常枯燥——也许除了一个纵横字谜游戏。图像和图片是 UI 设计的核心部分,许多应用不只是装饰性地使用它们,而是作为相册、图像编辑器等核心应用功能的一部分。
Android 有一对与您已经使用过的TextView和Button相对应的支持图像的部件——它们是ImageView和ImageButton小部件,同样,它们是从基础View类派生而来的。
与本章中的许多小部件一样,通常最好在蓝图/设计模式和附带的代码视图中使用布局编辑器,用 XML 定义您的ImageView或ImageButton,而不是在 Java 中费力地以编程方式定义它们。
ImageView和ImageButton都在android:src值中为它们的相关元素 XML 定义引入了一个额外的属性。这个属性的值是对您提供的图像资源的引用,无论它是一个.png文件、.jpg/.jpeg文件、.gif文件还是其他一些受支持的图像格式。在第四章中,我介绍了项目结构,包括res/drawable层级。在这个文件夹中,您可以放置图像文件以供参考,Gradle 将在这个文件夹中查找参考图像,以便在构建时捆绑到应用包中。
ImageButton与ImageView的不同之处在于支持类似按钮的行为,你已经在本章前面看到过常规的Button小部件。这意味着ImageButton小部件可以(也应该)定义onClick监听器,并构建后续逻辑来处理当ImageButton被点击时您希望您的应用采取的任何动作或行为,就像您对Button小部件所做的那样。
如果你还记得我们在第三章中对典型项目布局的概述,你可能会猜到指定android:src值的默认方法是引用你放在项目的res/drawable目录中的图形资源(和/或其特定密度的变体)。
清单 9-5 展示了一个 ImageView 的例子,它被配置为引用res/drawable文件夹中的一幅图像,在这个例子中,它是我在大英博物馆拍摄的罗塞塔石碑的照片。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/rosettastone"
android:contentDescription="The Rosetta Stone"
tools:layout_editor_absoluteX="83dp"
tools:layout_editor_absoluteY="144dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-5Using an ImageView widget for the ImageViewExample layout
运行中的应用如图 9-5 所示。
图 9-5
正在运行的 ImageView 示例应用显示了 ImageView 及其资源
您可以在Ch09/ImageViewExample文件夹中找到这个例子的代码。
使用编辑文本编辑和输入文本
到目前为止,在您的 Android 小部件之旅中,您已经看到了静态文本标签、可以触发活动的按钮以及显示图像的方式。几乎每个应用都需要用户的输入,而输入通常是文本。没有某种可编辑的表单或字段小部件,任何小部件集都是不完整的,Android 通过EditText小部件满足了这一需求。
EditText有一个类层次结构,可以看到它是从你已经知道的TextView类派生出来的,然后最终是View类。成为TextView的子类意味着EditText继承了很多你在TextView中看到的功能、方法和数据成员,比如textAppearance。EditText还引入了一系列新的属性和特性,让您可以精确地控制文本字段的外观和行为。这些新属性包括
-
android:singleLine:管理回车键的行为,决定是否应该在文本字段中创建新行,或者将焦点转移到活动布局中的下一个小部件
-
android:自动图文集:管理内置拼写纠正功能的使用
-
android:password:配置该字段,在输入字符时显示密码点
-
android:digits:限制输入,只接受数字,隐藏字母类型的字符
Android 还提供了一种更细致的——有些人会说是复杂的——方法来指定EditText的字段特征。可以使用inputType属性来捆绑EditText字段的所有期望属性。我们将在第十章中介绍inputType以及键盘和输入法的相关主题。清单 9-6 显示了inputType在其他选项中的作用。
清单 9-6 展示了android:inputType属性的介绍性用法,在这个例子中,标记用户文本的第一个单词的第一个字母应该自动大写。我们还将常规属性android:singleLine设置为 false,在EditText字段中启用多行文本。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/myfield"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:inputType="textCapSentences"
android:singleLine="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-6Configuring EditText field behavior with XML properties
清单 9-7 展示了如何从附带的 Java 包中以编程方式处理一个EditText字段。你可以在Ch09/EditTextExample中找到这个例子。
package org.beginningandroid.edittextexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText myfield=(EditText)findViewById(R.id.myfield);
myfield.setText("Our EditText widget");
}
}
Listing 9-7The EditText widget can be manipulated easily from your code
注意,我们在这个清单中引入了findViewById方法。您可以想象,如果您有任意数量的小部件,那么您需要一种编程方式来找到您想要使用哪一个作为您的程序逻辑的一部分。通过使用形式为R.id.named_id_from_XML_definition的资源 ID 引用,您可以通过小部件的 android:id 属性隐式地查找并链接到 XML 布局中定义的匹配小部件。所以在这种情况下,R.id.myfield找到并匹配你的EditText、@+id/myfield的android:id。
你可以在图 9-6 中看到来自EditText示例的结果。
图 9-6
EditText 小部件的运行,包括通过键盘编辑文本
还有其他一些典型的迹象表明这是一个可编辑的字段。内置的字典和拼写检查器是可用的——试着拼错一个单词,它会以红色下划线出现。当字段具有文本输入焦点时,闪烁的光标也很明显。您还可以通过选择称为AutoCompleteTextView变体的兄弟窗口小部件(再次从 TextView 和 View 派生)来帮助您的用户更快地键入,它将在用户键入时自动提示完整的建议单词。
Note
通过使用被称为TextInputLayout的布局,您可以在使用EditText小部件时变得更加复杂,这种布局包装并扩展了默认的EditText行为,具有文本提示、高亮显示、助手提示等特性。你可以在developer.android.com找到更多关于TextInputLayout的信息。
检查复选框
Android 包含的另一个经典 UI 小部件是CheckBox,它提供二进制开/关、是/否或选中/未选中小部件。CheckBox是 View(你猜对了)和TextView(可能会让你吃惊)的子类。这个类的祖先意味着你可以通过继承获得一系列有用的属性。CheckBox对象为您提供了一些 Java 助手方法,让您的复选框做一些有用的事情:
-
toggle():切换复选框的状态
-
setChecked():选中(设置)复选框,而不考虑当前状态
-
isChecked():检查复选框是否被选中的方法
在清单 9-8 中,我们有一个带有简单 Java 逻辑的示例复选框布局,您可以在Ch09/CheckboxExample项目中找到。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<CheckBox
android:id="@+id/check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="The checkbox is unchecked" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-8A layout featuring a CheckBox
虽然一个稍微有吸引力的CheckBox本身可能看起来不错,但它确实需要为你或你的用户做一些有用的事情。我们通过添加 Java 逻辑来配合我们的布局,从而释放了CheckBox的功能。清单 9-9 是演示如何将逻辑链接到复选框的 Java 包。
package org.beginningandroid.checkboxexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
public class MainActivity extends AppCompatActivity {
CheckBox myCheckbox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myCheckbox = (CheckBox)findViewById(R.id.check);
myCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.isChecked()) {
myCheckbox.setText("The checkbox is checked");
}
else
{
myCheckbox.setText("The checkbox is unchecked");
}
}
});
}
}
Listing 9-9Firing up a CheckBox with some programmatic logic
很明显,这里的CheckBox代码比之前的例子如EditText要多一些。如果您首先查看导入的类,您会对正在发生的事情有所了解。我已经导入了OnCheckedChangeListener,并提供了onCheckedChanged()回调方法的实现。这意味着我们已经将CheckBox设置为它自己的事件监听器,用于状态改变动作,比如被点击。
当用户现在点击CheckBox来切换它时,就会触发onCheckChanged()回调。我们的回调实现的逻辑在切换后测试了CheckBox的当前状态,并用新状态的书面描述更新了复选框的文本。这是一种很好的方式,既可以将小部件的所有行为捆绑在一个地方,又可以让我们在用户输入时在相同的逻辑流中进行表单验证,而不需要传递一包用户或数据状态。您的代码和相关的运行时输入检查优雅地并排在一起。
图 9-7 和 9-8 显示了我们的 CheckBoxExample 应用处于选中和未选中状态。
图 9-8
该复选框现已选中
图 9-7
该复选框未选中
用开关打开它
这个小部件是在 Android 开发的后期引入的,但是它的用途正如它的名字所暗示的那样。一个Switch就像一个二元开关,提供开/关模式的状态,用户可以通过用手指滑动或拖动来激活它,就像他们正在切换一个灯开关一样。用户也可以像点击Checkbox一样点击Switch部件来改变它的状态。在 Android 的几个版本中,Switch 小部件已经被调整以处理兼容性和其他问题,所以其他变体如“SwitchCompat”小部件有时被用来代替最初的Switch小部件,但总体目的和处理是相似的。
除了从View继承的属性之外,还有一个Switch小部件提供了android:text属性来显示与Switch状态相关的文本。文本由两个辅助方法控制,setTextOn()和setTextOff()。
对于Switch小部件,还可以使用其他方法,包括
-
setChecked():将当前开关状态变为开(类似复选框) -
getTextOn():返回开关打开时使用的文本 -
getTextOff():返回开关关闭时使用的文本
Ch09/SwitchExample项目提供了一个Switch的工作示例。清单 9-10 显示了一个简单的开关布局。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Switch
android:id="@+id/switchexample"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="The switch is off" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-10The layout for SwitchExample
Note
不要试图让你的android:id成为@+id/switch中的“开关”。Java 为其类似 case 的分支逻辑语句保留了单词switch,所以你将需要使用其他的东西,就像我在这个例子中一样。
配置开关行为的逻辑存在于我们的 Java 代码中,如清单 9-11 所示。
package org.beginningandroid.switchexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Switch;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
public class MainActivity extends AppCompatActivity {
Switch mySwitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mySwitch = (Switch) findViewById(R.id.switchexample);
mySwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.isChecked()) {
mySwitch.setText("The switch is on");
} else {
mySwitch.setText("The switch is off");
}
}
});
}
}
Listing 9-11Controlling switch behavior in code
如果代码结构和逻辑对您来说很熟悉,那就应该如此!从概念上讲,a Switch和 a CheckBox几乎是相同的,并且你对它们进行操作的逻辑几乎是可以互换的,至少在入门级上是这样。在图 9-9 和 9-10 中,你可以看到开关的动作,包括开和关。
图 9-10
开关打开,逻辑已触发更改其文本
图 9-9
处于关闭位置的开关部件
用单选按钮选择事物
结束我们对 Android 核心 UI 部件的详细观察后,是时候介绍一下RadioButton。就像在大多数其他小部件工具包中一样,Android 的RadioButton共享你已经体验过的CheckBox和Switch小部件的双态逻辑,并通过成为View和CompoundButton的子类获得许多相同的功能。正如您在前面的例子中看到的,您可以通过像toggle()和isChecked()这样的方法设置或测试状态,并通过样式属性控制文本大小、颜色等等。
Android RadioButton widget 通过添加更多的功能层来进一步扩展这些功能,允许将多个单选按钮分组到一个逻辑集合中,然后在任何时候只允许设置其中一个按钮。如果您近年来使用过任何其他 UI 工具包、网页或智能手机应用,这听起来应该很熟悉。为了对多个RadioButtons进行分组,在 XML 布局中,每个都被添加到一个称为RadioGroup的容器元素中。
就像其他小部件一样,您可以通过android:id属性给RadioGroup分配一个 ID,使用该引用作为起点可以利用整个RadioButtons组上可用的方法。这些方法包括
-
check():通过 ID 检查/设置特定的单选按钮,而不考虑其当前状态
-
clearCheck():清除 RadioGroup 中的所有单选按钮
-
getCheckedRadioButtonId():返回当前选中的 RadioButton 的 Id(如果没有选中 RadioButton,将返回-1)
清单 9-12 显示了带有单选按钮的单选按钮组的布局。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RadioGroup
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<RadioButton android:id="@+id/radio1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Red" />
<RadioButton android:id="@+id/radio2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Blue" />
<RadioButton android:id="@+id/radio3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Green" />
<RadioButton android:id="@+id/radio4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Yellow" />
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-12RadioButton and RadioGroup layout
你也可以在RadioGroup结构中散布和添加其他部件,这些部件将按照ConstraintLayout的约束规则或者我们将在下一章讨论的布局的其他放置规则在组内呈现。你可以在图 9-11 中看到行动中的基本RadioButtons。
图 9-11
RadioGroup 和 RadioButton 的作用
了解更多 UI 小部件
Android 总是有更多的小工具需要学习或掌握,你可以在网站上找到更多的例子,网址是 www.beginningandroid.org 。在这里,您可以找到更多的小部件示例,例如
-
滑块
-
模拟时钟
-
数字显示式时钟
我们还将在第 13 和 14 章中看到更多以音频和视频为中心的小部件。
摘要
在这一章中,你已经了解了许多基本的小部件——或者视图——在构建 Android 用户界面时是可用的。在下一章中,我们将进一步介绍用户界面设计的概念,并介绍布局作为一个容器和框架,用于在活动和其他用户界面中放置、组织和嵌套您的小部件。
十、探索 Android 概念:布局和更多
了解 Android 中可用的各种 UI 小部件无疑使您能够就应用中的特定功能和行为应该如何呈现做出各种设计选择,但是活动设计不仅仅是选择单选按钮或文本视图。布局是 Android 的声明性机制,让你能够控制应用的整个屏幕。
从概念上讲,布局既是您希望在应用或活动中使用的小部件的容器,也是所有小部件应该如何显示、交互和互补的蓝图和脚手架。一旦您构思出使用不止一个或两个小部件的设计,您就会希望布局的力量能够帮助您避免手动控制位置、缓冲区和空白、分组等等的繁琐。
在接下来的章节中,我们将回顾 Android 支持的一些最有用、最流行的布局类型,该网站还包含一些更专业或很少使用的布局的更多示例。
什么是 Android Jetpack
对 Android 设计的任何粗略搜索或回顾都会浮现出一些有趣的里程碑,有些人会说是 Android 近代史上的岔路口。你很可能会看到两个艺术术语从最近几年的任何结果中冒出来。首先,你会发现术语“材质设计”,这是谷歌在 2012 年向世界推出的一种风格和设计理念,它影响了默认调色板、小部件风格和 Android 中的其他设计功能。你会遇到的第二个术语是“Android Jetpack”,毫无疑问,你会发现关于它进行设计的评论——包括布局——“更容易”、“更好”、“新的和不同的”。这些说法是正确的,但它们可能会给新开发人员带来一些困惑。我在用喷气背包吗?我怎么才能得到喷气背包呢?诸如此类。
在 2018 年的谷歌 I/O 开发者大会上,谷歌推出了 Android Jetpack。在头条新闻的背后,事实证明 Jetpack 本身并没有什么“不同”,而是许多不同的 Android UI 框架片段和基础元素的重新打包,以及对 Android 支持库的修改和扩展。Jetpack 不是一种竞争性的建造方式。相反,Jetpack 会过滤你正在做的大部分事情,从引用androidx名称空间中的任何库,到管理向后兼容性和历史 Android 版本支持。这样,你并没有真正把 Jetpack“添加”到你的设计工作中。相反,当您利用您在构建应用和使用 Android Studio 来帮助您时,它会隐式地、几乎自动地介入。
谷歌在android.com网站上提供了一个在 Android Jetpack 支持下的快速浏览。Jetpack 旗下的 Android 现有部分分为四个领域:
-
基础组件
-
建筑构件
-
行为成分
-
UI 组件
谷歌提供了如图 10-1 所示的图表(来源:Android-developers . Google blog . com/2018/05/use-Android-Jetpack-to-accelerate-your . html)来演示 Android Jetpack 在这四个领域的具体部分。
图 10-1
了解 Android Jetpack 概念性组件 1
如果你从这一节学到一件事,那就是你不必担心问自己“我在使用 Jetpack 吗?”因为你已经是了!您可能没有使用所有的特性和功能组,但是没有必要这样做。
使用流行的布局设计
我们将在以下部分中介绍以下主要布局容器:
-
constraint layout:Android 中新项目的当前默认设置,也是 Jetpack 的一部分,其中小部件用最小的定位约束集表示,没有(或很少)树形层次结构。
-
RelativeLayout:在 Android 4.0 之后的许多年里一直是默认的,直到 Jetpack 和
ConstraintLayout的引入,它使用规则引导的方法来自我排列 UI 元素。 -
LinearLayout:最初的默认设置,在许多早期的 Android 应用中普遍使用,它遵循传统的盒子模型,将所有的小部件想象成盒子,以适合彼此。
在这本书的网站上, www.beginningandroid.org ,你还可以找到 Android 提供的其他布局的额外资料,包括
-
TableLayout:一种类似网格的方法,类似于在 web 开发中使用 HTML 表格。
-
GridLayout:看似与
TableLayout相似的是,GridLayout使用任意精度的网格线将你的显示器分割成一个个更具体的区域来放置小部件。
在这些主题之后,我们将转向如何从 Java 代码中操作 XML 布局,并着手修改 ButtonExample 应用的版本,以展示如何在代码中查找和使用布局。
Note
虽然我们将在这一章花很多时间来研究 XML 的布局,但是请记住,图形化的布局编辑器有一个设计模式,允许您可视化地添加小部件、放置和排列它们、设置关系等等。虽然您可能总是混合使用手写 XML 和布局编辑器,但是当您开始喜欢一种方法时,不时地跳到“另一种”是值得的。通过观察 Android Studio 在使用图形化布局编辑器时自动生成的 XML 中做了什么,这也是了解更多布局 XML 细微差别的好方法。
重新审视约束布局
在前面的章节中,你已经接触了非常简单的ConstraintLayout设计,包括你的MyFirstApp和第九章中的许多小部件示例。虽然你可以继续使用ConstraintLayout作为小部件的简单容器,为 Android 提供约束设置,以便它可以为你管理布局,但ConstraintLayout还有更多高级功能,你一定要了解和探索,以将你的活动设计提升到更高的水平。
ConstraintLayout提供了许多非常令人兴奋的特性,但我将标记出三个最近最有用的特性。
融入潮流
随着 Android 的最新更新,ConstraintLayout 已经扩展为一个可选功能,可以在活动中“流动”屏幕上的小部件,就像将它们倒在屏幕上一样,并在运行时根据给定设备界面的大小和密度包装和移动小部件。
用流的术语来说,你在一个虚拟流布局中将部件集合链接在一起,作为基础ConstraintLayout的助手。组的流链接在布局 XML 的<androidx.constraintlayout.helper.widget.Flow>元素中指定,然后两个关键属性控制流链接和行为。
第一个属性是app:constraint_referenced_ids属性,它采用逗号分隔的小部件 id 字符串,指示 Android 将界面的哪些部分分组到特定的虚拟流布局中。
第二个属性是app:flow_wrapMode,它采用三个字符串值中的一个来指示 Android 如何管理流组中的小部件流。app:flow_wrapMode的可能值和相关行为如下:
-
none:默认方法——这为组中的所有小部件创建了一个逻辑链,如果小部件不适合活动的维度,就会溢出它们。
-
链:如果发生溢出,将溢出的小部件添加到后续链中以包含它们。
-
align:大体上类似于链式方法,只是行与列对齐。
熟悉ConstraintLayouts中的流程选项非常容易。清单 10-1 显示了 Ch10/FlowExample 中FlowExample项目的相关布局 XML 文件。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flow TextView 1"
android:textSize="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flow TextView 2"
android:textSize="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toEndOf="@+id/text1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.499" />
<TextView
android:id="@+id/text3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flow TextView 3"
android:textSize="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toEndOf="@+id/text2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.499" />
<!-- Add/remove the androidx.constraintlayout.helper.widget.Flow spec to see Flow in action -->
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:flow_wrapMode="chain"
app:constraint_referenced_ids="text1, text2, text3"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 10-1Setting up Flow in your ConstraintLayout
FlowExample的布局非常简单。我们有三个TextViews,用约束条件定义,这样通常你会(试图)先用text1 TextView布局,然后用右侧的text2 TextView,再用右侧的text3 TextView``text2。我特意选择了长文本和大字体来表达我的观点。在布局的底部,您将看到在一个<androidx.constraintlayout.helper.widget.Flow>元素中定义的流,其中我指定我的flow_wrapMode来链接,并将组的引用 id 设置为"text1, text2, text3"–我的 TextView IDs。
如果我省略了流虚拟布局(参见 XML 布局中控制这一点的注释),Android 试图按照正常的ConstraintLayout规则呈现活动,结果如图 10-2 所示。
图 10-2
如果没有 Flow,ConstraintLayout 会在屏幕外呈现小部件
你马上就能发现问题。我故意使用虚拟屏幕尺寸较小的 AVD。我的text1 TextView渲染得很好,text2的一半也上了屏幕。但是text2的其余部分被切断了,text3也不见踪影。实际上,它被渲染,但不可见,因为布局没有办法适应屏幕大小,布局缺少流动选项。您可以尝试在各种不同屏幕大小的 avd 上运行移除了 Flow 元素的示例,以查看小部件是如何以及在哪里被截断或不显示的。
有了 Flow 元素之后,重新运行FlowExample应用就可以将 Flow 的功能展现出来。图 10-3 显示了同样的三个TextViews,但是这一次使用了流动虚拟布局功能,Android 已经能够遵循我指定的链规则,并且将小部件流动到新的行,以显示我的布局规范中的所有内容。
图 10-3
使用 Flow,ConstraintLayout 渲染所有
心流是一个超级容易掌握的新特性。您可以使用 FlowExample 示例代码,开始添加更多的小部件,扩展流引用的 id 集,甚至更改 flow_wrapMode,以查看流的行为。
用层分层
将调整小部件和视图的能力向前推进一步的是层。当涉及到界面设计时,名称层是非常重载的,所以非常清楚,一个层并不直接布局小部件或者帮助构建连续 UI 组件集的“栈”。一个ConstraintLayout可以用一个层来扩展,给你一个单一的方法,用一个机制来旋转、平移和缩放一组小部件和视图。
例如,您可能正在创建一个图形图块游戏,并希望降低用户当前未选择的任何图片图块的重要性。有了层,所有其他的ImageView小部件都可以通过将它们添加到一个层来应用相同的布局更改,然后所需的转换可以被应用到该层一次,它反过来将应用到它的组成小部件。
随运动而动
Android 最新版本的一个广为人知的特性是被称为 MotionLayout 的ConstraintLayout扩展。通过定义和使用ConstraintSets,你可以使用MotionLayout拍摄各种无聊的静态视图,并构建动画变化,如旋转、淡入淡出、大小变化等等。本质上,描述小部件和视图如何关联和定位的约束本身可以被视为控制运动和流动的变量。
手工构建基于MotionLayout的应用和它们使用的各种ConstraintSet配置可能非常繁琐。考虑到这种单调乏味,谷歌在 Android Studio 中引入了运动编辑器,为您提供了一个动画画布,用于构建引人注目的动画布局。
为了开始在动作编辑器中使用动作和MotionLayout设计,Android 为ConstraintLayout引入了androidx.constraintlayout.motion.widget.MotionLayout变体。MotionLayout 仍处于早期阶段,需要相当多的粗糙边缘和手动步骤来设置,并且随着 Android Studio 的单点发布而频繁更改。带有运动效果的布局也不太适合在像这样的静态书籍中呈现。
因此,为了确保您可以获得MotionLayout的最新演示,并且您可以在 MotionLayout 中看到选项如何在动态、移动的布局中发挥作用,可以从位于 www.beginningandroid.org 的网站上获得演示和MotionExample演示。
使用相对布局
RelativeLayout是 Android 的长期默认设置,现在仍然是活动和片段设计非常流行的选择。正如术语“Relative”所暗示的,一个RelativeLayout使用部件和父活动之间的关系来控制部件的布局。相对性的概念很容易理解,例如,您可以指定一个小部件放置在相对于相对位置的另一个小部件的下面,或者让它的上边缘与相关的小部件对齐,等等。
所有关系设置都利用一组分配给布局 XML 文件中的小部件 XML 定义的标准化属性。
相对于父容器放置小部件
理解RelativeLayout的一个很好的起点是探索允许您相对于父窗口定位小部件的属性。有一组核心属性可用于基于父对象(例如,活动)及其顶边、底边、边等来定位位置。这些属性包括
-
android:layout_alignParentTop:将小部件的上边缘与容器的顶部对齐。
-
Android:layout _ alignParentBottom:将小部件的下边缘与容器的底部对齐。
-
Android:layout _ alignParentStart:将小部件的开始端与容器的左侧对齐,在考虑从右到左和从左到右书写的脚本时使用。例如,在美国英语从左到右布局中,这将控制小部件的左侧。
-
android:layout_alignParentEnd:将小部件的末端与容器的左侧对齐,在考虑从右向左和从左向右书写的脚本时使用。
-
Android:layout _ center horizontal:将小部件水平放置在容器的中心。
-
android:layout_centerVertical:将小部件垂直放置在容器的中心。如果你想要水平和垂直居中,你可以使用组合的 layout_centerInParent。
在确定小部件边缘的最终位置时,会考虑各种其他属性,包括填充和边距宽度。如果您深入研究像素级精确的相对定位,请注意考虑到这些因素。
用 id 控制相对布局属性
为了RelativeLayout的目的正确引用小部件的关键是使用被引用的小部件的标识,这您已经遇到过:这是正在讨论的小部件的android:id标识符。例如,在前面章节的ButtonExample项目中,按钮有标识符@+id/button。
为了进一步控制小部件的布局并描述它们相对于布局中其他小部件的位置,您需要在布局容器中提供小部件的身份。这是通过在您想要引用的任何小部件上使用android:id标识符属性来完成的。
第一次引用android:id值时,确保使用加号修饰符(例如@+id/button)。对同一标识符(小部件)的任何进一步引用都可以省略加号。在对标识符的第一次引用中使用加号有助于 Android 林挺工具检测标识符不匹配,即您在布局文件中没有正确命名小部件。这相当于在使用变量之前声明变量。
有了 id,我们前面的例子@+id/button现在可以被另一个小部件引用,比如另一个按钮button2,通过在它自己的布局相关属性中引用 id 字符串的机制。
注意像button、button1和button2这样简单的名字对于这样的例子来说是不错的,但是你会非常希望在你的应用中使用有意义的小部件标识符和名字。
相对定位属性
现在您已经对标识符的机制有了很好的理解,您可以使用这六个属性来控制小部件之间的相对位置:
-
Android:layout _ over:用于将 UI 小部件放置在属性中引用的小部件之上
-
android:layout_below:用于在属性中引用的小部件下面放置一个 UI 小部件
-
android:layout_toStartOf:用于指示该小部件的结束边缘应该放置在属性中引用的小部件的开始边缘
-
android:layout_toEndOf:用于指示这个小部件的起始边缘应该放在属性中引用的小部件的结束边缘
-
android:layout_toLeftOf:用于将 UI 小部件放置在属性中引用的小部件的左侧
-
android:layout_toRightOf:用于将 UI 小部件放在属性中引用的小部件的右边
更微妙的是,您还可以使用许多其他属性中的一个来控制一个小部件相对于另一个小部件的对齐。这些属性包括
-
android:layout_alignStart:标记小部件的起始边缘应该与属性中引用的小部件的起始对齐
-
android:layout_alignEnd:标记小部件的结束边缘应该与属性中引用的小部件的结尾对齐
-
android:layout_alignBaseline:标记两个小部件的任何文本的基线,无论边框或填充如何,都应该对齐(参见下面的内容)
-
android:layout_alignTop:标记小部件的顶部应该与属性中引用的小部件的顶部对齐
-
android:layout_alignBottom:标记小部件的底部应该与属性中引用的小部件的底部对齐
使用相对布局示例
您已经对 RelativeLayout 的能力和行为有了足够的了解,可以深入研究一个工作示例了。清单 10-2 使用来自ch10/RelativeLayoutExample项目的 RelativeLayout 提供了布局 XML。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="URL:"
android:layout_alignBaseline="@+id/entry"
android:layout_alignParentLeft="true"/>
<EditText
android:id="@id/entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/label"
android:layout_alignParentTop="true"/>
<Button
android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/entry"
android:layout_alignRight="@id/entry"
android:text="OK" />
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/ok"
android:layout_alignTop="@id/ok"
android:text="Cancel" />
</RelativeLayout>
Listing 10-2The XML layout for the RelativeLayoutExample application
在这个RelativeLayoutExample布局中,我们基于您在之前章节中的学习,提供更丰富的理解。首先,您会看到根元素是<RelativeLayout>,这是该活动将使用RelativeLayout方法进行布局和放置的决定性因素。除了普通的样板 XML 名称空间属性之外,还引入了另外三个属性。
为了确保RelativeLayout跨越正在使用的任何尺寸屏幕上可用的整个宽度,使用了android:layout_width="match_parent"属性。我们可以对高度做同样的事情,但是我们也可以告诉RelativeLayout只使用包含所包含的小部件所需的垂直空间——因此使用了android:layout_height="wrap_content"属性。
依靠我们在第九章中介绍的小部件,我们为我们的活动引入了四个部件:一个TextView作为我们的标签,一个EditText作为我们的可编辑字段,以及按钮OK和Cancel。
对于TextView标签,布局指示 Android 使用android:layout_alignParentLeft="true"将其左边缘与父标签RelativeLayout的左边缘对齐。我们还想让TextView在引入相邻的EditText后自动管理它的基线,所以我们使用android:layout_alignBaseline="@+id/entry"调用像素推进完美。注意,我们引入了带加号的 id,因为我们还没有描述EditText,所以我们需要预先警告它即将存在。
对于EditText,我们希望它位于标签的右边,它本身位于布局的顶部,占据TextView右边的所有剩余空间。我们使用android:layout_toRightOf="@id/label"指示它向右布局(已经介绍过了,所以不需要添加加号)。我们使用android:layout_alignParentTop="true"迫使EditText在 RelativeLayout 的剩余空间中坐得尽可能高,并使用android:layout_width="match_parent"将画布上的剩余空间移到TextView的右侧。这是可行的,因为我们知道我们也要求父级使用最大可用剩余空间作为宽度。
最后,我们将两个按钮的位置与前面介绍的小部件联系起来。我们希望将OK放置在EditText的下方,并与其右侧对齐,因此给它赋予属性android:layout_below="@id/entry"和android:layout_alignRight="@id/entry"。然后我们告诉 Android 使用android:layout_toLeft="@id/ok"和android:layout_alignTop="@id/ok"将Cancel按钮放置在OK按钮的右边,按钮顶部对齐。
图 10-4 显示了在我们对一个普通的新的空活动项目做了这些布局更改之后,我们的布局的运行情况。
图 10-4
行动中的相对布局
相对布局中的重叠部件
Android 支持的每种布局都为你提供了专门和独特的功能,并且RelativeLayout配备了一些关键功能,包括让小部件相互重叠或看起来好像一个在另一个前面或重叠的能力。这是通过 Android 跟踪来自 XML 定义的子元素并为布局中的每个子元素应用一个层来实现的。这意味着,如果稍后在布局 XML 文件中定义的项目使用 UI 中的相同空间,它们将位于较旧/较早的项目之上。
没有什么比看到它的运行更能理解它是什么样子的,以及它对您的应用设计有何帮助。图 10-5 显示了一个有两个按钮声明的布局,第二个按钮位于第一个按钮的前面或上方。
图 10-5
RelativeLayout 在操作中的重叠特征
就您可能想要覆盖的部件类型和数量而言,天空是一个极限。您可能还想知道为什么有人会想以这种方式覆盖项目。除了针对一些古怪的布局想法,与RelativeLayout重叠的可能性意味着有可能在屏幕上有小部件但看不见,仍然有助于活动行为。
用线性布局排列
有时候在设计布局时,你不需要或不想要复杂的受约束的或相对的小部件定位,相反,你只想依靠一些古老的经过测试的布局方法。任何熟悉图形设计或界面设计历史的人都会听说过盒子模型,在这个模型中,组成界面的所有项目(或小部件)都被认为是分成行和列的元素。LinearLayout遵循这种模式,是早期 Android 版本使用的原始默认设置。它可能不再是当今的布局,但是你可以一直依赖LinearLayout作为一个简单的选项来调整你的小部件如何装箱、嵌套等等。LinearLayout缺少的是我们在 ConstraintLayout 和 RelativeLayout 中谈到的一些聪明、省时或有用的功能,但有时简单的解决方案才是真正需要的。
掌握 LinearLayout 的五个主要限定符
当使用 LinearLayout 时,一组五个关键属性帮助您控制整个布局和任何包含的小部件的放置和外观的几乎所有方面:
-
方向:对
LinearLayout的第一个也是最基本的控制是确定盒子模型是否应该被认为是水平、逐行或垂直、逐列填充的。这被称为LinearLayout的方向,由 android:orientation 属性控制,并接受字符串值HORIZONTAL或VERTICAL。也可以在运行时从 Java 代码中设置方向,使用类似参数的setOrientation()方法。 -
边距:默认情况下,你放置的任何小部件和相邻的小部件之间都没有缓冲区或间距。Margin 允许您控制这一点,并使用属性(如
android:layout_margin)添加缓冲区(或边距,顾名思义),这将影响小部件的所有边,或者使用单侧属性(如android:layout_marginTop)添加缓冲区。任何边距属性都以像素大小作为值,例如 10 度。这在概念上类似于RelativeLayout的填充,但只适用于小部件有不透明背景的时候(比如在按钮上)。 -
填充方法:我们已经为
RelativeLayout引入了wrap_content和match_parent的概念。在LinearLayout方法中,所有小部件都必须通过属性android:layout_width和android:layout_height指定填充方法。这些可以取三个值中的一个:wrap_content,按照您之前的理解,它指示小部件仅根据需要容纳文本或图像内容;match_parent,取父节点可用空间的最大值;或者以与设备无关的像素测量的特定像素值,例如 125 度倾斜。 -
权重:当一个
LinearLayout中的两个或两个以上的 widgets 都指定了match_parent时,哪一个胜出?他们怎样才能最大限度地利用父母提供的空间?答案是android:layout_weight属性,它允许您对每个小部件应用一个简单的数值,例如 1、2、3、4 等等。Android 将对LinearLayout中的所有权重求和,然后根据布局中所有小部件的总权重,为小部件提供 UI 空间。因此,例如,如果我们有两个都配置为match_parent的TextViews,第一个TextView的android:layout_weight为 5,第二个TextView为 10,那么第一个按钮将获得三分之一的可用空间(5/(5+10)),第二个按钮将获得三分之二的可用空间。还有其他一些设置权重的方法,但没有一种像android:layout_weight方法那样直观。 -
重力:当使用
LinearLayout时,Android 采用的默认布局方法是,如果使用HORIZONTAL方向,则从左侧对齐所有部件;如果使用VERTICAL方向,则从顶部对齐所有部件。有时,您可能希望覆盖此行为,以强制您的布局自下而上或向右倾斜。XML 属性android:layout_gravity允许您在运行时控制行为,方法setGravity()也是如此。android:layout_gravity的VERTICAL方向的可接受值为left、center_horizontal和right。对于HORIZONTAL方向,Android 将默认与你的小工具的文本的不可见底部对齐。使用值center_vertical让 Android 使用小部件的假想中心。
线性布局示例
首先回顾一下LinearLayouts的理论有点让人不知所措,因此开始探索一些概念有助于直观地掌握如何、何时以及在哪里使用关键的LinearLayout杠杆是有意义的。
从纯理论的角度来看,所有的选择都令人望而生畏。清单 10-3 中的动态例子显示了这些选项中有多少是有效的。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RadioGroup
android:id="@+id/orientation"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dip">
<RadioButton android:id="@+id/horizontal"
android:text="horizontal"/>
<RadioButton android:id="@+id/vertical"
android:text="vertical"/>
</RadioGroup>
<RadioGroup
android:id="@+id/gravity"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dip">
<RadioButton android:id="@+id/left"
android:text="left"/>
<RadioButton android:id="@+id/center"
android:text="center"/>
<RadioButton android:id="@+id/right"
android:text="right"/>
</RadioGroup>
</LinearLayout>
Listing 10-3Demonstrating the options provided by LinearLayout
这个例子使用了两个简单的构件来帮助我们理解方向和重力。我定义了两个独立的 RadioGroup 小部件,第一个小部件有一组RadioButtons来控制方向,另一个小部件有一组RadioButtons来控制重力。为了给我们一个起点,我们通过选择android:orientation="vertical"来使用垂直方向的 XML 定义,这将把RadioGroups一个放在另一个上面,并且将RadioButtons也以垂直方式放在其中。
然后我覆盖了初始RadioGroup中两个单选按钮的垂直堆叠,使用android:orientation="horizontal"将它们切换到水平布局。虽然我想从父RadioGroup继承其他属性,但我灵活地使用了覆盖子RadioButtons中特定属性的能力。最后,我们在 5 dip 的所有小部件周围设置填充,并设置wrap_content选项。
在没有支持 Java 逻辑的情况下运行这个示例代码——只有布局——显示了这些初始设置的运行情况,尽管还没有其他行为来改变您所看到的内容,但稍后会有其他行为。布局如图 10-6 所示。
图 10-6
设置方向和重力示例
很高兴看到呈现的布局,但实际上最好编写一些 Java 逻辑来根据单选按钮的设置改变方向和重心。这正是清单 10-4 显示的内容。
package org.beginningandroid.linearlayoutexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
public class MainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener {
RadioGroup orientation;
RadioGroup gravity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
orientation=(RadioGroup)findViewById(R.id.orientation);
orientation.setOnCheckedChangeListener(this);
gravity=(RadioGroup)findViewById(R.id.gravity);
gravity.setOnCheckedChangeListener(this);
}
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.horizontal:
orientation.setOrientation(LinearLayout.HORIZONTAL);
break;
case R.id.vertical:
orientation.setOrientation(LinearLayout.VERTICAL);
break;
case R.id.left:
gravity.setGravity(Gravity.LEFT);
break;
case R.id.center:
gravity.setGravity(Gravity.CENTER_HORIZONTAL);
break;
case R.id.right:
gravity.setGravity(Gravity.RIGHT);
break;
}
}
}
Listing 10-4Java logic to implement orientation and gravity changes based on UI selection
理解 Java 代码在做什么非常容易。首先,在应用的onCreate()调用过程中,我们使用我们在活动中实现的OnCheckedChangeListener的setOnCheckedChangedListener().为任一RadioGroup上的点击注册监听器,这意味着活动本身成为监听器。
当您单击任何一个RadioButtons时,监听器触发回调onCheckChanged()。在回调方法的定义中,我们确定了五个被渲染的图片中哪个RadioButton被点击了。一旦我们知道是哪一个RadioButton,我们的逻辑就调用setOrientation()从垂直布局切换到水平布局,或者调用setGravity()到相关的左、右或重心值。六种可能结果中的两种如图 10-7 和 10-8 所示。
图 10-8
另一个方向和重力的例子
图 10-7
向上、向下和周围的方向和重力
更多布局选项
Android 提供了一系列进一步的布局类型,其中许多随着 Android 的相继发布而受欢迎程度有升有降。以下是需要了解的主要布局,这些布局的更多示例可从网站 www.beginningandroid.org 获得。
表格布局
早在互联网的早期,网页设计者经常使用简陋的 HTML 表格在页面上放置内容。虽然网页设计已经向前迈进了一大步,但基于表格的布局概念仍然有用,Android 在TableLayout容器中采用了它们。
就像 HTML 一样,Android 中的TableLayout使用起来很快,并且依赖于这样一个概念,即TableLayout有TableRow子元素来控制大小、位置等等。与旧的网页 HTML 方法的另一个相似之处是高保真精度很难达到完美。
网格布局
如果TableLayout的想法让你的想象力飞速发展,那么你会爱上GridLayout。作为TableLayout的近亲,GridLayout将它的子部件和 UI 元素放在一个由无限细节线条组成的网格上,这些线条将你的活动所渲染的区域分隔成单元格。GridLayout's精确控制的秘密在于细胞的数量,或用来描述细胞的网格线,没有限制或阈值。只需添加更多的网格线,将您的 UI 分割成更精细的子单元,以放置小部件。
用 Java 逻辑掌握基于 XML 的布局:两全其美!
随着您对 XML 布局和 Java 管理的布局越来越熟悉,您最终会意识到您可以两全其美,并且您可以随时改变主意。您可能更喜欢用 XML 构建原型并使用图形布局编辑器,或者您可能更喜欢使用 Java 尝试真正细微的运行时布局选择。无论采用哪种方法,您都可以随时通过一点代码或调整后的 XML 来改变主意。举例来说,清单 10-5 列出了来自您的ButtonExample应用的计数按钮代码,转换成一个 XML 布局文件。您可以在Ch10/ButtonAgain示例项目中找到这段代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 10-5Flexing XML layouts and controlling them with Java logic
在清单 10-5 中,您应该可以看到我们为 ButtonExample 应用组合在一起的 XML 等价物,包括以下内容:
-
将 ConstraintLayout 设置为根元素
-
定义作为点击和计数器显示的按钮,并分配一个 android:id,这样我们就可以从代码中引用它,并在 Java 中执行我们的点击计数
-
Android:layout _ alignParentBottom、Android:layout _ alignParentTop、Android:layout _ alignParentLeft 和 Android:layout _ alignParentRight,每个都有助于按钮的“父”对齐
-
android:layout_width 和 android:layout_heigh,设置按钮消耗整个屏幕,就像我们对 ButtonExample 所做的那样
-
android:text,显示在按钮上的文本,最初是空字符串
虽然这是一个转换成 XML 的非常简单的例子,但是核心概念是重要的,并且它们保持不变。更复杂的例子会有多个用 XML 表示的小部件、子布局和分支来控制复杂性。由于我倾向于基于 XML 的布局,在本书的后半部分,您将会看到更多这样的内容。
用 Java 代码连接 XML 布局定义
当您采用 XML 布局方法并开始完善小部件定位等功能时,一个非常重要的问题出现了:即使我们的活动只有一种布局,如何从 Java 逻辑中决定使用哪种布局?该解决方案基于一个专门为此设计的方法调用,通常在活动的onCreate()回调中调用,以实现 Java 和 XML 的完美结合:方法setContentView()。
ButtonExample应用的 redux 在res/layout/activity_main.xml中的正常默认位置有它的布局,尽管这种技术不管活动 XML 文件的定制命名如何都能工作。要完成从代码到布局的连接,像这样调用setContentView():
setContentView(R.layout.activity_main);
如果你看一下ButtonExample的原始版本,你会注意到setContentView()也被调用过。那么,什么发生了变化?在ButtonAgain中,我们传递一个对我们定义的基于 XML 的视图的引用,这个视图基于 Android Studio 内置的 AAPT 实用程序,它已经解析了您的 XML 并为您生成了R Java 类。您从R类中受益,因为它能够对 XML 布局和其中的部分进行简单的代码引用。不管你有多少布局,也不管它们有多复杂,AAPT 都会把它们打包成一个综合 Java 类,并在R.layout命名空间中提供。使用调用约定R.layout.<your_layout_file_name_without_the_XML_extention>引用任何布局。
为了在由setContentView()返回的布局中找到小部件,您调用findViewById()方法并向其传递小部件的数字引用。再读一遍这句话,你会发现“抓住你了”我说的数字引用是什么意思?还没有 XML 声明是小部件的数字标识符或引用。这是一个容易解决的谜。
在打包时,AAPT 会识别你布局中的每个小部件,给它们分配一个 ID 号,并把它作为成员数据包含在R.java文件中。您可以随时打开R.java文件进行检查。但是不要试图用这种方式去记忆东西。你可以让 Android 使用R.id.<widget_android:id_value>参数来解析你想要引用的任何小部件的 id 号。您可以使用它来解析从基本视图类派生的任何小部件的 ID(这几乎是一切)。
随着机制的消失,你应该能够看到 AAPT 根据你的布局和setContentView()和findViewById()方法把东西整齐地打包成R.java的组合所带来的巨大力量。例如,不同的活动可以被传递不同的View实例,更有趣的是,您可以基于一些程序逻辑来改变View,例如,当您检测到不同类型的设备时,您可以使用不同的布局。
再次通过按钮访问 MyFirstApp
在最初的ButtonExample例子中,按钮的显示屏会显示按钮被按下的次数。当按钮通过onCreate()加载时,计数器从 1 开始计数。现有的大部分逻辑,比如计算点击次数的代码,在我们修改过的ButtonAgain版本中仍然有效。与ButtonExample代码的主要区别如清单 10-6 所示,其中我们用ButtonAgain app XML ConstraintLayout布局中的定义替换了活动的onCreate()方法中之前的 Java 调用。
package org.beginningandroid.buttonagain;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.app.Activity;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button myButton;
Integer myInt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myButton=(Button)findViewById(R.id.button);
myButton.setOnClickListener(this);
myInt = 0;
updateClickCounter();
}
public void onClick(View view) {
updateClickCounter();
}
private void updateClickCounter() {
myInt++;
myButton.setText(myInt.toString());
}
}
Listing 10-6ButtonAgain Java code, linking seamlessly to the layout XML
通过回顾onCreate()方法,您现在可以清楚地看到变化。首先,我们使用setContentView()为我们指定的 XML 布局自动生成R Java 类。然后我们使用findViewById()方法,要求它找到具有android:id值为“button”的小部件。我们将返回以编程方式驱动按钮行为所需的引用,包括更改其标签以反映我们计算的点击计数器值。
将所有的代码和 XML 放在一起会产生一个ButtonAgain应用,其外观和行为与 ButtonExample 应用惊人地相似,如图 10-9 所示。
图 10-9
ButtonAgain 应用,完美地结合了 Java 逻辑和 XML 布局
摘要
浏览了布局和它们的一些特性之后,你现在应该可以想象一下从哪里开始使用本章中的一些容器和布局样式了。还可以玩“猜猜你喜欢的 app 用的是什么布局风格”的游戏。这并不是我们布局之旅的结束,我们将在本书的剩余部分重新审视更多的概念。
Footnotes 1本页部分内容转载自 Android 开源项目创建和共享的作品,并根据知识共享 2.5 归属许可中描述的条款使用。来源: https://android-developers.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html
十一、理解活动
到目前为止,您对 activities 的介绍主要集中在使用它们作为学习 UI 小部件和布局的工具,知道它们在计算上是“廉价的”,是 Android UIs 的基本构建块,并且设计为您可以在您的应用中尽可能多地制作和使用,完全知道 Android 操作系统会愉快地回收资源并保持您的活动可管理。
理论上说得很好,但实践呢?在这一章中,我们将深入研究 Android“如何”在活动生命周期中管理活动,尝试活动生命的各个阶段,然后通过介绍活动的犯罪伙伴 fragments 来扩展您构建引人注目的用户界面的基线。您可以将片段视为一种合成技术,用于决定何时以及如何为更大的屏幕或维度极端的屏幕利用活动和活动组件的不同组合。
深入研究 Android 活动生命周期
到目前为止,本书中的所有例子都使用了一个 activity,尽管您已经阅读了多次,因为您的应用可以有任意多个 activity。不管您有多少个活动,每个活动的使用都受生命周期的控制,在生命周期中选择要运行的活动;被创建、使用、暂停和/或恢复;并最终被停止和处理。用简单的英语描述生命周期的各个阶段有助于你理解正在发生的事情,让我们看看生命周期状态的实际技术方面和 Android 用来触发状态转换的回调方法。图 11-1 展示了这些生命周期状态和伴随的回调方法的全貌。
图 11-1
使用回调转换方法的 Android 活动生命周期
应用通常以四种主要状态之一存在:
-
已启动:某个动作(通常是用户触发的动作)指示 Android 操作系统运行该应用时,该应用的初始状态。
-
运行中:用户第一次看到你的应用并与之交互的时刻(和正在进行的状态)。一般来说,这是在发射后已经进行了一系列准备步骤之后。
-
killed:Android 操作系统收到不再需要该应用的通知后,该应用进入的状态,原因可能是用户关闭了该应用,或者是发生了一些资源引发的收割。
-
关闭(以及任何不处于启动、运行或终止状态的应用):从操作系统内存中清除所有持久数据、视图层次结构、缓存数据等的最终状态。
这些状态中的每一个都是不言自明的。在生命周期回调方法的领域中,让应用在不同状态之间移动的是乐趣所在。
了解活动生命周期回调方法
图 11-1 中显示的每一种生命周期方法都有你应该熟悉的特定行为和用途。您可能不需要突然开始为您的应用的这些生命周期阶段添加定制逻辑,但是知道在哪里添加东西将为您在未来作为 Android 开发者的快速进步做好准备。我们将逐一介绍每种方法,突出它们的具体用途。
每个方法都有与其他方法相同的特征,例如当调用任何回调方法时,几乎普遍使用各自父类的等效方法作为第一个动作。例如,在本书前面的例子中可以看到,onCreate()方法的第一步是调用super.onCreate(),它调用父版本。当您不想自动调用父类时,会有例外,但是我会在遇到这些情况时标记它们。现在,假设一个默认的良好行为是遵循“家长呼叫”实践。
onCreate()
每个活动都以onCreate()方法开始生命。无论您的用户点击了 Android 主屏幕上的图标来启动您的应用,还是配置更改触发了重新创建显示的需要,都会调用onCreate()。该方法采用一个 Bundle 对象来考虑这些后来的重建情况,因为 Bundle 将存储任何以前的状态、数据和资源,这些都是使活动变得生动所需要的——统称为实例状态。
对于您活动的任何onCreate()方法,您都应该考虑以下行动:
-
将您计划使用的所有布局加载到内容视图中,以便在调用 onStart()方法时,Android 可以在显示 UI 之前创建它。
-
初始化您在活动类定义中定义的任何活动级变量。
您可能还需要做一些工作来使用您作为应用的一部分创建的全局资源或变量。当我们讨论onRetainNonConfigurationInstance()时,我们将很快回到全局资源这个主题,并且当我们在本章的后面和本书的剩余部分讨论首选项时,我们也将探索更多的示例使用。
onStart( )
一旦onCreate()完成了从布局定义构建所有对象的任务,以及任何其他初始化工作,那么onStart()的工作就开始了。onStart()的工作是在屏幕上向用户呈现最终的用户界面。在前面的生命周期模型中,onRestart()之后的路径上也调用了onStart()方法。
在很多情况下,您需要覆盖onStart()并提供自己的逻辑。即使您很想这么做,您也需要意识到,如果您的活动遵循onRestart()路径,实例状态在这一点上仍然没有完全恢复,直到对onRestoreInstanceState()的后续调用完成。
实现您自己的onStart()覆盖非常有用的一种情况是,当您有一些长期存在的自定义资源时,您已经根据您的活动完全冻结或暂停了这些资源——做一个好公民,在不必要时不使用资源。如果您选择建立这样的资源管理,那么onStart()方法将是您恢复或解冻这样的定制资源的地方。确保调用super.onStart()方法来调用父类的等价方法,即使你知道父类中没有覆盖代码。Android 本身在这种情况下还是需要做好自己的内功。
onRestoreInstanceState()
如果用户结束一个活动,无论是通过使用 back 按钮,还是通过其他路径将他们从您的活动转移到另一个活动,他们都表明他们已经完成了原始的活动以及与它相关的所有状态。这将向作为应用开发人员的您表明,除了您需要维护的任何长期资源之外,您还可以免除该活动。当 Android 本身终止一个活动时,情况通常是不同的。首先,Android 由于配置的改变而终止了一个活动,并且它确实需要关心需要什么来重新创建活动以满足用户的需求。这意味着不仅要重新创建活动的可视显示,还要重新创建它的所有实例状态。
您可能希望在这里执行任何自定义的实例状态恢复,并记住实例状态与长期存储不同。还有其他机制来处理这个问题,包括我们将在本书后面讨论的偏好。如果您希望持久存储比首选项系统所设计的更复杂或更大类型的数据,那么基本的计算概念(如文件和数据库)是最好的方法,我们将在第十八章中介绍这些内容。
onResume( )
对于 Android 用户界面来说,onResume()方法是关键时刻。onResume()方法是使完全渲染和膨胀的布局可见的关键方法,将所有累积的对象、逻辑等转移到前台。与onPause()一起,当其他活动和应用抢占它、跳转到焦点、被用户关闭等等时,到前台和从前台的转换将在您的活动生命周期中触发多次。
您通常应该考虑添加到onResume()的唯一逻辑与改变您活动的实时视觉方面相关,包括
-
动画
-
录像
-
自定义视图过渡效果
在所有其他方面,您应该避免在onResume()中使用任何其他定制逻辑。
onpause()
Android 从一开始就被设计为支持同时运行的许多应用,并依赖于应用能够简单方便地暂停其活动,以便为用户决定使用的其他活动释放资源。为了支持这一点,应用将频繁地从前台状态转移到后台状态。在此之前,将调用onPause()方法。
在onPause()调用期间要考虑的最重要的任务包括保存尚未放入活动包中的任何状态,以及处理任何自定义动画、视频、音频或逻辑的其他实时方面。此外,onPause()调用还充当了一个阈值,超过这个阈值,Android 可以单方面终止您的活动并收回其资源。这意味着一旦onPause()调用退出,就不能保证您的流程和活动会再次接收到事件——所以保存您需要的东西,不要假设您的活动状态在没有您干预的情况下会持续下去。
onStop()
如果onStart()管理移动到前台的活动,你可以把onStop()想成相反的角色:负责把你的活动转移到后台。在onStop()完成之后,您的活动不再可见,尽管相关的视图层次结构和属性保持不变。
在 Android 积极的资源管理方法下,你应该永远记住onStop()可能永远不会为你的活动而被调用,所以在这里依赖自定义逻辑是不明智的。您可能会考虑覆盖默认设置的一个领域是与 Android 服务的任何交互,在这种情况下通常不需要前台交互。
onSaveInstanceState()
方法的名称和它在图 11-1 中的生命周期图中的位置都很好地说明了它的用途。这个方法的作用是为任何将要被onDestroy()方法销毁的活动保存活动状态,从而避免用户发现重要的状态已经丢失而没有意识到他们的活动可能已经结束并被重新创建的情况。这在任何配置更改事件下都会频繁发生,因此用户的体验需要使其透明和自动化。
Note
现在有一种趋势,开始支持ViewModel的概念,并使用该技术来保存任何瞬态。但是,仍然完全支持onSaveInstanceState()方法。你可以在developer.android.com了解更多关于 ViewModel 的信息。
onRestart( )
每次活动从停止状态转移到开始状态时,都会调用onRestart()方法。该方法为您提供了灵活性,使您能够以不同于那些已停止且现在已重新启动的活动的方式来处理新启动的活动版本,例如在重新启动的情况下保留视图层次结构,您可以利用该层次结构来加速某些初始化活动。
onRetainNonConfigurationInstance()
对于一个方法来说,这是一个很长很奇怪的名字,仔细观察生命周期图会发现它也不存在!这是因为onRetainNonConfigurationInstance()与活动的生命周期没有严格的联系。相反,它的目的是提供回调机制,以便在 Android 系统经历配置更改时调用。
“什么是配置变更?”你可能会问。有几个突出的动作,Android 认为整个设备的配置发生了变化,所有运行的应用都必须得到通知,并执行任何必要的步骤来适应这种变化。这些配置更改操作包括
-
将设备从横向旋转到纵向,反之亦然
-
将设备连接到 USB 电源
-
将设备停靠在底座中
-
添加或删除 SD 风格的存储设备
-
更改 Android 操作系统的输入或显示语言
您的活动可能需要处理部分或全部这些事件。最明显的例子是需要根据设备的新方向重新绘制(或者严格地说,重新创建)布局。在这些情况下,Android 将保留活动前一个实例的所有资源,调用getLastNonConfiguationInstance() companion 方法将返回对这些资源的引用,使您能够执行您可能需要的任何进一步的更改处理。
随着无头片段的出现,这种方法在当代应用开发中不太常见。我们将在下一章 12 中讲述片段。
onDestroy()
我们已经到达了生命周期方法的终点,确切地说是onDestroy()方法,它本身就是关于结束活动的。我们知道,活动是廉价而丰富的,在构建应用时,只要需要,就应该使用然后丢弃。您可以考虑添加到onDestroy()覆盖中的核心逻辑是任何以活动为中心的清理。因为不能保证onDestroy()会被你的活动调用,所以你不应该依赖它,也不要期望与其他资源或服务交互。
理解活动生命周期的目标
对于新开发人员来说,掌握回调的整体概念对于更广泛地掌握消息和基于事件的开发至关重要。然而,如果你只把你的 Android 应用看作是在特定状态下的活动,等待回调信号时跳转,你就错过了一些更大的想法。
总的来说,廉价、易操作的活动和回调方法的简洁打包(这意味着每个活动都有自己的从“这里”到“那里”的说明)的最大目标是向用户呈现一个无缝的应用,它可以响应用户的输入和需求。这种以用户为中心的视图对于提供一致的体验是至关重要的,因为你的用户会倾向于把你的应用看作是屏幕的集合或层次结构,而不是那些容易创建和经常破坏的活动的松散联系。
当您开始用越来越多的活动编写应用时,记住以下原则会对您有好处:
-
活动要有重点。你创建的每个活动都应该有助于服务于一个单一的(但不一定是简单的)目的或结果,而不是一个不相关的选项、行动和结果的总称。
-
**保存状态,然后再次保存。**用户是不可预测的,他们在使用你的应用时会做最糟糕的事情——从关闭手机到反复切换你的应用等等。为了让您的用户相信您的应用可以直观地处理任何意外情况,经常保存状态是很重要的。把它想象成一个既便宜又有效的天然安全网。
-
给每个视图一个 ID 。每个小部件、每个视图和每个视图组都应该用 android:id 属性进行注释。这有助于您自助,因为当每个视图都有一个 ID 时,保存实例状态变得非常容易。
-
应用和活动可以消失。有些情况超出了您的控制范围,特别是无论您的应用多么高效和轻量级,您的用户都决定将它与其他占用大量内存和资源的应用一起安装。如果用户在这样的环境下暂停你的应用,Android 操作系统可能会关闭你正在运行的应用,以释放急需的内存。
管理活动配置更改
通过本章前面的描述,我强调了正常的活动生命周期至少有一个例外,即活动配置变更。配置更改通常被定义为影响整个应用、Android 用户空间或整个 Android 设备的事件。这可能会让你认为我在谈论地震事件——也许是一场 Android 地震。现实要稍微平凡一些。归类为配置更改的事件种类包括更改设备的界面语言、将设备插入电源、与另一个设备配对,或者只是将设备从纵向旋转到横向,反之亦然。
使用配置更改的默认方法
首先,好消息是:Android 为你跟踪设备上发生的所有配置变化,通过使用通知回调来通知所有正在运行的应用特定的变化。除了这个基本通知,Android 还会尽最大努力避免让您承担过多的工作来确定配置更改应该如何在您的应用中体现出来。Android 通过利用程序中的现有结构来做到这一点。
Android 为您采取的最有价值的行动之一是监控已经发生的任何配置更改,并参考您为应用创建的所有可用布局,并执行所有需要的布局工作,以使用与配置更改结果匹配的布局来重新创建您的活动,无论是将旋转匹配到相关的纵向/横向布局,还是将语言从左到右改为从右到左,等等。
布局行为和娱乐已经为您完成,剩下的只需要照顾您为给定活动创建或获得的任何资源。作为配置更改的一部分,您需要保存和恢复这些活动,因为 Android 会破坏之前的活动并重新创建它。您的活动有机会干净地保存其当前状态和资源,以便在重新创建时,您的新活动可以恢复它们,将活动状态恢复到破坏前的状态。以下回调涵盖了允许您跨配置更改保存和恢复状态和资源的流程:
-
onSaveInstanceState():当触发任何配置更改时,在 Android 销毁当前活动之前,会立即调用onSaveInstanceState()。这是保存任何临时或正在使用的资源或您需要保留的数据的时间。创建并使用一个捆绑对象来保存您希望保留的所有项目,以便在后续的活动重新创建中使用。如果你回头看看整本书已经展示过的例子,你可以看到在onCreate()时间使用 Bundle 对象的地方(稍后会有更多)。onSaveInstanceState()超类祖先还在幕后为您执行一系列非常有用的工作,包括保存任何对象的视图状态——小部件、UI 元素等等——这些对象已经用一个 ID 进行了声明,可以在需要时重新创建。 -
当一个活动开始活动的时候,它或者被重新创建,或者在我们当前的讨论中,它被传递给所需的 Bundle 对象,以便保存的资源和其他数据可以被恢复。除了确保调用父类
super.onCreate()方法,您不需要做任何更复杂的事情。您可以随时选择添加自定义状态并在onSaveInstanceState()期间保存它——然后您需要将您的自定义恢复逻辑也提供给onCreate(),或者提供给在(重新)创建活动时调用的下一个方法onRestoreInstanceState()。 -
您的 Bundle 对象也被传递给这个方法,此时您可以有选择地检索和恢复您想要的任何进一步的资源。等到活动生命周期中的这一点的一个好处是,您可以确保活动的布局已经展开,内容视图已经设置,这样活动的所有可视方面都已就位。
摘要
现在,您对活动生命周期和回调方法的重要性有了一个坚实的基础,您可以在需要时覆盖这些方法。当我们介绍片段时,我们将在第十二章继续讨论生命周期,然后将展示一个合并的示例应用,展示在现实生活中如何以及在哪里使用生命周期回调方法覆盖。
十二、片段简介
到目前为止,在我们的 Android 布局基础之旅中,我们已经介绍了基本的基于视图的 UI 小部件,并介绍了活动及其生命周期。多年来,这就是在 Android 世界中创建应用的全部内容——设计您的活动,为您的应用添加您想要的逻辑,并在用户浏览您的应用的功能时创建和处理由此产生的膨胀(或呈现)的活动。
2011 年左右,事情爆炸了。或者,更具体地说,随着平板电脑及其更大显示屏的出现和普及,屏幕尺寸出现了爆炸式增长。快进几年,我们已经有了汽车上的平板显示器、巨型电视屏幕显示器等等,所有这些都是由 Android 驱动的。最大限度地利用突然出现的屏幕空间,促使谷歌推出了 Android UI 世界自诞生以来最大的变化之一:片段。
碎片解决了很多问题。从确保没有大面积的空间浪费到克服看起来很糟糕的暴力缩放技巧,片段为您提供了从多组相关的小部件组合 ui 的机制,然后在活动中灵活地显示片段,从而能够在一个屏幕上显示更多的内容,或提供更多的功能。
从片段类开始
随着大屏幕的出现和通常会看到更多设备旋转的用户体验(例如,在纵向模式下阅读,然后在横向模式下观看电影)的出现,开发人员必须在创建更多活动或更好地重用已为应用构建的活动集合之间做出选择。片段偏向于后一种重用范式,并在活动和呈现它们的布局容器以及用于功能的 UI 小部件之间引入了一个中间层。这有助于降低复杂性,同时处理屏幕尺寸几乎无休止的增长。使用应用的某些方面会发生变化,尤其是活动的生命周期。接下来我们将详细讨论这一点。
Fragments And Backward Compatibility
早在 Android 3.0 版本中就引入了片段(称为蜂巢)。通过 Jetpack 或旧的 Android 兼容性库,您甚至可以依赖旧版本的良好支持。
为您的应用使用基于片段的设计
在深入以片段为中心的设计之前,您应该知道使用片段是完全可选的。如果你现在喜欢设计大量活动的想法,没有必要放弃这种方法。但是如果你更喜欢利用片段提供的好处,请继续阅读。
要在设计中考虑片段,请考虑您现有的布局以及各种小部件(如 TextView、Button、RadioGroup 和其他视图)放置的位置。只要有概念上相关并放在一起的小部件子集——比如 TextView 充当 EditText 旁边的标签——就可以认为该子集已经成熟,可以包装在片段中了。为了直观地展示这一点,图 12-1 显示了这种分组,在小部件和整体活动之间有一个片段中间层。
图 12-1
片段作为小部件和活动之间的中间组
使用这个模型,您可以看到片段分组是如何移动的,并且可能会在更大的屏幕上以不同的方向显示更多的片段及其包含的小部件。您仍然能够在更小的手机屏幕上显示完美的 UI,而不必在尺度的两端做出妥协。
为了实现这一壮举,片段首先使用<fragment>元素块将 XML 添加到布局中。在接下来的示例中,您将会看到这一点。您的布局定义的其余部分基本保持不变,这意味着到目前为止您所学的所有内容在片段设置中仍然是 100%可用的。这使您可以继续使用视图的层次结构,并使这些视图膨胀,以创建用户与之交互的结果屏幕,就像以前一样。
使用片段还引入了一种使用 Bundle 对象的额外情况,提供初始化、状态保存和重新创建,其方式与前面在第十一章中讨论的活动非常相似。片段还有其他值得了解的特性,包括:
-
您可以子类化基本片段类并添加您自己的自定义逻辑,但是您必须为派生类提供一个构造函数。
-
当使用片段时,Android 会创建一个片段管理器来处理你的片段之间的双向交互——你不需要为此编码。
需要注意的另一个主要变化是整体活动和片段生命周期是如何变化的。
使用片段生命周期
在片段生命周期和活动生命周期之间存在许多共享的行为和概念,我在前面的第十一章中介绍过。与活动生命周期一样,片段生命周期的可视化图表有助于概念化状态和转换。完整的片段生命周期如图 12-2 所示。
图 12-2
片段生命周期
原始活动生命周期和片段生命周期之间的主要区别与宿主活动和组成片段的交互有关。与父活动的单个状态转换相比,片段可能会增加复杂性和多个事件转换。
回顾片段生命周期回调方法
许多片段生命周期回调方法与您在活动中看到的方法同名,但是您应该注意这并不意味着它们做完全相同的事情。下面的列表展示了主要的区别,以及只有片段的回调方法。
onInflate()
调用onInflate()方法是为了使用<fragment>元素将布局 XML 文件中定义的片段布局扩展到屏幕 UI 中。如果您通过newInstance()调用在代码中以编程方式显式创建新的片段,您也可以直接调用onInflate(。传递给它的参数包括片段将存在于其中的引用活动和一个 AttributeSet,以及来自<fragment>标记的任何附加 XML 属性。在这个阶段,Android 正在决定你的片段在渲染时的样子,尽管它目前不会显示它。该步骤发生在onAttach()回调期间。
onCreate()
片段的onCreate()类似于活动的onCreate(),有一些小的调整。主要的变化是您不能依赖任何活动视图层次结构来引用onCreate()调用。仔细想想,这是有意义的,因为与片段相关联的活动正在经历它自己的生命周期,而就在您认为可以开始依赖它的时候,它可能会停止存在,或者经历一个配置更改或另一个导致视图层次结构被破坏或重新创建的事件。
onattach()
在 Android 确定片段被附加到哪个活动之后,回调立即发生。在这一点上,您可以安全地处理活动关系,比如为其他操作获取和使用上下文。任何片段都有一个继承的方法getActivity(),该方法将返回它所附加的活动。您的片段也可以使用getArguments()方法来获取和处理任何初始化参数。
onCreateView()
onCreateView()回调为您提供了为片段返回所选视图层次的机制。它接受一个LayoutInflater对象、ViewGroup和实例状态的Bundle,然后依靠 Android 根据所有常见的屏幕大小和密度属性选择一个合适的布局,用。inflate()的LayoutInflater方法,以你认为需要的任何方式修改布局,然后将结果视图对象交回进行渲染。
onViewCreated()
在onCreateView()返回后,立即触发onViewCreated()。在使用任何保存的状态修改视图之前,它可以执行进一步的后处理工作。
onViewStateRestored()
在片段的视图层次结构的所有状态都恢复的情况下,调用onViewStateRestored()方法。这对于区分新的创建和配置更改后的恢复等情况非常方便。
onStart( )
片段的onStart()回调直接与父活动的对等onStart()相关联,并在片段显示在用户 UI 中后立即被调用。您想放在活动级onStart()回调中的任何逻辑都应该放在相关的片段onStart()方法中。
onResume( )
片段onResume()方法也紧密地映射到等价的活动onResume()方法。这是用户完全控制活动及其片段之前的最后一次调用。
onpause()
onPause()回调也与整体活动的onPause()方法紧密匹配。如果您将逻辑移动到片段中,那么 activity variant 中关于暂停音频或视频、暂停或释放其他动作和资源等的规则在这里都适用。
onSaveInstanceState()
onSaveInstanceState()的片段版本与 activity equivalent 相同。您应该使用onSaveInstanceState()通过片段的Bundle对象来持久化您想要在片段实例之间保留的任何资源或数据。不要过分保存大量的数据和状态——记住,你可以在片段本身之外使用长寿命对象的标识符引用,只需要引用和保存那些。
onStop()
onStop()方法相当于 activity 的相同方法。
onDestroyView()
onDestroyView()在片段移动到生命结束阶段时被调用。当 Android 已经分离了与片段相关联的视图层次时,onDestroyView()被触发。
onDestroy()
一旦片段不再被使用,就调用onDestroy()方法。此时,该片段仍然与它的活动相关联,尽管它很快就会被送到废料堆!
底部( )
终止一个片段的最后一步是从它的父活动中分离出来。这也标志着应该销毁、移除或释放所有其他资源、引用和其他残留标识符的时刻。
从简单的片段生命周期事件开始
前面的一组生命周期阶段和相关的片段回调方法可能会让您晕头转向,您可能会担心为了使用和受益于片段,您必须立即为所有这些方法编码。你不用担心!正如对活动生命周期的介绍一样,您不必用自己的逻辑覆盖所有的存根方法。只有在特定的状态转换中,您需要或想要做一些事情时,您才需要提供支持代码。你可以从为onCreateView()方法提供一个覆盖开始,然后退出。
在下面的ColorFragmentsExample代码中,我就是这么做的,将回调逻辑保持在绝对最小。
创建基于片段的应用
是时候看看片段的作用了!我将使用一组简单的颜色主题小部件和活动来说明开始使用片段的容易程度。
创建片段布局:颜色列表
清单 12-1 显示了基于片段的布局,它将在父活动中用于显示颜色列表。
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_list"
android:name="org.beginningandroid.colorfragmentsexample.ColorListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
tools:context=".ColorListActivity"
tools:layout="@android:layout/list_content" />
Listing 12-1Fragment layout for displaying a list of colors
这个定义包含了<fragment> XML 元素,并依赖于股票列表内容布局来显示列表中的TextView条目。我们将把这个片段用于所有不同的可能显示尺寸和方向,无论是电话屏幕上的单窗格视图还是大屏幕上的多窗格视图布局。
创建片段布局:颜色细节
我将使用一个TextView小部件来显示颜色的细节,然后将它放在片段中,该片段将放在父活动中。在Ch12/ColorFragmentExample项目的 fragment_color_detail.xml 文件中可以找到TextView的简单布局。清单 12-2 显示了内容。
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_detail"
style="?android:attr/textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:textIsSelectable="true"
tools:context=".ColorDetailFragment" />
Listing 12-2The TextView layout used to show color details
颜色详细信息的单窗格父活动
当在小屏幕设备上运行时,我们会将TextView布置在一个适合大小的活动中。该活动的唯一任务是创建包含TextView的片段,您可以在清单 12-3 中看到这段代码。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ColorDetailActivity"
tools:ignore="MergeRootFrame" />
Listing 12-3The activity_color_detail.xml layout
这是一个简单的<FrameLayout>带有一些基本的华丽。TextView将通过一个片段放置在这个布局中。
颜色详细信息的双窗格父活动
当我们移动到一个更大的屏幕上时,一个更合适的布局会将所有的片段和 UI 小部件同时放在屏幕上,最大化地利用空间。
这个activity_color_twopane.xml布局文件可能会让你觉得有更多的工作要做,但是仔细观察,你会发现它实际上只是一个包含了<fragment>和<FrameLayout>的组合,我们把它们放到了小屏幕的单独布局中。清单 12-4 展示了这个 XML。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:baselineAligned="false"
android:divider="?android:attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".ColorListActivity">
<fragment android:id="@+id/color_list"
android:name="com.artifexdigital.android.colorfragmentsexample.ColorListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
tools:layout="@android:layout/list_content" />
<FrameLayout android:id="@+id/color_detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
Listing 12-4The activity_color_twopane.xml layout
与连接在一起的独立布局相比,唯一的区别是 android:layout_weight 值,它将用于管理两个片段在单个活动中一起呈现时所使用的相对屏幕空间。通过选择 1:3 的比例,这将给主列表片段四分之一的空间,给细节片段剩下的四分之三的空间。
选择要膨胀的布局
你的 Android 应用如何决定使用哪种布局,显示什么样的片段排列?答案在于在项目的res/资源文件夹层次结构中使用多个 refs.xml 文件。在我们的例子中,我们在每个res/values-large和res/values-sw600dp文件夹中都有一个 refs.xml 文件。
当我们的代码运行时,Android 将在运行时检查所有不同大小特定的res/目录中的任何大小特定的 XML 资源(可以有两个以上,正如您在本书前面对 Android 项目结构的探索中看到的)。对于大尺寸和 sw600dp 尺寸的屏幕,refs.xml 中只有一个子元素,如下所示:
<item type="layout" name="activity_color_list">@layout/activity_color_twopane</item>
任何被 Android 归类为“大”或符合 sw600dp 分辨率标准的屏幕都会触发 Android 使用来自同名 XML 文件的activity_color_twopane布局。
片段编码
在为基于片段的应用编写代码时,您需要考虑的差异很少。主要的不同之处在于我们在第十一章中提到的与生命周期相关的回调,以及你的以用户界面为中心的逻辑和任何相关的数据处理将转移到片段级别。您的活动仍然存在,并且它们处理活动生命周期事件的逻辑和跨片段的功能也保持不变。
我们的ColorListActivity是使用片段时低编码负担的一个很好的例子。清单 12-5 展示了我们的应用的完整逻辑,包括处理我们的应用最终在父活动中显示为一个还是两个片段。
package org.beginningandroid.colorfragmentexample;
import android.content.Intent;
import android.os.Bundle;
public class ColorListActivity extends FragmentActivity
implements ColorListFragment.Callbacks {
private boolean mTwoPane;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_color_list);
if (findViewById(R.id.color_detail_container) != null) {
mTwoPane = true;
((ColorListFragment) getSupportFragmentManager()
.findFragmentById(R.id.color_list))
.setActivateOnItemClick(true);
}
}
@Override
public void onItemSelected(String id) {
if (mTwoPane) {
Bundle arguments = new Bundle();
arguments.putString(ColorDetailFragment.ARG_ITEM_ID, id);
ColorDetailFragment fragment = new ColorDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.color_detail_container, fragment)
.commit();
} else {
Intent detailIntent = new Intent(this, ColorDetailActivity.class);
detailIntent.putExtra(ColorDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
}
Listing 12-5The code for the ColorListActivity
总的来说,逻辑非常简单。当调用onCreate()时,我们将activity_color_list布局展开到 UI 中。接下来,我们测试以确定 color_detail_container 视图对象是否已经实例化(不管它是否显示)。这给了我们一个代理来确定应用是否运行在 activity_color_twopane 布局中,基于 Android 中的屏幕检测规则和我们的refs.xml规则。如果我们在这种状态下运行,我们设置一个布尔值mTwoPane为真,并使用getSupportFragmentManager(),通过setActivateOnItemClick()方法设置点击处理。
然后,onItemSelected() override 承担了决定当用户点击一种颜色时做什么的任务。我们应该使用ColorDetailFragment.java中的color_detail_fragment布局和相关代码创建一个额外的片段,还是应该使用startActivity()来显式调用color_detail_activity布局和相关的ColorDetailActivity.java代码?
Ch12/ColorFragmentExample中的源代码还揭示了显示颜色细节的机制和支持的ColorContent类,这只是一种为颜色和一些管理功能设置的项目的 Java 打包方法(记住,Android 的 Java 支持尚未超过 Java 8,因此像数据类这样的更现代的方法不可用)。可以提供该列表的其他选项是内容供应器或其他数据源。
ColorFragmentExample 行动范例
应用逻辑、布局和片段完成后,让我们运行应用吧!
为了查看片段在运行中的威力,我们需要两个不同大小的模拟器,它们已经在前面的例子中设置好了。图 12-3 和 12-4 显示了一个小设备上独立活动中的颜色列表和颜色细节片段——我在这个例子中使用了我的 Pixel 2 AVD。
图 12-4
通过触发新的活动来显示颜色细节片段
图 12-3
Pixel 2 模拟器上显示的颜色列表片段
当在更大的屏幕上运行相同的应用时,您可以看到不同之处。图 12-5 和 12-6 展示了片段的威力,应用运行在 Pixel C 仿真器上。
图 12-6
检测到大屏幕后,第二个片段被添加到活动中
图 12-5
ColorListActivity 初始显示,像素 C 上有一个片段
摘要
既然您已经记住了片段的核心概念,那么探索片段方法的全部能力的最佳方式就是在越来越多的应用中进行实践。我们在本书网站 www.beginningandoid.org 上有更多使用片段的例子。你可以在网上找到成千上万的例子。
十三、为 Android 处理声音、音频和音乐
是时候转移到为 Android 开发应用的一些更复杂和有趣的方面了。在接下来的几章中,我们将探讨如何向您的应用添加音频、视频和静止图像,以及创建和记录这些媒体类型以及显示和使用它们的机制。
让我们先来看看音频功能。本章剩余部分有一个警告:印刷书籍不能真正提供音频示例(尽管在线版本可以)。为了充分利用本章中的示例代码,您绝对应该尝试在设备或 AVD 上运行这些应用。
播放音频
Android 提供了在应用中访问和使用音频的丰富方式。事实上,随着时间的推移,方式的数量已经增长得如此之多,以至于选择几乎太多了。不要害怕。Android 还建立了一个结构良好的方法来帮助管理所有这些不同的方法——称为媒体包——我们很快就会谈到。
选择您的音频方法
首先,让我们看看 Android 中利用音频播放的主要方式。根据您的需求以及您计划从何处获取音频,每种不同的方法都为您提供了优势和选择。
使用原始音频资源
原始音频资源是一个声音文件或音频源,它被打包成一个文件,并捆绑在您的应用的raw文件夹中。raw 方法意味着您可以保证您希望使用的音频始终可用,这种方法非常常用于音频,如游戏、通知等中的声音效果。原始方法的缺点是,只能通过替换(升级)整个应用来更改与应用的 APK 打包在一起的音频。
使用音频素材
音频资源也与您的应用打包在一起,因此具有与原始方法相同的优点和缺点。素材方法提供的额外好处是通过 URI 命名方案使您的音频可用,例如,file:///android_asset/some_URL.这在您使用任何期望或需要 URI 的 API 时都有好处,Android 中有许多这样的 API。
将文件存储用于音频
对于胆小的人来说,文件存储方法甚至比原始方法更基本。您可以使用设备的板载或可移动存储来保存音频文件。访问是通过文件 I/O API 进行的(我们将在本书后面介绍)。虽然这是一个更大的管理负担,但这意味着理论上您可以下载新文件、更改文件和删除文件,而不必升级应用。
访问音频流服务
如果你想完全摆脱存储音频的烦恼,流媒体就是答案。您可以从其他设备上的服务或 Android 内容供应器(他们自己可能从其他地方流式传输)流式传输音频,或者直接从基于互联网的服务流式传输。流式传输让您不必担心存储、文件管理、空间需求或升级。它用连接焦虑取代了这些担忧——只有在数据连接正常的情况下,你才能进行流媒体播放。
使用媒体包
处理音频的丰富选择可能是一件好事,也可能是一件坏事。有一种机制可以满足你的每一个突发奇想,但是随着选择而来的是复杂性。幸运的是,Android 配备了多才多艺的媒体包,有助于简化您作为开发人员的生活,同时保留您可用的选项。
Media 包提供了两个关键的类供您使用,一个是用于播放音频的 MediaPlayer,我们将首先处理它,另一个是用于设备上录制的 MediaRecorder,我们将在本章的后面处理它。
创建音频应用
为了使媒体包和 MediaPlayer 类变得生动,我们将创建一个您几乎肯定会熟悉的标志性应用。如果你曾经拥有过 iPod、智能手机或类似的设备,你可能会播放你最喜欢的音乐、播客或类似的 MP3 音频文件。是时候制作自己的 MP3 播放器应用了。
在素材和资源之间选择
如本章前面所述,您可以选择使用哪种方法来管理音频文件,以便应用可以访问它们。
如果我们想使用原始音频资源,我们首先需要在项目中创建一个原始文件夹来存放音频文件。这可以在 Android Studio 中完成,通过导航到层级中的 res 文件夹并选择菜单选项File ➤ New ➤ Directory。将该目录命名为“raw”,您的 raw 文件夹现在就已经准备好了。
如果我们想要使用基于素材的方法,我们同样需要为我们的应用创建素材文件夹。要在 Android Studio 中创建一个 assets 文件夹,请在您的项目层次结构中突出显示 app 父级文件夹,然后在 Android Studio 中选择File ➤ New ➤ Folder ➤ Assets Folder菜单选项,这将提示为您的项目创建一个 assets 文件夹,如图 13-1 所示。
图 13-1
提示将素材文件夹添加到 Android Studio 中的项目
项目中 Android Studio assets 文件夹对应的文件系统位置是./app/src/main/assets(在 Windows 下是.\app\source\main\assets)。
如果想继续使用传统的 raw 文件夹,可以在项目的./app/src/main/res文件夹下找到(或创建)它。
编写音频回放代码
本着学习 MediaPlayer 的精神以及如何将音频播放器的所有部分组装在一起的基本原则,我们将回避媒体包中的一些其他好东西,包括捆绑特殊的屏幕小部件和其他有吸引力的素材,它们会让您快速获得一个外观精美的媒体播放器,但会剥夺您了解其工作方式和原因的任何机会。
我们将简单地开始,在接下来的几章中逐步建立,并探索媒体框架的后续部分。
以简单的方式播放音频
为了初步探索音频播放的媒体框架,我们将使用一个非常简单的初始界面,如图 13-2 所示。别担心。我们将在接下来的几章中对此进行扩展和改进。
图 13-2
最初简单的音频播放器界面
虽然这种简朴的设计并不花哨,但它让我们探索开始回放和停止回放的机制。布局 XML 如清单 13-1 所示,可作为Ch13/AudioPlayExample项目的一部分获得。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/startButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/stopButton"
android:layout_marginTop="268dp"
android:onClick="onClick"
android:text="Start ♫"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="-16dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:onClick="onClick"
android:text="Stop ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startButton"
tools:layout_editor_absoluteX="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 13-1The layout for the AudioPlayExample project
简而言之,该布局提供了两个按钮,标记为“开始”和“停止”,并附加了一个用于音符的 Unicode 符号。重要的是,这两个按钮中的每一个都添加了android:onClick属性,这使我们能够连接一个在按钮被单击时调用的方法。我们为每个按钮使用了相同的目标方法名—onClick。单击任一按钮都会调用这个方法,我们将使用android:id值驱动该方法中的逻辑来决定要做什么。
为 AudioPlayExample 编写 Java 逻辑
清单 13-2 显示了我们的音频播放示例的 Java 逻辑,它也可以在Ch13/AudioPlayExample项目中获得。
package org.beginningandroid.audioplayexample;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private MediaPlayer mp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startButton:
doPlayAudio();
break;
case R.id.stopButton:
doStopAudio();
break;
}
}
private void doPlayAudio() {
mp = MediaPlayer.create(this, R.raw.audio_file);
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
mp.start();
}
private void doStopAudio() {
if (mp != null) {
mp.stop();
}
}
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mp != null) {
mp.release();
}
}
}
Listing 13-2The Java code supporting the AudioPlayExample application
在我们深入研究代码之前,后退一步,看看有多少行代码。很少,其中许多是您期望看到的生命周期回调方法样板。正如您将看到的,这是因为即使以低级方式使用 MediaPlayer,编码效率仍然很高。
除了预期导入的view.View(针对所有标准 Android 小部件和其他部件)和os.Bundle,我们还导入了用于音频播放的关键媒体框架包:
-
AudioManager 提供了一系列支持功能,使各种音频的音频处理变得更加简单。您可以使用 AudioManager 来标记音频源是流、语音、机器生成的音调等等。
-
MediaPlayer 是媒体包的主力,它让您可以完全控制本地和远程音频的准备和播放。
-
异步播放的关键,OnPreparedListener 是一个接口,它可以在线程外准备完成后回调播放音乐。
onCreate()回调实现做我们非常熟悉的工作,将我们的布局膨胀到应用的 UI 中。然后,它将控制权交给其他方法,以响应用户与按钮的交互。
从前面对布局 XML 的描述中,您知道实现了onClick()来根据传递给它的View确定动作。当用户决定点击startButton或stopButton按钮时,Android 会将代表被点击按钮的相应View的引用传递给onClick()方法。Java switch 语句检测哪个View被传递给该方法,并暗示哪个按钮被点击。如果点击了startButton,则调用doPlayAudio()方法。或者,如果是stopButton,我们就调用doStopAudio()方法。
当用户点击“开始”按钮并调用startButton逻辑时,一系列意料之中和意料之外的事情发生了。事情从创建一个MediaPlayer对象开始,我们将音频文件绑定到这个对象。R.raw.audio_file符号在概念上类似于你已经见过的布局膨胀符号,比如R.layout.activity_main。Android 将遍历与应用打包在一起的 raw 文件夹中的.apk文件,并尝试查找名为audio_file的具有任何支持的音频扩展名(例如,mp3、m4a 等)的素材。–我们示例中的文件名audio_file.m4a。
文件确定后,我们通过mp.setAudioStreamType()方法第一次使用 AudioManager 类。AudioManager 可以为你做很多事情,其中之一就是为给定的音频资源设置流类型。Android 支持一系列音频流类型,允许它为所讨论的音频提供音量、保真度等方面的最佳支持。我们使用STREAM_MUSIC流类型,表明我们想要设备支持的最高动态范围等等。其他选项包括 DTMF 音调的STREAM_DTMF——Android 过滤任何以这种方式标记的流以符合 DTMF 标准——和STREAM_VOICE_CALL流类型,它触发 Android 调用或抑制语音音频的各种回声消除技术。
因为我们直接使用原始素材,所以我们可以立即调用doPlayAudio()中的mp.start()。这将触发 MediaPlayer 对象开始实际播放文件,并向扬声器或耳机发送音频。
用户点击“Stop”触发doStopAudio()方法,这在很大程度上是不言自明的。如果被实例化,我们首先调用MediaPlayer对象上的stop()方法。我们使用if{}块测试结构进行实例化,以检查如果用户从未点击开始(例如,如果他们打开应用并错误地点击停止作为他们的第一个动作),我们不会试图停止任何事情。
接下来是onPrepared()回调方法。该方法链接到包定义,其中 AudioExample 实现了OnPreparedListener接口。从技术上讲,在 AudioExample 应用的第一次调用中,我们不使用onPrepared()回调,但是在这里包含它是为了强调在MediaPlayer对象被实例化并且AudioManager被调用来设置流类型之后,有时您不能立即开始回放。何时以及为何使用onPrepared()将在流回放示例中进一步讨论。
我们以onDestroy()回调来结束,以释放 MediaPlayer 对象(如果它之前已经被创建的话)。
至此,您已经准备好亲自尝试一下了。启动一个 AVD 图像,运行这个例子,或者如果你修改了Ch13/AudioPlayExample代码的话,运行你的变体,让你自己确信最终的工作产品实际上会产生一些噪音!
使用流媒体播放音频
虽然回放 MP3 文件和其他存储的音频在 iPod 和其他音乐播放器出现时被认为是非常流行的,但显然时代在变化,Android 不跟上这种趋势将是失职。Android 完全支持流媒体,包括设备上和远程的其他服务的音频。媒体框架再次为您处理事情。
图 13-3 显示了我们修改的 AudioPlayExample,作为一个新的和改进的 AudioStreamExample。我将把文本中重复出现的 XML 保存下来——请随意在Ch13/AudioStreamExample中查看。
图 13-3
音频流示例用户界面
为了以流的形式获取和播放音频,我们的 Java 代码需要做更多的工作,这可以在清单 13-3 中看到。
package org.beginningandroid.audiostreamexample
;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity implements OnPreparedListener {
// useful for debugging
// String mySourceFile=
// "https://ia801400.us.archive.org/2/items/rhapblue11924/rhapblue11924_64kb.mp3";
private MediaPlayer mp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startButton:
try {
EditText mySourceFile=(EditText)findViewById(R.id.sourceFile);
doPlayAudio(mySourceFile.toString());
} catch (Exception e) {
// error handling logic here
}
break;
case R.id.stopButton:
doStopAudio();
break;
}
}
private void doPlayAudio(String audioUrl) throws Exception {
mp = new MediaPlayer();
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
mp.setDataSource(audioUrl);
mp.setOnPreparedListener(this);
mp.prepareAsync();
}
private void doStopAudio() {
if (mp != null) {
mp.stop();
}
}
// The onPrepared callback is for you to implement
// as part of the OnPreparedListener interface
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mp != null) {
mp.release();
}
}
}
Listing 13-3AudioStreamExample logic
我们的代码从第一个 AudioPlayExample 以两种方式发展而来,这可以在doClick()和doStartAudio()方法中看到。doClick()方法已经改变,接受用户在EditText对象mySourceFile中输入的 URL,并将其作为选择的音频文件进行播放。我们使用EditText的String值传递给后续调用中修改过的doPlayAudio()方法。try-catch 块用于覆盖doPlayAudio()现在可以抛出的异常,例如,如果没有找到 URL 或者没有返回流。
doPlayAudio()方法现在避免了直接文件访问。相反,我们简单地创建新的 mp MediaPlayer对象。我们调用AudioManager包,并声明最终的数据源将是STREAM_MUSIC。随后,我们使用传递的 URL 调用setDataSource()(这个方法还有许多其他有用的选项,但是我们将这些留到以后讨论)。
为了成功使用setDataSource()调用,我们需要在清单文件中授予我们的应用android.permission.INTERNET权限,这样它就可以获取源(音乐)流。我们将在第十九章中深入介绍权限,但是现在你需要做的就是将以下内容添加到你的项目的AndroidManifest.xml文件中:
<uses-permission android:name="android.permission.INTERNET" />
最后,调用 MediaPlayer 对象上的.prepareAsync()。
Synchronous vs. Asynchronous Playback
无论音频来源如何,尝试立即播放音频都有利弊。简而言之,作为一名开发人员,您需要回答这样一个问题:在用户点击 Play(或类似功能)和音乐实际准备好并可以通过设备播放之间的时间间隔内发生了什么?你是阻止所有活动,等待还是让其他事情继续?这是一个有着一系列细微差别的更深入的话题,你可以在本书网站 www.beginningandroid.org 上阅读更多内容。
播放音乐流
掌握了这些变化后,我们的AudioStreamExample将最终接收到对onPrepared()的回调,这样音乐(或声音、鸟鸣声或其他声音)将开始播放。onPrepared的逻辑与前面的例子没有变化。
探索其他回放选项
使用媒体包和MediaPlayer对象并不是你在 Android 下处理音频和音乐的唯一选择。其他选项包括:
-
SoundPool:media player 的精简版本,sound pool 简化了处理设备上声音文件的方法。它无需任何流或服务提供的音频,并利用文件/资源访问,使用
FileDescriptor来访问应用打包的音频文件。apk 文件通过简单的方法,包括.load()和.getAssets().openFd()。 -
AsyncPlayer: MediaPlayer 为音频回放的异步准备提供了一些支持,但是您在 AudioPlayExample 应用中看到的许多实际回放机制是同步的。AsyncPlayer 使用完全异步的两步方法。首先,实例化一个 AsyncPlayer 对象,然后用要播放的音频的 URI 调用它的
.play()方法。此后的某个时间,音频将开始播放,但是考虑到这种方法的异步特性,所涉及的时间是不确定的。 -
JetPlayer:复杂性谱的另一端是 JetPlayer。使用 JetPlayer,您可以使用与 Android SDK 和 API 捆绑在一起的外部工具来为您的应用打包和管理音频,并通过 MIDI 标准提供对音频的访问。从那里,你的 Android 应用就可以使用这个音频,并且可以访问一些非常复杂的操作选项,这些已经超出了本书的范围。
-
ExoPlayer:最新且越来越受欢迎的播放方式。ExoPlayer 是谷歌的开源产品,在正常的 Android 机制之外发布,并提供实验性或新颖的功能,如平滑流式自适应播放。它是专门为像您这样的开发人员扩展和修改而设计的。详见
https://github.com/google/ExoPlayer。
在android.com网站上有大量关于这些音频替代品的文档。
录制音频
讲述了音频播放的第一个基础知识后,是时候将我们的注意力转向录制音频和分享这些声音了。正如播放有许多替代方法一样,录制音频在 Android 中也有多种可能性。
使用 MediaRecorder 录制音频
对本章开头介绍的MediaPlayer的补充,MediaRecorder提供了一套广泛的工具,用于在各种环境下录制声音。了解它能提供什么的最好方法是深入一个例子,我们将通过扩展我们之前的例子,在Ch13/AudioRecordExample代码中加入记录特性来做到这一点。在图 13-4 中,你可以看到扩展的——尽管仍然很简单——用户界面,它在你已经看过的现有回放示例中添加了录制按钮。
图 13-4
添加到 AudioRecordExample 用户界面的录音按钮
清单 13-4 中显示了 AudioRecordExample 布局 XML。最值得注意的一点是,按照前面的模式,所有的按钮都将触发onClick()回调方法,这意味着记录和回放(以及二者的停止)将在补充的 Java 代码中处理。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/startRecordingButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/stopRecordingButton"
android:layout_marginTop="236dp"
android:onClick="onClick"
android:text="Start Recording ♫"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/stopRecordingButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:onClick="onClick"
android:text="Stop Recording ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startRecordingButton"
tools:layout_editor_absoluteX="-16dp" />
<Button
android:id="@+id/startButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/stopRecordingButton"
android:onClick="onClick"
android:text="Start Playback ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/stopRecordingButton"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/startButton"
android:onClick="onClick"
android:text="Stop Playback ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startButton"
tools:layout_editor_absoluteX="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 13-4AudioRecordExample layout XML
AudioRecordExample布局有不言自明的按钮,用于开始和停止录制和回放。与我们前面的例子有一个显著的不同,它隐藏在幕后。为了访问任何 Android 设备的录音功能,您的应用将需要权限。它还需要将记录的内容写入存储的权限(假设您想要存储您记录的内容)。我们将在第十九章中详细介绍权限和安全性,但是现在下面两个权限声明应该添加到AndroidManifest.xml文件中:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
运行时需要一个额外的权限步骤,提示用户允许写入外部存储器。有了相关的权限设置,您的代码现在可以访问记录并将它们存储起来。清单 13-5 显示了 AudioRecordExample 的 Java 逻辑。
package org.beginningandroid.audiorecordexample;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.view.View;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private MediaRecorder mr;
private MediaPlayer mp;
private String myRecording="myAudioRecording";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startRecordingButton:
doStartRecording();
break;
case R.id.stopRecordingButton:
doStopRecording();
break;
case R.id.startButton:
doPlayAudio();
break;
case R.id.stopButton:
doStopAudio();
break;
}
}
private void doStartRecording() {
File recFile = new File(myRecording);
if(recFile.exists()) {
try {
recFile.delete();
} catch (Exception e) {
// This code can be extended to deal with errors in recording here.
}
}
mr = new MediaRecorder();
mr.setAudioSource(MediaRecorder.AudioSource.MIC);
mr.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
mr.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
mr.setOutputFile(myRecording);
try {
mr.prepare();
} catch (Exception e) {
// do exception handling here
}
mr.start();
}
private void doStopRecording() {
if (mr != null) {
mr.stop();
}
}
private void doPlayAudio() {
mp = new MediaPlayer();
try {
mp.setDataSource(myRecording);
} catch (Exception e) {
// do exception handling here
}
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
mp.prepare();
} catch (Exception e) {
// This code can be extended to deal with errors in playback here.
}
mp.start();
}
private void doStopAudio() {
if (mp != null) {
mp.stop();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mr != null) {
mr.release();
}
if(mp != null) {
mp.release();
}
}
}
Listing 13-5The AudioRecordExample code
AudioRecordExample的代码看起来应该很熟悉,因为它模拟了我们之前在onClick()方法中使用的控制逻辑。在onClick()中,我们根据用户点击的按钮进行切换,播放开始和停止基本上模仿了AudioPlayExample的早期代码,doStopRecording()的方法与doStopAudio()几乎相同,只是 MediaRecorder 和 MediaPlayer 对象的基础分别发生了变化。这两个类之间的相似之处是有意的,其中共同的目标由概念上匹配的方法来服务。
我们的代码中主要的新逻辑是使用doStartRecording()方法。首先,doStartRecording()确保文件对象 myRecording 是新创建的,如果需要,删除任何先前存在的对象。在这种情况下,我们利用java.io.File包来提供文件处理能力——严格地说,这是普通的 Java 在运行,访问标准的 Java 库,并且在 Android 的导轨之外。我们将在本书的后面介绍更多使用标准 Java 库的功能。
然后我们创建一个名为 mr 的 MediaRecorder 对象,并调用它的。setAudioSource()通知应用想要访问麦克风(MIC)以便它可以记录声音的方法。这就是在我们的清单文件中要求RECORD_AUDIO权限的逻辑。
给予我们的应用对麦克风的访问权后,我们就能够为音频设置所需的输出容器格式,并设置所需的编解码器来对将被记录并放入容器中的声音进行编码。这些是.setOutputFormat()和.setAudioEncoder()呼叫。在我们的例子中,我们在每种情况下都使用DEFAULT选项,它实际上根据硬件设备和使用的 Android 版本所提供的特定支持来选择容器和音频编解码器。
Android 支持的主要输出格式包括
-
AAC_ADTS:苹果和 AAC 音频格式推广的容器。
-
AMR_NB:当您希望在 Android 设备之间实现最大的可移植性时,推荐使用 AMR 窄带容器类型。
-
MPEG _ 4:MPEG-4 容器格式是最古老的格式之一,但也是最有可能在旧的平台和设备上被误解的格式。小心使用。
-
三 _GPP:另一个广泛支持 Android 的推荐容器格式。
-
WEBM:该容器用于谷歌的 WEBM 格式,然后专利-未阻碍 Ogg 编码文件格式。
容器格式以及音频和视频编解码器的主题非常广泛,充满了大量的历史和行业阴谋。它本身会成为一本伟大的书,但是,唉,我们没有空间在这里深入探讨它。为了帮助您开始编写利用音频的 Android 应用,这里列出了 Android 中用于音频(和视频)编码的更流行的编解码器:
-
AAC(以及 AAC_ELD 和 HE_AAC):高级音频编码标准的音频编解码器。受到苹果和其他设备和平台的广泛支持。
-
AMR _ NB:AMR 窄带的实际音频编码器。虽然没有在 Android 之外广泛使用,但这种编解码器提供了跨 Android 版本和设备的广泛支持。
-
VORBIS:Ogg VORBIS 音频编解码器格式。
如果您再次查看.doStartRecording()方法,.setOutputFile()调用配置了我们之前创建的 Java 文件对象,作为用户将要记录的音频流的存储库。
最后,我们进入正常的两步,为我们的MediaRecorder对象调用.prepare()和.start()。正如MediaPlayer对象必须应对一系列潜在的延迟一样,MediaRecorder也是如此。这些延迟可能是无响应的远程服务、缓慢的机载存储或诸如此类的问题。无论在什么情况下,.prepare()负责保存您的录音,并在一切就绪后将控制权交还给调用者(您的应用)。此时,对.start()的调用实际上开始捕获音频输入。
体验这一切的最佳方式是在您选择的设备或 AVD 上亲自运行示例Ch13/AudioRecordExample。
扩展您的开发者音频工具集
已经向您介绍了 Android 下音频和声音的第一个方面,将音频内容引入 Android 应用的其他部分也值得了解。这些领域包括计算机和移动设备音频的一些基础知识,以及 Android Studio 之外的各种工具,这些工具对于在应用中充分利用声音和音频至关重要。
数字音频——即以数字形式捕获或创建的音频,用于在计算机、电话和其他数字设备上复制和使用——是一个巨大的话题。本章的其余部分将为您提供进一步探索的起点。
了解音频的主要方面
当谈到评估音频有多“好”时,有很多主观的意见——我不会告诉你我的音乐偏好,因为它们很好!但是,您也应该熟悉音频的一些客观方面,以便首先了解哪些属性有助于音频质量,然后了解如何影响这些质量以制造“更好的声音”
音频采样和频率
您的 Android 设备及其音频播放、您编写的声音应用、您用来听音乐的计算机以及大多数现代电子音频设备都基于音频或声音的数字编码。这种编码以两种方式之一产生:或者通过直接创建数字值,或者更典型地(至少在历史上)通过采样连续的音频源或信号,并使用足够频繁地获取的足够多的样本,从而在数字“快照”中创建音频信号的良好近似
这是音频采样的基本原则。当你考虑应该多长时间采样一次模拟信号以获得足够的数字快照来提供一些保真度时,事情变得更加复杂——当重放时,声音几乎无法与模拟原始信号区分开来。这里使用的术语是采样频率,通常音频的采样速率为每秒 44100 次,例如 44.1 kHz。可能还有其他更高或更低的速率,这种选择会影响最终音频的质量和用于表示它的数据量。
音频分辨率
如果我们以 44.1 kHz 的频率对音频进行采样,那么在对模拟信号进行采样时,我们究竟捕捉到了什么?答案是关于声波在样本时间点的振幅(大小)的信息。存储样本的空间越大,我们就能覆盖信号幅度的绝对高低,但更重要的是,我们就能更好地区分信号中的小阶跃。这也称为音频样本的“比特率”。
众所周知的音频分辨率比特率的一些历史示例包括光盘(CD),其在 CD 标准中使用 8 比特比特率。这允许相当好的采样保真度,并产生在 44.1 kHz 采样速率下每秒大约 44 kB 的采样数据。DVD 提升了游戏的比特率,允许高达 16 位的音频,当代的“高清”音频通常被认为是比特率为 24 位或更高的任何东西。这些都有助于更好的数据采集,理论上更好的声音质量和保真度,但代价是需要越来越多的存储来保存结果数据。正是这些存储方面的考虑导致了以质量为代价节省空间的格式的激增,这就是编解码器的领域。
编码、解码和数据丢失
人们希望找到编码音频的方法,从而大大减少所需的空间,这种愿望有很多途径。简而言之,在许多研究和商业环境中开发了一系列研究和技术,对采样的音频进行处理,以丢弃或删除代表声音或声音方面的数据,而一旦丢弃,人们大多不会注意到这些数据丢失了。最著名的例子是由德国弗劳恩霍夫协会开发的标准,MPEG-1 音频第三层——通常被称为 MP3。这种方法使用频率削波技术和其他方法来捕获数量少得多的数据,这些数据仍然可以用于创建原始音频的合理高保真再现。
这种编码音频的方法通常被称为“有损”压缩或编码,因为一些数据会丢失。另一种方法是“无损”编码,即不丢弃任何源数据。包括 WAVE(或 WAV,一种脉码调制形式)和 FLAC 在内的编码标准都是无损格式。你会经常听到所有这些方法被称为编解码器,这是两个缩写的组合:code 来自单词 encode,dec 来自单词 decode。
对于您使用音频开发 Android 应用的工作,您可以看到编解码器和有损与无损采集的选择将如何直接影响您使用的音频的大小和质量。
更多音频理论
关于音频理论,还有很多东西需要学习和理解,这是一本 Android 书籍所缺乏的。诸如序列、合成等主题都有完整的书籍,网上也有大量关于这些主题的信息。Wikipedia 是一个很好的起点,有关采样、比特率、编码格式等实验的实践和教程,请查看下一节介绍的工具。
选择更多音频工具
回放已经创建的音频是对应用的一个很好的补充,无论是音乐、播客还是应用的声音效果。在某种程度上,你可能需要超越使用他人的音频素材,并希望创建自己的或混合和调整其他预先录制的声音。音频录制和编辑软件是一个很大的领域,但为了让你开始,你不会错看一些流行的(和免费的!)可供您选择的选项。
介绍 Audacity
有几个软件领域,自由和开源软件有很强的影响力,甚至占据主导地位,音频编辑就是其中之一。十多年来,Audacity 一直是一个受欢迎的音频编辑套件,用 Audacity 网站
的 about 页面的话说,【Audacity】是一个免费的、易于使用的多声道音频编辑器和记录器,适用于 Windows、macOS、GNU/Linux 和其他操作系统。该界面被翻译成多种语言。
Audacity 有大量的特性,但是最重要的特性可以总结如下:
-
录制现场音频–从您的计算机上可用的任何输入源
-
在计算机上录制回放–从您可能正在运行的其他系统中捕获音频
-
完整的编辑功能和对所有关键音频格式的支持,包括 MP3、WAV、AIFF、FLAC、Ogg Vorbis 等(显然,这涵盖了几乎所有 Android 支持的格式——这是一个意外收获!)
-
其他格式可通过丰富的附加库系统编辑,如 M4A 和 WMA
-
用于拷贝、剪切、拼接、混音、音高和速度调整以及速度改变的完整工具
-
支持 16 位、24 位和 32 位质量和全范围采样速率
-
高级功能,如频谱分析和其他强大的选项
我还可以继续介绍更多的特性和功能,但是你可以在 Audacity 自己的优秀文档网站 https://manual.audacityteam.org 上阅读更多内容。
在写这本书的时候,Audacity 的版本是 2.4.2。由于它是开源的,可以免费获得,我强烈建议您至少下载并安装它,试用一下 Audacity。在 https://manual.audacityteam.org/man/tutorials.html 有非常容易获得的教程,包括我推荐给每个初涉音频发烧友的两个教程:编辑音频文件和创建新录音。您将很快为您的 Android 应用创建素材。
使用 WavePad
另一个强大的音频编辑工具的竞争者是 WavePad,它在最近几年越来越突出。WavePad 拥有至少和 Audacity 一样强大的功能集,在某些情况下甚至更强。
WavePad 更广泛功能的一个关键领域是它支持更深奥和罕见的音频文件格式,如 VOX。也许 WavePad 最有趣的部分是它有自己的移动版本,包括一个 Android 版本。这意味着您可以使用自己的移动设备来创建、编辑和完善您为同一设备编写的新应用所需的音频。
WavePad 使用“免费增值”模式,免费提供一些功能,但其他功能需要付费许可。你可以在 www.nch.com.au/wavepad/index.html 找到更多关于 WavePad 的信息。
对于苹果 Mac 用户来说,有 GarageBand
对于那些拥有 Mac 电脑的人来说,还有一个选择值得考虑。过去十年或更长时间里,每台 Mac 都配备了 GarageBand,这是苹果自己的音乐编辑和创作软件。
GarageBand 有许多很棒的功能,并且围绕它有一个非常强大的社区。显然,由于 GarageBand 是与 Mac 捆绑在一起的,所以我没有给你提供下载,如果你有一台 Mac,GarageBand 已经包含在你购买机器的价格中——所以开始使用它没有边际费用。
摘要
在本章中,我们介绍了 Android 的一些核心音频和声音处理功能,包括媒体包和 MediaPlayer plus MediaRecorder。我们已经看到了各种播放和录制声音的方式,并简要介绍了 Android 为音频提供的替代包,包括 SoundPool 和 JetPlayer。
我们将在下一章讨论这个话题,届时我们将再次看到媒体包的威力。