Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

Android 的 Fragment 教程

$
0
0

原文:Introduction to Android Fragments Tutorial
作者:Huyen Tue Dao
译者:kmyhy

一个 fragment 可以是任何东西,但在本文中,它是一个代码模块,保存有一个 activity 的部分 UI 和行为。正如其名所指,fragment 不是一个完整的东西,而是一个 activity 的组成部分。
从某种意义上讲,它们具备和复制了 activity 的功能。

假设你是一个 activity。你有许多活要干,因此你雇了几个“小一号的你”为你跑腿、洗衣服、交打工换取食宿的税。这就类似于 activity 和 fragment 之间的关系。

就像你不一定要找几个“小号的你”来完成你的命令一样,你也不一定非要用 fragment。但是,如果你用好了 fragment,它们能带来如下好处:

  • 模块化:将复杂的 activity 分成几个 fragment 更有利于代码的组织和维护。
  • 可重用性:将功能和UI分成多个 fragment,能够在多个 activity 之间重用它们。
  • 适应性:用不同的 fragment 呈现不同的 UI 内容,并根据屏幕方向和大小来进行不同的布局。

在本教程中,你将编写一款迷你版的暴走漫画百科。这个 app 显示一个暴走漫画的网格列表。当某个漫画被选中时,app 会显示关于该漫画的信息。在本教程中,你将学到:

  • 如何创建 fragment 并添加到 activity 中。
  • 如何使 fragment 将数据传给 activity。
  • 如何用 transaction 添加和替换 fragment>

注意:本教程假设你熟悉基本的 Android 开发并理解 activity 生命周期。如果你是一个新手,你可以先看一下 Android Tutorial for BeginnersIntroduction to Activities(本专栏也翻译了这两个教程)。

下面我们开始学习 fragment!

开始

下载开始项目并打开 Android Studio。
在 Android Studio 的欢迎屏,选择 Import project (Eclipse ADT, Gradle, etc.)。

选择开始项目的最上层文件夹,点击 OK。

浏览项目,你会发现 String 和 Drawable、XML 布局和一个 Activity。它们包含了你将用到的一些模板代码、fragment 布局、非 fragment 布局,以及一个 fragment 类。

MainActivity 包含了你所有的小片段,RageComicListFragment 包含了显示一个暴走漫画列表的代码,以便你将精力集中在 fragment。

运行 app,你将看到:

这个问题你会解决的…

Fragment 生命周期

和 Activity 一样,一个 fragment 有完整的生命周期,在不同阶段会完成不同的事件。例如,当 fragment 变成可见和激活时会发生一个事件,当 fragment 不再使用并移除时发生另外一个事件。

这是从 Android 开发者官方文档中扒来的 fragment 生命周期示意图:

当你添加一个 fragment 时会发生下列生命周期事件:

  • onAttach: 这时 fragment 附着在它的宿主 activity 上
  • onCreate: 这时一个新的 fragment 实例初始化,这个事件总是会在 fragment 附着在宿主之后发生——这时 fragment 就有点像是病毒
  • onCreateView: 这时 fragment 创建自己视图树中的一部分,这个视图树会添加到 activity 的视图树中。
  • onActivityCreated: 这时 fragment 的 activity 的 onCreate 事件结束。
  • onStart: 这时 fragment 显示,一个 fragment 在 actvity 之前是不会启动的,通常 fragment 会在 activity 启动之后立即启动。
  • onResume: 这时 fragment 可见并可与用户交互;一个 fragment 在 activity resume 之前都不会 resume,通常在 activity resume 之后立即 resume。

还有,当你移除一个 fragment 时会发生这些生命周期事件:

  • onPause: 这时 fragment 不可交互,此时要么 fragment 即将被移除或替换,要么 activity 暂停。
  • onStop: 这时 fragment 不可见,此时要么 fragment 即将被移除或替换,要么宿主 activity 关闭。
  • onDestroyView: 这时在 onCreateView 时创建的视图和资源从 activity 的视图树中移除并销毁。
  • onDestroy: 这时 fragment 进行最后的清理。
  • onDetach: 这时 fragment 从它的宿主 activity 中解除绑定。

你看到了,fragment 的生命周期和 activity 的生命周期紧密相连,但它有一些事件是专门用于 fragment 的视图树、状态和附着到 activity。

创建 Fragment

Fragments 从经常被遗忘的、专用于平板的 Honeycomb (Android 3.1)中开始出现,目的是在一个 app 中创建设备相关布局。

4.0 支持库提供了一个 fragment 实现用于支持运行于 Android 3.0 以下的设备,在 android.support.v4.app.Fragment 下面。

如果你的 app 运行在 4.0+,你可以使用 fragment。
这并不是开发者唯一要用到的支持库,还需要其他支持库,比如 v7 AppCompat Library, 它包含了 AppCompatActivity 和其它 API 21 功能向下兼容的包。 AppCompatActivity 是 v4.0 版 FragmentActivity 的子类。

因此,如果你想在 Lollipop(Android 5.0)上运行,你需要重复 v4.0 上的步骤。

创建 fragment

最终,所有的暴走漫画都在启动时显示在列表中,点击任何一个 cell 都会显示这张漫画的信息。你需要将工作进行倒推,先实现详情页面。

打开开始项目,找到 fragment_rage_comic_details.xml; 这个 XML 文件用于对漫画细节的显示进行布局。它会显示一个 Drawables 和 Strings 资源。

点击 Android Studio 的 Project 标签,右击 com.raywenderlich.alltherages 包,在上下文菜单中,选择 New\Java Class, 命名为 RageComicDetailsFragment 然后点 OK。
这个类负责显示选中漫画的详情。

在 RageComicDetailsFragment.java 中,将 import 下面的代码替换为:

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class RageComicDetailsFragment extends Fragment {

  public static RageComicDetailsFragment newInstance() {
    return new RageComicDetailsFragment();
  }

  public RageComicDetailsFragment() {
  }

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_rage_comic_details, container, false);
  }
}

Activity 总是用 setContentView() 去指定布局所使用的 XML 文件,但 fragment 需要在 onCreateView() 方法中创建它们的视图树。

inflat 方法的第三个参数指定是否需要将 inflat 后的 fragment 添加到 container。你应当总是设为 false:由 FragmentManager 负责将 fragment 添加到 container。

这里出现了一个新词:FragmentManager。每个 activity 都有一个 FragmentManager,正如其名所指,负责管理 activity 的 fragment。它也提供一个接口,你可以添加和移除这些 fragment。

你会看到 RageComicDetailsFragment 有一个工厂初始化方法 newInstance(), a和一个空的公共构造函数。

为什么同时需要这两个方法?newInstance 只不过是调用了构造方法而已。

Fragment 子类需要一个空的默认构造函数。如果你不提供,但定义了一个非空的构造函数,Lint(Android 的静态代码扫描工具)会报错:

奇怪的是,仍然能够编译,当运行 app 的时候,你会得到一个严重异常。

你应该知道,当 app 进入后台时,Android 会销毁并在后面重建一个 activity 及其包含的 fragment。当 activity 恢复时,它的 FragementManager 开始调用这个空的默认构造方法重建 fragment。如果找不到这个方法,你会得到一个异常。

等下,如果你需要传点什么数据给 fragment 怎么办?
别急,这个问题后面会讲。

添加一个 Fragment

这里,你将添加你的崭新的 fragment,我们将用一种最简单的方式:将它加到一个 activty 的 XML 布局文件中。

打开 activity_main.xml 在根节点 FrameLayout 内部添加:

<fragment
  android:id="@+id/details_fragment"
  class="com.raywenderlich.alltherages.RageComicDetailsFragment"
  android:layout_width="match_parent"
  android:layout_height="match_parent"/>

我们在 activity 的布局中放了一个 标签,并指定 fragment 的类型为 inflate 时的类。 中,视图的 ID 是 FragmentManager 所必须的。

运行程序,你会看到:

动态添加 fragment

首先打开 activity_main.xml ,删除刚才添加的 (好了,我知道你刚刚才添加它,不好意思了)。你会将它替换成暴走漫画列表。

打开 RageComicListFragment.java,里面有许多列表相关代码。你会看到 RageComicListFragment 有一个空的默认构造函数和一个 newInstance() 方法。

RageComicListFragment 中的代码使用到了一些资源。为了访问这些资源,你必须保证 fragment 正确引用了一个 Context。这样 onAttach() 方法才能够发挥作用。
打开 RageComicListFragment.java, 在现有的 import 语句中添加下列导入语句:

import android.content.res.Resources;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.app.Activity;

这些代码只是用于引入所必须的类。

在 RageComicListFragment.java 中,将这两个方法加在 RageComicAdapter 定义之前:

@Override
public void onAttach(Context context) {
  super.onAttach(context);

  // 读取漫画名称和描述
  final Resources resources = context.getResources();
  mNames = resources.getStringArray(R.array.names);
  mDescriptions = resources.getStringArray(R.array.descriptions);
  mUrls = resources.getStringArray(R.array.urls);

  // 读取漫画图片
  final TypedArray typedArray = resources.obtainTypedArray(R.array.images);
  final int imageCount = mNames.length;
  mImageResIds = new int[imageCount];
  for (int i = 0; i < imageCount; i++) {
    mImageResIds[i] = typedArray.getResourceId(i, 0);
  }
  typedArray.recycle();
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  final View view = inflater.inflate(R.layout.fragment_rage_comic_list, container, false);

  final Activity activity = getActivity();
  final RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
  recyclerView.setLayoutManager(new GridLayoutManager(activity, 2));
  recyclerView.setAdapter(new RageComicAdapter(activity));
  return view;
}

onAttach() 方法中的代码通过 fragmen 将要附着的 Context 对象访问要用到的资源。因为这些代码是在 onAttach() 方法中,你不用担心 fragment 没有一个有效的 Context。

在 onCreateView(), 你 inflate RageComicListFragment 的视图树并进行一些设置工作。一个 RecyclerView 是一种用于显示多个 item 并需要用户上下滚动的有效手段。

它比传统的列表或网格方式显示 item 更加高效,因为一旦用户将当前显示的 item 滚动出视图,那个 item 会被“复用”并用于显示新的 item。

通常,当你需要调教 fragment 的时候,onCreateView() 是个不错的地方,因为你的视图已经准备好了。

然后需要将 RageComicListFragment 放到 MainActivity 中。你会调用你新认识的伙伴 FragmentManager 去添加它。
打开 MainActivity.java 在 onCreate() 中添加代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

  if (savedInstanceState == null) {
    getSupportFragmentManager()
      .beginTransaction()
      .add(R.id.root_layout, RageComicListFragment.newInstance(), "rageComicList")
      .commit();
  }
}

运行程序,你会看到一个暴走漫画列表:

FragmentManager 通过 FragmentTransactions 来完成基本的 fragment 操作,比如添加、删除等等。

首先调用 getSupportFragmentManager() 而不是 getFragmentManager 获取 FragmentManager,因为你正在用 fragment 兼容模式。

然后用 FragmentManager 的 beginTransaction() 开启一个新事务 — 你可以自己理解去。接着通过 add 方法指定你想进行的操作,并传递这些参数:

  • activity 布局中用于包含这个 fragment 视图树的视图 ID。如果你看一眼 activity_main.xml, 你会看见 @+id/root_layout。
  • 要添加的 fragment 实例。
  • 一个字符串,用于作为这个 fragment 的 Tag 或 Identifier。通过这个字符串 FragmentManager 可以在以后检索到这个 fragment。

最后,调用 commit() 让 FragmentManager 执行事务。

这样,fragment 就被添加上了。

if 语句用于显示 fragment 并判断 activity 是否有持久化的状态。当一个 activity 被保存时,它所有激活的 fragment 也会被保存。如果不进行判断,那么很可能会发生这种事情:

这时你就会:

结论:始终明白存储状态会对你的 fragment 造成什么样的影响。

与 activity 通讯

尽管 fragment 是附着在 activity 上的,如果没有你的“支持”,它们根本无法互相交谈。

对于所有的漫画,你需要让 RageComicListFragment 去通知 MainActivity,用户何时选中了一个漫画,以便 RageComicDetailsFragment 显示所选的漫画。

打开 RageComicListFragment.java 在底部添加一个 Java 接口:

public interface OnRageComicSelected {
  void onRageComicSelected(int imageResId, String name,
    String description, String url);
}
···

这个接口定义了一个监听器接口,用于 activity 监听 fragment。这个 Activity 会实现这个接口,当 item 被点击是,fragment 会调用 onRageComicSelected() 方法,并传递选中的漫画信息给 activity。

在已有的字段后添加一个新字段:

```java
private OnRageComicSelected mListener;




<div class="se-preview-section-delimiter"></div>

这个字段用于引用一个 fragment 监听器,它应当是当前 activity。
在 onAttach() 中,在 super.onAttach(context); 一句后添加:

if (context instanceof OnRageComicSelected) {
  mListener = (OnRageComicSelected) context;
} else {
  throw new ClassCastException(context.toString() + " must implement OnRageComicSelected.");
}




<div class="se-preview-section-delimiter"></div>

在这里初始化监听器实例。放在 onAttach() 方法中进行是为了确保 fragment 已经添加。然后检查 activity 是否实现 OnRageComicSelected 接口。
如果没有实现,扔出一个异常。如果实现了,将 activity 设置为 RageComicListFragment 的监听器。

在 onBindViewHolder() 方法的最后一句后,添加下列代码——我承认,我在这里搞错了,RageComicAdapter 还忘记了一个地方:

viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    mListener.onRageComicSelected(imageResId, name, description, url);
  }
});




<div class="se-preview-section-delimiter"></div>

这里将一个 View.OnClickListener 添加给每一个暴走漫画,这样它会调用这个 listner(这个 activity)的回调方法,并传递所选的漫画。

打开 MainActivity.java ,将类定义修改为:

public class MainActivity extends AppCompatActivity
  implements RageComicListFragment.OnRageComicSelected {




<div class="se-preview-section-delimiter"></div>

这里声明了 MainActivity 会实现 OnRageComicSelected 接口。

现在,你用一个 toast 来检验这个代码是否工作。在已有的 import 语句后添加如下 import,以便你可以使用 toast:

import android.widget.Toast;




<div class="se-preview-section-delimiter"></div>

然后在 onCreate() 方法后面添加:

@Override
public void onRageComicSelected(int imageResId, String name, String description, String url) {
  Toast.makeText(this, "Hey, you selected " + name + "!", Toast.LENGTH_SHORT).show();
}




<div class="se-preview-section-delimiter"></div>

运行程序。当 app 启动,点击一张暴走漫画。你会看到一个 toast 消息,显示所选的 item:

你的 activity 和它的 fragment 发生了一次交谈。你就像是一位杰出的数位外交官。

Fragment 参数和事务

当前,RageComicDetailsFragment 显示的是一个静态的图片和字符串,但我们需要能够显示用户选择的图片和字符串。

打开 RageComicDetailsFragment.java 在类定义头部加入下列常量:

private static final String ARGUMENT_IMAGE_RES_ID = "imageResId";
private static final String ARGUMENT_NAME = "name";
private static final String ARGUMENT_DESCRIPTION = "description";
private static final String ARGUMENT_URL = "url";




<div class="se-preview-section-delimiter"></div>

将 newInstance() 替换为:

public static RageComicDetailsFragment newInstance(int imageResId, String name,
  String description, String url) {

  final Bundle args = new Bundle();
  args.putInt(ARGUMENT_IMAGE_RES_ID, imageResId);
  args.putString(ARGUMENT_NAME, name);
  args.putString(ARGUMENT_DESCRIPTION, description);
  args.putString(ARGUMENT_URL, url);
  final RageComicDetailsFragment fragment = new RageComicDetailsFragment();
  fragment.setArguments(args);
  return fragment;
}




<div class="se-preview-section-delimiter"></div>

一个 fragment 可以把构造函数的参数作为它的 arguments 属性,这个属性可以用 getArguments() 和 setArguments() 来访问。这个 arguments 实际上是一个 Bundle,以键值对的方式存储,就像 Activity.onSaveInstanceState 中的 Bundle 一样。

这里你创建了一个 Bundle,并设置它的内容,然后调用 setArguments,当你后面要用到这些值时,调用 getArguments。

前面说过,当 fragment 重建时,使用默认的的空构造函数——这里没有参数。
因为 fragment 能够从它的持久化参数中找回它的构造参数,你可以在重建时利用这一点。上面的代码会将选中的暴走漫画保存在 RageComicDetailsFragment 的 arguments 属性中。

在 RageComicDetailsFragment.java 顶部加入 import 语句:

import android.widget.ImageView;
import android.widget.TextView;




<div class="se-preview-section-delimiter"></div>

然后,在 onCreateView() 方法中加入:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  final View view = inflater.inflate(R.layout.fragment_rage_comic_details, container, false);
  final ImageView imageView = (ImageView) view.findViewById(R.id.comic_image);
  final TextView nameTextView = (TextView) view.findViewById(R.id.name);
  final TextView descriptionTextView = (TextView) view.findViewById(R.id.description);

  final Bundle args = getArguments();
  imageView.setImageResource(args.getInt(ARGUMENT_IMAGE_RES_ID));
  nameTextView.setText(args.getString(ARGUMENT_NAME));
  final String text = String.format(getString(R.string.description_format), args.getString
    (ARGUMENT_DESCRIPTION), args.getString(ARGUMENT_URL));
  descriptionTextView.setText(text);
  return view;
}




<div class="se-preview-section-delimiter"></div>

因为你想用用户选择的漫画动态地刷新 RageComicDetailsFragment 的 UI,所以在 onCreateView 中获得了 fragment 视图中的 ImageView 和 TextView 的引用。然后用传递给 RageComicDetailsFragment 的图片和文字渲染它们,这些参数在 arguments 属性中。

最后,当用户点击一个 item 时,我们需要创建并显示一个 RageComicDetailsFragment 来代替 toast 消息。打开 MainActivity 将 onRageComicSelected 中的代码替换为:

@Override
public void onRageComicSelected(int imageResId, String name, String description, String url) {
  final RageComicDetailsFragment detailsFragment =
    RageComicDetailsFragment.newInstance(imageResId, name, description, url);
  getSupportFragmentManager()
    .beginTransaction()
    .replace(R.id.root_layout, detailsFragment, "rageComicDetails")
    .addToBackStack(null)
    .commit();
}

这段代码中用到了一些之前没有用过的类,你需要按 option+回车 来引入缺失的类。

你会发现这些代码和我们第一次添加 list 到 MainActivity 时的事务差不多,但有几个地方不同。

  • 创建了一个 fragment 实例,使用了一些漂亮的参数。
  • 调用了 replace 而不是 add 方法,replace 会从容器中移除当前 fragment,然后添加新的 fragment。
  • 调用了另一个新面孔:FragmentTransaction 的 addToBackStack() 方法。fragment 有一个返回栈,或者历史栈,就像 activity 一样。

fragment 返回栈不独立于 activity 返回栈。可以把它看成是位于宿主 activity 之上的另一个历史栈。

当你在两个 activity 之间切换时,每一个都会放到 activity 的返回栈中。当你提交一个 FragmentTransaction 时,你可以将这个事务添加到返回栈中。

因此 addToBackStack() 是干什么的?它将 replace() 添加到返回栈中,以便当用户点击设备的返回按钮时,它可以回退改事务。也就是说,点击返回按钮后用户会返回到整个列表。

列表的 add() 事务会忽略 addToBackStack() 方法调用。也就是说这个事务是整个 activity 历史记录的一部分。如果用户在 list 页面点击返回按钮,则退出应用程序。

猜到了吧,这就是全部代码了,所以我们来运行一下 app。

看起来没有太大的不同嘛,还是同一个 list。这次,当你点击一张暴走漫画,你会看到漫画的详情代替了 tost:


噢耶!你的 app 现在真正包含了所有暴走一族,你对 fragment 也有了一个很好的理解。

结束

你可以从这里下载完成了的项目。
还有许多关于 fragment 的东西去学习。就像所有工具或功能,请考虑 fragment 是否适合你的 app 的需求,如果是,请遵循下列最佳体验和约定。

为了让你的技能升级,有一些东西需要注意:

  • 在一个 ViewPager 中使用 fragment:许多 app 包括 Play Store 都使用了一个可拖动的、标签页式的 ViewPagers。
  • 使用更加强大和高级的 DialogFragment 替代老朽的 AlertDialog。
  • 尝试将 fragment 和其它 activity 组件进行交互,比如 app 的 bar。
  • 用 fragment 创建自适应 UI。实际你可以参考Adaptive UI in Android Tutorial

希望你喜欢这篇教程,有任何疑问和建议,请在下面留言。

作者:kmyhy 发表于2017/3/4 12:59:36 原文链接
阅读:177 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>