我们接着上一篇翻译吧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)。