首页 > 代码库 > Android Api Component---翻译Fragment组件(二)

Android Api Component---翻译Fragment组件(二)

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

Android Api Component---翻译Fragment组件(二)