使用AppCompat库
AndroidManifest.xml
...
<application
...
android:theme="@style/AppTheme">
...
</application>
...
res/values/styles.xml
...
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
...
</style>
...
修改 SingleFragmentActivity 和 CrimePagerActivity ,让它们直接继承 AppCompatActivity类。AppCompatActivity 恰恰就是 FragmentActivity 的子类。
在XML文件中定义菜单
右键单击res目录,New => Android resource file,Resource type选择Menu,创建资源文件fragment_crime_list.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_new_crime"
android:icon="@drawable/ic_menu_add"
android:title="@string/new_crime"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/menu_item_show_subtitle"
android:title="@string/show_subtitle"
app:showAsAction="ifRoom" />
</menu>
创建菜单
Activity 类提供了管理菜单的回调函数,需要选项菜单时,Android会调用Activity 的 onCreateOptionsMenu(Menu) 方法。按照CriminalIntent应用的设计,选项菜单相关的回调函数需在fragment而非activity里实现。Fragment 有一套自己的选项菜单回调函数。
//实例化选项菜单
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_crime_list, menu);
}
在以上方法中,调用 MenuInflater.inflate(int, Menu) 方法并传入菜单文件的资源ID,
将布局文件中定义的菜单项目填充到 Menu 实例中。然后通知FragmentManager其管理的fragment(CrimeListFragment)需接收选项菜单方法回调:
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/**
* 通知FragmentManager其管理的fragment(CrimeListFragment)需接收选项菜单方法回调
*/
setHasOptionsMenu(true);
}
响应菜单项选择
新增addCrime(Crime)方法:
CrimeLab.java
//添加新的陋习
public void addCrime(Crime crime) {
mCrimes.add(crime);
}
响应菜单项选择事件:
//响应菜单项选择事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_new_crime:
Log.d(TAG, "点击了[添加新的陋习]菜单...");
Crime crime = new Crime();
CrimeLab.get(getActivity()).addCrime(crime);
Intent intent = CrimePagerActivity.newIntent(getActivity(), crime.getId());
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
onOptionsItemSelected(MenuItem) 方法返回的是布尔值。一旦完成菜单项事件处理,应返回 true 值以表明全部任务已完成。另外,默认 case 表达式中,如果菜单项ID不存在,超类版本方法会被调用。
实现层级式导航
CriminalIntent应用主要靠后退键在应用内导航。后退键导航又称为临时性导航,只能返回到上一次浏览过的用户界面;而层级式导航(hierarchical navigation,有时又称为ancestral navigation)可在应用内逐级向上导航。
为AndroidManifest.xml添加parentActivityName属性:
...
<application
...
<activity
android:name=".CrimePagerActivity"
android:parentActivityName=".CrimeListActivity">
</activity>
</application>
...
实现[显示/隐藏子标题]菜单项
在fragment_crime_list.xml中添加新的item标签(前面已添加)。
设置工具栏子标题
CrimeListFragment.java
//设置工具栏子标题
private void updateSubtitle() {
CrimeLab crimeLab = CrimeLab.get(getActivity());
int crimeCount = crimeLab.getCrimes().size();
@SuppressLint("StringFormatMatches") String subtitle = getString(R.string.subtitle_format, crimeCount);
//实现菜单项文字与子标题的联动
if (!mSubtitleVisible) subtitle = null;
AppCompatActivity activity = (AppCompatActivity) getActivity();
activity.getSupportActionBar().setSubtitle(subtitle);
}
响应菜单单击事件
CrimeListFragment.java
//响应菜单项选择事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_new_crime:
Log.d(TAG, "点击了[添加新的陋习]菜单...");
//...
return true;
case R.id.menu_item_show_subtitle:
String msg = mSubtitleVisible ? "隐藏子标题" : "显示子标题";
Log.d(TAG, msg + "...");
mSubtitleVisible = !mSubtitleVisible;
getActivity().invalidateOptionsMenu(); //菜单项已经改变,应该重建
updateSubtitle();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
更新菜单项的文字
CrimeListFragment.java
//实例化选项菜单
@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.menu_item_show_subtitle);
if (mSubtitleVisible) subtitleItem.setTitle(R.string.hide_subtitle);
else subtitleItem.setTitle(R.string.show_subtitle);
}
一些问题
新建记录后回退子标题不刷新
在返回 CrimeListActivity 界面时,再次刷新子标题显示就能解决这个问题。也就是说,在 onResume 方法里再次调用 updateSubtitle 方法。既然 onResume 和onCreate 方法会调用 updateUI 方法,那就在 updateUI 方法里直接调用 updateSubtitle 方法。
但是使用向上按钮回退子标题显示会被重置,这是因为回退到的目标activity会被完全重建。
旋转设备显示的子标题消失
使用onSaveInstanceState(Bundle)来解决。
实现[删除此条陋习]菜单
添加删除陋习的方法
CrimeLab.java
//删除指定ID的陋习
public boolean removeCrime(UUID id) {
return mCrimes.remove(getCrime(id));
}
新建删除陋习的菜单布局
fragment_crime.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_remove_crime"
android:icon="@drawable/ic_menu_remove"
android:title="@string/remove_crime"
app:showAsAction="ifRoom|withText" />
</menu>
实例化选项菜单
CrimeFragment.java
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_crime, menu);
}
响应菜单项选择事件
CrimeFragment.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_remove_crime:
Log.d(TAG, "点击了[删除此条陋习]菜单...");
CrimeLab.get(getActivity()).removeCrime(mCrime.getId());
getActivity().finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
记得使用CrimeFragment托管活动(CrimePagerActivity)的finish()方法回退到前一个activity(CrimeListActivity)界面。
实现用于RecyclerView的空视图
新建无数据时显示的视图
list_item_empty.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/list_item_empty_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="30dp"
android:text="@string/empty_crime" />
</RelativeLayout>
新建EmptyHolder类
CrimeListFragment.java
private class EmptyHolder extends RecyclerView.ViewHolder {
private TextView mEmptyHintTextView;
public EmptyHolder(@NonNull View itemView) {
super(itemView);
mEmptyHintTextView = itemView.findViewById(R.id.list_item_empty_hint);
}
}
设置无数据时的viewType
CrimeListFragment.java
private class CrimeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int EMPTY_VIEW_TYPE = -1;
//...
}
重写getItemViewType(int)方法
CrimeListFragment.CrimeAdapter
@Override
public int getItemViewType(int position) {
if (mCrimes.size() <= 0) return EMPTY_VIEW_TYPE;
return super.getItemViewType(position);
}
同时修改getItemCount()方法,当没有数据时也要显示(要显示提示信息):
@Override
public int getItemCount() {
return mCrimes.size() > 0 ? mCrimes.size() : 1;
}
根据viewType创建不同的视图
CrimeListFragment.CrimeAdapter
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {EMPTY_VIEW
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
if (viewType == EMPTY_VIEW_TYPE) {
Log.d(TAG, "没有陋习可以显示...");
View view = layoutInflater.inflate(R.layout.list_item_empty, parent, false);
return new EmptyHolder(view);
}
Log.d(TAG, "创建了View视图,并封装到了ViewHolder中。");
View view = layoutInflater.inflate(R.layout.list_item_crime, parent, false);
return new CrimeHolder(view);
}
根据ViewHolder的类型决定是否绑定数据
CrimeListFragment.CrimeAdapter
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (viewHolder instanceof CrimeHolder) {
Log.d(TAG, "绑定了第 " + position + " 条数据。");
Crime crime = mCrimes.get(position);
((CrimeHolder)viewHolder).bindCrime(crime);
}
}