第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 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:orientation="horizontal"> <LinearLayout android:id="@+id/left" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="#FFFFFF" android:orientation="vertical" > <Button android:id="@+id/displayBtn" android:text="显示" .../> </LinearLayout> <LinearLayout android:id="@+id/right" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="3" android:background="#D3D3D3" android:orientation="vertical" > </LinearLayout> </LinearLayout> 上述代码较为简单,定义了一个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 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:orientation="vertical" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="false" android:text="新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容" /> <Button android:id="@+id/frgBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="show" /> </LinearLayout> 上述代码较为简单,定义了两个组件: 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 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:id="@+id/group1" > <item android:id="@+id/item_send" android:title="发送"/> <item android:id="@+id/item_rename" android:title="重命名"/> <item android:id="@+id/item_del" android:title="删除"/> </group> </menu> 在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子元素来实现子菜单,代码如下。 <item android:title="系统设置"> <menu> <item android:id="@+id/mi_display_setting"android:title="显示设置"/> <item android:id="@+id/mi_network_setting"android:title="网络设置"/> <!—其他菜单项 --> </menu> </item> (2) 为菜单项添加图标。 <item android:id="@+id/mi_exit" android:title="退出" android:icon="@drawable/exit"/> (3) 设置菜单项的可选策略。 使用android:checkableBehavior设置一组菜单项的可选策略,可选值为none、all或single。 <group android:id="..." android:checkableBehavior="all"> <!-- 菜单项 --> </group> (4) 使用android:checked设置特定菜单项。 <item android:id="..." android:title="sometitle" android:checked="true"/> (5) 设置菜单项可用/不可用。 <item android:id="..." android:title="sometitle" android:enabled="false"/> (6) 设置菜单项可见/不可见。 <item android:id="..." android:title="sometitle" android:visible="false"/> 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文件中,对<style>元素的parent属性进行设置,使用MaterialComponents.DayNight中的NoActionBar主题,从而去除ActionBar提供的操作栏,代码如下。 【语法】 <style name="Theme.Chapter05" parent="Theme.MaterialComponents.DayNight.NoActionBar"> 然后在Activity对应的布局文件中添加androidx.appcompat.widget包中的Toolbar组件,语法如下。 【语法】 <androidx.appcompat.widget.Toolbar android:id="@+id/my_toolbar" .../> 接下来,在Activity的onCreate()方法中,使用setSupportActionBar()方法将Toolbar设置为Activity的操作栏,其语法如下。 【语法】显示Toolbar组件 Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); setSupportActionBar(toolbar); 以上各步操作对应的完整代码如下。 【案例514】toolbar.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/relativeLayoutContainer" ...> <androidx.appcompat.widget.Toolbar android:id="@+id/my_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" /> </RelativeLayout> 【案例515】ToolbarActivity.java public class ToolbarActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); setSupportActionBar(toolbar); } } 图517Toolbar的简单使用 运行ToolbarActivity,结果如图517所示。 2. Toolbar的综合应用 在图517中只显示了应用程序的名称; 除此之外,Toolbar还可以包含导航按钮、应用的Logo、标题和子标题、若干个自定义View及动作菜单等元素,代码如下。 【案例516】toolbar.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"... android:id="@+id/relativeLayoutContainer"> < androidx.appcompat.widget.Toolbar android:id="@+id/my_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" > <TextView android:id="@+id/toolbar_title" android:text="自定义" android:textColor="#fff" android:textSize="21sp" .../> </ androidx.appcompat.widget.Toolbar> </RelativeLayout> 在上述代码中,在Toolbar组件中添加一个TextView组件,然后在Activity中通过ID来获取该TextView组件,并为其添加相应的事件处理。 下面在res\\menu目录中创建一个XML布局文件menu_tool_demo.xml,代码如下。 【案例517】menu_tool_demo.xml <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"...> <item android:id="@+id/toolbar_action" android:icon="@mipmap/ic_search" android:title="Action1"app:showAsAction="ifRoom"/> <item android:id="@+id/action_item1" android:title="item1" app:showAsAction="never" /> <item android:id="@+id/action_item2" android:title="item2" app:showAsAction="never" /> </menu> 上述代码用于设置Toolbar右侧导航栏的内容。 接下来,在Activity中设置Toolbar的标题及字体颜色、应用的图标、导航按钮图标等特征,代码如下。 【案例518】ToolbarActivity.java public class ToolbarActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); toolbar.setTitle("ToolbarDemo"); setSupportActionBar(toolbar); //显示应用的Logo并设置图标 getSupportActionBar().setLogo(R.mipmap.ic_launcher); //显示标题和子标题并设置颜色 toolbar.setTitleTextColor(Color.WHITE); toolbar.setSubtitle("Android基础"); toolbar.setSubtitleTextColor(Color.WHITE); //显示导航按钮图标 toolbar.setNavigationIcon(R.mipmap.ic_drawer_home); } //显示Menu菜单按钮 public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_toolbar_demo, menu); return true; } } 上述代码中,getSupportActionBar()是用于获取已设定的Toolbar组件,并通过setTitle()、setSubtitle()等方法对Toolbar进一步进行设置。 运行ToolbarActivity,结果如图518所示。 图518Toolbar的综合应用 注意上述代码中,Toolbar的setTitle()方法需要在setSupportActionBar()方法之前调用,否则无效。 5.3高级组件 5.3.1AdapterView与Adapter Java EE中提供了一种架构模式: MVC(Model View Controller)架构,即模型视图控制器三层架构。MVC架构的实现原理: 数据模型用于存放数据,利用控制器将数据显示在视图中。在Android中提供了一种高级组件AdapterView,其实现过程类似于MVC架构。AdapterView之所以称为高级组件,是因为该组件的使用方式与其他组件不同,不仅需要在界面中使用AdapterView,还需要通过适配器为其添加所需的数据或组件。 (1) 控制层: 在AdapterView实现的过程中,Adapter适配器承担了控制层的角色,通过Adapter可以将数据源中的数据以某种样式(例如XML文件)呈现到视图中。 (2) 视图层: AdapterView充当了MVC中的视图层,用于将前端显示和后端数据分离,其内容一般是包含多项相同格式资源的列表。 (3) 模型层: 将数据源当作模型层,其中包括数组、XML文件等形式的数据。 1. AdapterView组件 AdapterView组件是一组重要的组件。AdapterView本身是一个抽象类,其所派生的子类的使用方式十分相似,但显示特征有所不同。AdapterView具有以下特征。 AdapterView继承了ViewGroup,其本质上是容器。 AdapterView可以包括多个“列表项”,并将“列表项”以合适的形式显示出来。 AdapterView所显示的“列表项”是由Adapter提供的,通过AdapterView的setAdapter()方法来设置Adapter适配器。 AdapterView及其子类的继承关系如图519所示。 图519AdapterView及其子类的继承关系 由图519可以看出,从AdapterView派生出以下三个子类: AbsListView、AbsSpinner和AdapterViewAnimator; 这些子类依然是抽象类,在实际运用时需要使用这些类的子类,如GridView、ListView、Spinner等,具体如下。 ListView——列表类型。 Spinner——下拉列表,用于为用户提供选择。 Gallery——缩略图,已经被ScrollView和ViewPicker所取代,但有时也会用到,多用于将子项以中心锁定、水平滚动的列表。 GridView——网格图,以表格形式显示资源,并允许左右滑动。 注意通常将ListView、GridView、Spinner和Gallery等AdapterView子类作为容器,然后使用Adapter为容器提供“列表项”,AdapterView负责采用合适的方式显示这些列表项。 2. Adapter组件 Adapter是一个接口,ListAdapter和SpinnerAdapter是Adapter的子接口。其中,ListAdapter为AbsListView提供列表项; 而SpinnerAdapter为AbsSpinner提供列表项。Adapter接口、子接口以及实现类的关系如图520所示。 图520Adapter接口、子接口以及实现类的关系 大多数Adapter实现类都继承自BaseAdapter类,而BaseAdapter类实现了ListAdapter和SpinnerAdapter接口,因此BaseAdapter及其子类可以为AbsListView和AbsSpinner提供列表项。 Adapter的常用子接口及实现类介绍如下。 ListAdapter接口继承于Adapter接口,是ListView和List数据集合之间的桥梁。ListView组件能够显示由ListAdapter所包装的任何数据。 BaseAdapter抽象类是一个能够在ListView和Spinner中使用的Adapter类的父类。提供扩展BaseAdapter可以对各列表项进行最大限度的定制。 SimpleCursorAdapter类适用于简单的纯文字型ListView,需要将Cursor字段和View中列表项的ID对应起来,如需要实现更复杂的UI可以通过重写其方法来实现。 ArrayAdapter类是简单易用的Adapter,通常用于将数组或List集合包装成多个列表项。 SimpleAdapter类是一种简单Adapter,可以将静态数据在View组件中显示。开发人员可以把List集合中的数据封装为一个Map泛型的ArrayList。ArrayList的列表项与List集合中的数据相对应。SimpleAdapter功能强大,使用较为广泛。 注意Adapter对象扮演着桥梁的角色,通过桥梁连接着AdapterView和所要显示的数据。Adapter提供了一个连通数据项的途径,将数据集呈现到View中。 5.3.2ListView列表视图 ListView列表视图是以垂直列表的形式显示所有列表项,在手机应用中使用比较广泛。ListView通常具有以下两个职责。 将数据填充到布局,以列表的方式来显示数据。 处理用户的选择、单击等操作。 通常创建ListView有以下两种方式。 直接使用ListView进行创建。 使用Activity继承ListActivity,实现ListView对象的获取。 ListView常用的XML属性如表53所示。 表53ListView常用的XML属性 XML属性功 能 描 述 android:divider设置列表的分隔条(既可以用颜色分隔,也可以用Drawable分隔) android:dividerHeight用来指定分隔条的高度 android:entries指定一个数组资源(例如List集合或String集合),Android将根据该数组资源生成ListView android:footerDividersEnabled默认为true; 当设为false时,ListView将不会在各个footer之间绘制分隔条 android:headerDividersEnabled默认为true; 当设为false时,ListView将不会在各个header之间绘制分隔条 ListView从AbsListView中继承的XML属性如表54所示。 表54AbsListView常用的XML属性 XML属性功 能 描 述 android:cacheColorHint用于设置该列表的背景始终以单一、固定的颜色绘制,可以优化绘制过程 android:choiceMode为视图指定选择的行为,可选的类型如下。 none: 不显示任何选中项。 singleChoice: 允许单选。 multipleChoice: 允许多选。 multipleChoiceModal: 允许多选 android:drawSelectorOnTop默认为false; 如果为true,选中的列表项将会显示在上面 android:fastScrollEnabled用于设置是否允许使用快速滚动滑块; 如果设为true,则将会显示滚动图标,并允许用户拖动该滚动图标进行快速滚动 android:listSelector设置选中项显示的可绘制对象,可以是图片或者颜色属性 android:scrollingCache设置在滚动时是否使用绘制缓存,默认为true。如果为true,则将使滚动显示更快速,但会占用更多内存 android:smoothScrollbar默认该属性为true,列表会使用更精确的基于条目在屏幕上的可见像素高度的计算方法。如果适配器需要绘制可变高度的选项,此时应该设为false android:stackFromBottom设置GridView或ListView是否将列表项从底部开始显示 android:textFilterEnabled设置是否对列表项进行过滤; 当设为true时,列表会对结果进行过滤 android:transcriptMode设置该组件的滚动模式,该属性支持如下值。 disabled: 关闭滚动,默认值。 normal: 当新条目添加进列表中并且已经准备好显示的时候,列表会自动滑动到底部以显示最新条目。 alwaysScroll: 列表会自动滑动到底部,无论新条目是否已经准备好显示 如果想对ListView的外观、行为进行定制,需要将ListView作为AdapterView来使用,通过Adapter来控制每个列表的外观和行为。 在ListView中,每个Item子项既可以是一个字符串,也可以是一个组合控件。通常而言,使用ListView需要完成以下步骤。 (1) 准备ListView所要显示的数据。 (2) 使用数组或List集合存储数据。 (3) 创建适配器,作为列表项数据源。 (4) 将适配器对象添加到ListView,并进行展示。 对于简单的List列表,直接使用ArrayAdapter将数据显示到ListView中。如果列表中的内容比较复杂,就需要使用自定义布局来实现List列表。接下来分别演示并介绍ListView的使用场景。 1. 通过继承ListActivity实现ListView 通过继承ListActivity类可以实现ListView。ListActivity是Android中常用的布局组件之一,通常用于显示可以滚动的列表项。ListActivity默认布局是由一个位于屏幕中心的全屏列表构成(默认ListView占满全屏),该ListView组件本身默认的ID为@id/android:list,所以在onCreate()方法中不需要调用setContentView()方法进行设置布局,而且直接调用getListView()即可以获取系统默认的ListView组件并进行使用。 下述代码通过继承ListActivity实现一个简单的ListView组件。 【案例519】ListViewSimpleDemoActivity.java public class ListViewSimpleDemoActivity extends ListActivity { //数据源列表 private String[] mListStr = { "姓名: 张三", "性别: 男", "年龄: 25", "居住地: 青岛","邮箱: ZhangSan@163.com" }; ListView mListView = null; @Override protected void onCreate(Bundle savedInstanceState) { //获取系统默认的ListView组件 mListView =getListView(); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListStr)); mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(ListVewSimpleDemoActivity.this, "您选择了" + mListStr[position], Toast.LENGTH_LONG).show(); } }); //设置ListView作为显示 super.onCreate(savedInstanceState); } } 上述代码中,ListViewSimpleDemoActivity继承了ListActivity类,使用ListActivity中默认的布局及ListView组件,因此无须定义该Activity的布局文件,也无须调用setContentView()方法设置布局,直接调用getListView()获取系统默认的ID为@id/android:list的ListView组件即可。除此之外,代码中定义了一个ArrayAdapter对象,并通过ListActivity的setListAdapter()方法将其设为ListView的适配器对象。 运行上述代码,界面效果如图521所示。 图521简单的ListView视图 如果需要在ListActivity中显示其他组件,如文本框和按钮等组件,可以采用如下步骤。 (1) 先定义Activity的布局文件,在布局UI界面时先增加其他组件,再添加一个ListView组件用于展示数据。 (2) 在Activity中通过setContentView()方法来添加布局对象。 创建ListActivity的布局文件,代码如下。 【案例520】listview_demo.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ...> <!-- 添加按钮 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <EditText android:id="@+id/addTxt" android:layout_width="212dp" android:layout_height="wrap_content" > </EditText> <Button android:id="@+id/addBtn" android:layout_width="83dp" android:layout_height="wrap_content" android:text="添加" > </Button> </LinearLayout> <!-- 自定义的ListView --> <ListView android:id="@id/android:list " android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:drawSelectorOnTop="false" /> </LinearLayout> 注意通过继承ListActivity来实现ListView时,当用户也定义了一个ID为@id/android:list的ListView,与ListActivity中的默认ListView组件ID一致,则使用setContentView()方法可以指定用户定义的ListView作为ListActivity的布局,否则会使用系统提供的ListView作为ListActivity的布局。 下述代码创建一个Activity来加载自定义的XML布局文件。 【案例521】ListViewDemoActivity.java public class ListViewDemoActivity extends ListActivity { //数据源列表 private String[] mListStr = { "姓名: 张三", "性别: 男", "年龄: 25", "居住地: 青岛","邮箱: ZhangSan@163.com" }; ListView mListView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //设置Activity的布局 setContentView(R.layout.listview_demo); //获取ID为android:list的ListView组件 mListView =getListView(); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListStr)); mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(ListVewDemoActivity.this, "您选择了" + mListStr[position], Toast.LENGTH_LONG).show(); } }); } } 运行上述代码,结果如图522所示。 图522继承ListActivity实现自定义ListView 2. 在AppCompatActivity中使用自定义的ListView 首先,修改listview_demo.xml文件,将ListView组件中的ID修改为用户自定义的字段,代码如下。 【案例522】listview_demo.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" //省略 <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:drawSelectorOnTop="false" /> </LinearLayout> 下面创建一个Activity来加载上述自定义的XML布局文件,代码如下。 【案例523】ListViewDemoActivity.java public class ListViewDemoActivity extends AppCompatActivity{ //数据源列表 private String[] mListStr = { "姓名: 张三", "性别: 男", "年龄: 25", "居住地: 青岛","邮箱: zhangsan@163.com" }; ListView mListView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //设置Activity的布局 setContentView(R.layout.listview_demo); //获取ID为listview的ListView组件 mListView = (ListView) findViewById(R.id.listview); mListView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListStr)); mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(ListVewDemoActivity.this, "您选择了" + mListStr[position], Toast.LENGTH_LONG).show(); } }); }} 图523ListView视图 上述代码中,ListViewDemoActivity与ListViewSimpleDemoActivity基本相同,不同之处在于: ListViewSimpleDemoActivity没有调用setContentView()方法,而ListViewDemoActivity使用listview_demo.xml布局文件来渲染整个布局。 运行ListVewDemoActivity,界面效果如图523所示。 3. 复杂ListView的使用 前面介绍的两个例子都只展示文本行,在实际应用中图文混排也是较常见的,即在行中既包括文字又包括图片。图文混排功能需要用户根据需求来自定义Adapter适配器。通常实现图文混排的步骤如下。 (1) 定义行选项的布局格式。 (2) 自定义一个Adapter,并重写其中的关键方法,如getCount()、getView()等方法。 (3) 注册列表选项的单击事件。 (4) 创建Activity并加载对应的布局文件。 下述代码通过上述步骤来完成一个图文混排的列表案例。新建行选项的布局文件item.xml,代码如下。 【案例524】item.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"...> <TextView android:id="@+id/itemTxt".../> <ImageView android:id="@+id/itemImg" android:layout_alignParentRight="true" android:layout_marginRight="10dp" .../> </RelativeLayout> 上述代码主要定义一个TextView和一个ImageView,用于显示列表的每行中的文本和图片。 然后,创建一个自定义的TextImageAdapter,代码如下。 【案例525】TextImageAdapter.java public class TextImageAdapter extends BaseAdapter { private Context mContext; //展示的文字 private List<String> texts; //展示的图片 private List<Integer> images; public TextImageAdapter(Context context, List<String> texts, List<Integer> images) { this.mContext = context; this.texts = texts; this.images = images; } /** * 元素的个数 */ public int getCount() { return texts.size(); } public Object getItem(int position) { return null; } public long getItemId(int position) { return 0; } //用以生成在ListView中展示的一个View元素 public View getView(int position, View convertView, ViewGroup parent) { //优化ListView if (convertView == null) { convertView = LayoutInflater.from(mContext) .inflate(R.layout.item,null); ItemViewCache viewCache = new ItemViewCache(); viewCache.mTextView = (TextView) convertView .findViewById(R.id.itemTxt); viewCache.mImageView = (ImageView) convertView .findViewById(R.id.itemImg); convertView.setTag(viewCache); } ItemViewCache cache = (ItemViewCache) convertView.getTag(); //设置文本和图片,然后返回这个View,用于ListView的Item的展示 cache.mTextView.setText(texts.get(position)); cache.mImageView.setImageResource(images.get(position)); return convertView; } //元素的缓冲类,用于优化ListView private class ItemViewCache { public TextView mTextView; public ImageView mImageView; } } 上述代码中创建了TextImageAdapter类,用于进行数据的适配与展示,其中该类继承了BaseAdapter,由于BaseAdapter已经实现了Adapter的大部分方法,因此在TextImageAdapter中只需要实现所需要的部分即可,例如,getCount()方法和getView()方法; getCount()方法用于返回ListView中文本元素的数量,getView()方法用于生成所要展示的View对象。在ListView中,每添加一个View就会调用一次Adapter的getView()方法,所以有必要对该方法进行优化,上面例子中通过自定义ItemViewCache类实现了部分优化。 创建Activity的布局文件,代码如下。 【案例526】listview_image.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"...> <ListView android:id="@+id/list_image".../> </LinearLayout> 接下来创建一个用于展现图文混排的Activity,并注册单击选择项的事件,代码如下。 【案例527】ListVewImageDemoActivity.java public class ListVewImageDemoActivity extends AppCompatActivity { //展示的文字 private String[] texts=new String[]{"樱花","小鸡","坚果"}; //展示的图片 private int[] images=new int[]{R.drawable.cherry_blossom, R.drawable.chicken,R.drawable.chestnut}; ListView mListView = null; @Override protected void onCreate(Bundle savedInstanceState) { //设置ListView作为显示 super.onCreate(savedInstanceState); //设置Activity布局 setContentView(R.layout.listview_image); //获取ID为list_image的ListView组件 mListView=(ListView)findViewById(R.id.list_image); //加载适配器 TextImageAdapter adapter = new TextImageAdapter(this, texts, images); mListView.setAdapter(adapter); mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(ListVewImageDemoActivity.this, "您选择了" + texts[position], Toast.LENGTH_LONG).show(); } }); } } 上述代码中,首先通过ListActivity提供的getListView()方法来获取ListView对象,然后创建一个TextImageAdapter适配器对象,并作为setListAdapter()方法的传入参数,从而把ListView和Adapter对象进行绑定; 最后通过定义内部监听器来实现ListView中选择项的单击事件。 运行上述代码,界面运行效果如图524所示。 图524图文混排效果 注意上述几个案例主要介绍了ListView的常用功能,限于篇幅还有很多功能没能介绍,例如ListView的分隔部分、headView、footView及ListView的分页等,请参考其他资料。 5.3.3GridView网格视图 GridView用于按行和列的分布方式来显示多个组件。GridView与ListView拥有相同的父类AbsListView,因此两者有许多相同之处,唯一的区别在于: ListView只显示一列,GridView可以显示多列。从这个角度看,ListView可以视为一个特殊的GridView,当GridView只显示一列时,GridView就变成了ListView。GridView也需要通过Adapter来提供显示数据。 GridView常用的XML属性如表55所示。 表55GridView常用的XML属性 XML属性功 能 描 述 android:numColumns设置列数,可以设置自动,如auto_fit android:columnWidth设置每一列的宽度 android:stretchMode设置拉伸模式。 none: 拉伸被禁用,不允许被拉伸。 spacingWidth: 列与列之间的间距会被拉伸; 因此使用该拉伸模式时,必须指定columnWidth,而指定horizontalSpacing就会使columnWidth无效: 每列的宽度相等,只需要指定numColumns和horizontalSpacing属性。 spacingWidthUniform: 每列的间距均被拉伸。当拉伸被禁用时不可以被拉伸 android:verticalSpacing设置各个元素之间的垂直边距 android:horizontalSpacing设置各个元素之间的水平边距 在使用GridView时,一般都需要为其指定numColumns属性,否则numColumns默认为1; 当numColumns属性设置为1时,意味着该GridView只有1列,此时功能与ListView相同。在实际开发中,创建GridView的过程与ListView相似,步骤如下。 (1) 在布局文件中使用<GridView>元素来定义GridView组件。 (2) 自定义一个Adapter,并重写其中的关键方法,如getCount()、getView()等方法。 (3) 注册列表选项的单击事件。 (4) 创建Activity并加载对应的布局文件。 下面通过一个简单示例演示GridView的用法。新建布局文件gridview_demo.xml,代码如下。 【案例528】gridview_demo.xml <?xml version="1.0" encoding="utf-8"?> <GridView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/gridview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:columnWidth="90dp" android:gravity="center" android:horizontalSpacing="10dp" android:numColumns="auto_fit" android:stretchMode="columnWidth" android:verticalSpacing="10dp" > </GridView> 上述代码比较简单,整个布局文件中只有一个GridView。通过columnWidth属性设置列宽为90dp; 将属性numColumns设为auto_fit,Android会自动计算手机屏幕的大小以决定每行展示几个元素; 将属性stretchMode设为columnWidth则根据列宽自动缩放; horizontalSpacing属性用于定义列之间的间隔; verticalSpacing用于定义行之间的间隔。 然后,自定义一个Adapter适配器,用于适配GridView,代码如下。 【案例529】ImageAdapter.java public class ImageAdapter extends BaseAdapter { private Context mContext; //一组Image的Id private int[] mThumbIds; public ImageAdapter(Context context) { this.mContext = context; } @Override public int getCount() { return mThumbIds.length; } @Override public Object getItem(int position) { return mThumbIds[position]; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { //定义一个ImageView,显示在GridView里 ImageView imageView; if (convertView == null) { imageView = new ImageView(mContext); imageView.setLayoutParams(new GridView.LayoutParams(200, 200)); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setPadding(8, 8, 8, 8); } else { imageView = (ImageView) convertView; } imageView.setImageResource(mThumbIds[position]); return imageView; } } 上述代码中采用了自定义Adapter的方式,与前面自定义Adapter的方式相似,以“九宫格”的方式展示图片,每幅图片大小为200×200px。 最后,创建GridViewDemoActivity,并加载相应的布局文件,用于显示使用GridView布局的界面,代码如下。 【案例530】GridViewDemoActivity.java public class GridViewDemoActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.gridview_demo); GridView gridView=(GridView)findViewById(R.id.gridview); ImageAdapter imageAdapter = new ImageAdapter(this,mThumbIds); gridView.setAdapter(imageAdapter); //单击GridView元素的响应 gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { //弹出单击的GridView元素的位置 Toast.makeText(GridViewDemoActivity.this,mThumbIds[position], Toast.LENGTH_SHORT).show(); } }); } //展示图片 private int[] mThumbIds = { R.drawable.flg_1, R.drawable.flg_2, R.drawable.flg_3, R.drawable.flg_4, R.drawable.flg_5, R.drawable.flg_6, R.drawable.flg_7, R.drawable.flg_8, R.drawable.flg_9 }; } 上述代码中,定义了一组国旗图片,并通过setOnItemClickListener()方法实现了gridView中的图片单击事件,当单击一个图片时会显示该图片所存储的位置。 运行上述代码,界面运行效果如图525所示。 图525九宫格 5.3.4TabHost TabHost可以很方便地在窗口中放置多个选项卡,每个选项卡所显示的区域与其外部容器大小相同,通过叠放选项卡可以在容器中放置更多组件。TabHost是一种比较实用的组件,在应用中比较常见,例如,手机的通话记录中包括“未接电话”“已接电话”等选项卡。 在使用TabHost时,通常需要与TabWidget、TabSpec组件结合使用,具体功能如下。 TabWidget组件用于显示TabHost中上部和下部的按钮,单击按钮时切换选项卡。 TabSpec代表选项卡界面,通过将TabSpec添加到TabHost中实现选项卡的添加。 TabHost仅仅是一个简单的容器,通过以下方法来创建、添加选项卡。 newTabSpec(String tag)方法用于创建选项卡。 addTab(tabSpec)方法用于添加选项卡。 使用TabHost有两种形式: 继承TabActivity和不继承TabActivity。 1. 继承TabActivity使用TabHost 当继承TabActivity时,使用TabHost的步骤如下。 (1) 定义布局——在XML文件中使用TabHost组件,并在其中定义一个FrameLayout选项卡内容。 (2) 创建TabActivity——用于显示选项卡组件的Activity,需要继承TabActivity。 (3) 获取组件——通过getTabHost()方法获取TabHost对象。 (4) 创建选项卡——通过TabHost来创建一个选项卡。 下述代码通过一个简单示例演示TabHost的用法。新建布局文件tabhost_demo1.xml,代码如。 【案例531】tabhost_demo1.xml <?xml version="1.0" encoding="utf-8"?> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabhost"...> <LinearLayout android:orientation="vertical" ...> <TabWidget android:id="@android:id/tabs" android:orientation="horizontal" .../> <FrameLayout android:id="@android:id/tabcontent" android:layout_weight="1" ...> <LinearLayout android:id="@+id/content1" android:orientation="vertical" ...> <TextView android:text="内容1" .../> </LinearLayout> <LinearLayout android:id="@+id/content2" android:orientation="vertical" ...> <TextView android:text="内容2" .../> </LinearLayout> <LinearLayout android:id="@+id/content3" android:orientation="vertical" ...> <TextView android:text="内容3" .../> </LinearLayout> </FrameLayout> </LinearLayout> </TabHost> 上述布局文件解释如下。 布局文件中的根元素为TabHost,其中其ID必须引用Android系统自带的ID,即android:id=@android:id/tabhost。 使用TabHost一定要有TabWidget和FramLayout两个控件。 TabWidget必须使用系统ID,即@android:id/tabs。 FrameLayout作为标签内容的基本框架,也必须使用系统ID,即@android:id/tabcontent。 接下来创建TabHostDemo1Activity,代码如下。 【案例532】TabHostDemo1Activity.java public class TabHostDemo1Activity extends TabActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tabhost_demo1); TabHost tabHost = getTabHost(); //添加第1个标签 TabSpec page1 = tabHost.newTabSpec("tab1")//创建新标签 .setIndicator("标签1")//设置标签内容 .setContent(R.id.content1); tabHost.addTab(page1); //添加第2个标签 TabSpec page2 = tabHost.newTabSpec("tab2") .setIndicator("标签2") .setContent(R.id.content2); tabHost.addTab(page2); //添加第3个标签 TabSpec page3 = tabHost.newTabSpec("tab3") .setIndicator("标签3") .setContent(R.id.content3); tabHost.addTab(page3); } } 上述代码解释如下。 图526继承TabActiviy使用 TabHost 通过调用从TabActivity继承而来的getTabHost()方法来获取布局文件中的TabHost组件。 调用TabHost组件的newTabSpec(tag)方法创建一个选项卡,其中,参数tag是一个字符串,即选项卡的唯一标识。 使用TabHost.TabSpec的setIndicator()方法来设置新选项卡的名称。 使用TabHost.TabSpec的setContent()方法来设置选项卡的内容,可以是视图组件、Activity或Fragment。 使用tabHost.add(tag)方法将选项卡添加到TabHost组件中,其中,传入的tag参数是选项卡的唯一标识。 运行上述代码,界面效果如图526所示。 在实际应用中,有时会改变选项卡标签的高度,在代码中通过getTabWidget()方法来获取TabWidget对象,然后使用该对象的getChildAt()方法来获得指定的标签,最后对该标签中内容的位置进行设置,代码如下。 【示例】改变选项卡标签的高度 TabWidget mTabWidget = tabHost.getTabWidget(); for (int i = 0; i < mTabWidget.getChildCount(); i++) { //设置选项卡的高度 mTabWidget.getChildAt(i).getLayoutParams().height = 50; //设置选项卡的宽度 mTabWidget.getChildAt(i).getLayoutParams().width = 60; } 2. 不继承TabActivity使用TabHost 当不继承TabActivity时,使用TabHost的步骤如下。 (1) 定义布局——在XML文件中使用TabHost组件。 (2) 创建TabActivity——用于显示选项卡组件的Activity,需要继承TabActivity。 (3) 获取组件——通过findViewById()方法获取TabHost对象。 (4) 创建选项卡——通过TabHost来创建一个选项卡。 新建布局文件tabhost_demo2.xml,代码如下。 【案例533】tabhost_demo2.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"...> <TabHost android:id="@+id/tabHost" android:layout_weight="1" ...> <LinearLayout android:orientation="vertical" ...> <TabWidget android:id="@android:id/tabs" ...></TabWidget> <FrameLayout android:id="@android:id/tabcontent" android:layout_weight="1" ...> <LinearLayout android:id="@+id/content_1" android:orientation="vertical" ...> <TextView android:id="@+id/textView" android:textSize="25sp" android:text="内容1" .../> </LinearLayout> <LinearLayout android:id="@+id/content_2" android:orientation="vertical"...> <TextView android:id="@+id/textView2" android:textSize="25sp" android:text="内容2".../> </LinearLayout> <LinearLayout android:id="@+id/content_3" android:orientation="vertical" ...> <TextView android:id="@+id/textView3" android:textSize="25sp" android:text="内容3" .../> </LinearLayout> </FrameLayout> </LinearLayout> </TabHost> </LinearLayout> 布局文件tabhost_demo2.xml与tabhost_demo1.xml相比,TabHost组件的ID是用户自定义的ID,不再使用Android系统自带的ID。 然后,创建TabHostDemo2Activity,代码如下。 【案例534】TabHostDemo2Activity.java public class TabHostDemo2Activity extends AppCompatActivity{ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tabhost_demo2); TabHost tabHost = (TabHost) findViewById(R.id.tabHost); tabHost.setup(); tabHost.addTab(tabHost.newTabSpec("tab1").setIndicator("标签1") .setContent(R.id.content_1)); tabHost.addTab(tabHost.newTabSpec("tab2").setIndicator("标签2") .setContent(R.id.content_2)); tabHost.addTab(tabHost.newTabSpec("tab3").setIndicator("标签3") .setContent(R.id.content_3)); //设置选项卡的高度和宽度 TabWidget mTabWidget = tabHost.getTabWidget(); for (int i = 0; i < mTabWidget.getChildCount(); i++) { //设置选项卡的高度 mTabWidget.getChildAt(i).getLayoutParams().height = 80; //设置选项卡的宽度 mTabWidget.getChildAt(i).getLayoutParams().width = 60; } } } 与代码TabHostDemo1Activity相比,获取TabHost组件不再使用getTabHost()方法,而是使用findViewById()方法来获取。使用addTab()方法来添加选项卡,使用newTabSpec(tag)方法创建一个选项卡。运行上述代码,界面如图527所示。 图527不继承TabActivity使用TabHost 注意TabActivity在Android 3.0以后已过时,推荐使用“不继承TabActivity的方式”使用TabHost。 小结 (1) Fragment允许将Activity拆分成多个完全独立封装的可重用的组件,每个组件拥有自己的生命周期和UI布局。 (2) 创建Fragment需要实现三个方法: onCreate()、onCreateView()和onPause()。 (3) Fragment的生命周期与Activity的生命周期相似,具有以下状态: 活动状态、暂停状态、停止状态和销毁状态。 (4) Android中提供的菜单有选项菜单、子菜单、上下文菜单和图标菜单等。 (5) 在Android中提供了一种高级控件,其实现过程就类似于MVC架构,该控件就是AdapterView。 (6) ListView(列表视图)是手机应用中使用非常广泛的组件,以垂直列表的形式显示所有列表项。 (7) GridView用于在界面上按行、列分布的方式显示多个组件。 (8) GridView与ListView拥有相同的父类AbsListView,且都是列表项,两者唯一的区别在于: ListView只显示一列,GridView可以显示多列。 习题 1. 在Android中使用Menu时可能需要重写的方法有。(多选) A. onCreateOptionsMenu() B. onCreateMenu() C. onOptionsItemSelected() D. onItemSelected() 2. 自定义Adapter需要重写下列方法中的。(多选) A. getCount() B. getItem() C. getItemId() D. getView() 3. 下面自定style的方式正确的是。 A. <resources> <style name="myStyle"> <item name="android:layout_width">match_parent</item> </style> </resources> B. <style name="myStyle"> <item name="android:layout_width">match_parent</item> </style> C. <resources> <item name="android:layout_width">match_parent</item> </resources> D. <resources> <style name="android:layout_width">match_parent</style> </resources> 4. 通过使用ListView来完成用户的列表功能,并且在每个item上显示用户的删除和更新按钮,同时通过测试数据的方式实现对应的业务功能。 5. 使用GridView网格显示3行3列图片。