第5章〓UI进阶
本章思维导图
本章目标
能够熟练使用Fragment动态设计UI界面;
能够熟练使用Menu和Toolbar组件;
能够熟练使用AdapterView、ListView和GridView;
掌握TabHost组件的使用。
5.1Fragment
Android从3.0版本开始引入Fragment(碎片),允许将Activity拆分成多个完全独立封装的可重用的组件,每个组件拥有自己的生命周期和UI布局。使用Fragment为不同型号、尺寸、分辨率的设备提供统一的UI设计方案,Fragment最大的优点是让开发者更加灵活地根据屏幕大小(包括小屏幕的手机、大屏幕的平板电脑)来创建相应的UI界面。
以新闻列表为例,当针对小屏幕手机开发时,开发者通常需要编写两个Activity,分别是Activity A和Activity B,其中,Activity A用于显示所有的新闻列表,列表内容为新闻的标题; Activity B用于显示新闻的详细信息; 当用户单击某个新闻标题时,由Activity A启动Activity B,并显示该标题所对应的新闻内容,两个Activity界面如图51所示。
图51手机上显示新闻列表
当针对平板电脑开发时,使用Fragment A来显示标题列表,使用Fragment B来显示新闻的详细内容,将这两个Fragment在同一个Activity中并排显示; 当单击Fragment A中的新闻标题时,通过Fragment B来显示该标题对应的新闻内容,显示效果如图52所示。每个Fragment都有自己的生命周期和相应的响应事件,通过切换Fragment同样可以实现显示效果的切换。
图52平板电脑上显示新闻列表及内容
每个Fragment都是独立的模块,并与其所绑定的Activity紧密联系在一起,Fragment通常会被封装成可重用的模块。对于一个界面允许有多个UI模块的设备(如平板电脑等),Fragment拥有更好的适应性和动态构建UI的能力,在Activity中可以动态地添加、删除或更换Fragment。
由于Fragment具有独立的布局,能够进行事件响应,且具有自身的生命周期和行为,所以开发人员还可以在多个Activity中共用一个Fragment实例,即当程序运行在大屏设备时启动一个包含多个Fragment的Activity,当程序运行在小屏设备时启动一个包含少量Fragment的Activity。同样以新闻列表为例,对程序进行如下设置: 当检测到程序运行在大屏设备时,启动Activity A,并将标题列表和新闻内容所对应的两个Fragment都放在Activity A中; 当检测到程序运行在小屏设备时,依然启动Activity A,但此时Activity A中只包含一个标题列表Fragment,当用户单击某个新闻标题时,Activity A将启动Activity B,再通过Activity B加载新闻内容所对应的Fragment。
5.1.1使用Fragment
创建Fragment的过程与Activity类似,自定义的Fragment必须继承Fragment类或其子类。Fragment的继承体系如图53所示。
图53Fragment的继承体系
与Activity类似,同样需要实现Fragment中的回调方法,如onCreate()、onCreateView()、onStart()和onResume()等。
通常在创建Fragment时,需要实现以下三个方法。
onCreate(): 系统在创建Fragment对象时调用此方法,用于初始化相关的组件,例如,一些在暂停或者停止时依然需要保留的组件。
onCreateView(): 系统在第一次绘制Fragment对应的UI时调用此方法,该方法将返回一个View,如果Fragment未提供UI则返回null。当Fragment继承自ListFragment时,onCreateView()方法默认返回一个ListView。
onPause(): 当用户离开Fragment时首先调用此方法; 当用户无须返回时,可以通过该方法来保存相应的数据。
Fragment不能独立运行,必须嵌入Acitivity中使用,因此Fragment的生命周期与其所在的Activity密切相关。
将Fragment加载到Activity中主要有以下两种方式。
把Fragment添加到Activity的布局文件中。
在Activity的代码中动态添加Fragment。
在上述两种方式中,第一种方式虽然简单但灵活性不够。如果把Fragment添加到Activity的布局文件中,就会使得Fragment及其视图与Activity的视图绑定在一起,在Activity的生命周期中,无法灵活地切换Fragment视图。因此在实际开发过程中,多采用第二种方式。相对而言,第二种方式要比第一种方式复杂,但也是唯一一种可以在运行时控制Fragment的方式,可以动态地添加、删除或替换Fragment实例。
1. 创建Fragment
下述代码演示了Fragment的基本用法。屏幕分为左右两部分,通过单击屏幕左侧的按钮,在右侧动态地显示Fragment。其布局文件代码如下。
【案例51】fragment_main.xml
上述代码较为简单,定义了一个id为“left”的LinearLayout和一个id名为“right”的LinearLayout,将整个布局分为左右两部分: 左边占1/4,右边占3/4。其中,id为“right”的LinearLayout是一个包含Fragment的容器。
接下来创建FragmentDemoActivity,用于显示上面的布局效果,代码如下。
【案例52】FragmentDemoActivity.java
public class FragmentDemoActivity extends AppCompatActivity {
//展示内容Button
Button displayBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_main);
displayBtn = (Button) findViewById(R.id.displayBtn);
displayBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
图54fragment_main.xml界面效果
//TODO
}
});
}
}
上述代码中,声明并初始化名为“displayBtn”的Button组件,并为其添加了OnClickListener事件监听器,此处仅作演示,事件处理部分暂未实现。运行上述代码,界面效果如图54
所示。
当用户单击“显示”按钮时,需要在屏幕右侧动态地显示内容,此处通过动态地添加Fragment来实现。在工程中创建一个名为fragment_right.xml的文件,用于显示右侧的内容,代码如下。
【案例53】fragment_right.xml
上述代码较为简单,定义了两个组件: TextView组件用于显示普通的文本; Button按钮用于演示Fragment中的事件处理机制。
下述代码定义一个Fragment类,代码实现如下。
【案例54】RightFragment.java
public class RightFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
//重写onCreateView()方法 ①
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//获取view对象 ②
View view = inflater.inflate(R.layout.fragment_right, null);
//从view容器中获取组件③
Button button = (Button) view.findViewById(R.id.frgBtn);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getActivity(),
"我是Fragment", Toast.LENGTH_SHORT).show();
}
});
return view;
}
@Override
public void onPause() {
super.onPause();
}
}
上述代码需要注意以下几点。
标号①
处重写了onCreateView()方法,该方法返回的View对象将作为该Fragment显示的View组件,当Fragment绘制界面组件时将会回调该方法。
标号②
处通过LayoutInflater对象的inflate()方法加载fragment_right.xml布局文件,并返回对应的View容器对象,其他组件对象都是从该View对象中获取。
标号③
处从View容器中获取Button对象,并为该对象添加OnClickListener事件监听器,然后实现相应的事件处理功能。
修改案例52 FragmentDemoActivity.java中displayBtn按钮的事件处理方法,当用户单击“显示”按钮时,右侧动态显示Fragment对象对应的布局,所修改的代码如下。
...
displayBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//步骤1: 获得一个FragmentTransaction的实例
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager
.beginTransaction();
//步骤2: 用add()方法加上Fragment的对象rightFragment
RightFragment rightFragment = new RightFragment();
transaction.add(R.id.right, rightFragment);
//步骤3: 调用commit()方法使得FragmentTransaction实例的改变生效
transaction.commit();
}
});
...
图55动态显示Fragment对象
再次运行FragmentDemoActivity并单击“显示”按钮时,显示右侧的Fragment; 单击show按钮时,弹出“我是Fragment”提示信息,界面效果如图55所示。
2. 管理Fragment
通过FragmentManager可实现管理Fragment对象的管理。在Activity中可以通过getFragmentManager()方法来获取FragmentManager对象。
FragmentManager能够完成以下几方面的操作。
通过findFragmentById()或findFragmentByTag()方法获取Activity中已存在的Fragment对象。
通过popBackStack()方法将Fragment从Activity的回退栈中弹出(模拟用户按Back键)。
通过addOnBackStackChangedListerner()方法注册一个侦听器以监视回退栈的变化。
当需要添加、删除或替换Fragment对象时,需要借助于FragmentTransaction对象来实现,FragmentTransaction用于实现Activity对Fragment的操作,例如,添加或删除Fragment操作。
Fragment的最大特点是根据用户的输入可以灵活地对Fragment进行添加、删除、替换及执行其他操作。开发人员可以把每个事务保存在Activity的回退栈中,使得用户能够在Fragment之间进行导航(与在Activity之间导航相同)。
针对一组Fragment的变化称为一个事务,事务通过FragmentTransaction来执行,而FragmentTransaction对象需要通过FragmentManager来获取,示例代码如下。
【示例】获取FragmentTransaction对象
FragmentManager fragmentManager=getFragmentManager();
FragmentTransaction fragmentTransaction=fragmentManager.beginTransaction();
事务是指在同一时刻执行的一组动作,要么一起成功,要么同时失败。事务可以用add()、remove()、replace()等方法构成,最后使用commit()方法来提交事务。
在调用commit()方法之前,可以使用addToBackStack()方法把事务添加到一个回退栈中,这个后退栈属于所对应的Activity。当用户按下回退键时,就可以返回到Fragment执行事务之前的状态。
注意FragmentTransaction被称作Fragment事务,与数据库事务类似,Fragment事务代表了Activity对Fragment执行的多个改变操作。
下述代码演示了如何用一个Fragment代替另一个Fragment,并在回退栈中保存被代替的Fragment的状态。
【示例】使用FragmentTransaction
//创建一个新的Fragment对象
Fragment newFragment=new ExampleFragment();
//通过FragmentManager获取Fragment事务对象
FragmentTransaction transaction
=getFragmentManager().beginTransaction();
//通过replace()方法把fragment_container替换成新的Fragment对象
transaction.replace(R.id.fragment_container,newFragment);
//添加到回退栈
transaction.addToBackStack(null);
//提交事务
transaction.commit();
上述代码中,ExampleFragment类是一个自定义的Fragment子类。通过replace()方法使用newFragment代替组件R.id.fragment_container所指向的ViewGroup中包含的Fragment对象。然后调用addToBackStack()方法,将被代替的Fragment放入回退栈。当用户按Backspace键时,回退到事务提交之前的状态,即界面重新展示原来的Fragment对象。如果向事务中添加了多个动作,例如,多次调用了add()、remove()方法之后又调用了addToBackStack()方法,那么在commit()之前调用的所有方法都被作为一个事务。当用户按回退键时,所有的动作都会回滚。
事务中动作的执行顺序可以随意,但需要注意以下两点。
程序的最后必须调用commit()方法。
如果程序中添加了多个Fragment对象,则显示的顺序跟添加顺序一致(即后添加的覆盖之前的)。如果在执行的事务中有删除Fragment对象的动作,而且没有调用addToBackStack()方法,那么当事务提交时被删除的Fragment就会被销毁。反之,那些Fragment就不会被销毁,而是处于停止状态,当用户返回时,这些Fragment将会被恢复。
注意调用commit()方法后,事务并不会马上提交,而是会在Activity的UI线程(主线程)中等待直到线程能执行的时候才执行,不过可以在UI线程中调用executePendingTransactions()方法来立即执行事务。但一般不需要这样做,除非有其他线程在等待事务的执行。
3. 与Activity通信
Fragment的实现是独立于Activity的,可以用于多个Activity中; 而每个Activity允许包含同一个Fragment类的多个实例。在Fragment中,通过调用getActivity()方法可以获得其所在的Activity实例,然后使用findViewById()方法查找Activity中的组件,示例代码如下。
【示例】Fragment获取其所在的Activity中的组件
View listView=getActivity().findViewById(R.id.list);
在Activity中还可以通过FragmentManager的findFragmentById()等方法来查找其所包含的Frament实例,示例代码如下。
【示例】Activity获取指定Frament实例
ExampleFragment fragment = (ExampleFragment)getFragmentManager()
.findFragmentById(R.id.example_fragment);
有时需要Fragment与Activity共享事件,通常的做法是在Fragment中定义一个回调接口,然后在Activity中实现该回调接口。
下面以新闻列表为例,在Activity中包含两个Fragment: FragmentA用于显示新闻标题,FragmentB用于显示标题对应的内容。在FragmentA中,用户单击某个标题时通知Activity,然后Activity再通知FragmentB,此时FragmentB就会显示该标题所对应的新闻内容。在FragmentA中定义OnNewsSelectedListener接口,代码如下。
【示例】在Fragment中定义回调接口
public static class FragmentA extends ListFragment {
...
//Activity必须实现下面的接口
public interface OnNewsSelectedListener{
//传递当前被选中的标题的id
public void onNewsSelected(long id);
}
...
}
然后在Activity中实现OnNewsSelectedListener接口,并重写onNewsSelected()方法来通知FragmentB。当Fragment添加到Activity中时,会调用Fragment的onAttach()方法,在该方法中检查Activity是否实现了OnNewsSelectedListener接口,并对传入的Activity实例进行类型转换,代码如下。
【示例】使用onAttach()方法检查Activity是否实现回调接口
public static class FragmentA extends ListFragment {
OnNewsSelectedListener mListener;
...
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
try{
mListener =(OnNewsSelectedListener)activity;
}catch(ClassCastException e){
throw new ClassCastException(activity.toString()
+"必须继承接口 OnNewsSelectedListener");
}
}
...
}
上述代码中,如果Activity没有实现该接口,FragmentB会抛出ClassCastException异常。mListener成员变量用于保存OnNewsSelectedListener的实例,FragmentA通过调用mListener的方法实现与Activity共享事件。由于FragmentA继承自ListFragment类,所以每次选中列表项时,就会调用FragmentA的onListItemClick()方法,在onListItemClick()方法中调用onNewsSelected()方法实现与Activity的共享事件,示例代码如下。
【示例】Fragment与Activity共享事件
public static class FragmentA extends ListFragment {
OnNewsSelectedListener mListener;
...
@Override
public void onListItemClick(ListView l,View v,int position,long id){
mListener.onNewsSelected(id);
}
...
}
上述代码中,onListItemClick()方法中的参数id是列表中被选项的ID,Fragment通过该ID实现从程序的某个存储单元中取得标题的内容。
注意在数据传递时,也可以直接把数据从FragmentA传递给FragmentB,不过该方式降低了Fragment的可重用的能力。现在的处理方式只需要把发生的事件告诉宿主,由宿主决定如何处置,以便Fragment的重用性更好。
5.1.2Fragment的生命周期
Fragment的生命周期与Activity的生命周期类似,也具有以下几个状态。
活动状态——当前Fragment位于前台时,用户可见并且可以获取焦点。
暂停状态——其他Activity位于前台,该Fragment仍然可见,但不能获取焦点。
停止状态——该Fragment不可见,失去焦点。
销毁状态——该Fragment被完全删除或该Fragment所在的Activity结束。
Fragment的生命周期及相关回调方法如图56所示。
图56Fragment的生命周期及相关回调方法
Fragment生命周期中的方法说明如表51所示。
表51Fragment生命周期中的方法说明
序号方法功 能 描 述
1onAttach()当一个Fragment对象关联到一个Activity时被调用
2onCreate()初始化创建Fragment对象时被调用
3onCreateView()当Activity获得Fragment的布局时调用此方法,Fragment在其中创建自己的界面
4onActivityCreated()当Activity对象完成自己的onCreate()方法时调用
5onStart()Fragment对象在UI界面可见时调用
6onResume()Fragment对象的UI可以与用户交互时调用
7onPause()Fragment对象可见,但不可交互,由Activity对象转为onPause状态时调用
8onStop()有组件完全遮挡,或者宿主Activity对象转为onStop状态时调用
9onDestroyView()Fragment对象清理View资源时调用,即移除Fragment中的视图
10onDestroy()Fragment对象完成对象清理View资源时调用
11onDetach()当Fragment被从Activity中删掉时调用
上述方法中,当一个Fragment被创建的时候执行方法1~4; 当Fragment创建完毕并呈现到前台时,执行方法5~6; 当该Fragment从可见状态转换为不可见状态时,执行方法7~8; 当该Fragment被销毁(或者持有该Fragment的Activity被销毁)时,执行方法9~11; 此外,在方法3~5过程中,可以使用Bundle对象保存一个Fragment的对象。
无论是在布局文件中包含Fragment,还是在Activity中动态添加Fragment,Fragment必须依存于Activity,因此Activity的生命周期会直接影响到Fragment的生命周期。Fragment和Activity两者生命周期之间的关系如图57所示。
图57Activity与Fragment生命周期对比
Activity直接影响其所包含的Fragment的生命周期,所以对Activity生命周期中的某个方法调用时,也会产生对Fragment相应的方法调用。例如,当Activity的onPause()方法被调用时,其中包含的所有Fragment的onPause()方法都将被调用。
在生命周期中,Fragment的回调方法要比Activity多,多出的方法主要用于与Activity的交互,例如,onAttach()、onCreateView()、onActivityCreated()、onDestroyView()和onDetach()方法。
当Activity进入运行状态时(即running状态),才允许添加或删除Fragment。因此,只有当Activity处于resumed状态时,Fragment的生命周期才能独立运转,其他阶段依赖于Activity的生命周期。
为了使读者更好地理解Fragment的生命周期,下面分别介绍静态方式和动态方式。
1. 静态方式
静态方式是指将Fragment组件在布局文件中进行布局。Fragment的生命周期会随其所在的Activity的生命周期而发生变化,生命周期的方法调用过程如下。
当首次展示布局页面时,其生命周期方法调用的顺序是: onAttach()→onCreate()→onCreateView()→onActivityCreated()→onStart()→onResume()。
当关闭手机屏幕或者手机屏幕变暗时,其生命周期方法调用的顺序是: onPause()→onStop()。
当对手机屏幕解锁或者手机屏幕变亮时,其生命周期方法调用的顺序是: onStart()→onResume()。
当对Fragment所在屏幕按回退键时,其生命周期方法调用的顺序是: onPause()→onStop()→onDestroyView()→onDestroy()→onDetach()。
2. 动态方式
当使用FragmentManager动态地管理Fragment并且涉及addToBackStack时,其生命周期的展现显得有些复杂。
动态方式主要通过重写Fragment生命周期的方法,然后在Activity代码中动态使用Fragment。例如,定义两个Fragment分别为FragmentA和FragmentB,在其生命周期的各个方法中打印(Log输出方式)相关信息来验证方法的调用顺序。定义FragmentA的代码如下。
【案例55】FragmentA.java
public class FragmentA extends Fragment {
private static final String TAG = FragmentA.class.getSimpleName();
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Log.i(TAG, "onAttach");
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.i(TAG, "onCreateView");
return inflater.inflate(R.layout.fragment_a, null, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
Log.i(TAG, "onViewCreated");
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
@Override
public void onDetach() {
Log.i(TAG, "onDetach");
super.onDetach();
}
@Override
public void onDestroyView() {
Log.i(TAG, "onDestroyView");
super.onDestroyView();
}
@Override
public void onStart() {
Log.i(TAG, "onStart");
super.onStart();
}
@Override
public void onStop() {
Log.i(TAG, "onStop");
super.onStop();
}
@Override
public void onResume() {
Log.i(TAG, "onResume");
super.onResume();
}
@Override
public void onPause() {
Log.i(TAG, "onPause");
super.onPause();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.i(TAG, "onActivityCreated");
super.onActivityCreated(savedInstanceState);
}
}
定义FragmentB的代码如下。
【案例56】FragmentB.java
public class FragmentB extends Fragment {
private static final String TAG = FragmentB.class.getSimpleName();
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Log.i(TAG, "onAttach");
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.i(TAG, "onCreateView");
return inflater.inflate(R.layout.fragment_b, null, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
Log.i(TAG, "onViewCreated");
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
@Override
public void onDetach() {
Log.i(TAG, "onDetach");
super.onDetach();
}
@Override
public void onDestroyView() {
Log.i(TAG, "onDestroyView");
super.onDestroyView();
}
@Override
public void onStart() {
Log.i(TAG, "onStart");
super.onStart();
}
@Override
public void onStop() {
Log.i(TAG, "onStop");
super.onStop();
}
@Override
public void onResume() {
Log.i(TAG, "onResume");
super.onResume();
}
@Override
public void onPause() {
Log.i(TAG, "onPause");
super.onPause();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.i(TAG, "onActivityCreated");
super.onActivityCreated(savedInstanceState);
}
}
在Activity中调用FragmentA和FragmentB,代码如下。
【案例57】FragmentLifecircleActivity.java
public class FragmentLifecircleActivity extends AppCompatActivity
implements View.OnClickListener {
//声明Fragment管理器 ①
private FragmentManager fragmentManager;
//声明变量
private Button fragABtn;
private Button fragBBtn;
//Fragments
private FragmentA fragmentA;
private FragmentB fragmentB;
//Fragment名称列表
private String[] fragNames = {"FragmentA","FragmentB"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_frg_life);
//初始化Fragment管理器 ②
fragmentManager = getFragmentManager();
//初始化组件
fragABtn = (Button) findViewById(R.id.fragABtn);
fragBBtn = (Button) findViewById(R.id.fragBBtn);
//设置事件监听器 ③
fragABtn.setOnClickListener(this);
fragBBtn.setOnClickListener(this);
}
//单击事件监听 ④
@Override
public void onClick(View v) {
FragmentTransaction fragmentTransaction
= fragmentManager.beginTransaction();
switch (v.getId()) {
case R.id.fragABtn:
if (fragmentA == null) {
fragmentA = new FragmentA();
fragmentTransaction.replace(R.id.frag_container,
fragmentA, fragNames[0]);
//把FragmentA对象添加到BackStack回退栈中
//fragmentTransaction.addToBackStack(fragNames[0]);
} else {
Fragment fragment
= fragmentManager.findFragmentByTag(fragNames[0]);
//替换Fragment
fragmentTransaction.replace(R.id.frag_container,
fragment, fragNames[0]);
}
break;
case R.id.fragBBtn:
if (fragmentB == null) {
fragmentB = new FragmentB();
fragmentTransaction.replace(R.id.frag_container,
fragmentB, fragNames[1]);
//把FragmentB对象添加到BackStack回退栈中
//fragmentTransaction.addToBackStack(fragNames[1]);
} else {
Fragment fragment
= fragmentManager.findFragmentByTag(fragNames[1]);
//替换Fragment
fragmentTransaction.replace(R.id.frag_container,
fragment, fragNames[1]);
}
break;
default:
break;
}
fragmentTransaction.commit();
}
}
图58运行FragmentLifecircleActivity代码
上述代码需要说明以下几点。
标号①
分别声明了FragmentManager、Button类型的变量属性。
标号②
处用于对标号①
处所声明的属性变量进行初始化,使其可以进行后续的业务逻辑操作。
标号③
处用于对fragABtn、fragBBtn对象注册监听器,来监听用户单击时触发的事件。
标号④
处重写了OnClickListener监听器中的onClick()方法,用于处理单击事件发生时根据按钮的id来决定相应的处理逻辑功能。
运行FragmentLifecircleActivity代码时,产生的效果如图58所示。
当第一次单击“显示FRAGA”按钮时,实际执行的代码如下。
...
fragmentA = new FragmentA();
fragmentTransaction.replace(R.id.frag_container, fragmentA, fragNames[0]);
...
fragmentTransaction.commit();
在Logcat控制台中打印的日志如下。
... I/FragmentA: onAttach
... I/FragmentA: onCreate
... I/FragmentA: onCreateView
... I/FragmentA: onViewCreated
... I/FragmentA: onActivityCreated
... I/FragmentA: onStart
... I/FragmentA: onResume
由上述打印日志可知,FragmentA的生命周期和在布局文件中静态设置的表现完全一致,此处不再赘述。当继续单击“显示FRAGB”按钮时,在Logcat控制台打印的日志如下。
... I/FragmentA: onPause
... I/FragmentA: onStop
... I/FragmentA: onDestroyView
... I/FragmentA: onDestroy
... I/FragmentA: onDetach
... I/FragmentB: onAttach
... I/FragmentB: onCreate
... I/FragmentB: onCreateView
... I/FragmentB: onViewCreated
... I/FragmentB: onActivityCreated
... I/FragmentB: onStart
... I/FragmentB: onResume
由上述打印结果可知,FragmentA从运行状态到销毁状态所调用方法的顺序为: onPause()→onStop()→onDestroyView()→onDestroy()→onDetach()。此时,FragmentA已经由FragmentManager进行销毁,取而代之的是FragmentB对象。如果此时按回退键,FragmentB所调用的方法与FragmentA调用的顺序一样。在添加Fragment过程中如果没有调用addToBackStack()方法进行保存,那么使用FragmentManager更换Fragment时,不会保存Fragment的状态。
如果取消FragmentLifecircleActivity中的addToBackStack()部分的代码注释,如下。
//把FragmentA对象添加到BackStack回退栈中
fragmentTransaction.addToBackStack(fragNames[0]);
…
//把FragmentB对象添加到BackStack回退栈中
fragmentTransaction.addToBackStack(fragNames[1]);
重新运行FragmentLifecircleActivity,然后单击“显示FRAGA”按钮,在Logcat控制台打印的日志如下。
... I/FragmentA: onAttach
... I/FragmentA: onCreate
... I/FragmentA: onCreateView
... I/FragmentA: onViewCreated
... I/FragmentA: onActivityCreated
... I/FragmentA: onStart
... I/FragmentA: onResume
由上述日志可以得知: 此时FragmentA所调用的方法与没有添加addToBackStack()方法时没有任何区别。
然后继续单击“显示FRAGB”按钮,在Logcat控制台打印的日志如下。
... I/FragmentA: onPause
... I/FragmentA: onStop
... I/FragmentA: onDestroyView
... I/FragmentB: onAttach
... I/FragmentB: onCreate
... I/FragmentB: onCreateView
... I/FragmentB: onViewCreated
... I/FragmentB: onActivityCreated
... I/FragmentB: onStart
... I/FragmentB: onResume
由上述日志可以得知: FragmentA生命周期方法只是调用了onDestroyView(),而没有调用onDestroy()和onDetach()方法,即FragmentA界面虽然被销毁,但FragmentManager并没有完全销毁FragmentA,FragmentA仍然存在并保存在FragmentManager中。
继续单击“显示FRAGA”按钮,使用FragmentA来替换当前显示的FragmentB,此时实际上执行的代码如下。
Fragment fragment = fragmentManager.findFragmentByTag(fragNames[0]);
//替换Fragment
fragmentTransaction.replace(R.id.frag_container,fragment, fragNames[0]);
此时Logcat控制台打印的日志为如下。
... I/FragmentA: onCreateView
... I/FragmentA: onViewCreated
... I/FragmentA: onActivityCreated
... I/FragmentA: onStart
... I/FragmentA: onResume
... I/FragmentB: onPause
... I/FragmentB: onStop
... I/FragmentB: onDestroyView
由上述日志可以得知: 使用FragmentA替换FragmentB的方法调用顺序与使用FragmentB替换FragmentA时的调用顺序一致,其作用只是销毁视图,但依然保留了Fragment的状态。此时FragmentA直接调用onCreateView()方法重新创建视图,并使用上次被替换时的Fragment状态。
注意Fragment的生命周期对于初学者有些难度,希望读者通过实践、Log观察的方式对本节有关生命周期的案例运行并认真比对,深刻地理解Fragment生命周期的原理和机制,对后期Fragment的进一步使用会有很大的帮助。
5.2Menu和Toolbar
Menu(菜单)和ToolBar(活动条)都是在Android应用开发过程中必不可少的元素。Menu在桌面应用中使用十分广泛,几乎所有的桌面应用都有菜单。而由于受到手机屏幕大小的制约,菜单在手机应用中的使用减少了很多,但为了增强用户的体验仍然在手机应用中提供了菜单功能。
5.2.1Menu菜单
Android中提供的菜单有如下几种。
选项菜单(Option Menu): 是最常规的菜单,通过单击Android设备的菜单栏来启动。
子菜单: 单击子菜单会弹出悬浮窗口来显示子菜单项,子菜单不支持嵌套,即子菜单中只能包含菜单项而不能再包含其他子菜单。
上下文菜单: 长按视图控件时所弹出的菜单。在Windows中右击时弹出的菜单也是上下文菜单。
图标菜单: 带icon的菜单项。
扩展菜单: 选项菜单最多只能显示6个菜单项,当超过6个时第6个菜单项会被系统替换为一个“更多”子菜单,显示不出来的菜单项都作为“更多”菜单的子菜单项。
注意子菜单项、上下文菜单项、扩展菜单项均无法显示图标。
在Android中,android.view.Menu接口代表一个菜单,用来管理各种菜单项。在开发过程中,一般不需要自己创建菜单,因为每个Activity默认都自带了一个菜单,只要为菜单添加菜单项及相关事件处理即可。MenuItem类代表菜单中的菜单项,SubMenu代表子菜单,两者均位于android.view包中。Menu、MenuItem和SubMenu三者的关系如图59所示。
图59Menu、SubMenu和MenuItem三者的关系
每个Activity都包含一个菜单; 在菜单中又可以包含多个菜单项和子菜单; 由于子菜单实现了Menu接口,所以子菜单本身也是菜单,其中可以包含多个菜单项; 通常系统创建菜单的方法主要有以下两种。
onCreateOptionsMenu(): 创建选项菜单。
onCreateContextMenu(): 创建上下文菜单。
而OnCreateOptionsMenu()和OnOptionsMenuSelected()方法是Activity中提供的两个回调方法,分别用于创建菜单项和响应菜单项的单击事件。
下面介绍如何创建菜单项、菜单项分组及菜单事件的处理方法。
1. Option Menu选项菜单
前面介绍过Android的Activity中已经封装了Menu对象,并提供了onCreateOptionsMenu()回调方法供开发人员对菜单进行初始化,该方法只会在选项菜单第一次显示时被调用; 如果需要动态改变选项菜单的内容,可以使用onPrepareOptionsMenu()方法来实现。初始化菜单内容的代码如下。
【案例58】MenuDemoActivity.java
public class MenuDemoActivity extends AppCompatActivity{
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public boolean onCreateOptionsMenu(Menu menu) {
//调用父类方法来加入系统菜单
super.onCreateOptionsMenu(menu);
//添加菜单项
menu.add("菜单项1");
menu.add("菜单项2");
menu.add("菜单项3");
//如果希望显示菜单,请返回true
return true;
}
}
图510菜单项效果
上述代码重写了Activity的onCreateOptionsMenu()方法,在该方法中获得系统提供的Menu对象,然后通过Menu对象的add()方法向菜单中添加菜单项。运行上述代码并单击右侧的图标
时,效果如图510所示。
添加菜单项时,除了使用add(CharSequence title)方法,还可以使用以下两种方法。
add(int resId)——使用资源文件中的文本来设置菜单项的内容,例如add(R.string.menu1),其中,R.string.menu1对应的是在res\\string.xml中定义的文本。
add(int groupId,int itemId,int order,CharSequence title)——该方法的参数groupId表示组号,开发人员可以给菜单项进行分组,以便快速地操作同一组菜单; 参数itemId为菜单项指定唯一的ID,该项用户可以自己指定,也可以让系统来自动分配,在响应菜单时通过ID来判断被单击的
菜单; 参数order表示菜单项显示顺序的编号,编号小的显示在前面; 参数title用于设置菜单项的内容。
下面使用多个参数的add()方法实现菜单项的添加,代码如下。
【案例59】MenuDemoActivity.java
...
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
//添加4个菜单项,分成两组
int group1 = 1;
int gourp2 = 2;
menu.add(group1, 1, 1, "菜单项1");
menu.add(group1, 2, 2, "菜单项2");
menu.add(gourp2, 3, 3, "菜单项3");
menu.add(gourp2, 4, 4, "菜单项4");
//显示菜单
return true;
}
上述代码运行效果与图510类似,此处不再演示。对菜单项分组之后,使用Menu接口中提供的方法对菜单按组进行操作,该常用的方法如下。
removeGroup(int group)——用于删除一组菜单。
setGroupVisible(int group, boolean visible)——用于设置一组菜单是否可见。
setGroupEnabled(int group, boolean enabled)——用于设置一组菜单是否可单击。
setGroupCheckable(int group,boolean checkable,boolean exclusive)——用于设置一组菜单的勾选情况。
2. 响应菜单项
Android常用的菜单响应方式是通过重写Activity类的onOptionsItemSelected()方法来响应菜单项事件。当菜单项被单击时,Android会自动调用该方法,并传入当前所单击的菜单项,其核心代码如下。
【案例510】MenuDemoActivity.java
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 1:
Toast.makeText(this, "菜单项1", Toast.LENGTH_SHORT).show();
break;
case 2:
Toast.makeText(this, "菜单项2", Toast.LENGTH_SHORT).show();
break;
case 3:
Toast.makeText(this, "菜单项3", Toast.LENGTH_SHORT).show();
break;
case 4:
Toast.makeText(this, "菜单项4", Toast.LENGTH_SHORT).show();
图511单击菜单项效果
break;
}
return super.onOptionsItemSelected(item);
}
上述代码通过重写onOptionsItemSelected()方法来响应菜单事件,为方便代码演示,此处将菜单项ID直接编码在程序中。运行上述代码,并单击“菜单项1”按钮时,效果如图511所示。
3. SubMenu子菜单
子菜单是一种组织式菜单项,被大量地运用在Windows和其他操作系统的GUI设计中。Android同样支持子菜单,开发人员可以通过addSubMenu()方法来创建子菜单。创建子菜单的步骤如下。
(1) 重写Activity类的onCreateOptionsMenu()方法,调用Menu的addSubMenu()方法来添加子菜单。
(2) 调用SubMenu的add()方法为子菜单添加菜单项。
(3) 重写Activity类的onOptionsItemSelected()方法,以响应子菜单的单击事件。
下述代码演示创建子菜单的过程。
【案例511】SubMenuDemoActivity.java
public class SubMenuDemoActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
}
//初始化菜单
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//添加子菜单
SubMenu subMenu = menu.addSubMenu(0, 2, Menu.NONE, "基础操作");
//为子菜单添加菜单项
//"重命名"菜单项
MenuItem renameItem = subMenu.add(2, 201, 1, "重命名");
//"分享"菜单项
MenuItem shareItem = subMenu.add(2, 202, 2, "分享");
//"删除"菜单项
MenuItem delItem = subMenu.add(2, 203, 3, "删除");
return true;
}
//根据菜单执行相应内容
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 201:
Toast.makeText(getApplicationContext(), "重命名...",
Toast.LENGTH_SHORT).show();
Break;
case 202:
Toast.makeText(getApplicationContext(),
"分享...", Toast.LENGTH_SHORT).show();
Break;
case 203:
Toast.makeText(getApplicationContext(),
"删除...", Toast.LENGTH_SHORT).show();
Break;
}
return true;
}
上述代码中,通过addSubmenu()方法为menu菜单添加了SubMenu子菜单; 使用add()方法为子菜单连续添加了三个菜单项; 在子菜单中添加菜单项的方式和在菜单中添加菜单项的方式完全相同。此外,通过MenuItem的setIcon()方法为子菜单的每个菜单项设置相应的图标; 运行代码并单击右侧的
图标后,界面效果如图512所示。
然后单击“基础操作”菜单项,弹出与之对应的子菜单项,效果界面如图513所示。
图512子菜单弹出
图513子菜单项
与图513的菜单项相比,图512中显示的“基础操作”菜单项视觉效果较差; 接下来使用SubMenu的setIcon()方法为“基础操作”子菜单及子菜单项添加图标,代码如下。
//添加子菜单
SubMenu subMenu = menu.addSubMenu(0, 2, Menu.NONE, "基础操作");
subMenu.setIcon(android.R.drawable.ic_menu_manage);
//添加子菜单项
//"重命名"菜单项
MenuItem renameItem = subMenu.add(2, 201, 1, "重命名");
renameItem.setIcon(android.R.drawable.ic_menu_edit);
//"分享"菜单项
MenuItem shareItem = subMenu.add(2, 202, 2, "分享");
shareItem.setIcon(android.R.drawable.ic_menu_share);
//"删除"菜单项
MenuItem delItem = subMenu.add(2, 203, 3, "删除");
delItem.setIcon(android.R.drawable.ic_menu_delete);
图514为子菜单增加图标
重新运行修改后的SubMenuDemoActivity,并单击图标
后,界面效果如图514所示。
在Menu中可以包含多个SubMenu,SubMenu可以包含多个MenuItem,但SubMenu不能包含SubMenu,即子菜单不能嵌套。例如,下面的语句在运行时会报错。
subMenu.addSubMenu("子菜单嵌套"); //编译时通过,运
//行时报错
上面的语句虽然能够通过编译,但在运行时会报错。
4. ContextMenu上下文菜单
在Windows操作系统中,用户能够在文件上右击来执行“打开”“复制”“剪切”等操作,右击所弹出的菜单就是上下文菜单。在手机中经常通过长按某个视图元素来弹出上下文菜单。
上下文菜单是通过调用ContextMenu接口中的方法来实现的。ContextMenu接口继承了Menu接口,如图515所示,因此可以像操作选项菜单一样为上下文菜单增加菜单项。上下文菜单与选项菜单最大的不同是: 选项菜单的拥有者是Activity,而上下文菜单的拥有者是Activity中的View对象。每个Activity有且只有一个选项菜单,并为整个Activity服务。而一个Activity通常拥有多个View对象,根据需要为某些特定的View对象提供上下文菜单,通过调用Activity的registerForContextMenu()方法将某个上下文菜单注册到指定的View对象上。
图515Menu、ContextMenu关系示意图
虽然ContextMenu对象的拥有者是View对象,但是需要使用Activity的onCreateContextMenu()方法来生成ContextMenu对象,该方法的签名如下。
【语法】
onCreateContextMenu(ContextMenu menu, View v,
ContextMenu.ContextMenuInfo menuInfo)
上述方法与onCreateOptionsMenu(Menu menu)方法相似,两者的不同之处在于: onCreateOptionsMenu()只在用户第一次按菜单键时被调用,而onCreateContextMenu()会在用户每一次长按View组件时被调用,并且需要为该View注册上下文菜单对象。
注意在图515中ContextMenuInfo接口的实例作为onCreateContextMenu()方法的参数。该接口实例用于视图元素需要向上下文菜单传递一些信息,例如,该View对应DB记录的ID等,此时需要使用ContextMenuInfo。当需要传递额外信息时,需要重写getContextMenuInfo()方法,并返回一个带有数据的ContextMenuInfo实现类对象。限于篇幅,此处不再赘述。
创建上下文菜单的步骤如下。
(1) 通过registerForContextMenu()方法为ContextMenu分配一个View对象。
(2) 通过onCreateContextMenu()创建一个上下文对象。
(3) 重写onContextItemSelected()方法实现子菜单的单击事件的响应处理。
【案例512】ContextMenuDemoActivity.java
public class ContextMenuDemoActivity extends AppCompatActivity {
Button contextMenuBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contextmenu);
//显示列表
contextMenuBtn = (Button) findViewById(R.id.contextMenuBtn);
//(1)为按钮注册上下文菜单,长按按钮则弹出上下文菜单
this.registerForContextMenu(contextMenuBtn);
}
//(2)生成上下文菜单
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
//观察日志确定每次是否重新调用
Log.d("ContextMenuDemoActivity", "被创建...");
menu.setHeaderTitle("文件操作");
//为上下文添加菜单项
menu.add(0, 1, Menu.NONE, "发送");
menu.add(0, 2, Menu.NONE, "重命名");
menu.add(0, 3, Menu.NONE, "删除");
}
//(3)响应上下文菜单项
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 1:
Toast.makeText(this, "发送...", Toast.LENGTH_SHORT).show();
break;
case 2:
Toast.makeText(this, "重命名...", Toast.LENGTH_SHORT).show();
break;
case 3:
Toast.makeText(this, "删除...", Toast.LENGTH_SHORT).show();
break;
default:
return super.onContextItemSelected(item);
}
return true;
}
}
图516ContextMenu效果
上述代码中,首先在onCreate()方法中加载了activity_contextmenu.xml视图文件,该文件位于res\\layout文件夹下,其中只包含一个按钮组件,读者可自行查看; 然后为按钮注册上下文菜单; 接下来通过onCreateContextMenu()回调方法为系统创建的ContextMenu对象添加菜单项; 最后通过onContextItemSelected()方法实现菜单项事件处理。
运行上述代码并长按“上下文菜单”按钮时,系统会弹出上下文菜单,效果如图516所示。
注意在运行程序时,通过Logcat的输出信息发现: 每次弹出上下文菜单时都会调用onCreateContextMenu()方法。
5. 使用XML资源生成菜单
前面介绍的常用菜单,都是通过硬编码方式添加菜单项,Android为开发人员提供了一种更加方便的菜单生成方式,即通过XML文件来加载和响应菜单,此种方式易于维护,可读性更强。
使用XML资源生成菜单项的步骤如下。
(1) 在res目录中创建menu子目录。
(2) 在menu子目录中创建一个Menu Resource file(XML文件),文件名可以随意,Android会自动为其生成资源ID,例如,R.menu.context_menu对应menu目录的context_menu.xml资源文件; 在该XML文件中可以提供menu所需的菜单项。
(3) 使用XML文件的资源ID(如R.menu.context_menu),在Activity中将XML文件中所定义的菜单元素添加到menu对象中。
(4) 通过判断菜单项对应的资源ID(如R.id.item_send),来实现相应的事件处理。
下面将工程中的ContextMenuDemoActivity类文件进行复制,并改名为XMLContextMenuDemoActivity。接下来,在XMLContextMenuDemoActivity中使用XML资源来生成菜单。
1) 定义菜单资源文件
在res目录下创建menu子目录,在menu目录下创建一个XML资源文件,并命名为context_menu.xml,代码如下。
【案例513】context_menu.xml
在context_menu.xml文件中针对XMLContextMenuDemoActivity所定义的菜单项进行重写,并为每个菜单项分配了一个可读性较强的ID。
2) 使用MenuInflater添加菜单项
Inflater为Android建立了从资源文件到对象的桥梁,MenuInflater把XML菜单资源转换为对象并将其添加到menu对象中。在XMLContextMenuDemoActivity中重写onCreateContextMenu()方法,并使用Activity的getMenuInflater()方法可以获取MenuInflater对象,然后将XML文件中所定义的菜单元素添加到menu对象中,代码如下。
//(2)生成上下文菜单
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
Log.d("ContextMenuDemoActivity", "被创建...");
menu.setHeaderTitle("文件操作");
getMenuInflater().inflate(R.menu.context_menu, menu);
}
3) 响应菜单项
接下来重写XMLContextMenuDemoActivity类的onContextItemSelected()方法实现菜单项的事件处理功能,代码如下。
//(3)响应上下文菜单项
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.item_send:
Toast.makeText(this, "发送...", Toast.LENGTH_SHORT).show();
break;
case R.id.item_rename:
Toast.makeText(this, "重命名...", Toast.LENGTH_SHORT).show();
break;
case R.id.item_del:
Toast.makeText(this, "删除...", Toast.LENGTH_SHORT).show();
break;
default:
return super.onContextItemSelected(item);
}
return true;
}
上述代码演示了使用XML资源文件生成菜单的优势。Android不仅为context_menu.xml文件生成了资源ID,还为文件中group、menu和item等元素来自动生成相应的ID(与布局文件中所定义的ID相同)。菜单项ID的创建与管理全部由Android系统来完成,无须开发人员花费心思进行定义。运行XMLContextMenuDemoActivity,效果与图516完全相同。
使用XML生成菜单是在Android中创建菜单的推荐方式。实际上,开发人员在代码中对菜单项或分组等操作都能在XML资源文件中完成,下面简单介绍一些比较常见的操作。
(1) 资源文件实现子菜单。
通过在item元素中嵌套menu子元素来实现子菜单,代码如下。
-
(2) 为菜单项添加图标。
(3) 设置菜单项的可选策略。
使用android:checkableBehavior设置一组菜单项的可选策略,可选值为none、all或single。
(4) 使用android:checked设置特定菜单项。
(5) 设置菜单项可用/不可用。
(6) 设置菜单项可见/不可见。
5.2.2Toolbar操作栏
Toolbar是从Android 5.0开始推出的一个Material Design风格的导航组件,Google非常推荐人们使用Toolbar来作为Android客户端的导航栏,以此来取代之前的Actionbar。Actionbar需要要固定在Activity的顶部,与Actionbar相比,Toolbar明显要灵活,Toolbar可以放到界面的任意位置。除此之外,在设计Toolbar时,Google也为开发者预留了许多可定制修改的余地,例如,设置导航栏图标、设置App的Logo图标、支持设置标题和子标题、支持添加一个或多个自定义组件、支持Action Menu等。
Toolbar继承自ViewGroup类,Toolbar的常用方法如表52所示。
表52Toolbar常用方法
方法功 能 描 述
setTitle(int resId)设置标题
setSubtitle(int resId)设置子标题
setTitleTextColor(int color)设置标题字体颜色
setSubtitleTextColor(int color)设置子标题字体颜色
setNavigationIcon(Drawable icon)设置导航栏的图标
setLogo(Drawable drawable)设置Toolbar的Logo图标
1. Toolbar的简单应用
首先在res\\values\\themes.xml文件中,对
B.
C.
- match_parent
D.
4. 通过使用ListView来完成用户的列表功能,并且在每个item上显示用户的删除和更新按钮,同时通过测试数据的方式实现对应的业务功能。
5. 使用GridView网格显示3行3列图片。