2013年9月11日 星期三

單元三 模組化的UI畫面 --- Fragment

Fragment 可以用來顯示及執行 Activity 的某個需要的動作或畫面.
通常我們先將 Fragment 給模組化, 以方便在 Activity 裡添加.
換句話說, 我們可以用 Activity 來管控(數個) Fragment, 並藉此來決定 UI 畫面的顯示.

Framgent 的緣起

會有這樣的設計, 最早是因為須要處理不同尺寸螢幕的資料顯示, 例如: 平板.


平板的畫面比手機大得多, 因此我們希望可以將畫面切割成兩個部分, 讓左側 Fragment A 的點擊, 直接影響右側 Fragment B 的顯示.

而手機的部分, 因為畫面小, 只能將 Fragment A 與 Fragment B 分別使用不同的 Activity 來處理.

在沒有 Fragment 的情況下, 很可能必須為平板和手機分別寫不同的 Activity,
但有了 Fragment, 只需要做一個畫面就能在不同的 Activity 使用, 減少了重復寫 code 的可能性,
畫面模組化的好處在此時顯現了出來.

以至於在 Android 3.0 之後, Google 將許多元件都以 Fragment 來設計.
例如: Tab, ViewPager ... 等等.

Fragment 的 LifeCycle

Fragment 是依附 Activity 的模組畫面, 因此它的 LifeCycle 本身也與 Activity 息息相關.




看圖示可以知道, Fragment 跟 Activity 的 LifeCycle 很像.
當 Activity 添加 Fragment 時, 會先執行 onAttach(),
如果有預先儲存的 Bundle 資料, 應該在此載入.

在 onCreateView() 時, 要返回 Fragment 的 View.

接著, Framgent 的 onResume(), onStop(), onDestroy() 都是依附著 Activity 執行的. 也就是說,
當 Activity 處於畫面前端, 執行 onResume() 時, Framgent 就執行 onResume() ,
當 Activity 要結束了, 執行 onDestroy() 時, Fragment 就執行 onDestroy().

Fragment 的 LifeCycle 做得比 Activity 更細緻, 應是為了方便更細微的處理.

Fragment 的範例

這一次我們的範例取自 Android Developer 的 FragmentBasics,
用意就是做一個像我們最上面那個圖示一樣的 App,
在大尺寸的平板上, 左側是故事章節選擇的 ListView, 右側則是故事內容的 TextView,

平板畫面:


手機畫面:


Fragment 範例實作

1. 撰寫兩個同檔名 news_articles.xml 的 Layout

 一個放在 res/layout 的資料夾, 一個放在 res/large-large 的資料夾. 放在 res/layout 資料夾下的即是提供手機使用, 在 res/layout-large 下的則提供平板使用

res/layout/news_articles.xml
 <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" />  

res/layout-large/news_articles.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 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>  

這裡可以注意到, 在大尺寸的畫面上, 我們塞了兩個 fragment, 並且用 layout_weight 分別派給了這兩個不同等分的大小. (HeadlinesFragment 佔 1/3, ArticleFragment 佔 2/3)

2. 分別撰寫 HeadlinesFragment 以及 ArticleFragment  

HeadLinesFramgnet 跟 ArticleFragment 即是我們要用到的模組畫面, 可以注意到我們在大尺寸的畫面上已經悄悄用到這兩個 Fragment 了. 這種方法叫做靜態載入.

HeadlinesFragment.class
 public class HeadlinesFragment extends ListFragment {  
   OnHeadlineSelectedListener mCallback;  
   public interface OnHeadlineSelectedListener {  
     public void onArticleSelected(int position);  
   }  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     int layout = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ?  
         android.R.layout.simple_list_item_activated_1 : android.R.layout.simple_list_item_1;  
     setListAdapter(new ArrayAdapter<String>(getActivity(), layout, Ipsum.Headlines));  
   }  
   @Override  
   public void onStart() {  
     super.onStart();  
     if (getFragmentManager().findFragmentById(R.id.article_fragment) != null) {  
       getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);  
     }  
   }  
   @Override  
   public void onAttach(Activity activity) {  
     super.onAttach(activity);  
     try {  
       mCallback = (OnHeadlineSelectedListener) activity;  
     } catch (ClassCastException e) {  
       throw new ClassCastException(activity.toString()  
           + " must implement OnHeadlineSelectedListener");  
     }  
   }  
   @Override  
   public void onListItemClick(ListView l, View v, int position, long id) {  
     mCallback.onArticleSelected(position);      
     getListView().setItemChecked(position, true);  
   }  
 }  

在 HeadlinesFragment.class, 我們寫了一個 onHeadlineSelectedListener 的 interface, 裡頭有 onArticleSelected() , 代表 implement 這個 onHeadlineSelectedListener 的 Activity 要實作 onArticleSelected(). 藉此, 便可以在 onAttach() 的時候取得這個 Listener 物件, 做為 Fragment 跟 Activity 溝通使用.

ArticleFragment.class
 public class ArticleFragment extends Fragment {  
   final static String ARG_POSITION = "position";  
   int mCurrentPosition = -1;  
   @Override  
   public View onCreateView(LayoutInflater inflater, ViewGroup container,   
     Bundle savedInstanceState) {  
     if (savedInstanceState != null) {  
       mCurrentPosition = savedInstanceState.getInt(ARG_POSITION);  
     }  
     return inflater.inflate(R.layout.article_view, container, false);  
   }  
   @Override  
   public void onStart() {  
     super.onStart();  
     Bundle args = getArguments();  
     if (args != null) {  
       updateArticleView(args.getInt(ARG_POSITION));  
     } else if (mCurrentPosition != -1) {  
       updateArticleView(mCurrentPosition);  
     }  
   }  
   public void updateArticleView(int position) {  
     TextView article = (TextView) getActivity().findViewById(R.id.article);  
     article.setText(Ipsum.Articles[position]);  
     mCurrentPosition = position;  
   }  
   @Override  
   public void onSaveInstanceState(Bundle outState) {  
     super.onSaveInstanceState(outState);  
     outState.putInt(ARG_POSITION, mCurrentPosition);  
   }  
 }  

在ArticleFragment.class 的任務很簡單, 就是要把資料顯示在 article_view.xml 上.

3. 在 MainActivity 裡調用.

在MainActivity 裡, 我們必須大尺寸的雙畫面, 跟小尺寸的單畫面分別作不同的處置.
那要如何知道目前是大尺寸或小尺寸呢? 可以用有沒有 R.id.fragment_container 來判別.
(因為之前有說過, 裝置會自動調用 res/layout for 小尺寸, res/layout-large for 大尺寸)

如果有 R.id.fragment_container => 小尺寸, 沒有 R.id.fragment_container => 大尺寸.

MainActivity.class
 public class MainActivity extends FragmentActivity   
     implements HeadlinesFragment.OnHeadlineSelectedListener {  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.news_articles);  
     if (findViewById(R.id.fragment_container) != null) {  
       if (savedInstanceState != null) {  
         return;  
       }  
       HeadlinesFragment firstFragment = new HeadlinesFragment();  
       firstFragment.setArguments(getIntent().getExtras());  
       getSupportFragmentManager().beginTransaction()  
           .add(R.id.fragment_container, firstFragment).commit();  
     }  
   }  
   public void onArticleSelected(int position) {  
     ArticleFragment articleFrag = (ArticleFragment)  
         getSupportFragmentManager().findFragmentById(R.id.article_fragment);  
     if (articleFrag != null) {  
       articleFrag.updateArticleView(position);  
     } else {  
       ArticleFragment newFragment = new ArticleFragment();  
       Bundle args = new Bundle();  
       args.putInt(ArticleFragment.ARG_POSITION, position);  
       newFragment.setArguments(args);  
       FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();  
       transaction.replace(R.id.fragment_container, newFragment);  
       transaction.addToBackStack(null);  
       transaction.commit();  
     }  
   }  
 }  

同理, 如果 article_fragment 存在 => 大尺寸, 如果 article_fragment 不存在 => 小尺寸.
大尺寸當 HeadlinesFragment 被點擊時要 update ArticleFragment 的內容,
小尺寸當 HeadlinesFragment 被點擊時要產生新的 ArticleFragment, 並更新畫面.

原始碼連結





1 則留言:

  1. how we can show in fragment to display query data from SQLite database?
    Can you show by example how SQLite query data to be returned in String[] array that can be used by fragment?

    回覆刪除