UIScrollView滚动视图学习笔记

UIScrollView的作用

UIScrollView可以用于显示多余一个屏幕的内容,超出屏幕范围的内容可以通过滑动进行查看。

常见属性

CGSize contentSize :Scrollview中的可显示区域,可以设置ScrollView的滚动范围,假如有一个Scrollview,它的frame为(0,0,320,480),而它的contentSize为(320,960).也就是说,这个scrollview整个内容的大小为(320,960),要通过上下滑动Scrollview来查看(320,480)后的内容。

CGPoint contentOffset :Scrollview当前显示区域顶点相对于frame顶点的偏移量,可以设置ScrollView当前滚动的位置,假如有一个Scrollview,它的frame为(0,0,320,480),而它的contentSize为(320,960),也就是说你拉到最下面,contentOffset就是(0 ,480),也就是y偏移了480。

UIEdgeInsets contentInset :Scrollview中内容视图的原点与Scrollview本身原点的关系,这个属性可以在四周增加滚动范围,比如有一个Scrollview,它的frame为(0,0,320,480),内容视图的frame为(0,30,320,480),那么相对于Scrollview的contentInset则为(0, 30)。

BOOL bounces 是否有弹簧效果

BOOL scrollEnabled 是否能滚动

BOOL showsHorizontalScrollIndicator 是否显示水平方向的滚动条

BOOL showsVerticalScrollIndicator 是否显示垂直方向的滚动条

UIScrollViewIndicatorStyle indicatorStyle 设定滚动条的样式

BOOL dragging 是否正在被拖拽

BOOL tracking 当touch后还没有拖动的时候值是YES,否则NO

BOOL decelerating 是否正在减速

BOOL zooming 是否正在缩放

BOOL directionalLockEnabled 确定是否在特定方向上禁用滚动如果此属性为“YES”,并且用户开始沿一个常规方向(水平或垂直)拖动,则滚动视图将禁用沿另一个方向的滚动。如果拖动方向是对角线,则滚动将不会被锁定,用户可以在拖动完成之前向任何方向拖动。默认值为“NO”

代理提供的函数
/**
contentOffset内容偏移量改变时调用,委托通常实现此方法以从scrollView获取内容偏移量的更改,并绘制内容视图的受影响部分。
参数:
scrollView:发生滚动的滚动视图对象。

*/
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
/**
开始滚动时调用(可能需要一些时间或距离才能调用)在较短距离内发生滚动,代理可能无法接收此消息。
参数:
scrollView:即将滚动内容视图的滚动视图对象。
*/
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
/**
将要结束滚动时调用,应用程序可以更改targetContentOffset参数的值,以调整scrollview完成其滚动动画的位置。
参数:
scrollView:用户结束触摸的滚动视图对象。
velocity:释放触摸时滚动视图的速度(以点为单位)。
targetContentOffset :滚动操作减速到停止时的预期偏移量。
*/
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset API_AVAILABLE(ios(5.0));
/**
结束滚动时调用。
参数:
scrollView:完成内容视图滚动的滚动视图对象。
decelerate:设为“YES”,如果在滚动操作期间的触摸手势之后,滚动移动将继续,但会减速。如果该值为“NO”,则触摸后滚动将立即停止。
*/
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
/**
开始减速滚动时调用,在滚动操作期间,当用户的手指离开触摸时,滚动视图调用此方法;之后,滚动视图将继续移动一小段距离。
参数:
scrollView:正在减慢内容视图滚动速度的滚动视图对象。
*/
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;
/**
如果要点击状态栏滚动到顶部,请返回“YES”。如果没有定义,则默认YES。要使滚动到顶部手势(点击状态栏)生效,UIScrollView的scrollsToTop属性必须设置为YES。
参数:
scrollView:请求此信息的滚动视图对象。
返回值:
“YES”允许滚动到内容的顶部,“NO”禁止。
*/
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView; 
/**
通知代理滚动视图通过点击状态栏滚动到内容的顶部。滚动视图在滚动到内容顶部时发送此消息。如果内容的顶部已经显示,它可能会立即调用它。要使滚动到顶部手势(点击状态栏)生效,UIScrollView的scrollsToTop属性必须设置为YES。滑动到顶部不会执行。
参数:
scrollView:执行滚动操作的滚动视图对象。
*/
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;   

在视图无法滚动时,可以尝试重写viewDidLayoutSubviews,在此方法中设置滚动范围,例如:

- (void)viewDidLayoutSubviews{
    _scrollView.contentSize=CGSizeMake(0 , SCREEN_HEIGHT);
}
手势缩放

1.设置UIScrollView的id<UISCrollViewDelegate> delegate代理对象

2.设置minimumZoomScale :缩小的最小比例

3.设置maximumZoomScale :放大的最大比例

4.让代理对象实现下面的方法,返回需要缩放的视图控件

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView

与缩放相关的常用方法

正在缩放的时候调用 : -(void)scrollViewDidZoom:(UIScrollView *)scrollView

缩放完毕的时候调用 : -(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale

分页效果

设置pagingEnabled=YES即可,UIScrollView会被分割成多个独立页面,用户的滚动体验则变成了页面翻转,一般会配合UIPageControl增强分页效果。

UIPageControl常用属性:

NSInteger numberOfPages : 总页数

NSInteger currentPage : 当前的页码

BOOL hidesForSinglePage : 当只有一页的时候,是否要隐藏视图

添加监听的方法:[pageControl addTarget:self action:@selector(pageChange:)
forControlEvents:UIControlEventValueChanged];

然后开始学习了,Xcode创建工程,素材是这样的……
屏幕快照 2019-02-13 上午1.40.17.png
然后直奔ViewController.m文件,因为其他的文件基本没写东西,有图为证……但是这个window属性一定要声明啊,否则模拟器运行起来还以为图片是包大人呢,而Xcoed会亲切的提示你The app delegate must implement the window property if it wants to use a main storyboard file。即:如果要使用主情节提要文件,应用程序委托必须实现Window属性。
屏幕快照 2019-02-12 下午10.16.23.png
屏幕快照 2019-02-12 下午10.19.36.png
屏幕快照 2019-02-12 下午10.21.48.png

接下来就是ViewController.m中的内容了:

//引入ViewController头文件
#import "ViewController.h"
//声明为UIScrollView的代理类
@interface ViewController ()<UIScrollViewDelegate>
//分页视图控件
@property(nonatomic,strong) UIPageControl *pageControl;
//定时器
@property(nonatomic,strong) NSTimer *timer;
@end

@implementation ViewController
//视图生命周期的方法,加载视图时调用
- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化UIScrollWiew大小为屏幕大小
    UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:self.view.frame];
    //设置滚动的范围
    scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame) * 8, CGRectGetHeight(self.view.frame));
    //设置分页效果
    scrollView.pagingEnabled = YES;
    //水平滚动条隐藏
    scrollView.showsHorizontalScrollIndicator = NO;
    //声明图片名称
    NSString *imageName = nil;
    //声明图片
    UIImage *image = nil;
    
    for (int i = 0; i < 8; i++) {
        //为图片名称赋值
        imageName = [NSString stringWithFormat:@"0%d",i + 1];
        //初始化图片
        image= [UIImage imageNamed:imageName];
        //初始化图片视图,x轴的坐标在原点随着每次循环增加一个屏幕的宽度,y轴始终为0,宽高为屏幕的宽高
        UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) * i, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
        //为图片视图设置图片
        [imageView setImage:image];
        //为每个图片视图设置标签
        imageView.tag = 100 + I;
        //将图片视图添加到滚动视图
        [scrollView addSubview:imageView];
    }
    //viewWithTag的作用就是根据tag属性获取到对应的view、imageview、label等等。
    //获取第一张图片视图
    UIImageView *firstImageView = [scrollView viewWithTag:100];
    //为滚动视图最后面加一个视图,它和第一个视图一样,到这里实际已经有了9张图片视图
    UIImageView *lastImageView = [[UIImageView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) * 8, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
    //将第一张图片视图的图片赋值给最后一张图片视图的图片属性
    lastImageView.image = firstImageView.image;
    //将最后一张图片视图添加到滚动视图上
    [scrollView addSubview:lastImageView];
    //将滚动视图添加到控制器视图
    [self.view addSubview:scrollView];
    //为滚动视图添加标签
    scrollView.tag = 100;
    //初始化分页视图控件,并设置好位置
    self.pageControl = [[UIPageControl alloc]initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - 50, CGRectGetWidth(self.view.frame), 50)];
    //设置分为多少页
    self.pageControl.numberOfPages = 8;
    //设置当前所在页
    self.pageControl.currentPage = 0;
    //设置页面指示器的颜色(即分页的圆点标记)
    self.pageControl.pageIndicatorTintColor = [UIColor redColor];
    //设置当前所在页面指示器的颜色
    self.pageControl.currentPageIndicatorTintColor = [UIColor greenColor];
    //将分页视图控件添加到控制器视图
    [self.view addSubview: self.pageControl];
    //初始化定时器,scheduledTimerWithTimeInterval设定间隔时间1.0秒,target指定发送消息给哪个对象,selector指定要调用的方法名,userInfo可以给消息发送参数,repeats设定是否重复
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(circularDisplayView) userInfo:nil repeats:YES];
    //将本类赋值给scrollView的代理属性
    scrollView.delegate = self;
}
#pragma mark - 滚动视图的代理方法
//视图被拖拽时调用,在此方法中会暂停控制器,停止图片循环展示
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    //setFireDate:设置定时器的启动时间
    //[NSDate distantFuture]:遥远的未来
    [self.timer setFireDate:[NSDate distantFuture]];
}
//视图静止时(没有被拖拽)时调用,在此方法中会开启定时器,让图片循环展示
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    //延迟2秒在开启定时器
    //[NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]] 返回值为现在时刻开始在过1.5秒的时刻
    [self.timer setFireDate:[NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]]];
}
//定时器的回调方法,切换页面
-(void)circularDisplayView{
    //得到scrollView
    UIScrollView *scrollView = [self.view viewWithTag:100];
    //通过改变contentOffset来切换滚动视图的子界面
    CGFloat offset_x = scrollView.contentOffset.x;
    //每次切换一个屏幕
    offset_x +=CGRectGetWidth(self.view.frame);
    //当额外添加的最后一张图片视图开始滚动时,将偏移量改为第一个图片视图的位置
    if(offset_x > CGRectGetWidth(self.view.frame) * 8){
        scrollView.contentOffset = CGPointMake(0, 0);
    }
    //当显示的是额外添加的最后一张图片视图时,将分页控件的当前分页指示器重新开始设置,否则正常显示
    if(offset_x == CGRectGetWidth(self.view.frame) * 8){
        self.pageControl.currentPage = 0;
    }else{
        self.pageControl.currentPage = offset_x / CGRectGetWidth(self.view.frame);
    }
    //设置最终偏移量
    CGPoint finalPoint = CGPointMake(offset_x, 0);
    // 切换视图时带动画效果
    //当额外添加的最后一张图片视图开始滚动时,即相当于第一张图片开始滚动,直接将偏移量设置到第二张图片的位置
    if (offset_x > CGRectGetWidth(self.view.frame) * 8) {
        self.pageControl.currentPage = 1;
        [scrollView setContentOffset:CGPointMake(CGRectGetWidth(self.view.frame), 0) animated:YES];
    }else{
        [scrollView setContentOffset:finalPoint animated:YES];
    }
}


@end
运行效果:
Untitled.gif
一个滑动视图插件
//
//  SwipeView.h
//
//  Version 1.3.2
//
//  Created by Nick Lockwood on 03/09/2010.
//  Copyright 2010 Charcoal Design
//
//  Distributed under the permissive zlib License
//  Get the latest version of SwipeView from here:
//
//  https://github.com/nicklockwood/SwipeView
//


#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wauto-import"
#pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis"


#import <Availability.h>
#undef weak_delegate
#if __has_feature(objc_arc) && __has_feature(objc_arc_weak)
#define weak_delegate weak
#else
#define weak_delegate unsafe_unretained
#endif


#import <UIKit/UIKit.h>


typedef NS_ENUM(NSUInteger, SwipeViewAlignment) {
    ///滑动视图边缘对齐
    SwipeViewAlignmentEdge = 0,
    ///滑动视图居中对齐
    SwipeViewAlignmentCenter
};


@protocol SwipeViewDataSource, SwipeViewDelegate;

@interface SwipeView : UIView
///滑动视图数据源
@property (nonatomic, weak_delegate) IBOutlet id<SwipeViewDataSource> dataSource;
///滑动视图代理
@property (nonatomic, weak_delegate) IBOutlet id<SwipeViewDelegate> delegate;
///滑动视图项目数(只读)
@property (nonatomic, readonly) NSInteger numberOfItems;
///滑动视图所分页数(只读)
@property (nonatomic, readonly) NSInteger numberOfPages;
///滑动视图项目大小(只读)
@property (nonatomic, readonly) CGSize itemSize;
///每页的项目数(一页翻过几个项目)
@property (nonatomic, assign) NSInteger itemsPerPage;
///截断最后一页(到最会一页将阻止轮询)
@property (nonatomic, assign) BOOL truncateFinalPage;
///可见项的索引集合(只读)
@property (nonatomic, strong, readonly) NSArray *indexesForVisibleItems;
///可见项视图(只读)
@property (nonatomic, strong, readonly) NSArray *visibleItemViews;
///当前项目视图对象
@property (nonatomic, strong, readonly) UIView *currentItemView;
///当前项目索引
@property (nonatomic, assign) NSInteger currentItemIndex;
///当前页
@property (nonatomic, assign) NSInteger currentPage;
///滑动视图对齐方式,枚举
@property (nonatomic, assign) SwipeViewAlignment alignment;
///滚动偏移量
@property (nonatomic, assign) CGFloat scrollOffset;
///是否启动分页
@property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;
///是否手动滚动
@property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;
///是否轮询
@property (nonatomic, assign, getter = isWrapEnabled) BOOL wrapEnabled;
///是否延迟响应触摸事件
@property (nonatomic, assign) BOOL delaysContentTouches;
///弹簧效果(不轮询时有效)
@property (nonatomic, assign) BOOL bounces;
///手势滑动的速度
@property (nonatomic, assign) float decelerationRate;
///自动返回(数值越大返回动画越快)
@property (nonatomic, assign) CGFloat autoscroll;
///是否正在拖动(只读)
@property (nonatomic, readonly, getter = isDragging) BOOL dragging;
///是否正在减速(只读)
@property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;
///是否正在滚动(只读)
@property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;
///延迟视图加载
@property (nonatomic, assign) BOOL defersItemViewLoading;
///是否垂直滑动
@property (nonatomic, assign, getter = isVertical) BOOL vertical;

///重新加载数据
- (void)reloadData;
///重新加载某一项
- (void)reloadItemAtIndex:(NSInteger)index;
///在指定时间按偏移量滚动
- (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration;
///在指定时间滚动到指定偏移量
- (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;
///在指定时间按项目数滚动
- (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration;
///在指定时间滚动到索引处的项目
- (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)duration;
///在指定时间滚动到滚动到指定页面
- (void)scrollToPage:(NSInteger)page duration:(NSTimeInterval)duration;
///返回索引处的项目视图
- (UIView *)itemViewAtIndex:(NSInteger)index;
///返回项目视图的索引
- (NSInteger)indexOfItemView:(UIView *)view;
///返回项目视图子视图或子视图的索引索引
- (NSInteger)indexOfItemViewOrSubview:(UIView *)view;

@end


@protocol SwipeViewDataSource <NSObject>
///滑动视图中的项目数
- (NSInteger)numberOfItemsInSwipeView:(SwipeView *)swipeView;
///滑动视图中的每一个项目
- (UIView *)swipeView:(SwipeView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view;

@end


@protocol SwipeViewDelegate <NSObject>
@optional
///滑动视图项目大小
- (CGSize)swipeViewItemSize:(SwipeView *)swipeView;
///当scrollView的contentOffset改变时调用。
- (void)swipeViewDidScroll:(SwipeView *)swipeView;
///当前项目索引更新时调用
- (void)swipeViewCurrentItemIndexDidChange:(SwipeView *)swipeView;
///当开始滚动视图时,执行改方法,一次有效滑动只执行一次(开始滑动,滑动一小段距离,只要手指不松开,算一次滑动)
- (void)swipeViewWillBeginDragging:(SwipeView *)swipeView;
///滑动视图,当手指离开时,调用该方法,一次有效滑动只执行一次
- (void)swipeViewDidEndDragging:(SwipeView *)swipeView willDecelerate:(BOOL)decelerate;
///滑动减速时调用该方法
- (void)swipeViewWillBeginDecelerating:(SwipeView *)swipeView;
///滚动视图减速完成,滚动将停止时,调用该方法,一次有效滑动,只执行一次
- (void)swipeViewDidEndDecelerating:(SwipeView *)swipeView;
///滑动视图结束滚动时
- (void)swipeViewDidEndScrollingAnimation:(SwipeView *)swipeView;
///该索引处的项目是否选中
- (BOOL)swipeView:(SwipeView *)swipeView shouldSelectItemAtIndex:(NSInteger)index;
///滑动视图点击事件
- (void)swipeView:(SwipeView *)swipeView didSelectItemAtIndex:(NSInteger)index;

@end


#pragma GCC diagnostic pop


//
//  SwipeView.m
//
//  Version 1.3.2
//
//  Created by Nick Lockwood on 03/09/2010.
//  Copyright 2010 Charcoal Design
//
//  Distributed under the permissive zlib License
//  Get the latest version of SwipeView from here:
//
//  https://github.com/nicklockwood/SwipeView
//


#import "SwipeView.h"
#import <objc/message.h>

#pragma GCC diagnostic ignored "-Wdirect-ivar-access"
#pragma GCC diagnostic ignored "-Warc-repeated-use-of-weak"
//#pragma GCC diagnostic ignored "-Wreceiver-is-weak"
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wselector"
#pragma GCC diagnostic ignored "-Wgnu"

#import <Availability.h>
#if !__has_feature(objc_arc)
#error This class requires automatic reference counting
#endif

@implementation NSObject (SwipeView)

- (CGSize)swipeViewItemSize:(__unused SwipeView*)swipeView {
    return CGSizeZero;
}
- (void)swipeViewDidScroll:(__unused SwipeView*)swipeView {
}
- (void)swipeViewCurrentItemIndexDidChange:(__unused SwipeView*)swipeView {
}
- (void)swipeViewWillBeginDragging:(__unused SwipeView*)swipeView {
}
- (void)swipeViewDidEndDragging:(__unused SwipeView*)swipeView
                 willDecelerate:(__unused BOOL)decelerate {
}
- (void)swipeViewWillBeginDecelerating:(__unused SwipeView*)swipeView {
}
- (void)swipeViewDidEndDecelerating:(__unused SwipeView*)swipeView {
}
- (void)swipeViewDidEndScrollingAnimation:(__unused SwipeView*)swipeView {
}
- (BOOL)swipeView:(__unused SwipeView*)swipeView shouldSelectItemAtIndex:(__unused NSInteger)index {
    return YES;
}
- (void)swipeView:(__unused SwipeView*)swipeView didSelectItemAtIndex:(__unused NSInteger)index {
}

@end

@interface SwipeView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>

@property (nonatomic, strong) UIScrollView* scrollView;
@property (nonatomic, strong) NSMutableDictionary* itemViews;//项目视图字典
@property (nonatomic, strong) NSMutableSet* itemViewPool;
@property (nonatomic, assign) NSInteger previousItemIndex;
@property (nonatomic, assign) CGPoint previousContentOffset;
@property (nonatomic, assign) CGSize itemSize;
@property (nonatomic, assign) BOOL suppressScrollEvent;
@property (nonatomic, assign) NSTimeInterval scrollDuration;//滚动持续时间
@property (nonatomic, assign, getter=isScrolling) BOOL scrolling;
@property (nonatomic, assign) NSTimeInterval startTime;
@property (nonatomic, assign) NSTimeInterval lastTime;
@property (nonatomic, assign) CGFloat startOffset;
@property (nonatomic, assign) CGFloat endOffset;
@property (nonatomic, assign) CGFloat lastUpdateOffset;
@property (nonatomic, strong) NSTimer* timer;

@end

@implementation SwipeView

#pragma mark -
#pragma mark Initialisation

- (void)setUp {
    _scrollEnabled = YES;
    _pagingEnabled = YES;
    _delaysContentTouches = YES;
    _bounces = YES;
    _wrapEnabled = NO;
    _itemsPerPage = 1;
    _truncateFinalPage = NO;
    _defersItemViewLoading = NO;
    _vertical = NO;
    
    _scrollView = [[UIScrollView alloc] init];
    //autoresizingMask 该属性用于自动调节子控件在父控件中的位置和宽高。枚举类型。默认是UIViewAutoresizingNone
    _scrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
    //autoresizesSubviews 属性声明被设置为YES,则其子视图会根据autoresizingMask属性的值自动进行尺寸调整
    _scrollView.autoresizesSubviews = YES;
    _scrollView.delegate = self;
    //delaysContentTouches 默认值为YES。如果设置为NO,则会立即把触摸事件传递给subView
    _scrollView.delaysContentTouches = _delaysContentTouches;
    //bounces 是否有弹簧效果
    _scrollView.bounces = _bounces && !_wrapEnabled;
    //alwaysBounceHorizontal 总是水平反弹
    _scrollView.alwaysBounceHorizontal = !_vertical && _bounces;
    //alwaysBounceVertical 总是垂直反弹
    _scrollView.alwaysBounceVertical = _vertical && _bounces;
    //pagingEnabled 是否启动分页
    _scrollView.pagingEnabled = _pagingEnabled;
    //scrollEnabled 是否允许滚动
    _scrollView.scrollEnabled = _scrollEnabled;
    //decelerationRate 手势滑动的速度
    _scrollView.decelerationRate = _decelerationRate;
    //showsHorizontalScrollIndicator 是否显示水平方向的滚动条
    _scrollView.showsHorizontalScrollIndicator = NO;
    //showsVerticalScrollIndicator 是否显示垂直c方向的滚动条
    _scrollView.showsVerticalScrollIndicator = NO;
    //scrollsToTop 触摸状态栏,视图是否自动滚动的最顶端,默认值YES,多个滚动视图的话,要确保只有一个滚动视图scrollsToTop的值为YES
    _scrollView.scrollsToTop = NO;
    //clipsToBounds 是否裁剪超出父视图范围的子视图部分
    _scrollView.clipsToBounds = NO;
    
    _decelerationRate = _scrollView.decelerationRate;
    _itemViews = [[NSMutableDictionary alloc] init];
    _previousItemIndex = 0;
    //contentOffset 滚动范围
    _previousContentOffset = _scrollView.contentOffset;
    _scrollOffset = 0.0f;
    _currentItemIndex = 0;
    _numberOfItems = 0;
    
    //UITapGestureRecognizer 手势识别器对象
    UITapGestureRecognizer* tapGesture =
    [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector (didTap:)];
    tapGesture.delegate = self;
    //addGestureRecognizer 添加手势识别器到对应的view上
    [_scrollView addGestureRecognizer:tapGesture];
    
    self.clipsToBounds = YES;
    
    // 将ScrollView置于层次结构的底部
    [self insertSubview:_scrollView atIndex:0];
    
    if (_dataSource) {
        [self reloadData];
    }
}

- (id)initWithCoder:(NSCoder*)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self setUp];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}

- (void)dealloc {
    [_timer invalidate];
}

- (void)setDataSource:(id<SwipeViewDataSource>)dataSource {
    if (_dataSource != dataSource) {
        _dataSource = dataSource;
        if (_dataSource) {
            [self reloadData];
        }
    }
}

- (void)setDelegate:(id<SwipeViewDelegate>)delegate {
    if (_delegate != delegate) {
        _delegate = delegate;
        [self setNeedsLayout];
    }
}

- (void)setAlignment:(SwipeViewAlignment)alignment {
    if (_alignment != alignment) {
        _alignment = alignment;
        [self setNeedsLayout];
    }
}

- (void)setItemsPerPage:(NSInteger)itemsPerPage {
    if (_itemsPerPage != itemsPerPage) {
        _itemsPerPage = itemsPerPage;
        [self setNeedsLayout];
    }
}

- (void)setTruncateFinalPage:(BOOL)truncateFinalPage {
    if (_truncateFinalPage != truncateFinalPage) {
        _truncateFinalPage = truncateFinalPage;
        [self setNeedsLayout];
    }
}

- (void)setScrollEnabled:(BOOL)scrollEnabled {
    if (_scrollEnabled != scrollEnabled) {
        _scrollEnabled = scrollEnabled;
        _scrollView.scrollEnabled = _scrollEnabled;
    }
}

- (void)setPagingEnabled:(BOOL)pagingEnabled {
    if (_pagingEnabled != pagingEnabled) {
        _pagingEnabled = pagingEnabled;
        _scrollView.pagingEnabled = pagingEnabled;
        [self setNeedsLayout];
    }
}

- (void)setWrapEnabled:(BOOL)wrapEnabled {
    if (_wrapEnabled != wrapEnabled) {
        CGFloat previousOffset = [self clampedOffset:_scrollOffset];
        _wrapEnabled = wrapEnabled;
        _scrollView.bounces = _bounces && !_wrapEnabled;
        [self setNeedsLayout];
        [self layoutIfNeeded];
        self.scrollOffset = previousOffset;
    }
}

- (void)setDelaysContentTouches:(BOOL)delaysContentTouches {
    _delaysContentTouches = delaysContentTouches;
    _scrollView.delaysContentTouches = delaysContentTouches;
}

- (void)setBounces:(BOOL)bounces {
    if (_bounces != bounces) {
        _bounces = bounces;
        _scrollView.alwaysBounceHorizontal = !_vertical && _bounces;
        _scrollView.alwaysBounceVertical = _vertical && _bounces;
        _scrollView.bounces = _bounces && !_wrapEnabled;
    }
}

- (void)setDecelerationRate:(float)decelerationRate {
    if (fabsf (_decelerationRate - decelerationRate) > 0.0001f) {
        _decelerationRate = decelerationRate;
        _scrollView.decelerationRate = _decelerationRate;
    }
}

- (void)setAutoscroll:(CGFloat)autoscroll {
    if (fabs (_autoscroll - autoscroll) > 0.0001f) {
        _autoscroll = autoscroll;
        if (autoscroll)
            [self startAnimation];
    }
}

- (void)setVertical:(BOOL)vertical {
    if (_vertical != vertical) {
        _vertical = vertical;
        _scrollView.alwaysBounceHorizontal = !_vertical && _bounces;
        _scrollView.alwaysBounceVertical = _vertical && _bounces;
        [self setNeedsLayout];
    }
}

- (BOOL)isDragging {
    return _scrollView.dragging;
}

- (BOOL)isDecelerating {
    return _scrollView.decelerating;
}

#pragma mark -
#pragma mark View management
///返回可见项的索引
- (NSArray*)indexesForVisibleItems {
    //sortedArrayUsingSelector: 方法可以对字符串简单排序
    //compare: IOS提供的字符串比较方法,在没有制定比较的范围的情况下,那么这个compare只会默认比较第一个字符,也就是25并不大于3
    return [[_itemViews allKeys] sortedArrayUsingSelector:@selector (compare:)];
}

///返回可见项视图
- (NSArray*)visibleItemViews {
    NSArray* indexes = [self indexesForVisibleItems];
    //objectsForKeys:notFoundMarker: 没有对应的value的时候设置特定的mark.
    return [_itemViews objectsForKeys:indexes notFoundMarker:[NSNull null]];
}

///返回索引处的项目视图
- (UIView*)itemViewAtIndex:(NSInteger)index {
    return _itemViews[@(index)];
}

///返回当前项目视图
- (UIView*)currentItemView {
    return [self itemViewAtIndex:_currentItemIndex];
}

///返回项目视图索引
- (NSInteger)indexOfItemView:(UIView*)view {
    NSUInteger index = [[_itemViews allValues] indexOfObject:view];
    //NSNotFound 表示一个整型不存在
    if (index != NSNotFound) {
        return [[_itemViews allKeys][index] integerValue];
    }
    return NSNotFound;
}

///返回项目视图或子视图的索引
- (NSInteger)indexOfItemViewOrSubview:(UIView*)view {
    NSInteger index = [self indexOfItemView:view];
    if (index == NSNotFound && view != nil && view != _scrollView) {
        return [self indexOfItemViewOrSubview:view.superview];
    }
    return index;
}

///设置索引的项视图
- (void)setItemView:(UIView*)view forIndex:(NSInteger)index {
    ((NSMutableDictionary*)_itemViews)[@(index)] = view;
}

#pragma mark -
#pragma mark View layout
///更新滚动偏移量
- (void)updateScrollOffset {
    if (_wrapEnabled) {
        CGFloat itemsWide = (_numberOfItems == 1) ? 1.0f : 3.0f;
        if (_vertical) {
            CGFloat scrollHeight = _scrollView.contentSize.height / itemsWide;
            if (_scrollView.contentOffset.y < scrollHeight) {
                _previousContentOffset.y += scrollHeight;
                [self setContentOffsetWithoutEvent:CGPointMake (0.0f, _scrollView.contentOffset.y + scrollHeight)];
            } else if (_scrollView.contentOffset.y >= scrollHeight * 2.0f) {
                _previousContentOffset.y -= scrollHeight;
                [self setContentOffsetWithoutEvent:CGPointMake (0.0f, _scrollView.contentOffset.y - scrollHeight)];
            }
            _scrollOffset = [self clampedOffset:_scrollOffset];
        } else {
            CGFloat scrollWidth = _scrollView.contentSize.width / itemsWide;
            if (_scrollView.contentOffset.x < scrollWidth) {
                _previousContentOffset.x += scrollWidth;
                [self setContentOffsetWithoutEvent:CGPointMake (_scrollView.contentOffset.x + scrollWidth, 0.0f)];
            } else if (_scrollView.contentOffset.x >= scrollWidth * 2.0f) {
                _previousContentOffset.x -= scrollWidth;
                [self setContentOffsetWithoutEvent:CGPointMake (_scrollView.contentOffset.x - scrollWidth, 0.0f)];
            }
            _scrollOffset = [self clampedOffset:_scrollOffset];
        }
    }
    if (_vertical && fabs (_scrollView.contentOffset.x) > 0.0001f) {
        [self setContentOffsetWithoutEvent:CGPointMake (0.0f, _scrollView.contentOffset.y)];
    } else if (!_vertical && fabs (_scrollView.contentOffset.y) > 0.0001f) {
        [self setContentOffsetWithoutEvent:CGPointMake (_scrollView.contentOffset.x, 0.0f)];
    }
}

//更新滚动视图大小
- (void)updateScrollViewDimensions {
    CGRect frame = self.bounds;
    CGSize contentSize = frame.size;
    
    if (_vertical) {
        contentSize.width -= (_scrollView.contentInset.left + _scrollView.contentInset.right);
    } else {
        contentSize.height -= (_scrollView.contentInset.top + _scrollView.contentInset.bottom);
    }
    
    switch (_alignment) {
        case SwipeViewAlignmentCenter: {
            if (_vertical) {
                frame = CGRectMake (0.0f, (self.bounds.size.height - _itemSize.height * _itemsPerPage) / 2.0f,
                                    self.bounds.size.width, _itemSize.height * _itemsPerPage);
                contentSize.height = _itemSize.height * _numberOfItems;
            } else {
                frame = CGRectMake ((self.bounds.size.width - _itemSize.width * _itemsPerPage) / 2.0f,
                                    0.0f, _itemSize.width * _itemsPerPage, self.bounds.size.height);
                contentSize.width = _itemSize.width * _numberOfItems;
            }
            break;
        }
        case SwipeViewAlignmentEdge: {
            if (_vertical) {
                frame = CGRectMake (0.0f, 0.0f, self.bounds.size.width, _itemSize.height * _itemsPerPage);
                contentSize.height =
                _itemSize.height * _numberOfItems - (self.bounds.size.height - frame.size.height);
            } else {
                frame = CGRectMake (0.0f, 0.0f, _itemSize.width * _itemsPerPage, self.bounds.size.height);
                contentSize.width = _itemSize.width * _numberOfItems - (self.bounds.size.width - frame.size.width);
            }
            break;
        }
    }
    
    if (_wrapEnabled) {
        CGFloat itemsWide = (_numberOfItems == 1) ? 1.0f : _numberOfItems * 3.0f;
        if (_vertical) {
            contentSize.height = _itemSize.height * itemsWide;
        } else {
            contentSize.width = _itemSize.width * itemsWide;
        }
    } else if (_pagingEnabled && !_truncateFinalPage) {
        if (_vertical) {
            contentSize.height = ceilf (contentSize.height / frame.size.height) * frame.size.height;
        } else {
            contentSize.width = ceilf (contentSize.width / frame.size.width) * frame.size.width;
        }
    }
    
    if (!CGRectEqualToRect (_scrollView.frame, frame)) {
        _scrollView.frame = frame;
    }
    
    if (!CGSizeEqualToSize (_scrollView.contentSize, contentSize)) {
        _scrollView.contentSize = contentSize;
    }
}

///返回索引处项目的偏移量
- (CGFloat)offsetForItemAtIndex:(NSInteger)index {
    // 计算相对位置
    CGFloat offset = index - _scrollOffset;
    if (_wrapEnabled) {
        if (_alignment == SwipeViewAlignmentCenter) {
            if (offset > _numberOfItems / 2) {
                offset -= _numberOfItems;
            } else if (offset < -_numberOfItems / 2) {
                offset += _numberOfItems;
            }
        } else {
            CGFloat width = _vertical ? self.bounds.size.height : self.bounds.size.width;
            CGFloat x = _vertical ? _scrollView.frame.origin.y : _scrollView.frame.origin.x;
            CGFloat itemWidth = _vertical ? _itemSize.height : _itemSize.width;
            if (offset * itemWidth + x > width) {
                offset -= _numberOfItems;
            } else if (offset * itemWidth + x < -itemWidth) {
                offset += _numberOfItems;
            }
        }
    }
    return offset;
}

///为视图设置Frame
- (void)setFrameForView:(UIView*)view atIndex:(NSInteger)index {
    if (self.window) {
        CGPoint center = view.center;
        if (_vertical) {
            center.y = ([self offsetForItemAtIndex:index] + 0.5f) * _itemSize.height +
            _scrollView.contentOffset.y;
        } else {
            center.x = ([self offsetForItemAtIndex:index] + 0.5f) * _itemSize.width +
            _scrollView.contentOffset.x;
        }
        
        BOOL disableAnimation = !CGPointEqualToPoint (center, view.center);
        BOOL animationEnabled = [UIView areAnimationsEnabled];
        if (disableAnimation && animationEnabled)
            [UIView setAnimationsEnabled:NO];
        
        if (_vertical) {
            view.center = CGPointMake (_scrollView.frame.size.width / 2.0f, center.y);
        } else {
            view.center = CGPointMake (center.x, _scrollView.frame.size.height / 2.0f);
        }
        
        view.bounds = CGRectMake (0.0f, 0.0f, _itemSize.width, _itemSize.height);
        
        if (disableAnimation && animationEnabled)
            [UIView setAnimationsEnabled:YES];
    }
}

///布局项目视图
- (void)layOutItemViews {
    for (UIView* view in self.visibleItemViews) {
        [self setFrameForView:view atIndex:[self indexOfItemView:view]];
    }
}

///更新布局
- (void)updateLayout {
    [self updateScrollOffset];
    [self loadUnloadViews];
    [self layOutItemViews];
}

///布局子视图
- (void)layoutSubviews {
    [super layoutSubviews];
    [self updateItemSizeAndCount];
    [self updateScrollViewDimensions];
    [self updateLayout];
    if (_pagingEnabled && !_scrolling) {
        [self scrollToItemAtIndex:self.currentItemIndex duration:0.25];
    }
}

#pragma mark -
#pragma mark View queing
///队列项目视图
- (void)queueItemView:(UIView*)view {
    if (view) {
        [_itemViewPool addObject:view];
    }
}

///出列项目视图
- (UIView*)dequeueItemView {
    UIView* view = [_itemViewPool anyObject];
    if (view) {
        [_itemViewPool removeObject:view];
    }
    return view;
}

#pragma mark -
#pragma mark Scrolling
///是否滚动
- (void)didScroll {
    // handle wrap
    [self updateScrollOffset];
    
    // 更新视图
    [self layOutItemViews];
    [_delegate swipeViewDidScroll:self];
    
    if (!_defersItemViewLoading ||
        fabs ([self minScrollDistanceFromOffset:_lastUpdateOffset toOffset:_scrollOffset]) >= 1.0f) {
        // 更新项目索引
        // roundf(float) 四舍五入,返回最邻近的整数
        _currentItemIndex = [self clampedIndex:roundf (_scrollOffset)];
        
        // 加载视图
        _lastUpdateOffset = _currentItemIndex;
        [self loadUnloadViews];
        
        // 发送索引更新事件
        if (_previousItemIndex != _currentItemIndex) {
            _previousItemIndex = _currentItemIndex;
            [_delegate swipeViewCurrentItemIndexDidChange:self];
        }
    }
}

- (CGFloat)easeInOut:(CGFloat)time {
    //powf(float, float):计算以x为底数的y次幂,输入与输出皆为浮点数
    return (time < 0.5f) ? 0.5f * powf (time * 2.0f, 3.0f) : 0.5f * powf (time * 2.0f - 2.0f, 3.0f) + 1.0f;
}

///视图移动
- (void)step {
    //CFAbsoluteTimeGetCurrent() 返回的时钟时间将会网络时间同步
    NSTimeInterval currentTime = CFAbsoluteTimeGetCurrent ();
    double delta = _lastTime - currentTime;
    _lastTime = currentTime;
    
    if (_scrolling) {
        NSTimeInterval time = fminf (1.0f, (currentTime - _startTime) / _scrollDuration);
        delta = [self easeInOut:time];
        _scrollOffset = [self clampedOffset:_startOffset + (_endOffset - _startOffset) * delta];
        if (_vertical) {
            [self setContentOffsetWithoutEvent:CGPointMake (0.0f, _scrollOffset * _itemSize.height)];
        } else {
            [self setContentOffsetWithoutEvent:CGPointMake (_scrollOffset * _itemSize.width, 0.0f)];
        }
        [self didScroll];
        if (time == 1.0f) {
            _scrolling = NO;
            [self didScroll];
            [_delegate swipeViewDidEndScrollingAnimation:self];
        }
    } else if (_autoscroll) {
        if (!_scrollView.dragging)
            self.scrollOffset = [self clampedOffset:_scrollOffset + delta * _autoscroll];
    } else {
        [self stopAnimation];
    }
}

///开始动画
//NSRunLoop运行循环
/**
 Runloop状态:
 1.NSDefaultRunLoopMode:默认状态(空闲状态),比如点击按钮都是这个状态
 2.UITrackingRunLoopMode:滑动时的Mode。比如滑动UIScrollView时。
 3.UIInitializationRunLoopMode:私有的,APP启动时。就是从iphone桌面点击APP的图标进入APP到第一个界面展示之前,在第一个界面显示出来后,UIInitializationRunLoopMode就被切换成了NSDefaultRunLoopMode。
 4.NSRunLoopCommonModes:它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合。结构类似于一个数组。在这个mode下执行其实就是两个mode都能执行而已。
 典型的应用场景这样:当前界面有开启一个NSTimer,并且滑动UIScrollView。正常开启NSTimer后,滑动UIScrollView时它是不滑动的。解决办法就是把这个timer加入到当前的RunLoop,并把RunLoop的mode设置为NSRunLoopCommonModes。这样就可以保证不管你是NSDefaultRunLoopMode里跑,还是UITrackingRunLoopMode里跑,这个timer都可以执行。
 */
- (void)startAnimation {
    if (!_timer) {
        self.timer = [NSTimer timerWithTimeInterval:1.0 / 60.0
                                             target:self
                                           selector:@selector (step)
                                           userInfo:nil
                                            repeats:YES];
        
        [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop mainRunLoop] addTimer:_timer forMode:UITrackingRunLoopMode];
    }
}

///停止动画
- (void)stopAnimation {
    [_timer invalidate];
    self.timer = nil;
}

///固定索引
- (NSInteger)clampedIndex:(NSInteger)index {
    if (_wrapEnabled) {
        return _numberOfItems ? (index - floorf ((CGFloat)index / (CGFloat)_numberOfItems) * _numberOfItems) : 0;
    } else {
        return MIN (MAX (0, index), MAX (0, _numberOfItems - 1));
    }
}

///返回固定的偏移量
- (CGFloat)clampedOffset:(CGFloat)offset {
    CGFloat returnValue = 0;
    if (_wrapEnabled) {
        //floorf(float)向下取整,返回不大于自变量的最大整数
        returnValue =
        _numberOfItems ? (offset - floorf (offset / (CGFloat)_numberOfItems) * _numberOfItems) : 0.0f;
    } else {
        //fminf(float, float)求最小值
        //fmaxf(float, float)求最大值
        returnValue = fminf (fmaxf (0.0f, offset), fmaxf (0.0f, (CGFloat)_numberOfItems - 1.0f));
    }
    return returnValue;
}

///设置不带事件的内容偏移量
- (void)setContentOffsetWithoutEvent:(CGPoint)contentOffset {
    if (!CGPointEqualToPoint (_scrollView.contentOffset, contentOffset)) {
        //areAnimationsEnabled 返回一个布尔值表示动画是否结束,动画结束返回YES,否则NO
        BOOL animationEnabled = [UIView areAnimationsEnabled];
        //setAnimationsEnabled: 设置是否激活动画
        if (animationEnabled)
            [UIView setAnimationsEnabled:NO];
        _suppressScrollEvent = YES;
        _scrollView.contentOffset = contentOffset;
        _suppressScrollEvent = NO;
        if (animationEnabled)
            [UIView setAnimationsEnabled:YES];
    }
}

///返回当前页
- (NSInteger)currentPage {
    if (_itemsPerPage > 1 && _truncateFinalPage && !_wrapEnabled &&
        _currentItemIndex > (_numberOfItems / _itemsPerPage - 1) * _itemsPerPage) {
        return self.numberOfPages - 1;
    }
    return roundf ((float)_currentItemIndex / (float)_itemsPerPage);
}

///返回页数
- (NSInteger)numberOfPages {
    //ceilf(float) 向上取整,返回不小于自变量的最大整数
    return ceilf ((float)self.numberOfItems / (float)_itemsPerPage);
}

- (NSInteger)minScrollDistanceFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
    NSInteger directDistance = toIndex - fromIndex;
    if (_wrapEnabled) {
        NSInteger wrappedDistance = MIN (toIndex, fromIndex) + _numberOfItems - MAX (toIndex, fromIndex);
        if (fromIndex < toIndex) {
            wrappedDistance = -wrappedDistance;
        }
        return (ABS (directDistance) <= ABS (wrappedDistance)) ? directDistance : wrappedDistance;
    }
    return directDistance;
}

///最小滚动距离偏移量
- (CGFloat)minScrollDistanceFromOffset:(CGFloat)fromOffset toOffset:(CGFloat)toOffset {
    CGFloat directDistance = toOffset - fromOffset;
    if (_wrapEnabled) {
        CGFloat wrappedDistance = fminf (toOffset, fromOffset) + _numberOfItems - fmaxf (toOffset, fromOffset);
        if (fromOffset < toOffset) {
            wrappedDistance = -wrappedDistance;
        }
        //fabs(double) 处理double类型的取绝对值
        return (fabs (directDistance) <= fabs (wrappedDistance)) ? directDistance : wrappedDistance;
    }
    return directDistance;
}

///设置当前项目索引
- (void)setCurrentItemIndex:(NSInteger)currentItemIndex {
    _currentItemIndex = currentItemIndex;
    self.scrollOffset = currentItemIndex;
}

///设置当前页
- (void)setCurrentPage:(NSInteger)currentPage {
    if (currentPage * _itemsPerPage != _currentItemIndex) {
        [self scrollToPage:currentPage duration:0.5f];
    }
}

///设置滚动偏移量
- (void)setScrollOffset:(CGFloat)scrollOffset {
    if (fabs (_scrollOffset - scrollOffset) > 0.0001f) {
        _scrollOffset = scrollOffset;
        _lastUpdateOffset = _scrollOffset - 1.0f; // force refresh
        _scrolling = NO; // stop scrolling
        [self updateItemSizeAndCount];
        [self updateScrollViewDimensions];
        [self updateLayout];
        CGPoint contentOffset =
        _vertical ? CGPointMake (0.0f, [self clampedOffset:scrollOffset] * _itemSize.height) :
        CGPointMake ([self clampedOffset:scrollOffset] * _itemSize.width, 0.0f);
        [self setContentOffsetWithoutEvent:contentOffset];
        [self didScroll];
    }
}

///在指定时间按偏移量滚动
- (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration {
    if (duration > 0.0) {
        _scrolling = YES;
        _startTime = [[NSDate date] timeIntervalSinceReferenceDate];
        _startOffset = _scrollOffset;
        _scrollDuration = duration;
        _endOffset = _startOffset + offset;
        if (!_wrapEnabled) {
            _endOffset = [self clampedOffset:_endOffset];
        }
        [self startAnimation];
    } else {
        self.scrollOffset += offset;
    }
}
///在指定时间滚动到指定偏移量
- (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration {
    [self scrollByOffset:[self minScrollDistanceFromOffset:_scrollOffset toOffset:offset]
                duration:duration];
}

///在指定时间按项目数滚动
- (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration {
    if (duration > 0.0) {
        CGFloat offset = 0.0f;
        if (itemCount > 0) {
            offset = (floorf (_scrollOffset) + itemCount) - _scrollOffset;
        } else if (itemCount < 0) {
            offset = (ceilf (_scrollOffset) + itemCount) - _scrollOffset;
        } else {
            offset = roundf (_scrollOffset) - _scrollOffset;
        }
        [self scrollByOffset:offset duration:duration];
    } else {
        self.scrollOffset = [self clampedIndex:_previousItemIndex + itemCount];
    }
}

///在指定时间滚动到索引处的项目
- (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)duration {
    [self scrollToOffset:index duration:duration];
}

///在指定时间滚动到滚动到指定页面
- (void)scrollToPage:(NSInteger)page duration:(NSTimeInterval)duration {
    NSInteger index = page * _itemsPerPage;
    if (_truncateFinalPage) {
        index = MIN (index, _numberOfItems - _itemsPerPage);
    }
    [self scrollToItemAtIndex:index duration:duration];
}

#pragma mark -
#pragma mark View loading
///在索引处加载视图
- (UIView*)loadViewAtIndex:(NSInteger)index {
    UIView* view =
    [_dataSource swipeView:self viewForItemAtIndex:index reusingView:[self dequeueItemView]];
    if (view == nil) {
        view = [[UIView alloc] init];
    }
    
    UIView* oldView = [self itemViewAtIndex:index];
    if (oldView) {
        [self queueItemView:oldView];
        [oldView removeFromSuperview];
    }
    
    [self setItemView:view forIndex:index];
    [self setFrameForView:view atIndex:index];
    view.userInteractionEnabled = YES;
    [_scrollView addSubview:view];
    
    return view;
}

///更新项目大小和计数
- (void)updateItemSizeAndCount {
    // 获取项目数
    _numberOfItems = [_dataSource numberOfItemsInSwipeView:self];
    
    // 获取项目大小
    CGSize size = [_delegate swipeViewItemSize:self];
    if (!CGSizeEqualToSize (size, CGSizeZero)) {
        _itemSize = size;
    } else if (_numberOfItems > 0) {
        UIView* view = [[self visibleItemViews] lastObject] ?: [_dataSource swipeView:self
                                                                   viewForItemAtIndex:0
                                                                          reusingView:[self dequeueItemView]];
        _itemSize = view.frame.size;
    }
    
    // 防止项目相撞
    if (_itemSize.width < 0.0001)
        _itemSize.width = 1;
    if (_itemSize.height < 0.0001)
        _itemSize.height = 1;
}

///加载卸载视图
- (void)loadUnloadViews {
    // 检查项目大小是否已知
    CGFloat itemWidth = _vertical ? _itemSize.height : _itemSize.width;
    if (itemWidth) {
        // 计算偏移量和边界
        CGFloat width = _vertical ? self.bounds.size.height : self.bounds.size.width;
        CGFloat x = _vertical ? _scrollView.frame.origin.y : _scrollView.frame.origin.x;
        
        // 计算范围
        CGFloat startOffset = [self clampedOffset:_scrollOffset - x / itemWidth];
        NSInteger startIndex = floorf (startOffset);
        NSInteger numberOfVisibleItems = ceilf (width / itemWidth + (startOffset - startIndex));
        if (_defersItemViewLoading) {
            startIndex = _currentItemIndex - ceilf (x / itemWidth) - 1;
            numberOfVisibleItems = ceilf (width / itemWidth) + 3;
        }
        
        // 创建索引
        numberOfVisibleItems = MIN (numberOfVisibleItems, _numberOfItems);
        NSMutableSet* visibleIndices = [NSMutableSet setWithCapacity:numberOfVisibleItems];
        for (NSInteger i = 0; i < numberOfVisibleItems; i++) {
            NSInteger index = [self clampedIndex:i + startIndex];
            [visibleIndices addObject:@(index)];
        }
        
        // 删除屏幕外视图
        for (NSNumber* number in [_itemViews allKeys]) {
            if (![visibleIndices containsObject:number]) {
                UIView* view = _itemViews[number];
                [self queueItemView:view];
                [view removeFromSuperview];
                [_itemViews removeObjectForKey:number];
            }
        }
        
        // 添加屏幕视图
        for (NSNumber* number in visibleIndices) {
            UIView* view = _itemViews[number];
            if (view == nil) {
                [self loadViewAtIndex:[number integerValue]];
            }
        }
    }
}

///在索引处重新加载项目
- (void)reloadItemAtIndex:(NSInteger)index {
    // 如果视图可见
    if ([self itemViewAtIndex:index]) {
        // 重新加载视图
        [self loadViewAtIndex:index];
    }
}

///重新加载数据
- (void)reloadData {
    // 删除旧视图
    for (UIView* view in self.visibleItemViews) {
        [view removeFromSuperview];
    }
    
    // 重置视图池
    self.itemViews = [NSMutableDictionary dictionary];
    self.itemViewPool = [NSMutableSet set];
    
    // 获取项目数
    [self updateItemSizeAndCount];
    
    // 布局视图
    [self setNeedsLayout];
    
    // 固定滚动偏移
    if (_numberOfItems > 0 && _scrollOffset < 0.0f) {
        self.scrollOffset = 0;
    }
}
/**
 系统通过-(UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event方法来寻找最合适响应事件的view
 (UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event内部实现:
 - 1 自己是否能接收触摸事件?
 - 2 触摸点是否在自己身上?
 - 3 从后往前遍历子控件,重复前面的两个步骤
 - 4 如果没有符合条件的子控件,那么就自己最适合处理
 */

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
    UIView* view = [super hitTest:point withEvent:event];
    if ([view isEqual:self]) {
        for (UIView* subview in _scrollView.subviews) {
            CGPoint offset = CGPointMake (point.x - _scrollView.frame.origin.x +
                                          _scrollView.contentOffset.x - subview.frame.origin.x,
                                          point.y - _scrollView.frame.origin.y +
                                          _scrollView.contentOffset.y - subview.frame.origin.y);
            
            if ((view = [subview hitTest:offset withEvent:event])) {
                return view;
            }
        }
        return _scrollView;
    }
    return view;
}

///是否移至SuperView
- (void)didMoveToSuperview {
    if (self.superview) {
        [self setNeedsLayout];
        if (_scrolling) {
            [self startAnimation];
        }
    } else {
        [self stopAnimation];
    }
}

#pragma mark Gestures and taps
///返回视图或父视图索引
- (NSInteger)viewOrSuperviewIndex:(UIView*)view {
    if (view == nil || view == _scrollView) {
        return NSNotFound;
    }
    NSInteger index = [self indexOfItemView:view];
    if (index == NSNotFound) {
        return [self viewOrSuperviewIndex:view.superview];
    }
    return index;
}

///是否触摸视图或父视图
- (BOOL)viewOrSuperviewHandlesTouches:(UIView*)view {
    // thanks to @mattjgalloway and @shaps for idea
    // https://gist.github.com/mattjgalloway/6279363
    // https://gist.github.com/shaps80/6279008
    
    Class class = [view class];
    while (class && class != [UIView class]) {
        unsigned int numberOfMethods;
        Method* methods = class_copyMethodList (class, &numberOfMethods);
        for (unsigned int i = 0; i < numberOfMethods; i++) {
            if (method_getName (methods[i]) == @selector (touchesBegan:withEvent:)) {
                free (methods);
                return YES;
            }
        }
        if (methods)
            free (methods);
        class = [class superclass];
    }
    
    if (view.superview && view.superview != _scrollView) {
        return [self viewOrSuperviewHandlesTouches:view.superview];
    }
    
    return NO;
}

///手势识别器响应触摸
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gesture shouldReceiveTouch:(UITouch*)touch {
    if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) {
        // handle tap
        NSInteger index = [self viewOrSuperviewIndex:touch.view];
        if (index != NSNotFound) {
            if ((_delegate && ![_delegate swipeView:self shouldSelectItemAtIndex:index]) ||
                [self viewOrSuperviewHandlesTouches:touch.view]) {
                return NO;
            } else {
                return YES;
            }
        }
    }
    return NO;
}

///是否点击
- (void)didTap:(UITapGestureRecognizer*)tapGesture {
    CGPoint point = [tapGesture locationInView:_scrollView];
    NSInteger index = _vertical ? (point.y / (_itemSize.height)) : (point.x / (_itemSize.width));
    if (_wrapEnabled) {
        index = index % _numberOfItems;
    }
    if (index >= 0 && index < _numberOfItems) {
        [_delegate swipeView:self didSelectItemAtIndex:index];
    }
}

#pragma mark -
#pragma mark UIScrollViewDelegate methods

///当scrollView的contentOffset改变时调用。
- (void)scrollViewDidScroll:(__unused UIScrollView*)scrollView {
    if (!_suppressScrollEvent) {
        // 停止滚动动画
        _scrolling = NO;
        
        // 更新滚动偏移量
        CGFloat delta = _vertical ? (_scrollView.contentOffset.y - _previousContentOffset.y) :
        (_scrollView.contentOffset.x - _previousContentOffset.x);
        _previousContentOffset = _scrollView.contentOffset;
        _scrollOffset += delta / (_vertical ? _itemSize.height : _itemSize.width);
        
        // 更新视图并调用委托
        [self didScroll];
    } else {
        _previousContentOffset = _scrollView.contentOffset;
    }
}

///当开始滚动视图时,执行改方法,一次有效滑动只执行一次(开始滑动,滑动一小段距离,只要手指不松开,算一次滑动)
- (void)scrollViewWillBeginDragging:(__unused UIScrollView*)scrollView {
    [_delegate swipeViewWillBeginDragging:self];
    
    // 强制刷新
    _lastUpdateOffset = self.scrollOffset - 1.0f;
    [self didScroll];
}

///滑动视图,当手指离开时,调用该方法,一次有效滑动只执行一次
- (void)scrollViewDidEndDragging:(__unused UIScrollView*)scrollView
                  willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        // 强制刷新
        _lastUpdateOffset = self.scrollOffset - 1.0f;
        [self didScroll];
    }
    [_delegate swipeViewDidEndDragging:self willDecelerate:decelerate];
}

///滑动减速时调用该方法
- (void)scrollViewWillBeginDecelerating:(__unused UIScrollView*)scrollView {
    [_delegate swipeViewWillBeginDecelerating:self];
}

///滚动视图减速完成,滚动将停止时,调用该方法,一次有效滑动,只执行一次
- (void)scrollViewDidEndDecelerating:(__unused UIScrollView*)scrollView {
    // 防止舍入误差累积
    CGFloat integerOffset = roundf (_scrollOffset);
    if (fabs (_scrollOffset - integerOffset) < 0.01f) {
        _scrollOffset = integerOffset;
    }
    
    // 强制刷新
    _lastUpdateOffset = self.scrollOffset - 1.0f;
    [self didScroll];
    
    [_delegate swipeViewDidEndDecelerating:self];
}

@end

使用案例:

//
//  ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

//
//  ViewController.m

#import "ViewController.h"
#import "Masonry.h"
#import "TestCollectionViewController.h"
#import "BaseWebViewControlle.h"
#import "SwipeViewController.h"

//十六进制颜色宏定义
#define HEXCOLOR(c)                                  \
[UIColor colorWithRed:((c >> 16) & 0xFF) / 255.0 \
green:((c >> 8) & 0xFF) / 255.0  \
blue:(c & 0xFF) / 255.0         \
alpha:1.0]

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;//定时器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"滑动视图";
    [self setButton];
    
}

- (void)setButton{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setTitle:@"滑动视图" forState:UIControlStateNormal];
    button.titleLabel.font = [UIFont systemFontOfSize:15];
    [button addTarget:self action:@selector(goSwipeViewController) forControlEvents:UIControlEventTouchUpInside];
    
    [self.view addSubview:button];
    [button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(75, 35));
        make.center.equalTo(self.view);
    }];
    
    ///按钮渐变色
    //CAGradientLayer继承CALayer,可以设置渐变图层
    CAGradientLayer *grandientLayer = [[CAGradientLayer alloc] init];
    grandientLayer.frame = CGRectMake(0, 0, 75.0, 35.0);
    [button.layer addSublayer:grandientLayer];
    [button.layer insertSublayer:grandientLayer atIndex:0];
    //设置渐变的方向 左上(0,0)  右下(1,1)
    grandientLayer.startPoint = CGPointZero;
    grandientLayer.endPoint = CGPointMake(1.0, 0.0);
    //colors渐变的颜色数组 这个数组中只设置一个颜色是不显示的
    grandientLayer.colors = @[(id)HEXCOLOR(0x5C9CDC).CGColor, (id)HEXCOLOR(0x657CDA).CGColor];
    grandientLayer.type = kCAGradientLayerAxial;
    
}

- (void)goSwipeViewController{
    SwipeViewController *controller = [[SwipeViewController alloc]init];
    [self.navigationController pushViewController:controller animated:YES];
}

@end
//
//  SwipeViewController.h

#import <UIKit/UIKit.h>



@interface SwipeViewController : UIViewController

@end
//
//  SwipeViewController.m

#import "SwipeViewController.h"
#import "SwipeView.h"
#import "Masonry.h"
//屏幕的宽
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
//屏幕的高
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
//导航栏的高
#define NAVIGATIONBAR_HEIGHT self.navigationController.navigationBar.frame.size.height
//状态栏的高
#define STATUSBARHEIGHT [UIApplication sharedApplication].statusBarFrame.size.height

@interface SwipeViewController ()<SwipeViewDataSource,SwipeViewDelegate>

@property (nonatomic, strong) SwipeView *swipeView;//滑动视图
@property (nonatomic, strong) NSMutableArray *swipeViewDataSource;//滑动视图数据源
@property (nonatomic, strong) dispatch_source_t timer;//GCD定时器
@property(nonatomic,strong) NSTimer *delayTimer;//定时器

@end

@implementation SwipeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setSwipeView];
    //第一张图要展示3秒,延迟3秒后打开定时器
    self.delayTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(openTimer) userInfo:nil repeats:NO];
    
}

///设置滑动视图
- (void)setSwipeView{
    //初始化滑动视图
    self.swipeView = [[SwipeView alloc] init];
    //设置滑动视图代理
    self.swipeView.delegate = self;
    //设置滑动视图数据源
    self.swipeView.dataSource = self;
    //是否启动分页
    self.swipeView.pagingEnabled = YES;
    //每页的项目数
    self.swipeView.itemsPerPage = 1;
    //截断最后一页
    self.swipeView.truncateFinalPage = NO;
    //当前项目索引
    self.swipeView.currentItemIndex = 0;
    //当前页
    self.swipeView.currentPage = 0;
    //滚动偏移量(滚动到某一项目)
    self.swipeView.scrollOffset = 1;
    //滑动视图对齐方式
    self.swipeView.alignment = SwipeViewAlignmentCenter;
    //是否手动滚动
    self.swipeView.scrollEnabled = YES;
    //是否轮询
    self.swipeView.wrapEnabled = YES;
    //延迟内容接触
    self.swipeView.delaysContentTouches = YES;
    //弹簧效果
    self.swipeView.bounces = YES;
    //减速率
    self.swipeView.decelerationRate = 0.5;
    //是否垂直滑动
    self.swipeView.vertical = YES;
    //自动滚动
    //self.swipeView.autoscroll = 1.0;
    //添加滑动视图
    [self.view addSubview:self.swipeView];
    //设置约束
    [self.swipeView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(STATUSBARHEIGHT + NAVIGATIONBAR_HEIGHT);
        make.left.right.equalTo(self.view);
        make.height.mas_equalTo(SCREEN_HEIGHT / 2);
    }];
}

///计时器开始工作
- (void)openTimer{
    __weak typeof(self) weakSelf = self;
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(_timer, ^{
        [weakSelf.swipeView scrollToPage:(weakSelf.swipeView.currentPage + 1) duration:0.5];
    });
    dispatch_resume(_timer);
}

///数据源
- (NSMutableArray *)swipeViewDataSource{
    if(_swipeViewDataSource == nil){
        _swipeViewDataSource = [[NSMutableArray alloc]init];
        UIImage *image1 = [UIImage imageNamed:@"pl_user_avatar"];
        UIImage *image2 = [UIImage imageNamed:@"test"];
        UIImage *image3 = [UIImage imageNamed:@"Toast"];
        UIImage *image4 = [UIImage imageNamed:@"btn_back_dark"];
        [_swipeViewDataSource addObject:image1];
        [_swipeViewDataSource addObject:image2];
        [_swipeViewDataSource addObject:image3];
        [_swipeViewDataSource addObject:image4];
    }
    return _swipeViewDataSource;
}

#pragma make -- SwipeViewDataSource
///滑动视图中的项目数
- (NSInteger)numberOfItemsInSwipeView:(SwipeView *)swipeView {
    return self.swipeViewDataSource.count;
    
}

///滑动视图中的每一个项目
- (UIView *)swipeView:(SwipeView *)swipeView viewForItemAtIndex:(NSInteger)index
          reusingView:(UIView *)view {
    UIImageView *imageView = (UIImageView *)view;
    if(imageView == nil){
        imageView = [[UIImageView alloc] initWithFrame:swipeView.bounds];
    }
    [imageView setImage:[self.swipeViewDataSource objectAtIndex:index]];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    return imageView;
}

#pragma make -- SwipeViewDelegate
///滑动视图项目大小(不设置ScrollView只是一条线)
- (CGSize)swipeViewItemSize:(SwipeView *)swipeView {
    return swipeView.bounds.size;
}

/////滑动视图点击事件
- (void)swipeView:(SwipeView *)swipeView didSelectItemAtIndex:(NSInteger)index{
    NSLog(@"%@",swipeView.currentItemView);
    [self.swipeView scrollByOffset:1 duration:3];
}
//
///当scrollView的contentOffset改变时调用。
- (void)swipeViewDidScroll:(SwipeView *)swipeView{
    NSLog(@"当scrollView的contentOffset改变时调用");
}

///当前项目索引更新时调用
- (void)swipeViewCurrentItemIndexDidChange:(SwipeView *)swipeView{
    NSLog(@"当前项目索引更新时调用");
}
///当开始滚动视图时,执行改方法,一次有效滑动只执行一次(开始滑动,滑动一小段距离,只要手指不松开,算一次滑动)
- (void)swipeViewWillBeginDragging:(SwipeView *)swipeView{
    NSLog(@"当开始滚动视图时,执行改方法,一次有效滑动只执行一次(开始滑动,滑动一小段距离,只要手指不松开,算一次滑动)");
}
///滑动视图,当手指离开时,调用该方法,一次有效滑动只执行一次
- (void)swipeViewDidEndDragging:(SwipeView *)swipeView willDecelerate:(BOOL)decelerate{
    NSLog(@"滑动视图,当手指离开时,调用该方法,一次有效滑动只执行一次");
}
///滑动减速时调用该方法
- (void)swipeViewWillBeginDecelerating:(SwipeView *)swipeView{
    NSLog(@"滑动减速时调用该方法");
}
///滚动视图减速完成,滚动将停止时,调用该方法,一次有效滑动,只执行一次
- (void)swipeViewDidEndDecelerating:(SwipeView *)swipeView{
    NSLog(@"滚动视图减速完成,滚动将停止时,调用该方法,一次有效滑动,只执行一次");
}
///滑动视图结束滚动时()
- (void)swipeViewDidEndScrollingAnimation:(SwipeView *)swipeView{
    NSLog(@"滑动视图结束滚动时");
}

@end

发现的问题:currentPage、currentItemIndex属性似乎不起作用,而且在竖直方向滑动时,数据源长度会对开始的第一张视图产生影响,开始的第一张视图会混乱,并不是数据源中设置的第一个视图,感觉问题出在返回固定的偏移量方法中,我尝试排查一下问题的原因,但是失败了。

运行结果:
Untitled.gif

推荐阅读更多精彩内容

  • 一、简介 <<继承关系:UIScrollView --> UIView-->UIResponder-->NSObj...
    无邪8阅读 497评论 0 0
  • 掌握 UIScrollView的常见属性 UIScrollView的常用代理方法 UIScrollView的缩放 ...
    JonesCxy阅读 1,745评论 1 12
  • - 什么是UISCrollView + 当手机屏幕需要展示的内容较多超出一个屏幕时,用户可以通过滚动手势来查看屏幕...
    洋子总阅读 16,473评论 3 16
  • 在iOS中,滚动视图UIScrollView用于查看大于屏幕的内容。Scroll View有两个主要目的: 让用户...
    pro648阅读 10,824评论 2 12
  • iOS的坐标系 原点是屏幕的左上角,各机型取值范围如下表所述: UIView 视图的类(略讲) @property...
    JavaLily阅读 2,495评论 0 25