一种自定义循环滑动组件的设计
2017-03-28 21:26
417 查看
自定义循环滑动组件,通过UIView采用了复用机制来显示,UIPanGestureRecognizer来实现手势的识别,同时通过CADisplayLink来完成具体的动画。可以设定滚动方向,是否连续滚动,是否自动滚动,以及各个滚动状态的回调等 ZGLoopScrollView可以通过xib文件或者代码初始化,
先上效果图:
显示的部分由两部分组成:可视区和左右两侧的待显示区
所需示图个数为:可视区包含View个数+2
当触发滑动事件时,一侧的待显示区的View被移除,可视区变为待显示区,另一侧将会创建新的待显示区View,如下图所示:
轮播示图在滚动显示时有可分为以下几部分:
可视区和待显示区示图的确定及其复用方式
在交互过程中滑动时,可视区和待显示区示图内容和位置的确定
交互结束后的动画事件(减速,回弹,分页滑动等)
使用NSMutableDictionary来存储循环滚动组件中显示的示图,NSMutableSet来管理复用的示图
使用scrollOffset来记录当前滚动的位移,以计算需要加载的示图
使用dataSource来实现数据源委托
每当scrollOffset变化时
计算当前需要显示哪些示图,根据scrollOffset计算出需要显示的示图序号;
将itemViews中不需要的示图移除并存储的itemViewPool当中
获取itemViews中不存在但是需要显示的示图,先从集合检查是否有被移除的示图,有就从itemViewPool取出返回,否则从dataSource中初始化对应位置的示图,同时使用示图序号作为key值添加到itemViews当中;
根据scrollOffset调整itemViews中各个示图的位置,可以使用UIView的transform来方便
4000
的完成平移操作;
到这里,遍已经完成了示图显示初始化的核心操作,后面的滑动手势以及动画效果,都是基于scrollOffset的改变来完成的。
滑动开始时更改一些状态的标志
以免在滑动过程中对示图位置做的修改时,与未完成的动画事件所做的修改冲突,导致位置错乱。
滑动状态改变时
记录每次手势事件触发时的位移大小和方向,然后根据是否循环滚动(loopEnabled),是否边界回弹 (loopEnabled)来修改scrollOffset的大小,最后从新加载需要显示的示图,和调整位置。
滑动手势结束时
纪录当前滑动手势速度大小和方向,然后启动滑动动画,做减速处理等动画操作。
其中减速动画,是根据手势结束时记录的速度,结合固定的动画时间来做匀减速直线运动;
回弹效果动画,是根据当前和最后的scrollOffset差值作为位移距离,结合固定的动画时间来做匀加速直线运动。
运动位置都是一些基本的物理知识做的计算,就不再用代码一一罗列,有兴趣可以下载源码查看 。
到此,循环滑动组件的设计所包含的最基本内容已经完成。
为了不因clipsToBounds属性的设置导致待显示区的示图被显示出来,组件内部添加了容器UIView contentView,重写了layoutSubviews方法,对contentView的frame做出了修正;
仿照UIScrollView,通过delegate实现各个滑动状态的委托;
通过NSTimer实现自动滚动
具体源码和使用范例可以在我的GitHub主页下载 下载Demo查看
前言
最初实现循环滑动组件的时候,是通过使用的UIScrollView,在delegate里面改变View的位置来实现的,但是发现这样做无法再使用UIScrollView的delegate委托了,于是乎自己动手用UIView和UIPanGestureRecognizer撸了一个,顺便仿照了一下UITableView的复用机制。先上效果图:
原理模型
循环滑动示图显示模型如下:显示的部分由两部分组成:可视区和左右两侧的待显示区
所需示图个数为:可视区包含View个数+2
当触发滑动事件时,一侧的待显示区的View被移除,可视区变为待显示区,另一侧将会创建新的待显示区View,如下图所示:
轮播示图在滚动显示时有可分为以下几部分:
可视区和待显示区示图的确定及其复用方式
在交互过程中滑动时,可视区和待显示区示图内容和位置的确定
交互结束后的动画事件(减速,回弹,分页滑动等)
示图的加载及复用机制
循环滚动组件中的示图=可视区示图+两边待显示区的示图使用NSMutableDictionary来存储循环滚动组件中显示的示图,NSMutableSet来管理复用的示图
使用scrollOffset来记录当前滚动的位移,以计算需要加载的示图
使用dataSource来实现数据源委托
@property (nonatomic, strong) NSMutableDictionary *itemViews; @property (nonatomic, strong) NSMutableSet *itemViewPool; @property (nonatomic, assign) CGFloat scrollOffset; @property (nonatomic, weak) IBOutlet __nullable id<ZGLoopScrollViewDataSource> dataSource;
-(void)loadView{ CGFloat width = _itemWith; _visibleItemsCount = ceil(width / _itemWith) + 2; if(!_loopEnabled) _visibleItemsCount = MAX(0, MIN(_visibleItemsCount, _itemsCount)); NSMutableSet *visibleIndices = [NSMutableSet setWithCapacity:_visibleItemsCount]; NSInteger min = _bounceEnabled ? - kBounceFactor : 0; NSInteger max = _itemsCount - 1 - min; NSInteger offsetIdx = self.currentItemIndex - _visibleItemsCount/2; if (!_loopEnabled && !_bounceEnabled){ offsetIdx = MAX(min, MIN(max - _visibleItemsCount + 1, offsetIdx)); } for (NSInteger i = 0; i < _visibleItemsCount; i++){ NSInteger index = i + offsetIdx; [visibleIndices addObject:@(index)]; } for (NSNumber *number in [_itemViews allKeys]){ if (![visibleIndices containsObject:number]){ UIView *view = _itemViews[number]; [self queueItemView:view]; [view.superview removeFromSuperview]; [(NSMutableDictionary *)_itemViews removeObjectForKey:number]; } } for (NSNumber *number in visibleIndices){ UIView *view = _itemViews[number]; NSInteger index = [number integerValue]; if(!_loopEnabled && (index < 0 || index > _itemsCount - 1)) continue; if(_itemsCount < 1) continue; if (view == nil) view = [self loadViewAtIndex:index]; CGFloat offset = index* width - _scrollOffset; CGAffineTransform transform = CGAffineTransformIdentity; transform = _vertical ? CGAffineTransformTranslate(transform, 0.0, offset) : CGAffineTransformTranslate(transform, offset , 0.0); view.superview.transform = transform; } } - (UIView *)loadViewAtIndex:(NSInteger)index{ UIView *view = [_dataSource loopScrollView:self viewForItemAtIndex:(index + _itemsCount)%_itemsCount reusingView:[self dequeueItemView]]; if (view == nil) view = [[UIView alloc] init]; _itemViews[@(index)] = view; CGRect frame = self.bounds; UIView *containerView = [[UIView alloc] initWithFrame:frame]; view.center = containerView.center; [containerView addSubview:view]; [_contentView addSubview:containerView]; return view; }
每当scrollOffset变化时
计算当前需要显示哪些示图,根据scrollOffset计算出需要显示的示图序号;
将itemViews中不需要的示图移除并存储的itemViewPool当中
获取itemViews中不存在但是需要显示的示图,先从集合检查是否有被移除的示图,有就从itemViewPool取出返回,否则从dataSource中初始化对应位置的示图,同时使用示图序号作为key值添加到itemViews当中;
根据scrollOffset调整itemViews中各个示图的位置,可以使用UIView的transform来方便
4000
的完成平移操作;
到这里,遍已经完成了示图显示初始化的核心操作,后面的滑动手势以及动画效果,都是基于scrollOffset的改变来完成的。
根据滑动事件调整位置
滑动工过程中所做的工作主要是滑动开始时更改一些状态的标志
case UIGestureRecognizerStateBegan:{ _draging = YES; _bouncing = NO; _decelerating = NO; _scrolling = NO; _lastTranslation = - _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x; }break;
以免在滑动过程中对示图位置做的修改时,与未完成的动画事件所做的修改冲突,导致位置错乱。
滑动状态改变时
-(void)didScroll:(CGFloat)moveDistance{ BOOL isMoveLeft = moveDistance < 0; CGFloat offSet = _scrollOffset + moveDistance; if(!_loopEnabled ){ if(!_bounceEnabled){ if(offSet < 0) _scrollOffset = 0; else if(offSet > _itemWith*(_itemsCount-1)) _scrollOffset = _itemWith*(_itemsCount-1); else _scrollOffset = offSet; }else{ if(_scrollOffset < 0 && isMoveLeft){ CGFloat bounceMoveDistance = - (1 + _scrollOffset / (kBounceFactor * _itemWith)) * moveDistance/2; CGFloat limitMoveDistance = kBounceFactor * _itemWith + _scrollOffset; _scrollOffset += - MIN(limitMoveDistance / 4, bounceMoveDistance); } else if(_scrollOffset > _itemWith*(_itemsCount-1) && !isMoveLeft){ CGFloat bounceMoveDistance = (1 - (_scrollOffset-_itemWith*(_itemsCount-1)) / (kBounceFactor * _itemWith)) * moveDistance; CGFloat limitMoveDistance = _itemWith*(_itemsCount-1) + kBounceFactor * _itemWith - _scrollOffset; _scrollOffset += MIN(limitMoveDistance / 4, bounceMoveDistance/2); }else{ _scrollOffset = offSet; } } }else{ if(offSet < -(_itemWith*(_visibleItemsCount/2))) _scrollOffset = offSet + _itemWith * (_itemsCount); else if(offSet > _itemWith*(_itemsCount-1 + _visibleItemsCount/2)) _scrollOffset = offSet - _itemWith * (_itemsCount); else _scrollOffset = offSet; } [self loadView]; if([_delegate respondsToSelector:@selector(loopScrollViewDidScroll:)]) [_delegate loopScrollViewDidScroll:self]; }
记录每次手势事件触发时的位移大小和方向,然后根据是否循环滚动(loopEnabled),是否边界回弹 (loopEnabled)来修改scrollOffset的大小,最后从新加载需要显示的示图,和调整位置。
滑动手势结束时
纪录当前滑动手势速度大小和方向,然后启动滑动动画,做减速处理等动画操作。
滑动动画事件
滑动手势结束后的减速、回弹等动画都是采用CADisplayLink,根据屏幕刷新频率来不断的从新加载需要显示的示图,调整位置-(void)startScrollAnimation{ if(_displayLink == nil){ self.displayLink=[CADisplayLink displayLinkWithTarget:self selector:@selector(scrollAnimation)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } }
其中减速动画,是根据手势结束时记录的速度,结合固定的动画时间来做匀减速直线运动;
回弹效果动画,是根据当前和最后的scrollOffset差值作为位移距离,结合固定的动画时间来做匀加速直线运动。
运动位置都是一些基本的物理知识做的计算,就不再用代码一一罗列,有兴趣可以下载源码查看 。
到此,循环滑动组件的设计所包含的最基本内容已经完成。
其他内容
水平与垂直滚动两种模式,通过vertical属性确定。在两种模式下,scrollOffset,手势的位移、速度,动画效果,以及示图的位置计算分别与之对应;为了不因clipsToBounds属性的设置导致待显示区的示图被显示出来,组件内部添加了容器UIView contentView,重写了layoutSubviews方法,对contentView的frame做出了修正;
仿照UIScrollView,通过delegate实现各个滑动状态的委托;
通过NSTimer实现自动滚动
小结
本文主要分析了循环滚动示图的实现过程,同时通过UIPanGestureRecognizer和CADisplayLink,采用UIView复用的方式对其进行了实践。具体源码和使用范例可以在我的GitHub主页下载 下载Demo查看
相关文章推荐
- Visual C#实现自定义组件的设计
- Engine-Collection-Class,一种用来建立可重用企业组件的设计模式
- 安卓侧面滑动组件设计(二)
- Android自定义组件系列【4】——自定义ViewGroup实现双侧滑动
- Android自定义组件系列【4】——自定义ViewGroup实现双侧滑动
- Visual C#实现自定义组件的设计
- Android自定义组件系列【10】——随ViewPager滑动的导航条
- 安卓侧面滑动组件设计(一)
- android自定义listview,添加监听器,解决屏幕滑动组件状态干扰的问题checkbox ...
- XScrollView 自定义组件,使得被包含在其中的组件可以滑动,并且滑动后可以弹回到开始滑动的位置
- Visual C#实现自定义组件的设计3
- (系列2)可视 Mobile 设计器自定义组件:登录屏幕
- (系列4)可视 Mobile 设计器自定义组件:PIM 浏览器
- 安卓侧面滑动组件设计
- 自定义android循环拖动组件
- Visual C#实现自定义组件的设计2
- flex自定义组件如何在设计时显示预期结果/已添加控件
- (系列1)可视 Mobile 设计器自定义组件:文件浏览器
- Android 自定义listview 添加监听器 解决屏幕滑动组件状态干扰的问题checkbox
- Android UI设计之<五>自定义DrawView组件,实现数字签名效果