无限轮播视图
在仿写 ZARA 的时候, 在商品展示页需要写一个无限轮播视图用于展示商品, 笔者在这一部分遇到了很多困难, 现在在这里做一总结
首先创建一个 UIScrollView, 用于添加图片进行商品展示
笔者在定义部分定义了以下几个需要用到的属性
objc
// Searchpage.h
@interface Searchpage
// 分栏控件
@property (nonatomic, strong) UISegmentedControl* segmentedControl;
// 滚动视图
@property (nonatomic, strong) UIScrollView* scrollView;
// 滚动视图的图片名称
@property (nonatomic, strong) NSMutableArray* imageNames;
// 定时器, 使视图自动轮播
@property (nonatomic, strong) NSTimer* timer;
@end
其中:
- 分栏控件用于通过商品标签快速切换商品
- 滚动视图用于展示商品图片
- 定时器用于实现自动轮播
首先实现顶部分类栏的创建
objc
// 设置分栏控件
- (void)setSegmentedControl {
NSArray *items = @[@"女士", @"儿童", @"男士"];
self.segmentedControl = [[UISegmentedControl alloc] initWithItems:items];
// 设置默认选中
self.segmentedControl.selectedSegmentIndex = 0;
// 设置文字颜色
[self.segmentedControl setTitleTextAttributes:@{
NSForegroundColorAttributeName : [UIColor whiteColor]
} forState:UIControlStateNormal];
// 设置选中文字颜色
[self.segmentedControl setTitleTextAttributes:@{
NSForegroundColorAttributeName : [UIColor redColor]
} forState:UIControlStateSelected];
// 设置背景颜色
self.segmentedControl.backgroundColor = [UIColor blackColor];
// 设置滑块颜色
self.segmentedControl.selectedSegmentTintColor = [UIColor greenColor];
[self.view addSubview:self.segmentedControl];
// 使用Masonry进行约束
[self.segmentedControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.view).offset(80);
make.left.mas_equalTo(self.view);
make.width.mas_equalTo(self.view);
make.height.mas_equalTo(60);
}];
// 添加点击事件
[self.segmentedControl addTarget:self
action:@selector(segmentChanged:)
forControlEvents:UIControlEventValueChanged];
}
这里主要完成了以下几个功能:
- 创建
UISegmentedControl - 设置默认选中的栏目
- 设置文字颜色与滑块颜色
- 使用
Masonry添加约束 - 添加栏目切换事件
顶部分类栏完成后, 接下来开始实现滚动视图部分
objc
// 设置滚动视图
- (void)setInterface {
// 初始化图片数组
self.imageNames = [[NSMutableArray alloc] init];
// 无限轮播核心
// 在数组头尾添加临时图片
[self.imageNames addObject:@"3.jpg"];
for (int i = 1; i <= 3; i++) {
NSString *imageName = [NSString stringWithFormat:@"%d.jpg", i];
[self.imageNames addObject:imageName];
}
[self.imageNames addObject:@"1.jpg"];
// 创建scrollView
self.scrollView = [[UIScrollView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(160);
make.left.mas_equalTo(0);
make.width.mas_equalTo(self.view);
make.bottom.mas_equalTo(self.view).offset(-80);
}];
// 强制刷新布局
[self.view layoutIfNeeded];
// 设置contentSize
self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width * self.imageNames.count, 0);
// 默认显示第一页
self.scrollView.contentOffset = CGPointMake(self.scrollView.bounds.size.width, 0);
// 分页滑动
self.scrollView.pagingEnabled = YES;
// 添加图片
for (int i = 0; i < self.imageNames.count; i++) {
UIImage *image = [UIImage imageNamed:self.imageNames[i]];
UIImageView *iView = [[UIImageView alloc] initWithImage:image];
[self.scrollView addSubview:iView];
[iView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.scrollView.bounds.size.width * i);
make.top.mas_equalTo(self.scrollView);
make.width.mas_equalTo(self.scrollView.mas_width);
make.height.mas_equalTo(self.scrollView.mas_height);
}];
}
// 设置代理
self.scrollView.delegate = self;
}
这里用到了
[self.view layoutIfNeeded];这个方法的作用是立即让当前视图进行一次布局更新,使约束马上生效。在使用
Masonry或Auto Layout时:写完约束后,系统其实并不会立刻计算控件的真实大小和位置。
系统默认会等到:
- 当前 RunLoop 结束
- 或下一次刷新界面
才统一进行布局计算。
在刚设置完 Masonry 约束后,Auto Layout 还没有开始真正计算 frame,所以此时 view 的 frame / bounds 很可能还是初始值(通常是 0)
可以看以下例子
objc
// 设置滚动视图
self.scrollView = [[UIScrollView alloc] init];
[self.view addSubview: self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(160);
make.left.mas_equalTo(0);
make.width.mas_equalTo(self.view);
make.bottom.mas_equalTo(self.view).offset(-80);
}];
NSLog(@"%f", self.scrollView.bounds.size.width);
// 强制刷新视图, 使约束生效
[self.view layoutIfNeeded];
NSLog(@"%f", self.scrollView.bounds.size.width);
在设置完约束后,打印scrollView的width, 输出是 0 通过[self.view layoutIfNeeded];方法强制重新布局的时候, 就可以使约束立即生效

最关键的部分就是无限轮播的实现
正常情况下图片数组只有:
1.jpg
2.jpg
3.jpg
但这样无法形成循环轮播
因此需要在数组头尾各插入一张临时图片
最终数组会变成:
3.jpg
1.jpg
2.jpg
3.jpg
1.jpg
这样在滑动到边界时, 就可以偷偷修改 contentOffset, 用户视觉上就会感觉是无限滑动的效果。
在这里就需要用到UIScrollView的协议, 这里先遵守协议UIScrollViewDelegate. 并设置代理为当前的视图控制器
objc
@interface Searchpage : UIViewController <UIScrollViewDelegate>
@end
// 设置滚动视图的代理为当前控制器
self.scrollView.delegate = self;
那么就可以在协议方法中进行无限视图的轮播的实现了
通过滚动视图的偏移来计算视图的页码
objc
// 获取当前滚动视图页码
- (NSInteger)currentPage {
// 获取滚动视图的坐标
// 计算出是当前视图的页码
CGFloat width = self.scrollView.bounds.size.width;
return self.scrollView.contentOffset.x / width - 1;
}
然后在滑动滚动视图的时候同时切换分栏控件的标签, 特别注意的是, 当滑动到临时页的时候, 要修改contentOffset, 实现无限视图的效果
objc
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// CGFloat page = self.scrollView.contentOffset.x / self.scrollView.bounds.size.width;
CGFloat page = [self currentPage];
if (page == -1) {
[self.scrollView setContentOffset: CGPointMake(self.scrollView.bounds.size.width * (self.imageNames.count - 2), 0) animated: NO];
page = [self currentPage];
} else if (page == self.imageNames.count - 2) {
[self.scrollView setContentOffset: CGPointMake(self.scrollView.bounds.size.width, 0) animated: NO];
page = [self currentPage];
}
self.segmentedControl.selectedSegmentIndex = page;
}
通过分栏控件的改变切换滚动视图的偏移, 这一部分可以通过分栏控件的标签索引来定位滚动视图的偏移量
objc
// 分栏控件点击响应
- (void)segmentChanged:(UISegmentedControl *) sender {
NSInteger index = sender.selectedSegmentIndex;
// 当分栏控件的索引发生改变,滚动视图也跟随滚动到相对应的页面
// 设置当前scrollView的左边界
CGFloat left = self.view.bounds.size.width * (index + 1);
// 调用方法滚动到相应的页面
// 滑动到指定的坐标 (x, y)
// YES: 带动画
[self.scrollView setContentOffset: CGPointMake(left, 0) animated: NO];
}
通过定时器来实现自动轮播
可以通过定时器来实现滚动视图的自动翻页
首先创建定时器和设置事件
objc
- (void) setTimer {
// repeats: YES: 重复执行
self.timer = [NSTimer scheduledTimerWithTimeInterval: 2 target: self selector: @selector(nextPage) userInfo: nil repeats: YES];
}
// 定时器事件, 定时翻页
- (void) nextPage {
NSInteger nextIndex = self.segmentedControl.selectedSegmentIndex + 1;
if (nextIndex == 3) {
nextIndex = 0;
}
self.segmentedControl.selectedSegmentIndex = nextIndex;
// 需要手动改变滚动视图
[self segmentChanged: self.segmentedControl];
}
这里设置了时间间隔 2 秒翻页, 然后就需要思考在什么时候开启定时器和关闭定时器, 当用户手动滑动滚动视图的就需要关闭定时器, 用户不滑动的时候都要进行自动翻页
这样就可以使用滚动视图的代理方法- (void) scrollViewWillBeginDragging:(UIScrollView *)scrollView 和 \- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView 分别在当滑动时和结束滑动时调用
首先, 在滑动时候关闭定时器并置为空 (防止重复创建定时器导致无法关闭)
objc
// 当用户滑动视图的时候, 关闭定时器
- (void) scrollViewWillBeginDragging:(UIScrollView *)scrollView {
// 停止定时器并从运行循环中移除
[self.timer invalidate];
// 置为空,防止多次创建
self.timer = nil
}
然后当滑动结束的时候重新创建一个定时器进行自动翻页
objc
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self setTimer];
}
