安卓应用开发秘籍第二版(三)
原文:
zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1译者:飞龙
第六章:数据处理
在本章中,我们将涵盖以下主题:
-
存储简单数据
-
读写内部存储的文本文件
-
读写外部存储的文本文件
-
在项目中包含资源文件
-
创建和使用 SQLite 数据库
-
使用加载器在后台访问数据
简介
由于几乎任何大小应用都需要保存某种类型的数据,Android 提供了许多选项。从保存一个简单值到使用 SQLite 创建完整的数据库,存储选项包括以下内容:
-
共享偏好设置:简单的名称/值对
-
内部存储:私有存储中的数据文件
-
外部存储:在私有或公共存储中的数据文件
-
SQLite 数据库:私有数据可以通过内容提供者暴露数据
-
云存储:私有服务器或服务提供商
使用内部和外部存储有其优点和权衡。我们将在这里列出一些差异,以帮助你决定是使用内部存储还是外部存储:
-
内部存储:
-
与外部存储不同,内部存储始终可用,但通常可用空间较少
-
文件对用户不可见(除非设备拥有 root 权限)
-
当你的应用被卸载时,文件会自动删除(或者在应用管理器中使用清除缓存/清理文件选项)
-
-
外部存储:
-
设备可能没有外部存储,或者可能无法访问(例如连接到计算机时)
-
文件对用户(和其他应用)可见,无需 root 权限
-
当你的应用被卸载时,文件不会被删除(除非你使用
getExternalFilesDir()获取特定于应用的公共存储)
-
在本章中,我们将演示如何使用共享偏好设置、内部和外部存储以及 SQLite 数据库。对于云存储,请查看第十二章中的互联网食谱,电信、网络和互联网以及第十五章中的在线服务提供商,后端即服务选项。
存储简单数据
存储简单数据是一个常见需求,Android 使用偏好设置 API 使其变得简单。不仅限于用户偏好;你可以使用名称/值对存储任何原始数据类型。
我们将演示如何从EditText保存一个名字,并在应用启动时显示它。以下屏幕截图显示了应用首次启动时没有保存名字的样子,以及在保存名字后启动时的样子:
准备工作
在 Android Studio 中创建一个新项目,并将其命名为:Preferences。使用默认的手机 & 平板选项,在选择活动类型时选择空活动。
如何操作...
我们将使用现有的TextView显示欢迎回来的消息,并创建一个新的EditText按钮来保存名字。首先打开activity_main.xml文件:
-
替换现有的TextView并添加以下新的视图:
<TextView android:id="@+id/textView" android:text="Hello World!" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:id="@+id/editTextName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:hint="Enter your name" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" android:layout_centerHorizontal="true" android:layout_below="@id/editTextName" android:onClick="saveName"/> -
打开
ActivityMain.java文件,并添加以下全局声明:private final String NAME="NAME"; private EditText mEditTextName; -
在
onCreate()中添加以下代码,以便保存对EditText的引用并在加载已保存名称时使用:TextView textView = (TextView)findViewById(R.id.textView); SharedPreferences sharedPreferences = getPreferences(MODE_PRIVATE); String name = sharedPreferences.getString(NAME,null); if (name==null) { textView.setText("Hello"); } else { textView.setText("Welcome back " + name + "!"); } mEditTextName = (EditText)findViewById(R.id.editTextName); -
添加以下
saveName()方法:public void saveName(View view) { SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); editor.putString(NAME, mEditTextName.getText().toString()); editor.commit(); } -
在设备或模拟器上运行程序。由于我们要演示持久化数据,所以在
onCreate()期间会加载名称,因此保存一个名称并重新启动程序以查看加载过程。
它是如何工作的...
为了加载名称,我们首先获取对SharedPreference的引用,这样就可以调用getString()方法。我们传入名称/值对的关键字,以及如果找不到关键字时要返回的默认值。
为了保存首选项,我们首先需要获取对首选项编辑器的引用。我们使用putString()然后调用commit()。如果没有commit(),更改将不会被保存。
还有更多...
我们的示例将所有首选项存储在单个文件中。我们还可以使用getSharedPreferences()并传递名称,在不同文件中存储首选项。如果你想要为多个用户设置不同的配置文件,可以使用这个选项。
在内部存储中读写文本文件
当简单的名称/值对不够用时,Android 还支持常规文件操作,包括处理文本和二进制数据。
以下示例展示了如何将文件读取和写入内部或私有存储。
准备工作
在 Android Studio 中创建一个新项目,并将其命名为InternalStorageFile。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
如何操作...
为了演示读取和写入文本,我们需要一个带有EditText和两个按钮的布局。首先打开main_activity.xml文件,并按照以下步骤操作:
-
用以下视图替换现有的
<TextView>元素:<EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="textMultiLine" android:ems="10" android:layout_above="@+id/buttonRead" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Read" android:id="@+id/buttonRead" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="readFile"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Write" android:id="@+id/buttonWrite" android:layout_below="@+id/buttonRead" android:layout_centerHorizontal="true" android:onClick="writeFile"/> -
现在打开
ActivityMain.java文件,并添加以下全局变量:private final String FILENAME="testfile.txt"; EditText mEditText; -
在
setContentView()方法后,向onCreate()方法中添加以下内容:mEditText = (EditText)findViewById(R.id.editText); -
添加以下
writeFile()方法:public void writeFile(View view) { try { FileOutputStream fileOutputStream = openFileOutput(FILENAME, Context.MODE_PRIVATE); fileOutputStream.write(mEditText.getText().toString().getBytes()); fileOutputStream.close(); } catch (java.io.IOException e) { e.printStackTrace(); } } -
现在添加
readFile()方法:public void readFile(View view) { StringBuilder stringBuilder = new StringBuilder(); try { InputStream inputStream = openFileInput(FILENAME); if ( inputStream != null ) { InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String newLine = null; while ((newLine = bufferedReader.readLine()) != null ) { stringBuilder.append(newLine+"\n"); } inputStream.close(); } } catch (java.io.IOException e) { e.printStackTrace(); } mEditText.setText(stringBuilder); } -
在设备或模拟器上运行程序。
它是如何工作的...
我们使用InputStream和FileOutputStream类分别进行读取和写入操作。将文件写入操作简化为从EditText获取文本并调用write()方法。
读取内容会稍微复杂一些。我们可以使用FileInputStream类进行读取,但在处理文本时,辅助类会使操作更简单。在我们的示例中,我们使用openFileInput()打开文件,它返回一个InputStream对象。然后我们使用InputStream获取一个BufferedReader,它提供了ReadLine()方法。我们遍历文件中的每一行并将其附加到我们的StringBuilder中。当我们完成文件读取后,我们将文本赋值给EditText。
提示
我们之前的文件是在应用的私有数据文件夹中创建的。要查看文件内容,你可以使用 Android 设备监视器将文件拉取到你的电脑上。完整的文件路径是:/data/data/com.packtpub.androidcookbook.internalstoragetile/files/testfile.txt。
下面的屏幕截图显示了通过Android 设备监视器查看文件时的样子:
注意
你需要一个具有 root 权限的设备来查看之前显示的私有文件夹。
还有更多...
让我们看看一些可能有所帮助的额外信息。
缓存文件
如果你只需要临时存储数据,也可以使用缓存文件夹。以下方法返回缓存文件夹作为一个File对象(下一个食谱演示了如何使用File对象):
getCacheDir()
缓存文件夹的主要优点是,如果存储空间不足,系统可以清除缓存。(用户还可以在设置中的应用管理中清除缓存文件夹。)
例如,如果你的应用下载新闻文章,你可以将这些文章存储在缓存中。当你的应用启动时,可以显示已经下载的新闻。这些文件不是使你的应用工作所必需的。如果系统资源不足,可以清除缓存,而不会对你的应用产生不利影响。(尽管系统可能会清除缓存,但你的应用删除旧文件仍然是一个好主意。)
另请参阅
- 下一个食谱,读取和写入外部存储的文本文件。
读取和写入外部存储的文本文件
读取和写入外部存储的文件过程基本上与使用内部存储相同。区别在于获取存储位置的引用。另外,如介绍中提到的,外部存储可能不可用,因此在尝试访问之前最好检查其可用性。
这个食谱将读取和写入文本文件,就像之前的食谱中所做的那样。我们还将演示如何在访问之前检查外部存储状态。
准备工作
在 Android Studio 中创建一个新项目,将其命名为:ExternalStorageFile。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。我们将使用之前的食谱中的相同布局,所以如果你已经输入了,可以直接复制粘贴。否则,使用之前食谱中的第 1 步布局,读取和写入内部存储的文本文件。
如何操作...
如之前在准备工作部分提到的,我们将使用之前的食谱中的布局。布局文件完成后,第一步将是添加访问外部存储的写入权限。以下是步骤:
-
打开 Android Manifest 并添加以下权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
接下来,打开
ActivityMain.java并添加以下全局变量:private final String FILENAME="testfile.txt"; EditText mEditText; -
在
onCreate()方法中,在setContentView()之后添加以下内容:mEditText = (EditText)findViewById(R.id.editText); -
添加以下两种方法来检查存储状态:
public boolean isExternalStorageWritable() { if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { return true; } return false; } public boolean isExternalStorageReadable() { if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment.getExternalStorageState())) { return true; } return false; } -
添加以下
writeFile()方法:public void writeFile(View view) { if (isExternalStorageWritable()) { try { File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME); FileOutputStream fileOutputStream = new FileOutputStream(textFile); fileOutputStream.write(mEditText.getText().toString().getBytes()); fileOutputStream.close(); } catch (java.io.IOException e) { e.printStackTrace(); Toast.makeText(this, "Error writing file", Toast.LENGTH_LONG).show(); } } else { Toast.makeText(this, "Cannot write to External Storage", Toast.LENGTH_LONG).show(); } } -
添加以下
readFile()方法:public void readFile(View view) { if (isExternalStorageReadable()) { StringBuilder stringBuilder = new StringBuilder(); try { File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME); FileInputStream fileInputStream = new FileInputStream(textFile); if (fileInputStream != null ) { InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String newLine = null; while ( (newLine = bufferedReader.readLine()) != null ) { stringBuilder.append(newLine+"\n"); } fileInputStream.close(); } mEditText.setText(stringBuilder); } catch (java.io.IOException e) { e.printStackTrace(); Toast.makeText(this, "Error reading file", Toast.LENGTH_LONG).show(); } } else { Toast.makeText(this, "Cannot read External Storage", Toast.LENGTH_LONG).show(); } } -
在具有外部存储的设备或模拟器上运行程序。
工作原理...
对于内部和外部存储,读取和写入文件基本上是相同的。主要的区别在于,在尝试访问它之前,我们应该检查外部存储的可用性,这是通过isExternalStorageWritable()和isExternalStorageReadable()方法完成的。在检查存储状态时,MEDIA_MOUNTED意味着我们可以读取和写入它。
与内部存储示例不同,我们请求工作路径,就像在这行代码中所做的那样:
File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME);
实际的读写操作是由相同的类完成的,因为只是位置不同。
提示
硬编码外部文件夹路径是不安全的。该路径可能会因操作系统的版本不同而有所差异,尤其是在不同硬件制造商之间。最佳的做法是调用getExternalStorageDirectory(),如所示。
还有更多...
以下是一些额外的信息讨论。
获取公共文件夹
getExternalStorageDirectory()方法返回外部存储的根目录。如果你想获取特定的公共文件夹,比如Music或Ringtone文件夹,请使用getExternalStoragePublicDirectory()并传入所需的文件夹类型,例如:
getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
检查可用空间
内部存储和外部存储之间的一致问题是空间有限。如果你提前知道你需要多少空间,可以在File对象上调用getFreeSpace()方法。(getTotalSpace()将返回总空间。)以下是一个使用getFreeSpace()调用的简单示例:
if (Environment.getExternalStorageDirectory().getFreeSpace() < RQUIRED_FILE_SPACE) {
//Not enough space
} else {
//We have enough space
}
删除文件
通过File对象提供了许多帮助方法,包括删除文件。如果我们想删除在示例中创建的文本文件,我们可以如下调用delete():
textFile.delete()
使用目录
尽管它被称为File对象,但它也支持目录命令,比如创建和删除目录。如果你想创建或删除目录,构建File对象,然后调用相应的方法:mkdir()和delete()。(还有一个方法叫做mkdirs()(复数形式),它也会创建父目录。)有关完整列表,请参见以下链接。
防止文件被包含在图库中
安卓使用了一个媒体扫描器,它会自动将声音、视频和图像文件包含在系统集合中,比如图片库。要排除你的目录,请在你要排除的文件所在的同一目录中创建一个名为.nomedia的空文件(注意前面的句点)。
另请参阅
- 有关
File类中可用方法的完整列表,请访问developer.android.com/reference/java/io/File.html
在项目中包含资源文件
Android 为您的项目提供了两种包含文件的方式:raw 文件夹和 Assets 文件夹。您使用哪种选项取决于您的需求。首先,我们将简要概述每种选项,帮助您决定何时使用每种选项:
-
原始文件
-
包含在资源目录中:
/res/raw -
作为资源,通过原始标识符访问:
R.raw.<资源名> -
存储媒体文件(如 MP3、MP4 和 OOG 文件)的好地方
-
-
资产文件
-
在您的 APK 中编译文件系统(不提供资源 ID)
-
通过文件名访问文件,通常使得它们更容易与动态创建的名称一起使用。
-
某些 API 不支持资源标识符,因此需要作为资产包含
-
通常,raw 文件更容易处理,因为它们是通过资源标识符访问的。正如我们将在本食谱中演示的,主要区别在于您如何访问文件。在这个例子中,我们将加载一个 raw 文本文件和一个 asset 文本文件,并显示其内容。
准备工作
在 Android Studio 中创建一个新项目,并将其命名为:ReadingResourceFiles。使用默认的 手机 & 平板 选项,并在提示 活动类型 时选择 空活动。
如何操作...
为了演示从两个资源位置读取内容,我们将创建一个分割布局。我们还需要创建这两个资源文件夹,因为它们不包括在默认的 Android 项目中。以下是步骤:
-
打开
activity_main.xml文件,并将其内容替换为以下布局:<?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/textViewRaw" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center_horizontal|center_vertical"/> <TextView android:id="@+id/textViewAsset" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center_horizontal|center_vertical"/> </LinearLayout> -
在 res 文件夹中创建
raw资源文件夹。它将被读取为:res/raw。 -
在
raw文件夹上右键点击,选择 新建 | 文件 创建一个新文本文件。将文件命名为raw_text.txt,并在文件中输入一些文本。(运行应用程序时将显示此文本。) -
创建
asset文件夹。由于位置的原因,asset文件夹更难以处理。幸运的是,Android Studio 提供了一个菜单选项,使得创建它变得非常简单。转到 文件 菜单(或者在 app 节点上右键点击),然后选择 新建 | 文件夹 | 资产文件夹,如下截图所示: -
在 asset 文件夹中创建另一个名为
asset_text.txt的文本文件。同样,您在这里输入的任何文本在运行应用时都会显示。以下是创建两个文本文件后的最终结果应该看起来像这样: -
现在是编写代码的时候了。打开
MainActivity.java文件,并添加以下方法来读取文本文件(传递到该方法中):private String getText(InputStream inputStream) { StringBuilder stringBuilder = new StringBuilder(); try {; if ( inputStream != null ) { InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String newLine = null; while ((newLine = bufferedReader.readLine()) != null ) { stringBuilder.append(newLine+"\n"); } inputStream.close(); } } catch (java.io.IOException e) { e.printStackTrace(); } return stringBuilder.toString(); } -
最后,在
onCreate()方法中添加以下代码:TextView textViewRaw = (TextView)findViewById(R.id.textViewRaw); textViewRaw.setText(getText(this.getResources().openRawResource(R.raw.raw_text))); TextView textViewAsset = (TextView)findViewById(R.id.textViewAsset); try { textViewAsset.setText(getText(this.getAssets().open("asset_text.txt"))); } catch (IOException e) { e.printStackTrace(); } -
在设备或模拟器上运行程序。
工作原理...
总结一下,唯一的区别在于我们如何获取对每个文件的引用。这行代码读取 raw 资源:
this.getResources().openRawResource(R.raw.raw_text)
而这段代码读取 asset 文件:
this.getAssets().open("asset_text.txt")
这两个调用都返回一个 InputStream,getText() 方法使用它来读取文件内容。值得注意的是,打开 asset 文本文件的调用需要一个额外的 try/catch。正如菜谱介绍中所提到的,资源是经过索引的,因此我们有编译时验证,而 asset 文件夹没有。
还有更多...
一种常见的方法是将资源包含在 APK 中,但在新资源可用时下载它们。(请参阅 第十二章中的网络通信,电信、网络和互联网。)如果新资源不可用,你总是可以退回到 APK 中的资源。
另请参阅
- 第十二章中的网络通信菜谱,电信、网络和互联网。
创建和使用 SQLite 数据库
在这个菜谱中,我们将演示如何使用 SQLite 数据库。如果你已经熟悉来自其他平台的 SQL 数据库,那么你所知道的大部分内容都将适用。如果你是 SQLite 的新手,请查看“另请参阅”部分中的参考链接,因为此菜谱假设你具有数据库概念的基本理解,包括模式、表、游标和原始 SQL。
为了让你快速开始使用 SQLite 数据库,我们的示例实现了基本的 CRUD 操作。通常,在 Android 中创建数据库时,你会创建一个扩展 SQLiteOpenHelper 的类,这是实现数据库功能的地方。以下是为每个基本操作提供功能的函数列表:
-
创建:
insert() -
读取:
query()和rawQuery() -
更新:
update() -
删除:
delete()
为了演示一个完全工作的数据库,我们将创建一个简单的 Dictionary 数据库,以便我们可以存储单词及其定义。我们将通过允许添加新单词(及其定义)和更新现有单词定义来演示 CRUD 操作。我们将使用游标在 ListView 中显示单词。点击 ListView 中的单词将从数据库中读取定义并在 Toast 消息中显示。长按将删除单词。
准备就绪
在 Android Studio 中创建一个新项目,命名为 SQLiteDatabase。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity。
如何操作...
首先,我们将创建一个 UI,它包括两个 EditText 字段,一个按钮,和一个 ListView。当我们向数据库添加单词时,它们将填充 ListView。开始时,打开 activity_main.xml 并按照以下步骤操作:
-
用以下新视图替换现有的
<TextView>:<EditText android:id="@+id/et_word" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:hint="Word"/> <EditText android:id="@+id/et_definition" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/editTextWord" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:hint="Definition"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" android:id="@+id/button_add_update" android:layout_alignParentRight="true" android:layout_alignParentTop="true" /> <ListView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/listView" android:layout_below="@+id/et_definition" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" /> -
向项目中添加一个名为
DictionaryDatabase的新 Java 类。这个类从SQLiteOpenHelper扩展而来,处理所有的 SQLite 函数。以下是类声明:public class DictionaryDatabase extends SQLiteOpenHelper { -
在声明下方,添加以下常量:
private static final String DATABASE_NAME = "dictionary.db"; private static final String TABLE_DICTIONARY = "dictionary"; private static final String FIELD_WORD = "word"; private static final String FIELD_DEFINITION = "definition"; private static final int DATABASE_VERSION = 1; -
添加以下构造函数,
OnCreate()和onUpgrade()方法:DictionaryDatabase(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_DICTIONARY + "(_id integer PRIMARY KEY," + FIELD_WORD + " TEXT, " + FIELD_DEFINITION + " TEXT);"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //Handle database upgrade as needed } -
以下方法负责创建、更新和删除记录:
public void saveRecord(String word, String definition) { long id = findWordID(word); if (id>0) { updateRecord(id, word,definition); } else { addRecord(word,definition); } } public long addRecord(String word, String definition) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put(FIELD_WORD, word); values.put(FIELD_DEFINITION, definition); return db.insert(TABLE_DICTIONARY, null, values); } public int updateRecord(long id, String word, String definition) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put("_id", id); values.put(FIELD_WORD, word); values.put(FIELD_DEFINITION, definition); return db.update(TABLE_DICTIONARY, values, "_id = ?", new String[]{String.valueOf(id)}); } public int deleteRecord(long id) { SQLiteDatabase db = getWritableDatabase(); return db.delete(TABLE_DICTIONARY, "_id = ?", new String[]{String.valueOf(id)}); } -
而这些方法处理从数据库读取信息:
public long findWordID(String word) { long returnVal = -1; SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT _id FROM " + TABLE_ DICTIONARY + " WHERE " + FIELD_WORD + " = ?", new String[]{word}); Log.i("findWordID","getCount()="+cursor.getCount()); if (cursor.getCount() == 1) { cursor.moveToFirst(); returnVal = cursor.getInt(0); } return returnVal; } public String getDefinition(long id) { String returnVal = ""; SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT definition FROM " + TABLE_ DICTIONARY + " WHERE _id = ?", new String[]{String.valueOf(id)}); if (cursor.getCount() == 1) { cursor.moveToFirst(); returnVal = cursor.getString(0); } return returnVal; } public Cursor getWordList() { SQLiteDatabase db = getReadableDatabase(); String query = "SELECT _id, " + FIELD_WORD + " FROM " + TABLE_DICTIONARY + " ORDER BY " + FIELD_WORD + " ASC"; return db.rawQuery(query, null); } -
数据库类完成后,打开
MainActivity.java。在类声明下面添加以下全局变量:EditText mEditTextWord; EditText mEditTextDefinition; DictionaryDatabase mDB; ListView mListView; -
添加以下方法以在点击按钮时保存字段:
private void saveRecord() { mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString()); mEditTextWord.setText(""); mEditTextDefinition.setText(""); updateWordList(); } -
添加这个方法来填充
ListView:private void updateWordList() { SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter( this, android.R.layout.simple_list_item_1, mDB.getWordList(), new String[]{"word"}, new int[]{android.R.id.text1}, 0); mListView.setAdapter(simpleCursorAdapter); } -
最后,在
onCreate()中添加以下代码:mDB = new DictionaryDatabase(this); mEditTextWord = (EditText)findViewById(R.id.editTextWord); mEditTextDefinition = (EditText)findViewById(R.id.editTextDefinition); Button buttonAddUpdate = (Button)findViewById(R.id.buttonAddUpdate); buttonAddUpdate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { saveRecord(); } }); mListView = (ListView)findViewById(R.id.listView); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, mDB.getDefinition(id),Toast.LENGTH_SHORT).show(); } }); mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "Records deleted = " + mDB.deleteRecord(id), Toast.LENGTH_SHORT).show(); updateWordList(); return true; } }); updateWordList(); -
在设备或模拟器上运行程序并尝试一下。
它的工作原理是...
我们将从解释DictionaryDatabase类开始,因为这是 SQLite 数据库的核心。首先要注意的是构造函数:
DictionaryDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
注意DATABASE_VERSION吗?只有当你对数据库架构进行更改时,才需要增加这个值。
接下来是onCreate(),实际创建数据库的地方。这只有在第一次创建数据库时才会被调用,而不是每次创建类时。还值得注意的是_id字段。Android 并不要求表具有主字段,除了像SimpleCursorAdapter这样的某些类需要_id。
我们需要实现onUpgrade()回调,但因为是新的数据库,所以不需要做任何事情。当数据库版本增加时,将调用此方法。
saveRecord()方法负责调用addRecord()或updateRecord(),视情况而定。由于我们将要修改数据库,这两个方法都调用getWritableDatabase()以便我们可以进行更改。可写数据库需要更多资源,所以如果你不需要进行更改,请获取只读数据库。
需要注意的最后一个方法是getWordList(),它使用游标对象返回数据库中的所有单词。我们使用这个游标来填充ListView,这就把我们带到了ActivityMain.java。onCreate()方法进行了我们之前见过的标准初始化,并使用以下代码行创建数据库实例:
mDB = new DictionaryDatabase(this);
onCreate()方法也是我们设置事件的地方,当点击项目时显示单词定义(通过 Toast 弹出),以及长按删除单词。最复杂的代码可能是在updateWordList()方法中。
这不是我们第一次使用适配器,但这是我们第一次使用游标适配器,所以我们会解释一下。我们使用SimpleCursorAdapter来创建游标中的字段与ListView项之间的映射。我们使用layout.simple_list_item_1布局,它只包括一个带有 ID android.R.id.text1的单个文本字段。在实际应用中,我们可能会创建一个自定义布局,并在ListView项中包含定义,但我们想要演示一种从数据库读取定义的方法。
我们在三个地方调用updateWordList()——在onCreate()时创建初始列表,添加/更新列表后再次调用,以及删除列表时最后调用。
还有更多...
尽管这是一个功能完整的 SQLite 示例,但它仍然只是基础。整本书都可以,也确实有,关于 Android 中的 SQLite 的内容。
升级数据库
如我们之前提到的,当增加数据库版本时,将调用 onUpgrade() 方法。这里需要执行的操作取决于所做的更改。如果你更改了现有的表,理想情况下,你将希望通过查询现有数据并将其插入到新格式中来迁移用户数据。请记住,不能保证用户会按连续的顺序升级——例如,他们可能会从版本 1 直接跳到版本 4。
另请参阅
-
SQLite 主页:
www.sqlite.org/ -
SQLite 数据库 Android 参考文档:
developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html
在后台使用 Loader 访问数据
任何可能长时间运行的操作都不应该在 UI 线程上执行,因为这可能导致应用程序变慢或无响应。当应用程序无响应时,Android OS 会弹出 应用程序无响应 (ANR) 对话框。
由于查询数据库可能很耗时,Android 在 Android 3.0 中引入了 Loader API。Loader 在后台线程上处理查询,并在完成后通知 UI 线程。
Loaders 的两个主要优点包括:
-
数据库查询操作(自动)在后台线程中处理
-
查询(在使用内容提供者数据源时)会自动更新
为了演示 Loader,我们将修改之前的 SQLite 数据库示例,使用 CursorLoader 填充 ListView。
准备工作
我们将使用上一个示例中的项目,创建和使用 SQLite 数据库,作为这个示例的基础。在 Android Studio 中创建一个新项目,将其命名为 Loader。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。复制上一个示例中的 DictionaryDatabase 类和布局。尽管我们将使用之前 ActivityMain.java 代码的部分内容,但在这个示例中我们将从头开始,以便更容易跟随。
如何操作...
按照之前的描述设置项目后,我们将从创建两个新的 Java 类开始,然后在 ActivityMain.java 中将所有内容整合在一起。以下是步骤:
-
创建一个名为
DictionaryAdapter的新 Java 类,该类继承自CursorAdapter。这个类替代了我们在上一个示例中使用的SimpleCursorAdapter。以下是完整代码:public class DictionaryAdapter extends CursorAdapter { public DictionaryAdapter(Context context, Cursor c, int flags) { super(context, c, flags); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1,parent,false); } @Override public void bindView(View view, Context context, Cursor cursor) { TextView textView = (TextView)view.findViewById(android.R.id.text1); textView.setText(cursor.getString(getCursor().getColumnIndex("word"))); } } -
接下来,创建另一个新的 Java 类,将这个类命名为
DictionaryLoader。尽管这是处理后台线程数据加载的类,但它实际上非常简单:public class DictionaryLoader extends CursorLoader { Context mContext; public DictionaryLoader(Context context) { super(context); mContext = context; } @Override public Cursor loadInBackground() { DictionaryDatabase db = new DictionaryDatabase(mContext); return db.getWordList(); } } -
接下来,打开
ActivityMain.java。我们需要将声明更改为实现LoaderManager.LoaderCallbacks<Cursor>接口,如下所示:public class MainActivity extends AppCompatActivity implements { -
将适配器添加到全局声明中。完整的列表如下:
EditText mEditTextWord; EditText mEditTextDefinition; DictionaryDatabase mDB; ListView mListView; DictionaryAdapter mAdapter; -
修改
onCreate()以使用新的适配器,并在删除记录后添加调用以更新加载器。最终的onCreate()方法应如下所示:protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mDB = new DictionaryDatabase(this); mEditTextWord = (EditText) findViewById(R.id.editTextWord); mEditTextDefinition = (EditText) findViewById(R.id.editTextDefinition); Button buttonAddUpdate = (Button) findViewById(R.id.buttonAddUpdate); buttonAddUpdate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { saveRecord(); } }); mListView = (ListView) findViewById(R.id.listView); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, mDB.getDefinition(id), Toast.LENGTH_SHORT).show(); } }); mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "Records deleted = " + mDB.deleteRecord(id), Toast.LENGTH_SHORT).show(); getSupportLoaderManager().restartLoader(0, null, MainActivity.this); return true; } }); getSupportLoaderManager().initLoader(0, null, this); mAdapter = new DictionaryAdapter(this,mDB.getWordList(),0); mListView.setAdapter(mAdapter); } -
我们不再有
updateWordList()方法,因此按照以下方式更改saveRecord():private void saveRecord() { mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString()); mEditTextWord.setText(""); mEditTextDefinition.setText(""); getSupportLoaderManager().restartLoader(0, null, MainActivity.this); } -
最后,为加载器接口实现以下三个方法:
@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new DictionaryLoader(this); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } -
在设备或模拟器上运行程序。
工作原理...
默认的CursorAdapter需要一个内容提供者 URI。由于我们直接访问 SQLite 数据库(而不是通过内容提供者),我们没有 URI 传递,因此我们通过扩展CursorAdapter类创建了一个自定义适配器。DictionaryAdapter仍然执行与之前的SimpleCursorAdapter相同的功能,即将游标中的数据映射到项目布局。
我们添加的下一个类是DictionaryLoader,这是实际的加载器。如您所见,它实际上非常简单。它所做的只是从getWordList()返回游标。关键在于此查询是在后台线程中处理的,并在完成时调用onLoadFinished()回调(在MainActivity.java中)。幸运的是,大部分繁重的工作都在基类中处理。
这将我们带到ActivityMain.java,在那里我们实现了LoaderManager.LoaderCallbacks接口的以下三个回调:
-
onCreateLoader(): 最初在onCreate()中的initLoader()调用时调用。在我们对数据库进行更改后,通过restartLoader()调用再次调用。 -
onLoadFinished(): 当加载器的loadInBackground()完成时调用。 -
onLoaderReset(): 当加载器被重新创建时调用(例如使用restart()方法)。我们将旧的游标设置为null,因为它将无效,我们不想保留引用。
还有更多...
正如您在前一个示例中看到的,我们需要手动通知加载器使用restartLoader()重新查询数据库。使用加载器的一个好处是它可以自动更新,但这需要一个内容提供者作为数据源。内容提供者支持使用 SQLite 数据库作为数据源,对于严肃的应用程序,建议使用。请参阅以下内容提供者链接以开始操作。
另请参阅
-
第十四章中的AsyncTask配方,让你的应用准备好上架 Play 商店。
-
创建内容提供者:
developer.android.com/guide/topics/providers/content-provider-creating.html
第七章:警报和通知
在本章中,我们将涵盖以下主题:
-
灯光、动作和声音——吸引用户的注意!
-
使用自定义布局创建 Toast
-
使用 AlertDialog 显示消息框
-
显示进度对话框
-
使用通知重新实现灯光、动作和声音
-
创建媒体播放器通知
-
使用抬头通知制作手电筒
简介
Android 提供了多种方式来通知用户——从非视觉方法,包括声音、灯光和振动,到视觉方法,包括 Toast、对话框和状态栏通知。
请记住,通知会分散用户的注意力,因此在使用任何通知时都应该非常谨慎。用户喜欢控制他们的设备(毕竟这是他们的设备),所以给他们启用和禁用通知的选择。否则,用户可能会感到烦恼,并完全卸载你的应用。
我们将从以下基于非 UI 的通知选项开始回顾:
-
闪烁 LED
-
振动手机
-
播放铃声
然后我们将继续讨论视觉通知,包括:
-
Toasts
-
AlertDialog -
ProgressDialog -
状态栏通知
接下来的食谱将向你展示如何在你的应用程序中实现这些功能。阅读以下链接以了解使用通知时的“最佳实践”是非常值得的:
提示
请参考Android 通知设计指南,网址为:developer.android.com/design/patterns/notifications.html
灯光、动作和声音——吸引用户的注意!
本章中的大部分食谱使用 Notification 对象来提醒用户,所以这个食谱将展示当你实际上不需要通知时的替代方法。
如标题所示,我们将使用灯光、动作和声音:
-
灯光:通常,你会使用 LED 设备,但这仅通过 Notification 对象才可用,我们将在本章后面演示。相反,我们将借此机会使用
setTorchMode()(在 API 23—Android 6.0 中添加),使用相机闪光灯作为手电筒。(注意:正如你在代码中看到的,这个功能只会在带有相机闪光灯的 Android 6.0 设备上工作。) -
动作:我们将使手机振动。
-
声音:我们将使用
RingtoneManager播放默认通知声音。
如你所见,这些的代码都非常简单。
如以下 使用通知的 Lights, Action, 和 Sound Redux 配方所示,LED、振动和声音这三个选项都可以通过 Notification 对象使用。当用户没有积极使用你的应用时,Notification 对象当然是最合适的方法来提供警报和提醒。但是,当你想在用户使用你的应用时提供反馈时,这些选项是可用的。振动选项就是一个很好的例子;如果你想对按钮按下提供触觉反馈(键盘应用中很常见),可以直接调用振动方法。
准备工作
在 Android Studio 中创建一个新项目,命名为 LightsActionSound。当提示选择 API 级别时,我们需要 API 21 或更高版本来编译项目。在选择 Activity 类型 时,选择 Empty Activity。
如何操作...
我们将使用三个按钮来启动每个操作,首先打开 activity_main.xml 并按照以下步骤操作:
-
用以下三个按钮替换现有的
<TextView>元素:<ToggleButton android:id="@+id/buttonLights" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Lights" android:layout_centerHorizontal="true" android:layout_above="@+id/buttonAction" android:onClick="clickLights" /> <Button android:id="@+id/buttonAction" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Action" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="clickVibrate"/> <Button android:id="@+id/buttonSound" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Sound" android:layout_below="@+id/buttonAction" android:layout_centerHorizontal="true" android:onClick="clickSound"/> -
向 Android Manifest 添加以下权限:
<uses-permission android:name="android.permission.VIBRATE"></uses-permission> -
打开
ActivityMain.java并添加以下全局变量:private CameraManager mCameraManager; private String mCameraId=null; private ToggleButton mButtonLights; -
添加以下方法以获取相机 ID:
private String getCameraId() { try { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id); Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); Integer facingDirection = c.get(CameraCharacteristics.LENS_FACING); if (flashAvailable != null && flashAvailable && facingDirection != null && facingDirection == CameraCharacteristics.LENS_FACING_BACK) { return id; } } } catch (CameraAccessException e) { e.printStackTrace(); } return null; } -
在
onCreate()方法中添加以下代码:mButtonLights = (ToggleButton)findViewById(R.id.buttonLights); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); mCameraId = getCameraId(); if (mCameraId==null) { mButtonLights.setEnabled(false); } else { mButtonLights.setEnabled(true); } } else { mButtonLights.setEnabled(false); } -
现在添加处理每个按钮点击的代码:
public void clickLights(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { mCameraManager.setTorchMode(mCameraId, mButtonLights.isChecked()); } catch (CameraAccessException e) { e.printStackTrace(); } } } public void clickVibrate(View view) { ((Vibrator)getSystemService(VIBRATOR_SERVICE)).vibrate(1000); } public void clickSound(View view) { Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notificationSoundUri); ringtone.play(); } -
你已经准备好在物理设备上运行应用程序了。这里提供的代码需要 Android 6.0(或更高版本)才能使用手电筒选项。
工作原理...
如前文所述,大部分代码都是关于查找并打开摄像头以使用闪光灯功能。setTorchMode() 在 API 23 中引入,这就是为什么我们要进行 API 版本检查:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){}
这个应用展示了使用在 Lollipop (API 21) 中引入的新的 camera2 库。vibrate 和 ringtone 方法自 API 1 以来都已可用。
getCameraId() 方法是我们检查摄像头的位置。我们想要一个带闪光灯的外向摄像头。如果找到,则返回其 ID,否则为 null。如果摄像头 ID 为 null,我们将禁用按钮。
为了播放声音,我们使用来自 RingtoneManager 的 Ringtone 对象。除了实现相对简单之外,这种方法的好处是我们可以使用默认通知声音,通过以下代码获取:
Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
这样,如果用户更改了他们首选的通知声音,我们会自动使用它。
最后是调用手机振动的部分。这是最简单的代码使用,但它确实需要权限,我们已经将其添加到 Manifest 中:
<uses-permission android:name="android.permission.VIBRATE"></uses-permission>
还有更多...
在一个生产级别的应用中,如果你不必这样做,你不会想要简单地禁用按钮。在这种情况下,还有其他方法可以使用相机闪光灯作为手电筒。查看多媒体章节,了解更多关于使用摄像头的示例,我们将会再次看到 getCameraId() 的使用。
另请参阅
-
在本章后面的用通知的灯光、动作和声音 Redux食谱中,可以看到使用通知对象的等效功能。
-
有关使用新相机 API 和其他声音选项的示例,请参考第十一章,多媒体。
使用自定义布局创建 Toast
在前面的章节中,我们已经大量使用了 Toast,因为它们提供了一种快速简便的方式来显示信息——既适用于用户,也适用于我们调试时。
前面的例子都使用了简单的一行语法,但 Toast 并不限于此。与 Android 中的大多数组件一样,Toast 也可以自定义,我们将在本节中演示这一点。
Android Studio 为制作简单的 Toast 语句提供了快捷方式。当你开始输入 Toast 命令时,按下Ctrl + Spacebar,你会看到以下内容:
按下Enter键以自动完成。然后,再次按下Ctrl + Spacebar,你会看到以下内容:
当你再次按下Enter键时,它会自动完成以下内容:
Toast.makeText(MainActivity.this, "", Toast.LENGTH_SHORT).show();
在本节中,我们将使用 Toast Builder 来更改默认布局和定位,以创建一个自定义的 Toast,如以下屏幕截图所示:
准备就绪
在 Android Studio 中创建一个新项目,将其命名为CustomToast。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
如何操作...
我们将改变 Toast 的形状为正方形,并创建一个自定义布局来显示图像和文本信息。首先打开activity_main.xml并按照以下步骤操作:
-
使用以下内容替换现有的
<TextView>元素为<Button>:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Toast" android:id="@+id/button" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:onClick="showToast"/> -
在
res/drawable文件夹中创建一个名为border_square.xml的新资源文件,并输入以下代码:<?xml version="1.0" encoding="utf-8"?> <layer-list > <item android:left="4px" android:top="4px" android:right="4px" android:bottom="4px"> <shape android:shape="rectangle" > <solid android:color="@android:color/black" /> <stroke android:width="5px" android:color="@android:color/white"/> </shape> </item> </layer-list> -
在
res/layout文件夹中创建一个名为toast_custom.xml的新资源文件,并输入以下代码:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id="@+id/toast_layout_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:background="@drawable/border_square"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:layout_weight="1" android:src="img/ic_launcher" /> <TextView android:id="@android:id/message" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:textColor="@android:color/white" android:padding="10dp" /> </LinearLayout> -
现在,打开
ActivityMain.java并输入以下方法:public void showToast(View view) { LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View layout = inflater.inflate(R.layout.toast_custom, null); ((TextView) layout.findViewById(android.R.id.message)).setText("Custom Toast"); Toast toast = new Toast(this); toast.setGravity(Gravity.CENTER, 0, 0); toast.setDuration(Toast.LENGTH_LONG); toast.setView(layout); toast.show(); } -
在设备或模拟器上运行程序。
工作原理...
这个自定义的 Toast 更改了默认的定位、形状,并添加了图像,只是展示“这是可以做到的”。
第一步是创建一个新的 Toast 布局,我们通过膨胀我们的custom_toast布局来实现。一旦我们有了新的布局,我们需要获取TextView,这样我们就可以设置我们的信息,我们使用标准的setText()方法来完成这个操作。完成这些后,我们创建一个 Toast 对象并设置各个属性。我们使用setGravity()方法设置 Toast 的定位。定位决定了我们的 Toast 在屏幕上的显示位置。我们通过setView()方法调用指定我们的自定义布局。与单行版本一样,我们使用show()方法显示 Toast。
使用 AlertDialog 显示消息框
在第四章,菜单中,我们创建了一个主题,使活动看起来像一个对话框。在这个菜谱中,我们将演示如何使用AlertDialog类创建对话框。AlertDialog提供了标题,最多三个按钮,以及一个列表或自定义布局区域,如下例所示:
注意
按钮的位置可能会根据操作系统版本而有所不同。
准备就绪
在 Android Studio 中创建一个新项目,将其命名为:AlertDialog。使用默认的手机 & 平板选项,在选择活动类型时选择空活动选项。
如何操作...
为了演示,我们将创建一个确认删除对话框,在用户按下删除按钮后提示用户确认。首先打开main_activity.xml布局文件,并按照以下步骤操作:
-
添加以下
<Button>:<Button android:id="@+id/buttonClose" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Delete" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="confirmDelete"/> -
添加由按钮调用的
confirmDelete()方法:public void confirmDelete(View view) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Delete") .setMessage("Are you sure you?") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { Toast.makeText(MainActivity.this, "OK Pressed", Toast.LENGTH_SHORT).show(); }}) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { Toast.makeText(MainActivity.this, "Cancel Pressed", Toast.LENGTH_SHORT).show(); }}); builder.create().show(); } -
在设备或模拟器上运行应用程序。
作用机理...
这个对话框旨在作为一个简单的确认对话框——例如确认删除操作。基本上,只需创建一个AlertDialog.Builder对象并根据需要设置属性。我们使用一个 Toast 消息来指示用户的选择,甚至不需要关闭对话框;它由基类处理。
还有更多...
如菜谱介绍截图所示,AlertDialog还有一个第三按钮,称为中性按钮,可以通过以下方法设置:
builder.setNeutralButton()
添加一个图标
若要在对话框中添加图标,请使用setIcon()方法。以下是一个示例:
.setIcon(R.mipmap.ic_launcher)
使用列表
我们还可以创建一个项目列表供选择,包括各种列表设置方法:
.setItems()
.setAdapter()
.setSingleChoiceItems()
.setMultiChoiceItems()
如你所见,也有用于单选(使用单选按钮)和多选列表(使用复选框)的方法。
提示
你不能同时使用消息和列表,因为setMessage()将优先处理。
自定义布局
最后,我们还可以创建一个自定义布局,并通过以下方式设置:
.setView()
如果你使用自定义布局并替换标准按钮,你还需要负责关闭对话框。如果你打算重用对话框,请使用hide(),完成后使用dismiss()释放资源。
显示进度对话框
ProgressDialog从 API 1 开始可用,并被广泛使用。正如我们在这个食谱中展示的,它使用起来很简单,但请记住(来自 Android 对话框指南网站)的这句话:
避免使用 ProgressDialog
Android 另外提供了一个名为 ProgressDialog 的对话框类,它显示带有进度条的对话框。然而,如果你需要指示加载或不确定的进度,你应该遵循进度与活动的设计指南,并在你的布局中使用 ProgressBar。
developer.android.com/guide/topics/ui/dialogs.html
这条消息并不意味着ProgressDialog已经废弃或者代码不好。它建议应避免使用ProgressDialog,因为当对话框显示时,用户无法与你的应用互动。如果可能,使用包含进度条的布局,而不是使用ProgressDialog。
Google Play 应用提供了一个很好的例子。当添加下载项时,Google Play 显示一个进度条,但它不是一个对话框,所以用户可以继续与应用互动,甚至可以添加更多下载项。如果可能,请使用这种方法。
有时你可能没有这种奢侈,比如在下了订单之后,用户会期待一个订单确认。(即使是使用 Google Play,在实际购买应用时你仍然会看到一个确认对话框。)所以,请记住,如果可能的话,避免使用进度对话框。但是,对于那些必须在继续之前完成的事情,这个示例提供了一个如何使用ProgressDialog的例子。以下截图展示了示例中的ProgressDialog:
准备工作
在 Android Studio 中创建一个新项目,并将其命名为:ProgressDialog。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动。
如何操作...
-
由于这只是一个使用
ProgressDialog的演示,我们将创建一个按钮来显示对话框。为了模拟等待服务器响应,我们将使用一个延迟消息来关闭对话框。首先,打开activity_main.xml并按照以下步骤操作: -
将
<TextView>替换为以下<Button>:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Dialog" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="startProgress"/> -
打开
MainActivity.java并添加以下两个全局变量:private ProgressDialog mDialog; final int THIRTY_SECONDS=30*1000; -
添加由按钮点击引用的
showDialog()方法:public void startProgress(View view) { mDialog= new ProgressDialog(this); mDialog.setMessage("Doing something..."); mDialog.setCancelable(false); mDialog.show(); new Handler().postDelayed(new Runnable() { public void run() { mDialog.dismiss(); }}, THIRTY_SECONDS); -
在设备或模拟器上运行程序。当你按下显示对话框按钮时,你会看到与简介中屏幕显示的对话框一样的内容。
工作原理...
我们使用ProgressDialog类来显示我们的对话框。这些选项应该是自解释的,但这个设置值得注意:
mDialog.setCancelable(false);
通常,可以通过按下返回键来取消对话框,但当这被设置为 false 时,用户将停留在对话框上,直到从代码中隐藏/关闭它。为了模拟服务器的延迟响应,我们使用了一个Handler和postDelayed()方法。在指定的毫秒数(在本例中是 30,000,代表 30 秒)之后,将调用run()方法,该方法将关闭我们的对话框。
还有更多...
在这个示例中,我们使用了默认的ProgressDialog设置,创建了一个不确定的对话框指示器,例如,连续旋转的圆圈。如果你可以衡量当前的任务,比如加载文件,你可以使用确定样式代替。添加并运行这行代码:
mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
你将得到以下对话框样式作为前一行代码的输出:
使用通知重新实现灯光、动作和声音
你可能已经对通知(Notifications)很熟悉了,因为它们已经成为一个突出的功能(甚至已经应用到桌面环境中),而且有充分的理由。它们为向用户发送信息提供了极好的方式。与其他可用的警告和通知选项相比,它们提供了最小侵扰性的选择。
正如我们在第一个食谱“灯光、动作和声音——吸引用户的注意!”中所看到的,灯光、振动和声音都是吸引用户注意力的非常有用的方法。这就是为什么通知对象包括支持这三种方式的原因,我们将在本食谱中展示这一点。鉴于这种吸引用户注意力的能力,仍然应该注意不要滥用用户。否则,他们很可能会卸载你的应用。通常来说,给用户选择启用/禁用通知甚至如何显示通知是一个好主意——带声音或不带声音等。
准备就绪
在 Android Studio 中创建一个新项目,命名为:LightsActionSoundRedux。使用默认的手机 & 平板选项,并在提示选择活动类型时选择空活动。
如何操作...
我们需要获得使用振动功能的权限,因此首先打开 Android Manifest 文件,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.VIBRATE"/> -
打开
activity_main.xml,用以下按钮替换现有的<TextView>:<Button android:id="@+id/buttonSound" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Lights, Action, and Sound" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="clickLightsActionSound"/> -
现在打开
MainActivity.java,并添加以下方法来处理按钮点击:public void clickLightsActionSound(View view) { Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("LightsActionSoundRedux") .setContentText("Lights, Action & Sound") .setSound(notificationSoundUri) .setLights(Color.BLUE, 500, 500) .setVibrate(new long[]{250,500,250,500,250,500}); NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); } -
在设备或模拟器上运行程序。
工作原理...
首先,我们将三个动作合并为一个通知,仅仅是因为我们可以这样做。你不必使用所有三个额外的通知选项,甚至一个也不用。以下内容是必需的:
.setSmallIcon()
.setContentText()
如果你不设置图标和文本,通知将不会显示。
其次,我们使用了NotificationCompat来构建我们的通知。这是来自支持库的,使得更容易与旧操作系统版本向后兼容。如果我们请求的通知功能在用户的操作系统版本上不可用,它将被简单地忽略。
产生我们额外通知选项的三行代码包括以下内容:
.setSound(notificationSoundUri)
.setLights(Color.BLUE, 500, 500)
.setVibrate(new long[]{250,500,250,500,250,500});
值得注意的是,我们在此通知中使用与之前的“灯光、动作和声音”食谱中的RingtoneManager相同的铃声 URI。振动功能也要求与之前的食谱相同的振动权限,但请注意我们发送的值是不同的。我们不是只发送振动的持续时间,而是发送一个振动模式。第一个值表示关闭的持续时间(以毫秒为单位),下一个值表示振动的开启持续时间,并重复。
提示
在具有 LED 通知功能的设备上,当屏幕处于激活状态时,你不会看到 LED 通知。
还有更多...
本指南展示了通知的基础知识,但与 Android 上的许多功能一样,随着后来操作系统版本的更新,选项也扩展了。
使用 addAction()向通知中添加按钮
在添加操作按钮时,你应该考虑到一些设计上的注意事项,如本章引言中链接的通知指南所述。你可以使用通知构建器上的addAction()方法添加一个按钮(最多三个)。下面是一个带有一个操作按钮的通知的示例:
下面是创建此通知的代码:
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("LightsActionSoundRedux")
.setContentText("Lights, Action & Sound");
Intent activityIntent = new Intent(this,MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,activityIntent,0);
notificationBuilder.addAction(android.R.drawable.ic_dialog_email, "Email", pendingIntent);
notificationManager.notify(0, notificationBuilder.build());
Action需要三个参数——图像、文本和一个PendingIntent。前两项用于视觉显示,而第三项,即PendingIntent,在用户按下按钮时调用。
之前的代码创建了一个非常简单的PendingIntent;它只是启动了应用。这可能是通知中最常见的意图,通常用于用户点击通知时。要设置通知意图,请使用以下代码:
.setContentIntent(pendingIntent)
按钮操作可能需要更多信息,因为它应该引导用户到应用中的特定项目。你也应该创建一个应用的后退栈以获得最佳用户体验。查看以下链接中的话题"启动活动时保持导航":
developer.android.com/guide/topics/ui/notifiers/notifications.html#NotificationResponse
展开式通知
展开式通知在 Android 4.1(API 16)中引入,可以通过在通知构建器上使用setStyle()方法来使用。如果用户的操作系统不支持展开式通知,通知将显示为普通通知。
NotificationCompat库中当前可用的三种展开式样式包括:
-
InboxStyle -
BigPictureStyle -
BigTextStyle
下面是每种通知样式的示例,以及创建示例的代码:
-
InboxStyle:NotificationCompat.Builder notificationBuilderInboxStyle = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); inboxStyle.setBigContentTitle("InboxStyle - Big Content Title") .addLine("Line 1") .addLine("Line 2"); notificationBuilderInboxStyle.setStyle(inboxStyle); notificationManager.notify(0, notificationBuilderInboxStyle.build()); -
BigPictureStyle:NotificationCompat.Builder notificationBuilderBigPictureStyle = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher).setContentTitle("LightsActionSoundRedux").setContentText("BigPictureStyle"); NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); bigPictureStyle.bigPicture(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)); notificationBuilderBigPictureStyle.setStyle(bigPictureStyle); notificationManager.notify(0, notificationBuilderBigPictureStyle.build()); -
BigTextStyleNotificationCompat.Builder notificationBuilderBigTextStyle = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher).setContentTitle("LightsActionSoundRedux"); NotificationCompat.BigTextStyle BigTextStyle = new NotificationCompat.BigTextStyle(); BigTextStyle.bigText("This is an example of the BigTextStyle expanded notification."); notificationBuilderBigTextStyle.setStyle(BigTextStyle); notificationManager.notify(0, notificationBuilderBigTextStyle.build());
锁屏通知
Android 5.0(API 21)及以上版本可以根据用户的锁屏可见性在锁屏上显示通知。使用setVisibility()指定通知可见性,使用以下值:
-
VISIBILITY_PUBLIC:所有内容都可以显示 -
VISIBILITY_SECRET:不显示任何内容 -
VISIBILITY_PRIVATE:显示基本内容(标题和图标),其余内容隐藏
另请参阅
- 查看关于 Android 5.0(API 21)及更高版本的通知选项的创建媒体播放器通知和使用抬头通知制作手电筒的食谱。
创建媒体播放器通知
这个示例将查看在 Android 5.0(API 21)中引入的新媒体播放器样式。与之前使用NotificationCompat的示例使用通知的灯光、动作和声音重做不同,这个示例没有使用,因为这种样式在支持库中不可用。
下面是通知显示方式的截图:
这张截图展示了锁定屏幕上媒体播放器通知的一个示例:
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为:MediaPlayerNotification。当提示选择 API 级别时,我们需要为这个项目选择 API 21(或更高)。在选择活动类型时,选择空活动。
如何操作...
我们只需要一个按钮来调用我们的代码发送通知。打开activity_main.xml并按照以下步骤操作:
-
用以下按钮代码替换现有的
<TextView>:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Notification" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="showNotification"/> -
打开
MainActivity.java并添加showNotification()方法:@Deprecated public void showNotification(View view) { Intent activityIntent = new Intent(this,MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, activityIntent, 0); Notification notification; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { notification = new Notification.Builder(this).setVisibility(Notification.VISIBILITY_PUBLIC) .setSmallIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) .addAction(new Notification.Action.Builder(Icon.createWithResource(this, android.R.drawable.ic_media_previous), "Previous", pendingIntent).build()) .addAction(new Notification.Action.Builder(Icon.createWithResource(this, android.R.drawable.ic_media_pause), "Pause", pendingIntent).build()) .addAction(new Notification.Action.Builder(Icon.createWithResource(this, android.R.drawable.ic_media_next), "Next", pendingIntent).build()) .setContentTitle("Music") .setContentText("Now playing...") .setLargeIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) .setStyle(new Notification.MediaStyle().setShowActionsInCompactView(1)).build(); } else { notification = new Notification.Builder(this) .setVisibility(Notification.VISIBILITY_PUBLIC) .setSmallIcon(R.mipmap.ic_launcher) .addAction(new Notification.Action.Builder(android.R.drawable.ic_media_previous, "Previous", pendingIntent).build()) .addAction(new Notification.Action.Builder(android.R.drawable.ic_media_pause, "Pause", pendingIntent).build()) .addAction(new Notification.Action.Builder(android.R.drawable.ic_media_next, "Next", pendingIntent).build()) .setContentTitle("Music") .setContentText("Now playing...") .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setStyle(new Notification.MediaStyle() .setShowActionsInCompactView(1)).build(); } NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notification); } -
在设备或模拟器上运行程序。
工作原理...
首先要注意的细节是,我们对showNotification()方法进行了以下装饰:
@Deprecated
这告诉编译器我们知道我们正在使用弃用的调用。(如果没有这个,编译器会标记代码。)我们接着使用 API 检查,通过以下调用:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
图标资源在 API 23 中进行了更改,但我们希望这个应用程序能在 API 21(Android 5.0)及更高版本上运行,所以在 API 21 和 API 22 上仍然需要调用旧的方法。
如果用户运行在 Android 6.0(或更高版本)上,我们使用新的Icon类来创建我们的图标,否则我们使用旧的构造函数。(你会注意到 IDE 会用删除线显示弃用的调用。)在运行时检查当前的操作系统版本是一种保持向后兼容的常见策略。
我们使用addAction()创建了三个动作来处理媒体播放器的功能。由于我们实际上并没有一个正在运行的媒体播放器,所以所有动作我们都使用了相同的意图,但在你的应用程序中,你应创建独立的意图。
为了让通知在锁定屏幕上可见,我们需要将可见性级别设置为VISIBILITY_PUBLIC,我们通过以下调用实现:
.setVisibility(Notification.VISIBILITY_PUBLIC)
这个调用值得注意:
.setShowActionsInCompactView(1)
正如方法名称所暗示的,这设置了在通知以简化布局显示时展示的动作。(请参阅菜谱介绍中的锁定屏幕图片。)
还有更多...
在这个示例中,我们只创建了视觉通知。如果我们正在创建一个实际的媒体播放器,我们可以实例化一个MediaSession类,并通过以下调用传递会话令牌:
.setMediaSession(mMediaSession.getSessionToken())
这将允许系统识别媒体内容并做出相应的反应,例如在锁定屏幕上用当前专辑封面进行更新。
另请参阅
-
请参考
developer.android.com/reference/android/media/session/MediaSession.html的开发者文档——媒体会话 -
在“使用通知的灯光、动作和声音 Redux”食谱中的“锁屏可见性”部分讨论了可见性选项。
使用抬头通知制作手电筒
安卓 5.0—棒棒糖(API 21)引入了一种新的通知类型,称为抬头通知。很多人不喜欢这种新通知,因为它可能会非常侵入式,强制出现在其他应用之上。(请看以下截图。)在使用这种类型的通知时要记住这一点。我们将通过一个手电筒来演示抬头通知,因为这展示了一个好的使用场景。
下面是一张稍后我们将要创建的抬头通知的截图:
如果你有一个运行安卓 6.0 的设备,你可能已经注意到了新的手电筒设置选项。作为演示,我们将在本食谱中创建类似的东西。
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为FlashlightWithHeadsUp。当被提示选择 API 级别时,我们需要为这个项目选择 API 23(或更高)。在选择活动类型时,选择空活动。
如何操作...
我们的活动布局将仅包含一个ToggleButton来控制手电筒模式。我们将使用与之前提供的*灯光、动作和声音——吸引用户的注意!*食谱相同的setTorchMode()代码,并添加一个抬头通知。我们需要使用振动选项的权限,因此首先打开 Android 清单,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.VIBRATE"/> -
通过向
<MainActivity>元素添加android:launchMode="singleInstance"来指定我们只希望有一个MainActivity的实例。它将如下所示:<activity android:name=".MainActivity" android:launchMode="singleInstance"> -
对
AndroidManifest的更改完成后,打开activity_main.xml布局,并将现有的<TextView>元素替换为此<ToggleButton>代码:<ToggleButton android:id="@+id/buttonLight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Flashlight" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="clickLight"/> -
现在,打开
ActivityMain.java并添加以下全局变量:private static final String ACTION_STOP="STOP"; private CameraManager mCameraManager; private String mCameraId=null; private ToggleButton mButtonLight; -
在
onCreate()中添加以下代码来设置相机:mButtonLight = (ToggleButton)findViewById(R.id.buttonLight); mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); mCameraId = getCameraId(); if (mCameraId==null) { mButtonLight.setEnabled(false); } else { mButtonLight.setEnabled(true); } -
添加以下方法来处理用户按下通知时的响应:
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (ACTION_STOP.equals(intent.getAction())) { setFlashlight(false); } } -
添加获取相机 id 的方法:
private String getCameraId() { try { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id); Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); Integer facingDirection = c.get(CameraCharacteristics.LENS_FACING); if (flashAvailable != null && flashAvailable && facingDirection != null && facingDirection == CameraCharacteristics.LENS_FACING_BACK) { return id; } } } catch (CameraAccessException e) { e.printStackTrace(); } return null; } -
添加这两个方法来处理手电筒模式:
public void clickLight(View view) { setFlashlight(mButtonLight.isChecked()); if (mButtonLight.isChecked()) { showNotification(); } } private void setFlashlight(boolean enabled) { mButtonLight.setChecked(enabled); try { mCameraManager.setTorchMode(mCameraId, enabled); } catch (CameraAccessException e) { e.printStackTrace(); } } -
最后,添加这个方法来创建通知:
private void showNotification() { Intent activityIntent = new Intent(this,MainActivity.class); activityIntent.setAction(ACTION_STOP); PendingIntent pendingIntent = PendingIntent.getActivity(this,0,activityIntent,0); final Builder notificationBuilder = new Builder(this).setContentTitle("Flashlight") .setContentText("Press to turn off the flashlight") .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentIntent(pendingIntent) .setVibrate(new long[]{DEFAULT_VIBRATE}) .setPriority(PRIORITY_MAX); NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); } -
你已经准备好在物理设备上运行应用程序了。如前所述,你需要一个运行安卓 6.0(或更高版本)且具有外向摄像头闪光灯的设备。
它是如何工作的...
由于这个食谱使用了与*灯光、动作和声音——吸引用户的注意!*相同的闪光灯代码,我们将跳到showNotification()方法。大部分通知构建器的调用与之前的示例相同,但有两个重要的区别:
.setVibrate()
.setPriority(PRIORITY_MAX)
提示
除非将优先级设置为 HIGH(或更高)并使用振动或声音,否则通知不会被升级为浮动通知。
请注意来自开发者文档的以下内容,文档地址为:developer.android.com/reference/android/app/Notification.html#headsUpContentView:
"系统界面可以自行决定是否将此作为浮动通知显示。"
我们像之前一样创建了一个 PendingIntent,但在这里我们通过以下方式设置动作:
activityIntent.setAction(ACTION_STOP);
我们在 AndroidManifest 文件中将应用设置为只允许单个实例,因为当用户点击通知时,我们不希望启动应用的新实例。我们创建的 PendingIntent 设定了动作,我们在 onNewIntent() 回调中检查这个动作。如果用户在没有点击通知的情况下打开应用,他们仍然可以使用 ToggleButton 关闭闪光灯。
还有更多内容...
就像之前在使用自定义布局创建 Toast 的方法中一样,我们也可以在通知中使用自定义布局。在构建器上使用以下方法来指定布局:
headsupContentView()
另请参阅
- 请参考 灯光、动作和声音 —— 获取用户的注意! 的方法