iOS开源照片浏览器框架SGPhotoBrowser的设计与实现

简介

近日在制作一个开源加密相册时附带着设计了一个照片浏览器,在进一步优化后发布到了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两个属性的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处理,如果在这期间触发了双击,则取消单击事件的处理,实现如下。

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

转载于:https://www.cnblogs.com/aiwz/p/6153999.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/470342.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Koa框架

一、认识Koa 前面我们已经学习了express&#xff0c;另外一个非常流行的Node Web服务器框架就是Koa。 Koa官方的介绍&#xff1a; koa&#xff1a;next generation web framework for node.js&#xff1b;koa&#xff1a;node.js的下一代web框架&#xff1b; 事实上&#x…

Node使用MySQL

一、认识mysql2 如何可以在Node的代码中执行SQL语句来&#xff0c;这里我们可以借助于两个库&#xff1a; mysql&#xff1a;最早的Node连接MySQL的数据库驱动&#xff1b;mysql2&#xff1a;在mysql的基础之上&#xff0c;进行了很多的优化、改进&#xff1b;目前相对来说&a…

java安装后在哪里打开_冷却塔声屏障安装后降噪效果不理想,原因出在哪里?...

现有冷却塔大部分产品都不能满足噪音环保标准&#xff0c;只有少数几种低吨位超低噪声型号的冷却塔可以满足少部分区域噪声标准要求。在一些商住混合区&#xff0c;冷却塔的出风口强烈的噪声和热气往往干扰附近居民不得安宁&#xff0c;所以冷却塔的噪声问题成为环境投诉的热点…

iOS10 权限崩溃问题

iOS10 权限崩溃问题 原文: http://blog.csdn.net/runleelrg/article/details/51673025 今天 手机升级了 iOS10 Beta&#xff0c;然后用正在开发的项目 装了个ipa包&#xff0c;发现点击有关 权限访问 直接Crash了&#xff0c;并在控制台输出了一些信息&#xff1a; This app h…

Koa框架——coderhub实战

https://github.com/zep03/koa-coderhub/commits/main 一、coderhub功能接口说明 Coderhub旨在创建一个程序员分享生活动态的平台。 完整的项目接口包括&#xff1a; 面向用户的业务接口&#xff1b;面向企业或者内部的后台管理接口&#xff1b; 完成的功能如下&#xff1a;…

python 协程_Python多任务协程

协程协程&#xff0c;又称微线程&#xff0c;纤程。英文名Coroutine。协程是python个中另外一种实现多任务的方式&#xff0c;只不过比线程更小占用更小执行单元(理解为需要的资源)。为啥说它是一个执行单元&#xff0c;因为它自带CPU上下文。这样只要在合适的时机&#xff0c;…

JavaScript高级之构造函数和原型

1.1 概述 在典型的 OOP 的语言中&#xff08;如 Java&#xff09;&#xff0c;都存在类的概念&#xff0c;类就是对象的模板&#xff0c;对象就是类的实例&#xff0c;但在 ES6之前&#xff0c; JS 中并没用引入类的概念。 ES6&#xff0c; 全称 ECMAScript 6.0 &#xff0c;2…

JavaScript高级之继承

ES6之前并没有给我们提供 extends 继承。我们可以通过构造函数原型对象模拟实现继承&#xff0c;被称为组合继承。 2.1 call() 调用这个函数, 并且修改函数运行时的 this 指向 fun.call(thisArg, arg1, arg2, …) thisArg &#xff1a;当前调用函数 this 的指向对象arg1&…

【代码笔记】iOS-UILable电子表显示

一&#xff0c;效果图。 二&#xff0c;代码。 RootViewController.h #import <UIKit/UIKit.h>interface RootViewController : UIViewController {UILabel *label;NSDateFormatter *dateFormatter ; } end RootViewController.m #import "RootViewController.h&qu…

JavaScript高级之ES5 中的新增方法

3.1 ES5 新增方法概述 ES5 中给我们新增了一些方法&#xff0c;可以很方便的操作数组或者字符串&#xff0c;这些方法主要包括&#xff1a; 数组方法字符串方法对象方法 3.2 数组方法 迭代(遍历)方法&#xff1a;forEach()、map()、filter()、some()、every()&#xff1b; …

JavaScript高级之函数进阶

1. 函数的定义和调用 1.1 函数的定义方式 函数声明方式 function 关键字 (命名函数)函数表达式 (匿名函数)new Function() Function 里面参数都必须是字符串格式第三种方式执行效率低&#xff0c;也不方便书写&#xff0c;因此较少使用所有函数都是 Function 的实例(对象)…

react 前端解析二进制流_一年半前端跳槽面试经验(头条、微信、shopee)

在2019年末的时候&#xff0c;突然想搞点大事&#xff0c;思来想去&#xff0c;感觉只有跳槽是最刺激的。由于我比较懒&#xff0c;不想换城市&#xff0c;所以这次只面试了头条、微信和 shopee。十分幸运&#xff0c;都拿到了 offer。接下来就简单的说下大家关心的面试题吧。问…

国内app快速生成平台对比

泰格老虎 2013-03-07 00:39:10 这是海恒CEO高鹏写的一篇国内app快速生成平台对比文章&#xff0c;介绍了国内快速生成APP的平台与自己平台的对比&#xff0c;很有参考价值。同类网站安米网 http://www.appbyme.org/追信 http://app.zhui.cn/简网app工厂 http://app.cutt.com/ap…

JavaScript高级之正则表达式

1. 正则表达式概述 1.1 什么是正则表达式 正则表达式&#xff08; Regular Expression &#xff09;是用于匹配字符串中字符组合的模式。在 JavaScript中&#xff0c;正则表达式也是对象。 正则表通常被用来检索、替换那些符合某个模式&#xff08;规则&#xff09;的文本&am…

pushpop指令的操作数必须是字操作数_PLC的指令,电气人必须了解的基础内容

指令语句表编程语言是所有PLC都具有的最基本的编程语言。而指令语句表程序是由一条一条的指令堆砌而成的。因此&#xff0c;我们有必要对指令进行进一步的说明和解读。1、指令格式PLC的指令语句表程序和微机汇编语言程序非常的相似&#xff0c;我们也是以汇编语言的指令和指令系…

JavaScript高级之ECMAScript 6 新特性

2.1. let关键字 let关键字用来声明变量&#xff0c;使用 let声明的变量有几个特点&#xff1a; 不允许重复声明 块儿级作用域 不存在变量提升 不影响作用域链 应用场景&#xff1a;以后声明变量使用let就对了 案例&#xff1a;点击切换颜色 <!DOCTYPE html&g…

sql 关联使用id还是code_R语言实例:用glue批量生成SQL语句

背景在数据开发中&#xff0c;有些情况下&#xff0c;需要手动生成批量SQL&#xff0c;只需改变某个参数&#xff0c;比如日期&#xff0c;从某天到某天。之前有一个实例&#xff0c;是用 stringr::str_replace_all() 去实现&#xff0c;这次就用 glue 来做示例&#xff0c;会更…

JavaScript高级之ECMASript 7、8 、9 、10 新特性

第3章 ECMASript 7 新特性 3.1. Array.prototype.includes Includes 方法用来检测数组中是否包含某个元素&#xff0c;返回布尔类型值 3.2. 指数操作符 在ES7中引入指数运算符「 **」&#xff0c;用来实现幂运算&#xff0c;功能与 Math.pow结果相同 第4章 ECMASript 8 新特…

swagger core 和 swagger ui 如何关联【窥探】

几个片段&#xff1a; package io.swagger.jaxrs.listing;import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils;import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.ws.rs.GET; import javax.ws.r…

vb 根据pid获取句柄_C++中避免返回指向对象内部的句柄(handles)

点蓝色字关注“CurryCoder的程序人生”微信公众号&#xff1a;CurryCoder的程序人生欢迎关注我&#xff0c;一起学习&#xff0c;一起进步!1.问题的引入假如你正在给一个应用写一个矩形类&#xff0c;这个矩形由左上角和右下角的顶点坐标表示。为了表示这两个点&#xff0c;我们…