Android编程权威指南(第二版)学习笔记(十七)—— 第17章 Master-Detail 用户界面

本章介绍了如何写一个双版面 fragment 的布局,并对符合要求的设备进行适配,还介绍了回调接口的使用。
GitHub 地址:
完成17章

对平板设备来说,使用主从用户界面将会得到更好的体验,在这章我们将对其使用,传递数据的方式进行探究。

1. 增加布局灵活性

要实现双版面的布局,需要完成如下任务:

  1. 修改 SingleFragmentActivity,使其不再硬编码实例化布局
  2. 创建包含两个 fragment 容器的布局
  3. 修改 CrimeListActivity,实现在手机设备上实例化单版面布局,在平板设备上实例化双版面布局

1.1 修改抽象类 SingleFragmentActivity

在其中加入一个 protected 方法,返回 activity 需要的 ResId,这样对于继承 SingleFragmentActivity 的 activity 可以重写该函数以返回自己需要的 ResId。

@LayoutRes
protected int getLayoutResId() {
    return R.layout.activity_fragment;
}

1.2 使用别名资源

我们想让最小屏幕宽度 600dp 的设备使用双版面界面,其他的使用单版面界面,那么对于不同的设备,使用的布局就不同。要让不同的设备使用不同的布局资源,有两种方法:

  1. 让 res/layout/目录中的文件使用资源修饰符。如果想使用activity_masterdetail.xml布局文件, 就需要将activity_fragment.xml的内容复制到res/layout/activity_masterdetail.xml中,将activity_twopane.xml的内容复制到res/layout-sw600dp/activity_masterdetail.xml中。这样做最明显的缺点就是数据冗余,因为每个布局文件都要复制一份。

  2. 使用别名资源。别名资源是一种指向其他资源的特殊资源。它存放在 res/values/目录下,并按照约定定义在 refs.xml 文件中。比如在默认的 values 文件夹下面新建一个 refs.xml,然后写入代码:

    
    
        @layout/activity_fragment
    
    

    再新建一个最小宽度600dp 的 refs.xml(即在 values-sw600dp 目录下),写入双版面的 layout 资源:

    
    
        @layout/activity_twopane
    
    

    这样,在 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 这个应用来说,双版面和单版面的删除操作应该有着不同的结果,但这些行为在书上没有定义,所以我们再自己想一种解决方案,以便确立如何写接下来的补充程序。

  1. 双版面的界面下,点击删除应该要让左边的列表中去掉删除的那一项,并且详情页也要改为已存在的某一项的详情,为了方便实现,我们在这里改为已存在的第一项。如果只有最后一项并且点击了删除,那么右边应该要变成空白。
  2. 单版面的界面下,点击删除就直接删去该条记录,然后结束 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

你可能感兴趣的:(Android编程权威指南(第二版)学习笔记(十七)—— 第17章 Master-Detail 用户界面)