美文网首页DevSupportiOS Developer程序员
iOS图片浏览器(功能强大/性能优越)

iOS图片浏览器(功能强大/性能优越)

作者: 波儿菜 | 来源:发表于2018-04-20 18:09 被阅读2477次
图片浏览器效果

github地址:iOS图片浏览器组件

支持 cocopods,功能完善,性能不错,代码质量尚可,喜欢的朋友可以给个小星星😁。

为了适应组件的自定义需求,代码和逻辑有点多,所以尽量不要修改源码,有什么问题或者建议可以在本文或者 github issues 留言。

写在前面

更新日志:分页间距的逻辑及算法改造 —— 2018-4-27

本文讲解 YBImageBrowser 的组件设计思路和部分技术实现原理,对本框架有兴趣的朋友可以看看,若只是想使用该框架,请直接移步 github 。行文的重点是笔者的框架设计理念、代码及体验优化的思考、关键技术点的实现,希望不管是老鸟还是新手看完之后都能有所收获和感悟。

欢迎大家交流探讨,当然,笔者水平有限,若有大佬指教不胜感激🤝。

索引:
  • 一、组件框架整体设计
  • 二、组件中如何隐藏属性和方法
  • 三、拖拽动效的算法优化
  • 四、分页间距的算法优化(再次改造)
  • 五、内存的优化
  • 六、预下载和任务同步
  • 七、屏幕旋转UI适配

一、组件框架整体设计

其实对于图片浏览器,开源项目也有不少,不管是代码上还是功能上没有一个能完整的满足笔者的需求。所以笔者索性做了一个,力图将粒度做小,功能做全,当然这需要一个漫长的过程,空闲时间笔者会持续迭代和优化。

目前采用的是 UIViewController 做为底,上层是一个横向滚动的 UICollectionView ,在 UICollectionViewCell 上面是 UIScrollView ,当然还包括主要显示图片、动画图片、裁剪显示前景图片等。

使用 UICollectionView 是为了利用苹果为我们做的复用机制,不需要专门去实现,不然逻辑代码太多,得不偿失;而缩放的效果依托于 UIScrollView ;采用 UIViewController 为底是为了更好的控制旋转屏幕时的UI适配,之前也是考虑更轻一点的 UIView,但是它会受父视图的旋转影响,可能适配难度会翻几倍,而且使用 UIViewController 能更方便和优雅的实现图片浏览器的入场和出场动画。

二、组件中如何隐藏属性和方法

在做一个组件的时候,我们往往思考着向用户隐藏某些细节实现,一方面是为了避免用户的无意更改,一方面是为了简化 API 使其看起来更清爽。

对于属性,若想让用户只读不可写,可以在.h中对属性使用readonly修饰符;若根本不想要用户看到,可以直接将该属性创建在需要使用的目标类的.m文件内。

不过这样并不优雅,意味着我们很多代码和类必须搞到同一文件,才能达到外部无法直接访问,而内部可以访问的目的。若我们想分离多个文件好管理代码和实现更优秀的架构时,不得不将属性写到.h里面让其他文件可以访问。

那么,何不换一种思路?尽管我们将属性写在.m中隔离外部访问,实际上用户仍然可以用 KVC 的方式读写,那么我们框架组件内部为何不使用 KVC 进行读写?

于是,在组件的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 的键,组件内部就使用valueForKey:setValue:forKey: 通过这些常量来优雅的读写实例变量了。

对于方法的隐藏,组件中不将方法暴露在.h里面,只写在.m里面,然后组件其他文件通过<objc/message.h>下的objc_msgSend方法处理,比如随便截取一段代码:

    YBImageBrowserModelScaleImageSuccessBlock successBlock = ^(YBImageBrowserModel *backModel) {
       ...
    };
    ((void(*)(id, SEL, CGRect, YBImageBrowserModelScaleImageSuccessBlock)) objc_msgSend)(model, sel_registerName(YBImageBrowserModel_SELName_scaleImage), imageFrame, successBlock);

或者使用NSInvocation作为私有属性,外部也用 KVC 读写。

三、拖拽动效的算法优化

拖拽动效是目前很流行的图片浏览器出场效果,笔者看了好几个知名APP,“新浪微博”,“今日头条”,“QQ”,“QQ浏览器”,“微信”等都做了类似的动效,但是除了“微信”的效果人性化一点,其它的都有些不尽人意的地方。

这个效果咋一看比较简单,无非就是根据移动的距离,以某种数学关系移动图片并且缩小图片,实现可以直接计算frame或者使用CATransform3D等。

但是,有个容易忽略的问题,在拖动的时候我们希望看到的效果是图片跟随手指移动并且缩小,上图左右两种状态下的箭头指向的正是手指拖动触摸的点(理想状态),若写一个移动和缩放比例变化之间是线性的动画,手指触摸的点会是这种理想状态么?

答案是否定的,若移动的时候不缩放,是能达到理想状态,若缩放了状态二必然会是如下图所示:


拖动动效存在问题

处理方式:若是使用的动画相关的类库,可以考虑使用锚点来处理。本组件是使用frame的方式处理,通过一张图解释如何处理这个逻辑:

处理方式

实际上代码逻辑比看起来的复杂一些,有兴趣的可以看代码,这里只提出思路。

四、分页间距的算法优化

说起分页,几乎所有iOS工程师都会说.pagingEnabled属性,又说分页间距,稍有经验的工程师都会说重写UICollectionViewlayout,既创建一个UICollectionViewFlowLayout类重写约束。现在这里不浪费篇幅讨论 API 的用法,你只需要知道在重写的layout里面,几乎每一帧的界面都可以靠重写layoutAttributesForElementsInRect等方法重新计算。

按照常规的逻辑思路,最好想到的方案是:若当前是 第n页 时,所有的 Cell 都向左移动 (n-1) * 间距

确实,这种算法逻辑咋一看好像能解决问题,但当你滑到下图的情况下时,会发生奇怪的现象:


blog_pic3.png

你会发现在滑动到 第n页第n+1页 之间的临界点时,界面会突然向左或者向右跳动一段距离,因为这里就是上面所说方式判断移动的触发点,显然这不够平滑。

优化方案一

组件中笔者最初做法是,在每次重写布局时,都移动一个距离:当前偏移量 / 最大偏移量 * 总共页间距,这样就实现了“平滑”的移动。

隐晦问题:实际上这种算法的逻辑是完全正确的,但是在图片过多,需要移动的偏移量过大时,系统的UICollectionView的复用机制发生了问题,应该是复用时判断可视区域出现了偏差,直接导致系统放弃绘制实际上可视的区域(这个BUG很奇怪,不明白苹果工程师是如何设计集合视图layout和复用机制的关系的)。

优化方案二

在该组件开源的过程中,终于有一位技术朋友发现了这一BUG,所以不得不考虑其他方案。

首先,结合于方案一的结论,我们要尽量避免重写布局移动大量的距离,以防止集合视图的复用机制失效。

第一种方式是使用UIScrollView自己实现一套复用机制,但是考虑到时间成本,笔者果断放弃。

后来笔者灵光一现,思考出一种绝佳的方案:
找到离屏幕中心点最近的那个cell,然后将该cell的前一个cell向左移动一个间距,然后将该cell的后一个cell向右移动一个间距

完美解决😁。

五、内存的优化

由于如今的 APP 做的越来越复杂,作为一个合格的移动端程序员,我们需要时刻关注内存问题,虽然这并不是刚需。

本地图片的读取

在读取本地图片时,使用[UIImage imageNamed:]方式时系统会缓存该图片,而释放缓存的时机很微妙。所以在使用比较大、调用频率低的图片时,尽量使用读取文件的方式做:

[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:fileName ofType:fileType]]

超大图的处理

这样虽然能减少累加的内存,但若一张图片就非常大呢?系统将它解压过后将会占用比你想象中更大的内存,APP 可能变得非常卡顿甚至崩溃。

于是,组件中设置了一个 pt 的界限,当图片超过这个界限,组件会自动 异步压缩 到当前屏幕最大显示 pt 数量,当用户拖动或缩放放大图片时,组件会自动 异步裁剪 可视区域的图片,通过一张前景图片显示出来(当然裁剪也是有最大限度的)。

思路就两句话,实际逻辑结合其他功能会比较复杂,有兴趣可以看看代码,这里不过多阐述。

下载任务的释放

组件内部是利用SDWebImage做的下载和缓存,在每一个model释放的时候,都会将对应的下载任务取消已节约网络和内存开销。

六、预下载和任务同步

为了提高用户体验,在配置图片浏览器图片对应的model的时候,可以通过 API 设置异步预下载,当网络状况不错的时候,可能用户打开浏览器图片就下载好了,毕竟图片浏览器是有很短的创建时间和较长的入场时间的。

其实这也是一种提升效率的思维,我们要习惯性的去思考利用程序的空闲预先做一些任务,才能编写出高效的代码。

这里有一个点需要注意,若我们执行了预下载,而在图片浏览器打开的时候,图片仍未预下载完成,而此刻又会执行正式的下载,它们之间如何信息同步?

哈哈,其实很简单,就是将同一类的任务放到同一个地方统一管理,比如本组件就是将 图片下载、图片缓存、图片压缩、图片裁剪 等都放到图片数据模型 YBImageBrowserModel 中处理,其它地方就用方法调度这些任务,虽然可能会造成看起来比较多的方法调用,但是对稳定性、容错率的提高不容小觑。

这种思维很重要,可以不严密的理解为 AOP,功能分类集中管理。

七、屏幕旋转UI适配

找到组件必然支持的方向

组件支持了旋转功能,由于采用的是 UIViewController 作为底类,理所当然的是让组件内部子控件跟随 UIViewController 的旋转而旋转,目前不支持强制旋转,因为可能会有些麻烦,后期迭代考虑增加。

UIViewController 的旋转会直接受到工程 general -> deployment info -> Device Orientation 处的影响,所以,在判断组件支持的旋转方向的时候,需要取一个交集:

- (void)configSupportAutorotateTypes {
    UIApplication *application = [UIApplication sharedApplication];
    UIInterfaceOrientationMask keyWindowSupport = [application supportedInterfaceOrientationsForWindow:window];
    UIInterfaceOrientationMask selfSupport = ![self shouldAutorotate] ? UIInterfaceOrientationMaskPortrait : [self supportedInterfaceOrientations];
    supportAutorotateTypes = keyWindowSupport & selfSupport;
}

然后这个交集就是 UIViewController 可能旋转的方向,也就是组件可能旋转的方向。

布局更新时机优化

大家很容易就想到,当设备旋转过后,若组件支持该方向,就通知所有子界面刷新布局(可能有人会说用autolayout,但是考虑到效率和可控性方面的问题,本组件都采用frame处理)。

其实若你是这样做,已经满足了需求,剩下了可能就是繁杂的布局执行流。

然而我会说还能优化。试想一下,手机的两种竖屏状态(home在上,home在下),两种横屏状态(home在左,home在右),它们的frame是不是一样?

所以,这里需要加入一个标识,用来存储此时当前 UIView 显示的frame类型是“竖屏”还是“横屏”,而不是每一种屏幕状态变化都去做所有的布局更新,理论上提高了一倍的布局开销。

引入代理规范布局流程

由于通知子视图更新布局、存储当前视图分别在“竖屏”和“横屏”下的frame、存储当前适配的屏幕方向等信息是每一个视图几乎都会做的工作(虽然细节有些差异,但我们稍宏观的看这个问题)。

于是,组件做了一个代理:

@protocol YBImageBrowserScreenOrientationProtocol <NSObject>
@required
// 当前视图UI适配的屏幕方向
@property (nonatomic, assign) YBImageBrowserScreenOrientation so_screenOrientation;
// 当前视图在竖直屏幕的frame
@property (nonatomic, assign) CGRect so_frameOfVertical;
// 当前视图在横向屏幕的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

需要跟随屏幕旋转更新布局的UIView都实现这个代理,达到标准控制的目的,值得注意的是代理里面的属性需要自己在实现文件关联一个实例变量,类似于

@synthesize so_frameOfVertical = _so_frameOfVertical;
@synthesize so_frameOfHorizontal = _so_frameOfHorizontal;

其实吧,这个地方笔者感觉设计得比较鸡肋,容笔者有更好的想法的时候更新组件🤔。

写在后面

看到这里可能有的朋友有些蒙,这通篇都说些什么,没一句完整的代码。哈哈,实际上这就是组件的核心,是我花了许多时间做的一些思考和总结,科普基础知识挺费劲的,百度就是一大篇一大篇的,我相信本文的价值还是有的。

越来越觉得有位朋友的话很有道理:编程是靠思维的东西。

希望大家共勉~

相关文章

网友评论

  • 心里有个数:写的不错!赞一个!两个小问题,问一下大神。
    1、本地图片放在Assets里的时候,放大时获取图片路径的时候获取不到。
    2、图片浏览器状态栏怎么显示出来
    波儿菜:@心里有个数 目前 YBImage 和 YYImage 一样不支持 Assets 图片,不过我之后会优化,目前的话你可以返回一个 UIImage 对象(但是会警告);状态栏默认隐藏目前没有开放 API。
    这些作为待优化项,我这边空了之后处理。
  • 邓小帅:有没有办法点击放大后是本地图片的?看demo是一定要用url的
    邓小帅:@indulge_in 看到了,谢谢
    波儿菜:@邓小帅 可以啊 看api demo里面也有混合案例有配置本地图片的
  • 邓小帅:YBImageBrowser was resolved to 1.1.2, which depends on
    SDWebImage (~> 4.3.3)

    你这个指定SDWebImage的版本就很尴尬了
    邓小帅:@indulge_in OK, 那我先更新下本地索引再更新一次
    波儿菜:@邓小帅 新版本没有指定
  • 一直在跑:为什么我写了一个和你功能差不多的图片浏览。为什么查看和点赞的没多少人呢?而且我显示出来的时候还给了动画。完成度比微信还要好,就没有人查看蛋疼。
    一直在跑:@indulge_in 嗯嗯。你说的对,确实存在这些问题。以后多向你学习
    波儿菜:@一直在跑 做一个图片浏览器不是一件复杂的事情,但是做好却不容易。如何在功能全面的情况下保证拓展性?如何运用合理的设计模式架构代码?如何做好周全的容错?然后保证健壮性?
    大概看了一下你的代码,并不认同你所说的完成度比微信好,或者是和笔者这个库功能差不多。一个展示动画是很简单的东西,并不是炫酷就是更好的效果,花几分钟了解了一下,大概总结一下你这个代码的问题吧:
    1、代码不规范,风格混乱(这可能是最严重的问题)
    2、功能简陋
    3、定制性差
    4、拓展性差
    5、随意用了一下就发现很多BUG

    这篇文章是 YBImageBrowser 的第一版的技术分享,后面笔者花时间重做了 2.x,你可以看开头的链接看看 2.x 的技术分享。重构过程中实际上大部分时间都是花在思考上而不是coding上,你可以看到完整的 README,完整的演示 Demo,也支持 Pods。至于关注度可能和笔者长期更博积累的少许粉丝和点点人气有关吧。

    笔者比较直接,请见谅。说了这么多大概意思就是,足够多的思考、足够多的精力、足够多的测试,要让使用者能体会到你的认真和专业。
    一直在跑:@一直在跑 搞得我后来都不想维护和做视频版本了。我的用法也特别简单。就是没人可能我做的不够好。但是一直也查不到原因
  • 042a0e1be73f:刘海屏上,页数和保存分别在左右两个耳朵上,会影响审核吗?
    波儿菜:@iOSwift 现在是2.x版本 你是不是下的老版本
    042a0e1be73f:@indulge_in 你用模拟器跑一下。。。
    波儿菜:@iOSwift 不是在耳朵上的 是在下面安全区域的 猜测就算是在耳朵上也不会被拒吧
  • 4d4aa2a610a2:有一个简单的方法实现collection满屏分页间距,不用重写flowlayout,改变collection的宽度,然后在设置contentInset就可以达到这样的效果
    波儿菜:@Just_7 超屏绘制,始终感觉不太优雅
  • 牵绊Sunshine灬:能否支持视频呢
    波儿菜:新版已经支持
  • gxyDanny:自己写第三方,能不要依赖其他第三方就不要依赖,那才是合格的第三方,有些第三方所有的图标都是代码绘制,干净
    波儿菜:@gxyDanny 这只是一个小轮子 不依赖会很麻烦
  • 筑梦师Winston:可以,我依稀记得我在项目中用的还是仿微信的那款图片浏览器,这个很值得下载进行使用,并学习如何高效的构建一个图片浏览器!:relaxed::relaxed:
  • 虫子_Gray:�为什么 show的时候 没有动画,显得很生硬
  • TinaAndNike:老哥,你的框架需要pod~>SDWebImage4.3.3。但是开发中很多其他库目前都在用SDWebImage4.0.0,有点跟不上啊。
    Cocojiang:你更改成功了吗?
    TinaAndNike:@indulge_in 老哥 能加个Q吗
    波儿菜:@TinaAndNike 可以把我的代码拖进去,然后修改YBImageBrowserDownloader.m文件,改动很小。
  • 5fff1c1a968e:你好,我今天用代理写完,点击大图的时候有动画,消失的时候没有回位的动画,不知道问题在哪,支持瀑布流吗,我写在瀑布流上用的
    波儿菜:@酱酱君 如果回位的那张图片在屏幕中消失了 就没有回位动画了
  • handone:你好, 有个建议,是否能添加未加载完成前的默认图呢? 还有加载失败的图片。
    波儿菜:@handone 'SDWebImage/WebP' 这个文件如果直接导入工程,sd会自动解析webp格式图片么?没搞过这个格式。。
    handone:而且不支持webp格式的图片:joy:
    波儿菜:@handone YBImageBrowserModel.h 中有个属性 previewModel 就是预览图,readme有写。
    加载失败暂时不支持自定义,等我有时间了再考虑一下吧。
  • iOS更好的实现方案:要是同时支持视频就更完美了:smiley:
    波儿菜:新版已支持视频
  • handone:你好, 我发现个问题,加载网络图片时,点第一张图片时不会显示图片的index , 但如果先点击后面的再滑动到第一张就会显示了。
    handone:@indulge_in 但是又有了新问题:sweat: 如果一开始就点击第二张或者之后的图片, index就会显示当前是第一张, 但滑动之后index显示正常。
    handone:@indulge_in :+1:
    波儿菜:@handone 修复了 pods已更新
  • ttdiOS:可以缩放????每个图片
    波儿菜:@ttdiOS 可以的
  • de84319e78ab:你好,想请教一个问题,我才开始看您的源码,我看到YBImageBrowser.m里面你同时用到了成员变量和属性,请问为什么不全都用属性呢,有什么特别的考虑吗?因为在我平时工作的过程中没有发现在.m定义的属性和成员变量在生命周期和内存管理上有什么明显的区别.(个人见解,可能有误欢迎指正)
    波儿菜:@AndyTree 对
    de84319e78ab:@indulge_in 是指的getter和setter方法吗?
    波儿菜:@AndyTree 写属性的只是为了多两个方法去读写成员变量
  • a14bff135f0d:感觉写的很厉害,不过分页那块为啥不用内边距来实现呢
    a14bff135f0d:@indulge_in 图片之间的间距
    波儿菜:@洋洋红110 额 你是说集合视图左右超屏显示处理么?
  • b93b8f47b308:博主你好,文章写的很棒,请问可以转发你的文章吗?
    波儿菜:@帅气的码农哥 可以的 注明作者和出处就行了:blush:
  • 萧溪:厉害了我的GYB
  • 码农淏:厉害厉害
  • 那个谁_5207:大神路上越走越远了 :+1:
  • 乌黑的太阳:相片间距优雅的方法应该是把collectionview的宽度增加间距的宽度,cell上面的scrollview和imageview比cell少一个间距的宽度,开启pageenable
    波儿菜:@乌黑的太阳 也行 但是超屏显示感觉不那么优雅哈哈
  • f6ee6c2d7437:非常感谢,项目中正需要此功能,但是在集成的过程中,发现一点问题,就是当图片资源多的时候,大图页面滑到后面30左右的时候,黑屏。图片越多黑屏越多,看了您的源码目前还没找到出错的原因,您也帮忙看看是哪里的问题,非常感谢!
    波儿菜:1106355439 加个q吧
  • f3178190b1a5:学习中,发现demo在9.3模拟器横屏浏览有问题哦
    f3178190b1a5:@indulge_in 3Q,还有发现横竖屏切换的时候的图片旋转不太自然,我把[self resetUserInterfaceLayoutByDeviceOrientation];放到YBImageBrowser控制器的viewWillLayoutSubviews里动画自然些了,但是图片会闪一下。
    波儿菜:@活动活动吧 你随便一个控制器 模拟器疯狂旋转 大小偶尔会莫名其妙的变小
    波儿菜:@活动活动吧 在模拟器上 旋转屏幕的时候, 控制器会出bug哟,真机上不会有问题
  • 咖喱luya:可以,不断完善:clap:
  • 文豪英杰:不错不错,来学习学习
    波儿菜:@文豪英杰 :smile:

本文标题:iOS图片浏览器(功能强大/性能优越)

本文链接:https://www.haomeiwen.com/subject/ccszkftx.html