滚动交互引导界面的Ouroboros
2015-12-05 23:29
302 查看
转自:http://draveness.me/ouroboros-de-shi-xian-interaction-library-in-objectivec/
如有侵犯,请来信oiken@qq.com
Ouroboros 是一个根据
灵感来源于
我在使用
而且这种动画是可以回退的, 在我看来, 这种动画的方式完全适合于在 iOS App 中完成引导界面的动画.
在以往的开发经验中, 经常需要根据
而
当动画根据
我们不会知道动画什么时候开始, 什么时候结束, "动画" 的状态完全取决于位置. 而这也是这些动画可以后退的最重要原因.
为视图添加的其实并不是通常意义的动画, 而是根据当前的
计算出当前视图的状态, 然后再更新视图的这样一组动作.
提供当前位置
并负责更新视图的状态
也就是导致视图状态改变的原数据,
根据
我在
动态地改变了原有的
当
而我们就可以通过方法调剂改变原有方法的实现, 在
来通知
为
:
当这个方法被调用时:
首先会注册
当
然后会根据方法传入的
获取一个对应的
而在获取
会实例化一个
使用传入的
存入
在这里有这样几个问题, 每一个视图对应的一组
都由相同的
在这里当我们第一次调用
在
当
就会调用在该分类中另一个比较重要的方法
这个方法首先会从
然后遍历视图自己持有的
通过其中的每一个
根据
其中的
并且负责管理器持有的
当
调用了
这时可能会发生三种情况:
如果当前位置, 在某一个
就会直接通过
返回当前位置的数值.
否则, 当前位置不在某一个
但是存在
也就是
如果不存在
但是存在
也就是
而当该方法调用时, 一定会存在至少一个
比如说, 存在以下两个
如果当前位置在
如果在
就会返回
如果在
就会返回
如果在
就会返回
而这个是通过
实现这个功能主要调用了三个方法:
首先需要通过
的可变数组
然后在操作这个数组, 例如
而我们在只要在
在这里
我们需要关注的是
查看是否有 overlapping, 并找出最近的
重新设置
我相信由于上面也有类似的代码, 这里的逻辑也很好解释.
它的核心作用就是保存一个
toValue
trigger
offset
function
然后根据这些状态和
这个方法的实现非常长, 我们在这里只截取其中的一部分
当
默认的函数曲线为线性的, 也就是不会改变
要根据值类型的不同, 计算出不同的
fromValue + (toValue - fromValue) * value
如果是
然后重新组成
最后再重新组合成对应的值的类型.
整个框架的实现还是比较简单的. 如果你有更好的想法或者有新的建议, 可以在 github 上开一个 issue 或者 PR.
如有侵犯,请来信oiken@qq.com
Ouroboros 是一个根据
scrollView滚动的距离完成动画的一个仓库.
灵感来源于
javascript的第三方框架 scrollMagic.
我在使用
scrollMagic的过程中, 觉得这种根据当前滚动距离改变视图状态的方式非常的优雅,
而且这种动画是可以回退的, 在我看来, 这种动画的方式完全适合于在 iOS App 中完成引导界面的动画.
在以往的开发经验中, 经常需要根据
scrollView的滚动距离完成一些相应的动画,
而
Ouroboros就是 iOS 中用于解决这一类问题的框架.
动画?
Ouroboros与其说是为视图添加动画, 不如说是改变视图当前的状态.
当动画根据
scrollView的滚动距离而进行时, 所谓的
duration就失去了意义.
我们不会知道动画什么时候开始, 什么时候结束, "动画" 的状态完全取决于位置. 而这也是这些动画可以后退的最重要原因.
为视图添加的其实并不是通常意义的动画, 而是根据当前的
scrollView的滚动位置,
计算出当前视图的状态, 然后再更新视图的这样一组动作.
实现
Ouroboros的组成还是非常的简单, 总共分为以下几部分
UIScrollView的分类,
提供当前位置
Ouroboros, 聚合一组动画属性相同的
Scale
Scale, 用于计算当前状态
UIView的分类, 为添加动画提供便利的方法,
并负责更新视图的状态
UIScrollView+Ouroboros
UIScrollView+Ouroboros这个分类的主要作用就是为整个
Ouroboros提供数据支持,
也就是导致视图状态改变的原数据,
contentOffset.
根据
UIScrollView的滚动方向不同, 而
UIScrollView的属性
ou_scrollDirection决定了是根据
contentOffset.x还是
contentOffset.y来改变视图的状态.
我在
UIScrollView中通过 method swizillzing
动态地改变了原有的
-setContentOffset:方法的实现.
- (void)ou_setContentOffset:(CGPoint)contentOffset { [self ou_setContentOffset:contentOffset]; [[NSNotificationCenter defaultCenter] postNotificationName:OURScrollViewUpdateContentOffset object:nil userInfo:@{@"contentOffset": [NSValue valueWithCGPoint:contentOffset], @"direction":@(self.ou_scrollDirection)}]; }
当
UIScrollView滚动时, 它的
contentOffset会不断发生改变,
而我们就可以通过方法调剂改变原有方法的实现, 在
contentOffset改变时发出通知,
来通知
Ouroboros中的其他模块根据
contentOffset和
ou_scrollDirection来对视图的状态进行更新.
UIView+Ouroboros
UIView+Ouroboros这个分类与大多数框架中的分类的作用一样,
为
UIView提供"开箱即用"的方法, 其核心方法为
-our_animateWithProperty:configureBlock:,
:
- (void)our_animateWithProperty:(OURAnimationProperty)property configureBlock:(ScaleAnimationBlock)configureBlock { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateState:) name:OURScrollViewUpdateContentOffset object:nil]; Ouroboros *ouroboros = [self ouroborosWithProperty:property]; Scale *scale = [[Scale alloc] init]; configureBlock(scale); NSMutableArray<Scale *> *scales = [ouroboros mutableArrayValueForKey:@"scales"]; [scales addObject:scale]; }
当这个方法被调用时:
首先会注册
OURScrollViewUpdateContentOffset通知,
当
contentOffset改变时调用
-updateState:方法.
然后会根据方法传入的
property,
获取一个对应的
Ouroboros的对象, 这个对象的作用就是为了管理一组
Scale.
而在获取
ouroboros之后,
会实例化一个
scale
使用传入的
block配置这个
scale
存入
ouroboros持有的
scales数组中
在这里有这样几个问题, 每一个视图对应的一组
property相同的动画,
都由相同的
ouroboros对象来处理, 这样做的主要原因是防止在同一区间中改变视图的同一属性多次的问题.
[yellowView our_animateWithProperty:OURAnimationPropertyViewHeight configureBlock:^(Scale * _Nonnull scale) { scale.toValue = @(200); scale.trigger = 0; scale.offset = 320 * 2; }]; [yellowView our_animateWithProperty:OURAnimationPropertyViewHeight configureBlock:^(Scale * _Nonnull scale) { scale.toValue = @(400); scale.trigger = 320; scale.offset = 320 * 0.5; }];
在这里当我们第一次调用
-our_animateWithProperty:configureBlock:方法时,
在
[0, 640]之间改变了视图的高度, 而在我们第二次为视图高度添加动画时, 在
[320, 480]之间改变了视图的高度, 而这时就造成了冲突. 而这就是
ouroboros要解决的一个问题.
当
UIScrollView滚动并且发出通知告知视图需要更新状态时,
就会调用在该分类中另一个比较重要的方法
-updateState:
这个方法首先会从
notification对象中取出
contentOffset和
ou_scrollDirection的值
然后遍历视图自己持有的
ouroboroses数组
通过其中的每一个
ouroboros获取当前位置下的值,
根据
ouroboros.property更新视图的状态.
- (void)updateState:(NSNotification *)notification { CGPoint contentOffset = [[notification userInfo][@"contentOffset"] CGPointValue]; OURScrollDirection direction = [[notification userInfo][@"direction"] integerValue]; for (Ouroboros *ouroboros in self.ouroboroses) { CGFloat currentPosition = 0; if (direction == OURScrollDirectionHorizontal) { currentPosition = contentOffset.x; } else { currentPosition = contentOffset.y; } id value = [ouroboros getCurrentValueWithPosition:currentPosition]; OURAnimationProperty property = ouroboros.property; switch (property) { case OURAnimationPropertyViewBackgroundColor: { self.backgroundColor = value; } break; ... } } }
其中的
-getCurrentValueWithPosition:方法会在下面介绍.
Ouroboros
Ouroboros作为这个框架的核心类, 它为视图的更新提供数据支持,
并且负责管理器持有的
Scale对象, 发现其中的可能的动画冲突, 在这个类中也提供了几个创建
CGRect
CGSize和
CGPoint的方法.
NSValue *NSValueFromCGRectParameters(CGFloat x, CGFloat y, CGFloat width, CGFloat height); NSValue *NSValueFromCGPointParameters(CGFloat x, CGFloat y); NSValue *NSValueFromCGSizeParameters(CGFloat width, CGFloat height);
当
UIView调用
-updateState:方法更新视图时,
调用了
-getCurrentValueWithPosition:方法, 该方法根据当前的状态获取了视图该
property对应的值.
- (id)getCurrentValueWithPosition:(CGFloat)position { Scale *previousScale = nil; Scale *afterScale = nil; for (Scale *scale in self.scales) { if ([scale isCurrentPositionOnScale:position]) { CGFloat percent = (position - scale.trigger) / scale.offset; return [scale calculateInternalValueWithPercent:percent]; } else if (scale.trigger > position && (!afterScale || afterScale.trigger > scale.trigger)) { afterScale = scale; } else if (scale.stop < position && (!previousScale || previousScale.stop < scale.stop)) { previousScale = scale; } } if (previousScale) { return previousScale.toValue; } else if (afterScale) { return afterScale.fromValue; } NSAssert(NO, @"FATAL ERROR, Unknown current value for property %@", @(self.property)); return [[NSObject alloc] init]; }
Ouroboros对象会遍历持有的
scales数组,
这时可能会发生三种情况:
如果当前位置, 在某一个
scale的区间中,
就会直接通过
-calculateInternalValueWithPercent:方法,
返回当前位置的数值.
否则, 当前位置不在某一个
scale上,
但是存在
previousScale就会返回
previousScale.toValue,
也就是
previousScale结束时的值.
如果不存在
previousScale,
但是存在
afterScale那么就会返回
afterScale.fromValue,
也就是
afterScale的初始值.
而当该方法调用时, 一定会存在至少一个
scale, 所以
- getCurrentValueWithPosition:方法总会返回正确的值.
比如说, 存在以下两个
scales
[100, 200] [400, 500]
如果当前位置在
[100, 200]或者
[400, 500]之间, 那么直接通过这两个
scale计算就能获得当前位置的值.
如果在
[-Inf, 100]之间,
就会返回
100处的值.
如果在
[200, 400]之间,
就会返回
200处的值
如果在
[500, +Inf]之间,
就会返回
500处的值
Ouroboros这个类的另一个作用就是发现
scale之间的覆盖,
而这个是通过
Objective-C语言中的
KVO完成的,
实现这个功能主要调用了三个方法:
-mutableArrayValueForKey:
-insertObject:in<Key>AtIndex:
-removeObjectFrom<Key>AtIndex:
Objective-C中对数组的
KVO主要由这三个方法实现,
首先需要通过
-mutableArrayValueForKey:方法获取需要 observe
的可变数组
NSMutableArray<Scale *> *scales = [ouroboros mutableArrayValueForKey:@"scales"];
然后在操作这个数组, 例如
-addObject:等方法时, 就会调用
-insertObject:in<Key>AtIndex:,
-removeObjectFrom<Key>AtIndex:方法,
而我们在只要在
ouroboros覆写这两个方法就可以了.
- (void)insertObject:(Scale *)currentScale inScalesAtIndex:(NSUInteger)index { Scale *previousScale = nil; Scale *afterScale = nil; for (Scale *scale in self.scales) { if ([scale isSeparateWithScale:currentScale]) { if (scale.trigger >= currentScale.stop && (!afterScale || afterScale.trigger >= scale.stop)) { afterScale = scale; } else if (scale.stop <= currentScale.trigger && (!previousScale || previousScale.stop <= scale.trigger)) { previousScale = scale; } } else { NSAssert(NO, @"Can not added an overlapping scales to the same ouroboros."); } } if (previousScale) { currentScale.fromValue = previousScale.toValue; } else { currentScale.fromValue = self.startValue; } if (afterScale) { afterScale.fromValue = currentScale.toValue; } [self.scales insertObject:currentScale atIndex:index]; } - (void)removeObjectFromScalesAtIndex:(NSUInteger)index { [self.scales removeObjectAtIndex:index]; }
在这里
-removeObjectFromScalesAtIndex:方法的实现并不重要,
我们需要关注的是
-insertObject:inScalesAtIndex:方法. 这个方法在最开始会先将即将插入的
scale与其它所有的
scale进行比较,
查看是否有 overlapping, 并找出最近的
previousScale和
afterScale.
重新设置
currentScale和
afterScale的初始值.
我相信由于上面也有类似的代码, 这里的逻辑也很好解释.
Scale
Scale作为
Ouroboros的一部分,
它的核心作用就是保存一个
Ouroboros动画的状态
toValue
trigger
offset
function
然后根据这些状态和
percent计算视图的状态, 也就是
-calculateInternalValueWithPercent:方法.
这个方法的实现非常长, 我们在这里只截取其中的一部分
- (id)calculateInternalValueWithPercent:(CGFloat)percent { percent = [self justifyPercent:percent]; CGFloat value = self.functionBlock(self.offset * percent * 1000, 0, 1, self.offset * 1000); id result = [[NSValue alloc] init]; if ([self.fromValue isKindOfClass:[NSNumber class]]) { CGFloat fromValue = [self.fromValue floatValue]; CGFloat toValue = [self.toValue floatValue]; CGFloat resultValue = fromValue + (toValue - fromValue) * value; result = @(resultValue); } ... return result; }
当
percent传进来之后, 要调用
-justifyPercent:方法保证当前
percent值的范围在
[0, 1]之间, 然后通过
functionBlock根据不同的函数曲线偏移当前的
offset值,
默认的函数曲线为线性的, 也就是不会改变
percent值. 在这之后, 由于
fromValue和
toValue的值有不同的类型,
要根据值类型的不同, 计算出不同的
resultValue. 公式差不多都是这样的:
fromValue + (toValue - fromValue) * value
如果是
UIColor就会分别计算
red
green
blue
alpha四部分,
然后重新组成
UIColor, 如果是
CGRect等其他值也会分别计算各个组成部分,
最后再重新组合成对应的值的类型.
总结
到这里, 整个Ouroboros框架的实现就已经基本介绍完了,
整个框架的实现还是比较简单的. 如果你有更好的想法或者有新的建议, 可以在 github 上开一个 issue 或者 PR.
相关文章推荐
- 开源搜索引擎的尝试《引言》
- Bootstrap组件之响应式导航条
- 漂亮的Java Swing界面NimROD
- 所见即所得的合同填报(二)
- 所见即所得的合同填报(一)
- openssl 1.0.2d安装使用教程
- XDocViewer发布8.3.1版本,支持语法高亮显示
- Swing中做套打
- 分享两个用XDOC自动生成的字帖
- 使用XDOC自动生成Oracle健康检查报告
- 文档自动化平台XDOC发布了8.2.6版本
- Binomial Showdown
- PL/SQL Developer Logon the DB user with ORA-12154
- HDU-5170
- 超轻量Swing仪表盘组件
- 一份漂亮的健康体检报告(二)
- 一份漂亮的健康体检报告(一)
- 基于swing的UI原型辅助设计
- JFreeChart图表示例(超全)
- 将XDOC引擎加入你的J2EE应用中