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

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

为了适应组件的自定义需求,代码和逻辑有点多,所以尽量不要修改源码。

写在前面

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

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

索引:(简书不支持页内跳转很尴尬)

一、组件框架整体设计

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

三、拖拽动效的算法优化

四、分页间距的算法优化

五、内存的优化

六、预下载和任务同步

七、屏幕旋转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_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属性,又说分页间距,稍有经验的工程师都会说重写UICollectionView的layout,既创建一个UICollectionViewFlowLayout类重写约束。现在这里不浪费篇幅讨论API的用法,你只需要知道在重写的layout里面,几乎每一帧的界面都可以靠重写layoutAttributesForElementsInRect等方法重新计算。

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

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

blog_pic3.png

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

于是组件中笔者的做法是,在每次重写布局时,都移动一个距离:当前偏移量 / 最大偏移量 * 总共页间距

其实做法很简单,但这种思维方式却非常实用,在我们做很多需要平滑过渡的逻辑时(不局限于界面),都可以以这种思维做出“平滑”的效果。

五、内存的优化

由于如今的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 @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;

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

写在后面

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

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

希望大家共勉~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容