原文:
zh.annas-archive.org/md5/0C85816AE60A0FB2F72BC5A43007FCC3
译者:飞龙
第五章:使用意图进行数据传输
到目前为止,我们已经学习了意图的分类、它们在安卓组件中的用途以及在安卓应用程序中逐步实现它们的方法。现在是查看安卓应用程序最重要的部分的时候了。在安卓应用程序中,从一项活动向另一项活动传输数据(无论是隐式还是显式)是必不可少的。本章的主要焦点是数据的安全传输和检索。
本章包括以下主题:
-
传输数据的必要性
-
活动之间的数据传输——一种有意的方式
-
在显式意图中进行数据传输
-
使用意图显式传输数据的方法
-
在隐式意图中进行数据传输
寻找需要传输数据的需求
从技术上讲,一个安卓应用程序是由不同的活动组合而成的。这些活动包括布局、视图和一些内容。这些内容大多数不是动态的,也不是预先决定的。例如,如果一个安卓布局包含一个按钮,那么该按钮中的文本可以是静态的或预定义的。同样,如果一个活动中有任何文本字段或列表视图,它通常包含来自任何服务器或其他手段的动态数据。
在这些情况下,我们需要一些动态数据,这些数据我们的应用程序可以从服务器(或其他地方)获取,并在活动之间传输。正是在这种场景下发生数据传输。此外,在其中一个活动对数据进行一些操作,而另一个活动需要在其视图中显示它的情况下,数据传输的可能性非常高。
举一个简单的例子
为了更好地理解这张图片,让我们从理论上举例说明为什么需要在活动之间进行数据传输。读者应用程序可以作为一个很好的例子,来理解数据传输的原因。
读者应用程序是一个包含不同种类新闻的列表视图的应用程序,点击新闻可以进入描述页面,页面中会展示整条新闻以及图片和其他文本。让我们一步步了解这个应用程序的流程(以 TechCrunch 安卓应用为例)。应用程序将以一个启动屏幕开始,向读者或开发者描述谁制作了这个应用。
下面的截图是启动屏幕;应用程序将搜索互联网连接,以便将新闻源显示到应用程序的屏幕上。一旦本地获取了数据,它就会解析数据并将其放入列表视图中。请注意,以下列表视图的截图基本上是自定义列表视图,并不是直接通过安卓的内置布局获得的。我们需要为此制作一个自定义布局,然后在常规列表视图中填充它。为此,需要使用适配器(请参考互联网,了解如何在安卓中创建基本的列表视图)。
读者应用的活动,其中新闻在列表视图中列出。
现在,有两种可能的数据传输方式,如下所述:
-
获取包括描述在内的所有数据
-
一旦点击了列表视图中的馈送,将立即获取该馈送的描述。
显示前一个活动中点击的故事描述的活动
无论哪种情况,这一步都需要将数据从第一个活动传输到下一个活动。如果是第一种情况,由第一个活动解析的描述数据将被传递到第二个活动,以便在视图中填充它。否则,在第二种情况下,它将传递一些 URL 给第二个活动,从那里它可以获取新闻的描述。请参考前面的屏幕截图。
活动之间的数据传输——一种 INTENTed 方式
当我们讨论活动之间的数据传输时,我们需要记住,管理与活动流程交互的唯一方式是通过意图。在上一章中,我们详细讨论了如何使用意图从一个活动移动到另一个活动。在这里,我们将看到如何将数据与这些意图一起传输,以及如何在目标活动中安全地捕获传输的数据。
显式意图中的数据传输
你可以通过注意显式意图中数据传输的使用来开始理解意图中的数据传输。回想一下显式意图的定义,它们是指向另一个活动(在同一应用内或另一个应用内)的意图。显式意图通常指向同一应用内的活动,但根据应用需求,它们也可以指向属于其他应用的活动(例如,设备相机)。
活动间数据传输的方法
在本节中,我们将开始了解在 Android 应用程序中使用的各种数据传输技术。这些技术各有优缺点。总共有三种方法可以从一个活动显式地传输数据到另一个活动。我们很快就会看到它们以及它们的示例。这三种方法如下:
-
使用
putExtras()
进行数据传输 -
使用
Parcelable
进行数据传输(仅适用于自定义数据对象) -
使用
Serializable
进行数据传输(仅适用于自定义数据对象)
使用 putExtras()进行数据传输
在 Android 中,将数据从一个活动传输到另一个活动的最简单方式是通过 extras 发送。意图 extras 支持原始数据类型以发送数据。这意味着你可以以不同的数据类型如String
、Boolean
、Integer
或Float
的形式发送数据。
从理论上讲,意图额外信息可以在 Android API 的 Intent
类中找到。开发者需要创建一个 Intent
类的对象。这个对象将用于在活动之间导航。有了这个对象,就会有 putExtras()
函数的多种多态形式。这些多态形式接受不同的数据类型(如前所述)作为参数,并将意图对象加载到该数据中。这样,对象就完成了。现在,从 Activity
类中调用 startActivity()
方法开始执行意图。
这只是问题的一面。这个意图将应用程序的流程引导到第二个活动;在显式调用的情况下,它是同一应用程序的活动,或者在隐式意图的情况下,它可以是其他应用程序。这个新活动将接收意图对象并从中提取数据。因此,Intent
类中还有另一个方法,称为 getExtras()
。结果,它将给出源活动在意图对象中添加的所有额外信息,并使用它,我们可以轻松提取意图中额外信息中所需的数据。
这个理论解释可能不会让你完全理解使用意图进行数据传输的每一个细节。我们将在下一节通过示例更多地了解使用意图进行数据传输,其中将给出数据传输的分步解释。
putExtras()
的实现
在这一节中,我们将逐步学习如何借助额外信息从一个活动向另一个活动传输数据。正如你可能之前读到的,考虑到活动之间的数据传输,这种方法是最简单的。为了理解这个方法的运作和实现,你必须了解活动生命周期、不同活动的处理以及意图的实现,以便在活动之间导航作为前提条件。
为了开始第一个示例,第一步是创建一个 Android 项目。在 Android Studio 中创建项目的步骤在之前的章节中已经描述;如果你需要,可以参考它们。你最终会创建一个带有许多文件和文件夹的项目(这是 Android 项目的默认设置)。
实现一个现成的教程
为了简单起见,我们将源活动命名为 Activity1
,目标活动命名为 Activity2
。现在,按照以下步骤成功实现示例。
-
首先,创建一个新的 Android 项目,或者选择你想要使用意图实现数据传输的任何现有项目。在你新创建的项目中,相应类里实现以下代码:
//--------------------------------------------------------------- //Activity1 Class public class Activity1 extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_first); final EditText editTextFieldOne = (EditText) findViewById(R.id.edittext1); final EditText editTextFieldTwo = (EditText) findViewById(R.id.edittext2); final EditText editTextFieldThree = (EditText) findViewById(R.id.edittext3); Button transferButton = (Button) findViewById(R.id.button); String valueOne = editTextFieldOne.getText().toString(); String valueTwo = editTextFieldTwo.getText().toString(); String valueThree = editTextFieldThree.getText().toString(); transferButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Intent intent = new Intent(Activity1.this,Activity2.class); intent.putExtra("EDITTEXT_ONE_VALUE",valueOne); intent.putExtra("EDITTEXT_TWO_VALUE",valueTwo); intent.putExtra("EDITTEXT_THREE_VALUE",valueThree); Activity1.this.startActivity(intent); } }); } } //---------------------------------------------------------------- //Activity2.java public class Activity2 extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.main_second); Intent intent = getIntent(); String valueOne = intent.getExtras().getStringKey("EDITTEXT_ONE_VALUE"); String valueTwo = intent.getExtras().getStringKey("EDITTEXT_TWO_VALUE"); String valueThree = intent.getExtras().getStringKey("EDITTEXT_THREE_VALUE"); TextView textViewOne = (TextView) findViewbyId(R.id.textView1); TextView textViewTwo = (TextView) findViewbyId(R.id.textView2); TextView textViewThree = (TextView) findViewbyId(R.id.textView3); textViewOne.setText(valueOne); textViewTwo.setText(valueTwo); textViewThree.setText(valueThree); } } //---------------------------------------------------------------- //main_first.xml File <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".Activity1" > <EditText android:id="@+id/edittext1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/textView1" android:layout_alignBottom="@+id/textView1" android:layout_alignParentRight="true" android:layout_toRightOf="@+id/textView1" android:ems="10" android:inputType="textPersonName" > <requestFocus /> </EditText> <EditText android:id="@+id/edittext2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/textView2" android:layout_alignBottom="@+id/textView2" android:layout_alignLeft="@+id/edittext_enter_name" android:layout_alignRight="@+id/edittext_enter_name" android:ems="10" /> <EditText android:id="@+id/edittext3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/edittext_enter_sirname" android:layout_alignRight="@+id/edittext_enter_sirname" android:layout_alignTop="@+id/textView3" android:ems="10" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignRight="@+id/edittext_enter_address" android:layout_below="@+id/textView3" android:layout_marginRight="10dp" android:layout_marginTop="33dp" android:text="@string/enter_button_text" /> </RelativeLayout> //---------------------------------------------------------------- //main_second.xml File <?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/null_string" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/null_string" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/null_string" android:textAppearance="?android:attr/textAppearanceMedium" /> </LinearLayout> //---------------------------------------------------------------- //AndroidManifest.xml File <?xml version="1.0" encoding="utf-8"?> <manifest package="com.app.application" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.app.application.Activity1" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name= "android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.app.application.Activity2" android:label="@string/app_name" > </activity> </application> </manifest>
-
运行项目,屏幕上会出现以下截图:
Activity1.java
布局用于通过PutExtra()
从用户处获取输入。 -
填写
EditText
字段并点击按钮以传输数据。Activity2
屏幕将出现,并显示在Activity1
屏幕中输入的表单数据:Activity2.java
文件的视图,显示数据已成功捕获并显示。
理解代码
前一节“实现一个现成的教程”包含五个部分,我们将在以下各节中详细查看。像本书中呈现的每个示例一样,我们已根据新项目描述此示例,以使其灵活且易于理解。你可以轻松地将此示例放入你自己的应用程序中;一旦你正确理解了意图额外信息,这样做不会花费额外的努力。
Activity1.java
类
开始,这是源活动,它将启动意图以导航到下一个活动。
这是一个在创建新的 Android 项目时构建的简单活动。回顾基础知识,它将有一个onCreate()
方法,该方法将在活动由 Android 创建时首先执行。一旦创建了活动,在Layout
文件夹中的main_first.xml
文件中定义的布局将在屏幕上呈现。
现在,是获取布局文件中所有EditText
字段对象的时候了。为此,我们将在代码中添加以下几行,通过 ID 查找视图并返回对象:
EditText editTextFieldOne = (EditText) findViewById(R.id.edittext1);
提示
为你的对象使用有意义的名称是很好的。由于这本书面向初学者,他们通常不处理大型应用程序,因此给出的对象名称是为了使代码尽可能简单。
findViewById()
方法属于Activity
类,其目的是找到可以是任何布局子视图的特定视图并返回对象。同样,通过编写以下几行代码,我们将获得另外两个EditText
对象:
EditText editTextFieldTwo = (EditText) findViewById(R.id.edittext2);
EditText editTextFieldThree = (EditText) findViewById(R.id.edittext3);
目前,我们拥有Activity1.java
类中所有输入字段的对象。下一步是实现按钮的功能,该功能将从这些字段获取输入,将它们添加到意图对象中,并发送出去。
注意
findViewById()
方法的返回类型是View
类的对象。因此,在使用findViewbyId()
时,我们需要将返回的对象强制转换为特定的类类型。为了理解这一点,你可以看到在前面的代码中,视图被转换为EditText
。
现在,下一步是在按钮上实现OnClickListener()
方法。为此,第一步是使用与输入字段类似的方法获取按钮对象。
Button transferButton = (Button) findViewById(R.id.button);
一旦我们获得了按钮对象,我们将使用参数OnClickListener()
实现setOnClickListener()
方法及其实现:
transferButton.setOnClickListener(new OnClickListener(){});
如你所见,在前面的代码行中,我们已经为transferButton
附加了一个OnClickListener()
对象,以及它自己的setOnClickListener
方法。请记住,它仍然是一个原始方法。现在,是时候重写onClick()
方法了。
在前面的代码中,你可以看到给出了onClick()
方法的定义,在这个方法中,我们将从EditText
字段获取数据并将其放入意图额外数据中。如代码所述,通过在每个EditText
对象上调用以下行来从EditText
字段获取数据:
String valueOne = editTextFieldOne.getText().toString();
这将获取输入字段中当前存在的值。我们获取所有EditText
字段的值,并依次存储在valueOne
、valueTwo
和valueThree
中。
现在,我们已经有了要放入意图对象中的数据,我们使用之前描述的方法创建一个意图对象。我们设置源活动和目标活动(即Activity1.java
作为源活动,Activity2.java
作为目标活动)。下一步是在代码中传递值。Intent.putExtra(String name, String data)
方法最适合将字符串值放入额外数据中。
putExtra()
的参数有些像键值对。第一个参数name
,基本上是到达目标活动后用来识别它的键。另一个就是与该键相关联的额外传输的值。因此,在下面的代码行中,我们使用一个键将字符串放入意图对象中:
intent.putExtra("EDITTEXT_ONE_VALUE",valueOne);
现在,我们已经将第一个EditText
字段的值使用键EDITTEXT_ONE_VALUE
放入了意图对象中,对于其他两个值,我们重复这一信息。一旦值被加载到意图对象中,我们就调用startActivity()
方法来执行这个意图。
Activity2.java
类
这是处理传入意图的目标类。这个类包含一个简单的布局文件,其中包含三个TextView
视图,以显示来自前一个活动的值。在onCreate()
方法中,意图是通过getIntent()
方法接收的。这个方法属于Activity
类,用于获取将导航到它的意图。
Intent
类中有一个方法,用于获取与特定意图对象一起到来的所有额外数据。以下方法用于识别带有描述键的特定数据集:
String valueOne = intent.getExtras().getStringKey("EDITTEXT_ONE_VALUE");
与键EDITTEXT_ONE_VALUE
关联的值将从意图对象中提取,并保存到valueOne
字符串中。同样,所有的数据都将从意图对象中取出并保存在这个目标类中。
数据保存到变量后,是获取TextView
视图的对象,并将这些值设置给它们的时候了。如前所述,使用findViewById()
方法获取TextView
视图。
textViewOne.setText(valueOne);
setText()
方法用于在TextView
中设置文本,因此它保存的是来自第一个活动传入的值。这就是目标活动如何通过使用putExtras()
功能从源活动中获取数据。
main_first.xml 文件
main_first.xml 文件是一个简单的 XML 文件,其中包含三个EditText
字段,用于活动从中获取输入。此外,它还包含一个按钮,用于触发事件以便导航到下一个活动。这些视图的 ID 分别被指定为edittext1
、edittext2
、edittext3
和button1
。
提示
你可以通过在 GUI 中拖放来制作你想要的布局文件。布局文件的 XML 代码很简单,并且在之前的章节中也有解释。但是,请记住,拖放特别不推荐给新的 Android 开发者使用;因此,实现它的最佳方式是通过 XML 文件。
main_second.xml 文件
这是第二个活动的布局文件,实际上它是目标活动和数据接收活动。布局包括三个TextView
视图,用于显示从Activity1
即源活动发送的valueOne
、valueTwo
和valueThree
。
AndroidManifest.xml 文件
AndroidManifest.xml
文件是 Android 应用程序的基本部分。它跟踪整个应用程序的结构。它包含所有的活动、接收器、权限、与版本相关的问题、最小和最大 SDK 以及其他许多内容。由于我们的项目中有两个活动,Activity1
和Activity2
,因此AndroidManifest.xml
文件中包含了这些活动以及像应用程序版本名称和版本代码等不同的内容。
注意
从一个活动向另一个活动发送数据不需要特殊权限,但在 SD 卡或内部存储上进行数据读写时,我们确实需要某些权限来完成这项任务。
未来的考虑事项
通过包裹发送数据是 Android 意图使用的基本技术之一。它有许多改进和效率提升,我们将在以下数据传输方法中进行进一步研究。我们还应该记住,这种方法仅限于某些特定的数据类型(在下一节中给出)。为了从从一个活动向另一个活动传输自定义对象,我们需要使用下一个数据传输方法。
支持的数据类型
Intent 的putExtra()
方法支持多种数据类型,可以传输到目标活动。在之前的示例中,我们只使用了一种数据类型(String
),但除此之外,我们还可以添加其他各种数据类型。除了putParcelable()
和putSerializable()
之外,这些方法都是不言自明的,它们是本章的下一个主要话题。请查看以下截图,展示了各种数据类型:
可以在 putExtras() 意图中添加的不同的数据类型
安卓捆绑包(Android Bundles)的概念
安卓数据 Bundle 是一个可以添加和一起发送各种值的捆绑包。例如,如果我们想通过putExtra()
发送多个值,我们会创建一个 Bundle,将所有这些值添加到 Bundle 中,然后使用intent.putExtras()
方法发送此 Bundle。
注意
你可以直接将所有值分别添加到意图中,以直接向意图提供数据,或者第二种方法是将所有值添加到 Bundle 中,并通过意图发送它。
我们现在将探讨如何将数据提供给 Bundle,并通过修改上一个活动来将此 Bundle 发送到下一个活动。在从Activity1
发送数据时,我们不是直接将不同的值添加到意图中,而是进行以下操作:
如你在代码中所见,valueOne
、valueTwo
和valueThree
通过使用newBundle.putString()
函数和每个数据值统一的关键字添加到 Bundle 中。现在,这个 Bundle 通过intent.putExtras(newBundle)
函数添加到意图中,然后我们像在之前的例子中一样调用startActivity()
函数。
在目标活动中,我们可以通过首先使用getIntent().getExtras()
函数提取数据捆绑包直接捕获数据。这将返回 Bundle 对象,通过引用我们在源活动中添加的特定关键字,我们可以使用以下函数提取所有三个值的数据:
Bundle.getString("EDITTEXT_ONE_VALUE","DEFAULT_VALUE");
注意
Bundle.getString(key, defaultValue)
中的第二个参数是默认值,如果找不到指定键的值,将返回该默认值。
查看以下截图。你将看到可以同时添加到 Bundle 中的不同数据类型:
在 Bundle 中添加不同数据类型的各种函数
使用 Parcelable 进行数据传输
在活动之间传输数据的第二个也是最重要的方法是Parcelable()
。前面的方法有一个限制,即我们只能发送基本数据类型,如Strings
、Integers
、Doubles
和Floats
。在实际项目中,我们需要在活动之间传输自定义对象。这些自定义数据对象根据应用程序的需求保存信息。因此,应该相应地传输它们。
现在很清楚,上一个版本仅用于基本数据类型的数据传输,而 Parcelable 可以被称为前一个类型的子类型。
在此方法中,数据类通过实现Parcelable
类接口来继承,以使其对象与putExtra()
意图方法兼容。我们还需要覆盖Parcelable
类接口中的一些方法,以赋予其功能。一旦完成,该类的对象可以放入意图或 Bundle 中,以便在活动之间传递。
Parcelable
的实现
在本节中,我们将学习如何在数据类上实现Parcelable
,然后如何在不同活动之间传输该对象。对此有两种情况:
-
只有一个对象从源类发送到目标类
-
自定义对象数组正在从源类发送到目标类
按照传统,我们将从创建一个新的 Android 项目开始。在给定的示例中,我们将其称为 Parcel 应用。该项目在创建时将创建Activity1.java
作为默认活动,它也将作为源活动。第二个活动将是Activity2.java
,它将作为接收 parcel 的目标活动。
实现一个现成的教程
完成新项目的创建后,将此代码插入到你的应用程序中。这将影响Activity1.java
、Activity2.java
、layout_activity1.xml
、layout_activity2.xml
和AndroidManifest.xml
,并引入另一个名为Person.java
的类。
//----------------------------------------------------------------
//The Activity1 Class
public class Activity1 extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity1);
final EditText nameText = (EditText) findViewById(R.id.edittext_enter_name);
final EditText sirnameText = (EditText) findViewById(R.id.edittext_enter_sirname);
final EditText addressText = (EditText) findViewById(R.id.edittext_enter_address);
Button enterButton = (Button) findViewById(R.id.button1);
final Person firstPerson = new Person();
enterButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
firstPerson.setFirstName(nameText.getText().toString());
firstPerson.setSirName(sirnameText.getText().toString());
firstPerson.setAddress(addressText.getText().toString());
Intent parcelIntent = new Intent(Activity1.this,Activity2.class);
parcelIntent.putExtra("FIRST_PERSON_DATA", firstPerson);
Activity1.this.startActivity(parcelIntent);
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it//is present.
getMenuInflater().inflate(R.menu.activity1, menu);
return true;}
}
//----------------------------------------------------------------
//The MySecondActivity Class
public class Activity2 extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity1);
Person incomingPersonObj = getIntent().getParcelableExtra("FIRST_PERSON_DATA");
TextView nameTextView= (TextView) findViewById(R.id.person_name_text);
TextView sirnameTextView= (TextView) findViewById(R.id.person_sirname_text);
TextView addressTextView= (TextView) findViewById(R.id.person_address_text);
nameTextView.setText(incomingPersonObj.getFirstName());
sirnameTextView.setText(incomingPersonObj.getSirName());
addressTextView.setText(incomingPersonObj.getAddress());
}
}
//----------------------------------------------------------------
//The layout_activity1.xml File
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".Activity1" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginTop="20dp"
android:text="@string/name_text"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/edittext_enter_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textView1"
android:layout_alignBottom="@+id/textView1"
android:layout_alignParentRight="true"
android:layout_toRightOf="@+id/textView1"
android:ems="10"
android:inputType="textPersonName" >
<requestFocus />
</EditText>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/edittext_enter_name"
android:layout_marginTop="20dp"
android:layout_toLeftOf="@+id/edittext_enter_sirname"
android:text="@string/sirname_text"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/edittext_enter_sirname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textView2"
android:layout_alignBottom="@+id/textView2"
android:layout_alignLeft="@+id/edittext_enter_name"
android:layout_alignRight="@+id/edittext_enter_name"
android:ems="10" />
<EditText
android:id="@+id/edittext_enter_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/edittext_enter_sirname"
android:layout_alignRight="@+id/edittext_enter_sirname"
android:layout_alignTop="@+id/textView3"
android:ems="10" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/edittext_enter_sirname"
android:layout_marginTop="20dp"
android:layout_toLeftOf="@+id/edittext_enter_address"
android:text="@string/address_text"
android:textAppearance="?android:attr/textAppearanceMedium" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@+id/edittext_enter_address"
android:layout_below="@+id/textView3"
android:layout_marginRight="10dp"
android:layout_marginTop="33dp"
android:text="@string/enter_button_text" />
</RelativeLayout>
//----------------------------------------------------------------
//The activity_two_layout.xml File
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=http://schemas.android.com/apk/res/android
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/person_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/null_string"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/person_sirname_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/null_string"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/person_address_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/null_string"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
//----------------------------------------------------------------
//The Person.java File
public class Person implements Parcelable {
private String firstName;
private String sirName;
private String address;
public Person(){
firstName = null;
sirName = null;
address = null;
}
public Person(String fName, String sName, String add) {
firstName = fName;
sirName = sName;
address = add;
}
public Person(Parcel in) {
firstName = in.readString();
sirName = in.readString();
address = in.readString();
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getSirName() {
return sirName;
}
public void setSirName(String sirName) {
this.sirName = sirName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public int describeContents() {
// TODO Auto-generated method stub
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// TODO Auto-generated method stub
dest.writeString(firstName);
dest.writeString(sirName);
dest.writeString(address);
}
public static final Parcelable.Creator<Person> CREATOR =new Parcelable.Creator<Person>() {
public Person createFromParcel(Parcel in) {
return new Person(in);
}
public Person[] newArray(int size) {
return new Person[size];
}
};
}
//----------------------------------------------------------------
//The AndroidManifest.xml File
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.app.parcelapplication"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.app.parcelapplication.Activity1"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.app.parcelapplication.Activity2"android:label="@string/app_name" >
</activity>
</application>
</manifest>
运行此应用程序,它将在你的设备上显示Activity1
的输出屏幕。查看以下截图,了解应用程序在 Android 屏幕上的显示效果:
Activity1
包含三个EditText
字段以接收用户输入,以及一个按钮用于将数据传输到下一个活动
当你处于第一个屏幕时,输入数据并按下按钮,迁移到下一个活动,该活动将显示输入的数据。屏幕将如下所示:
Activity2
以Parcelable
的形式显示从Activity1
传输的数据
理解Parcelable
实现
为了理解此示例的工作原理,我们首先需要了解Parcelable
的工作方式。在 Android 中,需要将自定义数据(即自定义对象和自定义对象数组)从一个活动传输到另一个活动。通常,自定义数据类与 extras 不兼容;因此,我们为该类实现Parcelable
接口。
Parcelable
对自定义数据类的作用是它与 extras 兼容。使用Parcelable
实现的类的对象可以轻松地添加到意图的putExtra()
方法中。同样,它也可以成为 Bundle 对象的一部分,稍后可以通过意图进行传输。
我们现在可以解释前面的代码。
Activity1.java
类
这是Parcelable
对象开始迁移的源类。它从实现onCreate()
方法开始。在这个方法中,在设置主视图之后,我们通过它们的 ID 找到视图并将它们的对象带到活动中。这些视图包括三个EditText
字段和一个按钮。它们用于获取用户的输入并触发事件,以便开始将数据传输到下一个活动。
在button.setOnClickListener()
方法中,我们传递了一个新的OnClickListener()
对象,在其中覆盖了onClick()
方法。我们希望一旦点击按钮就启动意图,因此我们在onClick()
方法中实现了意图,并从字段中获取数据。
现在,我们不希望该方法直接将数据传输到意图中。这就是为什么我们要创建一个Person.java
类的对象,该对象将保存从字段中获取的值。我们将这个对象命名为firstPerson
。为了将值设置到这个对象中,我们实现了以下代码行:
firstPerson.setFirstName(nameText.getText().toString());
前面的行将把该对象的名字设置为从第一个EditText
字段中获取的值。第一个EditText
字段,nameText
,保存了名字的值。因此,使用nameText.getText()
方法,它将返回可以轻松通过调用其上的toString()
方法转换的Editable
对象。
为了从第二个和第三个EditText
字段获取值,将重复相同的方法。它们将被设置在同一个Person
对象中。你可以通过以下代码行看到这一操作:
firstPerson.setSirName(sirNameText.getText().toString());.
firstPerson.setAddress(addressText.getText().toString());.
在这个阶段,firstPerson
对象已经准备好从Activity1
传递到Activity2
。由于该对象是通过实现Parcelable
来继承的,我们可以直接将其添加到额外数据中。我们将在接下来的部分学习如何实现Parcelable
。在这里,我们将看到如何将Parcelable
添加到意图对象中。
创建一个Intent
类的对象,并为其提供源和目标,即源上下文和目标类的.class
引用,以让它知道从哪里启动这个意图以及在哪里结束。我们可以通过调用parcelIntent.putExtra()
来添加Parcelable
。请看以下代码行:
parcelIntent.putExtra("FIRST_PERSON_DATA", firstPerson);
使用这个方法,我们可以轻松地将自定义的Parcelable
数据对象添加到意图对象中,然后在下一行简单地调用startActivity()
函数以启动意图。
Activity2.java 类
在这个类中,我们将学习如何在目标类中捕获传递的Parcelable
对象。为此,首先按照实现活动的onCreate()
方法的正常流程开始。设置内容视图并通过查找布局中的 ID 引入三个文本视图。这三个文本视图将显示第一个活动对象的接收值。
getIntent()
方法将接收由Activity1.java
类传输的意图对象,该对象持有数据。一旦获取到对象,我们可以通过调用getExtras()
方法来获取其额外信息,这将返回一个包含数据的Bundle
。在那个Bundle
上调用getParcelable()
函数,并带上键,以获取对象。现在,这个对象被一个新的Person
类对象incomingPersonObj
接收。
现在,我们有了一个在从startActivity()
调用意图时从源类初始化的相同对象。我们现在将通过调用以下代码行来设置文本视图的文本:
nameTextView.setText(incomingPersonObj.getFirstName());
sirnameTextView.setText(incomingPersonObj.getSirName());
addressTextView.setText(incomingPersonObj.getAddress());
incomingPersonObj.getFirstname()
方法将从incomingPersonObj
获取人的名字,并在第一次调用方法时直接将其值设置给nameTextView
。对于sirnameTextView
和addressTextView
对象,过程相同。
layout_activity1.xml
文件
这是包含Activity1.java
视图的布局文件。如代码中所述,它包含三个EditText
字段,其 ID 分别为:edittext_enter_name
、edittext_enter_sirname
和edittext_enter_address
。除此之外,还有三个文本视图,用于简单地指示哪个EditText
字段包含哪个值。
每个活动都需要一个事件触发器,用于启动任何进程。在这个布局中,按钮将执行任务;因此,它也被放置在EditText
字段下方。
layout_activity2.xml
文件
这个布局文件创建了Activity2.java
的布局,即目标活动。这个活动负责提取数据并在其布局中显示。布局包括三个TextView
视图,其 ID 分别为person_name_text
、person_sirname_text
和person_address_text
。这些 ID 用于将这些视图带到代码中(如代码的第二部分所示)。
Person.java
类
Person
类基本上是一个数据持有类,其对象将在应用程序的任何地方创建。这也被称为豆类,用于接收来自服务器以 JSON、XML 或其他任何格式的数据。在我们的Person
类中,有三个字段。所有字段都是私有的,并具有它们各自的公共获取器和设置器。firstName
、sirName
和address
对象表示它们将持有的信息类型。Activity1.java
类创建了此类的一个对象,从EditText
字段获取数据,并将其添加到对象内部。
这个类通过实现Parcelable
接口来继承。这个Parcelable
接口需要实现一些重要的事情。首先,我们将实现一个接受Parcel
作为参数的此类构造函数。这个构造函数将在实现Parcelable
接口时从类内部使用。in.readString()
方法用于从包裹中读取值。
提示
为了使这项技术工作,需要按照在writeToParcel()
方法中写入的顺序读取 Parcel。查看代码中写入 Parcel 的顺序。它是firstName
、sirName
和address
。在构造函数中也可以观察到同样的顺序。
重写writeToParcel()
方法是为了通过Parcelable
生成同类对象,以便可以使用。Parcelable.Creator<Person>
用于创建类的实例,供Parcel
使用;它使用writeToParcel()
方法来完成这项工作。
一旦准备好对象,就会将其转发到下一个活动,并由Activity2.java
类捕获,如给定代码的第一和第二部分所解释的那样。
AndroidManifest.xml 文件
当谈论开发 Android 应用程序时,这个文件的重要性不容忽视。我们需要在这个文件中添加这两个活动,以便它们被 Android 应用程序识别。如您在文件中所见,这两个活动在清单文件中都有自己的标签,以及它们的参数和意图过滤器。
未来考虑
前述实现Parcelable
的方法是用于在 extras 或 Bundle 中传输单个Parcelable
对象的。同样,我们可以通过实现Parcelable
来传输自定义数据豆的数组或 ArrayList。
使用 Serializable 进行数据传输
在意图中使用的第三种数据传输方法是Serializable
。许多 Java 开发人员已经熟悉Serializable
这个词,因为它在 Android 引入之前就已经使用了。Android 最大的优势在于,其开发在 SDK 中是 Java,在 NDK 中是 C++。这使得它非常灵活且强大。
功能方面也是如此;Java.io.Serializable
是纯 Java 功能,可以原封不动地在 Android 开发中使用。putExtras()
意图有一个选项,可以在不进行任何特定努力的情况下将 Java 序列化对象从一个活动传输到另一个活动。我们从Serializable
的介绍开始这一部分,供非 Java 用户了解。
什么是 Serializable?
Java 带有一个机制,可以将对象表示为字节数组。这里不仅序列化了数据,而且还可以找到关于对象类型以及对象内部放置了什么类型的数据的信息。
这些序列化对象可以写入文件并存储在任何外部存储中(例如 SD 卡)。重新制作对象的过程称为反序列化,在这个过程中,可以收集隐藏在字节数组中的信息,以便在需要时在内存中重新生成对象。
制作和重新制作任何序列化对象的过程完全独立于 JVM。这意味着可以在 Java 中序列化对象,并且可以在支持与序列化时相同版本的 Java 的 JVM 的任何语言中重新制作。
在 Java 中,ObjectOutputStream
类用于序列化对象,而ObjectInputStream
类用于当我们从现有的序列化对象重新生成对象时。这些类分别包含writeObject()
和readObject()
方法。这些方法实际上开始了序列化或反序列化的过程。
Serializable
的一个例子
在本节中,我们将了解 Java 中如何实现Serializable
。这是 Android 处理这些对象的内部机制。这个例子包含两种方法,用于执行序列化和反序列化的任务。
首先,序列化的方法如下:
Person
类实现了Serializable
接口。这将使对象能被ObjectOutputStream
类的对象识别。首先,我们将通过设置名称和地址创建一个序列化的对象,如代码所示。
一旦创建了对象,它就可以被序列化。我们从创建一个FileOutputStream
对象开始,该对象用于将数据写入文件;同时添加将引用序列化文件所在位置的路径。创建一个将引用该文件的ObjectOutputStream
对象,即ObjectOutputStream out = new ObjectOutputStream(fileOut)
。现在我们可以通过调用out.writeObject(person)
来写入对象。这将开始序列化对象(将其转换为字节数组)并将其添加到给定位置。然后我们将关闭out
和fileOut
对象。
下一步,我们将看到如何从序列化源读取数据。请看以下代码:
代码很容易理解,因为它几乎包含了将对象写入序列化文件所需的所有步骤。我们将创建一个Person
类的实例以保存重建的对象。创建FileInputStream
对象。它指向要反序列化的文件的位置。使用ObjectInputStream
对象获取该文件路径并使其准备好读取。通过这种方式,调用in.readObject()
方法以反序列化对象,并将返回person
对象。完成此操作后,我们将关闭in
和fileIn
对象。
现在我们有了反序列化Person
类的对象,可以在日志中或打印在控制台上。
注意事项
在 Java 和 Android 中编写可序列化对象的方法是相同的。当我们在 Android 中执行此操作时,我们可以将文件写入 SD 卡。之后,该文件可以被任何其他活动或应用程序获取并反序列化。
Serializable
的实现
到目前为止,我们已经理解了使用 Serializable
的主要原因。快速复习总是好的。Serializable
技术用于将对象转换为字节数组;我们可以将其写入文件,并将其存储为 SD 卡或其他存储设备上的 .ser
文件。然后可以在任何位置读取此序列化对象,而与活动无关。
注意
Serializable
是执行数据传输的最简单方法。它还用于从一个活动向另一个活动传递一个或多个自定义数据对象。
序列化文件不一定总是需要 .ser
扩展名。
与其他示例和实现一样,我们将通过创建一个全新的项目来开始这一过程。这个项目将包含两个活动;一个作为源活动,另一个作为目标活动。序列化的对象将从其中一个活动开始导航,目标活动将捕捉它以便从中提取数据。Android 原生支持 Java 的对象序列化和反序列化过程;因此,我们不需要做任何额外的工作,因为序列化的现象由 Android 自身处理。
传递可序列化对象 —— 教程
在本章中,将通过创建一个新项目来开始实现 Serializable
。默认情况下,这个项目将包含一个活动(假设为 Activity1.java
)。实施以下步骤,您将制作一个实现了 Serializable
的项目。然后我们将了解其解释。
从第一步开始,在您新创建的 Android 项目中实现以下代码。这将引入三个新文件:
-
Activity2.java
:这将作为目标活动。 -
layout_activity2.xml
:这个文件将保存目标活动的布局。 -
Person.java
:这是负责提供数据豆(data beans)对象的序列化类。// ============================================================= //The Activity1.java file public class Activity1 extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.layout_activity1); final EditText edittext1 = (EditText) findViewById(R.id.editText1); final EditText edittext2 = (EditText) findViewById(R.id.editText2); final EditText edittext3 = (EditText) findViewById(R.id.editText3); Button button = (Button) findViewById(R.id.button1); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Person person = new Person(); person.setName(edittext1.getText().toString()); person.setSirname(edittext2.getText().toString()); person.setAddress(edittext3.getText().toString()); Intent intent = new Intent(Activity1.this, Activity2.class); intent.putExtra("PERSON_OBJECT", person); startActivity(intent); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } } //================================================================ //The Activity2.java file public class Activity2 extends Activity { String TAG = "MainActivity2"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.layout_activity2); Person person = (Person) getIntent().getExtras().getSerializable("PERSON_OBJECT"); TextView tView = (TextView) findViewById(R.id.data_text); if(person != null){ tView.setText("Data Successfully catched"); Log.d(TAG, person.getName); Log.d(TAG, person.getSirName); Log.d(TAG, person.getAddress); }else{ tView.setText("Data Object is null"); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } } //================================================================ //The Person.java public class Person implements Serializable { String name; String sirname; String address; private static final long serialVersionUID = 1L; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSirname() { return sirname; } public void setSirname(String sirname) { this.sirname = sirname; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } } //================================================================ //The layout_activity1.xml file <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <EditTextandroid:id="@+id/editText1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:layout_marginTop="26dp" android:ems="10" > <requestFocus /> </EditText> <EditText android:id="@+id/editText2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/editText1" android:layout_centerHorizontal="true" android:layout_marginTop="41dp" android:ems="10" /> <EditText android:id="@+id/editText3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@+id/button1" android:layout_centerHorizontal="true" android:layout_marginBottom="22dp" android:ems="10" /> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="153dp" android:text="Enter Data" /> </RelativeLayout> //================================================================ // The layout_activity2.xml file <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <TextView android:id="@+id/data_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" /> </RelativeLayout> //================================================================ //The AndroidManifest.xml file <?xml version="1.0" encoding="utf-8"?> <manifest package="com.app.serializable" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.app.serializable.Activity1" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.app.serializable.Activity2" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> </application> </manifest>
完成实现后,运行项目。将出现以下屏幕,您需要在字段中添加您的数据。然后点击 Enter Data 按钮:
源活动要求输入数据,以便通过序列化发送它
当您按下 Enter Data 按钮时,将打开 Activity2.java
,并显示以下屏幕:
Activity2 显示数据已成功输入
同时,当您查看项目的 LogCat 时,它将显示我们在 Activity2.java
文件中记录的数据。请看以下截图:
LogCat 显示在 Activity2 中成功捕捉到数据
漫步于 Serializable 代码中
为了理解 Android 中 Serializable 的工作原理,你需要回顾一下之前描述的 Java 中Serializable
的细节。如果你还没有看过这部分内容,我们建议你查看一下。在本节中,我们将重点解释前面的示例以及如何在 Android 项目中实现它。
为了简化,我们将其分为六个部分。每个部分都有详细的解释。
Activity1.java
类
Activity1.java
类将作为启动意图的源活动。它还将作为源活动,因为它负责创建并发送自定义数据对象。让我们从代码的第一部分开始,即实现Activity1.java
类。
如前所述,这个类负责从用户那里接收数据输入,并创建一个数据对象。在onCreate()
方法中,我们首先会通过setContentView()
方法设置一个特定的布局,该布局包含视图。布局设置完成后,我们的下一个任务是将这些视图作为对象带入到我们的 Java 代码中,如下面的代码所示。这将帮助我们执行这些对象上绑定在布局中的视图的各种任务。
final EditText edittext1 = (EditText) findViewById(R.id.editText1);
final EditText edittext2 = (EditText) findViewById(R.id.editText2);
final EditText edittext3 = (EditText) findViewById(R.id.editText3);
调用findViewById()
函数会带来与 ID 相关联的特定视图。我们将它强制转换为EditText
类,并分别将其放入edittext1
、edittext2
和edittext3
对象中。
这三个字段将用于从用户那里获取输入,但我们需要一个事件触发器,用于从一个活动导航到另一个活动,并负责数据传输。为此,我们在布局中实现了一个按钮,并通过调用findViewById()
方法在 Java 代码中获取它:
Button button = (Button) findViewById(R.id.button1);
现在,我们的 Java 代码中有了所有必要的视图。下一步是实现按钮的功能,即它被点击时将执行什么操作。为此,我们需要在这个按钮上实现OnClickListener
接口:
Button.setOnClickListener(new OnClickListener())
上述代码行负责为按钮设置点击监听器。它接收一个OnClickListener
参数,在其中我们实现了onClick()
方法。这个onClick()
方法将负责给按钮分配一个任务:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
完成这些后,我们将创建一个Person
类对象(Serializable
对象),并将其值设置为从EditText
字段获取的输入值。现在,它有两个部分;第一部分是创建Person
类的新对象。我们将通过调用其构造函数来实现这一点:
Person person = new Person();
在第二部分,我们将通过调用getText()
方法获取EditText
对象的值。getText()
方法返回一个Editable
对象;因此,为了将其转换为字符串,我们在其上调用toString()
方法。当你观察代码时,我们会一起执行所有这些任务:
person.setName(edittext1.getText().toString())
首先,从edittext1
对象获取值并将其转换为String
。其次,我们通过其值设置人员的姓名。我们将使用类似的过程进一步设置person
对象的sirName
和address
:
person.setSirname(edittext2.getText().toString());
person.setAddress(edittext3.getText().toString());
现在,我们已经有一个准备传输的对象。我们将创建一个Intent
对象,并为其分配源活动和目的地活动的上下文。这将表示意图从哪个活动启动,以及它将迁移到哪个活动。我们将通过调用构造函数来实现这一点:
Intent intent = new Intent(Activity1.this, Activity2.class);
一旦创建了意图对象,我们将在其中添加数据对象并调用startActivity()
方法。为了将序列化对象放入意图对象中,我们将调用intent.putExtra()
方法。这部分最后的步骤是调用startActivity()
方法,该方法将启动导航过程。
Activity2.java
类
目的地活动的主要目的是捕捉意图,从中提取数据对象,并在视图中显示。我们从实现onCreate()
方法开始。首先,通过调用Activity
类的setContentView()
方法来设置布局。现在,是时候捕捉从Activity1.java
类启动的intent
对象了。
与本章前面的示例一样,我们通过调用getIntent()
函数来获取意图。这个函数返回一个用于启动此活动的intent
对象。一旦有了intent
对象,我们就调用getExtras()
函数。这将返回一个包含发送活动添加到这个intent
对象的所有额外内容的 Bundle。
在这个 Bundle 上,我们现在将调用getSerializable()
方法,它将借助发送活动赋予的key
值带来一个Serializable
对象。那个key
值应与发送活动的值相同;否则,它将返回一个可能导致应用因NullPointException
而崩溃的空值。
现在Person
对象已经包含了所有的值。我们接下来的任务是把这些值在 LogCat 中记录下来,以便我们可以验证它。实现了一个空指针检查,以查看对象是否为空。如果不为空,我们从person.getName
、person.getSirName
和person.getAddress
获取并记录其值。如果对象为空,它会显示Data Object is null
,因此不会崩溃。
Log.d(TAG, person.getName);
Person.java
类
当我们谈论从Serializable
传输数据时,Person.java
类是我们需要实现的最重要的类。我们创建了一个 Java 类,其中包含三个私有字符串变量。每个变量都有自己获取和设置外部值的 getter 和 setter 函数。与前面的方法一样,我们使用Parcelable
接口实现了我们的数据豆类;在这里,我们将使用Serializable
实现我们的类。
一旦实现,Android 就可以将这个类的对象视为Serializable
。现在,每当它被添加到意图对象中时,Android 将以字节数组的形式传输它。与Parcelable
相比,这是一个较慢的过程,但当我们只处理几个对象时,这种缓慢是不明显的。如果我们想在有数千个数据对象的地方应用这种方法,可能会花费一些时间。
layout_activity1.xml
文件
布局文件属于Activity1.java
类。当你第一次运行代码时,将显示这个布局文件中描述的布局。在Activity1.java
类的onCreate()
方法中,我们使用了setContentView()
方法来粘贴用户界面。
在这个 XML 文件中,有四个视图;其中三个是EditText
字段,用于在 Activity1 中接收用户输入。除此之外,还有一个按钮,用于在字段中完全填充数据后触发事件。给它们分配的 ID 是Activity1.java
类默认使用的,用于在.java
类中获取这些视图,edittext1
、edittext2
和edittext3
分别是它们各自字段的 ID。
layout_activity2.xml
文件
这个布局文件包含了Activity2.java
类所需的视图。它由一个TextView
视图组成,该视图将告诉我们从Activity1.java
类传来的值是否正确获取。这个文本视图将根据数据对象显示数据成功捕获或数据对象为空的消息。
AndroidManifest.xml
文件
AndroidManifest.xml
文件包含所有的活动、权限、SDK 信息、版本代码等许多其他内容。简而言之,它用于保存有关应用程序的所有信息。在这里,我们有我们的活动,Activity1.java
和Activity2.java
类以及它们的意图值。如果你忘记在这个文件中提及任何活动,它将在 LogCat 中产生异常,显示ClassNotFoundException
。
提示
Parcelable
和Serializable
是两种用于从一个活动传输数据对象到另一个活动的方法。Serializable
是最简单实现的,而Parcelable
是所有方法中最快的。如果你需要基于较少的对象执行任务,并且想要一个简单的解决方案,你应该选择Serializable
。但是,如果你想要一个无论实现复杂性如何都完美的方法,你应该选择Parcelable
。
数据和隐式意图
在前面的示例中,描述了应用程序内部数据传输的需求。现在非常清楚,没有数据的传输和处理,任何应用程序都是不完整的。在本章的这一部分,我们将看到需要将数据传输到隐式意图的场景。
回顾隐式意图的定义,它们通常不会指向一个专门的目标,而是将应用程序的流程交给外部应用程序,或者换句话说,它们启动另一个活动以执行特定任务。
外部应用程序需要从你的应用程序中获取一些数据以执行任务。现在,我们将看到在哪种情况下,以 URI 形式的数据传递会传递给隐式意图。
查看地图
你可以从自己的应用程序启动谷歌地图,并定位到一个特定的地点。这意味着你可以通过隐式意图发送需要打开的任何位置到谷歌地图。基于纬度和经度,你可以打开谷歌地图上的一个特定点。这个纬度和经度通过 URI 传递给谷歌地图,这是向隐式意图发送数据的一种方式。
为了执行这项任务,我们需要编写以下代码行:
根据特定的语法,我们需要编写 URI 字符串。为了打开特定位置的地图,URI 字符串包含geo
关键字,后面跟着纬度和经度(如代码所示,以逗号分隔)。这个 URI 值通过android.content.Intent.ACTION_VIEW
动作传递给隐式意图。这个视图动作将获取 URI 并打开最佳的应用程序来执行任务。然后,我们将通过调用startActivity(intent)
方法启动意图。
安卓操作系统中的谷歌地图视图。
打开网页
如果你想打开网页,数据传递的隐式需求也可以按类别划分,你需要用特定的加载页面打开默认的网页浏览器。在这个过程中,我们需要传递应用程序想要在浏览器中打开的 URL。这个 URL 也是通过Uri.parse()
方法传递的。
提示
请记住,在此场景中我们使用的是默认的网络浏览器,并不是作为安卓应用程序一部分的网页视图。
为了在默认的网络浏览器中发送并打开 URL,请按顺序实现以下代码行:
如代码所示,有一个字符串包含要在网络浏览器中打开的值(URL)。这个值然后通过Uri.parse()
函数的意图构造器中,使用Intent.ACTION_VIEW
动作输入。这将选择最佳选项来打开 URL。
浏览器打开我们应用程序调用的 Google.com 网页的视图。
发送电子邮件
您可能需要一种功能,在您的应用程序中调用默认的 Google 邮件应用程序,并指定一个特定的收件人和电子邮件内容。在这种情况下,我们同样需要将数据添加到意图对象中,即发件人姓名、电子邮件正文和电子邮件主题,然后启动该活动。
为了执行此任务,我们需要编写以下代码行:
创建一个带有Intent.ACTION_SEND
动作的Intent
对象。其作用是打开发送选项的意图。现在,是时候向这个对象中添加数据了。Android API 考虑到了所有可能发生的场景;因此,在Intent
类中有一些定义的常量,可用于由 Android 唯一标识数据。Intent.EXTRA_EMAIL
是在使用putExtra()
方法向意图提供电子邮件地址时使用的关键字常量。同样,在额外信息中提及主题也有一个关键字常量;Intent.EXTRA_SUBJECT
和Intent.EXTRA_TEXT
将用于添加电子邮件的正文。
一旦我们使用这些参数调用应用程序,它将以这些参数填充字段的方式打开 Gmail。它看起来会类似于以下截图:
由我们的应用程序调用的 Gmail 应用程序视图
拨打电话
如果您想在应用程序中启动一个特定号码的呼叫,您需要调用拨号器意图。使用Intent.ACTION_DIAL
,您可以启动带有给定 URI 的特定号码的拨号器意图。按照以下代码,在您的应用程序中实现拨号器功能:
URI 字符串包含了拨号器应该启动的电话号码。一旦打开拨号器,它会显示号码原文,用户现在可以拨打这个号码。
其他杂项场景
本章还可以包含其他各种场景(例如日历和时间小部件),但由于篇幅和限制,我们只考虑了四个场景。在隐式意图间实现数据传输至关重要,且可以非常轻松地完成。
总结
在本章中,我们详细学习了如何在 Android 应用程序内操作数据。我们了解到如何使用不同的方法将数据从一个活动传输到另一个活动,以及如何使用意图的putExtra()
函数简单传递默认数据结构。自定义数据对象或自定义数据对象数组可以通过Parcelable
和Serializable
发送到另一个活动。我们还学习了如何在我们的 Android 应用程序中实现所有这些数据传输方法。在章节的最后,我们简要介绍了四种场景,在这些场景中,当通过意图从我们的应用程序调用其他应用程序时,数据会通过隐式意图发送到其他应用程序。
本章节对于实际应用开发至关重要,因为活动间甚至应用外部的数据传递是任何 Android 应用程序的基本组成部分,而使用 Android 意图可以轻松实现这一功能。
在接下来的章节中,我们将学习如何使用意图来访问 Android 的特性。我们还将了解意图过滤器是如何工作的,广播意图的基础知识是什么,最后,我们将学习意图服务及待定意图的实现。
第六章:使用意图访问 Android 功能
在上一章中,我们讨论了使用组件进行数据传输。我们了解了如何从一个活动向另一个活动传输数据,以及为什么应该在不同的组件之间传输数据。我们还讨论了使用意图进行数据传输的各种方法。Android 系统中有很多组件,意图提供了一种简单的接口,使这些组件能够相互通信。第四章中,我们讨论了使用系统硬件如 Wi-Fi、蓝牙、摄像头、麦克风等的不同的 Android 组件。我们还讨论了如何使用意图利用这些组件,以及如何使用不超过几行代码的 Android 硬件开发许多不同的应用程序。
到目前为止,我们只讨论了硬件组件及意图与这些组件的作用。这一章全是关于 Android 软件功能以及如何使用意图作为主要接口在我们的应用程序中使用这些功能。Android 包含大量的库和 API,开发者可以非常容易地使用不同的 Android 功能。本章将引导我们了解常见的 Android 功能,并且我们还将开发一些示例应用程序,展示如何与这些功能结合使用意图。
本章包括以下主题:
-
Android 操作系统的功能
-
Android 功能与组件对比
-
常见的 Android 操作系统功能
-
Android 功能和意图
-
<uses-feature>
和<uses-permission>
标签 -
使用 SEND 动作分享
-
使用意图发送短信/MMS
-
使用意图发送数据消息
-
确认消息送达
-
接收短信
-
使用意图进行电话通信和拨打电话
-
使用意图发送通知
-
其他一些 Android 功能
小贴士
本章以及后续章节需要理解意图的概念和结构,这是基本前提。如果你对这些东西的基本概念不了解,我们建议你阅读第三章,意图及其分类和第四章,移动组件的意图以便继续深入学习。
Android 操作系统的功能
Android 是一个开源的操作系统和智能设备(如手机和平板电脑)的中间件框架。这些设备包含了许多提供用户便捷生活方式的功能和功能。这些功能包括硬件功能,如音频、蓝牙、摄像头、网络、麦克风、GSM、NFC 以及诸如加速度计、气压计、指南针、陀螺仪和 Wi-Fi 等传感器。
它不仅包括硬件组件,还包括软件特性,如应用小部件、主屏幕、输入法、动态壁纸、布局、存储、消息传递、多语言支持、浏览器、Java 支持、媒体支持、多点触控、通话、消息传递、多任务处理、可访问性、外部存储等。我们之前已经将硬件特性称为移动组件,并在前面的章节中进行了讨论。我们将在本章中通过实际示例讨论软件特性。
注意事项
在这里我们使用两个关键词:组件和特性。摄像头、蓝牙等组件是安卓手机的硬件部分。特性是安卓手机的软件部分,如短信特性、电子邮件特性等。本章将介绍软件特性、它们的访问方式以及通过意图使用它们的方式。
安卓特性与组件
通常,“安卓特性”和“组件”这两个词可以互换使用。但为了明确起见,我们将关键词组件定义为使用硬件的特性,而将关键词特性定义为在后台使用软件的安卓特性。正如我们在上一节中讨论的,安卓包含许多组件和特性,当这些组件和特性被移植到任何手机上时,就使其成为智能手机。并非所有的组件和特性都可以通过意图使用。因此,我们将详细讨论那些可以通过意图使用的特性。
需要注意的是,直接或间接使用硬件的特性需要用户提供访问权限。这些权限在应用程序安装过程中提供。如果用户没有向应用程序提供权限,应用程序将无法访问硬件;因此,它无法使用该特性。
在本章中,我们将了解那些在后台使用软件但还需要一些权限的特性。我们将在以下部分提供有关权限的更多详细信息。
常见安卓特性
到目前为止,我们只是以一般方式讨论了安卓特性。在本节中,我们将讨论在安卓手机和平板电脑中常见的一些最常用的安卓特性。每个安卓设备在某些方面都是独一无二的,并且拥有许多与其他品牌和手机不同的独特特性和组件。但是在所有安卓手机中都能找到一些共同的特性。这些特性中的许多可以用于我们的应用程序,无论具体的型号或手机如何,意图无疑是最异步且简单的方式来在应用程序中使用这些特性。现在,让我们看看在许多设备中常见的安卓特性及其在手机中的功能。
布局和显示
现在,智能手机的尺寸越来越大,还有一些新的 Android 设备类型,即平板电脑。更大的屏幕和更高分辨率的显示已经将手机转变为多媒体设备。这些设备拥有从 240 x 320 到 1268 x 800 像素的布局尺寸和从 3 英寸到 11 英寸的屏幕尺寸。这些设备屏幕密度有所不同,如低、中、高、大、超大等。以下图片展示了三种不同分辨率设备的比较:
不同屏幕尺寸的 Android 设备
为了显示高质量的图形,Android 提供了用于 2D 画布绘图和 3D 图形的图形库,使用 OpenGL-ES 的 OpenGL。在 Android 3.0 版本之后,引入了一个新的渲染图形库 RenderScript。RenderScript 是一种针对 Android OS 的脚本语言,允许开发者编写高性能图形渲染和原始计算代码。它主要定位于并行数据处理计算,比如在多核处理器(如 CPU、GPU 或 DSP)之间分配处理。
注意
在 Android 3.0 之前,专为 Android 平板电脑开发的版本,Android 使用 2D 画布来渲染其布局、主屏幕和移动 UI。在 Android 3.0 引入 RenderScript 之后,Android 使用 RenderScript 以更美观和优化的图形来渲染其布局、主屏幕和移动 UI。
数据存储与检索
没有哪种 Android 设备在运行时不需要某种形式的存储。为了更好的性能,设备不仅需要像 RAM 这样的易失性内存以便处理和快速访问,还需要像外部 SD 卡这样的永久性存储。Android 设备支持多种数据存储和检索方式,这些方式根据开发者和将使用的应用程序而有所不同。
如果我们的应用程序使用大量数据,开发者可以使用 SQLite 轻量级关系数据库功能为每个应用程序提供支持。开发者可以使用 SQLite 数据库来管理具有保密和高效存储能力的数据。
不仅数据库,Android 设备还提供了文件存储功能。由于保存和加载数据对于几乎每个应用程序来说都是至关重要的,Android 提供了多种不同的方法来存储和检索数据,以确保我们应用程序的持久性。文件存储并非最佳选择,但有时,开发者除了读写文件来处理应用程序的持久数据外,别无他法。幸运的是,Android 提供了功能,让开发者可以在设备的内部或外部媒体上创建、保存和加载文件,比如 SD 卡。这些 microSD 卡采用 FAT32、Ext3 或 Ext4 文件系统格式。不仅有这些文件系统,而且一些 Android 设备,尤其是平板电脑,支持高容量存储媒体,如 USB 闪存盘。
除了在数据库或磁盘文件中存储大量或重型数据外,安卓还提供了一个用于存储简单应用程序数据的功能,如 UI 状态、游戏得分等。这是通过使用SharedPreferences
方法实现的。SharedPreferences
方法使用名称/值对(NVP)机制来存储应用程序的轻量级数据。
连接和通信
在具备连接功能之前,我们不能将一个安卓设备称为智能手机。支持连接、通信和数据传输的技术包括 GSM/Edge、IDEN、CDMA、EV-DO、蓝牙、Wi-Fi、LTE、近场通信(NFC)、WiMAX 等。安卓提供了一套完整的通信和连接库。这使得开发者可以轻松地在他们的应用程序中使用这些功能。例如,通过蓝牙支持,用户可以发送文件,访问电话簿和语音拨号,以及在不同手机间交换联系人。
注意
在安卓 3.1 及其之后的版本中,安卓包含了通过蓝牙通信将键盘、鼠标和游戏手柄设备与安卓手机连接的原生功能。在安卓 3.1 之前,一些第三方应用程序为此提供了定制的方法。
安卓手机不仅支持数据传输,还支持电话和短信通信。安卓包含了用于短信通信的 SMS 和 MMS,以及线程化文本消息和安卓云到设备消息传递(C2DM),新的谷歌云消息传递(GCM)也是安卓推送消息服务的一部分。
对于电话功能,安卓支持通话,但在撰写本书时并不支持原生视频通话;然而,一些安卓设备定制的操作系统版本允许开发者和用户通过 UMTS 网络(如三星 Galaxy S)或 IP 进行视频通话。此外,谷歌环聊(Google Hangout),取代了谷歌 Talk,在安卓 2.3.4 及更高版本中可用。这允许用户使用互联网连接进行视频通话。要使用谷歌环聊视频通话,用户需要一个谷歌+账户。微软公司的第三方工具 Skype 也用于在安卓 2.3 及以后版本中进行视频通话。
可访问性和多点触控
安卓设备运行在完全基于触摸的界面上,包含一些硬或软触摸按钮,这些按钮根据设备的不同而有所差异。Android 设备原生支持多点触控。多点触控技术允许开发者和用户使用单点触控、轻敲、双击、长按、捏合缩放手势、旋转手势、各个方向的滑动手势等等。Android 的最新版本(在撰写本书时是 Android 4.4 KitKat)包含一些新的触摸手势,如轻敲和长按手势、滚动手势等。同时,三星推出了无触摸手势,利用他们特定的 API,即 Look API。通过 Look API,用户无需触摸屏幕,只需在空中移动手或头部,Android 就会执行所需的功能。例如,头部向上移动将向上滚动页面,头部向下移动将向下滚动页面。此外,许多 Android 设备制造商,如三星,推出了笔功能,以便用户更轻松、更准确地用笔在手机上书写和使用。
注意
多点触控功能最早是在 HTC Hero Android 手机中推出的。在那之前,由于当时苹果对触摸屏技术的专利,该功能在 Linux 内核层面原本是被禁用的。
除了触摸操作,用户还可以通过原生引入的语音或语音识别引擎来访问他们的手机。同时,Android 包含一个名为 Talkback 的功能,该功能允许视力不佳的人听到他们的 Android 手机在特定时间正在进行的操作。这些人可以使用语音操作如拨打电话、发送短信、导航等来访问他们的手机。这些语音操作是从 Android 2.2 版本开始引入的。在撰写本书时,通过语音操作控制硬件的能力在 Android 中尚未实现。
注意
Android 4.1 及其后续版本在语音操作上进行了增强,当使用特定命令查询时,可以读取来自谷歌知识图谱的答案。
广泛的内容和多媒体支持
安卓设备在高清媒体支持方面不亚于任何电脑。Android 提供了全面的 API 来管理图片、视频和音频。Android 设备支持的格式包括 WebM、H.263、H.264、3GP、MP4、MPEG-4、AMR、MP3、MIDI、OGG、WAV、JPEG、PNG、GIF、BMP 和 WebP。不仅如此,Android 还提供了使用 RTP/RTSP 协议、HTML 渐进式下载(如 HTML5 <video>
标签)、HTTP 动态流媒体协议以及 Flash 插件提供的 Adobe Flash 流媒体(RTMP)协议在线流媒体的功能。
注意
新的 Android 设备支持 3D 图像捕捉和 3D 视频支持作为其原生功能。
除了广泛的多媒体支持外,Android 还提供了播放功能、控制选项、播放器、像其他手机一样的声音硬件按钮、全屏播放等。
安卓不仅支持多媒体,还支持内容格式,如文本文件、Word 文档、HTML 等。安卓中可用的网页浏览器基于开源的 WebKit 布局引擎,该引擎最初由苹果公司开发。这与 Chrome 的 V8 JavaScript 引擎相结合,在安卓系统中使用。
虽然大多数安卓应用程序原生是用 Java 编写的,但由于安卓系统中没有 Java 虚拟机,因此不支持 Java 字节码。这个 Java 代码反而被编译成 Dalvik 可执行文件,在专为安卓系统设计的 Dalvik 虚拟机上运行。Dalvik 最重要的特点是,它与 Java 虚拟机相比,针对低电量、有限内存和 CPU 进行了优化。
注意
安卓浏览器在安卓 4.0 版本中 Acid3 测试得到了 100/100 的分数。 Acid3 测试是由 Web Standards Project 提供的网页测试页,用于检查网页浏览器对各种网页标准元素(如文档对象模型(DOM)、JavaScript 等)的符合性。
硬件支持
安卓设备不仅提供了电话功能,如拨打电话、发送短信等,而且还具有许多新硬件组件的多种功能。安卓具有视频摄像头、触摸屏、全球定位系统(GPS)用于基于位置的应用程序、加速度计、陀螺仪、气压计、磁力计、接近传感器、压力传感器、温度计、Wi-Fi、蓝牙和专用的游戏控制功能。
注意
一些新的安卓手机,例如三星 Galaxy S4,提供了新的传感器,如光和颜色传感器,用于捕捉无需触摸的手势。
安卓手机包含了 GPS 和基于位置的技术,因此安卓系统本地支持谷歌地图,以及谷歌基于 GSM 小区的位置技术,用于确定设备的当前位置。为了使地图对开发者和用户更有用,安卓还提供了本地 API,支持正向和反向地理编码,帮助将坐标转换为地址,反之亦然。
后台服务和多任务处理
由于安卓智能手机的屏幕尺寸有限,用户界面屏幕上只能显示一个应用程序。但安卓支持多任务功能,可以让应用程序和服务在后台运行。开发者可以利用后台服务执行不需要用户交互的自动处理。这一特性的应用示例包括生成提醒;监控消息、统计和天气预报;从互联网下载数据;或者在后台播放音频文件。
提示
当安卓设备内存不足时,它会停止后台中优先级较低的应用程序。开发者应该在应用程序进入后台之前存储必要的数据和应用程序状态,这样在被停止时,应用程序可以从保存的状态中恢复其状态。
安卓同样支持通知功能,这是一种传统的标准方式,用于在手机中提醒用户。开发者可以使用安卓的通知库来创建可以发出声音、支持震动或者激活 LED 灯的通知提醒。此外,安卓还允许开发者设置通知界面图标、布局等。
这些后台应用程序可以是独立的,也可以依赖于其他应用程序。安卓提供了如意图和内容提供者等特性,用于应用程序间的通信方法和机制。
增强的主屏幕
主屏幕就像电脑或笔记本电脑的桌面屏幕。安卓用户可以在主屏幕上获得快速链接、应用快捷方式和信息。安卓为主屏幕提供了可定制功能。小部件、活动文件夹和动态壁纸使主屏幕对用户来说更加互动和美观。这些应用程序允许安卓开发者创建动态的应用程序组件,它们可以直接在主屏幕上提供应用程序的窗口或提供有用及时的信息。开发者还可以为用户提供在主屏幕上添加快捷方式的选项。这些快捷方式为用户提供必要的信息,他们无需打开应用程序。例如,我们有一个显示当前时间和天气的应用程序。现在,每当用户想要查看时间和天气时,都需要打开应用。因此,与其为此目的创建应用程序,不如创建一个主屏幕小部件会是一个更好的主意。这个小部件会在主屏幕上显示天气和时间,用户就不需要打开应用程序了。
安卓的其他功能
安卓开发者可以用多种语言开发应用程序,为用户提供应用程序的本地版本。安卓提供了多语言应用程序的特性。
同时,安卓支持网络共享功能,允许用户将设备的网络连接分享给其他手机和电脑。这种共享可以通过 Wi-Fi 热点或 USB 网络共享来实现。
注意
网络共享功能在安卓 2.2 版本中引入;因此,早期版本通过第三方应用程序和制造商支持网络共享。
同时按下电源键和音量减键可以让用户捕获设备的屏幕截图。这个功能最早是在安卓 4.0 中引入的。早期版本使用第三方应用程序,但这些应用程序需要设备已获得根权限作为前提条件。开发者也可以通过 PC 连接使用 DDMS 工具来截图。
注意
任何安卓设备的 root 操作都是被禁止的,这样做会违反所有的保修和保证协议,有时也可能对手机造成风险操作。
安卓功能和意图
迄今为止,我们已经讨论了安卓手机和平板电脑中常见的不同功能,但我们仍然不知道意图(intents)与这些功能之间的联系。有些功能可以通过意图使用,而有些则不能。简单提醒一下,意图是不同应用程序与安卓系统之间的异步消息。
在本章中,我们将讨论一些可以通过意图使用的功能,并了解意图是如何执行各种动作的。我们将功能分为四个部分:消息传递、电话、通知和闹钟。我们将开发一些使用意图并访问这些功能的示例,并讨论如何访问这些功能以及意图在其中的作用。
在开始讨论这些示例应用程序之前,我们将讨论一些在安卓中用于澄清意图和功能之间概念的基本术语。在下一节中,我们将讨论 AndroidManifest
文件中的两个不同标签,uses-feature
和 uses-permission
。这些标签用于声明任何安卓应用程序的一些权限和设置。让我们在下一节中看看它们的作用。
<uses-feature>
和 <uses-permission>
标签
任何安卓应用程序默认情况下都没有权限执行可能直接影响其他应用程序、系统或用户的行为。这包括读取或写入用户的私人数据,如联系人和短信,读取或写入其他应用程序的文件,或任何其他活动。安卓系统允许应用程序独立运行并被沙盒化,但在共享数据的情况下,应用程序必须明确地相互共享。为了更轻松地实现这一共享目标,安卓允许开发者在他们的应用程序中声明所需执行活动的权限。用户将被通知有关允许他们在设备上安装应用程序的权限。
开发者需要记住关于权限的两件事:设备功能的权限,如访问相机或硬件,以及定义自定义权限。在本主题中,我们将讨论第一种选择,即访问设备功能和硬件,并授予应用程序权限。这可以通过在清单文件中使用两个标签来实现:uses-feature
标签和 uses-permission
标签。
首先,我们将讨论<uses-feature>
标签。<uses-feature>
标签允许开发者声明应用程序将使用的任何单一硬件或软件特性。这将在应用程序的AndroidManifest
文件的<manifest>
标签中声明,正如标签名称所暗示的,这会通知应用程序有关要访问的依赖实体。以下代码段展示了<uses-feature>
标签的一般声明方式:
)
你可以看到<uses-feature>
标签中有三个属性:name
、required
和glEsVersion
。android:name
属性以字符串描述符的形式指定应用程序使用的任何单一硬件或软件特性。<uses-feature>
标签中的android:required
属性相当重要。它是一个布尔值,表示应用程序是否需要android:name
属性中指定的特性。如果开发者对任何特性声明android:required="true"
,这意味着没有指定特性在设备上可用时,应用程序将无法运行。如果开发者对特性声明android:required="false"
,这意味着应用程序希望设备上有该特性。如果该特性不可用,应用程序可能无法正常工作,或者在尝试使用不可用的特性时可能会崩溃。此属性的默认值为 true。<uses-feature>
标签中的最后一个属性是android:glEsVersion
。这是一个 16 位表示的版本号。此属性指定应用程序将使用的 OpenGL ES 版本。例如,我们的应用程序中使用了摄像头。以下代码段展示了如何在清单文件中为摄像头声明权限:
)
你可以在代码中看到,我们使用了android.hardware.camera
字符串作为android:name
属性。这个字符串声明了 Android 的摄像头特性,其他属性声明应用程序需要摄像头特性,并支持 OpenGL ES 1.0 版本以便它能正常工作。开发者必须在应用程序中为每个使用的特性在单独的<uses-feature>
标签中指定;因此,如果应用程序需要多个特性,应在清单文件中声明多个标签。声明应用程序中使用的所有特性是一个好习惯。这些声明的<uses-feature>
标签只提供信息,Android 系统在应用程序安装前不会检查匹配的特性。
注意
Google Play 使用在清单文件中声明的<uses-feature>
标签,来过滤掉不符合其软件和硬件要求的设备上的应用程序。
<uses-feature>
标签最早在 API 级别 4 中引入。如果包含<uses-feature>
标签的应用在低版本设备上运行,早期版本会忽略此标签。
下表展示了硬件和软件特性的一些特性类型和名称字符串。它们可以用于<uses-feature>
标签的android:name
属性中:
硬件特性
特性类型 | 特性描述符(Android 名称) | 描述 |
---|---|---|
蓝牙 | android.hardware.bluetooth | 此特性允许应用使用设备的蓝牙功能。 |
相机 | android.hardware.camera | 此特性允许应用使用设备的相机组件。 |
android.hardware.camera.flash | 这是一个子特性,允许应用使用设备的相机闪光灯。 | |
位置 | android.hardware.location.gps | 此子特性允许应用使用从设备的全球定位系统(GPS)接收器获得的精确位置坐标。 |
传感器 | android.hardware.sensor.accelerometer | 此特性允许应用使用设备加速度传感器的运动读数。 |
android.hardware.sensor.compass | 此特性允许应用使用设备指南针的方向读数。 | |
android.hardware.sensor.proximity | 此特性允许应用使用设备的距离传感器。 | |
屏幕 | android.hardware.screen.landscape | 此特性将应用屏幕方向设置为横屏。 |
android.hardware.screen.portrait | 此特性将应用屏幕方向设置为竖屏。 | |
触摸屏 | android.hardware.touchscreen.multitouch | 此子特性允许应用使用两点触控功能,如捏合。 |
Wi-Fi | android.hardware.wifi | 此特性允许应用使用设备的 Wi-Fi 组件。 |
软件特性
特性类型 | 特性描述符(Android 名称) | 描述 |
---|---|---|
应用小部件 | android.software.app_widgets | 此特性允许应用包含应用小部件,并且可以安装在具有主屏幕的设备上。 |
主屏幕 | android.software.home_screen | 此特性允许应用作为设备主屏幕的替代。 |
输入法 | android.software.input_methods | 此特性允许应用提供自定义输入法。 |
动态壁纸 | android.software.live_wallpaper | 此特性允许应用提供动态壁纸。 |
我们在前面表格中并未展示所有特性和描述符。我们只列出了一些最常用的特性。表格展示了每个特性的特性类型,用于android:name
标签的特性名称描述符,以及特性将如何影响设备中应用的功能的简短描述。
一些特性被归类为硬件特性,一些则为软件特性。硬件特性是使用后端硬件组件的特性。为了访问这些硬件组件,我们的应用程序应具有访问硬件的权限。需要注意的是<uses-feature>
标签只是提供信息,它只告诉用户应用程序正在使用某些特定功能。它并不允许应用程序使用任何特定功能或组件的访问权限。
为了允许应用程序使用任何特定的组件,安卓提供了另一个标签<uses-permission>
。如果用户在安装时允许,此标签将提供应用程序访问组件的权限。以下代码片段展示了在清单文件中编写<uses-permission>
标签的语法:
<uses-permission>
标签请求应用程序必须获得的任何特定权限,以便它能正常运作。权限仅在应用程序安装时由用户授予。与<uses-feature>
标签不同,<uses-permission>
标签只有一个android:name
属性。该标签的唯一属性指定了权限的名称。权限名称可以使用<permission>
标签定义(这超出了本书的范围,我们不会讨论),或者使用安卓系统提供的标准权限名称。例如,为了允许应用程序读取电话联系人,我们可以编写如下代码片段:
你可以看到我们是如何从android.permission
包提供了一个标准的权限名称,以便读取手机的联系人。
注意
<uses-feature>
标签声明的特性被谷歌应用市场用来筛选应用程序,而通过<uses-permission>
标签声明的权限将在安装时向用户展示,以获取访问权限。
一些 <uses-feature>
标签名称描述符是在 <uses-permission>
标签描述符之后添加到 API 中的。因此,一些使用 <uses-permission>
标签的应用程序能够在不声明清单文件中的 <uses-feature>
标签的情况下使用特定硬件。为了防止应用程序因这种不匹配而出现任何意外问题,一些权限与某些功能相关联。Google Play 假定某些与硬件相关的权限默认表示需要底层硬件功能。<uses-feature>
标签允许 Google Play 在市场上筛选应用程序,只向用户展示设备能够运行的应用程序。然而,当用户下载并安装应用程序时,<uses-permission>
标签执行其职责。在安装之前,会要求用户授予应用程序中指定的所有权限。只有当用户授权时,应用程序才会安装。因此,对于那些既有 <uses-feature>
又有 <uses-permission>
标签名称描述符的功能,在应用程序的清单中声明两者是一个好习惯,以确保其正常工作。以下表格展示了一些由权限暗示的功能:
类别 | <uses-permission> 描述符 | <uses-feature> 描述符 |
---|---|---|
蓝牙 | android.permission.BLUETOOTH | android.hardware.bluetooth |
摄像头 | android.permission.CAMERA | android.hardware.camera |
位置 | android.permission.ACCESS_COARSE_LOCATION | android.hardware.location``android.hardware.location.network |
android.permission.ACCESS_FINE_LOCATION | android.hardware.location.gps``android.hardware.location | |
麦克风 | android.permission.RECORD_AUDIO | android.hardware.microphone |
电话 | android.permission.CALL_PHONE | android.hardware.telephony |
android.permission.PROCESS_OUTGOING_CALLS | android.hardware.telephony | |
android.permission.READ_SMS | android.hardware.telephony | |
android.permission.RECIEVE_SMS | android.hardware.telephony | |
android.permission.SEND_SMS | android.hardware.telephony | |
android.permission.WRITE_SMS | android.hardware.telephony | |
Wi-Fi | android.permission.ACCESS_WIFI_STATE | android.hardware.wifi |
从表中可以看出,所有由权限暗示的功能都是硬件功能,需要硬件组件才能正常运行应用程序。因此,已经明确指出开发者应该声明 <uses-feature>
和 <uses-permission>
标签,以便在 Google Play 中筛选并在设备上正确安装,不会给用户和开发者造成任何麻烦。
使用 SEND 动词分享
任何手机的主要目的都是提供一种简单的通信方式。像所有手机一样,Android 智能手机提供了一种更简单的通信方式。在这个互联网和社交网络的时代,Android 手机在共享和社交网络方面被证明是非常高效的。Android 提供了诸如共享图片、状态、发送电子邮件、社交网络(如 Facebook、Twitter 等)的功能。幸运的是,对于开发者来说,所有这些共享功能都可以通过几行意图代码非常容易地实现。意图被证明是在 Android 组件和应用程序内部执行异步通信的一个非常好的方式。
在第三章中,我们讨论了使用意图共享状态的示例。在本章中,我们将更详细地解释同样的SEND
意图,并查看如何通过用户选择的任何媒介共享图像和文本。当涉及到在 Android 手机上共享任何东西时,带有SEND
动作的意图被广泛使用。在本节中,我们将讨论带有SEND
动作的意图,以了解它能够实现的功能。
为了使用SEND
动作定义意图,以下代码段展示了其声明:
你可以看到,我们在意图的构造函数中传递了Intent.ACTION_SEND
的字符串常量。这个字符串常量告诉 Android 系统,意图是用来在设备上发送任何东西的。我们可以通过调用以下代码段中显示的startActivity()
方法来执行以下意图:
在startActivity()
方法中传递SEND
意图将允许用户通过提供所有可能的共享应用程序的对话框,选择他喜欢的发送方式。但是,如果我们没有设置意图类型就在startActivity()
方法中传递SEND
意图,它将抛出一个运行时异常。以下日志显示了运行时抛出的一些异常行:
在日志中,你可以看到"Unable to start activity"
,然后抛出了android.content.ActivityNotFoundException
。当调用startActivity(intent)
方法或其变体失败时,因为找不到执行给定意图的活动,就会抛出此异常。不仅异常类型,而且日志还显示了活动失败的原因。它说"No activity is found to handle the intent"
。你可能想知道为什么 Android 找不到适合接收意图的活动。回想一下前面的章节中的隐式意图,Android 会查找所有可能与意图类型匹配的活动,并在对话框中显示所有这些应用程序。在我们的例子中,除了它的Intent.ACTION_SEND
动作外,我们没有为意图定义任何类型;这就是为什么我们会得到ActivityNotFoundException
的运行时异常。让我们设置动作的类型,看看显示所有可能接收意图的应用程序的对话框:
我们可以看到调用了setType()
方法,并传递了一个text/html
类型的字符串。这个方法设置了意图的显式 MIME 数据类型。这通常用于创建只指定类型而不指定数据的意图。这是 Android 系统中常用的隐式意图。此方法会清除之前设置的任何意图数据。
注意
Android 框架中的 MIME 类型匹配是区分大小写的。因此,你应始终使用小写字母书写你的 MIME 类型。你也可以使用normalizeMimeType(String)
方法确保它被转换为小写。
我们在方法参数中传递了text/html
作为 MIME 类型。这个类型告诉 Android 系统,所有支持 HTML 类型数据处理的应用程序都可以接收这个意图。因此,结果就是 Android 会将所有这些应用程序推送到对话框中,让用户选择他/她喜欢的应用程序。以下图片展示了text/html
类型的对话框:
你可以看到所有支持 HTML 类型内容的程序都显示在图片中,比如电子邮件、Imo Messenger和Skype。你可以看到在 Android 手机上使用SEND
意图分享内容是多么容易,选择应用程序并启动它们的工作留给了 Android。
注意
你可能已经注意到对话框中没有显示短信/彩信发送应用程序,因为短信/彩信只是纯文本应用程序,它们只支持那种类型的内容。
从列表中选择任何选项后,应用将会启动。由于我们没有设置任何要分享的内容,应用程序将基本上是空的。要在意图中设置内容,我们必须使用额外的信息(extras)。我们会为一些信息如标题、主题或文本添加额外的信息。以下代码片段展示了如何在SEND
意图中添加一些额外的信息:
你可以在代码中看到,在设置意图的 MIME 类型之后,我们多次调用了putExtra()
方法。这个方法向意图中添加扩展数据。该函数有两个参数:name
和value
。name
参数必须包含一个包前缀;例如,应用com.android.contacts
会使用像com.android.contacts.ShowAll
这样的名称。我们为意图的主题、标题和文本内容传递了三个字符串。这类数据的名称,如Intent.EXTRA_SUBJECT
、Intent.EXTRA_TITLE
和Intent.EXTRA_TEXT
在Intent
类中已经声明,我们可以以静态方式访问它们。你可能会想,既然我们已经传递了标题字符串,为什么还要传递主题呢?嗯,SEND
意图是一个隐式意图,Android 显示了所有支持该意图的应用。用户可以选择任何应用,因为不同的应用对不同的数据感兴趣。例如,任何电子邮件应用都会对主题、收件人和正文字符串感兴趣。而任何短信应用只会对收件人和正文字符串感兴趣。因此,为了有效地使用SEND
意图,你应该添加所有可能的内容,以便有效地与每个应用共享。以下是一个使用SEND
意图发送电子邮件的示例。以下代码段展示了我们如何使用SEND
意图来发送电子邮件:
首先,我们通过传递Intent.ACTION_SEND
参数给构造函数来声明我们的SEND
意图。然后,我们通过调用setType()
方法将意图的类型设置为"text/html"
MIME 类型。接下来,我们为电子邮件应用添加额外的内容,如下列表所示:
-
Intent.EXTRA_SUBJECT
:此名称常量用于添加主题。 -
Intent.EXTRA_EMAIL
:此名称常量用于填充收件人字段中的电子邮件地址。 -
Intent.EXTRA_CC
:此名称常量用于填充 Cc 字段的电子邮件地址。 -
Intent.EXTRA_BCC
:此常量用于填充 Bcc 字段的电子邮件地址。
在调用startActivity()
方法之前,我们通过Intent.EXTRA_TEXT
这个名称常量来设置电子邮件的正文,并在putExtra()
方法的值参数中传递我们的文本。startActivity()
方法将显示与之前图像相同的对话框,并在选择电子邮件应用后,显示以下截图:
一个已经使用意图中的内容填充的电子邮件应用
从图片中可以看出,我们放入 extras 的所有数据都已自动填充到电子邮件应用中,如主题、电子邮件、文本等。现在,用户只需轻触发送按钮,电子邮件就会被发送。在这个示例应用中,我们直接通过 To 字段向一个地址发送了电子邮件,并通过 Cc 和 Bcc 间接向另外两个地址发送。安卓允许我们添加多个电子邮件地址。为此,使用了名称常量Intent.EXTRA_EMAIL
。我们在代码中传递了一个地址;我们还可以传递包含电子邮件地址的字符串数组,以发送电子邮件。
从本节内容中,我们主要了解到ACTION_SEND
意图是如何被使用的,以及仅用几行代码就能通过此意图完成多少工作。如果我们从对话框中选择 Facebook、Twitter 或任何其他应用,我们会看到通过该应用分享数据的相同结果。这就是使用隐式意图的强大之处,几乎在每种可能的情况下都能使其通用,而无需进行任何复杂开发工作。
注意
ACTION_SEND
是意图的一个动作。像这个动作一样,还有其他动作,如ACTION_VIEW
、ACTION_SEARCH
,可以通过传递意图用于安卓中的其他目的。
使用意图进行电话通讯和拨打电话
不仅安卓手机,从发明之日起,所有电话的主要目的都是提供一种远距离交流的方式。与其他所有电话一样,安卓手机也提供了拨打电话和接收电话、检查通话记录(如未接电话和已拨号码)、存储联系人、编辑/修改/删除联系人等功能。由于安卓手机属于智能手机范畴,其通话功能更为丰富。用户可以进行视频通话、录音通话、电话会议、手机与电脑之间的通话等。所有这些功能为用户提供了非常有效的产品,并让开发者能够利用这些功能实现更大的灵活性和生产力。
安卓为开发者提供了许多电话功能的 API。这些电话 API 让你的应用程序和开发者能够访问底层的电话硬件,从而可以创建自定义拨号器,集成呼叫处理或电话状态监控等功能。
注意
由于安全原因,开发者无法自定义手机的通话界面。当用户拨打电话或接听来电时,会显示通话界面。
由于本书专注于意图,我们只讨论那些可以使用意图利用的电话功能。在许多功能中,如拨打电话、接收电话、检查通话记录、接听/拒绝电话等,只有少数可以直接且仅通过意图利用。幸运的是,拨打电话就是其中之一。让我们在下一节讨论如何使用意图拨打电话。
使用意图拨打电话
在 Android 中拨打电话有两种方法。开发者可以使用 Android 提供的 API 来拨打电话,或者仅通过发送包含必要信息(如电话号码)的意图来启动电话拨打。我们将在本节的后面探讨启动电话拨打的方法。
在前面的章节中,我们了解了如何使用意图中的动作来告诉 Android 系统我们的意图。我们将通过告诉 Android 我们的意图来拨打电话,其余工作留给系统完成。以下代码段允许应用程序启动已预拨指定号码的拨号器,用户可以通过按下其中的拨号按钮明确地拨打电话:
你可以看到我们在代码中做了非常少的改动。我们声明了一个phoneNumber
字符串,用于存储我们想要拨打的号码。你可能会想知道为什么我们在字符串中拼接了一个tel:
前缀。这个前缀用于获取号码的通用资源标识符(URI)。我们通过调用Uri
类的静态方法Uri.parse()
来获取这个 URI。这个方法返回 URI,我们反过来将其传递给构造函数的另一个参数。我们通过在意图声明中传递Intent.ACTION_DIAL
来提供DIAL
动作,最后,我们像往常一样调用startActivity(intent)
方法来执行意图,并告诉 Android 系统处理我们的意图。以下屏幕截图显示了之前提到的代码段的拨号器结果:
通过启动 DIAL 意图来显示一个已拨号码的拨号屏幕
当我们运行之前的代码时,应用程序将启动 Android 手机的默认拨号器,并在其中拨打代码中提供的号码。它不会打电话;它只会拨号,因为我们使用了Intent.ACTION_DIAL
。用户可以明确地按下拨号器的拨号按钮并进行通话。
如果用户不想拨号,也可以直接调用号码而不先进入拨号界面。Android 为此提供了Intent.ACTION_CALL
动作。以下代码段展示了如何直接拨打电话:
你可以从代码中看出,除了在意图构造函数中传递的动作外,其他都一样。在最后一个示例中,我们传递了Intent.ACTION_DIAL
,而在这个例子中,我们传递了Intent.ACTION_CALL
来直接拨打电话。当我们运行这段代码时,应用程序将在 Android 手机上开始拨打电话。以下屏幕截图显示了通话:
通过启动CALL
意图来显示一个通话中的屏幕
这个ACTION_CALL
直接拨打电话的操作需要用户向应用程序授予权限。以下代码片段展示了需要放在AndroidManifest
文件中的权限,以使应用程序能够完美运行:
需要注意的是,ACTION_CALL
不能使用意图拨打紧急电话;然而,使用ACTION_DIAL
可以拨号紧急号码。如果用户在手机上安装了多个拨号器,ACTION_DIAL
操作将显示拨号器列表,用户可以选择一个喜欢的拨号器。以下截图展示了多个拨号器的场景:
从对话框中选择多个拨号器
ACTION_DIAL
和ACTION_CALL
意图之间几乎没有区别。ACTION_DIAL
意图只是拨号,用户可以通过按下拨号按钮明确地拨打电话,但ACTION_CALL
会直接拨打电话,而不会向用户显示拨号器。
注意
应用程序直接拨打电话可能会受到限制。因此,除非需要ACTION_CALL
,否则在应用中使用ACTION_DIAL
是一个好习惯。
这就是我们如何通过意图轻松拨打电话并使用 Android 的通话功能。在下一节中,我们将了解如何使用意图发送短信、彩信和数据消息。除了发送,我们还可以确认消息送达以及接收消息。接下来的一节,我们将详细讨论这些内容。
使用意图发送短信/彩信
除了拨打电话的功能外,手机还支持短信服务,如短消息服务(SMS)、多媒体消息服务(MMS)以及最近的数据消息。SMS/MMS 功能在手机中应用最广泛,许多人更喜欢使用它而不是拨打电话。Android 提供了 API 和框架,让开发人员能够从他们的应用程序中发送和接收消息。开发人员甚至可以替换原生的短信应用程序来发送和接收文本消息。
注意
在撰写本书时,还没有用于从应用程序内部发送彩信的 API 或库,但您可以使用ACTION_SEND
或ACTION_SENDTO
意图来发送。
本节将引导您了解各种操作,如使用意图发送短信、发送彩信、发送数据消息、确认消息送达以及接收短信。然后,我们将简要介绍所有这些操作在不使用意图的情况下是如何执行的,以及 Android 的 API 对我们有何帮助。接下来的一小节,我们将看看使用意图发送短信的任务。
使用意图发送短信
使用意图的最大优点是它将我们的需求责任传递给了 Android 系统,而不是我们从核心创建完整的功能。如果我们在我们当前的情况下使用意图,即给某人发送短信,我们只需提供要发送消息的号码和要发送的消息。其余的工作由 Android 自身完成。
关于使用意图发送或分享内容的话题,我们已经进行了很多讨论,幸运的是,这里没有什么是我们需要吸收的新知识。这是我们之前使用的创建ACTION_SEND
意图并调用startActivity(intent)
方法执行的老方法。以下代码片段展示了我们之前使用的ACTION_SEND
意图示例:
现在,如果我们使用这段代码,它对我们来说并不实用,因为它没有执行我们发送短信的动作。它既没有在选择器对话框中显示支持短信的应用,也没有使用EXTRA_TEXT
额外参数传递的数据发送任何短信。为了使用ACTION_SEND
发送短信,我们需要注意一些额外的事项。使用意图发送短信有两种方法:通过ACTION_SEND
意图和通过ACTION_SENDTO
意图。让我们看看如何使用ACTION_SEND
意图发送短信。
我们需要使用ACTION_SEND
动作创建意图,并在其中嵌入消息的额外参数"sms_body"
。Android 会自行向用户询问接收者的电话号码。但由于我们还未指定意图的类型,所以在选择器列表对话框中仍然不会显示任何短信支持应用。由于短信是短文本消息,我们应该将意图类型设置为"text/html"
,但由于大多数短信应用对彩信(MMS)没有原生支持,它们会寻找"image/jpg"
或"image/png"
作为意图类型。因此,将意图类型设置为"image/png"
后,我们将得到以下代码片段:
当我们执行这段代码时,我们会看到包括短信支持应用、电子邮件应用等在内的各种应用的选择器对话框。当我们选择任何短信应用时,我们会看到类似于以下图片的内容:
发送短信意图后显示默认短信应用
您可能已经注意到,短信应用中的文本部分已经用我们在"sms_body"
额外参数中添加的内容填充,用户正在输入消息的接收者数量。
注意
前面的图片展示了 QMobile Noir A10 智能手机的默认短信应用。您的设备将显示您设置为手机默认的短信应用,当然,它不会和这个应用相同。
这样我们就可以使用意图发送短信了。现在,让我们考虑另一个情况,我们希望通过编程设置收件人的数量。为此,我们必须使用ACTION_SENDTO
意图而不是ACTION_SEND
意图。以下代码段展示了ACTION_SENDTO
意图的使用:
在前面的代码中,你可以看到我们在之前发送 SMS 消息的代码中做了一些更改。我们将动作设置为ACTION_SENDTO
,而不是ACTION_SEND
。同时,在意图的构造函数中传递了另一个电话号码 URI 的参数。我们为电话号码创建了一个字符串,并在号码前拼接了"sms:"
标签。这个标签让Uri
类明白该字符串代表要发送消息的电话号码,并相应地解析它。你可能还记得,在上一节中,我们使用"tel"
标签通过意图拨打任何号码。当你执行代码时,它会要求应用程序选择短信应用。选择任何支持 SMS 的应用程序后,它会直接将短信发送到提供的电话号码,而不是像上一个示例中那样询问电话号码。你可能已经注意到,在这段代码中我们没有设置意图的类型。这是因为当我们使用ACTION_SENDTO
意图时,我们不需要明确设置意图的类型。Android 会从如"sms"
或"tel"
的标签以及如ACTION_SENDTO
或ACTION_CALL
的动作中理解开发者试图做什么。
注意事项
如果你想要使用ACTION_SEND
并明确使用代码设置收件人号码,Android 提供了"address"
额外项,无需使用任何如"tel"
或"sms"
的标签,就可以在其中放入号码字符串。
到目前为止,我们讨论了使用ACTION_SEND
和ACTION_SENDTO
发送 SMS 文本消息。在下一节中,我们将看到如何使用意图发送包含嵌入图片的多媒体消息。
使用意图发送 MMS
文本消息与多媒体消息唯一的区别在于嵌入的富媒体内容。MMS 消息包含照片、视频和贺卡等富媒体内容,以及一些作为内容消息的文本。目前,Android 没有提供让开发者原生发送 MMS 的库,与 SMS 不同。但幸运的是,意图为我们提供了一种明确的方式来发送 MMS。正如真正的区别所定义的,我们需要在文本消息意图中添加一些媒体,并将其类型设置为多媒体,如"image/png"
,然后我们就完成了 MMS 消息的发送。以下代码段展示了如何使用用于 SMS 消息的意图发送任何 MMS 消息:
你可以看到代码有两部分。在第一部分,我们获取了存储在外部存储的images
文件夹中所需图像的 URI。在第二部分,我们使用ACTION_SEND
创建意图。然后,我们使用"sms_body"
额外项添加我们的文本,并将类型设置为"image/png"
,使其对多媒体消息有意义。之后,我们使用Intent.EXTRA_STREAM
额外项附加我们的媒体,并在其中传递我们的图像 URI 作为值。最后,我们通过调用startActivity(intent)
方法执行意图。唯一的区别是使用EXTRA_STREAM
额外项附加媒体 URI,其余与短信中的操作相同。你也应该注意,我们可以使用ACTION_SENDTO
指定收件人号码,或者我们也可以使用电话号码作为值的"address"
额外项。
注意
在上一个示例中,我们已经设置了"image/png"
的类型。这只能发送 PNG 图像。对于其他图像格式,我们可以指定"images/*"
。
到目前为止,我们只讨论了发送短信和彩信。但我们确定这些消息已经成功送达了吗?好吧,下一节是关于确认消息送达和了解意图在其中扮演的角色。让我们在下一节中看看如何使用意图来确认消息的送达。
使用意图确认消息送达
当我们使用意图发送短信时,无论是短信还是彩信,我们都无法追踪这些消息以执行操作,例如确认送达。背后的原因是隐式地使用意图,并依赖于安卓系统的默认操作。如果我们使用意图发送消息,这意味着我们将发送消息的责任交给了安卓系统。现在,如果我们想要确认消息的送达状态,这意味着我们正在向安卓系统询问我们的消息。不幸的是,要实现这一点,我们缺少两样东西:一是告诉安卓系统我们确认了某条消息,二是安卓系统可能不记得我们谈论的是哪条消息。
为了能够确认送达,我们必须手动使用原生 API 发送消息。这个 API 的任务是跟踪我们正在谈论的消息的送达状态和消息本身。此外,使用这个 API,我们可以轻松发送查询,询问安卓系统关于送达确认的信息。
现在,如果我们使用原生 API 发送消息,我们必须考虑彩信。如前所述,对彩信的原生支持仍然不存在;因此,我们将无法追踪和确认彩信的送达,但我们可以确认短信的送达状态。在本节中,我们将讨论如何使用原生短信 API 检查短信送达状态,以及如何使用意图来实现我们的目标。
意图在 Android 中是一种异步的通信方式,并且被广泛使用。唯一的变化是,它们被用来实现我们的目标和完成需求。关于确认消息送达状态的简短解释,我们将使用名为SmsManager
的原生短信 API,通过SmsManager.sendTextMessage()
方法发送文本消息。为了跟踪消息,我们将使用两个意图:一个用于发送动作,另一个用于送达动作。除了这两个意图,我们还将创建两个挂起的意图:一个用于发送动作,另一个用于送达动作。最后,为了执行所有四个意图,我们将创建两个广播接收器:一个检查发送动作,另一个检查送达动作。这里看起来可能相当复杂,但实际上非常简单。让我们看看声明我们四个意图的代码片段:两个意图和两个挂起意图:
你可以看到,我们在代码中以常规方式声明了我们的意图。这里的唯一区别是,我们使用了代表字符串的自定义动作,如"sent_sms_action"
或"delivered_sms_action"
。然后,我们使用PendingIntent
类的getBroadcast()
工厂方法创建了两个挂起意图。getBroadcast()
方法将检索到将执行任何广播的PendingIntent
,例如调用Context.sendBroadcast()
方法。
因此,在创建完所有四个意图之后,我们现在需要创建并注册广播接收器,这些接收器将执行挂起的意图。以下代码片段展示了两个接收器的实现:
如前一段代码所示,我们使用Activity.registerReceiver()
方法注册了两个广播接收器,并传递了匿名对象。重写的方法onReceive()
实现了我们的目的。当一个消息被发送时,会调用一个onReceive()
方法;当一个消息被送达时,会调用另一个onReceive()
方法。我们在代码中加入了注释,以展示你可以在哪里使用自定义功能。你可能想知道 Android 如何知道这些是用于发送和送达状态的广播接收器。通过检查意图过滤器,Android 会知道这一点。你可以看到,我们在意图过滤器的构造函数中传递了我们在意图中使用的自定义动作,这些过滤器将过滤广播,接收器只接收它注册的广播。到目前为止,我们已经为确认消息送达做了核心工作。现在剩下的就是去执行它,这时SmsManager
API 就派上用场了。我们将创建一个SmsManager
的实例,并调用其sendTextMessage()
方法来发送消息,并放入所有意图,然后我们就完成了。以下代码片段展示了SmsManager
的使用代码:
请记住,SmsManager
API 使用了android.permission.SEND_SMS
权限;因此,不要忘记在您的清单文件中添加它,如下面的代码片段所示:
这样,我们就能确认消息的送达。我们只能确认文本消息的送达状态,并且需要用户授予SEND_SMS
权限。但是,如果我们使用意图,我们只能发送消息,并且不需要用户任何权限。
注意
安卓模拟器支持发送和接收短信。这可以通过创建多个模拟器实例并向模拟器的端口号发送短信来实现。
总结意图在确认消息送达中的作用,意图在这里并不是执行确认消息送达的核心动作。它们只是提供了一种通信方式,携带必要的信息,比如哪条消息的送达需要被检查等。然后,这些意图被广播接收器使用,它们不断检查消息的送达和发送状态。一旦完成,它们将状态传递到我们的意图中,然后这些意图为我们提供消息是否已发送或送达的更新。
在下一节中,我们将做几乎相同的事情和编码,但这次,我们将这样做是为了接收消息。使用所有这些代码片段后,我们可以开发出能发送和接收消息的短信应用。让我们看看如何接收消息以及意图在背后的作用。
使用意图接收短信
迄今为止,我们讨论了发送短信/MMS 消息以及在应用程序中意图的重要性。在本节中,我们将讨论如何监听传入的消息,以便我们能在应用程序中使用它们。利用这个功能,我们可以开发消息传递应用。意图可以使用ACTION_SEND
或ACTION_SENDTO
意图直接发送消息,但这些意图并不直接参与监听传入消息和接收消息。意图与Broadcast Receiver
的使用方式相同,用于获取发送者号码、消息内容、消息时间等信息。在讨论如何监听传入消息之前,我们必须了解一些在以下应用程序中使用的类。
SmsManager 类
在前面的子节中,我们已经使用了SmsManager
类来确认消息的送达。这个类用于管理如发送数据、短信和 PDU 消息等 SMS 操作。我们不能通过构造函数实例化这个对象;我们可以通过调用SmsManager.getDefault()
静态方法来获取它的实例。我们可以使用这个类来发送消息。
注意
有两个不同类名为SmsManager
:android.telephony.SmsManager
和android.telephony.gsm.SmsManager
。在 GSM 包中的后者在 API 级别 4 及以后版本中已被弃用。
SmsMessage 对象
这个类代表了一个简单的短信消息对象。在接收到来电消息时,我们将得到一个SmsMessage
对象的数组。这个类用于获取诸如消息正文、消息时间、发送者号码等信息。
协议数据单元(PDU)
PDU 是短信消息的行业格式。开发者无需担心详细阅读 PDU 或理解其格式,因为 Android 的SmsManager
类会读取和写入 PDU,并为开发者提供使用 PDU 的方法。
这些类和概念将在接收来电消息的应用中使用。现在,让我们讨论一下在 Android 中是如何接收消息的。当任何设备接收到新的短信消息时,会触发一个新的广播意图。这个意图的动作是android.provider.telephony.SMS_RECEIVED
。我们必须创建一个自定义的广播接收器,它将寻找这个广播意图。每当我们收到任何消息时,广播接收器的onReceive()
方法将被调用。以下代码段展示了我们为来电消息实现的自定义广播接收器:
public class IncomingMsgReceiver extends BroadcastReceiver {
private static final String SMS_RECEIVED = "android.provider.
Telephony.SMS_RECEIVED";
public void onReceive(Context _context, Intent _intent) {
if (_intent.getAction().equals(SMS_RECEIVED)) {
// SMS Received. Write your code here.
Bundle msgBundle = _intent.getExtras();
getMessageData(msgBundle);
}
}
}
与往常一样,我们的类从BroadcastReceiver
扩展而来,并覆盖了onReceive()
方法。当设备接收到任何来电消息时,会调用这个方法。我们首先检查这个意图是否包含任何收到的消息。如果意图动作与我们的SMS_RECEIVED
字符串字面量相同,这意味着我们已经收到了消息。
注意
在 Android 中,收到的短信动作android.provider.Telephony.SMS_RECEIVED
是不被支持的,并且在未来的平台版本中可能会发生变化。开发者在使用这些 Android 中不被支持的隐藏方法和属性时应谨慎。
在检查并确认动作后,我们必须从意图中获取消息数据并执行应用程序的自定义操作。我们首先通过调用getExtras()
方法从意图中获取 extras bundle,然后我们将这个 bundle 传递给我们的getMessageData()
方法。这是我们自定义的方法,在这个方法中,我们将了解如何从 bundle 中获取消息数据。以下代码实现展示了方法的定义:
我们首先检查我们的 bundle 不是一个 null 对象。然后我们通过调用get()
方法并传递"pdus"
键从 bundle 中提取 PDUs。
注意
如果你不清楚在get()
方法中应该传递哪个键,你可以调用Set<String> Bundle.keySet()
方法来获取 bundle 中使用的所有键。
回顾 PDU,PDU 是短信的行业格式。一旦我们有了数组中的所有 PDU 对象,我们就可以使用SmsMessage.createFromPdu()
方法从这些 PDU 创建短信。创建所有消息后,我们遍历数组,并使用SmsMessage.getMessageBody()
、SmsMessage.getOriginatingAddress()
和SmsMessage.getTimestampMillis()
方法从其中获取消息数据,如消息正文文本、消息发送者号码和消息时间。现在,我们可以将这些数据字符串用于我们的应用程序。需要注意的是,任何长消息都会被分解成许多小消息,这就是为什么我们得到对象数组的原因。
除非我们在应用程序中注册此广播,否则它不会工作。要在我们的应用程序中注册它,我们必须在主活动中编写以下代码:
这里没有新的讨论内容。我们正在创建一个带有SMS_RECEIVED
动作和广播接收器实例的意图过滤器。然后,我们将它们都传递给活动的registerReceiver()
方法。消息接收器需要android.permission.RECEIVE_SMS
权限;因此,不要忘记在您的清单文件中添加这一行:
这样,我们就可以在应用程序中接收来电消息,并以多种不同的方式使用它们。您可能想知道意图在这个应用程序中的作用。如前所述,这个应用程序中没有直接使用意图。当设备收到任何消息时,会触发一个广播意图。我们使用该意图从中提取数据和消息,这些消息在我们的应用程序中使用。在 Android 设备接收到消息后,意图的作用就是提供关于消息的数据。
我们可以使用 Android 调试工具中的Dalvik 调试监控服务器(DDMS)面板,在 Android 模拟器上模拟来电消息。以下是 DDMS 视图中用于模拟消息的模拟器控制面板的屏幕截图:
在 DDMS 视图中用于模拟消息的模拟器控制面板
在本节中,我们了解了发送短信、彩信、确认消息送达以及接收来电消息。我们还讨论了在这些应用中使用意图的重要性和方法。在下一节中,我们将学习关于通知的知识,以及如何在制作交互式通知时使用意图。
使用意图的通知
从传统手机到智能手机,每款手机都使用某种方式来通知和提醒用户某些事件,如接收短信或电话。与这些手机一样,Android 手机使用通知系统来提醒用户。通知是显示在应用程序正常 UI 之外的消息。当触发任何新的通知时,它会在通知区域显示。用户可以通过向下拉动手势来随时从通知抽屉和通知区域查看通知。以下屏幕截图展示了 Android 中两种不同通知的示例:
Android 手机中的通知
通知就像是在用户忙于其他移动活动(如玩游戏)时,发生重要事件时提醒用户的渠道。
对于任何开发者来说,通知是开发者显示在应用正常用户界面(UI)之外的一个用户界面元素,用以指示并告知用户某个事件已经发生。然后,用户可以在使用其他应用时选择查看通知,并在他们希望的时候进行响应。使用通知是让不可见的应用组件(如广播接收器和服务)告知用户任何事件发生的首选方式。
在本节中,我们将讨论通知、它们的布局、在通知布局中显示附加信息以及启动意图。我们将了解意图的作用,并创建一个具有自定义通知布局的示例应用程序,以及意图在这类应用程序中的重要性。在我们开始开发示例应用程序之前,让我们讨论通知中使用的某些基本概念。
通知形式
通知可以采取不同的形式,例如状态栏中的任何持久图标,可以通过启动器访问。当用户选择此通知时,当发生某些活动或服务时,会触发任何指定的意图。通知还可以用来打开设备的闪烁 LED 灯。此外,设备在接收到通知时也可以振动或播放铃声。
NotificationManager 类
NotificationManager
类代表了一个用于处理 Android 中通知系统的系统服务。我们不能实例化这个类,但可以通过调用getSystemService()
方法并传递Context.NOTIFICATION_SERVICE
来获取它的实例对象。以下代码段展示了如何获取NotificationManager
类的实例:
Notification 类
Notification
类代表了 Android 中的任何通知。它提供了 API,允许开发者设置通知的图标、标题、时间等。以下代码段展示了如何在 Android 中创建一个通知:
通知布局
每个通知都有一个图标和滚动文本,有时称为状态文本。当启动通知且通知抽屉关闭时,会显示图标。当触发通知时,滚动文本会在状态栏中滚动,当打开通知抽屉时,它会被设置为通知信息文本。下面的截图概述了通知区域的不同方面:
通知和通知区域
你可以从前面的截图中看到,当触发任何通知时,其滚动文本会在状态栏中滚动。滚动完整个文本后,其图标将显示在状态栏上。当用户通过下拉打开通知抽屉时,将显示通知的大图标以及通知标题、内容文本和时间戳。这就是在 Android 中触发任何通知的方式。
现在,我们将讨论通知是如何触发的以及通知应用中意图的使用方法。我们将创建一个通知,如下面的截图所示:
一个简单的通知
因此,在创建通知之前,我们需要通知的布局。以下代码实现展示了我们通知的布局:
我们在RelativeLayout
中放置了四个视图:一个用于图标的ImageView
,一个用于标题的大型TextView
,以及两个分别用于描述和时间戳的小型TextViews
。我们使用了RelativeLayouts
的对其方式,将视图放置在其他视图的下方、上方、右侧和左侧,以便在不同分辨率智能手机上以相同方式显示。我们将此文件保存为资源目录中 layout 文件夹的notification_layout.xml
。这就是我们的通知布局。现在,让我们学习如何创建使用此布局的任何通知。
要创建具有自定义布局的通知,我们在 Android 中有两种不同的方法。第一种方法是使用setLatestEventInfo
方法更新标准扩展状态通知显示中显示的详细信息。这是最简单的方法,在更多应用中使用。另一种方法是设置通知的contentView
和contentIntent
属性,以使用RemoteView
类为扩展状态显示分配自定义 UI 布局。
注意
RemoteView
是一种机制,允许开发者在任何独立应用程序中嵌入和控制布局。这通常用于创建主屏幕小部件。
在本节中,我们将使用一个复杂的方法来创建通知,因为此方法在代码中使用了意图。我们首先创建一个RemoteView
对象,并将其分配给通知对象的contentView
属性。contentView
视图表示在展开状态栏中的通知。通知通常表示对操作的请求,当用户点击通知抽屉区域或展开状态栏中的通知时,执行此操作。我们可以指定当用户点击通知项时将被触发的PendingIntent
。通常,此意图会打开我们的应用程序,并提供有关我们通知的更多信息。除了设置contentView
之外,我们还需要将contentIntent
设置为我们创建的PendingIntent
对象,其中自定义内容视图被分配给我们的通知。contentIntent
意图是在点击展开状态条目时必须执行的目的地。如果这是活动的意图,我们必须包括FLAG_ACTIVITY_NEW_TASK
,这将使我们的活动在新任务中启动。
注意
当你手动设置contentView
属性时,还必须设置contentIntent
属性;否则,在触发通知时将抛出异常,导致应用程序的任何运行时崩溃。
一旦将contentView
属性设置为我们的自定义远程视图,我们就不能以常规方式设置所需视图。我们必须使用RemoteView
对象上的 set 方法,这些方法会修改在定义的布局中使用的每个视图。这就是任何具有自定义布局的通知的开发方式。以下代码展示了具有自定义布局通知的实现,这可以添加到任何活动中:
从代码中可以看出,我们首先创建了一个Notification
对象,带有初始图标、ticker 文本和触发通知的时间。然后,我们创建意图对象,Intent
和PendingIntent
,用于指定当通知被点击时的动作。接着,我们设置了通知对象的contentIntent
和contentView
。我们为contentView
创建了一个新的RemoteView
对象,并在其中传递了我们的notification_layout.xml
引用。这就是通知布局通过RemoteView
构造函数设置为我们的自定义布局的方式。然后,我们将我们的待定意图设置为contentIntent
。最后,我们使用 set 方法更新我们布局的值,如setImageViewResource()
和setTextViewText()
。到目前为止,我们已经使用自定义布局开发了自己的通知。现在,我们将了解如何触发通知。以下代码段展示了如何触发通知:
我们通过调用getSystemService()
方法来获取NotificationManager
类的实例。为了触发通知,我们调用了NotificationManager.notify()
方法,该方法接收两个参数:第一个是通知的 ID,第二个是通知对象本身。以下屏幕截图显示了应用程序的输出:
来自我们应用程序的通知触发
到目前为止,我们已经了解了如何创建通知并为它们的视图设置自定义布局。你可能正在思考意图在这个应用中的重要性及其用途。在这个应用中,意图仅用于一个目的,那就是当用户点击通知时,引导用户到我们所需的应用程序或活动。我们创建了一个Intent
对象,并由此生成了一个PendingIntent
对象,这个对象在通知中被用作contentIntent
。
概要总结
在本章中,我们讨论了 Android 的特性。我们了解了常见的 Android 特性,如布局、显示、连接性、通信、可访问性、触摸和硬件支持,以及它们与 Android 移动组件的比较。然后,我们看到了AndroidManifest
文件中两个最重要的标签<uses-feature>
和<uses-permission>
的用途及其如何使用。
我们还讨论了硬件和软件特性与 Android 移动组件之间的关系,以及它们与这些清单标签的联系。然后,我们了解了最常见的意图动作ACTION_SEND
,它用于通过隐式意图的方法与其他应用程序分享或发送任何内容。接着,我们将意图的知识扩展到手机更具体的特性,包括拨打电话、发送短信/MMS、确认消息送达和接收消息。我们使用了意图以及 Android 的原生 API 来执行这些操作。然后,我们讨论了通知和警告,并学习了如何在通知中设置自定义布局。我们了解到了两种不同的方法,并在我们的示例应用程序中使用了一种方法。我们了解了这些类型的应用程序中意图的使用,也了解了它们与这些类的作用。
在下一章中,我们将讨论意图过滤器,并了解 Android 如何识别不同的意图并根据调用和应用程序对它们进行过滤。
第七章:意图过滤器
意图过滤器是理解安卓意图微小但重要细节的高级步骤。在本章中,我们将了解意图过滤器的基础知识以及如何在安卓应用程序中有效使用它们。本章还处理意图在传递到期望组件之前应通过的各类测试。
注意
意图过滤器可以在AndroidManifest.xml
文件中的activity
标签下找到。
在本章中,我们将涵盖以下主题:
-
意图对象及其分类
-
理解意图过滤器是什么
-
理解意图测试是什么
-
为特定任务实现意图过滤器
意图对象及其分类(重复,不翻译)
意图对象附带大量信息。这个信息包将帮助组件从中提取知识。例如,应该对随意图对象传递的数据采取什么类型的操作;同样,还有关于安卓系统的信息。当系统不知道将处理即将到来的意图的组件时,需要关于安卓系统的信息。
为了更好地理解前一段提到的示例,考虑一个意图被传递以启动电影的场景。在这种情况下,操作系统必须知道执行此动作需要哪个软件。
下文将讨论安卓意图对象中包含的分类。
组件名称
意图对象包含将处理数据的相关组件名称信息。通常,此组件由完整的类名组成,例如com.app.demoactivity.MyActivity
或com.example.demoactivity.MainActivity
。对于意图对象来说,此组件信息是可选的。如果意图对象知道该组件,安卓会将数据处理指向那个特定的组件;如果不知道,安卓会确定哪个组件最适合处理此事件。
注意
组件名称中的包部分不一定与AndroidManifest.xml
文件中的项目名称相同。
组件名称通过安卓 API 提供的setComponent()
或setClassName()
方法设置。
意图解析
安卓意图分为两部分(如第三章所述,意图及其分类),隐式意图和显式意图。对于显式意图,不为其分配组件名称并不会造成问题,因为必须将组件包含在意图对象中,然后安卓会自动将显式意图指向所描述的组件。
另一方面,在隐式意图中,如果没有向 Android 系统提供组件名称,它将自动将其指向所有可能处理此传入意图的应用程序。只有当意图具有意图过滤器时,此操作才会发生,否则 Android 不会引导它。这个术语称为 Android 意图解析;当你不需要为隐式意图定义组件时,它将自动显示所有可能接收此意图的应用程序列表。
动作
动作是一个描述在意图上要执行操作的字符串。例如,ACTION_CALL
、ACTION_BATTERY_LOW
和 ACTION_SCREEN_ON
。你可以在 developer.android.com/reference/android/content/Intent.html
找到其他各种动作的常量。你也可以创建自己的意图动作,但请确保在前面加上项目名称,例如 com.example.myproject.SHOW_CONTACT
。当开发者想要创建一个之前未添加到 Android SDK 的事件时,需要自定义动作。当开发者想要触发/检查与该应用程序紧密相关且不在 Android SDK 中的动作时,也可能出现这个需求。
注意
com.example.XXX
是在 Java 和 Android 应用程序开发中不推荐使用的包名。这确保了使用这个包大多是因为理解了本例中的用途。
动作通常可以告诉你意图是如何构建的,尤其是数据和额外信息。它就像方法的现象,有参数并且返回值。最佳实践是始终尽可能具体地使用你的动作名称,并与意图紧密耦合。意图动作可以通过使用 Android API 的 setAction()
方法来设置,你可以通过 getAction()
方法获取它。
下表给出了一些预定义的意图动作常量:
常量 | 组件关系 | 动作 |
---|---|---|
ACTION_CALL | 活动 | 初始化一个电话通话 |
ACTION_EDIT | 活动 | 显示用户数据以进行编辑 |
ACTION_MAIN | 活动 | 作为初始活动启动,无需数据输入,也没有返回输出 |
ACTION_SYNC | 活动 | 在服务器上与移动设备上的数据同步 |
ACTION_BATTERY_LOW | 广播接收器 | 电池电量低的警告 |
ACTION_HEADSET_PLUG | 广播接收器 | 耳机插入设备 |
ACTION_SCREEN_ON | 广播接收器 | 屏幕已开启 |
数据
在 Android 意图中,根据提供的数据类型,采取不同类型的动作。数据是 Android 意图的基本部分之一,尤其是在隐式类别中。让我们通过一些例子来更好地了解如何在 Android 意图中使用与其相关动作的数据。
在ACTION_EDIT
中使用数据
考虑一个ACTION_EDIT
的例子。每当我们在意图中调用这个动作时,很明显,编辑功能需要在某种文档中实现。这个文档路径应以 URI 的形式给出,然后由 Android 意图处理。这个 URI 实际上是我们要放入意图对象中的数据部分。
当开发者希望打开默认的 Android 的添加新联系人屏幕并期待用户进行编辑时,可以使用ACTION_EDIT
。在这种情况下,用于打开添加新联系人屏幕的意图应该定义ACTION_EDIT
动作。
在ACTION_CALL
中使用数据
考虑另一个ACTION_CALL
的例子。当我们需要通过意图执行呼叫功能时,使用此动作。因此,为了完成这项任务,我们需要通过使用tel://
URI 引用来提供电话号码。这是需要与意图一起提供的数据集,以便 Android 知道需要执行拨号功能的数据。
在ACTION_VIEW
中使用数据
接下来是我们的第三个例子,即ACTION_VIEW
。在大多数情况下,当调用此动作时,会通过 URI 链接到一个网站。这帮助 Android 了解要在其上执行查看动作的数据。通常,ACTION_VIEW
动作会附加一个http://
URI,以便 Android 可以处理查看任何网页的功能。
类别
这是向意图提供的信息,以便了解执行该特定意图所需的最佳组件类型。例如,如果我们想使用ACTION_VIEW
动作查看一个网页,我们可以将其类别指定为CATEGORY_BROWSABLE
,以让 Android 知道与意图关联的数据是安全的,并且可以通过 Android 浏览器轻松执行。
在任何 Android 程序中都可以轻松使用的类别常量列表如下:
常量 | 说明 |
---|---|
CATEGORY_BROWSABLE | 活动可以在与意图关联的数据的 Android 浏览器上安全执行。 |
CATEGORY_GADGET | 活动与任何 Android 设备托管的另一个活动相关联。 |
CATEGORY_HOME | 活动显示主屏幕,或者当按下主页按钮时用户看到的第一个屏幕。 |
CATEGORY_LAUNCHER | 特定活动的类别是启动器,这意味着它将成为堆栈顶部的活动。 |
CATEGORY_PREFERENCE | 目的地活动来自偏好设置面板。 |
附加项
在前面的章节中,我们已经详细了解了 extras 功能以及如何与意图一起使用。与数据类似,一些 extras 与将要启动的意图绑定在一起。例如,ACTION_HEADSET_PLUG
动作具有额外的"State",用以指示耳机是否已连接到手机。
这些方法与 bundle 对象的方法类似。因此,可以使用putExtras()
和getExtras()
方法将 extras 安装和读取为 bundle。
意图过滤器
在此时刻,我们对 Android 意图及其实现有了完美的理解。Android 意图负责告诉 Android 已经发生了某个事件,它也用于提供额外数据,以执行某些特定动作。但是 Android 如何知道哪个组件可以促进任何意图的执行呢?为此,引入了意图过滤器的概念。意图过滤器用于识别哪些组件可以响应活动、服务或广播意图的特定调用。
通常,意图过滤器通过AndroidManifest.xml
文件提供给活动或服务,该文件包含动作、数据和类别测试。对于广播接收器,意图过滤器也可以通过代码动态定义。
对于隐式意图,为了将其传递给特定组件,必须通过所有三个测试。基于这些情况,可能会出现两种条件:一种情况是意图没有通过任何一项测试,那么它将不会被传递给组件。另一种情况是,当它通过了所有测试,它将直接移交给相应的组件。在第一种情况下,有一个例外,如果没有通过测试,它可以被传递给同一活动的下一个意图过滤器。通过这种方式,它有可能按照预期执行。
提示
在AndroidManifest.xml
文件中,我们可以在一个活动中拥有多个意图过滤器。
具有内部意图过滤器的活动的普通 XML 标签如下所示:
如代码所示,它由一个包含所有内容的 activity 标签组成。这个活动仅包含一个意图过滤器,其中包含两个主要组件:动作和类别。此意图执行时要采取的动作是 android.intent.action.MAIN
,通过调用此动作,任何对活动的先前引用都会被移除,活动会以全新的启动执行。这样,类别被设置为 android.intent.category.LAUNCHER
;这表明在 AndroidManifest
文件中编写的活动是启动器的 activity
标签。这意味着,一旦执行应用程序,它就是第一个要启动的活动。如果在 AndroidManifest.xml
文件中有两个或更多描述为启动器的活动,Android 操作系统将询问用户要从哪个活动开始。
注意
<intent-filter>
是 AndroidManifest.xml
文件的一部分,而不是 Java 代码,因为它包含的信息需要在项目应用程序启动之前获取。例如,在项目应用程序启动之前,需要确定它是否为启动器活动。由于 AndroidManifest.xml
文件在项目应用程序启动之前执行,以便提取有关项目的信息,意图过滤器是此文件的一部分。唯一的例外是在广播意图的情况下,其信息可以从 Java 代码动态修改,而不是从 AndroidManifest.xml
文件。
处理多个意图过滤器
并非强制要求任何 Android 活动只能有一个意图过滤器。一个活动可能包含多个意图过滤器,这些过滤器占据了诸如类别、数据和动作等多个子组件。请看下面的截图,展示了带有不同类型参数的两个意图过滤器:
前述截图中提到的代码解释将在本章后续内容中介绍。目前,了解活动内部各种意图过滤器的实现是重要的。
意图过滤器的测试组件
过滤器是意图对象中动作、数据和类别字段的代表。每当调用隐式意图时,都会针对这些过滤器进行测试,以便执行。如果该意图不满足任何一个测试组件,它将不会执行,或者更确切地说,它将被引导到同一活动的另一个意图过滤器(如果存在的话)。
现在,为了正确理解意图过滤器的意图,我们需要逐步评估与意图过滤器相关的每个测试组件。存在三个测试组件:
-
动作测试
-
数据测试
-
类别测试
动作测试
操作描述了即将由传入意图执行的操作类型。AndroidManifest.xml
文件确定了传入意图需要满足的要求。如果任何意图无法匹配AndroidManifest.xml
文件中指定的操作,它将不会被执行。
操作测试基本上是由项目清单文件中提供的信息来执行的一个测试。所有的操作组件都在<intent-filter>
标签内定义,然后通过匹配来执行意图。在下面的截图中,你可以看到操作测试时intent-filter
标签的样子:
在前一个截图中给出的代码中,intent-filter
标签内列出了三个操作。这些操作测试将由 Android 操作系统确定,如果传入的意图能够执行这些操作。在前面的代码中列出了三个测试,你可以在Action部分给出的表格中看到它们的描述。需要遵循以下两个条件:
如果intent-filter
标签内没有写入操作,Android 操作系统将拒绝处理意图,因为没有可匹配的内容。
如果intent-filter
标签包含多个操作,但传入意图中没有列出任何操作,意图仍将毫无问题地继续执行。
<action>
的编写约定
在定义操作时,Android 遵循一些约定。需要记住,对于默认操作,我们必须使用 Android API 中给出的预定义常量。在 Android 库中,每个操作字符串都以ACTION_
开头,然后是实际的操作名称,例如ACTION_MAIN
、ACTION_TIME_ZONE_CHANGED
和ACTION_WEB_SEARCH
。
同样,在提到需要在AndroidManifest.xml
文件中包含此字符串的约定时,Android 遵循android.intent.action.STRING
模式。在这个语句中,单词STRING被替换为要匹配的具体操作,但不包含单词ACTION。为了理解这个语句,以ACTION_MAIN
常量为例。如果我们想在AndroidManifest.xml
文件中提到它,我们不会写ACTION_
,而是会这样写:
ACTION_EDIT
也是同样的情况,它使 Android 能够编辑 URI 中给出的任何文档的引用。我们将在下面的截图中编写代码,以便在AndroidManifest.xml
文件中容易理解:
当涉及到自定义动作时,动作是由用户定义而不是 Android API。有一个最佳实践是在编写之前始终以您的包名开始,以保持其唯一性。例如,如果您想创建一个名为HIDE_OBJECTS
的动作,您必须在 XML 文件中编写如下截图所示的代码:
类别测试
为了通过类别测试,必须确保传入的意图类别与AndroidManifest.xml
中的<category>
标签内至少提到的一个类别相匹配。如果在不知道其中类别的情况下创建意图对象,它应该总是通过,无论清单文件中定义了哪些类别。
需要记住,如果我们想使用startActivity()
方法在活动之间移动,那么愿意接收隐式意图的活动在AndroidManifest.xml
文件中必须有一个默认类别,即CATEGORY_DEFAULT
(如 Android API 中所提及)。
提示
这与为动作编写约定一样,类别应该写成android.intent.category.DEFAULT
,在AndroidManifest.xml
中不提CATEGORY_
字符串。
然而,对于启动类别来说,这不是情况;它是一个例外。我们在启动活动标签中提到android.intent.category.LAUNCHER
。类别测试的表示在下图中显示:
在前一个截图给出的代码中,提到了两个类别。第一个类别是android.intent.category.DEFAULT
,这是因为这个特定的活动已经准备好接收隐式意图。在清单文件中提到的另一个类别是android.intent.category.BROWSABLE
,它使此活动能够浏览手机中的本地 Android 浏览器或其他用于浏览网站的应用程序。
设置启动活动
设置启动活动主要是类别的一部分。在这里,我们需要确保我们完全理解了与意图相比启动活动的异常情况。由于众所周知,启动活动是在应用程序首次启动后立即启动的活动,因此我们现在可以在此基础上深入了解其类别概念。如果已知活动将接收某些隐式意图,则使用DEFAULT
类别;另一方面,LAUNCHER
活动是在任何应用程序中首次启动的活动。
在这个意义上,没有一个启动活动可以同时是默认的。结论是,在AndroidManifest.xml
中,没有任何活动可以同时具有android.intent.category.DEFAULT
和android.intent.category.LAUNCHER
。在清单中呈现的启动活动看起来像下面截图中的代码:
<activity android:name="com.example.android.application.MyList"
android:label="@string/title_my_list">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
在前述截图给出的代码中,活动com.example.android.application.MyList
是启动活动,它将在应用程序开始时生成一个列表。由于这是应用程序的主要入口点,我们在清单中提供了ACTION_MAIN
作为动作。而你可以看到第二个标签,提供的类别名称为android.intent.category.LAUNCHER
。
数据测试
提到数据标签是为了便于对执行的活动采取行动。这就是为什么在<activity>
标签内可以有多个数据标签的原因。标签包含有关特定 URI 或 MIME 媒体类型的信息。例如,一个活动可能包含如下截图所示的数据标签:
在前述屏幕截图给出的代码中,意图过滤器包含两个数据标签。在每一个标签中,android:mimeType
属性下给出的 MIME 媒体类型指定了活动支持特定操作的数据格式。video/avi
值描述了.avi
文件的视频格式,这是活动所支持的。同样,如果需要提及音频文件类型,我们可以使用audio/mpeg
。
我们也可以在视频或音频 MIME 类型后加上星号。例如,请看以下截图:
这段代码与之前的相同,除了video/*
和audio/*
MIME 类型。星号表示此活动支持它们所有可能的子类型。
现在,有一些要点我们需要确保:
-
如果一个意图对象不包含有关 URI 的任何特定信息,它只会在
AndroidManifest.xml
文件中没有提供数据信息的情况下通过intent-filter
标签。 -
如果一个意图对象只包含 URI 但不包含数据 MIME 类型,只有当它与过滤器中指定的 URI 匹配并且没有为数据类型指定过滤器时,它才会被传递。
-
如果一个意图对象只包含 MIME 类型,但不包含 URI,只有当它与过滤器中指定的 MIME 类型匹配并且没有为 URI 指定过滤器时,它才会被传递。
-
当一个意图对象包含 URI 以及 MIME 类型时,只有当它们与
AndroidManifest.xml
中指定的意图过滤器对应值匹配时,它才会被传递。
标签的典型表示
<data>
标签包含许多属性以使其信息完整。以下语法包含了可以在<data>
标签中定义的所有属性,这将在处理意图时增加对活动的了解:
在前一个屏幕截图给出的代码中,有各种属性,它们都是可选的,但它们相互之间更为依赖。以下是可选属性的列表:
-
scheme
-
host
-
port
-
path
-
pathPrefix
-
pathPattern
现在我们来谈谈它们之间的相互依赖关系。如果在数据标签中没有提到方案,那么在此之后将没有有效的 URI。同样,如果没有定义主机元素,所有的路径标签和主机标签值将无效。
概述
在本章中,我们详细地查看了意图过滤器和意图对象。我们了解了意图对象的基本构建块,在其中我们在 Java 代码中定义元素,而在另一方面,意图过滤器让 Android OS 了解应用程序内部的活动。我们学习了intent-filters
标签如何通过匹配传入的意图对象及其属性来工作。然后,它们决定是否应该执行意图。
我们还了解了动作、数据和类别以及它们是如何工作的。不同的数据、类别和动作如何在单个活动的不同意图过滤器中合并,以及如果存在多种过滤器选择,主要机制是什么。我们还研究了某些写作约定,在 Android Manifest 中编写启动活动的典型方式,以及当数据对格式的不同子类型有效时包含多少 MIME 类型。在下一章中,我们将看到意图如何与广播接收器一起使用,它们的实际示例以及可能因为它们而产生的问题类型。