使用Fragment创建动态用户界面(翻译)
使用Fragment创建动态用户界面
在Android上创建动态的并且多窗格的用户界面,你需要使用模块封装UI组件和Activity的行为,并且这些模块你可以将它们换入和换出你的Activity中。你可以使用Fragment类来创建这些模块,它表现的就像是嵌套的Activity,可以定义自己的布局并管理自己的生命周期。
当一个Fragment可以指定它自己的布局时,它也可以在一个Activity中和其他的Fragment组合在一起进行配置,从而为不同的屏幕大小修改你的布局配置(比如在小的屏幕上可能一次只显示一个Fragment,但在一个大的屏幕上可以显示两个或者更多)。
这个课程告诉你如何使用Fragment创建一个动态的用户体验并且为不同屏幕大小的设备优化你的app的用户体验。这些支持运行最老Android1.6版本系统的设备。
使用Android支持库
Android支持库提供一个带有API库的JAR文件,这些API库允许你的app运行在更早版本的Android上时,可以使用一些最近的Android API。举个例子,支持库提供了一个版本的Fragment API,使你可以使用Android1.6(API level 4)及以上版本才能使用的Fragment API。
这一课告诉你如何设置app来使用支持库,从而可以使用Fragment来创建动态app用户界面。
设置你的工程使用支持库
设置你的工程:
- 使用SDK Manager下载Android支持库
- 在你的Android工程的顶级层次创建一个libs目录
- 定位你想使用的库的jar文件,并将它拷贝到libs目录。举个例子,支持API level 4及以上版本的支持库是<sdk>/extras/android/support/v4/android-support-v4.jar
- 更新你的manifest文件,设置最小API级别为4,目标API级别为最新发布的版本。
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15" />
导入支持库API
支持库包含大量的API,要么是最近版本Android增加的要么是在平台上完全不存在的,它们只是在你开发特殊的应用程序特性时提供额外的支持。
你可以在平台文档库(android.support.v4.*)中找到所有的API支持库的指导文档。
警告:为了确保你不会在老的系统版本上意外的使用新的API,请确定你是从android.supoort.v4.app包中导入Fragment类和相关的API。
import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; ...
当使用Android支持库时,创建一个使用Fragment的Activity类时,你必须继承FragmentActivity类而不是传统的Activity类,你将会在下一课看到一些关于Fragment和Activity的示例代码。
创建一个Fragment
你可以认为Fragment是Activity的一个单元段,它拥有自己的生命周期,接收它自己的输入事件,可以在Activity运行的时候被增加和删除(就像是一种子Activity,你可以在不同的Activity中重用它)。这一课将告诉你如何使用Android支持库继承Fragment类,因此你的app可以仍然兼容运行像Android1.6这么旧的系统的设备。
注意:如果你因为其他原因决定你的app要求的最低的API level为11的话,你就不需要使用Android支持库了,可以使用框架自带的Fragment类及相关的API。请注意这一课重点关注使用来自支持库的API,它使用一个特殊的包签名,并且有的时候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) { // Inflate the layout for this fragment return inflater.inflate(R.layout.article_view, container, false); } }
就像一个Activity,一个Fragment应该实现其他的生命周期方法,从而允许你管理它的状态随着它被从一个Activity增加或删除,就像Activity在它的生命周期状态之间转换一样。例如,当一个Activity的onPause()方法被调用,Activity中的每个Fragment都会收到一个onPause()回调。
更多的关于Fragment生命周期和回调方法的信息可以在Fragment开发者指南里获得。
通过XML将一个Fragment添加到Activity中
虽然Fragment是可重用的模块化的UI组件,但是每个Fragment实例必须和一个父组件FragmentActivity关联在一起。你可以通过在Activity的布局xml文件中定义每一个Fragment来达到这个关联的目录。
注意:FragmentActivity是一个在支持库中提供的特殊的Activity,来在版本低于API level 11的系统上支持Fragment。如果你支持的最低的系统版本是API level 11或更高,则你可以使用一个普通的Activity。
这里有一个布局文件的例子,当设备屏幕被认为是大的时候,它添加两个Fragment到Activity中(通过目录名的large限定词来指定)。
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); } }
注意:当你通过在XML布局文件中定义Fragment的方式将一个Fragment添加到一个Activity中时,你不能在运行时移除这个Fragment。如果你计划在用户交互过程中换入和换出你的Fragment,你必须在Activity首次启动时添加Fragment到Activity中,就像下一课所示。
创建一个灵活的用户界面
当设计你的应用程序使其支持大范围的屏幕大小时,你能在不同的布局配置中重用你的Fragment,从而在可用的屏幕空间的基础上优化用户体验。
举个例子,在手持设备上,每次只为单个窗格用户界面显示一个Fragment可能是合适的。相反的,你可能想要在平板上设置并排的Fragment从而向用户显示更多的信息,因为平板上有一个更宽的屏幕。
FragmentManager类提供方法允许你在运行时为Activity增加、移除和替换Fragment,从而可以创建一个动态的体验。
在运行时为Activity增加一个Fragment
与其在布局文件中为Activity定义Fragment,就像前面的课程展示的<fragment>元素一样,你可以在Activity运行期间为Activity增加Fragment。如果你计划在Activity的生命周期中更改Fragment的话,这很重要。
为了执行一个增加或移除Fragment的事务,你必须使用FragmentManager来创建一个FragmentTransaction,这个类提供API来增加、移除、替换和执行其他Fragment事务。
如果你的Activity允许Fragment被移除或替换的,你应该在Activity的onCreate()方法期间增加初始Fragment到这个Activity上。
处理Fragment时有一个重要的原则,尤其是那些你需要在运行增加的Fragment,那就是在布局中Fragment必须有一个容器视图,Fragment的布局将会在这个视图中。
下面这个布局是前面的课程中展示的一个可选布局,每次只展示一个Fragment。为了能够使用另一个Fragment替换这个,Activity的布局中包括一个空的FrameLayout用作Fragment的容器。
注意:这个文件的文件名和前一课中的布局文件是一样的,但是布局目录不包括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在一个Activity执行多个Fragment事务。当你准备好做出这些改变后,你必须执行commit()方法。
这里有一个例子,告诉你如何在前面的布局中增加一个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); // Check that the activity is using the layout version with // the fragment_container FrameLayout if (findViewById(R.id.fragment_container) != null) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. if (savedInstanceState != null) { return; } // Create an instance of ExampleFragment HeadlinesFragment firstFragment = new HeadlinesFragment(); // In case this activity was started with special instructions from an Intent, // pass the Intent's extras to the fragment as arguments firstFragment.setArguments(getIntent().getExtras()); // Add the fragment to the 'fragment_container' FrameLayout getSupportFragmentManager().beginTransaction() .add(R.id.fragment_container, firstFragment).commit(); } } }
因为这个Fragment是在运行时被增加到FrameLayout容器中的,而不是通过<fragment>元素定义在Activity的布局文件中的,因此Activity可以移除这个Fragment或使用另一个Fragment替换它。
使用另一个Fragment替换这个
替换一个Fragment的过程和增加一个很像,只是要求调用replace()方法,而不是add()方法。
始终在大脑中记住,当你执行Fragment事务时,比如替换或者删除一个Fragment,允许用户导航返回并撤销这个改变通常是合适的。为了允许用户通过Fragment事务导航回来,你必须在commit FragmentTransaction之前调用addToBackStack()方法。
注意:当你移除或替换一个Fragment并把这个事务添加到返回栈中时,移除的Fragment是被停止了(不是销毁)。如果用户导航回来恢复这个Fragment,它重新启动了。如果你没有将这个事务添加到返回栈中,则这个Fragment在被移除或替换时被销毁了。
这里有一个使用另一个Fragment替换的例子:
// Create fragment and give it an argument specifying the article it should show ArticleFragment newFragment = new ArticleFragment(); Bundle args = new Bundle(); args.putInt(ArticleFragment.ARG_POSITION, position); newFragment.setArguments(args); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); // Replace whatever is in the fragment_container view with this fragment, // and add the transaction to the back stack so the user can navigate back transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); // Commit the transaction transaction.commit();
addBackStack()方法使用一个可选的字符串参数,用于为这个指定一个唯一的名字。这个名字不需要,除非你要使用FragmentManager.BackStackEntry API执行高级Fragment操作
和其他Fragment通信
为了重用Fragment UI组件,你应该将每个Fragment构建成完全独立的、模块化的组件,定义它们自己的布局和行为。一旦你已经定义好这些可重用的Fragment,你就能将它们和Activity关联在一起,并将它们和应用程序逻辑联系在一起从而实现完全合成的用户界面。
通常,你会想要一个Fragment和另一个通信,例如基于用户时间改变内容。所有的Fragment到Fragment之间的通信都是通过关联的Activity完成的。两个Fragment应该从不直接通信。
定义一个接口
为了让一个Fragment的通信取决于它的Activity,你可以在Fragment类中定义一个接口然后在Activity中实现它。Fragment在它的onAttach()声明周期方法期间获得这个接口的实现,从而可以通过调用接口方法来与Activity通信。
这里有一个Fragment同Activity通信的例子:
public class HeadlinesFragment extends ListFragment { OnHeadlineSelectedListener mCallback; // Container Activity must implement this interface public interface OnHeadlineSelectedListener { public void onArticleSelected(int position); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { mCallback = (OnHeadlineSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnHeadlineSelectedListener"); } } ... }
现在Fragment可以通过使用OnHeadlineSelectedListener接口的mCallback实例调用onArticleSelected()方法(或者接口中的其他方法),发送消息给Activity。
例如,当用户点击一个列表项目时,Fragment中的如下方法被调用。Fragment使用回调接口来发布事件给父Activity。
@Override public void onListItemClick(ListView l, View v, int position, long id) { // Send the event to the host activity mCallback.onArticleSelected(position); }
实现接口
为了接收从Fragment过来的事件回调,作为Fragment的宿主的Activity必须实现Fragment类中定义的接口。
例如,下面这个Activity实现了上面例子中Fragment定义的接口:
public static class MainActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener{ ... public void onArticleSelected(int position) { // The user selected the headline of an article from the HeadlinesFragment // Do something here to display that article } }
发送消息给Fragment
宿主Activity使用findFragmentById()函数获取Fragment的实例后,可以直接调用Fragment的公共方法发送消息给Fragment。
举个例子,设想上面所示的Activity可能包含另一个Fragment用于显示上面的回调函数返回的数据所指定的列表项。在这种情况下,Activity可以传递在回调函数中收到的信息给另一个将要显示这个列表项的Fragment。
public static class MainActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener{ ... public void onArticleSelected(int position) { // The user selected the headline of an article from the HeadlinesFragment // Do something here to display that article ArticleFragment articleFrag = (ArticleFragment) getSupportFragmentManager().findFragmentById(R.id.article_fragment); if (articleFrag != null) { // If article frag is available, we're in two-pane layout... // Call a method in the ArticleFragment to update its content articleFrag.updateArticleView(position); } else { // Otherwise, we're in the one-pane layout and must swap frags... // Create fragment and give it an argument for the selected article ArticleFragment newFragment = new ArticleFragment(); Bundle args = new Bundle(); args.putInt(ArticleFragment.ARG_POSITION, position); newFragment.setArguments(args); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); // Replace whatever is in the fragment_container view with this fragment, // and add the transaction to the back stack so the user can navigate back transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); // Commit the transaction transaction.commit(); } } }