本章介紹了如何寫一個雙版面 fragment 的布局,并對符合要求的設(shè)備進(jìn)行適配,還介紹了回調(diào)接口的使用胞谈。
GitHub 地址:
完成17章
對平板設(shè)備來說填具,使用主從用戶界面將會得到更好的體驗,在這章我們將對其使用艰额,傳遞數(shù)據(jù)的方式進(jìn)行探究澄港。
1. 增加布局靈活性
要實現(xiàn)雙版面的布局,需要完成如下任務(wù):
- 修改 SingleFragmentActivity柄沮,使其不再硬編碼實例化布局
- 創(chuàng)建包含兩個 fragment 容器的布局
- 修改 CrimeListActivity回梧,實現(xiàn)在手機設(shè)備上實例化單版面布局,在平板設(shè)備上實例化雙版面布局
1.1 修改抽象類 SingleFragmentActivity
在其中加入一個 protected 方法祖搓,返回 activity 需要的 ResId狱意,這樣對于繼承 SingleFragmentActivity 的 activity 可以重寫該函數(shù)以返回自己需要的 ResId。
@LayoutRes
protected int getLayoutResId() {
return R.layout.activity_fragment;
}
1.2 使用別名資源
我們想讓最小屏幕寬度 600dp 的設(shè)備使用雙版面界面拯欧,其他的使用單版面界面详囤,那么對于不同的設(shè)備,使用的布局就不同镐作。要讓不同的設(shè)備使用不同的布局資源藏姐,有兩種方法:
讓 res/layout/目錄中的文件使用資源修飾符。如果想使用
activity_masterdetail.xml
布局文件该贾, 就需要將activity_fragment.xml
的內(nèi)容復(fù)制到res/layout/activity_masterdetail.xml
中包各,將activity_twopane.xml
的內(nèi)容復(fù)制到res/layout-sw600dp/activity_masterdetail.xml
中。這樣做最明顯的缺點就是數(shù)據(jù)冗余靶庙,因為每個布局文件都要復(fù)制一份问畅。-
使用別名資源。別名資源是一種指向其他資源的特殊資源六荒。它存放在 res/values/目錄下护姆,并按照約定定義在 refs.xml 文件中。比如在默認(rèn)的 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 生命周期沒有結(jié)束的時候傳遞數(shù)據(jù)出去殴玛,就要使用回調(diào)接口。
回調(diào)就相當(dāng)于一個委托添祸,首先 fragment 自己定義回調(diào)的接口滚粟,托管的 acitivity 來實現(xiàn)這個接口,接著 fragment 需要持有實現(xiàn)了自己定義接口的對象刃泌,以便自己可以實時調(diào)用凡壤。
對于一個回調(diào)接口而言,fragment 只要求實現(xiàn)這個接口的類在函數(shù)里要做的是什么耙替,卻不知道實現(xiàn)類到底會做什么亚侠,每個實現(xiàn)類有自己的方法來實現(xiàn)。
2.1 CrimeListFragment 的回調(diào)接口
對于 CrimeListFragment俗扇,其所能響應(yīng)的就是點擊列表中的某一項硝烂,那么它的回調(diào)接口定義如下:
public interface Callbacks {
void onCrimeSelected(Crime crime);
}
然后應(yīng)該在需要托管的 Activity 中實現(xiàn)該接口,在這里是 CrimeListActivity:
// 省略 implement 以節(jié)約版面
@Override
public void onCrimeSelected(Crime crime) {
// 如果發(fā)現(xiàn)布局里沒有包含詳情 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 中持有實現(xiàn)接口的 activity 的引用滞谢,然后在生命周期末去除引用以便內(nèi)存的回收
// CrimeListFragment
private Callbacks mCallbacks;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
}
// 中間的函數(shù)……
@Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
最后修改 onClick 事件,調(diào)用 mCallbacks.onCrimeSelected(Crime crime) 即可啥酱。這樣以后爹凹,在雙版面視圖中點擊列表中的某一項,在詳情版面中就會顯示相應(yīng)的信息镶殷。
但是有一個問題禾酱,那就是在詳情頁(CrimeFragment)更改信息,在列表頁沒有任何響應(yīng)绘趋,因為 CrimeListFragment 不會暫停颤陶,所以也就不會刷新,所以下一步要在 CrimeFragment 中定義回調(diào)接口陷遮, 讓托管 activity 去更新 CrimeListFragment滓走。
2.2 CrimeFragment 的回調(diào)接口
首先定義回調(diào)接口,這里想讓托管者做的就是在 Crime 詳情進(jìn)行更新時更新列表
// CrimeFragment
private Callbacks mCallbacks;
public interface Callbacks {
void onCrimeUpdated(Crime crime);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
}
// 中間的函數(shù)……
@Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
在 CrimeListActivity 中實現(xiàn)該接口:
@Override
public void onCrimeUpdated(Crime crime) {
CrimeListFragment listFragment = (CrimeListFragment)
getSupportFragmentManager()
.findFragmentById(R.id.fragment_container);
listFragment.updateUI();
}
由于只要托管 CrimeFragment 的 activity 都應(yīng)該實現(xiàn)其回調(diào)接口帽馋,所以在 CrimePagerActivity 中提供一個空的接口實現(xiàn)
之后在每次數(shù)據(jù)發(fā)生更改時都調(diào)用 mCallbacks.onCrimeUpdated(mCrime);
即可搅方。書上將更新模型層也放到了一起。
3. 挑戰(zhàn)的后遺癥:刪除 Crime
還記得我們在 ToolBar 那一章加入的挑戰(zhàn)嗎绽族,就是刪除一個 Crime姨涡,對于 CriminalIntent 這個應(yīng)用來說,雙版面和單版面的刪除操作應(yīng)該有著不同的結(jié)果吧慢,但這些行為在書上沒有定義涛漂,所以我們再自己想一種解決方案,以便確立如何寫接下來的補充程序。
- 雙版面的界面下匈仗,點擊刪除應(yīng)該要讓左邊的列表中去掉刪除的那一項瓢剿,并且詳情頁也要改為已存在的某一項的詳情,為了方便實現(xiàn)悠轩,我們在這里改為已存在的第一項间狂。如果只有最后一項并且點擊了刪除,那么右邊應(yīng)該要變成空白哗蜈。
- 單版面的界面下前标,點擊刪除就直接刪去該條記錄坠韩,然后結(jié)束 activity距潘。
在這里我在 CrimeFragment 的 Callbacks 接口中加入了 onCrimeDelete(Crime crime) 方法與 onCrimeAllDeleted(Crime crime) 方法,在 CrimeListActivity 中實現(xiàn)如下:
@Override
public void onCrimeDeleted(Crime crime) {
// 如果只是刪除了一個只搁,而還有其他的 Crime 的話音比,
// 就相當(dāng)于選中一個 Crime,這里傳過來的應(yīng)該是第一個 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 中也要實現(xiàn)這兩個方法洞翩,但是對于這個 activity 來說只要進(jìn)行 finish() 即可。
在刪除按鈕的選中監(jiān)聽中:
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); // 這里相當(dāng)于選中第一個
updateCrime(); // 這里面升級了數(shù)據(jù)層并且更新了列表
}
return true;
整個程序就此完成啦~
GitHub Page: kniost.github.io
簡書:http://www.reibang.com/u/723da691aa42