iOS開源照片瀏覽器架構SGPhotoBrowser的設計與實現

來源:互聯網
上載者:User

標籤:

簡介

近日在製作一個開源加密相簿時附帶著設計了一個照片瀏覽器,在進一步最佳化後發布到了GitHub供大家使用,該架構雖然沒有MWPhotoBrowser那麼強大,但是使用起來更為方便,操作更符合常規相簿習慣,自訂和修改源碼也十分簡單。
本文主要介紹這個照片瀏覽器架構的技術要點,如果要深入研究和使用,可以在下面的連結中下載源碼。

如果你對這個架構有興趣,可以點擊這裡前去GitHub下載源碼,歡迎Star與指出不足

縮圖預覽,點擊縮圖進入原圖瀏覽,點擊底部工具列可以進入編輯模式。

大量匯出與刪除,通過底部工具列操作。

查看原圖,單擊可以隱藏導覽列和工具列,支援雙擊切換縮放狀態、捏和手勢以及左右滑動切圖。

功能與特點
  • block資料來源
    照片瀏覽器的資料來源是通過block回調的,通過實現相應的block並且提供資料模型即可完成圖片顯示。

  • 記憶體最佳化
    高解析度的圖片在讀入到記憶體後的記憶體佔用是十分可觀的,因此在點擊縮圖進入原圖瀏覽後,由於要左右滑動來查看其它圖片的原圖,因此至少載入三張原圖(不考慮邊緣情況),分別是當前查看的圖片和與之相鄰的圖片,而其他圖片則先載入縮圖,在滾動到那些圖片時才去載入原圖以及與之相鄰的原圖,並且替換遠處的原圖為縮圖。

  • 滾動最佳化
    在滾動完全結束後才去載入原圖並替換縮圖,以防止滾動時卡頓。

  • 同時支援本地與網狀圖片
    通過URL的類型來判斷圖片是否來自網路,如果來自網路則非同步下載並顯示進度,同時進行緩衝。

  • 原圖瀏覽時支援常見的手勢
    原圖瀏覽器時支援單擊隱藏和顯示導覽列和工具條,雙擊在適應螢幕和原始大小之間切換,捏和手勢可以縮放圖片,左右滑動可以切換圖片。

  • 支援大量匯出與刪除照片
    可以通過工具列進入編輯模式來批量處理圖片的匯出與刪除。

技術要點概述

照片瀏覽器架構依賴了SDWebImage和MBProgressHUD,前者用於處理圖片的非同步下載與緩衝,後者用於顯示圖片下載的進度。用於縮圖顯示的是collectionView,查看原圖時每一張圖片都被均勻排列在scrollView上,每一張圖片也被包裹了一個scrollView用於處理縮放。

block資料來源

使用代理模式回調資料來源會使得代碼較為分散,因此本架構使用了block來回調,在SGPhotoBrowser中有四個資料來源block,通過實現他們並且提供相應的資料即可完成圖片顯示,這四個block如下面代碼所示。

@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或進行刪除照片後的資料重新整理。

記憶體最佳化

在查看原圖時,載入當前位置和與其相鄰位置的原圖,其他位置均載入縮圖,在滑動過程中,動態切換原圖的載入位置並將原來位置的原圖替換為縮圖,以保證記憶體中最多有三張原圖被載入以節省記憶體,具體實現代碼如下。

// 點擊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減速完畢後才將縮圖替換為原圖,具體實現如下。

- (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來指示進度,具體代碼如下。

@interface UIImageView (SGExtension)// 通過動態綁定來實現為UIImageView添加屬性@property (nonatomic, weak) MBProgressHUD *hud;@property (nonatomic, strong) SGPhotoModel *model;- (void)sg_setImageWithURL:(NSURL *)url model:(SGPhotoModel *)model;@end
@implementation UIImageView (SGExtension)// 動態綁定hud和model兩個屬性的keystatic 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處理,如果在這期間觸發了雙擊,則取消單擊事件的處理,實現如下。

- (void)touchesEnded:(NSSet<UITouch *> *)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:來處理圖片的選中與反選,為了統一點擊事件,將點擊縮圖進入原圖瀏覽模式的代碼也放到了這裡,通過是否是編輯模式來區分,編輯模式由於和工具列直接相關,因此被記錄在工具列中,具體實現代碼如下。

- (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和指出不足。

iOS開源照片瀏覽器架構SGPhotoBrowser的設計與實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.