項(xiàng)目重構(gòu)的Git地址:
https://github.com/razerdp/FriendCircle
項(xiàng)目同步更新的文集:
http://www.lxweimin.com/notebooks/3224048/latest
本文控件Git地址:
https://github.com/razerdp/PhotoContents
上集概述
在上篇文章中,我們選擇了繼承FlowLayout來實(shí)現(xiàn)我們的九宮格,選擇它的原因很簡(jiǎn)單,就是因?yàn)槠鋖ayout過程實(shí)現(xiàn)很優(yōu)秀,可擴(kuò)展性也很強(qiáng),所以這就是選擇它的原因(我打死也不會(huì)說是為了偷懶)
OK,前言說完,本篇將會(huì)開始搭建這個(gè)控件的基本要素,可能涉及較多的代碼方面的東西,也許略微枯燥【于是我這次選擇了代碼截圖,弄得色彩斑斕點(diǎn),看起來應(yīng)該會(huì)爽很多】,我盡量述說的清晰一點(diǎn)
設(shè)計(jì)
擼一個(gè)控件并不是說隨便繼承個(gè)什么,然后就是一陣狂敲,在擼代碼之前我們需要做的就是設(shè)計(jì)。
設(shè)計(jì)一個(gè)控件,往往都會(huì)從以下幾個(gè)方面入手:
-
對(duì)外的有:
- 視覺樣式
- 交互方式
- 數(shù)據(jù)綁定
-
對(duì)內(nèi)的有:
- 邏輯處理
- 靈活性、耦合性、擴(kuò)展性、穩(wěn)定性、可維護(hù)性等
如果具體到細(xì)節(jié),還可以細(xì)到初始化方式、布局時(shí)機(jī)、繪制性能、touch傳遞響應(yīng)、邊界裁定、內(nèi)存控制等等。。。。
當(dāng)然,我們這里肯定不會(huì)有那么細(xì)致的描述了,否則感覺都可以寫一本書了QAQ;
在這里我們就針對(duì)上面的內(nèi)部和外部進(jìn)行設(shè)計(jì)就好了。
1、視覺樣式
用慣了朋友圈的我們肯定都知道朋友圈的九宮格是有幾個(gè)明顯的特征的:
- 單張圖片可大可小
- 4張圖片田字?jǐn)[放
- 最多九張圖片
- 多圖的時(shí)候圖片皆為邊長一樣的正方形
因此,針對(duì)這幾個(gè)特征,我們可以根據(jù)FlowLayout
的特性來進(jìn)行初步的設(shè)想:
- 單張圖片的情況下,我們僅僅控制圖片大小的上限和下限,至于具體數(shù)值隨圖片大小而自適應(yīng)。
- 4張圖片需要田字?jǐn)[放,對(duì)于這個(gè)的處理我們可以覆寫
onLayout()
,也可以通過設(shè)置LayoutParam的newLine參數(shù),讓其換行(FlowLayout的LayoutParam有該參數(shù)) - 最9張圖片,這個(gè)應(yīng)該是最好處理的
- 多圖的圖片顯示為正方形:同第二點(diǎn),針對(duì)
LayoutParam
做操作,限制寬高即可。
2、交互方式
朋友圈的圖片交互是點(diǎn)擊圖片的時(shí)候從圖片的位置放大到整屏,退出的時(shí)候則是縮回到原來的位置。
在設(shè)計(jì)的時(shí)候,按照我的思路是這個(gè)控件順便把圖片瀏覽實(shí)現(xiàn)了,也順便把動(dòng)畫給實(shí)現(xiàn)了,但考慮到后面的數(shù)據(jù)綁定等,以及單一職責(zé)準(zhǔn)則,最終還是決定本控件僅僅負(fù)責(zé)圖片的渲染和緩存處理,其余不管。
3、數(shù)據(jù)綁定
數(shù)據(jù)綁定其實(shí)跟外面使用者掛鉤,也跟內(nèi)部邏輯,擴(kuò)展性等掛鉤,所以這個(gè)需要慎重處理。
按照目前的思路,我們的控件所負(fù)責(zé)的東西是十分明確的:拿到View
→得到數(shù)據(jù)(圖片地址)
→加載圖片
→擺放控件
→渲染或緩存
也就是說,得到數(shù)據(jù)
和加載圖片
這兩部分不應(yīng)該由控件來限定死方式,而是應(yīng)該暴露給使用者自行處理,因此,適配器模式是一個(gè)非常好的選擇,而且大家也用的習(xí)慣。
對(duì)內(nèi)設(shè)計(jì)部分主要看大家的碼代碼水平,,,這里就不著重討論了。
初步搭建
很多人在寫自定義控件的時(shí)候往往很久都下不了手,依我的經(jīng)驗(yàn)來看(以我踩過的坑來看),很多時(shí)候都是顧慮的太多,,,換句話說,就是想得太多從而導(dǎo)致第一步都還沒邁出,就想著第一百步的情景。
對(duì)此,我的解決方法很簡(jiǎn)單,首先不管三七二十一,先把包弄出來,以及把控件的類給弄出來。。。。至少在沒方向的時(shí)候給自己一個(gè)方向也好。
于是我們就有了下面初步結(jié)構(gòu):
接下來,既然選擇了適配器模式,我們就首先搭建我們的adapter吧。
1、Adapter的設(shè)計(jì):
適配器模式簡(jiǎn)單地說其實(shí)就是對(duì)客戶端統(tǒng)一接口(統(tǒng)一為設(shè)計(jì)者希望最終得到對(duì)應(yīng)類的接口),使復(fù)雜的使用場(chǎng)景或不兼容的類或接口都能正常的工作在一起。
但是,在數(shù)據(jù)和View
的刷新間,我們總得需要一個(gè)橋梁,用來溝通View
和Bean
說到通知,我們首當(dāng)其沖想到的是。。。。EventBus對(duì)吧,既然想到了EventBus,也就不難想到觀察者
這貨了。
所以在寫Adapter前,我們先把橋搭好。
1.1、Observer
在adapter包下新建一個(gè)observer包,我們將在這里完成我們的觀察者的搭建。
首先我們需要一個(gè)觀察者(Observer)
這個(gè)觀察者只要做的只有兩件事:
- 通知requestLayout()【為了觸發(fā)measure()/layout()】
- 通知invalidate()【為了重繪】
因?yàn)橄到y(tǒng)本來就已經(jīng)有相關(guān)的觀察者,所以我們直接使用就好,畢竟咱們這個(gè)觀察者的功能實(shí)在有點(diǎn)簡(jiǎn)單。。。
OK,觀察者有了,接下來我們需要的是被觀察的對(duì)象,一般被觀察的都是 xxx -able結(jié)尾(able就是“可以..XXOO..的”)
當(dāng)然,被觀察的對(duì)象可能有好多個(gè),也可能好多種實(shí)現(xiàn),所以我們先抽象出一個(gè)被觀察對(duì)象作為以后其他被觀察對(duì)象的父類。
最后就實(shí)現(xiàn)我們的具體觀察對(duì)象就行了
到目前為止,看起來還是很輕松愉快的嘛~
1.2、Adapter
Adapter的設(shè)計(jì),我們只需要認(rèn)準(zhǔn)一個(gè)東西就行:
不管具體實(shí)現(xiàn)類怎么干,反正我只想要我需要的東西,最終實(shí)現(xiàn)類必須返回這個(gè)東西給我
所以與其說定制一個(gè)Adapter,倒不如說我們是在定義一個(gè)協(xié)議,當(dāng)然,換到Java說法就是定義一系列的接口。。。
在開干之前,我們可以思考一下,我們需要的是什么。。。
想了大概30秒,幾乎可以確定,我們似乎需要的東西不多,就一個(gè):ImageView
,其他的似乎都不太需要。。。(真好滿足)
目標(biāo)很明確,所以我們可以動(dòng)手了。
不過因?yàn)槭且粋€(gè)初步的搭建,所以就沒有抽象為接口,而是以一個(gè)抽象類來制定這個(gè)Adapter。
代碼量很少,我們只做了幾件事:
- 注冊(cè)觀察者(當(dāng)然,觀察者在控件實(shí)現(xiàn))
- 制定接口:
onCreateView(),很明顯,跟
ListView
非常類似,這也是為了方便使用這個(gè)控件的人進(jìn)行開發(fā),這個(gè)方法返回的就是ImageView
,至于返回的這個(gè)View怎么擺放,是我們控件該干的事情了。onBindData(),這個(gè)方法是用來加載數(shù)據(jù)用的,上一個(gè)方法目的用來創(chuàng)建View,這一個(gè)方法則是用來加載圖片
getCount(),獲取數(shù)據(jù)(或者說圖片)的數(shù)量
Adapter的設(shè)計(jì)就這樣輕松愉悅的搞定了,當(dāng)然這只是一個(gè)初步的,我們其實(shí)還可以進(jìn)一步的抽象。。。
比如:
接口化,而不用抽象類,因?yàn)閷?duì)于Java而言,接口簡(jiǎn)直就是各種干爹啊,我們不能多繼承,但接口提供了另一種的“多繼承”
泛型:如您所見,我們的
onCreateView
指定返回ImageView
,但實(shí)際項(xiàng)目中,我們可能很多自定義的ImageView
,如果實(shí)現(xiàn)類里強(qiáng)轉(zhuǎn)什么的,既不方便于靜態(tài)檢查,又不那么爽,所以我們其實(shí)可以采取泛型,比如:
public abstract class PhotoContentsBaseAdapter<V extends ImageView> {
...注冊(cè)觀察者,略
public abstract V onCreateView(V convertView, ViewGroup parent, int position);
public abstract void onBindData(int position, @NonNull V convertView);
public abstract int getCount();
}
嘛,其實(shí)還有好多好多了啦,喜歡就折騰一下咯-V- 這個(gè)項(xiàng)目是16年底才粗略的擼了一個(gè),所以就不怎么維護(hù)了(基本滿足我朋友圈項(xiàng)目使用就好~畢竟懶。。。。)
2、控件的設(shè)計(jì)(因?yàn)槠@里僅講述部分內(nèi)容,剩余內(nèi)容放到【中下】篇):
嘿嘿,終于到了重頭戲了,前面干了那么多東東,為的就是養(yǎng)這個(gè)親兒子啊。。。
這個(gè)控件代碼量不多。。。嗯。。。。至今碼了560多行,嘛~看起來這個(gè)控件還是很年輕嘛。。。
不過對(duì)于一篇文章來說,500多行還是太多太多了,所以接下來就針對(duì)幾個(gè)地方著重講述吧
整理了一下代碼,著重講述的地方我已經(jīng)標(biāo)注了起來(如果圖片模糊,PC上可以先放大圖片,然后右鍵新窗口打開再放大就可以看完整了,手機(jī)上的話,經(jīng)過我的小手機(jī)測(cè)試,直接放大也是可以看得挺清晰的):
因?yàn)槠晕覀冞@里著重說一下Adapter相關(guān)吧(畢竟上面那么多空間都在描述adapter)
2.1、觀察者
首先我們搭橋,在上面的文字里,我們把橋的橋墩搭好了,所以這里我們就直接鋪橋。
要使控件跟Adapter的觀察者聯(lián)合起來,要做的就是把我們的控件作為觀察者給塞進(jìn)去。
我們可選擇兩種方式:
控件實(shí)現(xiàn)觀察者的接口,然后把自己塞進(jìn)去
控件內(nèi)部類實(shí)現(xiàn)觀察者接口,然后把它塞進(jìn)去
這里我采取后者。
首先實(shí)現(xiàn)一個(gè)內(nèi)部類:
在onChanged里面我們執(zhí)行requestLayout()
,也就是要求控件重新布局以觸發(fā)measure和layout,畢竟setAdapter的時(shí)候可能是set了不同的Adapter,所以我們需要做這兩步操作以適應(yīng)新的需求。
在控件里我們直接new一個(gè)觀察者:
private PhotoImageAdapterObserver mAdapterObserver = new PhotoImageAdapterObserver();
這個(gè)觀察者就是我們和adapter溝通的橋梁了,以后需要換adapter的時(shí)候,我們先取消注冊(cè)然后再重新new一個(gè)并注冊(cè)就好了。
接下來就是setAdapter方法
在setAdapter前,我們需要先注銷掉原來的觀察者,然后執(zhí)行控件的初始化,接著重新new一個(gè)觀察者并注冊(cè),最后才requestLayout(),也許有人問為何這里需要requestLayout()
,上面的觀察者不是應(yīng)該實(shí)現(xiàn)了么,這里需要注明的是,我們的觀察者的觸發(fā)條件是外部執(zhí)行adapter.notifyDataChanged()
,平時(shí)我們用刀setAdapter也不會(huì)說下一行立刻就執(zhí)行adapter.notifyDataChanged()
吧,所以在這個(gè)方法里直接就提醒控件進(jìn)行測(cè)量,重繪。
那么,寫到這里,關(guān)于這個(gè)控件的一些細(xì)節(jié)已經(jīng)開始滲透了,本篇主要講的是控件的搭建和Adapter的搭建,下一篇我們將會(huì)著重說明一下關(guān)于緩存池和View的創(chuàng)建時(shí)機(jī)以及方法這幾個(gè)部分。
然后就可以進(jìn)入下篇了。