第6章 使用Fragments構建動態UI
為了在Android創建一個動態的多面的用戶界面,你需要封裝UI組件和activity的行為到一種可以相互交換的act的模塊中。我們能使用Fragment類創建這些模塊,這行為有點像一個嵌套的act,它可以定義自己的布局和管理自己的生命周期。Fragment的好處已經越發明顯,它是Android3.0新增的API。當一個fragment指定它的布局,它能以不同的組合配置到act中,為不同的屏幕大小修改你的布局配置,一個小屏幕可能只顯示一個fragment,而在大屏幕中可能顯示2個或2個以上的fragment。本章說明怎樣使用fragment創建動態的用戶體驗並為不同屏幕大小的設備優化用戶體驗,同時繼續支持例如Android1.6這樣的老版本。以下是內容預覽:
1. 使用Android支持的庫
通過綁定Android支持庫,學習怎樣使用近期新版本的APIs
2. 創建一個Fragment
學習怎樣創建fragment並實現一些基本行為
3.構建一個靈活的UI
學習怎樣為不同的屏幕提供不同的fragment配置布局
4. 與其他Fragment通信
學習如何為fragment設置通信路徑來與啟動fragment或Activity通信
6.1 使用Android支持庫
Android支持庫(Support Library)提供了一個支持API庫的Jar文件,這個庫允許你在早期的系統版本中使用近期系統版本中的API,一個好消息就是Support Library提供一個Fragment版本的APIs,所以我們可以在1.6-3.0之間的系統版本也能使用它了。本小節主要說明怎樣設置Support Library來支持fragments讓我們的低系統版本的應用夜支持動態UI。
6.1.1根據支持庫設置我們的工程
1. 使用SDK Manager下載Android Support包,如圖6-1所示:
圖6-1 下載Android Support的截圖
2. 在你的Android工程中創建一個libs目錄
3.定位你的jar文件,把你想要使用的庫復制到ligs/目錄中,例如
<sdk>/extras/android/support/v4/android-support-v4.jar
4. 更新你的manifest文件設置最小API Level為4,目標API Level為15(截止此刻最新的系統版本4.0.3)
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15" />
6.1.2導入我們支持庫的APIs
Support Library包含各種各樣的APIs,這些APIs可能你很想使用,為了考慮使用低系統版本的用戶,可能他們的系統中沒有此APIs。SL就是專為它而生。你能找到所有關於SL的API的文檔,請參考官方API文檔中的android.support.v4.*
當你使用了SL后,特別注意如果你自己的開發環境使用的是新系統,並使用了新的Fragment類,千萬注意兼容性,不是所有設備都跟你一樣使用的是新系統,你應該導入如代碼清單6-1所示:
import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; ...
代碼清單6-1
6.2 創建一個Fragment
你可以認為一個Fragment作為act模塊化的一個部分,它有其自己的生命周期,接收它自己的輸入事件,在該act運行時,你可以添加或刪除。本節說明如何在支持庫的情況下使用Fragment類,我們的應用程序仍然運行舊的Android 1.6系統版本上。當然如果你是直接聲明你的應用支持的最小API level為11(android 3.0)的話,就不需要使用支持庫了,不過這種做法目前不可取。
6.2.1創建一個Fragment類
首先我們需要繼承Fragment類,然后重寫關鍵的生命周期方法,就像Activity一樣。實際上我們需要重寫的方法僅僅是onCreateView()這一個方法,因為必須在這里創建一個布局,Fragment才能正確運行,我們可以參考下面的代碼清單6-2:
import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.ViewGroup; public class ArticleFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 為這個fragment填充布局 return inflater.inflate(R.layout.article_view, container, false); } }
代碼清單6-2
就像activity一樣,一個fragment也應該實現其他聲明周期的回調方法,以允許你管理狀態(例如從activity中添加或刪除),例如,當act調用onPause()時,在act中的任意fragments也將接收到onPause()回調。關於fragment更詳細的使用方法,會在API框架中講解。
6.2.2使用XML添加一個Fragment到Activity中
fragments是可復用的,模塊化的UI組件,每一個Fragment類實例都必須和父對象FragmentActivity關聯起來。你能通過在activity中的layout XML文件來定義每個fragment以完成這個關聯。注意如果你支持的最低系統版本為API Level 11那么你可以直接使用Activity,因為FragmentActivity是專門為早期系統版本,並使用支持庫中的Fragment而創建的。下面是一個布局文件的例子,當你設備的屏幕為“large”時,允許你在一個act中添加2個fragments。如代碼清單6-3所示:
res/layout-large/news_articles.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="fill_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>
代碼清單6-3
以下是在代碼中如何使用以上的layout xml文件,如代碼清單6-4所示:
import android.os.Bundle; import android.support.v4.app.FragmentActivity; public class MainActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.news_articles); } }
代碼清單6-4
注意:當你通過xml來定義fargment時,在運行時是不能移除的。下一小節我們會講解如何動態與用戶交互
6.3 創建一個靈活的Fragment
當你為了支持廣泛的屏幕大小而設計你的App時,你能在不同的布局配置中重用你的fragments以優化各種屏幕大小的用戶體驗。例如,某個時刻在手機上顯示一個fragment,相反的在平板中由於屏幕更大可能顯示更多的fragment。
下面讓我們看一下圖6-2:
圖6-2 平板和手機上使用Fragment的不同之處
FragmentManager類提供添加,刪除,替換fragment的方法,可以在Activity運行時的使用,以創建一個動態的用戶體驗。
6.3.1在Activity運行時添加一個Fragment
與其用xml定義fragments ,不如在運行時動態添加更靈活。如果你計划在act的生命周期中改變fragments ,那么使用動態機制是必要的。例如執行添加或移除一個fragment的事務處理,我們必須使用FragmentManager類創建一個FragmentTransction,它提供添加,移除,替換fragment的各種APIs。如果你的act允許fragments被移除和替換的話,那么你應該添加初始的(也可理解為原始父類)fragment到act的onCreate()中。一個很重要的規則就是當處理fragments,尤其是那些運行時添加的fragment時,必須有一個View容器包裹fragment。我們依舊使用上一節的布局xml文件但僅僅顯示一個fragment,因為我們支持的是layout而不是layout-large,由於我們使用代碼動態添加fragments,所以XML中是不能有fragment節點聲明的。並且XML中只有一個FrameLayout,如代碼清單6-5所示:
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" />
代碼清單6-5
在我們的activity中,使用支持庫APIs調用getSupportFragmentManager()來獲得一個FragmentManger。然后調用beginTransaction()創建一個FragmentTransaction以通過它的add()方法來添加一個fragment。你能在act中使用同一個FragmentTransaction執行多個fragment處理事務。當你確定改變時,需要調用commit()來提交我們的操作。具體操作,如代碼清單6-6所示:
import android.os.Bundle; import android.support.v4.app.FragmentActivity; public class MainActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.news_articles); //檢查FrameLayout是否為空 if (findViewById(R.id.fragment_container) != null) { // 如果我們從先前的狀態恢復,那么我們不需要做任何事,直接返回 if (savedInstanceState != null) { return; } //創建一個HeadlinesFragment對象,光盤資源中有此類的實現 HeadlinesFragment firstFragment = new HeadlinesFragment(); // 給fragment設置一個Intent's extras參數 firstFragment.setArguments(getIntent().getExtras()); //添加fragment到'fragment_container'(FrameLayout) getSupportFragmentManager().beginTransaction() .add(R.id.fragment_container, firstFragment).commit(); } } }
代碼清單6-6
因為fragment在運行時被添加到FrameLayout,而不是我們6.2中使用layout XML文件把fragment寫到<fragment>中,所以我們可以動態添加,移除,替換fragment。
6.3.2替換Fragment
替換fragment ,不是用add()而是使用replace()。請記住,當你使用FragmentTransaction執行更換或刪除一個fragment交易,它常常是適當允許用戶向后導航“撤消”的轉變。在你提交之前請調用FragmentTransaction.addToBackStack()方法。當刪除或替換一個fragment,並添加回棧的操作時,被刪除的fragment已停止(沒有被destroyed)。如果用戶后退導航,那么fragment恢復並重新啟動。如果你不添加addToBackStack(),則當fragment被移除和替換時,直接被destroyed。下面我們看下代碼清單6-7:
// 創建fragment並給他一個參數用於確定選擇文章的位置 ArticleFragment newFragment = new ArticleFragment(); Bundle args = new Bundle(); args.putInt(ArticleFragment.ARG_POSITION, position); newFragment.setArguments(args); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); //我們需要把文章標題Fragment替換為文章內容Fragment // 我們添加了addToBackStack(),表示用戶向后導航的時候fragment不會被destroyed transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); // 提交 transaction.commit();
代碼清單6-7
如果在addToBackStack()中傳入null,表示你可能更高級的操作,例如使用FragmentManager.BackStackEntry。
6.4 與其他Fragment通信
為了重用Fragment的UI組件,你應該建立一個定義了自己的布局和行為的完全獨立並模塊化的組件。一旦你定義了這些可重用的Fragment,你可以與Activity相關聯,並與應用邏輯連接,以實現整體復合UI。通常情況下,你會想幾個Fragment相互通信,例如基於用戶事件改變內容。所有Fragment的通信是通過相關的Activity。兩個Fragment,不應該直接通信。
6.4.1定義一個接口
為了允許一個Fragment 與Activity通信, 你可以在Fragment類里定義一個接口並在Activity中實現此接口。Fragment在它的onAttach()生命周期方法中捕獲接口的實現,然后調用接口方法為了與Activity通信。下面讓我們來看下代碼清單6-8:
public class HeadlinesFragment extends ListFragment { OnHeadlineSelectedListener mCallback; // 容器Activity必須實現這個接口,用來傳遞消息 public interface OnHeadlineSelectedListener { public void onArticleSelected(int position); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // 確保容器Activity已經實現回調接口,如果沒有則會拋出一個異常 try { mCallback = (OnHeadlineSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnHeadlineSelectedListener"); } } ... }
代碼清單6-8
現在fragment能通過調用Activity中的onArticleSelected() 方法來傳遞消息了。等會我們將展示實現的代碼。
例如下面的onListItemClick()方法,當用戶點擊fragment 中的list item時會調用此方法。我們這個fragment 使用這個回調接口傳遞事件到父類Activity。如代碼清單6-9所示:
@Override public void onListItemClick(ListView l, View v, int position, long id) { //通知父類activity選擇的item mCallback.onArticleSelected(position); }
代碼清單6-9
6.4.2 實現一個接口
為了從fragment接收事件回調, 父類activity必須實現這個在fragment類中定義的接口。例如下面的代碼實現了這個接口,如代碼清單6-10所示:
public static class MainActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener{ ... public void onArticleSelected(Uri articleUri) { //用戶從HeadlinesFragment選擇文章的標題 // do something } }
代碼清單6-10
6.4.3 傳遞消息到Fragment
我們通過Fragment實例的findFragmentById()方法直接獲取ArticleFragment。
關於接口的具體實現,下面讓我們看下代碼清單6-11:
public static class MainActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener{ ... public void onArticleSelected(int position) { //用戶從HeadlinesFragment選擇文章的標題 //從 res/layout-large/獲得文章 fragment ArticleFragment articleFrag = (ArticleFragment) getSupportFragmentManager().findFragmentById(R.id.article_fragment); if (articleFrag != null) { //如果articleFrag不為空,我們正在使用的是雙面板布局(平板設備) //調用 ArticleFragment的這個方法來更新內容 articleFrag.updateArticleView(position); } else { //否則,我們使用的是單面板(手機設備) //創建fragment並給他傳入一個參數用於確定選擇文章的位置 ArticleFragment newFragment = new ArticleFragment(); Bundle args = new Bundle(); args.putInt(ArticleFragment.ARG_POSITION, position); newFragment.setArguments(args); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); //我們需要把文章標題Fragment替換為文章內容Fragment //表示用戶可以后退導航 transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); //最后需要使用transaction提交 transaction.commit(); } } }
代碼清單6-11
完整的項目下載地址:http://url.cn/J49JRr
本文來自jy02432443,QQ78117253。轉載請保留出處,並保留追究法律責任的權利