为了在Android上产生动态和多面板的用户界面,你需要将UI组件和activity行为封装成模块,你能将这些模块添加进或者移除你的Activity。你能产生这些模块用Fragment类。它的行为一定程度上像内嵌的activity,能定义它自己的布局和管理自己的生命周期。
当一个Fragment指定自己的布局时,它能和其他不同的fragment组合配置在一个activity里去修改你的布局配置去适应不同的屏幕尺寸(小屏幕一次可能显示一个fragment,但是大屏幕可能显示两个或者更多)。
本文讲述如何通过fragment产生一个动态的用户界面,以及不同的屏幕尺寸的设备上如何优化你的app的用户界面,所有的这些特性最低可以支持到Android 1.6.
Lessons
Create a Fragment
学习如何产生一个Fragment,以及如何在它的回调方法里实现基本的用户行为。
Building a Flexible UI
学习如何针对不同屏幕提供不同fragment配置的布局构建你的app。
Communicationg with Other Fragments
学习如何在Fragment与activity之前、以及Fragment与Fragment之前建立通信路径。
Creating a Fragment
你能把Fragment认为成activity的一个模块化部分,但它有自己的生命周期,接受它自己的输入事件,你也能在activity是running时添加或者移除它(简而言之,它像一个“子activity”,你能在不同的activity里复用它)。本文讲述了如何利用Support Library扩展和继承Fragment类。这样你的app仍然能兼容至android1.6的设备。
注:如果你确定你的app支持的最低API版本是11或者更高,你不需要使用Support Library,能直接使用SDK框架的Fragment和相关APIs。但本文主要讲如何基于Support Library使用这些APIs。这些APIs使用专门的package签名,有时稍稍和平台库里的版本名字上有些不同。
开始本课之前,你必须创建使用Support Library库的Android工程。之前如果你还没有使用Support Library,遵循Support Library Setup文档创建使用V4库包的工程。然而,你也能使用v7 appcompat包来代替使用v4包,这样,在你的Activity你同时也能使用action bar,v7最底兼容到android2.1(API level 7),也包括了Fragment APIs。
create a Fragment Class
为了产生一个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也应该实现其他的生命周期回调方法,这些方法能让fragment被添加或者移出activity时管理它的状态。例如,当activity的onPause()方法被调用时,在activity里的任何fragment的onPause()方法也将被调用。
更多的关于fragment生命周期和回调方法的信息可参见Fragments开发者向导。
Add a Fragment to an Activity using XML
当fragment是可重用的、模块化UI组件时,每个Fragment类的实例必须与一个FragmentActivity相关联。该类作为你的activity类的父类。你必须通过在你的activity布局文件里定义每个fragment来实现这种联系。
注:FragmentActivity是Support Library里为处理fragment向下兼容专门提供的一个类。如果你app支持的最低版本库是API 11,你可以直接使用Activity。
下面是一个加两个fragment到Activity的布局文件里的例子。该例子认为设备屏幕是“large”的(文件目录里专门指定了large标识符)。
<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>提示:更多的产生适应不同屏幕尺寸的布局,阅读 Supporting Different Screen Sizes.
然后在你的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 library,你的activity应该继承自ActionBarActivity,该类是FragmentActivity的子类(更多的信息,阅读 Adding the Action Bar)。
注:当通过在布局文件里定义fragment加fragment到activity的布局时,你不能在运行时移除该fragment。如果你计划在和用户交换时从fragment里移入和移出发fragment,你必须在activity开始时添加fragment到activity,下面我们将会讨论。
Building a Flexible UI
当设计你的app支持多种屏幕尺寸时,你能复用你的fragment在不同的布局配置里以优化用户体验。
例如,一个手持设备对于单面板用户界面,一次可能只适合展示一个fragment。相反地,在一个平板设备上由于有更宽的屏幕空间可以展示更多的信息给用户,你可能想要展示多个fragment。
为了能产生动态的用户体验,FragmentManager类提供了在运行时动态向activity里添加、移除和替换fragment的方法。
Add a Fragment to an Activity at Runtime
比起在activity的布局文件里定义一个fragment——像上节显示的使用<fragment>元素——你能在actiivty运行时添加fragment到activity里。
为了添加或移除fragment时执行事务操作,你必须使用FragmentManager产生FragmentTransaction,用于在添加、移除或者替换fragment等操作时进行事务管理。
如果你的activity允许fragment被添加、移除或者替换,你应该在activity的onCreate()方法里添加初始化的fragment。
处理fragmen(特别那些在运行时添加的fragment)的一个重要原则是必须有一个能持有fragment布局的容器View。
上节中讲述的布局文件可以写成如下的形式,该布局一次仅显示一个fragment。为了用一个fragment替换另一个fragment,你的activity布局包含了一个空FrageLayout,该FrameLayout作为fragment
的容器。
注意该文件文件名和上节介绍的文件文件名相同,但布局文件目录并没有large标识符。因此该布局用于比大屏幕更小的设备,因为小屏幕不能一次适应两个fragment。
<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里,如果使用的是Support Library APIs,调用getSupportFragmentManager()得到FragmentManager。然后调用beginTransaction()产生FragmentTransaction,再调用add()方法添加一个fragment。
你能使用相同的FragmentTransaction执行多个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 a new Fragment to be placed in the activity layout 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容器——而不是在activity的布局文件里使用<fragment>元素定义——activity能移除和用另一个fragment替换该fragment。
Replace One Fragment with Another
替换一个fragment与添加一个fragment的过程是相似的,但使用的是replace()方法而不是add()方法。
记住:当执行fagment事务时,例如替换或者移除,可能需要能让用户向后返回,能“撤销”这改变。为了能让用户回退对fragmen所做的操作,你必须在执行提交FragmentTransaction之前调用addBackStack()。
注:当你移除或者替换fragment,提添加操作到back stack时,被移除的fragment没有被销毁而是仅仅stop了。如果用户执行回退操作恢复该fragment,该fragment重新开始。如果你不加这些操作到back stack,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();
addToBackStack()方法接受一个可选字符串参数,该参数指定该事务操作的唯一的名字。名字一般情况下是不需要指定,除非你需要使用FragmentManager.BackStackEntry APIs执行高级的fragment操作。
Communicating with Other Fragments
为了重用fragment UI组件,你应该把fragment创建成完全自包含的模块化组件,让Fragment有自己的布局和行为。一旦你已定义了这些重用的fragment,你能将它们与一个activity联系起来。用应用逻辑将它们联系起来去实现总的复杂的UI。
你经常可能需要一个fragment和另一个fragment通信。例如,为了基于用户事件去改变fragment内容。所有Fragment与Fragment之间的交互视通过Activity完成。两个Fragment应该从不直接的交互。
Define an Interface
为了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"); } } ... }
现在通过调用onHeadlieSelectedListener接口的mCallback实例的onArticleSelected()方法(或者接口里的其他方法)fragment能传递消息给activity。
例如,当用户点击list里的条目时,如下在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); }
Implement the Interface
为了收到来自于frgment的事件回调,宿主activity必须实现fragment类里定义的接口。
例如,如下activity实现了上面例子的接口
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 } }
Deliver a Message to a Fragment
宿主activity通过调用findFragmentById()捕获Fragment的实例,然后传递消息给该Fragment——直接在Activity里调用fragment的公共方法。
例如,假设上面例子里的activity里还有另一个被用于展示上面接口回调返回的数据item的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(); } } }