iOS開源照片瀏覽器框架SGPhotoBrowser的設(shè)計(jì)與實(shí)現(xiàn)

簡介

近日在制作一個(gè)開源加密相冊(cè)時(shí)附帶著設(shè)計(jì)了一個(gè)照片瀏覽器,在進(jìn)一步優(yōu)化后發(fā)布到了GitHub供大家使用,該框架雖然沒有MWPhotoBrowser那么強(qiáng)大,但是使用起來更為方便,操作更符合常規(guī)相冊(cè)習(xí)慣,自定義和修改源碼也十分簡單。
本文主要介紹這個(gè)照片瀏覽器框架的技術(shù)要點(diǎn),如果要深入研究和使用,可以在下面的鏈接中下載源碼。

如果你對(duì)這個(gè)框架有興趣,可以點(diǎn)擊這里前去GitHub下載源碼,歡迎Star與指出不足。

效果圖

縮略圖預(yù)覽,點(diǎn)擊縮略圖進(jìn)入原圖瀏覽,點(diǎn)擊底部工具欄可以進(jìn)入編輯模式。


批量導(dǎo)出與刪除,通過底部工具欄操作。

查看原圖,單擊可以隱藏導(dǎo)航欄和工具欄,支持雙擊切換縮放狀態(tài)、捏和手勢(shì)以及左右滑動(dòng)切圖。

功能與特點(diǎn)

  • block數(shù)據(jù)源
    照片瀏覽器的數(shù)據(jù)源是通過block回調(diào)的,通過實(shí)現(xiàn)相應(yīng)的block并且提供數(shù)據(jù)模型即可完成圖片顯示。

  • 內(nèi)存優(yōu)化
    高分辨率的圖片在讀入到內(nèi)存后的內(nèi)存占用是十分可觀的,因此在點(diǎn)擊縮略圖進(jìn)入原圖瀏覽后,由于要左右滑動(dòng)來查看其它圖片的原圖,因此至少加載三張?jiān)瓐D(不考慮邊緣情況),分別是當(dāng)前查看的圖片和與之相鄰的圖片,而其他圖片則先加載縮略圖,在滾動(dòng)到那些圖片時(shí)才去加載原圖以及與之相鄰的原圖,并且替換遠(yuǎn)處的原圖為縮略圖。

  • 滾動(dòng)優(yōu)化
    在滾動(dòng)完全結(jié)束后才去加載原圖并替換縮略圖,以防止?jié)L動(dòng)時(shí)卡頓。

  • 同時(shí)支持本地與網(wǎng)絡(luò)圖片
    通過URL的類型來判斷圖片是否來自網(wǎng)絡(luò),如果來自網(wǎng)絡(luò)則異步下載并顯示進(jìn)度,同時(shí)進(jìn)行緩存。

  • 原圖瀏覽時(shí)支持常見的手勢(shì)
    原圖瀏覽器時(shí)支持單擊隱藏和顯示導(dǎo)航欄和工具條,雙擊在適應(yīng)屏幕和原始尺寸之間切換,捏和手勢(shì)可以縮放圖片,左右滑動(dòng)可以切換圖片。

  • 支持批量導(dǎo)出與刪除照片
    可以通過工具欄進(jìn)入編輯模式來批量處理圖片的導(dǎo)出與刪除。

技術(shù)要點(diǎn)

概述

照片瀏覽器框架依賴了SDWebImage和MBProgressHUD,前者用于處理圖片的異步下載與緩存,后者用于顯示圖片下載的進(jìn)度。用于縮略圖顯示的是collectionView,查看原圖時(shí)每一張圖片都被均勻排列在scrollView上,每一張圖片也被包裹了一個(gè)scrollView用于處理縮放。

block數(shù)據(jù)源

使用代理模式回調(diào)數(shù)據(jù)源會(huì)使得代碼較為分散,因此本框架使用了block來回調(diào),在SGPhotoBrowser中有四個(gè)數(shù)據(jù)源block,通過實(shí)現(xiàn)他們并且提供相應(yīng)的數(shù)據(jù)即可完成圖片顯示,這四個(gè)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;

每個(gè)照片通過一個(gè)SGPhotoModel數(shù)據(jù)模型類要描述,其中包含了photoURL與thumbURL,分別代表原圖和縮略圖的URL,通過URL是否是fileURL來決定是否要異步下載緩存。
block數(shù)據(jù)源在縮略圖瀏覽時(shí)被collectionView的dataSource所調(diào)用,在原圖瀏覽時(shí)被調(diào)用以獲取特定位置的圖片URL或進(jìn)行刪除照片后的數(shù)據(jù)刷新。

內(nèi)存優(yōu)化

在查看原圖時(shí),加載當(dāng)前位置和與其相鄰位置的原圖,其他位置均加載縮略圖,在滑動(dòng)過程中,動(dòng)態(tài)的切換原圖的加載位置并將原來位置的原圖替換為縮略圖,以保證內(nèi)存中最多有三張?jiān)瓐D被加載以節(jié)省內(nèi)存,具體實(shí)現(xiàn)代碼如下。

// 點(diǎn)擊index處的縮略圖時(shí)調(diào)用,來顯示原圖
- (void)loadImageAtIndex:(NSInteger)index {
    // 通過browser的數(shù)據(jù)源方法獲取模型數(shù)量
    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;
            // 根據(jù)URL選擇圖片是直接從本地加載還是異步下載緩存的方法
            [imageView.innerImageView sg_setImageWithURL:photoURL model:model];
            // 用于指示這個(gè)imageView是否加載的是原圖
            imageView.isOrigin = YES;
            // 縮放至適應(yīng)屏幕
            [imageView scaleToFitAnimated:NO];
        } else {
            // 對(duì)于其他位置的圖片,如果是原圖,則替換為縮略圖
            if (!imageView.isOrigin) continue;
            [imageView.innerImageView sg_setImageWithURL:thumbURL model:model];
            imageView.isOrigin = NO;
            [imageView scaleToFitAnimated:NO];
        }
    }
}

滾動(dòng)優(yōu)化

在scrollView的滾動(dòng)效果尚未停止時(shí)進(jìn)行耗時(shí)操作會(huì)造成卡頓,為了避免這種情況,可以在scrollView減速完畢后再進(jìn)行耗時(shí)操作。在本框架中,在左右滑動(dòng)切換圖片時(shí),如果立即加載原圖,會(huì)造成卡頓,因此在scrollView減速完畢后才將縮略圖替換為原圖,具體實(shí)現(xiàn)如下。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // 先通過偏移量計(jì)算出當(dāng)前滾動(dòng)到的圖片的索引
    CGFloat offsetX = scrollView.contentOffset.x;
    NSInteger index = (offsetX + _pageW * 0.5f) / _pageW;
    // 索引發(fā)生變化時(shí)才更新并加載原圖
    if (_index != index) {
        _index = index;
        // 上文提到的加載原圖的方法
        [self loadImageAtIndex:_index];
    }
}

本地圖片與網(wǎng)絡(luò)圖片的處理

所有的圖片都是通過URL進(jìn)行設(shè)置,通過為UIImageView添加分類,并添加方法sg_setImageWithURL:model:方法,傳入當(dāng)前要加載的圖片的URL以及照片模型,在方法內(nèi),通過URL類型來判斷是否要進(jìn)行異步下載和緩存,在異步下載時(shí),使用MBProgressHUD來指示進(jìn)度,具體代碼如下。

@interface UIImageView (SGExtension)
// 通過動(dòng)態(tài)綁定來實(shí)現(xiàn)為UIImageView添加屬性
@property (nonatomic, weak) MBProgressHUD *hud;
@property (nonatomic, strong) SGPhotoModel *model;

- (void)sg_setImageWithURL:(NSURL *)url model:(SGPhotoModel *)model;

@end
@implementation UIImageView (SGExtension)
// 動(dòng)態(tài)綁定hud和model兩個(gè)屬性的key
static char hudKey;
static char modelKey;
// 由于分類不允許添加屬性,因此需要手動(dòng)實(shí)現(xiàn)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;
        }
        // 如果已經(jīng)有了進(jìn)度指示器,則說明正在下載圖片,直接返回
        if (self.hud != nil) {
            return;
        }
        // 圖片需要下載,且任務(wù)還未開始,通過MBProgressHUD指示下載進(jìn)度,通過SDWebImage來下載和緩存圖片
        MBProgressHUD *hud = [[MBProgressHUD alloc] initWithView:self];
        self.hud = hud;
        hud.mode = MBProgressHUDModeAnnularDeterminate;
        [self addSubview:hud];
        [hud showAnimated:YES];
        // 如果對(duì)應(yīng)于當(dāng)前原圖的縮略圖已經(jīng)下載完成,則先在原圖瀏覽中顯示縮略圖作為占位圖,否則顯示默認(rèn)的黑色圖片。
        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 {
        // 對(duì)于文件URL,直接從文件系統(tǒng)中加載
        self.image = [UIImage imageWithContentsOfFile:url.path];
    }
}
// 公共方法,由于占位圖相關(guān)邏輯需要縮略圖URL,因此需要傳遞model,上面的方法為私有方法
- (void)sg_setImageWithURL:(NSURL *)url model:(SGPhotoModel *)model {
    self.model = model;
    [self sg_setImageWithURL:url];
}
// 動(dòng)態(tài)綁定的兩屬性的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

原圖瀏覽時(shí)的手勢(shì)處理

每張圖片使用一個(gè)scrollView包裹來處理捏合手勢(shì)縮放,同時(shí)通過touchesEnded::方法來判斷單擊和雙擊,由于雙擊時(shí)會(huì)經(jīng)過單擊狀態(tài),這里將單擊事件滯后0.2s處理,如果在這期間觸發(fā)了雙擊,則取消單擊事件的處理,實(shí)現(xiàn)如下。

- (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:
            // 延時(shí)執(zhí)行,防止和雙擊事件重疊
            [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];
    // 在適應(yīng)屏幕和原始尺寸之間翻轉(zhuǎn)圖片的顯示狀態(tài)
    [self toggleStateAnimated:YES];
}

圖片的批量處理

在照片的數(shù)據(jù)模型SGPhotoModel上有一個(gè)isSelected屬性來判斷當(dāng)前圖片是否被選中,通過collectionView的代理方法didUnhighlightItemAtIndexPath:來處理圖片的選中與反選,為了統(tǒng)一點(diǎn)擊事件,將點(diǎn)擊縮略圖進(jìn)入原圖瀏覽模式的代碼也放到了這里,通過是否是編輯模式來區(qū)分,編輯模式由于和工具欄直接相關(guān),因此被記錄在工具欄中,具體實(shí)現(xiàn)代碼如下。

- (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;
        // 記錄所有選中的圖片數(shù)據(jù)模型
        if (model.isSelected) {
            [self.selectModels addObject:model];
        } else {
            [self.selectModels removeObject:model];
        }
        cell.model = model;
        return;
    }
    // 如果縮略圖在下載中,則不允許進(jìn)入原圖瀏覽,hud用于指示下載進(jìn)度,因此有hud則正在下載
    if (cell.imageView.hud) return;
    // 如果縮略圖已經(jīng)下載完畢,則允許進(jìn)入原圖瀏覽模式
    SGPhotoViewController *vc = [SGPhotoViewController new];
    vc.browser = self;
    vc.index = indexPath.row;
    [self.navigationController pushViewController:vc animated:YES];
}

更多技術(shù)細(xì)節(jié)可以在GitHub上的源碼中查看,點(diǎn)擊這里前去GitHub下載源碼,歡迎Star和指出不足。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 緣起 那時(shí),我想要一個(gè)這樣的圖片瀏覽器: 從小圖進(jìn)入大圖瀏覽時(shí),使用轉(zhuǎn)場(chǎng)動(dòng)畫 可加載網(wǎng)絡(luò)圖片,且過渡自然,不阻塞操...
    囧書閱讀 7,159評(píng)論 35 107
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,026評(píng)論 4 61
  • 簡介 雖然目前市面上有一些不錯(cuò)的加密相冊(cè)App,但不是內(nèi)置廣告,就是對(duì)上傳的張數(shù)有所限制。本文介紹了一個(gè)加密相冊(cè)的...
    Soulghost閱讀 673評(píng)論 0 7
  • 復(fù)習(xí) copy與retain的區(qū)別: copy是創(chuàng)建一個(gè)新對(duì)象,retain是創(chuàng)建一個(gè)指針,引用對(duì)象計(jì)數(shù)加1。Co...
    葉飛樓閱讀 240評(píng)論 0 0

友情鏈接更多精彩內(nèi)容