第十二章 对话框
一、创建DialogFragment
建议将AlertDialog封装在DialogFragment(Fragment的子类)实例中使用。当然,不使用DialogFragment也可以显示AlertDialog视图,但是不推荐这样做。使用FragmentManager管理对话框,可以更加灵活的显示对话框。
另外,如果旋转设备,单独使用的AlertDialog会消失,而封装在fragment中的AlertDialog则不会有此问题(旋转后对话框会被重建恢复)。
DialogFragment类有这个方法:public Dialog onCreateDialog(Bundle savedInstanceState),为了在屏幕上显示DialogFragment,托管activity的FragmentManager会调用这个方法。在继承DialogFragment的子类中重写此方法,之后创建AlertDialog。
要将DialogFragment添加给FragmentManager管理并放置在屏幕上,可调用fragment实例的两个方法:public void show(FragmentManager manager , String tag);public void show(FragmentTransaction transaction , String tag)。String参数可唯一识别FragmentManager队列中的DialogFragment。两个方法都可用,如果使用第二个方法,你自己负责创建并提交事务;如果使用第一个方法,系统会自己创建并提交事务。
二、Fragment之间的数据传递
假设两个Fragment:CrimeFragment和DatePickerFragment。
CrimeFragment要将日期传递给DatePickerFragment,可以创建一个newInstance(Date)方法,将Date作为argument附加给fragment。
DatePickerFragment将新日期返回给CrimeFragment,需要将日期打包给extra并附加到 Intent 上,然后调用CrimeFragment.onActivityResult(......)方法,并传入准备好的参数。
我们可以看到,我们没有调用托管activity的Activity.onActivityResult(......) 方法,而是调用了 Fragment.onActivityResult(......) 方法。
1、传递数据给 DatePickerFragment
要传递数据给 DatePickerFragment,需要将数据保存在 DatePickerFragment 的argument bundle 中,DatePickerFragment就能直接获取它。
2、返回数据给 CrimeFragment
要让CrimeFragment接收 DatePickerFragment 返回的数据,首先需要清除它们之间的关系。
如果是Activity之间的回传,我们调用startActivityForResult(......)方法,ActivityManager 负责跟踪管理activity父子关系。回传数据后,子activity被销毁,但ActivityManager知道接收数据的是哪个activity。
(1)设置目标Fragment
类似于activity之间的关联,可将 CrimeFragment 设置为 DatePickerFragment 的目标fragment。这样,在CrimeFragment和DatePickerFragment被销毁重建后,系统会重新关联它们。调用如下 Fragment 方法可建立这种关联:public void setTargetFragment (Fragment fragment ,int requestCode)。
该方法的两个参数:目标fragment和请求代码,由FragmentManager负责跟踪管理,我们可调用fragment(设置目标fragment 的 fragment)的 getTargetFragment() 方法和 getTargetRequestCode() 方法获取它们。
(2)传递数据给目标Fragment
让 DatePickerFragment 类调用 CrimeFragment.onActivityResult(int,int,Intent)方法。
处理由同一activity托管的两个fragment间的数据返回时,可借用 Fragment.onActivityResult(......) 方法:
private void sendResult(int resultCode, Date date) {
if (getTargetFragment() == null) {
return;
}
Intent intent = new Intent();
intent.putExtra(EXTRA_DATE, date);
getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, intent);
}
之后在 CrimeFragment 中,覆盖 onActivityResult(......) 方法,从Intent 中获取数据。
第十三章 工具栏
一、AppCompat
1、使用AppCompat库
如果想给老项目添加AppCompat库,需要:
- 添加AppCompat依赖项
- 使用一种AppCompat主题
- 确保所有Activity都是AppCompatActivity子类
AppCompat库自带以下三种主题:
- Theme.AppCompat:黑色主题
- Theme.AppCompat.Light:浅色主题
- Theme.AppCompat.Light.DarkActionBar:带黑色工具栏的浅色主题
应用级别的主题设置在 AndroidManifest.xml 文件中进行。
二、工具栏菜单
工具栏菜单由菜单项组成,它占据着工具栏的右上方区域。菜单项的操作应用于当前屏幕,甚至整个应用。
1、在xml文件中定义菜单
菜单是一种类似于布局的资源。创建菜单定义文件并将其放置在res/menu目录下,Android会自动生成相应的资源ID。随后,在代码中实例化菜单时,就可以直接使用。
菜单文件:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/new_crime"
android:icon="@android:drawable/ic_menu_add"
android:title="@string/new_crime"
app:showAsAction="ifRoom|withText"/>
</menu>
showAsAction属性用于指定菜单项是显示在工具栏上,还是隐藏于溢出菜单。该属性当前设置为 ifRoom 和 withText 的组合值。因此,只要空间足够,菜单项图标及其文字描述都会显示在工具栏上。如果空间仅够显示菜单项图标,文字描述就不会显示。如果空间大小不够显示任何项,菜单项就会隐藏到溢出菜单中。如果溢出菜单包含其他项,它们就会以三个点表示(位于工具栏最右端)。
属性 showAsAction 还有另外两个可选值:always 和 never。不推荐使用always,应尽量使用 ifRoom 属性值,让操作系统决定如何显示菜单项。对于那些很少用到的菜单项,never属性值是个不错的选择。总之,为了避免用户界面混乱,工具栏上只应放置常用菜单项。
(1)app命名空间
注意,不同于常见的android命名空间声明,该菜单文件使用xmlns标签定义了全新的app命名空间。指定showAsAction属性时,就用了这个新定义的命名空间。
处于兼容性考虑,AppCompat库需要使用app命名空间。操作栏API随Android 3.0引入。为了支持各种旧系统版本设备,早期创建的AppCompat库捆绑了兼容版操作栏。这样一来,不管新旧,所有的设备都能用上操作栏。在运行Android 2.3或更早版本系统的设备上,菜单及其相应的XML文件确实是存在的,但是 android:showAsAction 属性是随着操作栏的发布才添加的。
AppCompat库不希望使用原生showAsAction属性,因此,它提供了定制版 showAsAction 属性(app:showAsAction)。
(2)使用 Android Asset Studio
应用使用的图标有两种:系统图标和项目资源图标。系统图标是Android操作系统内置的图标。android:icon的属性值@android:drawable/ic_menu_add 就引用了系统图标。
在应用原型设计阶段,使用系统图标不会有什么问题;而在应用发布时,无论用户运行什么设备,最好能统一应用的界面风格。要知道,不同设备或操作系统版本间,系统图标的显示风格差别很大。有些设备的系统图标甚至与应用的整体风格完全不搭。
一种解决方案是创建定制图标。这需要针对不同屏幕显示密度或各种可能的设备配置,准备不同版本的图标。
另一种解决方案是找到适合应用的系统图标,将它们直接复制到项目的 drawable 资源目录中。系统图标可在Android SDK的安装目录下找到。如果是Mac计算机,路径通常为/Users/user/Library/Android/sdk;如果是Windows计算机,默认的路径是\Users\user\sdk。此外,还可以打开项目结构窗口,选择SDK Location来确认SDK的具体存放路径。打开SDK目录,可找到包括ic_menu_add 在内的Android系统资源。资源的具体目录是/platforms/android-25/data/res,路径中的数字25代表Android的API级别。
还有第三种,也是最容易的解决方案:使用Android Studio内置的Android Asset Studio工具。在项目工具窗口中,右键单击drawable目录,选择new——>Image Asset菜单项。在 Icon Type一栏选 Action Bar and Tab Icons,在Name一栏输入自己定义的名称,Asset Type 处选 Clip Art,最后,更新Theme为HOLO_DARK。工具栏使用了深色系主题,那图标图像就应选浅色。
2、创建菜单
在代码中,Activity类提供了管理菜单的回调函数。需要选项菜单时,Android会调用Activity的 onCreateOptionsMenu(Menu)方法。而Fragment也有一套自己的选项菜单回调函数,以下为创建菜单和响应菜单项选择事件的两个回调方法:
- public void onCreateOptionsMenu(Menu menu,MenuInflater inflater)
- public boolean onOptionsItemSelected(MenuItem item)
Fragment.onCreateOptionsMenu(Menu,MenuInflater)方法是由FragmentManager负责调用的。因此,当activity接收到操作系统的onCreateOptionsMenu(......)方法回调请求时,我们必须明确告诉FragmentManager:其管理的fragment应接收onCreateOptionsMenu(......)方法的调用指令。要通知FragmentManager,需调用以下方法:public void setHasOptionsMenu(boolean hasMenu)
三、实现层级式导航
后退键导航又称为临时性导航,只能返回到上一次浏览过的用户界面;而层级式导航可在应用内逐级向上导航。有了层级式导航,用户可点击工具栏左边的向上按钮向上导航。
打开 AndroidManifest.xml,在 Activity 中添加 parentActivityName 属性,开启项目的层级导航。
<activity
android:name=".CrimePagerActivity"
android:parentActivityName=".CrimeListActivity">
</activity>
1、层级式导航的工作原理
用户点击向上按钮自本Activity向上导航时,如下的 intent 会被创建:
Intent intent = new Intent(this, CrimeListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
FLAG_ACTIVITY_CLEAR_TOP指示Android在回退栈中寻找指定的Activity实例。如果实例存在,则弹出栈内所有其他Activity,让启动的目标Activity出现在栈顶(显示在屏幕上)。
四、可选菜单项
有一个方法:getString(int resId,Object....formatArgs),接受字符串资源中占位符的替换值
例如:
字符串资源中有占位符:
<string name="subtitle_format">%1$d crimes</string>
方法:getString(int resId, Object...formatArgs)中resId是代表资源id,formatArgs就代表想要替换成的数据
1、切换菜单项标题
调用 onOptionsItemSelected(MenuItem)方法的时候,传入的参数是用户点击的 MenuItem。虽然可以在这个方法里更新菜单项的文字,但设备旋转并重建工具栏时,子标题的变化会丢失。
比较好的方法是在 onCreateOptionsMenu(......)方法内更新菜单项,并在用户点击子标题菜单项时重建工具栏。对于用户选择菜单项或重建工具栏的场景,都可以使用这段菜单项更新代码:
首先,新增跟踪记录子标题状态的成员变量:
private boolean mSubtitleVisible;
接着,用户点击菜单项时,在 onCreateOptionsMenu(...)方法内更新子标题,同时重建菜单项:
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_crime_list, menu);
MenuItem subtitleItem = menu.findItem(R.id.show_subtitle);
if (mSubtitleVisible) {
subtitleItem.setTitle(R.string.hide_subtitle);
} else {
subtitleItem.setTitle(R.string.show_subtitle);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.new_crime:
...
case R.id.show_subtitle:
mSubtitleVisible = !mSubtitleVisible;
getActivity().invalidateOptionsMenu();
updateSubtitle();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
最后,根据 mSubtitleVisible 变量值,联动菜单项标题与子标题:
private void updateSubtitle() {
CrimeLab crimeLab = CrimeLab.get(getActivity());
int crimeCount = crimeLab.getCrimes().size();
String subtitle = getString(R.string.subtitle_format, crimeCount);
if (!mSubtitleVisible) {
subtitle = null;
}
AppCompatActivity activity = (AppCompatActivity) getActivity();
activity.getSupportActionBar().setSubtitle(subtitle);
}
五、工具栏与操作栏
工具栏与操作栏有什么区别?
首先,两者给我们最直观的印象就是工具栏界面更美观。
除了视觉上的差异,在使用上工具栏比操作栏更灵活。比如,整个应用只能配置一个操作栏且位置及尺寸必须固定(在屏幕顶部),工具栏就没有这些限制。可以在屏幕的任何位置摆放工具栏,甚至可以在同一屏配置多个工具栏。
此外,工具栏还能支持内嵌视图和调整高度。
六、挑战练习:复数字符串资源
只有一条 crime 记录的时候,显示总记录数的子标题依然会显示 1 crimes,想要显示单数形式 1 crime。
实现思路:
首先,在strings.xml文件中定义复数字符串资源:
<plurals name="subtitle_plural">
<item quantity="one">%1$d crime</item>
<item quantity="other">%1$d crimes</item>
</plurals>
然后,使用getQuantityString方法正确处理单复数问题:
int crimeSize = crimeLab.getCrimes().size();
String subtitle = getResources()
.getQuantityString(R.plurals.subtitle_plural, crimeSize, crimeSize);
这个代码写法我测试过,但是并不管用,一条记录的时候依旧显示的是复数形式。这个还需调研。