支持cocopods率触,功能完善,性能不錯汇竭,代碼質(zhì)量尚可葱蝗,喜歡的朋友可以給個小星星。
為了適應(yīng)組件的自定義需求细燎,代碼和邏輯有點(diǎn)多两曼,所以盡量不要修改源碼。
寫在前面
本文講解YBImageBrowser的組件設(shè)計思路和部分技術(shù)實(shí)現(xiàn)原理玻驻,對本框架有興趣的朋友可以看看?合愈。行文的重點(diǎn)是筆者的框架設(shè)計理念、代碼及體驗(yàn)優(yōu)化的思考击狮、關(guān)鍵技術(shù)點(diǎn)的實(shí)現(xiàn),希望不管是老鳥還是新手看完之后都能有所收獲和感悟益老。
歡迎大家交流探討彪蓬,當(dāng)然,筆者水平有限捺萌,若有大佬指教不勝感激档冬。
索引:(簡書不支持頁內(nèi)跳轉(zhuǎn)很尷尬)
一、組件框架整體設(shè)計
二桃纯、組件中如何隱藏屬性和方法
三酷誓、拖拽動效的算法優(yōu)化
四、分頁間距的算法優(yōu)化
五态坦、內(nèi)存的優(yōu)化
六盐数、預(yù)下載和任務(wù)同步
七、屏幕旋轉(zhuǎn)UI適配
一伞梯、組件框架整體設(shè)計
其實(shí)對于圖片瀏覽器玫氢,開源項(xiàng)目也有不少,不管是代碼上還是功能上沒有一個能完整的滿足筆者的需求谜诫。所以筆者索性做了一個漾峡,力圖將粒度做小,功能做全喻旷,當(dāng)然這需要一個漫長的過程生逸,空閑時間筆者會持續(xù)迭代和優(yōu)化。
目前采用的是UIViewController做為底,上層是一個橫向滾動的UICollectionView槽袄,在UICollectionViewCell上面是UIScrollView烙无,當(dāng)然還包括主要顯示圖片、動畫圖片掰伸、裁剪顯示前景圖片等皱炉。
使用UICollectionView是為了利用蘋果為我們做的復(fù)用機(jī)制,不需要專門去實(shí)現(xiàn)狮鸭,不然邏輯代碼太多合搅,得不償失;而縮放的效果依托于UIScrollView歧蕉;采用UIViewController為底是為了更好的控制旋轉(zhuǎn)屏幕時的UI適配灾部,之前也是考慮更輕一點(diǎn)的UIView,但是它會受父視圖的旋轉(zhuǎn)影響惯退,可能適配難度會翻幾倍赌髓,而且使用UIViewController能更方便和優(yōu)雅的實(shí)現(xiàn)圖片瀏覽器的入場和出場動畫。
二催跪、組件中如何隱藏屬性和方法
在做一個組件的時候锁蠕,我們往往思考著向用戶隱藏某些細(xì)節(jié)實(shí)現(xiàn),一方面是為了避免用戶的無意更改懊蒸,一方面是為了簡化API使其看起來更清爽荣倾。
對于屬性,若想讓用戶只讀不可寫骑丸,可以在.h中對屬性使用readonly修飾符舌仍;若根本不想要用戶看到,可以直接將該屬性創(chuàng)建在需要使用的目標(biāo)類的.m文件內(nèi)通危。
不過這樣并不優(yōu)雅铸豁,意味著我們很多代碼和類必須搞到同一文件,才能達(dá)到外部無法直接訪問菊碟,而內(nèi)部可以訪問的目的节芥。若我們想分離多個文件好管理代碼和實(shí)現(xiàn)更優(yōu)秀的架構(gòu)時,不得不將屬性寫到.h里面讓其他文件可以訪問框沟。
那么藏古,何不換一種思路?盡管我們將屬性寫在.m中隔離外部訪問忍燥,實(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:通過這些常量來優(yōu)雅的讀寫實(shí)例變量了输玷。
對于方法的隱藏,組件中不將方法暴露在.h里面靡馁,只寫在.m里面欲鹏,然后組件其他文件通過下的objc_msgSend方法處理,比如隨便截取一段代碼:
YBImageBrowserModelScaleImageSuccessBlock?successBlock?=?^(YBImageBrowserModel?*backModel)?{
???????...
????};
????((void(*)(id,?SEL,?CGRect,?YBImageBrowserModelScaleImageSuccessBlock))?objc_msgSend)(model,?sel_registerName(YBImageBrowserModel_SELName_scaleImage),?imageFrame,?successBlock);
或者使用NSInvocation作為私有屬性臭墨,外部也用KVC讀寫赔嚎。
三、拖拽動效的算法優(yōu)化
拖拽動效是目前很流行的圖片瀏覽器出場效果胧弛,筆者看了好幾個知名APP尤误,“新浪微博”,“今日頭條”结缚,“QQ”损晤,“QQ瀏覽器”,“微信”等都做了類似的動效红竭,但是除了“微信”的效果人性化一點(diǎn)尤勋,其它的都有些不盡人意的地方。
這個效果咋一看比較簡單茵宪,無非就是根據(jù)移動的距離最冰,以某種數(shù)學(xué)關(guān)系移動圖片并且縮小圖片,實(shí)現(xiàn)可以直接計算frame或者使用CATransform3D等稀火。
但是锌奴,有個容易忽略的問題,在拖動的時候我們希望看到的效果是圖片跟隨手指移動并且縮小憾股,上圖左右兩種狀態(tài)下的箭頭指向的正是手指拖動觸摸的點(diǎn)(理想狀態(tài)),若寫一個移動和縮放比例變化之間是線性的動畫箕慧,手指觸摸的點(diǎn)會是這種理想狀態(tài)么服球?
答案是否定的,若移動的時候不縮放颠焦,是能達(dá)到理想狀態(tài)斩熊,若縮放了狀態(tài)二必然會是如下圖所示:
拖動動效存在問題
處理方式:若是使用的動畫相關(guān)的類庫,可以考慮使用錨點(diǎn)來處理伐庭。本組件是使用frame的方式處理粉渠,通過一張圖解釋如何處理這個邏輯:
處理方式
實(shí)際上代碼邏輯比看起來的復(fù)雜一些,有興趣的可以看代碼圾另,這里只提出思路霸株。
四、分頁間距的算法優(yōu)化
說起分頁集乔,幾乎所有iOS工程師都會說.pagingEnabled屬性去件,又說分頁間距,稍有經(jīng)驗(yàn)的工程師都會說重寫UICollectionView的layout,既創(chuàng)建一個UICollectionViewFlowLayout類重寫約束∮攘铮現(xiàn)在這里不浪費(fèi)篇幅討論API的用法倔叼,你只需要知道在重寫的layout里面,幾乎每一幀的界面都可以靠重寫layoutAttributesForElementsInRect等方法重新計算宫莱。
按照常規(guī)的邏輯思路丈攒,最好想到的方案是:若當(dāng)前是第n頁時,所有的Cell都向左移動(n-1) * 間距授霸。
確實(shí)巡验,這種算法邏輯咋一看好像能解決問題,但當(dāng)你滑到下圖的情況下時绝葡,會發(fā)生奇怪的現(xiàn)象:
blog_pic3.png
你會發(fā)現(xiàn)在滑動到第n頁和第n+1頁之間的臨界點(diǎn)時深碱,界面會突然向左或者向右跳動一段距離,因?yàn)檫@里就是上面所說方式判斷移動的觸發(fā)點(diǎn)藏畅,顯然這不夠平滑敷硅。
于是組件中筆者的做法是,在每次重寫布局時愉阎,都移動一個距離:當(dāng)前偏移量 / 最大偏移量 * 總共頁間距
其實(shí)做法很簡單绞蹦,但這種思維方式卻非常實(shí)用,在我們做很多需要平滑過渡的邏輯時(不局限于界面)榜旦,都可以以這種思維做出“平滑”的效果幽七。
五、內(nèi)存的優(yōu)化
由于如今的APP做的越來越復(fù)雜溅呢,作為一個合格的移動端程序員澡屡,我們需要時刻關(guān)注內(nèi)存問題,雖然這并不是剛需咐旧。
本地圖片的讀取
在讀取本地圖片時驶鹉,使用[UIImage imageNamed:]方式時系統(tǒng)會緩存該圖片,而釋放緩存的時機(jī)很微妙铣墨。所以在使用比較大室埋、調(diào)用頻率低的圖片時,盡量使用讀取文件的方式做:
[UIImage?imageWithContentsOfFile:[[NSBundle?mainBundle]?pathForResource:fileName?ofType:fileType]]
超大圖的處理
這樣雖然能減少累加的內(nèi)存伊约,但若一張圖片就非常大呢姚淆?系統(tǒng)將它解壓過后將會占用比你想象中更大的內(nèi)存,APP可能變得非陈怕桑卡頓甚至崩潰腌逢。
于是,組件中設(shè)置了一個pt的界限超埋,當(dāng)圖片超過這個界限上忍,組件會自動異步壓縮到當(dāng)前屏幕最大顯示pt數(shù)量骤肛,當(dāng)用戶拖動或縮放放大圖片時,組件會自動異步裁剪可視區(qū)域的圖片窍蓝,通過一張前景圖片顯示出來(當(dāng)然裁剪也是有最大限度的)腋颠。
思路就兩句話,實(shí)際邏輯結(jié)合其他功能會比較復(fù)雜吓笙,有興趣可以看看代碼淑玫,這里不過多闡述。
下載任務(wù)的釋放
組件內(nèi)部是利用SDWebImage做的下載和緩存面睛,在每一個model釋放的時候絮蒿,都會將對應(yīng)的下載任務(wù)取消已節(jié)約網(wǎng)絡(luò)和內(nèi)存開銷。
六叁鉴、預(yù)下載和任務(wù)同步
為了提高用戶體驗(yàn)土涝,在配置圖片瀏覽器圖片對應(yīng)的model的時候,可以通過 API 設(shè)置異步預(yù)下載幌墓,當(dāng)網(wǎng)絡(luò)狀況不錯的時候但壮,可能用戶打開瀏覽器圖片就下載好了,畢竟圖片瀏覽器是有很短的創(chuàng)建時間和較長的入場時間的常侣。
其實(shí)這也是一種提升效率的思維蜡饵,我們要習(xí)慣性的去思考利用程序的空閑預(yù)先做一些任務(wù),才能編寫出高效的代碼胳施。
這里有一個點(diǎn)需要注意溯祸,若我們執(zhí)行了預(yù)下載,而在圖片瀏覽器打開的時候舞肆,圖片仍未預(yù)下載完成焦辅,而此刻又會執(zhí)行正式的下載,它們之間如何信息同步椿胯?
哈哈氨鹏,其實(shí)很簡單,就是將同一類的任務(wù)放到同一個地方統(tǒng)一管理压状,比如本組件就是將圖片下載、圖片緩存跟继、圖片壓縮种冬、圖片裁剪等都放到圖片數(shù)據(jù)模型YBImageBrowserModel中處理,其它地方就用方法調(diào)度這些任務(wù)舔糖,雖然可能會造成看起來比較多的方法調(diào)用娱两,但是對穩(wěn)定性、容錯率的提高不容小覷金吗。
這種思維很重要十兢,可以不嚴(yán)密的理解為AOP趣竣,功能分類集中管理。
七旱物、屏幕旋轉(zhuǎn)UI適配
找到組件必然支持的方向
組件支持了旋轉(zhuǎn)功能遥缕,由于采用的是UIViewController作為底類,理所當(dāng)然的是讓組件內(nèi)部子控件跟隨UIViewController的旋轉(zhuǎn)而旋轉(zhuǎn)宵呛,目前不支持強(qiáng)制旋轉(zhuǎn)单匣,因?yàn)榭赡軙行┞闊笃诘紤]增加宝穗。
UIViewController的旋轉(zhuǎn)會直接受到工程general -> deployment info -> Device Orientation處的影響户秤,所以,在判斷組件支持的旋轉(zhuǎn)方向的時候逮矛,需要取一個交集:
-?(void)configSupportAutorotateTypes?{
????UIApplication?*application?=?[UIApplication?sharedApplication];
????UIInterfaceOrientationMask?keyWindowSupport?=?[application?supportedInterfaceOrientationsForWindow:window];
????UIInterfaceOrientationMask?selfSupport?=?![self?shouldAutorotate]???UIInterfaceOrientationMaskPortrait?:?[self?supportedInterfaceOrientations];
????supportAutorotateTypes?=?keyWindowSupport?&?selfSupport;
}
然后這個交集就是UIViewController可能旋轉(zhuǎn)的方向鸡号,也就是組件可能旋轉(zhuǎn)的方向。
布局更新時機(jī)優(yōu)化
大家很容易就想到须鼎,當(dāng)設(shè)備旋轉(zhuǎn)過后鲸伴,若組件支持該方向,就通知所有子界面刷新布局(可能有人會說用autolayout莉兰,但是考慮到效率和可控性方面的問題挑围,本組件都采用frame處理)。
其實(shí)若你是這樣做糖荒,已經(jīng)滿足了需求杉辙,剩下了可能就是繁雜的布局執(zhí)行流。
然而我會說還能優(yōu)化捶朵。試想一下蜘矢,手機(jī)的兩種豎屏狀態(tài)(home在上,home在下)综看,兩種橫屏狀態(tài)(home在左品腹,home在右),它們的frame是不是一樣红碑?
所以舞吭,這里需要加入一個標(biāo)識,用來存儲此時當(dāng)前UIView顯示的frame類型是“豎屏”還是“橫屏”析珊,而不是每一種屏幕狀態(tài)變化都去做所有的布局更新羡鸥,理論上提高了一倍的布局開銷。
引入代理規(guī)范布局流程
由于通知子視圖更新布局忠寻、存儲當(dāng)前視圖分別在“豎屏”和“橫屏”下的frame惧浴、存儲當(dāng)前適配的屏幕方向等信息是每一個視圖幾乎都會做的工作(雖然細(xì)節(jié)有些差異,但我們稍宏觀的看這個問題)奕剃。
于是衷旅,組件做了一個代理:
@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)這個代理捐腿,達(dá)到標(biāo)準(zhǔn)控制的目的,值得注意的是代理里面的屬性需要自己在實(shí)現(xiàn)文件關(guān)聯(lián)一個實(shí)例變量柿顶,類似于
@synthesize?so_frameOfVertical?=?_so_frameOfVertical;
@synthesize?so_frameOfHorizontal?=?_so_frameOfHorizontal;
其實(shí)吧茄袖,這個地方筆者感覺設(shè)計得比較雞肋,容筆者有更好的想法的時候更新組件九串。
寫在后面
看到這里可能有的朋友有些蒙绞佩,這通篇都說些什么,沒一句完整的代碼猪钮。哈哈品山,實(shí)際上這就是組件的核心,是我花了許多時間做的一些思考和總結(jié)烤低,科普基礎(chǔ)知識挺費(fèi)勁的肘交,百度就是一大篇一大篇的,我相信本文的價值還是有的扑馁。
越來越覺得有位朋友的話很有道理:編程是靠思維的東西涯呻。
希望大家共勉~