簡介
近日在製作一個開源加密相簿時附帶著設計了一個照片瀏覽器,在進一步優化後釋出到了GitHub供大家使用,該框架雖然沒有MWPhotoBrowser那麼強大,但是使用起來更為方便,操作更符合常規相簿習慣,自定義和修改原始碼也十分簡單。
本文主要介紹這個照片瀏覽器框架的技術要點,如果要深入研究和使用,可以在下面的連結中下載原始碼。
如果你對這個框架有興趣,可以點選這裡前去GitHub下載原始碼,歡迎Star與指出不足。
效果圖
縮圖預覽,點選縮圖進入原圖瀏覽,點選底部工具欄可以進入編輯模式。
批量匯出與刪除,通過底部工具欄操作。
檢視原圖,單擊可以隱藏導航欄和工具欄,支援雙擊切換縮放狀態、捏和手勢以及左右滑動切圖。
功能與特點
- block資料來源
照片瀏覽器的資料來源是通過block回撥的,通過實現相應的block並且提供資料模型即可完成圖片顯示。 - 記憶體優化
高解析度的圖片在讀入到記憶體後的記憶體佔用是十分可觀的,因此在點選縮圖進入原圖瀏覽後,由於要左右滑動來檢視其它圖片的原圖,因此至少載入三張原圖(不考慮邊緣情況),分別是當前檢視的圖片和與之相鄰的圖片,而其他圖片則先載入縮圖,在滾動到那些圖片時才去載入原圖以及與之相鄰的原圖,並且替換遠處的原圖為縮圖。 - 滾動優化
在滾動完全結束後才去載入原圖並替換縮圖,以防止滾動時卡頓。 - 同時支援本地與網路圖片
通過URL的型別來判斷圖片是否來自網路,如果來自網路則非同步下載並顯示進度,同時進行快取。 - 原圖瀏覽時支援常見的手勢
原圖瀏覽器時支援單擊隱藏和顯示導航欄和工具條,雙擊在適應螢幕和原始尺寸之間切換,捏和手勢可以縮放圖片,左右滑動可以切換圖片。 - 支援批量匯出與刪除照片
可以通過工具欄進入編輯模式來批量處理圖片的匯出與刪除。
技術要點
概述
照片瀏覽器框架依賴了SDWebImage和MBProgressHUD,前者用於處理圖片的非同步下載與快取,後者用於顯示圖片下載的進度。用於縮圖顯示的是collectionView,檢視原圖時每一張圖片都被均勻排列在scrollView上,每一張圖片也被包裹了一個scrollView用於處理縮放。
block資料來源
使用代理模式回撥資料來源會使得程式碼較為分散,因此本框架使用了block來回撥,在SGPhotoBrowser
中有四個資料來源block,通過實現他們並且提供相應的資料即可完成圖片顯示,這四個block如下面程式碼所示。
1 2 3 4 |
@property (nonatomic, copy, readonly) SGPhotoBrowserDataSourceNumberBlock numberOfPhotosHandler; @property (nonatomic, copy, readonly) SGPhotoBrowserDataSourcePhotoBlock photoAtIndexHandler; @property (nonatomic, copy, readonly) SGPhotoBrowserReloadRequestBlock reloadHandler; @property (nonatomic, copy, readonly) SGPhotoBrowserDeletePhotoAtIndexBlock deleteHandler; |
每個照片通過一個SGPhotoModel
資料模型類要描述,其中包含了photoURL與thumbURL,分別代表原圖和縮圖的URL,通過URL是否是fileURL來決定是否要非同步下載快取。
block資料來源在縮圖瀏覽時被collectionView的dataSource所呼叫,在原圖瀏覽時被呼叫以獲取特定位置的圖片URL或進行刪除照片後的資料重新整理。
記憶體優化
在檢視原圖時,載入當前位置和與其相鄰位置的原圖,其他位置均載入縮圖,在滑動過程中,動態的切換原圖的載入位置並將原來位置的原圖替換為縮圖,以保證記憶體中最多有三張原圖被載入以節省記憶體,具體實現程式碼如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 點選index處的縮圖時呼叫,來顯示原圖 - (void)loadImageAtIndex:(NSInteger)index { // 通過browser的資料來源方法獲取模型數量 NSInteger count = self.browser.numberOfPhotosHandler(); // 遍歷所有照片模型以及照片檢視 for (NSInteger i = 0; i < count; i++) { SGPhotoModel *model = self.browser.photoAtIndexHandler(i); SGZoomingImageView *imageView = self.imageViews[i]; NSURL *photoURL = model.photoURL; NSURL *thumbURL = model.thumbURL; // index位置和與其相鄰的位置載入原圖 if (i >= index - 1 && i <= index + 1) { if (imageView.isOrigin) continue; // 根據URL選擇圖片是直接從本地載入還是非同步下載快取的方法 [imageView.innerImageView sg_setImageWithURL:photoURL model:model]; // 用於指示這個imageView是否載入的是原圖 imageView.isOrigin = YES; // 縮放至適應螢幕 [imageView scaleToFitAnimated:NO]; } else { // 對於其他位置的圖片,如果是原圖,則替換為縮圖 if (!imageView.isOrigin) continue; [imageView.innerImageView sg_setImageWithURL:thumbURL model:model]; imageView.isOrigin = NO; [imageView scaleToFitAnimated:NO]; } } } |
滾動優化
在scrollView的滾動效果尚未停止時進行耗時操作會造成卡頓,為了避免這種情況,可以在scrollView減速完畢後再進行耗時操作。在本框架中,在左右滑動切換圖片時,如果立即載入原圖,會造成卡頓,因此在scrollView減速完畢後才將縮圖替換為原圖,具體實現如下。
1 2 3 4 5 6 7 8 9 10 11 |
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // 先通過偏移量計算出當前滾動到的圖片的索引 CGFloat offsetX = scrollView.contentOffset.x; NSInteger index = (offsetX + _pageW * 0.5f) / _pageW; // 索引發生變化時才更新並載入原圖 if (_index != index) { _index = index; // 上文提到的載入原圖的方法 [self loadImageAtIndex:_index]; } } |
本地圖片與網路圖片的處理
所有的圖片都是通過URL進行設定,通過為UIImageView新增分類,並新增方法sg_setImageWithURL:model:方法,傳入當前要載入的圖片的URL以及照片模型,在方法內,通過URL型別來判斷是否要進行非同步下載和快取,在非同步下載時,使用MBProgressHUD來指示進度,具體程式碼如下。
1 2 3 4 5 6 7 8 |
@interface UIImageView (SGExtension) // 通過動態繫結來實現為UIImageView新增屬性 @property (nonatomic, weak) MBProgressHUD *hud; @property (nonatomic, strong) SGPhotoModel *model; - (void)sg_setImageWithURL:(NSURL *)url model:(SGPhotoModel *)model; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
@implementation UIImageView (SGExtension) // 動態繫結hud和model兩個屬性的key static char hudKey; static char modelKey; // 由於分類不允許新增屬性,因此需要手動實現setter與getter @dynamic hud; @dynamic model; - (void)sg_setImageWithURL:(NSURL *)url { if (![url isFileURL]) { // 如果不是檔案URL,則說明需要下載,通過SDWebImage處理 SDImageCache *cache = [SDImageCache sharedImageCache]; SDWebImageManager *mgr = [SDWebImageManager sharedManager]; NSString *key = [mgr cacheKeyForURL:url]; // 如果在快取中找到了圖片,則直接載入並返回 if ([cache diskImageExistsWithKey:key] || ([cache imageFromMemoryCacheForKey:key] != nil)) { [self sd_setImageWithURL:url]; return; } // 如果已經有了進度指示器,則說明正在下載圖片,直接返回 if (self.hud != nil) { return; } // 圖片需要下載,且任務還未開始,通過MBProgressHUD指示下載進度,通過SDWebImage來下載和快取圖片 MBProgressHUD *hud = [[MBProgressHUD alloc] initWithView:self]; self.hud = hud; hud.mode = MBProgressHUDModeAnnularDeterminate; [self addSubview:hud]; [hud showAnimated:YES]; // 如果對應於當前原圖的縮圖已經下載完成,則先在原圖瀏覽中顯示縮圖作為佔點陣圖,否則顯示預設的黑色圖片。 UIImage *placeHolderImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"SGPhotoBrowser.bundle/ImagePlaceholder.png" ofType:nil]]; if (self.model.thumbURL) { NSString *key = [mgr cacheKeyForURL:self.model.thumbURL]; UIImage *tempImage = [cache imageFromMemoryCacheForKey:key]; if (tempImage == nil) { tempImage = [cache imageFromDiskCacheForKey:key]; } if (tempImage) { placeHolderImage = tempImage; } } [self sd_setImageWithURL:url placeholderImage:placeHolderImage options:SDWebImageRetryFailed progress:^(NSInteger receivedSize, NSInteger expectedSize) { hud.progress = (float)receivedSize / expectedSize; } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { [hud removeFromSuperview]; self.hud = nil; }]; } else { // 對於檔案URL,直接從檔案系統中載入 self.image = [UIImage imageWithContentsOfFile:url.path]; } } // 公共方法,由於佔點陣圖相關邏輯需要縮圖URL,因此需要傳遞model,上面的方法為私有方法 - (void)sg_setImageWithURL:(NSURL *)url model:(SGPhotoModel *)model { self.model = model; [self sg_setImageWithURL:url]; } // 動態繫結的兩屬性的getter和setter #pragma mark - Setter - (void)setHud:(MBProgressHUD *)hud { objc_setAssociatedObject(self, &hudKey, hud, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)setModel:(SGPhotoModel *)model { objc_setAssociatedObject(self, &modelKey, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - Getter - (MBProgressHUD *)hud { return objc_getAssociatedObject(self, &hudKey); } - (SGPhotoModel *)model { return objc_getAssociatedObject(self, &modelKey); } @end |
原圖瀏覽時的手勢處理
每張圖片使用一個scrollView包裹來處理捏合手勢縮放,同時通過touchesEnded::方法來判斷單擊和雙擊,由於雙擊時會經過單擊狀態,這裡將單擊事件滯後0.2s處理,如果在這期間觸發了雙擊,則取消單擊事件的處理,實現如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint touchPt = [touch locationInView:self.innerImageView]; self.currentTouchPoint = touchPt; NSInteger tapCount = touch.tapCount; switch (tapCount) { case 1: // 延時執行,防止和雙擊事件重疊 [self performSelector:@selector(handleSingleTap) withObject:nil afterDelay:0.2]; break; case 2: [self handleDoubleTap]; break; default: break; } [[self nextResponder] touchesEnded:touches withEvent:event]; } - (void)handleDoubleTap { // 取消單擊事件 [NSObject cancelPreviousPerformRequestsWithTarget:self]; // 在適應螢幕和原始尺寸之間翻轉圖片的顯示狀態 [self toggleStateAnimated:YES]; } |
圖片的批量處理
在照片的資料模型SGPhotoModel
上有一個isSelected屬性來判斷當前圖片是否被選中,通過collectionView的代理方法didUnhighlightItemAtIndexPath:來處理圖片的選中與反選,為了統一點選事件,將點選縮圖進入原圖瀏覽模式的程式碼也放到了這裡,通過是否是編輯模式來區分,編輯模式由於和工具欄直接相關,因此被記錄在工具欄中,具體實現程式碼如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath { SGPhotoCell *cell = (SGPhotoCell *)[collectionView cellForItemAtIndexPath:indexPath]; // 如果處於編輯模式,則處理圖片的選中和反選並返回 if (self.toolBar.isEditing) { SGPhotoModel *model = self.photoAtIndexHandler(indexPath.row); model.isSelected = !model.isSelected; // 記錄所有選中的圖片資料模型 if (model.isSelected) { [self.selectModels addObject:model]; } else { [self.selectModels removeObject:model]; } cell.model = model; return; } // 如果縮圖在下載中,則不允許進入原圖瀏覽,hud用於指示下載進度,因此有hud則正在下載 if (cell.imageView.hud) return; // 如果縮圖已經下載完畢,則允許進入原圖瀏覽模式 SGPhotoViewController *vc = [SGPhotoViewController new]; vc.browser = self; vc.index = indexPath.row; [self.navigationController pushViewController:vc animated:YES]; } |
更多技術細節可以在GitHub上的原始碼中檢視,點選這裡前去GitHub下載原始碼,歡迎Star和指出不足。