iCarousel简介:
iCarousel是一个优秀开源的视图控件,专门用于实现各种类型的旋转木马(分页滚动视图)iPhone、iPad和Mac OS,iCarousel实现一些常见的如圆柱、平面式的旋转木马。
阅读源码的原因:
在iOS中可以使用 UIScrollView 或者 UICollectionView 去取巧地实现一个无限循环的视图,取巧地方式非常便捷,但是有些情况会遇到小问题。因为 UIScrollView 的坐标系是基于Bounds的,要实现无限循环的效果就需要在滑动中跳动ContentOffset。
使用其他方式呢?实现一个无限循环的视图 首先要抽象出一个循环坐标系:
... 4 5 0 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 1 2 3 4 5 0 1 2 ...
再根据这个坐标系做布局就可以去实现一个无限循环的视图。
根据这种思路去实现就需要做到 :
1.管理抽象的坐标系
2.处理手势动作
3.管理滑动的动画
恰巧我发现iCarousel 是以这种思路实现的,所以我开始阅读iCarousel的源码
带着问题去读源码:
在开启了滑动动画的时候,假如有手势输入了,界面如何快速响应手势呢?如何有效地管理动画呢?
iCarousel主要流程图:
主要流程图didScroll :
didScroll 主要做的是更新界面,首先会对offset坐标做一些更正,确保offset的正确。 第二步是管理视图的加载,滑动过程中,加载滑入界面的视图,然后移除滑出界面的视图。第三步是调整界面界面的视图3D transform 。最后进行的是协议的回调。
- (void)didScroll
{
if (_wrapEnabled || !_bounces)
{
//循环状态下
_scrollOffset = [self clampedOffset:_scrollOffset];
}
else
{
//去除bounces效果
CGFloat min = -_bounceDistance;
CGFloat max = MAX(_numberOfItems - 1, 0.0) + _bounceDistance;
if (_scrollOffset < min)
{
_scrollOffset = min;
_startVelocity = 0.0;
}
else if (_scrollOffset > max)
{
_scrollOffset = max;
_startVelocity = 0.0;
}
}
//如果index变化了
NSInteger difference = [self minScrollDistanceFromIndex:self.currentItemIndex toIndex:self.previousItemIndex];
if (difference)
{
_toggleTime = CACurrentMediaTime();
_toggle = MAX(-1, MIN(1, difference));
#ifdef ICAROUSEL_MACOS
if (_vertical)
{
//invert toggle
_toggle = -_toggle;
}
#endif
[self startAnimation];
}
//加载没有加载的视图
[self loadUnloadViews];
//界面变化
[self transformItemViews];
//delegate 回调
if (fabs(_scrollOffset - _previousScrollOffset) > FLOAT_ERROR_MARGIN)
{
[self pushAnimationState:YES];
[_delegate carouselDidScroll:self];
[self popAnimationState];
}
//notify delegate of index change
if (_previousItemIndex != self.currentItemIndex)
{
[self pushAnimationState:YES];
[_delegate carouselCurrentItemIndexDidChange:self];
[self popAnimationState];
}
//update previous index
_previousScrollOffset = _scrollOffset;
_previousItemIndex = self.currentItemIndex;
}
loadUnloadViews
loadUnloadViews 首先计算可视区域视图的数量,然后根据offset 和numberOfVisibleItems 得到 visibleIndices。然后遍历之前的可视视图字典,将已经滑出的视图移除,并加入了复用池中。最后遍历 visibleIndices 将没有在可视视图字典视图加入。
- (void)loadUnloadViews
{
//set item width
[self updateItemWidth];
//update number of visible items
[self updateNumberOfVisibleItems];
//calculate visible view indices
NSMutableSet *visibleIndices = [NSMutableSet setWithCapacity:_numberOfVisibleItems];
NSInteger min = -(NSInteger)(ceil((CGFloat)_numberOfPlaceholdersToShow/2.0));
NSInteger max = _numberOfItems - 1 + _numberOfPlaceholdersToShow/2;
NSInteger offset = self.currentItemIndex - _numberOfVisibleItems/2;
if (!_wrapEnabled)
{
offset = MAX(min, MIN(max - _numberOfVisibleItems + 1, offset));
}
for (NSInteger i = 0; i < _numberOfVisibleItems; i++)
{
NSInteger index = i + offset;
if (_wrapEnabled)
{
index = [self clampedIndex:index];
}
CGFloat alpha = [self alphaForItemWithOffset:[self offsetForItemAtIndex:index]];
if (alpha)
{
//only add views with alpha > 0
[visibleIndices addObject:@(index)];
}
}
//remove offscreen views
for (NSNumber *number in [_itemViews allKeys])
{
if (![visibleIndices containsObject:number])
{
UIView *view = _itemViews[number];
if ([number integerValue] < 0 || [number integerValue] >= _numberOfItems)
{
[self queuePlaceholderView:view];
}
else
{
[self queueItemView:view];
}
[view.superview removeFromSuperview];
[(NSMutableDictionary *)_itemViews removeObjectForKey:number];
}
}
//add onscreen views
for (NSNumber *number in visibleIndices)
{
UIView *view = _itemViews[number];
if (view == nil)
{
[self loadViewAtIndex:[number integerValue]];
}
}
step
回到开头的问题,iCarousel 处理动画是通过定时器去实现的帧动画。定时器的频率 1/60 与屏幕的刷新率相同,相当于 CADisplayLink。定时器每一次调用step 就相当于处理每一帧的动画。通过 endOffset 、scrollDuration 和currentTime,来得到每一帧的offset ,为了更符合真实,通过一个缓冲方程式计算,在滑动状态下 ,会实现easeIneaseOut 慢入慢出的效果,offset计算完成后会调用didScroll 进行真正的界面更新 。因为iCarousel 视图的变化是通过CATransform3D,所以每一次step 开头都会将 隐式动画关闭。
- (void)step
{
[self pushAnimationState:NO];
NSTimeInterval currentTime = CACurrentMediaTime();
double delta = currentTime - _lastTime;
_lastTime = currentTime;
if (_scrolling && !_dragging)
{
NSTimeInterval time = MIN(1.0, (currentTime - _startTime) / _scrollDuration);
delta = [self easeInOut:time];
_scrollOffset = _startOffset + (_endOffset - _startOffset) * delta;
[self didScroll];
if (time >= 1.0)
{
_scrolling = NO;
[self depthSortViews];
[self pushAnimationState:YES];
[_delegate carouselDidEndScrollingAnimation:self];
[self popAnimationState];
}
}
else if (_decelerating)
{
CGFloat time = MIN(_scrollDuration, currentTime - _startTime);
CGFloat acceleration = -_startVelocity/_scrollDuration;
CGFloat distance = _startVelocity * time + 0.5 * acceleration * pow(time, 2.0);
_scrollOffset = _startOffset + distance;
[self didScroll];
if (fabs(time - _scrollDuration) < FLOAT_ERROR_MARGIN)
{
_decelerating = NO;
[self pushAnimationState:YES];
[_delegate carouselDidEndDecelerating:self];
[self popAnimationState];
if ((_scrollToItemBoundary || fabs(_scrollOffset - [self clampedOffset:_scrollOffset]) > FLOAT_ERROR_MARGIN) && !_autoscroll)
{
if (fabs(_scrollOffset - self.currentItemIndex) < FLOAT_ERROR_MARGIN)
{
//call scroll to trigger events for legacy support reasons
//even though technically we don't need to scroll at all
[self scrollToItemAtIndex:self.currentItemIndex duration:0.01];
}
else
{
[self scrollToItemAtIndex:self.currentItemIndex animated:YES];
}
}
else
{
CGFloat difference = round(_scrollOffset) - _scrollOffset;
if (difference > 0.5)
{
difference = difference - 1.0;
}
else if (difference < -0.5)
{
difference = 1.0 + difference;
}
_toggleTime = currentTime - MAX_TOGGLE_DURATION * fabs(difference);
_toggle = MAX(-1.0, MIN(1.0, -difference));
}
}
}
else if (_autoscroll && !_dragging)
{
//autoscroll goes backwards from what you'd expect, for historical reasons
self.scrollOffset = [self clampedOffset:_scrollOffset - delta * _autoscroll];
}
else if (fabs(_toggle) > FLOAT_ERROR_MARGIN)
{
NSTimeInterval toggleDuration = _startVelocity? MIN(1.0, MAX(0.0, 1.0 / fabs(_startVelocity))): 1.0;
toggleDuration = MIN_TOGGLE_DURATION + (MAX_TOGGLE_DURATION - MIN_TOGGLE_DURATION) * toggleDuration;
NSTimeInterval time = MIN(1.0, (currentTime - _toggleTime) / toggleDuration);
delta = [self easeInOut:time];
_toggle = (_toggle < 0.0)? (delta - 1.0): (1.0 - delta);
[self didScroll];
}
else if (!_autoscroll)
{
[self stopAnimation];
}
[self popAnimationState];
}
didPan
pan手势的处理,根据不同的手势状态来 模拟真实滑动,如减速效果、弹簧bounces效果。
- (void)didPan:(UIPanGestureRecognizer *)panGesture
{
if (_scrollEnabled && _numberOfItems)
{
switch (panGesture.state)
{
case UIGestureRecognizerStateBegan:
{
_dragging = YES;
_scrolling = NO;
_decelerating = NO;
_previousTranslation = _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x;
#if defined(USING_CHAMELEON) && USING_CHAMELEON
_previousTranslation = -_previousTranslation;
#endif
[_delegate carouselWillBeginDragging:self];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed:
{
_dragging = NO;
_didDrag = YES;
if ([self shouldDecelerate])
{
_didDrag = NO;
[self startDecelerating];
}
[self pushAnimationState:YES];
[_delegate carouselDidEndDragging:self willDecelerate:_decelerating];
[self popAnimationState];
if (!_decelerating)
{
if ((_scrollToItemBoundary || fabs(_scrollOffset - [self clampedOffset:_scrollOffset]) > FLOAT_ERROR_MARGIN) && !_autoscroll)
{
if (fabs(_scrollOffset - self.currentItemIndex) < FLOAT_ERROR_MARGIN)
{
//call scroll to trigger events for legacy support reasons
//even though technically we don't need to scroll at all
[self scrollToItemAtIndex:self.currentItemIndex duration:0.01];
}
else if ([self shouldScroll])
{
NSInteger direction = (int)(_startVelocity / fabs(_startVelocity));
[self scrollToItemAtIndex:self.currentItemIndex + direction animated:YES];
}
else
{
[self scrollToItemAtIndex:self.currentItemIndex animated:YES];
}
}
else
{
[self depthSortViews];
}
}
else
{
[self pushAnimationState:YES];
[_delegate carouselWillBeginDecelerating:self];
[self popAnimationState];
}
break;
}
case UIGestureRecognizerStateChanged:
{
CGFloat translation = _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x;
CGFloat velocity = _vertical? [panGesture velocityInView:self].y: [panGesture velocityInView:self].x;
#if defined(USING_CHAMELEON) && USING_CHAMELEON
translation = -translation;
velocity = -velocity;
#endif
CGFloat factor = 1.0;
if (!_wrapEnabled && _bounces)
{
factor = 1.0 - MIN(fabs(_scrollOffset - [self clampedOffset:_scrollOffset]),
_bounceDistance) / _bounceDistance;
}
_startVelocity = -velocity * factor * _scrollSpeed / _itemWidth;
_scrollOffset -= (translation - _previousTranslation) * factor * _offsetMultiplier / _itemWidth;
_previousTranslation = translation;
[self didScroll];
break;
}
case UIGestureRecognizerStatePossible:
{
//do nothing
break;
}
}
}
}
网友评论