带片段的 ViewPager

ViewPager 是一个布局管理器,可用于实现手势导航。 通过 Gestural 导航,用户可以向左和向右轻扫浏览数据页。 本指南介绍如何使用 Fragments 作为数据页,通过 ViewPager 实现可轻扫 UI。

概述

ViewPager 通常与片段结合使用,以便更轻松地管理 ViewPager 中每个页面的生命周期。 在本演练中,ViewPager 用于创建名为 FlashCardPager 的应用,该应用在抽认卡上呈现一系列数学问题。 每个抽认卡都作为片段实现。 用户通过抽认卡向左和向右轻扫,然后点击数学问题以显示答案。 此应用为每个抽认卡创建一个 Fragment 实例,并实现派生自 FragmentPagerAdapter 的适配器。 在 Viewpager 和 Views中,大部分工作是在 MainActivity 生命周期方法中完成的。 在 FlashCardPager中,大部分工作将由 Fragment 在其生命周期方法之一完成。

如果尚不熟悉 Xamarin.Android 中的片段,本指南不介绍片段的基础知识,请参阅片段帮助你开始使用片段。

启动应用项目

创建名为 FlashCardPager 的新 Android 项目。 接下来,启动 NuGet 包管理器(有关安装 NuGet 包的详细信息,请参阅演练:在项目中包括 NuGet)。 查找并安装 Xamarin.Android.Support.v4 包,如 Viewpager 和 Views 中所述。

添加示例数据源

FlashCardPager中,数据源是一系列由 FlashCardDeck 类表示的抽认卡;此数据源提供包含项内容的 ViewPagerFlashCardDeck 包含一组现成的数学问题和答案。 FlashCardDeck 构造函数不需要任何参数:

FlashCardDeck flashCards = new FlashCardDeck();

FlashCardDeck 中的抽认卡集合进行组织,以便索引器可以访问每个抽认卡。 例如,以下代码行检索排列中的第四个抽认卡问题:

string problem = flashCardDeck[3].Problem;

此代码行检索对上一问题的相应答案:

string answer = flashCardDeck[3].Answer;

由于 FlashCardDeck 的实现详细信息与理解 ViewPager 无关,因此此处未列出 FlashCardDeck 代码。 FlashCardDeck.cs 提供了要 FlashCardDeck 的源代码。 下载此源文件(或将代码复制并粘贴到新的 FlashCardDeck.cs 文件中),并将其添加到项目中。

创建 ViewPager 布局

打开 Resources/layout/Main.axml,并将其内容替换为以下 XML:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    </android.support.v4.view.ViewPager>

此 XML 定义占用整个屏幕的 ViewPager。 请注意,必须使用完全限定名称 android.support.v4.view.ViewPager,因为 ViewPager 打包在支持库中。 ViewPager 仅适用于 Android 支持库 v4;它在 Android SDK 中不可用。

设置 ViewPager

编辑 MainActivity.cs 并添加以下 using 语句:

using Android.Support.V4.View;
using Android.Support.V4.App;

更改 MainActivity 类声明,使其派生自 FragmentActivity

public class MainActivity : FragmentActivity

MainActivity 派生自FragmentActivity(而不是 Activity),因为 FragmentActivity 知道如何管理片段的支持。 将 OnCreate 方法替换为以下代码:

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
    SetContentView(Resource.Layout.Main);
    ViewPager viewPager = FindViewById<ViewPager>(Resource.Id.viewpager);
    FlashCardDeck flashCards = new FlashCardDeck();
}

此代码执行以下操作:

  1. 设置 Main.axml 布局资源的视图。

  2. 从布局中检索对 ViewPager 的引用。

  3. 将新的 FlashCardDeck 实例化为数据源。

生成并运行此代码时,应会看到类似于以下屏幕截图的显示:

Screenshot of FlashCardPager app with empty ViewPager

此时,ViewPager 为空,因为它缺少用于填充 ViewPager 的片段,并且缺少适配器用于从 FlashCardDeck 中的数据创建这些片段。

在以下部分中,将创建一个 FlashCardFragment 来实现每个抽认卡的功能,并创建一个 FragmentPagerAdapter,用于将 ViewPager 连接到从 FlashCardDeck 中的数据创建的片段。

创建片段

每个抽认卡将由名为 FlashCardFragment 的 UI 片段管理。 FlashCardFragment 的视图将显示包含单个抽认卡的信息。 FlashCardFragment 的每个实例将由 ViewPager 托管。 FlashCardFragment 的视图将包含显示抽认卡问题文本的 TextView。 此视图将实现事件处理程序,该事件处理程序使用 Toast 在用户点击抽认卡问题时显示答案。

创建 FlashCardFragment 布局

在实现 FlashCardFragment 之前,必须定义其布局。 此布局是单个片段的片段容器布局。 将新的 Android 布局添加到称为 flashcard_layout.axml资源/布局。 打开 Resources/layout/flashcard_layout.axml,并将其内容替换为以下代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/flash_card_question"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textAppearance="@android:style/TextAppearance.Large"
            android:textSize="100sp"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="Question goes here" />
    </RelativeLayout>

此布局定义单个抽认卡片段;每个片段由一个 TextView 组成,该使用大字体 (100sp) 字体显示数学问题。 此文本在抽认卡上垂直和水平居中。

创建初始 FlashCardFragment 类

添加名为 FlashCardFragment.cs 的新文件,并将其内容替换为以下代码:

using System;
using Android.OS;
using Android.Views;
using Android.Widget;
using Android.Support.V4.App;

namespace FlashCardPager
{
    public class FlashCardFragment : Android.Support.V4.App.Fragment
    {
        public FlashCardFragment() { }

        public static FlashCardFragment newInstance(String question, String answer)
        {
            FlashCardFragment fragment = new FlashCardFragment();
            return fragment;
        }
        public override View OnCreateView (
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            View view = inflater.Inflate (Resource.Layout.flashcard_layout, container, false);
            TextView questionBox = (TextView)view.FindViewById (Resource.Id.flash_card_question);
            return view;
        }
    }
}

此代码会存根用于显示抽认卡的基本 Fragment 定义。 请注意,FlashCardFragment 派生自 Android.Support.V4.App.Fragment 中定义的 Fragment 的支持库版本。 构造函数为空,以便 newInstance 工厂方法用于创建新的 FlashCardFragment 而不是构造函数。

OnCreateView 生命周期方法创建和配置 TextView。 它夸大片段的 TextView 布局,并将膨胀的 TextView 返回到调用方。 LayoutInflaterViewGroup 传递给 OnCreateView,以便它可以膨胀布局。 savedInstanceState 捆绑包包含 OnCreateView 用于从保存状态重新创建 TextView 的数据。

片段的视图由调用 inflater.Inflate 显式膨胀。 container 参数是视图的父级,false 标志指示膨胀器不要将膨胀视图添加到视图的父级(ViewPager 调用适配器的 GetItem 方法稍后在本演练中添加)。

将状态代码添加到 FlashCardFragment

与活动一样,片段具有用于保存和检索其状态的 Bundle。 在 FlashCardPager 中,此 Bundle 用于保存关联抽认卡的问题和答案文本。 在 FlashCardFragment.cs 中,将以下 Bundle 键添加到 FlashCardFragment 类定义顶部:

private static string FLASH_CARD_QUESTION = "card_question";
private static string FLASH_CARD_ANSWER = "card_answer";

修改 newInstance 工厂方法,使其创建 Bundle 对象,并使用上述键在实例化后将传递的问题和答案文本存储在片段中:

public static FlashCardFragment newInstance(String question, String answer)
{
    FlashCardFragment fragment = new FlashCardFragment();

    Bundle args = new Bundle();
    args.PutString(FLASH_CARD_QUESTION, question);
    args.PutString(FLASH_CARD_ANSWER, answer);
    fragment.Arguments = args;

    return fragment;
}

修改片段生命周期方法 OnCreateView,以从传入捆绑包中检索此信息,并将问题文本加载到 TextBox

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    string question = Arguments.GetString(FLASH_CARD_QUESTION, "");
    string answer = Arguments.GetString(FLASH_CARD_ANSWER, "");

    View view = inflater.Inflate(Resource.Layout.flashcard_layout, container, false);
    TextView questionBox = (TextView)view.FindViewById(Resource.Id.flash_card_question);
    questionBox.Text = question;

    return view;
}

此处未使用 answer 变量,但在将事件处理程序代码添加到此文件时,稍后将使用该变量。

创建适配器

ViewPager 使用位于 ViewPager 和数据源之间的适配器控制器对象(请参阅 ViewPager Adapter 文章中的插图)。 若要访问此数据,ViewPager 需要提供派生自 PagerAdapter 的自定义适配器。 由于此示例使用片段,因此它使用 FragmentPagerAdapterFragmentPagerAdapter 派生自 PagerAdapterFragmentPagerAdapter 将每个页面表示为一个永久保留在片段管理器中的 Fragment,只要用户可以返回到页面。 当用户轻扫 ViewPager 的页面时,FragmentPagerAdapter 从数据源中提取信息,并使用它创建 Fragment,以便 ViewPager 显示。

实现 FragmentPagerAdapter 时,必须替换以下内容:

  • Count – 只读属性,该属性返回可用的视图数(页面)。

  • GetItem – 返回要为指定页面显示的片段。

添加名为 FlashCardDeckAdapter.cs 的新文件,并将其内容替换为以下代码:

using System;
using Android.Views;
using Android.Widget;
using Android.Support.V4.App;

namespace FlashCardPager
{
    class FlashCardDeckAdapter : FragmentPagerAdapter
    {
        public FlashCardDeckAdapter (Android.Support.V4.App.FragmentManager fm, FlashCardDeck flashCards)
            : base(fm)
        {
        }

        public override int Count
        {
            get { throw new NotImplementedException(); }
        }

        public override Android.Support.V4.App.Fragment GetItem(int position)
        {
            throw new NotImplementedException();
        }
    }
}

此代码会存根基本 FragmentPagerAdapter 实现。 在以下部分中,每个方法都替换为工作代码。 构造函数的目的是将片段管理器传递给 FlashCardDeckAdapter 的基类构造函数。

实现适配器构造函数

当应用实例化 FlashCardDeckAdapter时,它提供对片段管理器的引用和实例化的 FlashCardDeck。 将以下成员变量添加到 FlashCardDeckAdapter.csFlashCardDeckAdapter 类的顶部:

public FlashCardDeck flashCardDeck;

将以下代码行添加到 FlashCardDeckAdapter 构造函数:

this.flashCardDeck = flashCards;

此代码行存储 FlashCardDeckAdapter 将使用的 FlashCardDeck 实例。

实现计数

Count 实现相对简单:它返回抽认卡排列中的抽认卡数。 将 Count 替换为以下代码:

public override int Count
{
    get { return flashCardDeck.NumCards; }
}

FlashCardDeckNumCards 属性返回数据集中的抽认卡数(片段数)。

实现 GetItem

GetItem 方法返回与给定位置关联的片段。 当 GetItem 在抽认卡排列中调用某个位置时,它将返回一个配置为在该位置显示抽认卡问题的 FlashCardFragment。 将 GetItem 方法替换为以下代码:

public override Android.Support.V4.App.Fragment GetItem(int position)
{
    return (Android.Support.V4.App.Fragment)
        FlashCardFragment.newInstance (
            flashCardDeck[position].Problem, flashCardDeck[position].Answer);
}

此代码执行以下操作:

  1. 查找指定位置的 FlashCardDeck 排列中的数学问题字符串。

  2. FlashCardDeck 排列中查找指定位置的答案字符串。

  3. 调用 FlashCardFragment 工厂方法 newInstance,传入抽认卡问题和应答字符串。

  4. 创建并返回一个新的抽认卡 Fragment,其中包含该位置的问题和答案文本。

ViewPagerposition 呈现 Fragment 时,它将显示包含抽认卡排列中 position 的数学问题字符串的 TextBox

将适配器添加到 ViewPager

实现 FlashCardDeckAdapter 后,即可将其添加到 ViewPager。 在 MainActivity.cs 中,将以下代码行添加到 OnCreate 方法的末尾:

FlashCardDeckAdapter adapter =
    new FlashCardDeckAdapter(SupportFragmentManager, flashCards);
viewPager.Adapter = adapter;

此代码实例化 FlashCardDeckAdapter,传入第一个参数中的 SupportFragmentManager。 (FragmentActivity 的 SupportFragmentManager 属性用于获取对 FragmentManager 的引用 – 如需了解有关 FragmentManager 的详细信息,请参阅管理片段。)

核心实现现已完成 – 生成并运行应用。 应会看到抽人卡排列的第一张图像显示在屏幕上,如下一屏幕截图左侧所示。 向左轻扫以查看更多抽认卡,然后向右轻扫以在抽认卡排列上移动:

Example screenshots of FlashCardPager app without pager indicators

添加寻呼指示器

这种最小的 ViewPager 实现显示排列中的每个抽认卡,但它没有说明用户位于排列内的位置。 下一步是添加 PagerTabStripPagerTabStrip 通过显示上一个和下一个抽认卡的提示,通知用户显示问题编号,并提供导航上下文。

打开 Resources/layout/Main.axml,并向布局添加 PagerTabStrip

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

  <android.support.v4.view.PagerTabStrip
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="top"
      android:paddingBottom="10dp"
      android:paddingTop="10dp"
      android:textColor="#fff" />

</android.support.v4.view.ViewPager>

生成并运行应用时,应会看到每个抽认卡顶部显示的空 PagerTabStrip

Closeup of PagerTabStrip without text

显示标题

若要向每个页面选项卡添加标题,请在适配器中实现 GetPageTitleFormatted 方法。 ViewPager 调用 GetPageTitleFormatted(如果已实现),以获取描述位于指定位置的页面的标题字符串。 将以下方法添加到 FlashCardDeckAdapter.cs 中的 FlashCardDeckAdapter 类:

public override Java.Lang.ICharSequence GetPageTitleFormatted(int position)
{
    return new Java.Lang.String("Problem " + (position + 1));
}

此代码将抽认卡排列中的位置转换为问题编号。 生成的字符串将转换为返回到 ViewPager 的 Java String。 使用此新方法运行应用时,每个页面都会在 PagerTabStrip 中显示问题编号:

Screenshots of FlashCardPager with the problem number displayed above each page

可以来回轻扫,以查看每个抽认卡顶部显示的抽认卡排列片中的问题编号。

处理用户输入

FlashCardPagerViewPager 中提供了一系列基于片段的抽认卡,但它还没有办法揭示每个问题的答案。 在本部分中,在用户点击抽认卡问题文本时,会将事件处理程序添加到 FlashCardFragment 以显示答案。

打开 FlashCardFragment.cs,并将以下代码添加到 OnCreateView 方法的末尾,然后再将视图返回到调用方:

questionBox.Click += delegate
{
    Toast.MakeText(Activity.ApplicationContext,
            "Answer: " + answer, ToastLength.Short).Show();
};

Click 事件处理程序在用户点击 TextBox 时显示的 Toast 中显示答案。 从传递给 OnCreateView 的捆绑包读取状态信息时,answer 变量已初始化。 生成并运行应用,然后点击每个抽认卡上的问题文本以查看答案:

Screenshots of FlashCardPager app Toasts when math problem is tapped

本演练中显示的 FlashCardPager 使用派生自 FragmentActivityMainActivity,但也可以从 AppCompatActivity 派生 MainActivity(这也为管理片段提供支持)。 若要查看 AppCompatActivity 示例,请参阅示例库中 FlashCardPager

总结

本演练提供了有关如何使用 Fragment 构建基于基本 ViewPager 的应用的分步示例。 它演示了一个示例数据源,其中包含抽认卡问题和答案、用于显示抽认卡的 ViewPager 布局,以及将 ViewPager 连接到数据源的 FragmentPagerAdapter 子类。 为了帮助用户浏览抽认卡,包含说明如何添加 PagerTabStrip 以在每个页面顶部显示问题编号。 最后,添加了事件处理代码,当用户点击抽认卡问题时显示答案。