Android 进阶: Fragments

Fragments

一个 Fragment 通常是用来表示 Activity 界面的一部分. 在一个 Activity 中可以嵌入多个 fragment 来实现不同的界面效果. 你可以将 fragment 类似于一个 "sub activity". 其拥有自己的生命周期和消息机制.

一个 fragment 必须嵌入到一个主 activity 中, 且其生命周期会受到主 activity 的影响. 如主 activity 被 paused, 则其所有 fragment 也将会 paused, 主 activity 被 destroy 则其所有 fragment 也会 destroy. 
当一个 activity 在运行的时候, 你可以直接操作每一个 fragment. 如添加或删除等操作. 当你执行一个 fragment transaction(事务)  时候, 你还可以将该事务操作添加到 back stack(栈) 中, 若需要返回, 可通过点击手机的 Back button 返回该事务之前的状态.

若需要添加一个 fragment 到你的 activity 的图层中时,  fragment 定义的视图会添加到 activity 的 ViewGroup 中. 当然, 你也可以通过 <fragment> 标签来声明一个 fragment 在 activity 的图层文件中, 或者在应用中也可以通过代码添加到 ViewGroup. 不过有时候, 一个 fragment 有时候不一定是作为 activity 界面交互中的一部分, 而是作为一个类似于后台进程一样无UI 来进行工作.

Design Philosophy


引入 fragment 其主要目的是兼容不同屏幕尺寸的界面设计. 下面通过一个例子来进行说明.

如上图所示. 在平板上时, 由于其屏幕足够大, 因此我们可以在 Activity A 中放置两个 Fragment (分别为文章列表和文章详细内容), 而在手机上时, 由于其屏幕的限制, 因此在Activity A 中只放置了 Fragment A(文章列表), 通过用户的选择来跳转到另外一个 Activity B(放置 Fragment B) 来显示具体文章内容.因此在开发应用的时候, 通过复用和组合来很好的兼容不同尺寸屏幕的设备.

Creating a Fragment


创建一个 fragment , 需要创建一个继承于 Fragment 的子类.  对于 Fragment 而言, 其内部代码非常类似于 Actvity. 其也有 onCreate(), onStart(), onPause() 和 onDestroy() 方法. 在实际开发中, 若你需要创建一个 fragment , 一般需要实现如下几个方法:
onCreateView()
当界面显示在前台时, 系统会调用该方法. 此方法是用于返回一个 View 对象. 该View 为fragment 界面图层的根节点. 若此 fragment 无界面则返回null.
onPause()
界面不在前台显示时候会被系统调用. 在此方法中可以进行一些持久化的操作, 如数据的保存. 由于当系统执行 onPause() 后, 可能将不在返回该 fragment, 因此我们需要保存好数据, 方便下次再次打开时候读取.

同时, 在 Fragment 还提供了几个子类便于开发者使用:
DialogFragment, ListFragment, PreferenceFragment, 实现大同小异不一一具体介绍.

Adding a user interface

一个 fragment 在 activity 中, 通常是作为其界面的一部分. 因此你需要去实现 onCreateView() 来返回一个 View , 添加到 activity 中去. (若继承于 ListFragment 则不需要实现 onCreateView, 其内部已经为我们实现了 onCreateView 返回了一个View 对象.) 示例代码如下.
public static class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false);
    }
}
参数  container  为主 activity 的 ViewGroup. 即此 fragment 返回的view 将要被插入的layout.
  inflate()  方法中的参数解释:
  • resource ID 资源ID, 该 fragment 视图图层.
  • ViewGroup 父图层的 ViewGroup. 
  • 表示是否依附于viewgroup,此处传递false,由于系统已经将该layout插入到viegroup.

    Adding a fragment to an activity

    将 fragment 添加到 activity 通常有两种方法:
    • 直接在 activity layout 文件中声明.
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <fragment android:name="com.example.news.ArticleListFragment"
                android:id="@+id/list"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent" />
        <fragment android:name="com.example.news.ArticleReaderFragment"
                android:id="@+id/viewer"
                android:layout_weight="2"
                android:layout_width="0dp"
                android:layout_height="match_parent" />
    </LinearLayout>
     

    • Or, programmatically add the fragment to an existing ViewGroup.
    在代码中添加. 你可以在activity 运行的任何时候添加 fragment.当然, 你需要指定其在 ViewGroup 的具体位置.
    在进行fragment 的添加,替换,移除时, 需要使用到  FragmentTransaction, 你可以通过如下代码来实例化一个  FragmentTransaction  :
    FragmentManager fragmentManager = getFragmentManager()
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    接下来你可以使用 add() 来进行添加一个 fragment:
    ExampleFragment fragment = new ExampleFragment();
    fragmentTransaction.add(R.id.fragment_container, fragment);
    fragmentTransaction.commit();
    在 add() 方法中,第一个参数表示其在ViewGroup 中的位置, 其资源ID. 第二个参数则为添加的 fragment. 一旦你改变了  FragmentTransaction, 你需要调用  commit()来提交.

    Adding a fragment without a UI
    当然, 你也可以添加一个无界面的 fragment 到 activity中, 由于该 fragment 无UI 界面, 因此不在需要去实现 onCreateView(). 添加到 activity 通过调用 add(Fragment, String)(字符串 "tag", 为View ID) 方法来实现. 由于fragment 没有界面, 因此此tag 为该 fragment的唯一标识.  可以通过 findFragmentByTag() . 来得到该fragment.

    Managing Fragments


    管理在 activity 中的 fragments 你需要用到  FragmentManager . 可通过调用 getFragmentManager() 来得到.
    在  FragmentManager 中你可以做如下一些操作:

    Performing Fragment Transactions


    在 activity 中通过使用 FragmentTransaction 可 对 fragment 进行添加, 替换等操作时, 其一系列操作我们将其称之为一个事务(transaction),  其每个事务我们将其保存到 back stack 栈中, 允许用户点击返回来返回到事务之前的状态(其类似于 activity 的back ).
    在每一个事务中, 你可以同时进行add(), remove(), 和 replace(), 在最后需要 commit()来提交使得 操作生效.
    在调用  commit()之前, 可以将其添加到 back stack 通过调用  addToBackStack(). 当添加到back stack 中, 用户可以通过点击 Back 按键来返回之前的状态.
    // Create new fragment and transaction
    Fragment newFragment = new ExampleFragment();
    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    
    // Replace whatever is in the fragment_container view with this fragment,
    // and add the transaction to the back stack
    transaction.replace(R.id.fragment_container, newFragment);
    transaction.addToBackStack(null);
    
    // Commit the transaction
    transaction.commit();
    在上述示例代码中,  newFragment  替换掉当前在 ID 为  R.id.fragment_container  的 fragment, 并调用了 addToBackStack() 讲当前事务添加到 back stack. 所以, 用户可以通过点击 Back 按钮来返回事务之前的状态.
    当调用    commit()  时, 其并不会立即执行当前提交的事务, 而是在主UI 线程的计划中尽可能快的去执行该方法. 如果需要立即执行该方法, 可调用  executePendingTransactions() 来让UI 线程立即执行, 不过通常情况下是没有必要这样.

    Communicating with the Activity


    .在 activity 中为了得到一个view , 可通过如下方法 查找所需的 view 对象.
    View listView = getActivity().findViewById(R.id.list);
    而获取到fragment  可以通过利用  FragmentManager  中的   findFragmentById()  或者  findFragmentByTag() .来得到 fragment
    ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

    Creating event callbacks to the activity

    在有些情况下, 在主 activity 中需要获得 fragment 的一些消息事件, 一个好的解决办法是通过接口来实现, 在 fragment 中定义需要的接口方法, 在主 activity 中实现该接口, activity 可以通过该接口来获取到 fragment 的一些消息.
    比如, 在一个应用程序中, 一个activity 包含了两个 fragment, 一个是显示文章的标题的列表(fragment A), 而另一个是显示文章的具体信息(fragment B),  当用户在 fragment A 中点击选择某个文章的item , 此处 fragment A 需要通知 activity 用户点击的是哪一篇文章, 然后activity 再去通知 fragment B 去显示其文章的具体内容. 
    其 fragment A 接口定义如下所示:
    public static class FragmentA extends ListFragment {
        ...
        // Container Activity must implement this interface
        public interface OnArticleSelectedListener {
            public void onArticleSelected(Uri articleUri);
        }
        ...
    }
    在主 activity 中 需要实现 fragment 中的    OnArticleSelectedListener  接口来重写 onArticleSelected()  , 来通知 fragment B 去显示具体文章内容.为了确保主 activity 实现了其接口, 在 fragment 的  onAttach()  回调方法中将传递进来的   Activity  强转为    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 未实现其接口, 则抛出异常.否则则将activity 实现的接口对象传递给 mListener   . 则fragment A 可通过 mListener  来调用其在主 activity 中实现的方法. 如之前的例子, fragment A 继承于  ListFragment, 当用户点击列表的 item 时候, 系统将会调用    onListItemClick(),因此我们可以在其中来发送出其被点击的消息事件.
    public static class FragmentA extends ListFragment {
        OnArticleSelectedListener mListener;
        ...
        @Override
        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);
        }
        ...
    }

    Adding items to the Action Bar

    在 fragments 中可以添加菜单到 主activity 中, 通过实现  onCreateOptionsMenu(), 在方法中添加菜单item. 当然若需要接受到菜单的点击消息, 因此还需要在  onCreate()中设置 setHasOptionsMenu()  . 在 onOptionsItemSelected()  中接受菜单消息. 需要注意, 在菜单点击的时候, 主 activity 会先收到菜单消息, 若主 activity 未处理才会往下传递到 fragment.
         
         
    @Override
        public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
            super.onCreateOptionsMenu(menu, inflater);
            inflater.inflate(R.menu.fragment01_menu, menu);
        }
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            Log.d("fragment 01", "onOptionsItemSelected");
            return super.onOptionsItemSelected(item);
        }

    Handling the Fragment Lifecycle


    fragment 的生命周期非常类似于 activity 的生命周期. 其也可以使用    Bundle 来保存数据.其可以在  onSaveInstanceState()  回调方法中存储同时也可以在  onCreate() onCreateView() , 或者  onActivityCreated().
    而和 activity 最大的区别在于 back stack. 其 activity 是由系统自行管理, 通过Back 按钮来返回, 然而在 fragment, 需要用户调用  addToBackStack()   才会添加到栈里.


    Caution:  在 fragment 中若需要用到 Conetxt 对象, 可以通过调用   getActivity(), 不过需要注意的是只有当 fragment 于 activity 关联之后才能调用, 否则调用    getActivity() .返回的将是 null.

    Coordinating with the activity lifecycle

    由于 fragment 的生命周期受到主 activity, 因此当主 activity 回调    onPause()时, 每个 fragment 也会回调   onPause()
    同时, fragment 还具有几个特有的回调方法, 其在于 activity 交互时会调用.
    onAttach()
    当 fragment 被关联到 activity 的时调用, 其参数    Activity  为主activity.
    onCreateView()
    创建 fragment 的UI 界面 .
    onActivityCreated()
    当主 activity 的   onCreate() 方法返回时调用.
    onDestroyView()
    fragment 被移除时调用.
    onDetach()
    fragment 不在关联主 activity的时调用.
    其具体 fragment 的生命周期可见上图.

    Example


    此处将通过实例代码来进行演示在 activity 中使用 fragment, 在本例中, 将会用到两个fragment , 一个是通过列表来显示文章的标题, 另一个根据列表的选择来显示具体文章的内容. 
    在主 activity 的  onCreate() :中设置图层
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        setContentView(R.layout.fragment_layout);
    }
    其    fragment_layout.xml :
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/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"
                android:layout_width="0px" android:layout_height="match_parent"
                android:background="?android:attr/detailsElementBackground" />
    
    </LinearLayout>
    在此 layout 中, TitleFragment 直接通过标签<fragment /> 添加到了 activity 中.然后再其下面添加了一个   FrameLayout 来用于显示文章的具体内容(若其屏幕有足够的空间来显示), 此在此处保留一个空的图层, 当用户点击文章列表的item 的时候再进行动态的添加.
    当然, 需要注意的是, 上述代码为横屏所实现的界面, 其保存在   res/layout-land/fragment_layout.xml .
    而当屏幕为竖屏的时候, 则使用如下图层 res/layout/fragment_layout.xml :
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/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, 因此当用户竖屏的时候, 只有文章的标题列表可见, 当用户点击item, 则开启一个新的 activity 来显示文章内容,而不是替换 fragment.
    接下来, 在代码中,你需要判断当用户点击item 的时候是开启一个新的activity 还是 替换 fragment 到 FrameLayout .
    public static class TitlesFragment extends ListFragment {
        boolean mDualPane;
        int mCurCheckPosition = 0;
    
        @Override
        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);
            }
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putInt("curChoice", mCurCheckPosition);
        }
    
        @Override
        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.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                    ft.commit();
                }
    
            } else {
                // Otherwise we need to launch 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 用于显示文章的具体内容:
    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);
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            if (container == null) {
                // We have different layouts, and in one of them this
                // fragment's containing frame doesn't exist.  The fragment
                // may still be created from its saved state, but there is
                // no reason to try 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 中点击了 item, 而其layout 中不包含  R.id.details   , 则需要开启  DetailsActivity   来显示.在  DetailsActivity  中嵌入了 DetailsFragment  用于显示文章具体内容.

    public static class DetailsActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(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.content, details).commit();
            }
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值