我们接着上一篇翻译吧Android Api Component---翻译Fragment组件(一)
与activity通信
尽管一个Fragment独立于一个Activity作为一个对象被实现并且在多个activity中被使用,给定的fragment实例绑定到了包含它的那个activity中。
特别的是,这个fragment使用getActivity()可以访问activity实例并且容易的执行像在activity布局中查找一个视图的任务:
View listView = getActivity().findViewById(R.id.list);
同样的,你的activity通过从FragmentManager中请求一个到Fragment的映射可以调用fragment中的方法,使用findFragmentById()或者findFragmentByTag()。例如:
ExampleFragment fragment = (ExampleFragment)getFragmentManager().findFragmentById(R.id.example_fragment);
创建事件回调到activity
在一些例子中,你也许需要一个fragment跟那个activity共享事件。一种好的方式是在fragment中定义一个回调接口,并且要求主activity实现它。当activity通过这个接口接收一个回调的时候,当需要的时候,它可以跟其它的fragment共享信息。
例如,如果一个新闻应用程序在activity中有两个fragment-一个是展示文章的列表(fragment A),另一个是展示一个文章(fragment B),那么当一个列表项被选中的时候,这个fragment必须告诉这个activity来告诉fragment B显式这个文章。在这个例子中,fragment A中定义了接口OnArticaleSelectedListener:
public static class FragmentA extends Fragment {
......
//Container Activity must implement this interface
public Interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
}
然后fragment的主activity实现了OnArticleSelectedListener接口并且覆盖了onArticleSelected()来通知来自于fragment A的事件给fragment B。为了确保主activity实现了这个接口,fragment A的onAttach()(当framgment添加到activity的时候,系统会调用这个方法)回调方法通过映射Activity到onAttach()中来初始化一个OnArticleSelectedListener:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch(ClassCastException e) {
throw new ClassCastException(activity.toString()+" must implement OnArticleSelectedListener");
}
}
}
如果这个activity还没有实现这个接口,那么fragment会抛出一个ClassCastException异常。成功的关键是mListener成员持有一个映射到activity的OnArticleSelectedListener的实现,为的是fragment A可以通过调用被定义在OnArticleSelectedListener接口中的方法来和这个activity共享事件。例如,如果fragment A是一个ListFragment的扩展,用户每次点击一个列表项的时候,系统都会在fragment中调用onListItemClick(),这个方法可以调用onArticleSelected()来和这个activity共享事件:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
public void onListItemClick(ListView l, View v, int position, long id) {
//Append the clicked item's row ID with the content provider Uri
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI,id);
//Send the event and Uri to the host activity
mListener.onArticleSelected(noteUri);
}
....
}
传递给onItemClick()的id参数是被点击的项的行ID,这个ID是用来让activity(或者其它fragment)从应用程序的ContentProvider中抓取文章用的。
关于更多关于ContentProvider的信息,请看它的文档。
运用fragment的生命周期
管理fragment的生命周期跟管理activity的生命周期是不一样的。像activity,fragment可能存在于三种状态:
Resumed:
在运行的activity中fragment是可见的。
Paused:
另一个activity在它前面并且获得了焦点,但是在这个activity中的fragment仍然是存活着可见(前面的activity是部分的外观或者没有覆盖整个屏幕)。
Stopped:
frgment不是可见的。要么主activity已经被停止了,要么这个fragment已经从activity中被移除了,但是被添加到了回退栈。一个被停止的fragment仍然是存活的(所有的状态和成员信息通过系统被保持)。然而,它对用户不在是可见的并且activity被杀死之后这个fragment也被杀死。
在activity和fragment的生命周期之间最重要的不同点是它如何被存储在各自的回退栈中。当activity被停止的时候,这个activity默认会被放置在被系统管理的activity的回退栈中。然而,在一个移除fragment的事务期间,当你显式的请求通过调用addToBackStack()保存的实例的时候,这个fragment会被放在被它的主activity管理的回退栈中。
否则,管理activity的生命周期和管理fragment的生命周期是非常相似的。因此,管理activty的生命周期的相同习惯也应用与fragment。你也需要理解,activity的生命如何影响着fragment的生命。
警告:在你的fragment内如果你需要一个Context对象,你可以调用getActivity()。但是,当fragment被绑定到一个activity的时候,要小心调用getActivity()。当fragment还没有被绑定的时候,或者在它的生命周期结束时松绑了,getActivity()会返回null。
跟activity的生命周期整和
activity的生命周期直接影响着在它里面的fragment的生命周期。像每一个对activity的声明周期回调会导致一个相似的对每一个fragment的回调。例如,当activity接收onPause()的时候,在activyt中的每一个fragment接收onPause()。
fragment有几个额外的生命周期回调,例如,与activity运用唯一的相互作用来执行像构建和销毁fragment的UI的这样的动作。那些额外的方法是:
onAttach()
当fragment已经被关联到activity的时候被调用(在这里传递Activity)。
onCreateView()
创建一个跟fragment关联的视图层级的时候被创建。
onActivityCreated()
当activity的onCreate()方法被返回的时候调用。
onDestroyView()
当跟fragment关联的视图层级被移除的时候被创建。
onDetach()
当fragment从activity取消关联的时候被调用。
fragment的生命周期的流程被它的主activity所影响,参考上面的图片。在这个图片中,你可以看到,每一个activity的成功的状态决定着回调哪一个它接收的fragment的回调方法。例如,当activity的onCreate()回调它被接收的时候,在activity中的fragment接收不会越过onActivityCreated()回调。
一旦activity到达了被恢复的状态,你可以给activity轻易的添加和移除fragment。因此,只有当activity的状态为恢复时,fragment的声明周期就可以独立的改变了。
例子
为了把文档中讨论的事情聚集到一起,这里给了一个使用两个fragment的activity例子来创建两个面板布局。activity下面包含一个fragment来展示Shakespeare的标题列表并且当从这个列表中选中了一个剧本的时候,另一个fragment展示这个剧本的详述。它也展示了基于屏幕配置如何提供fragment的不同配置。
注意:这个activity的完整源码在FragmentLayout.java中。
主activity在onCreate()期间用普通的方式应用了一个布局:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_layout);
}
这个布局文件fragment_layout.xml:
<LinearLayout xmlns:android="
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
android:id="@+id/titles
android:layout_weight="1"
android:layout_width="0px" android:layout_height="match_parent"/>
<FrameLayout android:id="@+id/details" android:layout_weight="1"
androd:layout_width="0px" android:layout_height="match_parent"
android:background="?android.attr/detailsElementBackground"/>
</LinearLayout>
使用这个布局,当activity载入布局的时候,系统初始化TitlesFragment(剧本标题列表),然而这个FrameLayout(在这个里面fragment将展示剧本的详述)消费了屏幕右边的空间,但是起初是空的。随着你下面看到的,直到用户选择了列表中的项,它才不会是空的,并且一个fragment会被放在这个FrameLayout中。
然而,不是所有的屏幕配置都足够宽能够既显示剧本列表又显示剧本详述。因此,通过保存在res/layout-land/fragment_layout.xml中,上面的布局仅仅用于宽边屏幕配置。
因此,当屏幕在portrait方向时,系统应用下面的配置,它被保存在res/layout/fragment_layout.xml中:
<FrameLayout xmlns:android="
android:layout_width="match_parent" android:layout_height="match_parent">
<fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
android?id="@+id/titles"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
这个布局只包含了TitlesFragment。这就意味着,当设备在portrait方向时,只有剧本标题列表是可见的。因此,在这个配置中当用户点击列表项的时候,应用程序取代载入第二个fragment,将开启一个新的activity展示这个详述。
接下来,你可以看到这个在fragment类中如何被完成。首先是TitlesFragment,它展示了Shakespeare剧本列表。这个fragment继承了ListFragment并且依赖于运用大多数列表视图工作。
当你检查这个代码的时候,当用户点击列表项的时候,注意有两个可能的行为:依赖于两个布局中的哪一个是活动的,它可以要么是创建并且展示一个新fragment来在相同的activity展示剧本的详述(添加fragment到FrameLayout),要么开启一个新的activity(这里是展示fragment的地方)。
public static class TitlesFragment extends ListFragment {
boolean mDualPane;
int mCurCheckPosition = 0;
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//Populate list with our static array of titles.
setListAdapter(new ArrayAdapter<String>(getActivity(),android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));
//Check to see if we have a frame in which to embed the details
//fragment directly in the containing UI.
View detailsFrame = getActivity().findViewById(R.id.details);
mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE.
if(savedInstanceState != null) {
//Restore last state for checked position
mCurCheckPosition = savedInstanceState.getInt("curChoice",0);
}
if(mDualPane) {
//In dual-pane mode, the list view highlights the selected item.
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
//Make sure our UI is in the correct state.
showDetails(mCurCheckPosition);
}
}
public void onSaveInsanceState(Bundle outState) {
super.onSaveinstanceState(outState);
outState.putInt(curChoice",mCurCheckPosition);
}
public void onListItemClick(ListView l, View v, int position, long id) {
showDetails(position);
}
/**
* Helper function to show the details of a selected item, either by displaying a fragment in-place in the current UI, or
* starting a whole new activity in which it is displayed.
*/
void showDetails(int index) {
mCurCheckPosition = index;
if(mDualPane) {
//We can display everything in-place with fragments, so update
//the list to highlight the selected item and show the data.
getListView().setItemChecked(index, true);
//Check what fragment is currently shown, replace if needed.
DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.details);
if(details == null || details.getShownIndex() != index) {
//Make new fragment to show this selection.
details = DetailsFragment.newInstance(index);
//Execute a transaction , replacing any existing fragment
//with this one inside the frame.
FragmentTransaction ft = getFragmentManager().beginTransaction();
if(index == 0) {
ft.replace(R.id.details,details);
} else {
ft.replace(R.id.a_item,details);
}
ft.setTransaction(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
} else {
//Otherwise we need to lanuch a new activity to display
//the dialog fragment with selected text.
Intent intent = new Intent();
intent.setClass(getActivity(), DetailsActivity.class);
intent.putExtra("index",index);
startActivity(intent);
}
}
}
第二个fragment DetailsFragment展示了从TitlesFragment的列表中被选中额项的这个剧本的详述:
public static class DetailsFragment extends Fragment {
/**
* Create a new instance of DetailsFragment, initialized to
* show the text at 'index'.
*/
public static DetailsFragment newInstance(int index) {
DetailsFragment f = new DetailsFragment();
//Supply index input as an argument.
Bundle args = new Bundle();
args.putInt("index",index);
f.setArguments(args);
return f;
}
public int getShownIndex() {
return getArguments().getInt("index",0);
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(container == null) {
//We have different layouts, and in one of them this fragment's conatining frame doesn't exist. The fragment may still be
//created from its saved state, but there is no reason to cry to create its view hierarchy because it won't be displayed.
//Note this is not needed -- we could just run the code below, where we would create and return the view hierarchy.
//it would just never be used.
return null;
}
ScrollView scroller = new ScrollView(getActivity());
TextView text = new TextView(getActivity());
int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getActivity().getResources().getDisplayMetrics());
text.setPadding(padding, padding, padding, padding);
scroller.addView(text);
text.setText(Shakespeare.DIALOGUE[getShownIndex()]);
return scroller;
}
}
从TitlesFragment类中回调,如果用户点击了列表项并且当前的布局不包含在R.id.details视图中(它是DetailsFragment所属的),那么应用程序开启这个DetailsActivity的activity来展示这个项的内容。
这是DetailsActivity,当屏幕是portrait方向的时候,它简单的嵌入了DetailsFragment来展示被选中的剧本的详述:
public static class DetailsActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
//If the screen is now in landscape mode, we can show the dialog in-line with the list so we don't need this activity.
finish();
return;
}
if(savedInstanceState == null) {
//During initial setup, plug in the details fragment.
DetailsFragment details = new DetailsFragment();
details.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(android.R.id.context, details).commit();
}
}
}
注意,如果配置是landscape,那么这个activity销毁它自己,为的是主activity能接管并且展示与TitlesFragment紧挨的DetailsFragment。这可能会发生在当portrait方向时用户开始了DetailsActivity。但是然后又翻转到landscape(它重新启动当前activity)。