支持cocopods,功能完善,性能不錯(cuò),代碼質(zhì)量尚可,喜歡的朋友可以給個(gè)小星星。
為了適應(yīng)組件的自定義需求,代碼和邏輯有點(diǎn)多,所以盡量不要修改源碼。
寫在前面
本文講解YBImageBrowser的組件設(shè)計(jì)思路和部分技術(shù)實(shí)現(xiàn)原理,對(duì)本框架有興趣的朋友可以看看?。行文的重點(diǎn)是筆者的框架設(shè)計(jì)理念、代碼及體驗(yàn)優(yōu)化的思考、關(guān)鍵技術(shù)點(diǎn)的實(shí)現(xiàn),希望不管是老鳥還是新手看完之后都能有所收獲和感悟。
歡迎大家交流探討,當(dāng)然,筆者水平有限,若有大佬指教不勝感激。
索引:(簡(jiǎn)書不支持頁(yè)內(nèi)跳轉(zhuǎn)很尷尬)
一、組件框架整體設(shè)計(jì)
二、組件中如何隱藏屬性和方法
三、拖拽動(dòng)效的算法優(yōu)化
四、分頁(yè)間距的算法優(yōu)化
五、內(nèi)存的優(yōu)化
六、預(yù)下載和任務(wù)同步
七、屏幕旋轉(zhuǎn)UI適配
一、組件框架整體設(shè)計(jì)
其實(shí)對(duì)于圖片瀏覽器,開(kāi)源項(xiàng)目也有不少,不管是代碼上還是功能上沒(méi)有一個(gè)能完整的滿足筆者的需求。所以筆者索性做了一個(gè),力圖將粒度做小,功能做全,當(dāng)然這需要一個(gè)漫長(zhǎng)的過(guò)程,空閑時(shí)間筆者會(huì)持續(xù)迭代和優(yōu)化。
目前采用的是UIViewController做為底,上層是一個(gè)橫向滾動(dòng)的UICollectionView,在UICollectionViewCell上面是UIScrollView,當(dāng)然還包括主要顯示圖片、動(dòng)畫圖片、裁剪顯示前景圖片等。
使用UICollectionView是為了利用蘋果為我們做的復(fù)用機(jī)制,不需要專門去實(shí)現(xiàn),不然邏輯代碼太多,得不償失;而縮放的效果依托于UIScrollView;采用UIViewController為底是為了更好的控制旋轉(zhuǎn)屏幕時(shí)的UI適配,之前也是考慮更輕一點(diǎn)的UIView,但是它會(huì)受父視圖的旋轉(zhuǎn)影響,可能適配難度會(huì)翻幾倍,而且使用UIViewController能更方便和優(yōu)雅的實(shí)現(xiàn)圖片瀏覽器的入場(chǎng)和出場(chǎng)動(dòng)畫。
二、組件中如何隱藏屬性和方法
在做一個(gè)組件的時(shí)候,我們往往思考著向用戶隱藏某些細(xì)節(jié)實(shí)現(xiàn),一方面是為了避免用戶的無(wú)意更改,一方面是為了簡(jiǎn)化API使其看起來(lái)更清爽。
對(duì)于屬性,若想讓用戶只讀不可寫,可以在.h中對(duì)屬性使用readonly修飾符;若根本不想要用戶看到,可以直接將該屬性創(chuàng)建在需要使用的目標(biāo)類的.m文件內(nèi)。
不過(guò)這樣并不優(yōu)雅,意味著我們很多代碼和類必須搞到同一文件,才能達(dá)到外部無(wú)法直接訪問(wèn),而內(nèi)部可以訪問(wèn)的目的。若我們想分離多個(gè)文件好管理代碼和實(shí)現(xiàn)更優(yōu)秀的架構(gòu)時(shí),不得不將屬性寫到.h里面讓其他文件可以訪問(wèn)。
那么,何不換一種思路?盡管我們將屬性寫在.m中隔離外部訪問(wèn),實(shí)際上用戶仍然可以用KVC的方式讀寫,那么我們框架組件內(nèi)部為何不使用KVC進(jìn)行讀寫?
于是,在組件的YBImageBrowserModel的.h.m文件中你可以看到這樣的代碼:
.h?中
FOUNDATION_EXTERN?NSString?*?const?YBImageBrowserModel_KVCKey_isLoading;
FOUNDATION_EXTERN?NSString?*?const?YBImageBrowserModel_KVCKey_isLoadFailed;
.m?中
NSString?*?const?YBImageBrowserModel_KVCKey_isLoading?=?@"isLoading";
NSString?*?const?YBImageBrowserModel_KVCKey_isLoadFailed?=?@"isLoadFailed";
這里使用字符串常量存放KVC的鍵,組件內(nèi)部就使用valueForKey:和setValue:forKey:通過(guò)這些常量來(lái)優(yōu)雅的讀寫實(shí)例變量了。
對(duì)于方法的隱藏,組件中不將方法暴露在.h里面,只寫在.m里面,然后組件其他文件通過(guò)下的objc_msgSend方法處理,比如隨便截取一段代碼:
YBImageBrowserModelScaleImageSuccessBlock?successBlock?=?^(YBImageBrowserModel?*backModel)?{
???????...
????};
????((void(*)(id,?SEL,?CGRect,?YBImageBrowserModelScaleImageSuccessBlock))?objc_msgSend)(model,?sel_registerName(YBImageBrowserModel_SELName_scaleImage),?imageFrame,?successBlock);
或者使用NSInvocation作為私有屬性,外部也用KVC讀寫。
三、拖拽動(dòng)效的算法優(yōu)化
拖拽動(dòng)效是目前很流行的圖片瀏覽器出場(chǎng)效果,筆者看了好幾個(gè)知名APP,“新浪微博”,“今日頭條”,“QQ”,“QQ瀏覽器”,“微信”等都做了類似的動(dòng)效,但是除了“微信”的效果人性化一點(diǎn),其它的都有些不盡人意的地方。
這個(gè)效果咋一看比較簡(jiǎn)單,無(wú)非就是根據(jù)移動(dòng)的距離,以某種數(shù)學(xué)關(guān)系移動(dòng)圖片并且縮小圖片,實(shí)現(xiàn)可以直接計(jì)算frame或者使用CATransform3D等。
但是,有個(gè)容易忽略的問(wèn)題,在拖動(dòng)的時(shí)候我們希望看到的效果是圖片跟隨手指移動(dòng)并且縮小,上圖左右兩種狀態(tài)下的箭頭指向的正是手指拖動(dòng)觸摸的點(diǎn)(理想狀態(tài)),若寫一個(gè)移動(dòng)和縮放比例變化之間是線性的動(dòng)畫,手指觸摸的點(diǎn)會(huì)是這種理想狀態(tài)么?
答案是否定的,若移動(dòng)的時(shí)候不縮放,是能達(dá)到理想狀態(tài),若縮放了狀態(tài)二必然會(huì)是如下圖所示:
拖動(dòng)動(dòng)效存在問(wèn)題
處理方式:若是使用的動(dòng)畫相關(guān)的類庫(kù),可以考慮使用錨點(diǎn)來(lái)處理。本組件是使用frame的方式處理,通過(guò)一張圖解釋如何處理這個(gè)邏輯:
處理方式
實(shí)際上代碼邏輯比看起來(lái)的復(fù)雜一些,有興趣的可以看代碼,這里只提出思路。
四、分頁(yè)間距的算法優(yōu)化
說(shuō)起分頁(yè),幾乎所有iOS工程師都會(huì)說(shuō).pagingEnabled屬性,又說(shuō)分頁(yè)間距,稍有經(jīng)驗(yàn)的工程師都會(huì)說(shuō)重寫UICollectionView的layout,既創(chuàng)建一個(gè)UICollectionViewFlowLayout類重寫約束?,F(xiàn)在這里不浪費(fèi)篇幅討論API的用法,你只需要知道在重寫的layout里面,幾乎每一幀的界面都可以靠重寫layoutAttributesForElementsInRect等方法重新計(jì)算。
按照常規(guī)的邏輯思路,最好想到的方案是:若當(dāng)前是第n頁(yè)時(shí),所有的Cell都向左移動(dòng)(n-1) * 間距。
確實(shí),這種算法邏輯咋一看好像能解決問(wèn)題,但當(dāng)你滑到下圖的情況下時(shí),會(huì)發(fā)生奇怪的現(xiàn)象:
blog_pic3.png
你會(huì)發(fā)現(xiàn)在滑動(dòng)到第n頁(yè)和第n+1頁(yè)之間的臨界點(diǎn)時(shí),界面會(huì)突然向左或者向右跳動(dòng)一段距離,因?yàn)檫@里就是上面所說(shuō)方式判斷移動(dòng)的觸發(fā)點(diǎn),顯然這不夠平滑。
于是組件中筆者的做法是,在每次重寫布局時(shí),都移動(dòng)一個(gè)距離:當(dāng)前偏移量 / 最大偏移量 * 總共頁(yè)間距
其實(shí)做法很簡(jiǎn)單,但這種思維方式卻非常實(shí)用,在我們做很多需要平滑過(guò)渡的邏輯時(shí)(不局限于界面),都可以以這種思維做出“平滑”的效果。
五、內(nèi)存的優(yōu)化
由于如今的APP做的越來(lái)越復(fù)雜,作為一個(gè)合格的移動(dòng)端程序員,我們需要時(shí)刻關(guān)注內(nèi)存問(wèn)題,雖然這并不是剛需。
本地圖片的讀取
在讀取本地圖片時(shí),使用[UIImage imageNamed:]方式時(shí)系統(tǒng)會(huì)緩存該圖片,而釋放緩存的時(shí)機(jī)很微妙。所以在使用比較大、調(diào)用頻率低的圖片時(shí),盡量使用讀取文件的方式做:
[UIImage?imageWithContentsOfFile:[[NSBundle?mainBundle]?pathForResource:fileName?ofType:fileType]]
超大圖的處理
這樣雖然能減少累加的內(nèi)存,但若一張圖片就非常大呢?系統(tǒng)將它解壓過(guò)后將會(huì)占用比你想象中更大的內(nèi)存,APP可能變得非??D甚至崩潰。
于是,組件中設(shè)置了一個(gè)pt的界限,當(dāng)圖片超過(guò)這個(gè)界限,組件會(huì)自動(dòng)異步壓縮到當(dāng)前屏幕最大顯示pt數(shù)量,當(dāng)用戶拖動(dòng)或縮放放大圖片時(shí),組件會(huì)自動(dòng)異步裁剪可視區(qū)域的圖片,通過(guò)一張前景圖片顯示出來(lái)(當(dāng)然裁剪也是有最大限度的)。
思路就兩句話,實(shí)際邏輯結(jié)合其他功能會(huì)比較復(fù)雜,有興趣可以看看代碼,這里不過(guò)多闡述。
下載任務(wù)的釋放
組件內(nèi)部是利用SDWebImage做的下載和緩存,在每一個(gè)model釋放的時(shí)候,都會(huì)將對(duì)應(yīng)的下載任務(wù)取消已節(jié)約網(wǎng)絡(luò)和內(nèi)存開(kāi)銷。
六、預(yù)下載和任務(wù)同步
為了提高用戶體驗(yàn),在配置圖片瀏覽器圖片對(duì)應(yīng)的model的時(shí)候,可以通過(guò) API 設(shè)置異步預(yù)下載,當(dāng)網(wǎng)絡(luò)狀況不錯(cuò)的時(shí)候,可能用戶打開(kāi)瀏覽器圖片就下載好了,畢竟圖片瀏覽器是有很短的創(chuàng)建時(shí)間和較長(zhǎng)的入場(chǎng)時(shí)間的。
其實(shí)這也是一種提升效率的思維,我們要習(xí)慣性的去思考利用程序的空閑預(yù)先做一些任務(wù),才能編寫出高效的代碼。
這里有一個(gè)點(diǎn)需要注意,若我們執(zhí)行了預(yù)下載,而在圖片瀏覽器打開(kāi)的時(shí)候,圖片仍未預(yù)下載完成,而此刻又會(huì)執(zhí)行正式的下載,它們之間如何信息同步?
哈哈,其實(shí)很簡(jiǎn)單,就是將同一類的任務(wù)放到同一個(gè)地方統(tǒng)一管理,比如本組件就是將圖片下載、圖片緩存、圖片壓縮、圖片裁剪等都放到圖片數(shù)據(jù)模型YBImageBrowserModel中處理,其它地方就用方法調(diào)度這些任務(wù),雖然可能會(huì)造成看起來(lái)比較多的方法調(diào)用,但是對(duì)穩(wěn)定性、容錯(cuò)率的提高不容小覷。
這種思維很重要,可以不嚴(yán)密的理解為AOP,功能分類集中管理。
七、屏幕旋轉(zhuǎn)UI適配
找到組件必然支持的方向
組件支持了旋轉(zhuǎn)功能,由于采用的是UIViewController作為底類,理所當(dāng)然的是讓組件內(nèi)部子控件跟隨UIViewController的旋轉(zhuǎn)而旋轉(zhuǎn),目前不支持強(qiáng)制旋轉(zhuǎn),因?yàn)榭赡軙?huì)有些麻煩,后期迭代考慮增加。
UIViewController的旋轉(zhuǎn)會(huì)直接受到工程general -> deployment info -> Device Orientation處的影響,所以,在判斷組件支持的旋轉(zhuǎn)方向的時(shí)候,需要取一個(gè)交集:
-?(void)configSupportAutorotateTypes?{
????UIApplication?*application?=?[UIApplication?sharedApplication];
????UIInterfaceOrientationMask?keyWindowSupport?=?[application?supportedInterfaceOrientationsForWindow:window];
????UIInterfaceOrientationMask?selfSupport?=?![self?shouldAutorotate]???UIInterfaceOrientationMaskPortrait?:?[self?supportedInterfaceOrientations];
????supportAutorotateTypes?=?keyWindowSupport?&?selfSupport;
}
然后這個(gè)交集就是UIViewController可能旋轉(zhuǎn)的方向,也就是組件可能旋轉(zhuǎn)的方向。
布局更新時(shí)機(jī)優(yōu)化
大家很容易就想到,當(dāng)設(shè)備旋轉(zhuǎn)過(guò)后,若組件支持該方向,就通知所有子界面刷新布局(可能有人會(huì)說(shuō)用autolayout,但是考慮到效率和可控性方面的問(wèn)題,本組件都采用frame處理)。
其實(shí)若你是這樣做,已經(jīng)滿足了需求,剩下了可能就是繁雜的布局執(zhí)行流。
然而我會(huì)說(shuō)還能優(yōu)化。試想一下,手機(jī)的兩種豎屏狀態(tài)(home在上,home在下),兩種橫屏狀態(tài)(home在左,home在右),它們的frame是不是一樣?
所以,這里需要加入一個(gè)標(biāo)識(shí),用來(lái)存儲(chǔ)此時(shí)當(dāng)前UIView顯示的frame類型是“豎屏”還是“橫屏”,而不是每一種屏幕狀態(tài)變化都去做所有的布局更新,理論上提高了一倍的布局開(kāi)銷。
引入代理規(guī)范布局流程
由于通知子視圖更新布局、存儲(chǔ)當(dāng)前視圖分別在“豎屏”和“橫屏”下的frame、存儲(chǔ)當(dāng)前適配的屏幕方向等信息是每一個(gè)視圖幾乎都會(huì)做的工作(雖然細(xì)節(jié)有些差異,但我們稍宏觀的看這個(gè)問(wèn)題)。
于是,組件做了一個(gè)代理:
@protocol?YBImageBrowserScreenOrientationProtocol?@required
//?當(dāng)前視圖UI適配的屏幕方向
@property?(nonatomic,?assign)?YBImageBrowserScreenOrientation?so_screenOrientation;
//?當(dāng)前視圖在豎直屏幕的frame
@property?(nonatomic,?assign)?CGRect?so_frameOfVertical;
//?當(dāng)前視圖在橫向屏幕的frame
@property?(nonatomic,?assign)?CGRect?so_frameOfHorizontal;
//?更新約束是否完成
@property?(nonatomic,?assign)?BOOL?so_isUpdateUICompletely;
-?(void)so_setFrameInfoWithSuperViewScreenOrientation:(YBImageBrowserScreenOrientation)screenOrientation?superViewSize:(CGSize)size;
-?(void)so_updateFrameWithScreenOrientation:(YBImageBrowserScreenOrientation)screenOrientation;
@end
需要跟隨屏幕旋轉(zhuǎn)更新布局的UIView都實(shí)現(xiàn)這個(gè)代理,達(dá)到標(biāo)準(zhǔn)控制的目的,值得注意的是代理里面的屬性需要自己在實(shí)現(xiàn)文件關(guān)聯(lián)一個(gè)實(shí)例變量,類似于
@synthesize?so_frameOfVertical?=?_so_frameOfVertical;
@synthesize?so_frameOfHorizontal?=?_so_frameOfHorizontal;
其實(shí)吧,這個(gè)地方筆者感覺(jué)設(shè)計(jì)得比較雞肋,容筆者有更好的想法的時(shí)候更新組件。
寫在后面
看到這里可能有的朋友有些蒙,這通篇都說(shuō)些什么,沒(méi)一句完整的代碼。哈哈,實(shí)際上這就是組件的核心,是我花了許多時(shí)間做的一些思考和總結(jié),科普基礎(chǔ)知識(shí)挺費(fèi)勁的,百度就是一大篇一大篇的,我相信本文的價(jià)值還是有的。
越來(lái)越覺(jué)得有位朋友的話很有道理:編程是靠思維的東西。
希望大家共勉~