本章介绍了如何写一个双版面 fragment 的布局,并对符合要求的设备进行适配,还介绍了回调接口的使用。
GitHub 地址:
完成17章
对平板设备来说,使用主从用户界面将会得到更好的体验,在这章我们将对其使用,传递数据的方式进行探究。
1. 增加布局灵活性
要实现双版面的布局,需要完成如下任务:
- 修改 SingleFragmentActivity,使其不再硬编码实例化布局
- 创建包含两个 fragment 容器的布局
- 修改 CrimeListActivity,实现在手机设备上实例化单版面布局,在平板设备上实例化双版面布局
1.1 修改抽象类 SingleFragmentActivity
在其中加入一个 protected 方法,返回 activity 需要的 ResId,这样对于继承 SingleFragmentActivity 的 activity 可以重写该函数以返回自己需要的 ResId。
@LayoutRes
protected int getLayoutResId() {
return R.layout.activity_fragment;
}
1.2 使用别名资源
我们想让最小屏幕宽度 600dp 的设备使用双版面界面,其他的使用单版面界面,那么对于不同的设备,使用的布局就不同。要让不同的设备使用不同的布局资源,有两种方法:
-
让 res/layout/目录中的文件使用资源修饰符。如果想使用
activity_masterdetail.xml
布局文件, 就需要将activity_fragment.xml
的内容复制到res/layout/activity_masterdetail.xml
中,将activity_twopane.xml
的内容复制到res/layout-sw600dp/activity_masterdetail.xml
中。这样做最明显的缺点就是数据冗余,因为每个布局文件都要复制一份。 -
使用别名资源。别名资源是一种指向其他资源的特殊资源。它存放在 res/values/目录下,并按照约定定义在 refs.xml 文件中。比如在默认的 values 文件夹下面新建一个 refs.xml,然后写入代码:
<?xml version="1.0" encoding="utf-8"?> <resources> <item name="activity_masterdetail" type="layout">@layout/activity_fragment</item> </resources>
再新建一个最小宽度600dp 的 refs.xml(即在 values-sw600dp 目录下),写入双版面的 layout 资源:
<?xml version="1.0" encoding="utf-8"?> <resources> <item name="activity_masterdetail" type="layout">@layout/activity_twopane</item> </resources>
这样,在 CrimeListActivity 中只要引用
R.layout.activity_masterdetail
即可
2. Activity:Fragment 的托管者
为了保证 fragment 的独立性,即不需要了解其托管者的工作,但要想在 fragment 生命周期没有结束的时候传递数据出去,就要使用回调接口。
回调就相当于一个委托,首先 fragment 自己定义回调的接口,托管的 acitivity 来实现这个接口,接着 fragment 需要持有实现了自己定义接口的对象,以便自己可以实时调用。
对于一个回调接口而言,fragment 只要求实现这个接口的类在函数里要做的是什么,却不知道实现类到底会做什么,每个实现类有自己的方法来实现。
2.1 CrimeListFragment 的回调接口
对于 CrimeListFragment,其所能响应的就是点击列表中的某一项,那么它的回调接口定义如下:
public interface Callbacks {
void onCrimeSelected(Crime crime);
}
然后应该在需要托管的 Activity 中实现该接口,在这里是 CrimeListActivity:
// 省略 implement 以节约版面
@Override
public void onCrimeSelected(Crime crime) {
// 如果发现布局里没有包含详情 fragment 容器的 id,
// 就启动单独的 activity 用于显示详情
if (findViewById(R.id.detail_fragment_container) == null) {
Intent intent = CrimePagerActivity.newIntent(this, crime.getId());
startActivity(intent);
} else {
// 否则就将 detail 页面放到 fragment 容器中
Fragment newDetail = CrimeFragment.newInstance(crime.getId());
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_fragment_container, newDetail)
.commit();
}
}
在 CrimeListFragment 中持有实现接口的 activity 的引用,然后在生命周期末去除引用以便内存的回收
// CrimeListFragment
private Callbacks mCallbacks;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
}
// 中间的函数……
@Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
最后修改 onClick 事件,调用 mCallbacks.onCrimeSelected(Crime crime) 即可。这样以后,在双版面视图中点击列表中的某一项,在详情版面中就会显示相应的信息。
但是有一个问题,那就是在详情页(CrimeFragment)更改信息,在列表页没有任何响应,因为 CrimeListFragment 不会暂停,所以也就不会刷新,所以下一步要在 CrimeFragment 中定义回调接口, 让托管 activity 去更新 CrimeListFragment。
2.2 CrimeFragment 的回调接口
首先定义回调接口,这里想让托管者做的就是在 Crime 详情进行更新时更新列表
// CrimeFragment
private Callbacks mCallbacks;
public interface Callbacks {
void onCrimeUpdated(Crime crime);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
}
// 中间的函数……
@Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
在 CrimeListActivity 中实现该接口:
@Override
public void onCrimeUpdated(Crime crime) {
CrimeListFragment listFragment = (CrimeListFragment)
getSupportFragmentManager()
.findFragmentById(R.id.fragment_container);
listFragment.updateUI();
}
由于只要托管 CrimeFragment 的 activity 都应该实现其回调接口,所以在 CrimePagerActivity 中提供一个空的接口实现
之后在每次数据发生更改时都调用 mCallbacks.onCrimeUpdated(mCrime);
即可。书上将更新模型层也放到了一起。
3. 挑战的后遗症:删除 Crime
还记得我们在 ToolBar 那一章加入的挑战吗,就是删除一个 Crime,对于 CriminalIntent 这个应用来说,双版面和单版面的删除操作应该有着不同的结果,但这些行为在书上没有定义,所以我们再自己想一种解决方案,以便确立如何写接下来的补充程序。
- 双版面的界面下,点击删除应该要让左边的列表中去掉删除的那一项,并且详情页也要改为已存在的某一项的详情,为了方便实现,我们在这里改为已存在的第一项。如果只有最后一项并且点击了删除,那么右边应该要变成空白。
- 单版面的界面下,点击删除就直接删去该条记录,然后结束 activity。
在这里我在 CrimeFragment 的 Callbacks 接口中加入了 onCrimeDelete(Crime crime) 方法与 onCrimeAllDeleted(Crime crime) 方法,在 CrimeListActivity 中实现如下:
@Override
public void onCrimeDeleted(Crime crime) {
// 如果只是删除了一个,而还有其他的 Crime 的话,
// 就相当于选中一个 Crime,这里传过来的应该是第一个 Crime
onCrimeSelected(crime);
}
@Override
public void onCrimeAllDeleted(Crime crime) {
// 如果全部删除,就直接将该 fragment 移去
CrimeFragment fragment = (CrimeFragment)
getSupportFragmentManager()
.findFragmentById(R.id.detail_fragment_container);
if (fragment != null) {
getSupportFragmentManager()
.beginTransaction()
.remove(fragment)
.commit();
}
// 并且更新列表页
onCrimeUpdated(crime);
}
在 CrimePagerActivity 中也要实现这两个方法,但是对于这个 activity 来说只要进行 finish() 即可。
在删除按钮的选中监听中:
CrimeLab.get(getActivity()).deleteCrime(mCrime);
if (CrimeLab.get(getActivity()).getCrimes().isEmpty()) {
mCallbacks.onCrimeAllDeleted(mCrime);
} else {
mCrime = CrimeLab.get(getActivity()).getCrimes().get(0);
mCallbacks.onCrimeDeleted(mCrime); // 这里相当于选中第一个
updateCrime(); // 这里面升级了数据层并且更新了列表
}
return true;
整个程序就此完成啦~
GitHub Page: kniost.github.io
简书:http://www.jianshu.com/u/723da691aa42
网友评论