前言
開門見山開篇名義,本篇博客將講解一下Android中Fragment的內容,必要的地方會提供相應的演示代碼,並且會在最后給出源碼下載。
本文主要有以下內容:
Fragment,碎片,是Android3.0之后新增加的特性。主要是為了支持更多的UI設計在大屏幕設備上,如平板。因為現在設備的屏幕越來越大,使用Fragment可以更靈活的管理視圖層次的變化。像Activity一樣,可以創建Fragment來包含View,進行布局,但是Fragment必須嵌入在Activity中,不能單獨存在,而且一個Activity可以嵌入多個Fragment,同時一個Fragment可以被多個Activity重用。
上圖是從官方文檔中掛載的,可以很清晰的說明Activity和Fragment的關系和優點。在平板中,因為屏幕大,顯示的內容全,如果還像手機哪樣通過Activity跳轉的方式去加載UI,太浪費屏幕資源了,而如上左圖,可以結合Fragment布局,使一個Activity左右分別包含一個Fragment,這樣可以通過對左邊Fragment的操作來影響右邊Fragment的顯示,例如:新聞閱讀,系統設置等。如果一個應用的是采用Activity+Fragment結合布局,那么可以很方便的在平板與手機之間相互移植,大部分代碼是可以重用的,並且Fragment無需在AndroidManifest.xml清單文件中注冊。
上面已經介紹了Fragment,再來講講如何使用Fragment。使用Fragment必須繼承這個類或其子類,並且重寫其的onCreateView()方法,這個方法是用於指定Fragment在初次加載的時候,顯示的View。下面是這個方法的簽名:
public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState)
- inflater:當前布局的填充者,可以用inflater.inflate()方法去填充一個布局文件。
- container:為包裹當前Fragment的容器,一般是一個布局控件。
- savedInstanceState:當前實例化的狀態,一般用不上。
onCreateView()返回一個View,用於Fragment的顯示,這里使用inflater.inflate()方法動態膨脹一個View對象做返回值,inflate()的簽名如下:
public View inflate(int resource,ViewGroup root,boolean attachToRoot)
- resource:動態膨脹的布局資源ID。
- root:膨脹出的View的上層布局對象,一般傳遞onCreateView的container參數即可。
- attachToRoot:指定展開的布局是否依附到root這個ViewGroup中,一般傳遞false即可。
inflate()的resource是一個普通的布局資源,和之前的布局沒有什么特殊性。而在布局中使用Fragment使用<fragment/>標簽來在XML文件中布局,大多數屬性與UI控件一樣,但是其中有兩個屬性需要特別注意:
- android:name:這個Fragment的實現類。
- android:layout_weight:當前Fragment在Activity的權重,數值越大,在Activity中占的權重越大。
下面通過一個示例,來演示一下Fragment在Activity中的應用。示例中在一個Activity中,添加了兩個Fragment。
activity_fragment.xml:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="horizontal" > 6 7 <fragment 8 android:id="@+id/fragment1" 9 android:name="com.example.fragmentSimple.Fragment1" 10 android:layout_width="0px" 11 android:layout_height="match_parent" 12 android:layout_weight="2" /> 13 14 <fragment 15 android:id="@+id/fragment2" 16 android:name="com.example.fragmentSimple.Fragment2" 17 android:layout_width="0px" 18 android:layout_height="match_parent" 19 android:layout_weight="1" /> 20 21 </LinearLayout>
Fragment1:
1 package com.example.fragmentSimple; 2 3 import com.example.fragmentdemo.R; 4 import android.app.Fragment; 5 import android.os.Bundle; 6 import android.view.LayoutInflater; 7 import android.view.View; 8 import android.view.ViewGroup; 9 10 public class Fragment1 extends Fragment { 11 12 @Override 13 public View onCreateView(LayoutInflater inflater, ViewGroup container, 14 Bundle savedInstanceState) { 15 // 填充一個布局View到ViewGrope中 16 return inflater.inflate(R.layout.fragment1, container, false); 17 } 18 }
Fragment2:
1 package com.example.fragmentSimple; 2 3 import com.example.fragmentdemo.R; 4 5 import android.os.Bundle; 6 import android.app.Fragment; 7 import android.view.LayoutInflater; 8 import android.view.View; 9 import android.view.ViewGroup; 10 11 public class Fragment2 extends Fragment { 12 13 @Override 14 public View onCreateView(LayoutInflater inflater, ViewGroup container, 15 Bundle savedInstanceState) { 16 return inflater.inflate(R.layout.fragment2, container, false); 17 } 18 }
啟動后顯示效果:
Fragment有自己獨立的生命周期,但是它有是依托於Activity的,所以Fragment的生命周期直接受Activity的影響。下圖很直觀的描述了Activity的生命周期:
從上圖中可以看出Fragment的生命周期大體上和Activity一樣,有兩個生命周期方法需要注意,onAttach()附加、onDetach()剝離,從這兩個方法的位置可以看出,Fragment在創建的時候,是先附加到Activity中,然后才開始從onCreateView()中加載View的,記住這一點很重要。並且在生命周期結束的時候,也是先銷毀onDestroy()然后才回調onDetach()從Activity中剝離這個Fragment。
在代碼中管理一個Fragment非常簡單,需要用到一個FragmentTransaction對象,這個對象通過getFragmentManager().beginTransaction()獲取,它將開啟一個事務,用於操作一個ViewGroup中的Fragment。
FragmentTransaction的常用方法:
- add():增加一個Fragment,具有多個重載。
- replace():替換一個Fragment,具有多個重載。
- remove():移除掉一個指定的Fragment。
- addToBackStack():在事務中添加一個棧,用於回退。
- commit():提交事務。
其中add、replace、remove都是很常見的方法,無需過多介紹。但是addToBackStack()方法就需要額外講解一下,正常情況下,應用中的Activity是有一個任務棧去管理它的。默認情況下,當我們在不同的Activity中跳轉的時候,點擊回退總是能回到上一個Activity中。而Fragment是嵌套在Activity,所以默認無法向Activity的任務棧中添加,當點擊回退的時候只會回到上一個Activity,不會理會Fragment的操作(add、replace、remove),而使用addToBackStack()可以將當前的事務添加到另一個棧中,這個棧由Fragment的Activity管理,這個棧中的每一條都是一個Fragment的一次事務,有了這個棧去管理Fragment,就可以通過回退按鍵,反向回滾Fragment的事務。這一點很重要,因為Fragment無需在清單文件中配置,所以現在有些應用會使用Fragment來布局跳轉。
下面通過一個示例,演示一下動態操作Fragment的例子。在示例中,會實現一個分欄的效果,在左邊點擊項會動態修改右邊的內容。
布局文件,activity_fragmenttab.xml:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="horizontal" > 6 7 <LinearLayout 8 android:layout_width="wrap_content" 9 android:layout_height="match_parent" 10 android:orientation="vertical" > 11 12 <TextView 13 android:id="@+id/tabfgt1" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:text="fragment1" /> 17 18 <TextView 19 android:id="@+id/tabfgt2" 20 android:layout_width="wrap_content" 21 android:layout_height="wrap_content" 22 android:text="fragment2" /> 23 24 <TextView 25 android:id="@+id/tabfgt3" 26 android:layout_width="wrap_content" 27 android:layout_height="wrap_content" 28 android:text="fragment3" /> 29 </LinearLayout> 30 31 <LinearLayout 32 android:id="@+id/content" 33 android:layout_width="match_parent" 34 android:layout_height="match_parent" 35 android:orientation="vertical" > 36 </LinearLayout> 37 38 </LinearLayout>
FragmentTabActivity.java:
1 package com.example.fragmentTab; 2 3 import com.example.fragmentSimple.Fragment1; 4 import com.example.fragmentSimple.Fragment2; 5 import com.example.fragmentTurn.Fragment3; 6 import com.example.fragmentdemo.R; 7 8 import android.app.Activity; 9 import android.app.FragmentManager; 10 import android.app.FragmentTransaction; 11 import android.graphics.Color; 12 import android.os.Bundle; 13 import android.view.View; 14 import android.widget.TextView; 15 16 public class FragmentTabActivity extends Activity { 17 private TextView tabfgt1, tabfgt2, tabfgt3; 18 19 @Override 20 protected void onCreate(Bundle savedInstanceState) { 21 // TODO Auto-generated method stub 22 super.onCreate(savedInstanceState); 23 setContentView(R.layout.activity_fragmenttab); 24 25 tabfgt1 = (TextView) findViewById(R.id.tabfgt1); 26 tabfgt2 = (TextView) findViewById(R.id.tabfgt2); 27 tabfgt3 = (TextView) findViewById(R.id.tabfgt3); 28 29 tabfgt1.setOnClickListener(click); 30 tabfgt2.setOnClickListener(click); 31 tabfgt3.setOnClickListener(click); 32 33 } 34 35 private View.OnClickListener click = new View.OnClickListener() { 36 37 @Override 38 public void onClick(View v) { 39 tabfgt1.setBackgroundColor(Color.GRAY); 40 tabfgt2.setBackgroundColor(Color.GRAY); 41 tabfgt3.setBackgroundColor(Color.GRAY); 42 // 獲取FragmentManager對象 43 FragmentManager fm = getFragmentManager(); 44 // 開啟事務 45 FragmentTransaction ft = fm.beginTransaction(); 46 switch (v.getId()) { 47 case R.id.tabfgt1: 48 tabfgt1.setBackgroundColor(Color.GREEN); 49 // 替換R.id.content中的Fragment 50 ft.replace(R.id.content, new Fragment1()); 51 break; 52 case R.id.tabfgt2: 53 tabfgt2.setBackgroundColor(Color.YELLOW); 54 ft.replace(R.id.content, new Fragment2()); 55 break; 56 case R.id.tabfgt3: 57 tabfgt3.setBackgroundColor(Color.RED); 58 ft.replace(R.id.content, new Fragment3()); 59 break; 60 default: 61 break; 62 } 63 // 提交事務 64 ft.commit(); 65 } 66 }; 67 }
效果展示:
既然Fragment是嵌套在Activity中的,而在Fragment加載的布局文件中,又可以額外的布局,那么出現了新的問題,如何操作兩個不同Fragment中的控件呢?回憶一下在Activity中,操作一個控件需要通過findViewById(int)方法通過控件的ID去找到控件,而使用Fragment其實到最后Fragment.onCreateActivity()的時候是把膨脹的View加載到Activity中了,所以可以直接在Activity中通過findViewById()方法找到控件,進而操作它,這一點和直接操作Activity的方式一致。但是如果需要在一個Fragment中操作另外一個Fragment的控件,就需要用到Fragment.getActivity()來獲取到當前Fragment承載的Activity對象,拿到這個Activity對象,獲取到其中的控件就不成問題了。
下面通過一個示例來演示Fragment中的交互,在Activity中,有三個Fragment,從其中的一個Fragment的Button點擊的時候,修改其他Fragment的值。
布局,activity_fragmentturn.xml
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="horizontal" > 6 7 <fragment 8 android:id="@+id/fragment1" 9 android:name="com.example.fragmentSimple.Fragment1" 10 android:layout_width="0px" 11 android:layout_height="match_parent" 12 android:layout_weight="1" /> 13 <!-- 加載了兩個Fragment1 --> 14 <fragment 15 android:id="@+id/fragment2" 16 android:name="com.example.fragmentSimple.Fragment1" 17 android:layout_width="0px" 18 android:layout_height="match_parent" 19 android:layout_weight="1" /> 20 <fragment 21 android:id="@+id/fragment3" 22 android:name="com.example.fragmentTurn.Fragment3" 23 android:layout_width="0px" 24 android:layout_height="match_parent" 25 android:layout_weight="1" /> 26 27 </LinearLayout>
帶Button的Fragment:
1 package com.example.fragmentTurn; 2 3 import com.example.fragmentdemo.R; 4 5 import android.app.Fragment; 6 import android.os.Bundle; 7 import android.view.LayoutInflater; 8 import android.view.View; 9 import android.view.ViewGroup; 10 import android.widget.Button; 11 import android.widget.TextView; 12 import android.widget.Toast; 13 public class Fragment3 extends Fragment { 14 15 16 @Override 17 public View onCreateView(LayoutInflater inflater, ViewGroup container, 18 Bundle savedInstanceState) { 19 // TODO Auto-generated method stub 20 return inflater.inflate(R.layout.fragment3, container, false); 21 } 22 @Override 23 public void onStart() { 24 super.onStart(); 25 // 方法2: 在Fragment中獲取操作其他Fragment的控件 26 // Button btnGetText=(Button)getActivity().findViewById(R.id.btnGetText); 27 // btnGetText.setOnClickListener(new View.OnClickListener() { 28 // 29 // @Override 30 // public void onClick(View v) { 31 // TextView tv=(TextView)getActivity().findViewById(R.id.tvFragment1); 32 // Toast.makeText(getActivity(), tv.getText().toString() ,Toast.LENGTH_SHORT).show(); 33 // } 34 // }); 35 } 36 }
FragmentTurnActivity.java:
1 package com.example.fragmentTurn; 2 3 4 import com.example.fragmentdemo.R; 5 6 import android.app.Activity; 7 import android.os.Bundle; 8 import android.view.View; 9 import android.widget.Button; 10 import android.widget.TextView; 11 import android.widget.Toast; 12 13 public class FragmentTurnActivity extends Activity { 14 @Override 15 protected void onCreate(Bundle savedInstanceState) { 16 // TODO Auto-generated method stub 17 super.onCreate(savedInstanceState); 18 setContentView(R.layout.activity_fragmentturn); 19 // 方法1:在Activity中操作旗下Fragment中的控件 20 Button btn=(Button)findViewById(R.id.btnGetText); 21 btn.setOnClickListener(new View.OnClickListener() { 22 23 @Override 24 public void onClick(View v) { 25 TextView tv=(TextView)findViewById(R.id.tvFragment1); 26 tv.setText("動態修改"); 27 Toast.makeText(FragmentTurnActivity.this,tv.getText().toString() ,Toast.LENGTH_SHORT ).show(); 28 } 29 }); 30 } 31 }
效果展示:
從上面的例子有一個問題,無論是在Activity中使用findViewById()還是在Fragment中使用getActivity().findViewById(),雖然可以獲取到控件,但是有個例外的情況。就是在Activity中,同時使用了兩個一樣的Fragment,這個時候僅僅使用上面介紹的方法,只能通過id獲取到第一個Fragment中的控件。因為,在布局文件中定義的控件,就算ID重復了,AndroidSDK維護的R.java類中,也只會聲明一次,也就是說,想在Activity中區分同一個Fragment類的兩個實例中的控件,是無法做到的。
那么就嘚換一個思路,我的解決方案:在Fragment中聲明一個View變量,然后在onCreateView中膨脹的View並不直接返回,而是把它引用到聲明的View變量上,然后在應用的任何地方,使用getFragmentManager().findFragmentById(int)通過Fragment的Id找到這個Fragment對象,然后獲取其中的View對象,使用View.findViewById(int)找到Fragment的對應Id的控件,進而操作它,這里就不提供示例了。雖然這個方法可以解決問題,但是一般不推薦如此做,因為大部分場景沒必要在一個Activity中定義兩個相同的Fragment。
上面已經提到,Fragment是Android3.0行增加的特性。 而對於低版本的Android設備,Google也沒有放棄。細心的朋友應該已經發現了,當對Fragment引用包的時候,會有兩個選項,android.app.Fragment和android.support.v4.app.Fragment,其中android.support.v4.app.Fragment就是為了兼容低版本而考慮的,只需要引用它即可。
一般而言,如果考慮向下兼容的問題的話,推薦直接引用android.support.v4.app.Fragment包進行開發,就不會存在兼容性的問題。