3
使用这个代码片段开始练习
也可以参考 Codepath 教程
高级内容补充:
你是否在思考ArrayAdapter’s 的 getView() 方法和CursorAdapter 的 newView() 和 bindView() 方法?
你可以查看 CursorAdapter 类的源码. getView() 方法依然存在, 但是它实际根据是否存在列表项能够被循环使用,来决定调用 newView() 或 bindView(),如果 convertView 为空,那么我们需要创建新的列表项,如果 convertView 不为空,那么我们可以循环使用旧的列表项。
因此,作为一个开发者,我们不需要重载 CursorAdapter 的 getView() 方法. 我们可以仅仅重载 newView() 和 bindView() 方法, 剩下的工作交给适配器来完成!
4
空视图是在 ListView 中无项目时展示的视图。在没有数据的情况下不是在应用中展示一个空白屏幕,而是通过有吸引力的图像或描述性文字来提升用户体验。 文字甚至可以提示用户添加一些数据。
在我们的 Pets 应用中,我们想要在 ListView 中没有要显示的宠物时,设置以下空视图。
设置空视图非常简单。
第 1 步:在 ListView 旁边创建空视图
首先在你的布局中创建空视图。在我们的案例中,应该有两个 TextViews 和一个 ImageView,像这样对齐。并给空视图一个 id“@+id/empty_view”,以便我们之后在 Java 代码中引用。
<red lines><!-- Empty view for the list --><RelativeLayout android:id="@+id/empty_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true"> <ImageView android:id="@+id/empty_shelter_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:src="@drawable/ic_empty_shelter"/> <TextView android:id="@+id/empty_title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/empty_shelter_image" android:layout_centerHorizontal="true" android:fontFamily="sans-serif-medium" android:paddingTop="16dp" android:text="@string/empty_view_title_text" android:textAppearance="?android:textAppearanceMedium"/> <TextView android:id="@+id/empty_subtitle_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/empty_title_text" android:layout_centerHorizontal="true" android:fontFamily="sans-serif" android:paddingTop="8dp" android:text="@string/empty_view_subtitle_text" android:textAppearance="?android:textAppearanceSmall" android:textColor="#A2AAB0"/> </RelativeLayout>
对于文本,使用 strings.xml 资源文件。
<!-- Title text for the empty view, which describes the empty dog house image [CHAR LIMIT=50] --><string name="empty_view_title_text">It\'s a bit lonely here...</string> <!-- Subtitle text for the empty view that prompts the user to add a pet [CHAR LIMIT=50] --> <string name="empty_view_subtitle_text">Get started by adding a pet</string>
第 2 步:添加空视图
在创建 ListView 时,你可以使用 id 指定一个空视图。使用我们之前设置的 id,用 findViewById() 方法获取视图,然后使用 ListView 的 setEmptyView() 方法设置空视图。
// Find the ListView which will be populated with the pet data
ListView petListView = (ListView) findViewById(R.id.list);// Find and set empty view on the ListView, so that it only shows when the list has 0 items.
View emptyView = findViewById(R.id.empty_view);petListView.setEmptyView(emptyView);
第 3 步:测试
删除数据或卸载并重装应用,使用一个空数据库启动。现在,你应该会看到空视图出现在屏幕上!
练习完成前后的差异。
6
参阅此教程 或此文档。
提示:添加需要从接口实现的方法的键盘快捷键:Ctrl + I 提示:将 CursorAdapter 作为全局变量 提示:对于 onLoadFinished() 和 onLoaderReset() 方法,你可以使用 CursorAdapter 的 swapCursor() 方法更改数据源 Cursor。
注:插入新宠物(从虚拟菜单项或编辑器)不会自动更新宠物列表(直到下一个编码任务前)。你需要强制关闭应用并重新打开它,才能看到宠物列表更新。
如果你在想为什么 projection 需要包含 _id,请参见CursorAdapter 类的描述。CursorAdapter 假设 Cursor 包含一个名为 _id 的列。
7
目前,我们的 CursorLoader 有一个小缺陷,那就是它不会在底层 SQLite 数据库变化时进行自动更新。
为什么列表之前可以更新,而现在不行?我们返回去看看旧的代码。
在之前,displayDatabaseInfo 在 onStart() 内及在菜单项被单击时调用。这可以完全确保数据始终为最新状态,但它也会在主线程上多次进行不必要的数据加载。
在主线程上进行加载会降低应用的速度,如我们之前所讲,而且进行无用的加载更是浪费资源。
那么,它什么时候的数据加载是无用的呢?是这样,每次当应用被旋转或你导航到别处一会儿并返回后 onStart 就会被触发。 如果你旋转应用或导航到别处并返回后,你真的需要再次从数据库获取数据吗?数据库中有任何东西发生变化了吗?答案是没有,数据库未发生任何变化,所以你并不需要重新加载数据。
这实际上就是 CursorLoader 主要用于解决的问题之一:我们想追踪数据是否已加载,且如果已加载,则避免重新加载,除非有必要。
很显然我们不需要在每次调用 onStart 时重新加载方法。那么我们何时需要重新加载数据呢?
解答
我们仅需要在 SQLite 数据库中的某些数据发生变化时更新Cursor。
用户关闭和重新打开应用、旋转应用和滚动 ListView 实际上不会更改数据库中的数据,我们这时就不需要重新加载数据。
8
这个 教程 展示了何时调用notifyChange()
Cursor setNotificationUri() 方法
9
注意:在这里,编辑器尚不会显示数据库中特定宠物的数据。这将在之后的步骤中实现。
提示 1:通过 setOnItemClickListener() 方法将 OnItemClickListener 设置到 ListView。
提示 2:创建特定宠物内容 URI,使用 Uri.withAppendedId() 方法。
提示 3:在 AndroidManifest.xml 中,你可以删除 EditorActivity 活动元素中的 label 属性,因为应用栏中新宠物和现有宠物情况的活动标题被在 Java 代码中通过编程方式覆写了。
10
好的,现在我们的 Editor Activity 中有了宠物的 URI。在此情况下,我们还应从内容提供程序 中加载宠物数据。我们可以使用 CursorLoader 进行加载。
这与我们上次使用 CursorLoader 的区别这次我们不是获取Cursor并将其放入 CursorAdapter,而是获取Cursor中的所有项然后使用它们来填充 EditText 字段。
我们使用的步骤大体和之前一样,只是当我们创建加载器时,URI 将为单个宠物的,而非全部宠物的。
然后我们从 onLoadFinished 中获得Cursor,这时候我们不是使用 Cursor Adapter,而是更新所有输入,即包含宠物值的 editTexts 和性别 spinner。
最后在 onLoaderReset 方法中,我们应清除输入字段。
现在花一些时间来实际操作吧。
你的步骤:
- 实现加载器回调接口。
- 初始化加载器。
- 编写加载器回调方法 onCreateLoader 的代码;确保它使用的是单个宠物的 URI。
- 编写加载器回调方法 onLoadFinished 的代码,并使用宠物数据更新输入。
- 编写加载器回调方法 onLoaderReset 的代码,然后清除输入字段。
提示 1:当你从加载器收到Cursor结果,记得在开始从中提取列值前,将Cursor移到位置 0。
提示 2:你可以使用 Spinner 的 setSelection() 方法来设置下拉菜单 Spinner。
解答
首先实现加载器回调,然后使用 Ctrl + I 热键来获取所有加载器回调方法:
public class EditorActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
接下来,使用此代码初始化加载器:
getLoaderManager().initLoader(EXISTING_PET_LOADER, null, this);
在 onCreateLoader() 中,我将创建一个新的 CursorLoader,传入 uri 和 projection。我需要从 onCreate() 获取 uri,所以我需要将它放在一个名为 mCurrentPetUri
的实例变量:
/** Content URI for the existing pet (null if it's a new pet) */
private Uri mCurrentPetUri;...@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { // Since the editor shows all pet attributes, define a projection that contains // all columns from the pet table String[] projection = { PetEntry._ID, PetEntry.COLUMN_PET_NAME, PetEntry.COLUMN_PET_BREED, PetEntry.COLUMN_PET_GENDER, PetEntry.COLUMN_PET_WEIGHT }; // This loader will execute the ContentProvider's query method on a background thread return new CursorLoader(this, // Parent activity context mCurrentPetUri, // Query the content URI for the current pet projection, // Columns to include in the resulting Cursor null, // No selection clause null, // No selection arguments null); // Default sort order }
当宠物的数据加载到游标后,onLoadFinished() 将被调用。在这里,我首先要将游标移到第一个项的位置。尽快它只有一个项,并从位置 -1 开始。
// Proceed with moving to the first row of the cursor and reading data from it// (This should be the only row in the cursor)if (cursor.moveToFirst()) {
然后,我将获得每个数据项的索引,然后使用所有和 get() 方法抓取实际整数和字符串,以从游标中获得数据。
if (cursor.moveToFirst()) {// Find the columns of pet attributes that we're interested inint nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);int genderColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_GENDER); int weightColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_WEIGHT); // Extract out the value from the Cursor for the given column index String name = cursor.getString(nameColumnIndex); String breed = cursor.getString(breedColumnIndex); int gender = cursor.getInt(genderColumnIndex); int weight = cursor.getInt(weightColumnIndex);
对于每一个 textView,我将设置适当的文本。
// Update the views on the screen with the values from the database mNameEditText.setText(name); mBreedEditText.setText(breed); mWeightEditText.setText(Integer.toString(weight));
对于 Spinner,我将使用一个 switch 语句来使用 setSelection 方法设置它的状态。
// Gender is a dropdown spinner, so map the constant value from the database// into one of the dropdown options (0 is Unknown, 1 is Male, 2 is Female).// Then call setSelection() so that option is displayed on screen as the current selection.switch (gender) {case PetEntry.GENDER_MALE: mGenderSpinner.setSelection(1); break; case PetEntry.GENDER_FEMALE: mGenderSpinner.setSelection(2); break; default: mGenderSpinner.setSelection(0); break; }
练习完成前后的差异
11
我们的“编辑宠物”(Edit Pet) 版本页面缺失一项重大功能,那就是实际编辑宠物。
目前,当你更改现有宠物的任何值时,并不会更新宠物,而是会保存宠物的一个副本,这并非我们想要的。
解决方法就是使“编辑宠物”模式更新当前宠物,使“添加宠物”模式插入新宠物。
这次我们不使用“insertPet”方法,我们将它改名或“重构”为“savePet”。在这里,就像我们之前一样,你可以判断 EditorActivity 是“插入”模式还是“编辑”模式。如果为编辑模式,你需要使用内容提供程序 执行一个“更新”操作。
好的,现在来试一试吧,更新 savePet 方法。完成后,EditActivity 应该不会创建宠物的副本,而是更新现有宠物。在你按下 FAB 按钮来插入新宠物时,它也应正确执行。在这两种情况下,你的应用都应显示 toast 消息。
提示:如何判断我们是在“插入新宠物”还是“编辑现有宠物”?你可以在设置 EditorActivity 标题时执行此检查。
注意:确保在 strings.xml 文件中声明 toast 消息的字符串。
ContentResolver update() 示例。
练习完成前后的差异.
无论你是第一次更新宠物还是创建宠物,你都需要使用 content values 对象。创建它的方式还是跟之前一样。创建好后,你需要根据是要编辑现有宠物还是插入新宠物来进行不同操作。
你可以使用设置标题时所用的方法,通过检查 mCurrentPetUri 是否为空值来区分编辑模式和插入模式。如果为空值,那么就是新宠物,你可以运行此方法中原有的代码。
if (mCurrentPetUri == null) {
如果 uri 不为空值,这意味着你在更新现有宠物。你可以使用 ContentResolver 的 update 方法来更新宠物。你更新的内容的 URI 为 mCurrentPetUri,这会简化代码编写。
} else {// Otherwise this is an EXISTING pet, so update the pet with content URI: mCurrentPetUri// and pass in the new ContentValues. Pass in null for the selection and selection args// because mCurrentPetUri will already identify the correct row in the database that// we want to modify. int rowsAffected = getContentResolver().update(mCurrentPetUri, values, null, null);
现在,它会返回已更新的列数。要显示更新是否成功,我们可以检查已更新的列数。若为 0,那么更新不成功,则我们可以显示一条错误 toast 消息。若为相反情况,则显示成功 toast 消息。
// Show a toast message depending on whether or not the update was successful.if (rowsAffected == 0) {// If no rows were affected, then there was an error with the update.Toast.makeText(this, getString(R.string.editor_update_pet_failed), Toast.LENGTH_SHORT).show(); } else { // Otherwise, the update was successful and we can display a toast. Toast.makeText(this, getString(R.string.editor_update_pet_successful), Toast.LENGTH_SHORT).show(); }
12
干得不错!
EditorActivity 在编辑模式下的主要功能已完成。
后续操作
我们依然需要实现 delete 和解决几个 bug。例如,当你为插入新宠物而打开 EditorActivity 时,你在没有输入任何东西时不小心点了保存按钮……这时候就会发生崩溃。
阅读错误消息,我们得知这是因为你没有为体重输入数字;基本上是因为它无法将空白值转换为数字。
通常情况下,我们应在 UI 中要求用户在尝试将宠物信息存入数据库之前先输入一些宠物信息。而所有空白值应视为用户不小心按下保存键,而非作为新宠物进行保存。
你的任务
你的任务就是解决这个 bug。为此,首先你得检查输入是否为空字符串。你可以使用 TextUtils.isEmpty()) 方法来检查一个字符串是否全由空格或空字符串构成。如果所有的输入都为空字符串,且根据 GENDER UNKNOWN 消息性别未更新,则 savePet 方法会直接返回但不包含任何内容。
此外,当你在保存新宠物时没有提供体重,则宠物的给定体重将为 0,而不会导致应用崩溃。
提示 1:调用 finish() 方法来终止 activity。
提示 2:如果输入字符串为空值或空字符串,TextUtils.isEmpty(String s) 返回 true。
解答
我们要做的所有更改都在 EditorActivity.java 文件中进行,特别是在 savePet() 方法中。
要解决第一个问题,在我们获得所有值后但在创建 ContentValues 对象之前,我将添加一个 if 语句来检查它们是否为空字符串及性别是否为未知。如果是,我将不再执行剩余的方法或插入宠物,而是直接返回。
if (mCurrentPetUri == null &&TextUtils.isEmpty(nameString) && TextUtils.isEmpty(breedString) &&TextUtils.isEmpty(weightString) && mGender == PetEntry.GENDER_UNKNOWN) {return;}
在将体重放入 content value 对象之前,我要解决的下一个问题是:如果用户未指定宠物体重,应将其设为 0,而不是导致崩溃。所以在代码中,我们将体重的默认值设为 0,然后在用户输入体重后,我会将其更改为输入的整数值。
// If the weight is not provided by the user, don't try to parse the string into an// integer value. Use 0 by default.int weight = 0;if (!TextUtils.isEmpty(weightString)) { weight = Integer.parseInt(weightString); } values.put(PetEntry.COLUMN_PET_WEIGHT, weight);
做了这些更改后,应用便不会崩溃,也不会保存完全空白的宠物信息。非常棒!
练习完成前后的差异
13
警告用户
你可能注意到了,按下 EditorActivity 的“上一个”(UP) 或“返回”(back) 按钮会使你离开 EditorActivity,但不会保存你的更改。你想要明确保存数据库更改的方法,那么我们只在用户点击“保存”(Save) 按钮的时候保存更改。
那么,当用户在添加一些编辑时不小心按下“上一个”(UP) 或“返回”(back) 按钮时会怎样呢?为避免用户丢失工作,我们可以跳出一个对话框,警告用户其在离开编辑器时有未保存的更改。
下面是联系人应用中的警告演示。当你输入新的联系人信息,然后在未保存的情况下点击了“上一个”(UP) 按钮时,你会看到此行为:
期望的行为
在我们的应用中,我们所期望的行为是跳出一个对话框,用消息“是否放弃更改并退出编辑”(Discard your changes and quit editing?) 来警告用户。用户可以选择“继续编辑”(Keep Editing)(即留在此活动)或“放弃”(Discard)(即结束活动)。
步骤
第 1 步:监听是否进行了更改
第一步是决定什么时候显示“放弃更改”对话框。这应仅在用户更改了表格的某一个部分时发生。你可以做的是创建一个名为 mPetHasChanged 的 boolean,如果用户更新了 pet 表的任何部分则为 true。
private boolean mPetHasChanged = false;
你可以添加 OnTouchListener 来检查是否发生了更改:
private View.OnTouchListener mTouchListener = new View.OnTouchListener() {@Overridepublic boolean onTouch(View view, MotionEvent motionEvent) { mPetHasChanged = true; return false; } };
在 onCreate
中:
mNameEditText.setOnTouchListener(mTouchListener);mBreedEditText.setOnTouchListener(mTouchListener); mWeightEditText.setOnTouchListener(mTouchListener); mGenderSpinner.setOnTouchListener(mTouchListener);
第 2 步:编写创建“放弃更改”对话框的方法
我们在下面编写一个创建此对话框的方法:
private void showUnsavedChangesDialog(DialogInterface.OnClickListener discardButtonClickListener) {// Create an AlertDialog.Builder and set the message, and click listeners// for the positive and negative buttons on the dialog.AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setMessage(R.string.unsaved_changes_dialog_msg);builder.setPositiveButton(R.string.discard, discardButtonClickListener);builder.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() {public void onClick(DialogInterface dialog, int id) {// User clicked the "Keep editing" button, so dismiss the dialog// and continue editing the pet.if (dialog != null) {dialog.dismiss();}}});// Create and show the AlertDialogAlertDialog alertDialog = builder.create();alertDialog.show(); }
此代码使用 AlertDialogBuilder 创建了一个 AlertDialog。此方法接受放弃按钮的 OnClickListener。我们这样做是因为点击“返回”(back) 或“上一个”(up) 的行为略有不同。
并且注意,我们还使用了很多 R.string,你可以添加这些:
Discard your changes and quit editing?DiscardKeep Editing
第 3 步:连接“返回”(Back)按钮
这是“返回”按钮的代码。你需要覆盖此活动的正常返回按钮。如果宠物发生了变化,你建立会关闭当前活动的放弃点击侦听器。然后将此侦听器传入刚创建的 showUnsavedChangesDialog
方法。
@Override public void onBackPressed() {// If the pet hasn't changed, continue with handling back button pressif (!mPetHasChanged) {super.onBackPressed();return;}// Otherwise if there are unsaved changes, setup a dialog to warn the user.// Create a click listener to handle the user confirming that changes should be discarded.DialogInterface.OnClickListener discardButtonClickListener =new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int i) {// User clicked "Discard" button, close the current activity.finish();}};// Show dialog that there are unsaved changesshowUnsavedChangesDialog(discardButtonClickListener); }
第 4 步:连接“上一个”(Up)按钮
这里是“上一个”按钮的代码。它在 onOptionsItemSelected
方法中。如果宠物发生变化,你建立一个会导航至“上一个”的放弃点击侦听器。然后将此侦听器传入刚创建的 showUnsavedChangesDialog
方法。
case android.R.id.home:// If the pet hasn't changed, continue with navigating up to parent activity// which is the {@link CatalogActivity}.if (!mPetHasChanged) {NavUtils.navigateUpFromSameTask(EditorActivity.this);return true;}// Otherwise if there are unsaved changes, setup a dialog to warn the user.// Create a click listener to handle the user confirming that// changes should be discarded.DialogInterface.OnClickListener discardButtonClickListener =new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int i) {// User clicked "Discard" button, navigate to parent activity.NavUtils.navigateUpFromSameTask(EditorActivity.this);}};// Show a dialog that notifies the user they have unsaved changesshowUnsavedChangesDialog(discardButtonClickListener);return true;
练习完成前后的差异
有用链接:
要向“返回”(back)按钮被点击的情况添加行为,参阅此 StackOverflow 帖子。
要向“上一个”(Up)按钮被点击的情况添加行为,参阅此文章。你还需要向 android.R.id.home 按钮被点击的情况添加代码。
如何创建 AlertDialog。
14
如果宠物还不存在,则无法删除它
我们当前的代码还有一个“问题”,那就是当你处于 EditorActivity 的插入模式时,会在菜单中看到一个“删除”(Delete) 选项。由于你当前在插入宠物,因此没有宠物可供删除,那么这个选项是多余的。
我们来解决这个问题。我们要删除“插入模式”中的删除选项。
如何选择运行应用时的菜单
当你第一次打开活动时,会调用 onCreateOptionsMenu
方法。它会用所有可见的菜单项填充菜单。要更改它,你需要做的是在确定它是一个新宠物后(mPetContentUri 为空值),你要使选项菜单无效。
所以在 onCreate 中……
if (mCurrentPetUri == null) {// This is a new pet, so change the app bar to say "Add a Pet"setTitle(getString(R.string.editor_activity_title_new_pet));// Invalidate the options menu, so the "Delete" menu option can be hidden.// (It doesn't make sense to delete a pet that hasn't been created yet.) invalidateOptionsMenu(); } else { //other stuff }
然后,onPrepareOptionsMenu
会被调用,你可以在它是新宠物时隐藏删除菜单选项来修改菜单对象。 此操作的代码如下:
@Overridepublic boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // If this is a new pet, hide the "Delete" menu item. if (mCurrentPetUri == null) { MenuItem menuItem = menu.findItem(R.id.action_delete); menuItem.setVisible(false); } return true; }
练习完成前后的差异
有用链接:
Android开发文档:菜单选项
Stack Overflow 提问:如何改变菜单项
15
我们的最后一项重大任务是实现删除宠物的能力。此功能在应用的两个地方:第一个是在 EditorActivity 页面的菜单中,当你更新宠物时会用到。 第二个是在 CatalogActivity 的菜单中,在此处,删除 (delete) 选项可用于删除所有宠物,你在测试数据库时可以使用。
首先我们来看 EditorActivity 中的 delete 方法。我们已添加了使它仅在“编辑宠物”模式出现的代码。现在我想要实现的功能是,当我们选择删除宠物时,出现一个对话框, 即按下删除按钮应出现一条 toast 消息“已删除宠物”(Pet deleted) ,并从数据库中删除宠物。
我们复制此代码片段。它包含 tring.xml 的一些新字符串,也包含生成对话框的代码,位于一个名为“showDeleteConfirmationDialog”的方法和一个空的“deletePet”方法中。
此对话框有两个 onClick 侦听器,一个用于这里显示的“删除”(delete) 按钮,另一个用于这里显示的“取消”(cancel) 按钮。Delete 对话框调用空的“deletePet”方法,你需要在这个方法中添加代码来实际删除当前宠物。
你的任务:
首先,找到代码中正确的位置,以当你从下拉菜单中选择“删除宠物”(Delete Pet) 时,它会打开 DeleteConfirmationDialog。
接下来,填上deletePet 方法以删除当前宠物。你应仅在“编辑模式”中删除宠物。你还应显示一条 toast 消息,说明是否删除了宠物,方法和我们之前用 toast 消息表明是否成功更新了宠物一样。
ContentResolver delete() 示例
解答
在复制代码片段后,首先我们要通过在按下删除按钮时调用 showDeleteConfirmationDialog 方法来确保触发它。方法是前往 onOptionsItemSelected 并在 action.delete 下添加对 showDeleteConfirmationDialog 的调用。
// Respond to a click on the "Delete" menu optioncase R.id.action_delete:// Pop up confirmation dialog for deletionshowDeleteConfirmationDialog();
showDeleteConfirmationDialog 会创建一个对话框,在删除按钮被按下时调用 deletePet。首先,我确保我们是对现有宠物执行删除。因为对数据库中尚不存在的新宠物执行删除是没有意义的。所以我检查是否存在现有宠物(mCurrentPetUri 是否等于 null)。然后使用 content resolver 和当前的宠物 uri 删除宠物。
/*** Perform the deletion of the pet in the database.*/
private void deletePet() {// Only perform the delete if this is an existing pet.if (mCurrentPetUri != null) { // Call the ContentResolver to delete the pet at the given content URI. // Pass in null for the selection and selection args because the mCurrentPetUri // content URI already identifies the pet that we want. int rowsDeleted = getContentResolver().delete(mCurrentPetUri, null, null);
delete 方法和 update 方法一样,会返回被删除的行数。我可以通过检查被删除的行数是否为 0 来确定删除成功与否。若为 0,则删除失败,我会显示一条 toast 消息,说明“删除宠物时出错”(Error with deleting pet)。 否则,操作将是成功的,且我跳出一条 toast 消息说“宠物已删除”(Pet deleted)。
// Show a toast message depending on whether or not the delete was successful.if (rowsDeleted == 0) {// If no rows were deleted, then there was an error with the delete.Toast.makeText(this, getString(R.string.editor_delete_pet_failed), Toast.LENGTH_SHORT).show(); } else { // Otherwise, the delete was successful and we can display a toast. Toast.makeText(this, getString(R.string.editor_delete_pet_successful), Toast.LENGTH_SHORT).show(); }
完成操作后,调用 finish() 方法关闭此 activity。
// Close the activityfinish();
到此,我们就能成功从 EditorActivity 中删除宠物了!
练习完成前后的差异.
16
现在,我要你添加当用户从 CatalogActivity 上的溢出菜单点击“删除所有宠物”(Delete All Pets) 选项时,用于删除所有宠物的代码。无需添加确认消息,但是如果你想,也可以自行实现。
完成后,菜单看起来应该是这样的:
这里的变更较小,所以我就不多做讲解,由你自己来完成。
Android 中的 菜单选项。
解答
对于此练习,我们在 CatalogActivity.java 文件中进行。
此菜单按钮与 R.id.action_delete_all_entries 关联,所以在 onOptionsMenuSelected 方法的该 case 下,我将添加对名为“deleteAllPets()”方法的调用。
case R.id.action_delete_all_entries:deleteAllPets();return true;
然后在该方法中,我将使用 ContentResolver 的 delete 方法,并传入PetEntry.CONTENT_URI。为什么要用内容 URI?因为这是一个一般 __/pets uri,在我们的 内容提供程序中将删除所有宠物。
/*** Helper method to delete all pets in the database.*/
private void deleteAllPets() {int rowsDeleted = getContentResolver().delete(PetEntry.CONTENT_URI, null, null); Log.v("CatalogActivity", rowsDeleted + " rows deleted from pet database"); }
此菜单按钮与R.id.action_delete_all_entries 关联,所以在 onOptionsMenuSelected 方法的该 case 下,我将添加对名为“deleteAllPets()”方法的调用。
练习完成前后的差异.
17
对于我们的最后一项更新,你需要解决 UI 中的另一个奇怪的行为。当品种未知时,宠物列表看起来总觉得缺点什么。因为宠物名下就是一片空白。
在未提供品种信息的情况下,比起留白,显示一个语句“品种未知”(Unknown breed) 会提供更好的用户体验。
完成此步后应用看起来应该是这个样子的:
注意,这只是 CatalogActivity 中的一个 UI 变化,文本“Unknown breed”不应保存在数据库中。如果你在编辑器中打开此宠物,“品种”(breed) 字段应该是空白的:
提示:如果输入字符串为空值或空字符串TextUtils.isEmpty(String s) 将返回 true。
解答
首先,由于我们要添加字符串“Unknown breed”,我们在 strings.xml 文件中保存它。
<!-- Label for the pet's breed if the breed is unknown [CHAR LIMIT=20] -->
<string name="unknown_breed">Unknown breed</string>
接下来,我们移至 PetCursorAdapter.java 文件。当我们在 CatalogActivity 中显示宠物时,我们要检查是否有使用 TextUtils.isEmpty 方法的品种。如果没有,我们将列表项的 TextView 设为显示“Unknown breed”。
In PetCursorAdapter.java:
/*** This method binds the pet data (in the current row pointed to by cursor) to the given* list item layout. For example, the name for the current pet can be set on the name TextView* in the list item layout.** @param view Existing view, returned earlier by newView() method* @param context app context* @param cursor The cursor from which to get the data. The cursor is already moved to the* correct row.*/
@Override public void bindView(View view, Context context, Cursor cursor) { // Find individual views that we want to modify in the list item layout TextView nameTextView = (TextView) view.findViewById(R.id.name); TextView summaryTextView = (TextView) view.findViewById(R.id.summary); // Find the columns of pet attributes that we're interested in int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME); int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED); // Read the pet attributes from the Cursor for the current pet String petName = cursor.getString(nameColumnIndex); String petBreed = cursor.getString(breedColumnIndex); // If the pet breed is empty string or null, then use some default text // that says "Unknown breed", so the TextView isn't blank. if (TextUtils.isEmpty(petBreed)) { petBreed = context.getString(R.string.unknown_breed); } // Update the TextViews with the attributes for the current pet nameTextView.setText(petName); summaryTextView.setText(petBreed); }
练习完成前后的差异.