Fragment
- Fragment,碎片,是 Android 3.0 開始引入的,其目的在于同時兼容手機和平板的開發,能讓程序更合理地利用大屏幕的空間;
- Fragment 是可以嵌入到 Activity 中的 UI 片段,也必須嵌入到 Activity 中,它可以包含布局,也有自己的生命周期(其生命周期受到其宿主 Activity 的影響),是一個輕量型的 Activity;
- 一個 Activity 中可以包含多個 Fragment,一個 Fragment 也可以在多個 Activity 中復用;
下圖取自官方文檔,很好地解釋了 Fragment 的應用:
Fragment 的使用
Fragment 有兩種使用方式,一種是是靜態添加到 Activity 中,另一種是動態添加 Activity 中。
靜態添加
- 首先在一個 Activity 中添加兩個 Fragment,這兩個 Fragment 是水平分布,首先是左側的 Fragment 布局文件,fragment_left.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0000ff"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="左側Fragment按鈕" />
</LinearLayout>
我們將左側的 Fragment 背景顏色設為藍色,在里面放了一個 Button,并讓它水平居中。
- 然后是右側的 Fragment 的布局文件,fragment_right.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0000"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="右側Fragment"
android:textSize="36sp" />
</LinearLayout>
我們將右側的 Fragment 背景顏色設為紅色,在里面放了一個 TextView,并讓它水平居中。
- 左側的 XML 布局文件對應的 Java 代碼,LeftFragment.java
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_left, container, false);
return view;
}
}
- 右側的 XML 布局文件對應的 Java 代碼,RightFragment.java
public class RightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_right, container, false);
return view;
}
}
上面分別新建了兩個類,并讓它們繼承自 Fragment 類,代表左右側 Fragment,并將各自對應的布局傳進去。
- 接下來是 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/fragment_left"
android:name="net.monkeychan.fragmenttest.LeftFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<fragment
android:id="@+id/fragment_right"
android:name="net.monkeychan.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
在 Activity 的布局中,我們定義了兩個 fragment 控件,并讓它們水平排列,其中右側的 Fragment 充滿了剩下的空間。
注意: fragment 控件中有個屬性 android:name,其值為 包名.類名,包名不能省略。
- MainActivity 的代碼,默認即可,MainActivity.java:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- 效果演示:
總結:靜態添加 Fragment 的步驟
- 新建 Fragment 的布局;
- 新建一個繼承自 Fragment 類 的類,并將對應的布局傳進去;
- 在 Activity 布局中定義 fragment 控件,將 fragment 控件的 *android:name * 屬性的值設為 包名.類名。
動態添加
上面的程序中我們定義了一個按鈕,但沒有給它添加點擊事件,現在我們把給它增加一個功能,使按鈕被按下時,把右側的 Fragment 更換成另一個 Fragment。
- 新建 fragment_right_2.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00FF00"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="右側第二個Fragment"
android:textSize="36sp" />
</LinearLayout>
上面只是在原來 fragment_right.xml 上修改了背景顏色和 TextView 的顏色。
- SecondRightFragment.java:
public class SecondRightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_right_2, container, false);
return view;
}
}
- activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/fragment_left"
android:name="net.monkeychan.fragmenttest.LeftFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/layout_right"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<fragment
android:id="@+id/fragment_right"
android:name="net.monkeychan.fragmenttest.RightFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
activity_main.xml 中我們添加了在 LinearLayout 中嵌套了一個 FrameLayout 布局,并讓它與左側的 fragment 控件水平排列,后面我們將在代碼中用另一個 Fragment 來取代 FrameLayout 里的內容,FrameLayout 在這里起占位作用,FrameLayout 也可以換成其他布局。
- MainActivity.java:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(this);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn:
replaceFragment();
break;
default:
break;
}
}
private void replaceFragment() {
// 1. 創建需要添加的 Fragment 的對象
SecondRightFragment fragment = new SecondRightFragment();
// 2. 獲取 FragmentManager,在 Activity 中直接調用 getFragmentManager() 方法獲取
FragmentManager fm = getFragmentManager();
// 3. 開啟一個事務,通過調用 beginTransaction() 方法開啟
FragmentTransaction transaction = fm.beginTransaction();
// 4. 向布局中添加 Fragment,調用 replace() 方法實現,需要傳入布局的 id 和待添加的 Fragment 的對象
transaction.replace(R.id.layout_right, fragment);
// 5. 提交事務,調用 commit() 方法來完成
transaction.commit();
}
}
- 效果演示:
點擊按鈕之后,變成下面的界面:
此時點擊返回鍵,會發現直接退出程序。如果想要點擊返回鍵不退出程序,而是返回上一個 Fragment 時,可以在調用 FragmentTransaction 的 commit() 方法之前調用 addToBackStack() 方法,并給它傳入 null,之后按下返回鍵將不會直接退出程序,而是返回上一個 Fragment。
總結:動態添加 Fragment 的步驟
- 創建需要添加的 Fragment 的對象;
- 獲取 FragmentManager,在 Activity 中直接調用 getFragmentManager() 方法獲取;
- 開啟一個事務,通過調用 FragmentManager 對象的 beginTransaction() 方法開啟;
- 向布局中添加 Fragment,調用 FragmentTransaction 的 replace() 方法實現,需要傳入布局的 id 和待添加的 Fragment 的對象;
- 提交事務,調用 FragmentTransaction 的 commit() 方法來完成。
Fragment 的生命周期
-
先通過一張圖片來了解 Fragment 的生命周期,以下圖片取自《第一行代碼》:
- onAttach(): 當 Fragment 和 Activity 第一次建立聯系時調用,之后不再調用;
- onCreate(): onAttach() 方法之后立刻調用;
- onCreateView(): 加載布局時調用;
- onActivityCreated(): 與 Fragment 關聯的 Activity 創建之后調用;
- onStart(): Fragment 可見時調用;
- onResume(): onStart() 執行之后調用;
- onPause(): 當另一個 Activity 在前臺并獲得焦點,但該 Activity 并沒有占滿整個屏幕,而 Fragment 的 宿主 Activity (即Fragment 所在的 Activity) 仍然可見時調用;
- onStop(): Fragment 不可見時調用:當 Fragment 的宿主 Activity 處于 stopped 狀態時調用;當 Fragment 從宿主 Activity 中刪除 (調用 remove() 方法) 但沒有被添加到返回棧 (沒有調用 addToBackStack() 方法) 之中時調用。(處于 stopped 狀態的 Fragment 依然存活,它的所有狀態和信息會被系統保留起來,只是對于用戶而言是不可見的,當 宿主 Activity 被殺死時,Fragment 相應地也會被殺死);
- onDestroyView(): 當與 Fragment 關聯的 View 被被移除時調用;
- onDestroy(): 當 Fragment 不再被使用時調用,如按返回鍵時;
-
onDeatach(): 當 Fragment 與 Activity 解除關聯時調用。
詳細內容請看這篇文章 Fragment 整個生命周期演示
- 接下來修改上面的 RightFragment.java 代碼,重寫圖片中的方法,重寫的規則是在每個方法里面增加一條 Log.d 語句,這樣我們就能在 logcat 里面看到每個方法在什么時候被調用:
public class RightFragment extends Fragment {
private static final String TAG = "Lifecycle";
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d(TAG,"onAttach");
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG,"onCreate");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d(TAG,"onCreateView");
View view = inflater.inflate(R.layout.fragment_right, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG,"onActivityCreated");
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG,"onStart");
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG,"onResume");
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG,"onPause");
}
@Override
public void onStop() {
super.onStop();
Log.d(TAG,"onStop");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG,"onDestroyView");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
}
@Override
public void onDetach() {
super.onDetach();
Log.d(TAG,"onDestroy");
}
}
3.?好了,接下來運行程序,并打開 logcat 窗口,看看發生了什么:
可以看到,在程序啟動時調用了 onCreate() 方法,接著在加載布局的時候調用了 onCreateView() 方法,緊接著又分別調用了 onActivityCeated() 、onStart() 、onResume() 這三個方法。哎?等等,好像有點不對,怎么一開始不是調用 onAttach() 方法?回到我們上面的代碼,我們在重寫 onAttach() 方法時,傳入的參數為類型為 Context,所以在一開始是不會調用的,將傳入的參數類型修改為 Activity 就可以了。另外,在 SDK API 23 的版本中,當我們傳入的參數類型為 Activity 時會提示該方法已過時。
- 將 onAttach() 方法傳入的參數類型改為 Activity,再次運行程序,此時執行的方法如下:
當我們點擊左側 Fragment 中的按鈕時,將會動態替換掉 RightFragment,此時執行的方法如下:
- 讓我們重新運行程序,此時執行的方法如下:
此時按下主頁鍵,執行的方法如下:
再按下多任務鍵,回到程序,此時執行的方法如下:
- 再重新運行程序,然后按下多任務鍵,此時執行的方法如下:
再回到程序,此時執行的方法如下:
- 再重新運行程序,然后按下返回鍵,此時執行的方法如下:
通過上面的一系列操作,我們對 Fragment 的生命周期已經有了一些了解了。
Fragment 與 Activity 之間的通信
- 在 Activity 里調用 Fragment 里的方法:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
在 Activity 中調用 FragmentManager 對象的 findFragmentById() 方法,就能獲得與 Activity 關聯的 Fragment 的實例,進而通過該實例調用 Fragment 里的方法。
- 在 Fragment 里調用 Activity 里的方法:
MainActivity activity = (MainActivity) getActivity();
在 Fragment 中通過 getActivity() 方法,就能獲得與 Fragment 關聯的 Activity 的實例,進而通過該實例調用 Activity 里的方法。
- 下面來看一個實例:
左邊是一個 Activity 里的 ListView,右邊是一個 Fragment,當點擊左邊 ListView 的某一個子項時,在右邊顯示該子項的名字及大圖。
- 首先是 Activity 的布局,activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_view"
android:layout_width="150dp"
android:layout_height="match_parent"
android:layout_marginTop="16dp" />
<!-- FrameLayout 在此起占位作用,后面將用 Fragment 替代它里面的內容 -->
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
- 接下來是右邊 Fragment 的布局,fragment_large.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="#FF7F00"
android:textSize="24sp" />
<ImageView
android:id="@+id/iv_large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:scaleType="centerCrop" />
</LinearLayout>
- 樣式文件,控制 ListView 的外觀,list_view_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_small"
android:layout_width="60dp"
android:layout_height="60dp" />
<TextView
android:id="@+id/tv_small"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:gravity="center" />
</LinearLayout>
- 定義一個接口,接口里面定義了一個 send() 方法,此方法用于從 Activity 向 Fragment 發送數據,即當 Activity 里的 ListView 的某一子項被點擊時,將該子項的名字與圖片 id 傳給右邊的 Fragment,右邊的 Fragment 接收到數據之后,將數據在自己的布局中顯示出來。
FragmentCallbackListener.java:
public interface FragmentCallbackListener {
void send(String name, int imageId);
}
- MainActivity.java:
public class MainActivity extends AppCompatActivity {
// 定義一個 int 型的數組,用于存放圖片資源的 id
private int[] imageId = {R.drawable.aries, R.drawable.taurus, R.drawable.gemini, R.drawable.cancer,
R.drawable.leo, R.drawable.virgo, R.drawable.libra, R.drawable.scorpio, R.drawable.sagittarius,
R.drawable.capricorn, R.drawable.aquarius, R.drawable.pisces, R.drawable.fuck_off};
// 定義一個 String 類型的數組,用于存放名稱
private String[] titles = {"白羊座", "金牛座", "雙子座", "巨蟹座", "獅子座", "處女座",
"天秤座", "天蝎座", "射手座", "摩羯座", "水瓶座", "雙魚座", "Surprise"};
// 定義一個 FragmentCallbackListener 接口的引用
private FragmentCallbackListener listener;
public FragmentCallbackListener getListener() {
return listener;
}
public void setListener(FragmentCallbackListener listener) {
this.listener = listener;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
List<Map<String, Object>> listItems = new ArrayList<>();
for (int i = 0; i < titles.length; i++) {
Map<String, Object> listItem = new HashMap<>();
listItem.put("imageId", imageId[i]);
listItem.put("titles", titles[i]);
listItems.add(listItem);
}
// 1. 實例化布局中的控件
ListView listView = (ListView) findViewById(R.id.list_view);
// 2. 創建一個 Adapter 對象,并進行設置
SimpleAdapter adapter = new SimpleAdapter(this, listItems, R.layout.list_view_item,
new String[]{"imageId", "titles"}, new int[]{R.id.iv_small, R.id.tv_small});
// 3. 將 Adapter 設置到 AdapterView
listView.setAdapter(adapter);
// 4. 設置 AdapterView 的監聽事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
// 調用 FragmentCallbackListener 接口的 send() 方法,
// 把用戶當前點擊的子項的名稱和圖片 id 傳給實現 FragmentCallbackListener 接口的地方
listener.send(titles[i], imageId[i]);
}
});
replaceFragment();
}
// 此方法用于動態添加 Fragment
private void replaceFragment() {
// 1. 創建一個待添加的 Fragment 的對象
LargeViewFragment fragment = new LargeViewFragment();
// 2. 獲取 FragmentManager,通過 getFragmentManager() 方法獲取
FragmentManager fm = getFragmentManager();
// 3. 開啟一個事務,通過 FragmentManager 對象的 beginTransaction() 方法開啟
FragmentTransaction transaction = fm.beginTransaction();
// 4. 向布局中添加 Fragment
transaction.replace(R.id.frame_layout, fragment);
// 5. 提交事務
transaction.commit();
}
}
- LargeViewFragment.java:
public class LargeViewFragment extends Fragment {
private TextView tv_large;
private ImageView iv_large;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// 加載 Fragment 的布局
View view = inflater.inflate(R.layout.fragment_large, container, false);
// 實例化布局中的控件
tv_large = (TextView) view.findViewById(R.id.tv_large);
iv_large = (ImageView) view.findViewById(R.id.iv_large);
// 獲取宿主 Activity 的實例
MainActivity activity = (MainActivity) getActivity();
// 調用 Activity 中的 setListener 方法,并實現 FragmentCallbackListener 接口
activity.setListener(new FragmentCallbackListener() {
@Override
public void send(String name, int imageId) {
// 更新 UI
tv_large.setText(name);
iv_large.setImageResource(imageId);
}
});
return view;
}
}
注意: 上面的代碼中使用了接口回調,在 MainActivity 中并不實現該接口,而是在 Fragment 中實現該接口;也就是說,發送數據的一方不實現接口,接收數據的一方負責實現接口。
- 演示效果:
Fragment 與 Fragment 之間的通信
在一個 Fragment 中可以得到與它關聯的 Activity,然后再通過這個 Activity 去獲取另外一個 Fragment 的實例,這樣也就實現了不同碎片之間的通信功能。——《第一行代碼》- 郭霖
參考資料: