使用Fragment创建动态UI
在Android中创建动态、多栏的UI,你需要将UI组件和Activity行为封装到模块中,以便在Activity中交换。你可以使用Fragment类创建这些模块,这种行为有点像一个可以自己定义布局和管理生命周期的嵌套Activity。
当一个Fragment指定它自身的布局时,它能和Activity内的其它Fragment配置成不同的组合以便为不同的屏幕大小修改你的布局结构(小屏幕一次可能只显示一个Fragment,大屏幕则可以显示两个或更多)。
本课程展示了如何使用Fragment创建动态的用户体验以及为不同屏幕大小的设备优化App的用户体验,同时继续支持运行于低至Android1.6版本的设备。
创建Fragment
你可以认为Fragment是Activity的一个模块化部分,它有自己的生命周期,有自己的输入事件,并且你还可以在Activity运行时添加或移除它(有点像可以在不同的Activity中重用的“子Activity”)。这节内容展示了如何使用支持库继承Fragment,使你的App可以和运行低至Android1.6的设备保持兼容。
注意:如果你决定了App要求的最小API级别为11以上,你可以使用框架内建的Fragment类及相关的API,而不需要使用支持库。只要知道这节课的重点在于使用支持库API,它使用特定的包签名,并且某些API名称和平台包含的版本有点不同。
在你开始课程之前,你必须设置你的Android项目使用支持库。如果你之前没有使用支持库,按“安装支持库”文档所说的把你的项目设为使用v4库,你也可以使用v7 appcompat库以便在Activity中包含操作栏,v7兼容Android2.1(API级别7)并且也包含Fragment API。
创建Fragment类
要创建Fragment,从Fragment类继承,重写关键的生命周期方法以插入App的应用逻辑,就像你在Activity类做的一样。
不同的是,创建Fragment时,你必须在onCreateView()回调方法中定义布局。事实上,这是运行Fragment所需要的唯一回调方法。下例是一个指定自己布局的简单Fragment:
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.ViewGroup;
public class ArticleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// 填充fragment的布局
return inflater.inflate(R.layout.article_view, container, false);
}
}
就像Activity一样,Fragment应该实现其它的生命周期回调方法,这样,当它在Activity中被添加或移除时,以及Activity本身在生命周期状态中切换时,允许你管理Fragment的状态。例如,Activity的onPause()被调用时,Activity中的所有Fragment也会收到onPause()的调用。
更多有关Fragment生命周期和回调方法的信息,请参考Fragment开发者指南。
使用XML向Activity添加Fragment
Fragment是可重用的、模块化的UI组件,每个Fragment类的实例都必须关联一个父FragmentActivity。你可以在Activityr的布局XML文件中定义每一个Fragment来获取这种关联。
注意:FragmentActivityj 是一个支持库提供的特殊的Activity,用来在低于API级别11的系统版本中处理Fragment。如果你支持的最低系统版本高于API级别11,你可以使用Activity。
这里是一个当设备屏幕为“大”(目录名使用了“large”修饰符)时,向Activity添加两个Fragment的布局文件的示例:
res/layout-large/news_articles.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<fragment android:name="com.example.android.fragments.HeadlinesFragment"
android:id="@+id/headlines_fragment"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<fragment android:name="com.example.android.fragments.ArticleFragment"
android:id="@+id/article_fragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />
</LinearLayout>
提示:更多有关为不同屏幕大小创建布局的内容,参看“支持不同屏幕大小”。
在Activity中应用布局:
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
public class MainActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
}
}
如果你使用了v7 appcompat库,Activity应该继承ActionBarActivity,这是FragmentActivity的一个子类(更多信息请参见“添加操作栏”)。
注意:当你通过在布局XML文件中的声明Fragment的方式向Activity布局添加Fragment时,你不能在运行时移除Fragment。如果你打算在用户交互时交换Fragment,你必须在Activity首次启动时添加Fragment,这将在下一节演示。
构建灵活的UI
当为较大范围的屏幕尺寸设计应用程序时,你可以基于屏幕所允许的空间,在不同的布局配置中重用Fragment以优化用户体验。
例如,在手机设备上为单面板用户界面一次只显示一个Fragment,相反,在有较宽屏幕尺寸的平板上可以并排设置Fragment来为用户显示更多信息。
两个Fragment,用不同配置显示在不同屏幕尺寸上的同一个Activity中。在大屏幕上,两个Fragment并排显示,而在手机设备上,一次只显示一个Fragment,必须在用户导航时用一个Fragment替换另一个。
FragmentManager类提供方法允许你在运行时为Activity添加、移除以及替换Fragment,从而创建动态用户体验。
在运行时向Activity添加Fragment
相比在布局文件中为Activity定义Fragment——就象上节课中演示的使用<fragment>元素——你可以在Activity运行时添加Fragment,如果你打算在Activity生存期内改变Fragment,你必须这样做。
要执行添加或移除Fragment的事务,你必须使用FragmentManager创建一个FragTransaction,它提供了添加、移除、替换及执行其它Fragment事务的API。
如果你的Activity允许移除或替换Fragment,你应该在onCreate()方法中添加初始化Fragment的代码。
在处理Fragment(尤其是那些运行时添加的)时有一个重要的规则,在布局中必须有一个容器视图供这些Fragment布局驻留。
下面的布局是一个替换在上节内容中出现的一次只显示一个Fragment的布局。为了用一个Fragment替换另一个,Activity包含一个FrameLayout做为Fragment的容器。
注意文件名和上节内容中的布局文件名相同,但是目录名中没有“large”修饰符,因此这个布局用于设备屏幕比“large”小,不能同时填充两个Fragment的情况中。
res/layout/news_articles.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
在Activity中插入代码,调用支持库API的getSupportFragmentManager()方法获得FragmentManager,然后调用beginTransaction()创建FragmentTransaction,调用add()方法添加Fragment。
你可以使用同一个FragmentTransaction执行多个Fragment事务,当你准备好要改变时,你必须调用commit()方法。
下面是如何向Activity中添加Fragment的例子:
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
public class MainActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
// 检查Activity正在使用的包含Fragment容器FrameLayout的布局版本
if (findViewById(R.id.fragment_container) != null) {
// 如果我们是从较早的状态中恢复
// 我们不需要做任何事情,直接返回
// 否则,我们可能会覆盖掉已经存在的Fragment
if (savedInstanceState != null) {
return;
}
// 创建放在Activity中的新Fragment
HeadlinesFragment firstFragment = new HeadlinesFragment();
// 在这个例子中,Activity是被Intent对象的特殊指令启动的,
// 把Intent的extras集合传递给Fragment做为参数
firstFragment.setArguments(getIntent().getExtras());
// 把Fragment添加到容器FrameLayout中
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
}
}
因为Fragment在运行时添加到FrameLayout容器中(替换在Activity布局文件中用<fragment>元素定义的),Activity可以移除它,并用另一个Fragment替换它。
用一个Fragment替换另一个
替换Fragment的过程就是简单的添加一个Fragment,只不过是用replace()方法代替add()方法。
请记住,当执行Fragment事务,如替换或删除Fragment时,它通常允许用户向后导航并“撤销”改变。要允许用户通过Fragment事务向后导航,你必须在提交FragmentTransaction之前调用addToBackStack()方法。
注意:当你移除或替换一个Fragment并把事务添加到返回栈时,被移除的Fragment处于停止状态(不是被销毁)。如果用户向后导航以恢复Fragment,它会重新启动。如果你没有向返回栈中添加事务,那么移除或替换Fragment时,它被销毁。
下面是替换Fragment的例子:
// 创建一个Fragment并给它指定一个要显示的文章做为参数
ArticleFragment newFragment = new ArticleFragment();
Bundle args = new Bundle();
args.putInt(ArticleFragment.ARG_POSITION, position);
newFragment.setArguments(args);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// 用这个Fragment替换Fragment容器中的内容
// 向返回栈中添加事务以便用户可以向后导航
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// 提交事务
transaction.commit();
addToBackStack()方法有一个可选参数来为事务指定唯一名称,不需要这个名称,除非你打算用FragmentManager.BackStackEntry API来执行Fragment的高级操作。
和其它Fragment通信
为了重用Fragment UI组件,你应该定义一个完全独立的、模块化的组件,它定义了自己的布局和行为。一旦你定义了这些可重用的Fragment,你可以把它们和Activity结合起来,并关连应用程序逻辑以实现整体组合UI。
通常你会希望一个Fragment能和其它Fragment通信,比如基于用户事件改变内容。所有Fragment到Fragment的通信都是通过相关的Activity,两个Fragment之间应该永远不要直接通信。
定义接口
要允许一个Fragment和它所在的Activity通信,你可以在Fragment类中定义一个接口,在Activity中实现它。Fragment会在onAttach()生命周期回调方法中捕获接口的实现,并能调用接口方法和Activity通信。
下面是Fragment和Activity通信的例子:
public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener mCallback;
// 容器Activity必须实现这个接口
public interface OnHeadlineSelectedListener {
public void onArticleSelected(int position);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// 确保容器Activity实现了回调接口,
// 否则,它会抛出异常
try {
mCallback = (OnHeadlineSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnHeadlineSelectedListener");
}
}
...
}
现在Fragment可以使用OnHeadlineSelectedListener接口的实例mCallback调用onArticleSelected()方法把消息发送给Activity。
例如,当用户点击列表项时,Fragment中的方法使用回调接品把事件发送到父Activity:
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
// 把事件发送到父Activity
mCallback.onArticleSelected(position);
}
实现接口
为了从Fragment中接收事件回调,父Activity必须实现在Fragment类中定义的接口。
下面是一个实现上例中接口的例子:
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener{
...
public void onArticleSelected(int position) {
// 当用户选择了HeadlinesFragment的标题时,
// 完成显示文章的代码
}
}
发送消息到Fragment
父Activity可以把消息发给通过findFragmentById()方法捕获的Fragment实例,然后直接调用Fragment的公开方法。
例如,假设上面的Activity包含另一个Fragment,用来显示通过上面的回调方法获得的列表项数据。在该例中,Activity可以把在回调方法中收到的信息传递给另一个Fragment并显示它。
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener{
...
public void onArticleSelected(int position) {
// 当用户选择了HeadlinesFragment的标题时,
// 完成显示文章的代码
ArticleFragment articleFrag = (ArticleFragment)
getSupportFragmentManager().findFragmentById(R.id.article_fragment);
if (articleFrag != null) {
// 如果显示文章的Fragment可用,我们处于双栏布局中...
// 调用ArticleFragment中的方法,更新它的内容
articleFrag.updateArticleView(position);
} else {
// 否则,我们处于单栏布局中,需要交换Fragment...
// 创建Fragment并把所选择的文章做为它的参数
ArticleFragment newFragment = new ArticleFragment();
Bundle args = new Bundle();
args.putInt(ArticleFragment.ARG_POSITION, position);
newFragment.setArguments(args);
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
// 用这个Fragment替换Fragment容器中的内容
// 并在返回栈中添加事务,以便用户可以向后导航
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// 提交事务
transaction.commit();
}
}
}