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 的实例,这样也就实现了不同碎片之间的通信功能。——《第一行代码》- 郭霖
参考资料: