安卓应用开发秘籍第二版-二-

107 阅读52分钟

安卓应用开发秘籍第二版(二)

原文:zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:菜单

在本章中,我们将涵盖以下主题:

  • 创建选项菜单

  • 在运行时修改菜单和菜单项

  • 为视图启用上下文操作模式

  • 在 ListView 中使用上下文批量模式

  • 创建弹出菜单

引言

Android 操作系统是一个不断变化的环境。最早的 Android 设备(在 Android 3.0 之前)需要有一个硬件菜单按钮。尽管现在不再需要硬件按钮,但菜单的重要性并未降低。实际上,Menu API 已经扩展,现在支持三种不同类型的菜单:

  • 选项菜单和操作栏:这是标准的菜单,用于应用程序的全局选项。使用它来添加额外的功能,如搜索、设置等。

  • 上下文模式上下文操作模式):这通常通过长按激活。(可以把它看作是在桌面上右键点击。)这用于对按下的项目执行操作,如回复电子邮件或删除文件。

  • 弹出菜单:这为附加操作提供了一个弹出式选择(如下拉菜单)。菜单选项不是用来影响按下的项目,而是像前面描述的那样使用上下文模式。例如,点击分享按钮并获得额外的分享选项列表。

菜单资源与其他 Android UI 组件类似;它们通常在 XML 中创建,但也可以在代码中创建。我们第一个食谱,如下一节所示,将展示 XML 菜单格式以及如何扩展它。

创建选项菜单

在我们实际创建和显示菜单之前,先来看一下菜单的最终效果。以下是 Chrome 浏览器菜单部分的截图:

创建选项菜单

最明显的特点是,菜单会根据屏幕大小显示不同的样子。默认情况下,菜单项将被添加到溢出菜单中——这就是当你点击最右侧边缘的三个点时看到的菜单。

菜单通常在资源文件中使用XML创建(像许多其他 Android 资源一样),但它们存储在res/menu目录中,尽管也可以在代码中创建。要创建菜单资源,请使用如下所示的<menu>元素:

<menu >
</menu>

<item>元素定义了每个单独的菜单项,并包含在<menu>元素中。一个基本的菜单项如下所示:

<item 
    android:id="@+id/settings"
    android:title="@string/settings" />

最常见的<item>属性如下:

  • id:这是标准的资源标识符

  • title:这表示要显示的文本

  • icon:这是一个可绘制的资源

  • showAsAction:这已经如下解释(见下一段

  • enabled:默认情况下是启用的

让我们更详细地看一下showAsAction

showAsAction属性控制菜单项的显示方式。选项包括以下内容:

  • ifRoom:如果空间足够,此菜单项应包含在操作栏中

  • withText:表示应显示标题和图标

  • never:表示菜单项不应包含在操作栏中;始终在溢出菜单中显示。

  • always:表示菜单项应始终包含在操作栏中(空间有限,请谨慎使用)。

    注意

    可以使用管道符(|)分隔组合多个选项,例如 showAsAction="ifRoom|withText"

了解了菜单资源的基础知识后,我们现在准备创建一个标准的选项菜单并充气它。

准备就绪

使用 Android Studio 创建一个名为 OptionsMenu 的新项目。选择默认的 Phone & Tablet 选项,并在提示 Activity 类型时选择 Empty Activity。由于向导默认不创建 res/menu 文件夹,在继续之前,请导航至 File | New | Directory 来创建它。

如何操作...

按照前一部分描述创建新项目后,您就可以创建菜单了。但是,首先,我们将在 strings.xml 文件中为菜单标题添加一个字符串资源。在创建菜单的 XML 时,我们将使用新的字符串作为菜单标题。以下是步骤:

  1. 首先打开 strings.xml 文件,并向 <resources> 元素中添加以下 <string> 元素:

    <string name="menu_settings">Settings</string>
    
  2. res/menu 目录中创建一个新文件,并将其命名为 menu_main.xml

  3. 打开 menu_main.xml 文件,并添加以下 XML 来定义菜单:

    <?xml version="1.0" encoding="utf-8"?>
    <menu
    
        >
        <item android:id="@+id/menu_settings"
            android:title="@string/menu_settings"
            app:showAsAction="never">
        </item>
    </menu>
    
  4. 创建好菜单后,我们只需在 ActivityMain.java 中重写 onCreateOptionsMenu() 方法来充气菜单:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
    
  5. 在设备或模拟器上运行程序,以查看操作栏中的菜单。

工作原理...

这里有两个基本步骤:

  1. 在 XML 中定义菜单。

  2. 在 Activity 创建时充气菜单。

作为良好的编程习惯,我们在 strings.xml 文件中定义字符串,而不是在 XML 中硬编码。然后在步骤 3 中使用标准的 Android 字符串标识符为菜单设置标题。由于这是一个“设置”菜单项,我们不希望它在操作栏中显示。为确保它从不显示,请使用 showAsAction="never"

定义好菜单后,我们将在步骤 4 中使用菜单解析器在 Activity 创建期间加载菜单。注意 R.menu.menu_main 菜单资源语法吗?这就是为什么我们在 res/menu 目录中创建 XML,以便系统知道这是一个菜单资源。

在步骤 4 中,我们使用了 app:showAsAction 而不是 Android: android:showAsAction。这是因为我们使用了 AppCompat 库(也称为 Android 支持库)。默认情况下,Android Studio 新项目向导会在项目中包含支持库。

还有更多...

如果你按照第 5 步运行了程序,那么当你按下菜单溢出按钮时,你一定看到了 设置 菜单项。但仅此而已。没有其他反应。显然,如果应用程序不响应它们,菜单项就没有什么用处。通过 onOptionsItemSelected() 回调来响应 选项 菜单。

向应用程序添加以下方法,以在选中设置菜单时显示 Toast:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == R.id.menu_settings) {
        Toast.makeText(this, "Settings", Toast.LENGTH_LONG).show();
    } else {
        return super.onContextItemSelected(item);
    }
    return true;
}

就这样。你现在拥有了一个可工作的菜单!

提示

如前一个示例所示,处理完回调后返回 true;否则,按照 else 语句调用超类。

使用菜单项启动活动

在此示例中,我们展示了一个 Toast 以便我们可以看到一个工作示例;然而,如果需要,我们同样可以轻松地启动一个新的活动。就像你在第一章的使用 Intent 对象启动新活动食谱中所做的那样,创建一个 Intent 并使用 startActivity() 调用它。

创建子菜单

子菜单 的创建和访问几乎与其他菜单元素完全相同,并且可以放置在提供的任何菜单中,尽管它们不能放置在其他的子菜单中。要定义子菜单,请在 <item> 元素中包含一个 <menu> 元素。以下是此食谱的 XML 形式,其中添加了两个子菜单项:

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

    >
    <item android:id="@+id/menu_settings
        android:title="@string/menu_settings"
        app:showAsAction="never">
        <menu>
            <item android:id="@+id/menu_sub1"
                android:title="Storage Settings" />
            <item android:id="@+id/menu_sub2"
                android:title="Screen Settings" />
        </menu>
    </item>
</menu>

分组菜单项

Android 支持的另一个菜单特性是分组菜单项。Android 为组提供了几种方法,包括以下几种:

  • setGroupVisible(): 显示或隐藏所有项目

  • setGroupEnabled(): 启用或禁用所有项目

  • setGroupCheckable(): 设置可勾选行为

提示

Android 会将带有 showAsAction="ifRoom" 的所有分组项目保持在一起。这意味着具有 showAsAction="ifRoom" 的组中的所有项目将位于操作栏中,或者所有项目都位于溢出菜单中。

要创建一个组,请将 <item> 菜单项元素添加到 <group> 元素中。以下是使用此食谱中的菜单 XML 的示例,其中包含两个分组中的附加项:

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

    >

    <group android:id="@+id/group_one" >
        <item android:id="@+id/menu_item1"
            android:title="Item 1"
            app:showAsAction="ifRoom"/>
        <item android:id="@+id/menu_item2"
            android:title="Item 2"
            app:showAsAction="ifRoom"/>
    </group>
    <item android:id="@+id/menu_settings"
        android:title="@string/menu_settings"
        app:showAsAction="never"/>
</menu>

另请参阅

在运行时修改菜单和菜单项

尽管已经多次提到,但在 XML 中创建 UI 而非在 Java 中被认为是“最佳”的编程实践。仍然有些时候你可能需要在代码中完成这一操作。特别是如果你想根据某些外部条件让菜单项可见(或可用)时。菜单可以包含在资源文件夹中,但有时候你需要代码来执行逻辑。例如,如果你想仅在用户登录应用时提供上传菜单项。

在此食谱中,我们将仅通过代码创建和修改菜单。

准备就绪

在 Android Studio 中创建一个新项目,将其命名为RuntimeMenu,使用默认的手机和平板选项。当提示添加活动时,选择空活动选项。由于我们将完全在代码中创建和修改菜单,因此我们不需要创建res/menu目录。

如何操作...

首先,我们将为我们的菜单项添加字符串资源以及一个切换菜单可见性的按钮。打开res/strings.xml文件并按照以下步骤操作:

  1. 在现有的<resources>元素中添加以下两个字符串:

    <string name="menu_download">Download</string>
    <string name="menu_settings">Settings</string>
    
  2. activity_main.xml中添加一个按钮,将onClick()设置为toggleMenu,如下所示:

    <Button
        android:id="@+id/buttonToggleMenu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Toggle Menu"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:onClick="toggleMenu"/>
    
  3. 打开ActivityMain.java文件,在类声明下方添加以下三行代码:

    private final int MENU_DOWNLOAD = 1;
    private final int MENU_SETTINGS = 2;
    private boolean showDownloadMenu = false;
    
  4. 添加以下方法供按钮调用:

    public void toggleMenu(View view) {
        showDownloadMenu=!showDownloadMenu;
    }
    
  5. 当活动首次创建时,Android 调用onCreateOptionsMenu()来创建菜单。以下是动态构建菜单的代码:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        menu.add(0, MENU_DOWNLOAD, 0, R.string.menu_download);
        menu.add(0, MENU_SETTINGS, 0, R.string.menu_settings);
        return true;
    }
    
  6. 为了最佳编程实践,不要使用onCreateOptionsMenu()来更新或更改你的菜单;而是使用onPrepareOptionsMenu()。以下是根据我们的标志更改下载菜单项可见性的代码:

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuItem menuItem = menu.findItem(MENU_DOWNLOAD);
        menuItem.setVisible(showDownloadMenu);
        return true;
    }
    
  7. 虽然从技术上讲这个菜谱不需要,但这段onOptionsItemSelected()代码展示了如何响应每个菜单项:

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_DOWNLOAD:
                Toast.makeText(this, R.string.menu_download, Toast.LENGTH_LONG).show();
                break;
            case MENU_SETTINGS:
                Toast.makeText(this, R.string.menu_settings, Toast.LENGTH_LONG).show();
                break;
            default:
                return super.onContextItemSelected(item);
        }
        return true;
    }
    
  8. 在设备或模拟器上运行程序以查看菜单更改。

工作原理...

我们为onCreateOptionsMenu()创建了一个重写,就像在之前的菜谱创建选项菜单中所做的那样。但我们没有膨胀现有的菜单资源,而是使用Menu.add()方法创建菜单。由于我们希望在以后修改菜单项以及响应菜单项事件,因此我们定义了自己的菜单 ID 并将它们传递给add()方法。

onOptionsItemSelected()被所有菜单项调用,因此我们获取菜单 ID 并根据我们创建的 ID 使用switch语句。如果我们正在处理菜单事件,则返回true;否则,我们将事件传递给超类。

菜单的更改发生在onPrepareOptionsMenu()方法中。为了模拟外部事件,我们创建了一个按钮来切换布尔标志。下载菜单的可见性由该标志决定。这就是你需要根据你设定的条件创建自定义代码的地方。你的标志可以使用当前玩家等级来设置,或者当有新等级准备发布时;你发送一个推送消息,从而启用菜单项。

还有更多...

如果我们希望这个下载选项容易被注意到以指示其是否可用呢?我们可以在onPrepareOptionsMenu()中添加以下代码(在返回语句之前),告诉 Android 我们希望操作栏中显示菜单:

menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);

现在如果你运行代码,你将在操作栏中看到下载菜单项,但行为并不正确。

之前,当我们没有在操作栏中显示菜单项时,每次打开溢出菜单,安卓都会调用 onPrepareOptionsMenu() 以更新可见性。为了纠正这种行为,请在由按钮调用的 toggleMenu() 方法中添加以下代码行:

invalidateOptionsMenu();

invalidateOptionsMenu() 调用告诉安卓我们的选项菜单不再有效,这将强制调用 onPrepareOptionsMenu(),从而实现我们预期的行为。

注意

安卓将菜单视为始终开启状态,如果菜单项显示在操作栏中。

为视图启用上下文操作模式

上下文菜单为特定视图提供附加选项,这与在桌面上右键点击的概念相同。安卓目前支持两种不同的方法:浮动的上下文菜单和上下文模式。上下文操作模式是在安卓 3.0 中引入的。较旧的浮动上下文菜单可能导致混淆,因为没有当前选定项的指示,并且不支持对多个项目进行操作,例如一次选择多个电子邮件进行删除。

创建浮动上下文菜单

如果你需要使用旧式的上下文菜单,例如,支持低于安卓 3.0 的设备,它与选项菜单 API 非常相似,只是方法名称不同。要创建菜单,请使用 onCreateContextMenu() 而不是 onCreateOptionsMenu()。要处理菜单项的选择,请使用 onContextItemSelected() 而不是 onOptionsItemSelected()。最后,调用 registerForContextMenu() 以便系统知道你希望为视图处理上下文菜单事件。

由于上下文模式被认为是显示上下文选项的首选方式,本教程将重点介绍新的 API。上下文模式提供了浮动上下文菜单的所有功能,但通过在批量模式下允许选择多个项目,还增加了额外的功能。

本教程将演示如何在单个视图中设置上下文模式。一旦激活,通过长按,一个上下文操作栏CAB)将替代操作栏,直到上下文模式结束。

注意

上下文操作栏与操作栏不同,你的活动不需要包含操作栏。

准备就绪

使用 Android Studio 创建一个新项目,命名为 ContextualMode。选择默认的手机 & 平板选项,并在提示添加活动时选择空活动。创建一个菜单目录(res/menu),就像在第一个教程中创建选项菜单时所做的那样,用于存储上下文菜单的 XML。

如何操作...

我们将创建一个 ImageView 作为初始化上下文模式的宿主视图。由于上下文模式通常是通过长按触发的,我们将在 onCreate() 中为 ImageView 设置一个长按监听器。当被调用时,我们将启动上下文模式,并传递一个 ActionMode 回调来处理上下文模式事件。以下是步骤:

  1. 我们将从添加两个新的字符串资源开始。打开 strings.xml 文件,并添加以下内容:

    <string name="menu_cast">Cast</string>
    <string name="menu_print">Print</string>
    
  2. 字符串创建后,我们现在可以通过在 res/menu 下创建一个名为 context_menu.xml 的新文件来创建菜单,使用以下 XML:

    <?xml version="1.0" encoding="utf-8"?>
    <menu
    
        >
    <item android:id="@+id/menu_cast"
        android:title="@string/menu_cast" />
    <item android:id="@+id/menu_print"
        android:title="@string/menu_print" /> </menu>
    
  3. 现在,向 activity_main.xml 添加一个 ImageView,作为启动上下文模式的源头。以下是 ImageView 的 XML 代码:

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:src="img/ic_launcher"/>
    
  4. 界面设置好后,我们可以添加上下文模式的代码。首先,我们需要一个全局变量来存储调用 startActionMode() 时返回的 ActionMode 实例。在 MainActivity.java 的类构造函数下方添加以下代码行:

    ActionMode mActionMode;
    
  5. 接下来,创建一个 ActionMode 回调以传递给 startActionMode()。在 MainActivity 类中添加以下代码,位于上一步的代码下方:

    private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            mode.getMenuInflater().inflate(R.menu.context_menu, menu);
            return true;
        }
        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }
        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
                case R.id. menu_cast:
                    Toast.makeText(MainActivity.this, "Cast", Toast.LENGTH_SHORT).show();
                    mode.finish();
                    return true;
                case R.id. menu_print:
                    Toast.makeText(MainActivity.this, "Print", Toast.LENGTH_SHORT).show();
                    mode.finish();
                    return true;
                default:
                    return false;
            }
        }
        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mActionMode = null;
        }
    };
    
  6. 创建了 ActionMode 回调后,我们只需调用 startActionMode() 来开始上下文模式。在 onCreate() 方法中添加以下代码,以设置长按监听器:

    ImageView imageView = (ImageView)findViewById(R.id.imageView);
    imageView.setOnLongClickListener(new View.OnLongClickListener() {
        public boolean onLongClick(View view) {
            if (mActionMode != null) return false;
            mActionMode = startActionMode(mActionModeCallback);
            return true;
        }
    });
    
  7. 在设备或模拟器上运行程序,以查看 CAB 的实际效果。

工作原理...

正如你在第二步所看到的,我们使用了相同的菜单 XML 来定义上下文菜单,以及其他的菜单。

需要理解的主要代码是 ActionMode 回调。这里是我们处理上下文模式事件的地方:初始化菜单、处理菜单项选择以及清理。我们在长按事件中通过调用 startActionMode() 并传入在第五步创建的 ActionMode 回调来开始上下文模式。

当触发动作模式时,系统会调用 onCreateActionMode() 回调,该回调会填充菜单并在上下文操作栏中显示。用户可以通过按返回箭头或返回键来关闭上下文操作栏。当用户进行菜单选择时,CAB 也会关闭。我们显示一个 Toast 以对此食谱提供视觉反馈,但这里是你实现功能的地方。

还有更多...

在此示例中,我们从 startActionMode() 调用中存储返回的 ActionMode。我们使用它来防止在动作模式已经激活时创建新实例。我们还可以使用此实例对上下文操作栏本身进行更改,例如,使用以下方式更改标题:

mActionMode.setTitle("New Title");

当处理多个项目选择时,这特别有用,我们将在下一个食谱中看到。

另请参阅

  • 请参阅下一个食谱,在 ListView 中使用上下文批量模式,以处理多个项目选择

在 ListView 中使用上下文批量模式

如前一个食谱所述,上下文模式支持两种使用形式:单一视图模式(如所示)和多重选择(或批量)模式。批量模式是上下文模式优于旧的上下文菜单的地方,因为旧菜单不支持多选。

如果你曾经使用过像 Gmail 这样的电子邮件应用或文件浏览器,你可能在选择多个项目时见过上下文模式。以下是 Solid Explorer 的截图,它展示了材料和上下文模式的优秀实现:

在 ListView 中使用上下文批量模式

在此食谱中,我们将创建一个填充有多个国家名称的ListView,以演示多项选择或批量模式。此示例将使用正常的长按事件以及项目点击事件来启动上下文模式。

准备工作

在 Android Studio 中创建一个新项目,将其命名为ContextualBatchMode。选择默认的手机和平板选项,并在提示添加活动时选择空活动。为上下文菜单创建一个菜单目录(res/menu)。

如何操作...

与之前的食谱类似,我们首先在 XML 中创建一个菜单,以便在开始上下文模式时展开。我们需要定义MultiChoiceModeListener来处理ListView的批量模式。然后设置ListView以允许多项选择,并传入MultiChoiceModeListener。以下是步骤:

  1. 打开strings.xml文件,并按照以下方式添加两个新的字符串资源用于菜单项:

    <string name="menu_move">Move</string>
    <string name="menu_delete">Delete</string>
    
  2. res/menu文件夹中创建一个名为contextual_menu.xml的新文件,内容如下:

    <?xml version="1.0" encoding="utf-8"?>
    <menu
    
        >
        <item android:id="@+id/menu_move"
            android:title="@string/menu_move" />
        <item android:id="@+id/menu_delete
            android:title="@string/menu_delete" />
    </menu>
    
  3. 由于我们需要一个ListView,我们将改变MainActivity使其从ListActivity继承,如下所示:

    public class MainActivity extends ListActivity
    
  4. 创建一个MultiChoiceModeListener来处理上下文操作栏事件。在MainActivity.java的类构造函数下方添加以下代码:

    AbsListView.MultiChoiceModeListener mMultiChoiceModeListener = new AbsListView.MultiChoiceModeListener() {
        @Override
        public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
        }
    
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            // Inflate the menu for the CAB
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.contextual_menu, menu);
            return true;
        }
    
        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }
    
        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            // Handle menu selections
            switch (item.getItemId()) {
                case R.id.menu_move
                    Toast.makeText(MainActivity.this, "Move", Toast.LENGTH_SHORT).show();
                    mode.finish();
                    return true;
                case R.id.menu_delete
                    Toast.makeText(MainActivity.this, "Delete", Toast.LENGTH_SHORT).show();
                    mode.finish();
                    return true;
                default:
                    return false;
            }
        }
    
        @Override
        public void onDestroyActionMode(ActionMode mode) {
        }
    };
    
  5. 接下来,我们将改变onCreate()以设置ListView并使用国家名称的字符串数组填充ListAdapter,如下所示:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"};
        ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_checked, countries);
        setListAdapter(countryAdapter);
        getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
        getListView().setMultiChoiceModeListener(mMultiChoiceModeListener);
    
        getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                ((ListView)parent).setItemChecked(position, true);
            }
        });
    }
    
  6. 在设备或模拟器上运行程序,以查看 CAB 的实际操作。

它是如何工作的...

使操作模式在批量模式下工作的三个关键元素是:

  1. 创建一个上下文菜单以展开

  2. 定义MultiChoiceModeListener以传递给setMultiChoiceModeListener()

  3. ListViewChoiceMode设置为CHOICE_MODE_MULTIPLE_MODAL

MultiChoiceModeListener与单视图上下文模式中使用的ActionMode回调相同,实际上实现了ActionMode.Callback。与ActionMode.Callback一样,当MultiChoiceModeListener调用onCreateActionMode()时,菜单会被展开。

默认情况下,通过在ListView中的项目上长按来启动上下文模式。我们将更进一步,当使用onItemClick()事件选中项目时启动上下文模式。如果我们不这样做,启动上下文模式的唯一方式就是通过长按,这可能会让许多用户不知道有额外的功能。

还有更多...

正如本章引言中提到的,你的活动不需要包含操作栏就可以使用上下文操作栏。如果你确实有一个操作栏并且它可见,它将被 CAB 覆盖。如果你没有操作栏作为此食谱的默认设置,布局将被重绘以包含 CAB(当 CAB 消失时再次重绘)。如果你希望操作栏可见,可以更改活动的主题或更改基类,并手动设置ListView

另请参阅

  • 有关ListView的更多信息,请参考第二章,布局

创建弹出菜单

弹出菜单附加到一个类似于下拉菜单的视图上。弹出菜单的想法是提供额外的选项来完成一个动作。一个常见的例子可能是电子邮件应用中的回复按钮。按下时,会显示几个回复选项,如:回复回复所有人转发

下面是食谱中的弹出菜单示例:

创建弹出菜单

如果有空间,Android 将在锚视图下方显示菜单选项;否则,它将在视图上方显示。

提示

弹出菜单不是用来影响视图本身的。这是上下文菜单的目的。相反,请参考为视图启用上下文操作模式食谱中描述的浮动菜单/上下文模式。

在这个食谱中,我们将使用一个ImageButton作为锚视图,创建前面显示的弹出菜单。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为PopupMenu。使用默认的手机 & 平板选项,在选择添加活动时选择空活动。像以前一样,创建一个菜单目录(res/menu)来存储菜单 XML。

如何操作...

我们首先创建在按钮按下时充气的 XML 菜单。在充气弹出菜单之后,我们通过传递回调来处理菜单项选择的回调来调用setOnMenuItemClickListener()。以下是步骤:

  1. strings.xml添加以下字符串:

    <string name="menu_reply">Reply</string>
    <string name="menu_reply_all">Reply All</string>
    <string name="menu_forward">Forward</string>
    
  2. res/menu目录下创建一个名为menu_popup.xml的新文件,使用以下 XML:

    <?xml version="1.0" encoding="utf-8"?>
    <menu
    
        >
        <item android:id="@+id/menu_reply
            android:title="@string/menu_reply" />
        <item android:id="@+id/menu_reply_all
            android:title="@string/menu_reply_all" />
        <item android:id="@+id/menu_forward
            android:title="@string/menu_forward" />
    </menu>
    
  3. activity_main.xml中创建一个ImageButton,为弹出菜单提供锚视图。按照以下 XML 代码所示创建它:

    <ImageButton
        android:id="@+id/imageButtonReply"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:src="img/ic_menu_revert"
        android:onClick="showPopupMenu"/>
    
  4. 打开MainActivity.java,并在类构造函数下面添加以下OnMenuItemClickListener

    private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener = new PopupMenu.OnMenuItemClickListener() {
        @Override
        public boolean onMenuItemClick(MenuItem item) {
            // Handle menu selections
            switch (item.getItemId()) {
                case R.id.menu_reply
                    Toast.makeText(MainActivity.this, "Reply", Toast.LENGTH_SHORT).show();
                    return true;
                case R.id.menu_reply_all
                    Toast.makeText(MainActivity.this,"Reply All",Toast.LENGTH_SHORT).show();
                    return true;
                case R.id.menu_forward
                    Toast.makeText(MainActivity.this, "Forward", Toast.LENGTH_SHORT).show();
                    return true;
                default:
                    return false;
            }
        }
    };
    
  5. 最后的代码是处理按钮onClick()事件,如下所示:

    public void showPopupMenu(View view) {
        PopupMenu popupMenu = new PopupMenu(MainActivity.this,view);
        popupMenu.inflate(R.menu.menu_popup);
        popupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener);
        popupMenu.show();
    }
    
  6. 在设备或模拟器上运行程序,以查看弹出菜单。

它是如何工作的...

如果你读过前面的菜单食谱,这可能看起来非常熟悉。基本上,我们只是在ImageButton被按下时充气弹出菜单。我们设置一个菜单项监听器来响应菜单选择。

关键是要了解 Android 中可用的每个菜单选项,这样你就可以为特定场景使用正确的菜单类型。这将帮助你的应用程序提供一致的用户体验,并降低学习曲线。

第五章:探索片段、应用小部件和系统界面

在本章中,我们将涵盖以下主题:

  • 创建和使用片段

  • 运行时添加和移除片段

  • 在片段间传递数据

  • 在主屏幕上创建快捷方式

  • 在主屏幕上创建小部件

  • 向操作栏添加搜索

  • 让你的应用全屏显示

引言

通过第二章对布局的深入了解,布局,我们将进一步探讨使用片段的用户界面开发。片段是将你的用户界面分割成更小部分的一种方式,这些部分可以轻松复用。将片段视为迷你活动,它们有自己的类、布局和生命周期。你不需要在一个活动布局中设计整个屏幕,可能在多个布局中重复功能,你可以将屏幕分解成更小、逻辑上的部分,并将它们转换为片段。然后,你的活动布局可以根据需要引用一个或多个片段。前三个食谱将深入探讨片段。

了解片段后,我们准备扩展关于小部件的讨论。在第三章中,视图、小部件和样式,我们讨论了如何向你的应用添加小部件。现在,我们将看看如何创建一个应用小部件,以便用户可以将他们的应用放在主屏幕上。

本章最后的食谱将探讨系统界面选项。我们有一个食谱,介绍如何使用 Android SearchManager API 在操作栏中添加 Search 选项。最后一个食谱展示了全屏模式以及几种改变系统界面的额外变体。

创建和使用片段

安卓并非一直支持片段。早期的安卓版本是为手机设计的,当时屏幕相对较小。直到安卓开始被用在平板上,才需要将屏幕分割成更小的部分。安卓 3.0 引入了 Fragments 类和片段管理器。

随着新类的出现,也引入了片段生命周期。片段生命周期与第一章中介绍的活动生命周期相似,活动,因为大多数事件与活动生命周期平行。

下面是主要回调函数的简要概述:

  • onAttach(): 当片段与活动关联时调用。

  • onCreate(): 当片段首次被创建时调用。

  • onCreateView(): 当片段即将第一次显示时调用。

  • onActivityCreated(): 当关联的活动被创建时调用。

  • onStart(): 当片段将要对用户可见时调用。

  • onResume(): 在片段显示之前调用。

  • onPause(): 当片段首次被暂停时调用。用户可能会返回到片段,但这里是你应该保存任何用户数据的地方。

  • onStop(): 当片段对用户不再可见时调用。

  • onDestroyView():它被调用以允许最后的清理。

  • onDetach():当片段不再与活动关联时调用。

在我们的第一个练习中,我们将创建一个从标准Fragment类派生的新片段。但我们还可以从其他几个Fragment类派生,包括:

  • DialogFragment:用于创建一个浮动的对话框

  • ListFragment:它在片段中创建一个ListView,类似于ListActivity

  • PreferenceFragment:它创建一个偏好设置对象列表,通常用于设置页面

在本教程中,我们将通过创建一个基于Fragment类的简单片段,并将其包含在活动布局中来逐步操作。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为CreateFragment。使用默认的Phone & Tablet选项,在选择活动类型时选择Empty Activity选项。

如何操作...

在本教程中,我们将创建一个带有伴随布局文件的新Fragment类。然后,我们将片段添加到活动布局中,以便在活动启动时能够看到它。以下是创建和显示新片段的步骤:

  1. 使用以下 XML 创建一个名为fragment_one.xml的新布局:

    <RelativeLayout 
        android:layout_height="match_parent"
        android:layout_width="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Fragment One"
            android:id="@+id/textView"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
    
  2. 创建一个名为FragmentOne的新 Java 文件,并使用以下代码:

    public class FragmentOne extends Fragment {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_one, container, false);
        }
    }
    
  3. 打开main_activity.xml文件,用以下<fragment>元素替换现有的<TextView>元素:

      <fragment
        android:name="com.packtpub.androidcookbook.createfragment.FragmentOne"
        android:id="@+id/fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        tools:layout="@layout/fragment_one" />
    
  4. 在设备或模拟器上运行程序。

它是如何工作的...

我们首先像创建活动一样创建一个新类。在本教程中,我们只创建了一个onCreateView()方法的覆盖,以加载我们的片段布局。但是,与活动事件一样,我们可以根据需要覆盖其他事件。创建新的片段后,我们将其添加到活动布局中。由于Activity类是在Fragments存在之前创建的,所以它们不支持Fragments。如果我们使用纯框架类,我们会希望使用FragmentActivity。如果你使用了 Android Studio 的新项目向导,那么默认情况下MainActivity扩展了AppCompatActivity,这已经包括了片段的支持。

还有更多...

在此教程中,我们仅创建一个简单的片段来教授片段的基础知识。但现在是指出片段强大功能的好时机。如果我们正在创建多个片段(通常是这样,因为使用片段的目的就在于此),在步骤 4 中创建活动布局时,我们可以使用 Android 资源文件夹创建不同的布局配置。竖屏布局可能只有一个片段,而横屏可能有多个片段。

在运行时添加和移除片段

在布局中定义一个 Fragment,就像我们在上一个配方中所做的那样,这称为静态 Fragment,在运行时无法更改。我们将创建一个容器来保存 Fragment,而不是使用<fragment>元素,然后在 Activity 的onCreate()方法中动态创建 Fragment。

FragmentManager提供了在运行时使用FragmentTransaction添加、移除和更改 Fragments 的 API。一个 Fragment 事务包括:

  • 开始一个事务

  • 执行一个或多个动作

  • 提交事务

这个配方将展示通过在运行时添加和移除 Fragments 来演示FragmentManager

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为RuntimeFragments。使用默认的Phone & Tablet选项,在选择Activity Type时选择Empty Activity

如何操作...

为了演示添加和移除 Fragments,我们首先需要创建 Fragments,这可以通过扩展Fragment类来完成。创建新的 Fragments 后,我们需要更改主活动的布局以包含Fragment容器。从那里,我们只需添加处理 Fragment 事务的代码。以下是步骤:

  1. 创建一个名为fragment_one.xml的新布局文件,并包含以下 XML:

    <RelativeLayout 
        android:layout_height="match_parent"
        android:layout_width="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Fragment One"
            android:id="@+id/textView"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
    
  2. 第二个名为fragment_two.xml的布局文件几乎相同,唯一的区别是文本:

    android:text="Fragment Two"
    
  3. 创建一个名为FragmentOne的新 Java 文件,并包含以下代码:

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

    从以下库导入:

    android.support.v4.app.Fragment
    
  4. 创建第二个名为FragmentTwo的 Java 文件,并包含以下代码:

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

    从以下库导入:

    android.support.v4.app.Fragment
    
  5. 现在我们需要在主活动布局中添加一个容器和一个按钮。如下更改main_activity.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 
    
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <FrameLayout
            android:id="@+id/frameLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@+id/buttonSwitch"
            android:layout_alignParentTop="true">
        </FrameLayout>
        <Button
            android:id="@+id/buttonSwitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Switch"
            android:layout_alignParentBottom="true"
            android:layout_centerInParent="true"
            android:onClick="switchFragment"/>
    </RelativeLayout>
    
  6. 创建了 Fragments 并且将容器添加到布局中后,我们现在准备编写操作 Fragments 的代码。打开MainActivity.java并在类构造函数下面添加以下代码:

    FragmentOne mFragmentOne;
    FragmentTwo mFragmentTwo;
    int showingFragment=0;
    
  7. 在现有的onCreate()方法中,在setContentView()下面添加以下代码:

    mFragmentOne = new FragmentOne();
    mFragmentTwo = new FragmentTwo();
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.add(R.id.frameLayout, mFragmentOne);
    fragmentTransaction.commit();
    showingFragment=1;
    

    从以下库导入:

    android.support.v4.app.FragmentManager
    android.support.v4.app.FragmentTransaction
    
  8. 我们需要添加的最后一段代码处理按钮触发的 Fragment 切换:

    public void switchFragment(View view) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        if (showingFragment==1) {
            fragmentTransaction.replace(R.id.frameLayout, mFragmentTwo);
            showingFragment = 2;
        } else {
            fragmentTransaction.replace(R.id.frameLayout, mFragmentOne);
            showingFragment=1;
        }
        fragmentTransaction.commit();
    }
    
  9. 在设备或模拟器上运行程序。

工作原理...

这个配方的多数步骤涉及设置 Fragments。一旦声明了 Fragments,我们将在onCreate()方法中创建它们。尽管代码可以压缩成单行,但它以长形式展示,这样更容易阅读和理解。

首先,我们获取FragmentManager以便开始一个FragmentTransaction。一旦有了FragmentTransaction,我们通过beginTransaction()开始事务。在事务中可以发生多个动作,但这里我们只需要add()我们的初始 Fragment。我们调用commit()方法来最终确定事务。

现在您理解了 Fragment 事务,这是onCreate()的简洁版本:

getFragmentManager().beginTransaction().add(R.id.framLayout, mFragmentOne).commit();

switchFragment基本上执行相同类型的 Fragment 事务。我们不是调用add()方法,而是使用replace()方法替换现有 Fragment。我们通过showingFragment变量跟踪当前 Fragment,这样我们就知道接下来要显示哪个 Fragment。我们不仅限于在两个 Fragment 之间切换。如果我们需要额外的 Fragment,只需创建它们即可。

还有更多...

在第一章《活动》中的活动间切换一节,我们讨论了返回栈。大多数用户会期望按返回键可以向后穿过“屏幕”,他们不知道或不在乎这些屏幕是活动还是 Fragment。幸运的是,Android 通过在调用commit()之前添加对addToBackStack()的调用,非常容易地将 Fragment 添加到返回栈中。

提示

如果在没有将 Fragment 添加到返回栈的情况下移除或替换它,它将被立即销毁。如果添加到返回栈中,它会被停止,如果用户返回到该 Fragment,它将被重新启动,而不是重新创建。

在 Fragment 之间传递数据

通常,需要在 Fragment 之间传递信息。电子邮件应用程序就是一个典型的例子。通常在一个 Fragment 中显示电子邮件列表,在另一个 Fragment 中显示电子邮件详情(这通常被称为 Master/Detail 模式)。由于我们只需要为每个 Fragment 编写一次代码,然后就可以将它们包含在不同的布局中,所以 Fragment 使得创建这种模式变得更容易。我们可以在纵向布局中轻松地拥有一个单独的 Fragment,并在选择电子邮件时,用详情 Fragment 替换主 Fragment。我们还可以创建一个双面板布局,其中列表和详情 Fragment 并排显示。无论哪种方式,当用户点击列表中的电子邮件时,电子邮件都会在详情面板中打开。这就是我们需要在两个 Fragment 之间进行通信的时候。

由于 Fragment 的一个主要目标是完全自包含,因此不建议直接在 Fragment 之间进行通信,这有充分的理由。如果 Fragment 必须依赖其他 Fragment,当布局更改且只有一个 Fragment 可用时,你的代码很可能会出问题。幸运的是,在这种情况下也不需要直接通信。所有 Fragment 的通信都应该通过宿主活动进行。宿主活动负责管理 Fragment,并且可以正确地传递消息。

现在的问题变成了:Fragment 是如何与活动通信的?答案是:通过一个接口。你可能已经熟悉了接口,因为视图就是通过接口将事件回传给活动的。按钮点击就是一个常见的例子。

在此食谱中,我们将创建两个片段,以展示通过宿主活动从一个片段向另一个片段传递数据。我们将在上一个食谱的基础上,包括两种不同的活动布局——一种用于竖屏,一种用于横屏。在竖屏模式下,活动将根据需要交换片段。以下是应用程序首次在竖屏模式下运行时的截图:

在片段间传递数据

这是当你点击国家名称时显示详情片段的屏幕:

在片段间传递数据

在横屏模式下,两个片段将并排显示,如下横屏截图所示:

在片段间传递数据

由于主/详模式通常涉及主列表,我们将利用 ListFragment(在创建和使用片段介绍中提到)。当列表中的项目被选中时,项目文本(在我们的示例中是国家名称)将通过宿主 Activity 发送到详情片段。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 Fragmentcommunication。使用默认的 Phone & Tablet 选项,在选择 Activity Type 时选择 Empty Activity

如何操作...

为了完全展示工作的片段,我们需要创建两个片段。第一个片段将继承自 ListFragment,因此不需要布局。我们还将进一步创建活动和横屏模式下的两种布局。对于竖屏模式,我们将交换片段,对于横屏模式,我们将并排显示两个片段。

注意

在输入这段代码时,Android Studio 会提供两个不同的库导入选项。由于新项目向导会自动引用 AppCompat 库,我们需要使用支持库 API 而非框架 API。尽管它们非常相似,以下代码使用了支持片段 API。

下面是从第一个片段开始的步骤:

  1. 创建一个名为 MasterFragment 的新 Java 类,并将其更改为继承 ListFragment,如下所示:

    public class MasterFragment extends ListFragment
    

    从以下库导入:

    android.support.v4.app.ListFragment
    
  2. MasterFragment 类内部创建以下 interface

    public interface OnMasterSelectedListener {
        public void onItemSelected(String countryName);
    }
    
  3. 使用以下代码设置接口回调监听器:

    private OnMasterSelectedListener mOnMasterSelectedListener=null;
    
    public void setOnMasterSelectedListener(OnMasterSelectedListener listener) {
        mOnMasterSelectedListener=listener;
    }
    
  4. MasterFragment 的最后一步是创建一个 ListAdapter 来填充 ListView,我们在 onViewCreated() 方法中进行。我们将使用 setOnItemClickListener() 在选择国家名称时调用我们的 OnMasterSelectedListener 接口,代码如下:

    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"};
        ListAdapter countryAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, countries);
        setListAdapter(countryAdapter);
        getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (mOnMasterSelectedListener != null) {
                    mOnMasterSelectedListener.onItemSelected(((TextView) view).getText().toString());
                }
            }
        });
    }
    
  5. 接下来我们需要创建 DetailFragment,从布局开始。创建一个名为 fragment_detail.xml 的新布局文件,包含以下 XML:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
    
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/textViewCountryName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
    
  6. 创建一个名为 DetailFragment 的新 Java 类,继承自 Fragment,如下所示:

    public class DetailFragment extends Fragment
    

    从以下库导入:

    android.support.v4.app.Fragment
    
  7. 将以下常量添加到类中:

    public static String KEY_COUNTRY_NAME="KEY_COUNTRY_NAME";
    
  8. 按如下方式覆盖 onCreateView()

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_detail, container, false);
    }
    
  9. 按如下方式编写 onViewCreated()

    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    
        Bundle bundle = getArguments();
        if (bundle != null && bundle.containsKey(KEY_COUNTRY_NAME)) {
            showSelectedCountry(bundle.getString(KEY_COUNTRY_NAME));
        }
    }
    
  10. 对于这个 Fragment 的最后一个步骤是在接收到选定的国家名称时更新 TextView。将以下方法添加到类中:

    public void showSelectedCountry(String countryName) {
        ((TextView)getView().findViewById(R.id.textViewCountryName)).setText(countryName);
    }
    
  11. 现有的activity_main.xml布局将处理竖屏模式的布局。移除现有的<TextView>并替换为以下<FrameLayout>

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
  12. res文件夹中为横屏布局创建一个新目录,如:res/layout-land

    提示

    如果你没有看到新的res/layout-land目录,从Android视图更改为Project视图。

  13. res/layout-land中创建一个新的activity_main.xml布局,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 
    
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <FrameLayout
            android:id="@+id/frameLayoutMaster"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"/>
        <FrameLayout
            android:id="@+id/frameLayoutDetail"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"/>
    </LinearLayout>
    
  14. 最后的步骤是设置MainActivity以处理 Fragments。打开MainActivity.java文件,并添加以下类变量以跟踪单/双面板:

    boolean dualPane;
    
  15. 接下来,按如下方式更改onCreate()

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        MasterFragment masterFragment=null;
        FrameLayout frameLayout = (FrameLayout)findViewById(R.id.frameLayout);
        if (frameLayout != null) {
            dualPane=false;
            FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
            masterFragment=(MasterFragment)getSupportFragmentManager().findFragmentByTag("MASTER");
            if (masterFragment == null) {
                masterFragment = new MasterFragment();
                fragmentTransaction.add(R.id.frameLayout, masterFragment, "MASTER");
            }
            DetailFragment detailFragment = (DetailFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutDetail);
            if (detailFragment != null) {
                fragmentTransaction.remove(detailFragment);
            }
            fragmentTransaction.commit();
        } else {
            dualPane=true;
            FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
            masterFragment=(MasterFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutMaster);
            if (masterFragment==null) {
                masterFragment = new MasterFragment();
                fragmentTransaction.add(R.id.frameLayoutMaster, masterFragment);
            }
            DetailFragment detailFragment=(DetailFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutDetail);
            if (detailFragment==null) {
                detailFragment = new DetailFragment();
                fragmentTransaction.add(R.id.frameLayoutDetail, detailFragment);
            }
            fragmentTransaction.commit();
        }
        masterFragment.setOnMasterSelectedListener(new MasterFragment.OnMasterSelectedListener() {
            @Override
            public void onItemSelected(String countryName) {
                sendCountryName(countryName);
            }
        });
    }
    
  16. 需要添加的最后一段代码是处理将国家名称发送给DetailFragmentsendCountryName()方法:

    private void sendCountryName(String countryName) {
        DetailFragment detailFragment;
        if (dualPane) {
            //Two pane layout
            detailFragment = (DetailFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutDetail);
            detailFragment.showSelectedCountry(countryName);
        } else {
            // Single pane layout
            detailFragment = new DetailFragment();
            Bundle bundle = new Bundle();
            bundle.putString(DetailFragment.KEY_COUNTRY_NAME, countryName);
            detailFragment.setArguments(bundle);
            FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
            fragmentTransaction.replace(R.id.frameLayout, detailFragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
        }
    }
    
  17. 在设备或模拟器上运行程序。

工作原理...

我们首先创建MasterFragment。在我们使用的 Master/Detail 模式中,这通常代表一个列表,因此我们通过扩展ListFragment来创建一个列表。ListFragmentListActivity对应的 Fragment 版本。除了从 Fragment 扩展而来,基本上是相同的。

如菜谱介绍中所说,我们不应该尝试直接与其他 Fragments 进行通信。

为了提供一种方法来通知列表项的选择,我们暴露了接口:OnMasterSelectedListener。每次在列表中选择一个项目时,我们都会调用onItemSelected()

在 Fragments 之间传递数据的大部分工作由宿主活动完成,但最终,接收 Fragment 需要一种接收数据的方法。DetailFragment通过以下两种方式支持这一点:

  • 在创建时通过参数包传递国家名称。

  • 一个供活动直接调用的公共方法。

当活动创建 Fragment 时,它还会创建一个bundle来保存我们想要发送的数据。这里我们使用在步骤 7 中定义的KEY_COUNTRY_NAME添加国家名称。我们在onViewCreated()中使用getArguments()获取这个包。如果包中找到了键,则提取并使用showSelectedCountry()方法显示。如果 Fragment 已经可见(在双面板布局中),活动将直接调用此方法。

本菜谱的大部分工作都在活动中完成。我们创建了两个布局:一个用于竖屏,一个用于横屏。Android 将使用在步骤 12中创建的res/layout-land目录选择横屏布局。这两个布局都使用类似于之前练习的<FrameLayout>占位符。我们在onCreate()sendCountryName()中管理两种 Fragments。

onCreate()中,我们通过检查当前布局是否包含frameLayout视图来设置dualPane标志。如果找到了frameLayout(它不会为空),那么我们只有一个面板,因为frameLayout的 ID 只存在于竖屏布局中。如果没有找到 frameLayout,那么我们将有两个<FrameLayout>元素:一个用于MasterFragment,另一个用于DetailFragment

onCreate()中我们最后要做的事情是设置MasterFragment的监听器,通过创建一个匿名回调函数,将国家名称传递给sendCountryName()

sendCountryName()是实际将数据传递给DetailFragment的地方。如果我们处于竖屏(或单面板)模式,我们需要创建一个DetailFragment并替换现有的MasterFragment。在这里,我们创建带有国家名称的包,并调用setArguments()。注意我们在提交事务之前是如何调用addToBackStack()的吗?这使得按下返回键可以将用户带回列表(MasterFragment)。如果我们处于横屏模式,DetailFragment已经可见,因此我们直接调用showSelectedCountry()公共方法。

还有更多...

MasterFragment中,在发送onItemSelected()事件之前,我们通过以下代码检查以确保监听器不为空:

if (mOnMasterSelectedListener != null)

虽然活动负责设置回调以接收事件,但我们不希望如果没有监听器这段代码崩溃。另一种方法是在 Fragment 的onAttach()回调中验证活动是否扩展了我们的接口。

另请参阅

  • 有关 ListViews 的更多信息,请参见第二章中的使用 ListView、GridView 和适配器布局

  • 有关资源目录的更多信息,请参见第三章中的根据 Android 版本选择主题视图、小部件和样式

在主屏幕上创建快捷方式

本食谱解释了如何在用户的主屏幕上为你的应用创建链接或快捷方式。为了避免过于突兀,通常最好让用户在设置中自行选择启动这一选项。

下面是一张截图,展示了我们在主屏幕上的快捷方式:

在主屏幕上创建快捷方式

如你所见,这只是个快捷方式,但在下一个食谱中我们将探讨创建主屏幕(AppWidget)的方法。

准备工作

在 Android Studio 中创建一个新项目,将其命名为:HomescreenShortcut。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动选项。

如何操作...

第一步是添加适当的权限。以下是步骤:

  1. 打开AndroidManifest文件,并添加以下权限:

    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
    
  2. 接下来,打开activity_main.xml,并用以下按钮替换现有的 TextView:

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Create Shortcut"
        android:id="@+id/button"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:onClick="createShortcut"/>
    
  3. ActivityMain.java添加以下方法:

    public void createShortcut(View view) {
        Intent shortcutIntent = new Intent(this, MainActivity.class);
        shortcutIntent.setAction(Intent.ACTION_MAIN);
        Intent intent = new Intent();
        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
        intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.app_name));
        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher));
        intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
        sendBroadcast(intent);
    }
    
  4. 在设备或模拟器上运行程序。注意,每次你按按钮,应用将在主屏幕上创建一个快捷方式。

工作原理...

设置了适当的权限后,这个任务相当直接。当按钮被点击时,代码创建了一个名为shortcutIntent的新意图。这是当在主屏幕上按下图标时将被调用的意图。接下来创建的意图installIntent负责实际创建快捷方式。

还有更多...

如果你还想移除快捷方式,你需要以下权限:

<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />

与其使用 INSTALL_SHORTCUT 动作,不如设置以下动作:

com.android.launcher.action.UNINSTALL_SHORTCUT

创建主屏幕小部件

在我们深入研究创建 App Widget 的代码之前,让我们先了解基础知识。有三个必需和一个可选的组件:

  • AppWidgetProviderInfo文件:稍后描述的 XML 资源

  • AppWidgetProvider类:这是一个 Java 类

  • 视图布局文件:这是一个标准的布局 XML 文件,稍后会列出一些限制

  • App Widget 配置 Activity(可选):在放置小部件时启动此 Activity 以设置配置选项

AppWidgetProvider还必须在AndroidManifest文件中声明。由于AppWidgetProvider是基于广播接收器的辅助类,因此它使用<receiver>元素在清单中声明。以下是一个示例清单条目:

<receiver android:name="AppWidgetProvider" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/appwidget_info" />
</receiver>

元数据指向AppWidgetProviderInfo文件,该文件位于res/xml目录中。以下是一个示例AppWidgetProviderInfo.xml文件:

<appwidget-provider 
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="1800000"
    android:previewImage="@drawable/preview_image"
    android:initialLayout="@layout/appwidget"
    android:configure="com.packtpub.androidcookbook.AppWidgetConfiguration"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

下面是可用属性的简要概述:

  • minWidth:放置在主屏幕上的默认宽度

  • minHeight:放置在主屏幕上的默认高度

  • updatePeriodMillis:它是onUpdate()轮询间隔的一部分(以毫秒为单位)

  • initialLayout:AppWidget 布局

  • previewImage(可选):浏览 App Widgets 时显示的图片

  • configure(可选):用于启动配置设置的 activity

  • resizeMode(可选):标志表示调整大小选项 - horizontal(水平)、vertical(垂直)、none(无)

  • minResizeWidth(可选):调整大小时允许的最小宽度

  • minResizeHeight(可选):调整大小时允许的最小高度

  • widgetCategory(可选):Android 5+仅支持主屏幕小部件

AppWidgetProvider扩展了BroadcastReceiver类,这就是在清单中声明 App Widget 时使用<receiver>的原因。由于它是BroadcastReceiver,该类仍然接收操作系统的广播事件,但辅助类将这些事件过滤为适用于 App Widget 的事件。AppWidgetProvider类公开以下方法:

  • onUpdate():在最初创建时以及指定的时间间隔调用。

  • onAppWidgetOptionsChanged():在最初创建时以及任何大小更改时调用。

  • onDeleted():每当小部件被移除时调用。

  • onEnabled(): 当小部件首次被放置时调用(在添加第二个及以后的小部件时不会调用)。

  • onDisabled(): 当最后一个部件被移除时调用。

  • onReceive(): 在接收到每个事件时调用,包括前面的那些事件。通常不重写,因为默认实现只发送适用的事件。

最后一个必需的组件是布局。Remote Views 只支持可用布局的一个子集。由于 App Widget 是 Remote View,因此只支持以下布局:

  • FrameLayout

  • LinearLayout

  • RelativeLayout

  • GridLayout

以及以下小部件:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextView

  • ViewFlipper

  • ListView

  • GridView

  • StackView

  • AdapterViewFlipper

了解了 App Widget 的基础知识后,现在开始编码。我们的示例将涵盖基础知识,以便你可以根据需要扩展功能。这个示例使用了一个带有时钟的视图,按下时,会打开我们的活动。

这张截图显示了在添加到主屏幕时小部件在部件列表中的样子:

创建主屏幕小部件

注意

小部件列表的外观因启动器而异。

这是一张添加到主屏幕后的小部件截图:

创建主屏幕小部件

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:AppWidget。使用默认的Phone & Tablet选项,在选择Activity Type时选择Empty Activity

如何操作...

我们将从创建小部件布局开始,该布局位于标准布局资源目录中。然后我们将创建 xml 资源目录以存储AppWidgetProviderInfo文件。我们将添加一个新的 Java 类并扩展AppWidgetProvider,它处理小部件的onUpdate()调用。创建接收器后,我们可以将其添加到 Android 清单中。

以下是详细的步骤:

  1. res/layout中创建一个名为widget.xml的新文件,使用以下 XML:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <AnalogClock
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/analogClock"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
    
  2. 在资源目录中创建一个名为xml的新目录。最终结果将是:res/xml

  3. res/xml中创建一个名为appwidget_info.xml的新文件,使用以下 xml:

    <appwidget-provider 
        android:minWidth="40dp"
        android:minHeight="40dp"
        android:updatePeriodMillis="0"
        android:initialLayout="@layout/widget"
        android:resizeMode="none"
        android:widgetCategory="home_screen">
    </appwidget-provider>
    

    提示

    如果你看不到新的 xml 目录,请在项目面板下拉菜单中将视图从Android切换到Project

  4. 创建一个名为HomescreenWidgetProvider的新 Java 类,继承自AppWidgetProvider

  5. HomescreenWidgetProvider类中添加以下onUpdate()方法:

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        for (int count=0; count<appWidgetIds.length; count++) {
            RemoteViews appWidgetLayout = new RemoteViews(context.getPackageName(), R.layout.widget);
            Intent intent = new Intent(context, MainActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
            appWidgetLayout.setOnClickPendingIntent(R.id.analogClock, pendingIntent);
            appWidgetManager.updateAppWidget(appWidgetIds[count], appWidgetLayout);
        }
    }
    
  6. AndroidManifest中使用以下 XML 声明将HomescreenWidgetProvider添加到<application>元素中:

    <receiver android:name=".HomescreenWidgetProvider" >
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
            android:resource="@xml/appwidget_info" />
    </receiver>
    
  7. 在设备或模拟器上运行程序。首次运行应用程序后,小部件就可以添加到主屏幕上了。

工作原理...

我们的第一步是创建小部件的布局文件。这是一个标准的布局资源,受到 App Widget 作为远程视图的限制,如菜谱介绍中所述。尽管我们的示例使用了模拟时钟小部件,但这是根据你的应用程序需求扩展功能的地方。

XML 资源目录用于存储AppWidgetProviderInfo,它定义了默认的小部件设置。配置设置决定了在最初浏览可用小部件时如何显示小部件。本例中我们使用非常基础的设置,但它们可以轻松扩展,以包含如预览图像显示功能正常的小部件和大小选项等附加功能。updatePeriodMillis属性设置了更新频率。由于更新将唤醒设备,这是保持数据最新与电池寿命之间的权衡。(这时可选的设置活动就很有用,可以让用户决定。)

AppWidgetProvider类是我们处理由updatePeriodMillis轮询触发的onUpdate()事件的地方。我们的示例不需要任何更新,因此我们将轮询设置为 0。尽管如此,在最初放置小部件时仍会调用更新。在onUpdate()中,我们设置了当按下时钟时打开我们应用的待定意图。

由于onUpdate()方法可能是 AppWidgets 最复杂的方面,我们将详细解释这一点。首先需要注意的是,onUpdate()在每个轮询间隔内只发生一次,对于由此提供者创建的所有小部件。(创建第一个之后的小部件将遵循第一个小部件的周期。)这就解释了for循环的必要性,我们需要它来遍历所有现有的小部件。在这里,我们创建了一个待定意图,当按下时钟时调用我们的应用。如前所述,AppWidget 是一个远程视图。因此,为了获取布局,我们使用带有完全限定包名和布局 ID 的RemoteViews()。一旦我们有了布局,就可以使用setOnClickPendingIntent()将待定意图附加到时钟视图上。我们调用名为updateAppWidget()AppWidgetManager来启动我们做出的更改。

要使所有这些工作正常,最后一步是在 Android Manifest 中声明小部件。我们使用<intent-filter>标识我们要处理的行为。大多数应用小部件可能都想处理更新事件,就像我们的例子一样。声明中需要注意的另一项是这一行:

<meta-data android:name="android.appwidget.provider"
    android:resource="@xml/appwidget_info" />

这告诉系统在哪里找到我们的配置文件。

还有更多...

添加 App Widget 配置活动可以使你的小部件更加灵活。你不仅可以提供轮询选项,还可以提供不同的布局、点击行为等。用户往往非常重视灵活的 App Widgets。

添加配置 Activity 需要几个额外的步骤。Activity 需要像往常一样在 Manifest 中声明,但需要包含 APPWIDGET_CONFIGURE 动作,如下例所示:

<activity android:name=".AppWidgetConfigureActivity">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

Activity 还需要在 AppWidgetProviderInfo 文件中使用 configure 属性指定,如下例所示:

android:configure="com.packtpub.androidcookbook.appwidget.AppWidgetConfigureActivity"

configure 属性需要完全限定的包名,因为此 Activity 将从您的应用外部调用。

提示

请记住,使用配置 Activity 时不会调用 onUpdate() 方法。如果需要,配置 Activity 负责处理任何初始设置。

另请参阅

将搜索添加到操作栏

除了操作栏,Android 3.0 还引入了 SearchView 小部件,创建菜单时可以作为菜单项包含。这是现在推荐使用以提供一致用户体验的 UI 模式。

以下截图展示了搜索图标在操作栏中的初始外观:

将搜索添加到操作栏

此截图展示了按下搜索选项时如何展开:

将搜索添加到操作栏

如果你想在应用中添加搜索功能,本指南将引导你完成设置用户界面和正确配置搜索管理器 API 的步骤。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为 SearchView。使用默认的 Phone & Tablet 选项,在选择 Activity 类型时选择 Empty Activity

如何操作...

要设置搜索 UI 模式,我们需要创建一个搜索菜单项和一个名为 searchable 的资源。我们将创建第二个 Activity 以接收搜索查询。然后我们将在 AndroidManifest 文件中连接所有内容。首先,打开 res/values 中的 strings.xml 文件,并按照以下步骤操作:

  1. 添加以下字符串资源:

    <string name="search_title">Search</string>
    <string name="search_hint">Enter text to search</string>
    
  2. 创建菜单目录:res/menu

  3. res/menu 中创建一个名为 menu_options.xml 的新菜单资源,使用以下 xml:

    <?xml version="1.0" encoding="utf-8"?>
    <menu
    
        >
        <item android:id="@+id/menu_search"
            android:title="@string/search_title"
            android:icon="@android:drawable/ic_menu_search"
            app:showAsAction="collapseActionView|ifRoom"
            app:actionViewClass="android.support.v7.widget.SearchView" />
    </menu>
    
  4. 重写 onCreateOptionsMenu() 以展开菜单并按以下方式设置搜索管理器:

    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_options, menu);
        SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.menu_search));
        searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
        return true;
    }
    
  5. 创建一个新的 xml 资源目录:res/xml

  6. res/xml 中创建一个名为 searchable.xml 的新文件,使用以下 xml:

    <?xml version="1.0" encoding="utf-8"?>
    <searchable 
        android:label="@string/app_name"
        android:hint="@string/search_hint" />
    
  7. 使用此 xml 创建一个名为 activity_search_result.xml 的新布局:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 
    
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/textViewSearchResult"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true" />
    </RelativeLayout>
    
  8. 创建一个名为 SearchResultActivity 的新 Activity。

  9. 向类中添加以下变量:

    TextView mTextViewSearchResult;
    
  10. onCreate() 方法改为加载我们的布局,设置 TextView 并检查 QUERY 动作:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_search_result);
        mTextViewSearchResult = (TextView)findViewById(R.id.textViewSearchResult);
    
        if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
            handleSearch(getIntent().getStringExtra(SearchManager.QUERY));
    }
    
  11. 添加以下方法来处理搜索:

    private void handleSearch(String searchQuery) {
        mTextViewSearchResult.setText(searchQuery);
    }
    
  12. 界面和代码现在已完成,我们只需要在 AndroidManifest 中正确地连接所有内容。以下是包含两个活动的完整 manifest:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest 
        package="com.packtpub.androidcookbook.searchview" >
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme" >
            <meta-data
                android:name="android.app.default_searchable"
                android:value=".SearchResultActivity" />
            <activity android:name=".MainActivity" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity android:name=".SearchResultActivity" >
                <intent-filter>
                    <action android:name="android.intent.action.SEARCH" />
                </intent-filter>
                <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
            </activity>
        </application>
    </manifest>
    
  13. 在设备或模拟器上运行应用程序。输入搜索查询并点击搜索按钮(或按回车键)。SearchResultActivity将显示,并展示输入的搜索查询。

工作原理...

由于新建项目向导使用了AppCompat库,我们的示例使用了支持库 API。使用支持库可以提供最大的设备兼容性,因为它允许在旧版本的 Android OS 上使用现代功能(如操作栏)。这有时可能会带来额外的挑战,因为官方文档通常关注的是框架 API。尽管支持库通常紧跟框架 API,但它们并不总是可以互换的。搜索 UI 模式就是这样的情况,因此值得对之前概述的步骤给予更多关注。

我们从为searchable创建字符串资源开始,如第 6 步中声明的那样。

在第 3 步中,我们创建菜单资源,就像我们之前多次做的那样。一个不同点是,我们为showAsActionactionViewClass属性使用app命名空间。早期版本的 Android OS 在其 Android 命名空间中不包括这些属性。这可以作为将新功能引入旧版本 Android OS 的一种方式。

在第 4 步中,我们设置了SearchManager,同样使用了支持库 API。

第 6 步是定义searchable的地方,这是一个由SearchManager使用的 xml 资源。唯一必需的属性是label,但建议使用hint,以便用户了解他们应该在字段中输入什么。

提示

android:label必须与应用程序名称或活动名称相匹配,并且必须使用字符串资源(因为它不适用于硬编码的字符串)。

第 7-11 步是针对SearchResultActivity的。调用第二个活动不是SearchManager的要求,但通常这样做是为了为应用程序中启动的所有搜索提供一个单一的活动。

如果您在此刻运行应用程序,您将看到搜索图标,但没有任何功能可以使用。第 12 步是我们将所有内容在AndroidManifest文件中整合在一起的地方。首先要注意的是以下内容:

<meta-data
android:name="android.app.default_searchable"
android:value=".SearchResultActivity" />

请注意,这是在应用程序元素中,而不是在任一<activity>元素中。

我们在SearchResultActivity <meta-data>元素中指定可搜索的资源:

<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />

我们还需要为SearchResultActivity设置意图过滤器,就像这里做的那样:

<intent-filter>
    <action android:name="android.intent.action.SEARCH" />
</intent-filter>

当用户发起搜索时,SearchManager会广播SEARCH意图。此声明将意图指向SearchResultActivity活动。一旦触发搜索,查询文本就会通过SEARCH意图发送到SearchResultActivity。我们在onCreate()中检查SEARCH意图,并使用以下代码提取查询字符串:

if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
    handleSearch(getIntent().getStringExtra(SearchManager.QUERY));
}

现在你已经完全实现了搜索 UI 模式。UI 模式完成后,如何处理搜索取决于你的应用需求。根据你的应用,你可能要搜索本地数据库,或者可能是网络服务。

另请参阅

如果要在互联网上进行搜索,请查看第十二章中的互联网查询电信、网络和 Web

显示应用全屏

Android 4.4(API 19)引入了一个名为沉浸模式的 UI 特性。与之前的全屏标志不同,应用在沉浸模式下接收所有触摸事件。这种模式非常适合某些活动,如阅读书籍和新闻、全屏绘图、游戏或观看视频。实现全屏有几种不同的方法,每种方法都有最佳使用场景:

  • 阅读书籍/文章等:带有便捷访问系统 UI 的沉浸模式

  • 游戏/绘图应用:沉浸模式用于全屏使用但系统 UI 最小化

  • 观看视频:全屏和正常系统 UI

这两种模式的主要区别在于系统 UI 的响应方式。在前两种场景中,应用期望用户交互,因此隐藏系统 UI 以便用户更容易操作(例如,在玩游戏时不会误按返回按钮)。而在使用带有正常系统 UI 的全屏观看视频时,你不会期望用户操作屏幕,所以当用户操作时,系统 UI 应该正常响应。在所有模式下,用户可以通过在隐藏的系统栏内向内滑动来调出系统 UI。

由于观看视频不需要新的沉浸模式,可以使用两个标志SYSTEM_UI_FLAG_FULLSCREENSYSTEM_UI_FLAG_HIDE_NAVIGATION实现全屏模式,这两个标志自 Android 4.0(API 14)起可用。

我们的教程将演示如何设置沉浸模式。我们还将添加通过在屏幕上轻敲来切换系统 UI 的功能。

准备就绪

在 Android Studio 中创建一个新项目,名为ImmersiveMode。使用默认的手机 & 平板选项,在选择活动类型时选择空活动。在选择最低 API 级别时,选择API 19或更高。

如何操作...

我们将创建两个处理系统 UI 可见性的函数,然后我们将创建一个手势监听器来检测屏幕轻敲。这个食谱的所有步骤都是向MainActivity.java添加代码,所以打开文件,让我们开始吧:

  1. 添加以下方法以隐藏系统 UI:

    private void hideSystemUi() {
        getWindow().getDecorView().setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
    }
    
  2. 添加以下方法以显示系统 UI:

    private void showSystemUI() {
        getWindow().getDecorView().setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
    }
    
  3. 添加以下类变量:

    private GestureDetectorCompat mGestureDetector;
    
  4. 在类级别的先前类变量下方,添加以下GestureListener类:

    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDown(MotionEvent event) {
            return true;
        }
    
        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
            return true;
        }
    
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            if (getSupportActionBar()!= null && getSupportActionBar().isShowing()) {
                hideSystemUi();
            } else {
                showSystemUI();
            }
            return true;
        }
    }
    
  5. 使用以下内容重写onTouchEvent()回调:

    public boolean onTouchEvent(MotionEvent event){
        mGestureDetector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }
    
  6. onCreate()方法中添加以下代码,以设置GestureListener和隐藏系统 UI:

    mGestureDetector = new GestureDetectorCompat(this, new GestureListener());
    hideSystemUi();
    
  7. 在设备或模拟器上运行应用。向内滑动隐藏的系统栏将显示系统界面。轻触屏幕将切换系统界面。

工作原理...

我们通过在应用窗口上使用setSystemUiVisibility()来创建showSystemUI()hideSystemUI()方法。我们设置的标志(以及不设置的标志)控制着哪些是可见的,哪些是隐藏的。当我们不带有SYSTEM_UI_FLAG_IMMERSIVE标志设置可见性时,实际上,我们禁用了沉浸模式。

如果我们只想隐藏系统界面,我们可以在onCreate()中添加hideSystemUI()就完成了。问题是它不会保持隐藏。一旦用户退出沉浸模式,它将保持常规显示模式。这就是为什么我们创建了GestureListener。(我们将在第八章,使用触摸屏和传感器中再次讨论手势。)由于我们只想响应onSingleTapUp()手势,所以我们没有实现全套手势。当检测到onSingleTapUp时,我们会切换系统界面。

还有更多...

让我们看看一些其他重要的任务:

粘性沉浸

如果我们希望系统界面能自动保持隐藏,还有另一种选择。我们可以不使用SYSTEM_UI_FLAG_IMMERSIVE来隐藏界面,而是使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY

淡化系统界面

如果你只需要减少导航栏的可见性,还可以使用SYSTEM_UI_FLAG_LOW_PROFILE来淡化界面。

使用这个标志与沉浸模式标志相同的setSystemUiVisibility()调用:

getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);

使用setSystemUiVisibility()并传入 0 来清除所有标志:

getWindow().getDecorView().setSystemUiVisibility(0);

将操作栏设置为覆盖层

如果你只需要隐藏或显示操作栏,请使用以下方法:

getActionBar().hide();
getActionBar().show();

这种方法的一个问题是,每次调用这两个方法时,系统都会调整布局的大小。相反,你可能需要考虑使用主题选项使系统界面表现为覆盖层。要启用覆盖模式,请在主题中添加以下内容:

<item name="android:windowActionBarOverlay">true</item>

透明的系统栏

这两个主题启用了半透明设置:

Theme.Holo.NoActionBar.TranslucentDecor
Theme.Holo.Light.NoActionBar.TranslucentDecor

如果你正在创建自己的主题,请使用以下主题设置:

<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowTranslucentStatus">true</item>

另请参阅

在第八章,使用触摸屏和传感器中的识别手势部分。