OCUI界面设计:滚动视图与分页控件结合NSTimer实现图片自动循环与无限滚动展示

时间:2022-06-15 00:03:28

前言

在开发过程中,经常会遇到一些图片展示的需求,比如影视类App,会在主页循环滚动播放电影信息,亦或是电子商务类,如主页循环展示当前的折扣界面信息等。可见图片循环展示的重要性,如下我将详细讲解图片循环展示的实现方式。

效果展示

OCUI界面设计:滚动视图与分页控件结合NSTimer实现图片自动循环与无限滚动展示

技术分析

1、滚动视图(UIScrollView):上述效果中,不仅可以自动展示图片,用户也可以直接滑动图片,查看图片内容,既然可以滑动,必然会用到滚动视图,滚动视图的一大特性就是实现了很多手势,用户可以利用滑动手势,滚动视图,查看超出视图部分的内容信息。

2、定时器(NSTimer):利用定时器可实现图片的自动展示,无需用户滑动查看,图片即可自动切换。

3、分页控件(UIPageControl):UIPageControl称为分页控件,通常和UIScrollView配合使用,用于指示页面总数及当前页指示。

实现过程

首先我们创建一个工程,我使用的是手动管理内存,尽管现在苹果推荐使用ARC,但我还是建议初学者在初期使用手动管理内存的形式,以加深对内存管理的理解。首先我们需要为视图控制器添加导航栏,具体实现方式这里省略。然后为了分离逻辑,这里我创建了初始化数据与初始化视图的方法,并且在视图加载完成时调用,注意调用顺序,先初始化数据,再初始化界面。如下所示:

@interface ViewController () 

- (void)initializeDataSource; /**< 初始化数据源 */
- (void)initializeUserInterface; /**< 初始化用户界面 */

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initializeDataSource];
    [self initializeUserInterface];

}

@end

接下来在视图控制器中创建滚动视图与分页控件,这里我使用懒加载的形式创建,当然使用懒加载,必然需要去声明对应的属性,如下所示:

@property (nonatomic, copy) UIScrollView *scrollView; /**< 滚动视图 */
@property (nonatomic, copy) UIPageControl *pageControl; /**< 分页控件 */

然后重写getter()方法,实现懒加载,创建对象。

#pragma mark *** Getters ***
- (UIScrollView *)scrollView {
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc] init];
        _scrollView.bounds = CGRectMake(0, 0, ScrollView_Width, ScrollView_Height);
        _scrollView.center = self.view.center;
        _scrollView.backgroundColor = [UIColor redColor];
        // 设置内容大小
        _scrollView.contentSize = CGSizeMake(3 * ScrollView_Width, ScrollView_Height);
        // 设置偏移量
        _scrollView.contentOffset = CGPointMake(ScrollView_Width, 0);
        // 设置能否回弹
        _scrollView.bounces = NO;
        // 设置是否分页
        _scrollView.pagingEnabled = YES;
        // 设置能否滚动
        _scrollView.scrollEnabled = YES;
        // 设置是否点击状态栏是否回到顶部
        _scrollView.scrollsToTop = YES;
        // 设置是否显示横向指示器
        _scrollView.showsHorizontalScrollIndicator = YES;
        // 设置是否显示纵向指示器
        _scrollView.showsVerticalScrollIndicator = YES;
        // 设置代理
        _scrollView.delegate = self;
    }
    return _scrollView;
}

- (UIPageControl *)pageControl {
    if (!_pageControl) {
        _pageControl = [[UIPageControl alloc] init];
        _pageControl.bounds = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 40);
        _pageControl.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMaxY(self.scrollView.frame) - CGRectGetMidY(_pageControl.bounds));
        // 设置总页数
        _pageControl.numberOfPages = _imageNames.count;
        // 设置当前页数
        _pageControl.currentPage = 0;
        // 设置总页数指示色
        _pageControl.pageIndicatorTintColor = [UIColor darkGrayColor];
        // 设置当前页数指示色
        _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor];
    }
    return _pageControl;
}

上述代码中,我们看到ScrollView_Height,以及ScrollView_Width两个宏定义,其分别表示滚动视图的高度与宽度。因此我们还需声明宏定义,在导入文件的下方做如下声明:

#define ScrollView_Height 280
#define ScrollView_Width [UIScreen mainScreen].bounds.size.width

除了上述宏定义,这里我还需说明在设置滚动视图contentSize属性时,如果高度等于滚动视图高度,宽度大于滚动视图宽度时,说明创建的是一个横向的滚动视图,反之则是纵向滚动视图,这里我设置的是3倍滚动视图宽度,因此是一个横向的,而之所以要创建3倍滚动视图的宽度,是因为我们知道,在一般情况下,一张图片显示默认宽度为滚动视图的宽度,如果有10张图片需要显示,应该是10倍滚动视图宽度,如果是100张呢?那就是100倍,而且对应的我们会去 alloc + init 100次创建100个图片视图展示图片,这样做无论是对内存的使用亦或是在性能上或用户体验上,都是不佳的,因此我设置3倍屏幕宽度,并且将滚动视图的偏移量设置为屏幕的宽度,默认显示中间的图片,这样我不仅可以实现左滑,也可以实现右滑,这里大家可能会有疑问,那就是如果是3倍滚动视图宽度,那如果我要显示10张图片该如何解决呢?不用着急,在后面我将会提到具体的解决方法。在设置滚动视图属性时,还设置了一个代理属性delegate,此时工程会报警告,不用担心,这是由于没有遵守协议导致,所以我们只需遵守对应的协议即可。

实现了滚动视图与分页控件的懒加载之后,我们需要在初始化用户界面方法中添加滚动视图与分页控件。接下来我们需要去处理数据,这里需要创建两个可变数组,一个用于存储图片名字,这里我使用了10张jpg格式的图片素材,素材各位可自己找,这里我就不提供了。另一个用于存储创建的三个图片视图,具体实现方式如下:

#pragma mark *** Initialize methods ***
- (void)initializeDataSource {

    // 初始化数据集合
    _imageViews = [[NSMutableArray alloc] init];
    _imageNames = [[NSMutableArray alloc] init];

    // 将图片名字放入图片名字数组中
    for (int i = 0; i < 10; i++) {
        // 获取图片名字,图片名字以0.jpg, 1.jpg...形式命名;
        NSString *imageName = [NSString stringWithFormat:@"%d.jpg", i];
        // 将图片名字放入图片名字数组中
        [_imageNames addObject:imageName];
    }

    // 创建图片视图,将其添加到滚动视图中,并且放入图片视图集合中。
    for (int i = 0; i < 3; i++) {
        // 创建图片视图
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(i * ScrollView_Width, 0, ScrollView_Width, ScrollView_Height)];
        // Set imageView's scale model.
        // UIViewContentModeScaleAspectFit:完整显示图片
        // UIViewContentModeScaleToFill:完整显示图片(缩放)并且充满图片视图
        // UIViewContentModeScaleAspectFill:不改变图片原有大小充满图片视图

        imageView.contentMode = UIViewContentModeScaleToFill;
        // 设置图片视图边框
        imageView.layer.borderWidth = 1.0f;

        [self.scrollView addSubview:imageView];

        [_imageViews addObject:imageView];

        [imageView release];
    }
}

现在数据已经处理完毕,并且滚动视图上也有三个图片视图了,那么接下来我们需要将图片显示在滚动视图上,同样的,我创建了一个方法来实现这样的逻辑,如下所示:

#pragma mark *** Private methods ***
- (void)insertImageToImageView {
    [_imageViews enumerateObjectsUsingBlock:^(UIImageView *  _Nonnull imageView, NSUInteger idx, BOOL * _Nonnull stop) {
        imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForAuxiliaryExecutable:_imageNames[idx]]];
    }];
}

上述代码中,我是通过块枚举来遍历集合,配置图片,别忘了我们还需要在视图加载完成后调用,这里需要注意,添加的图片仅仅只是显示了前三张图片。并且我们会发现,滚动视图图片上方有一部分空白,这是由于系统自动偏移导致的,隐藏空白区域,我们需要到视图加载完成方法中添加如下代码,关闭系统自动偏移。

self.automaticallyAdjustsScrollViewInsets = NO;

接下来,我们就要解决如何通过三个图片视图显示10张图片并且实现无限滚动的逻辑,这里我们需要利用滚动视图的协议方法来实现,主要用到scrollViewDidScroll:方法,该方法会在滚动视图滚动时调用,具体实现方式如下:

#pragma mark *** UIScrollViewDelegate ***
// 滚动
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 实现无限滚动逻辑

    // 根据偏移量
    if (scrollView.contentOffset.x >= 2 * ScrollView_Width) {
        // 手势向左
        id firstObject = [_imageNames.firstObject mutableCopy];

        [_imageNames removeObjectAtIndex:0];

        [_imageNames addObject:firstObject];

        [firstObject release];

        // 更新分页控件指示
        _pageControl.currentPage = _pageControl.currentPage == 9 ? 0 : ++ _pageControl.currentPage;

    }else if (scrollView.contentOffset.x <= 0 ) {
        // 手势向右
        id lastObject = [_imageNames.lastObject mutableCopy];

        [_imageNames removeLastObject];

        [_imageNames insertObject:lastObject atIndex:0];

        [lastObject release];

        // 更新分页控件指示
        _pageControl.currentPage = _pageControl.currentPage == 0 ? 9 : -- _pageControl.currentPage;

    }else {
        // 停在中间
        return;
    }
    // 更新图片
    [self insertImageToImageView];
    // 更新偏移量
    scrollView.contentOffset = CGPointMake(ScrollView_Width, 0);


}

这里我解释一下逻辑,我们知道,当用户向左滑时,偏移量会增加,反之减少,因此,可通过偏移量的x值,判断用户当前是左滑还是右滑。由于滚动视图内容显示宽度为3倍滚动视图宽度,而偏移量主要是视图左上角的点在滚动视图中的位置,所以,当偏移量大于等于2倍宽度时,用户在向左滑动,当偏移量小于等于0时,用户在向右滑动。理解了左滑右滑之后,我们还需要知道,如何去显示其他更多的图片呢?这里我需要解释一下,其实我们可以发现,当第一次显示图片的时候,并非显示的是第一张,而是显示的第二张,因为我们在设置滚动视图偏移量的时候,显示的是中间的图片视图,我们只是给了用户一个错觉。而要实现无限滚动,其实并不复杂,这里我只解释手势向左的情况,当向左滑动图片时,顺序显示图片,显示到最后一张图片时,如何又返回到第一张图片呢?如果是10张图片,用10个视图显示,可能很多人会想着直接将滚动视图的偏移量设置到(0, 0)点不就OK了吗?这样是没有问题,但是用户体验不佳,因为这样去设置,会发现当从最后一张切换到第一张时,滚动视图是逆向滚动直接跳转到第一张图片的,所以不建议通过偏移量来操作,而应该向上述代码一样,我们可对数据源做操作,只需修改图片名字的顺序就OK了,如果已经显示到最后一张图片,我们可将第一张图片名字放到集合的最后,依次循环操作即可,但是为了保证图片集合的大小不变,我们需要拷贝第一张图片名字,然后将集合中的第一个元素删除,最后将拷贝出来的元素追加到集合中即可。这样就实现了集合顺序的改变,当然这里我们只是对数据做了处理,我们还需要调用设置图片的方法,刷新图片视图,并且将偏移量重新置为屏幕的中间,这样即可实现左滑右滑,也可实现无限滚动。当然我们还需去更新分页控件,逻辑也比较简单,只需判断如果当前已经显示到最后一张,我只需修改其指向的位置,指向第一张即可,否则就让当前指示页做一个自加的操作。

现在我们已经可以左滑右滑,并且无限滚动了,而我要实现自动播放,就需要使用定时器NSTimer了,首先声明一个全局变量:

NSTimer *_timer; /**< 定时器 */

然后在初始化用户界面方法中初始化定时器,并且设置启动时间。

    // 初始化定时器
    // 参数1:时间间隔,即没多少秒执行一次关联方法;
    // 参数2:执行对象;
    // 参数3:关联方法;
    // 参数4:用户信息(可用于传递数据);
    // 参数5:是否重复执行
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.5f target:self selector:@selector(respondsToTimer) userInfo:nil repeats:YES];
    // 启动定时器,在2秒后执行
    _timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:2.0];

下面我们看看定时器关联方法中的逻辑:

#pragma mark *** Timer methods ***
- (void)respondsToTimer {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 改变偏移
    [_scrollView setContentOffset:CGPointMake(2 * ScrollView_Width, 0) animated:YES];
}

上述代码,我将滚动视图偏移量移到了2倍滚动视图宽度的位置,偏移量一旦改变,并会执行滚动视图协议方法scrollViewDidScroll:,因此这里,我用定时器模拟了用户滑动的操作,实现了自动轮播。

但是会有一个小小的问题,那就是,因为定时器每隔1.5秒都会执行一次关联方法,如果此时我再去滑动图片,并且刚好在定时器触发方法的时间节点上,那么就会出现我刚滑动到一张图片然后瞬间就显示到下一张图片上的情况,显然,这样的用户体验是非常差的。要解决这个方法,我可在滚动视图协议方法中去做操作,当用户开始拖动的时候,我暂停定时器,当用户停止拖动的时候,我延后2秒,给用户浏览图片的时间,然后再启动定时器即可。具体实现方式如下:

// 开始拖动
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 暂停timer
    _timer.fireDate = [NSDate distantFuture];
}

// 停止拖动
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 启动timer
    _timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:2.0f];
}

OK,到了这一步,我们就实现了上述展示的效果了。在后面,我将贴上整个实现过程的完整代码,以供各位参考。

完整实现代码

#import "ViewController.h"

#define ScrollView_Height 280
#define ScrollView_Width [UIScreen mainScreen].bounds.size.width

static NSString *const kViewControllerTitle = @"滚动视图";

@interface ViewController () <UIScrollViewDelegate>

{
    NSTimer *_timer; /**< 定时器 */
    NSMutableArray *_imageNames;
    NSMutableArray *_imageViews;

}

@property (nonatomic, copy) UIScrollView *scrollView; /**< 滚动视图 */
@property (nonatomic, copy) UIPageControl *pageControl; /**< 分页控件 */

- (void)initializeDataSource; /**< 初始化数据源 */
- (void)initializeUserInterface; /**< 初始化用户界面 */
- (void)insertImageToImageView; /**< 将图片添加到图片视图中 */

@end

@implementation ViewController

- (void)dealloc
{
    NSLog(@"%@", NSStringFromSelector(_cmd));

    [_imageNames  release]; _imageNames = nil;
    [_imageViews  release]; _imageViews = nil;
    [_scrollView  release]; _scrollView = nil;
    [_pageControl release]; _pageControl = nil;


    // 销毁定时器
    [_timer    invalidate];
    [_timer       release]; _timer = nil;

    [super dealloc];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initializeDataSource];
    [self initializeUserInterface];

}

#pragma mark *** Initialize methods ***
- (void)initializeDataSource {

    // 初始化数据集合
    _imageViews = [[NSMutableArray alloc] init];
    _imageNames = [[NSMutableArray alloc] init];

    // 将图片名字放入图片名字数组中
    for (int i = 0; i < 10; i++) {
        // 获取图片名字,图片名字以0.jpg, 1.jpg...形式命名;
        NSString *imageName = [NSString stringWithFormat:@"%d.jpg", i];
        // 将图片名字放入图片名字数组中
        [_imageNames addObject:imageName];
    }

    // 创建图片视图,将其添加到滚动视图中,并且放入图片视图集合中。
    for (int i = 0; i < 3; i++) {
        // 创建图片视图
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(i * ScrollView_Width, 0, ScrollView_Width, ScrollView_Height)];
        // Set imageView's scale model.
        // UIViewContentModeScaleAspectFit:完整显示图片
        // UIViewContentModeScaleToFill:完整显示图片(缩放)并且充满图片视图
        // UIViewContentModeScaleAspectFill:不改变图片原有大小充满图片视图

        imageView.contentMode = UIViewContentModeScaleToFill;
        // 设置图片视图边框
        imageView.layer.borderWidth = 1.0f;

        [self.scrollView addSubview:imageView];

        [_imageViews addObject:imageView];

        [imageView release];
    }
}

- (void)initializeUserInterface {
    self.view.backgroundColor = [UIColor whiteColor];
    self.title = kViewControllerTitle;
    self.automaticallyAdjustsScrollViewInsets = NO;

    [self.view addSubview:self.scrollView];
    [self.view addSubview:self.pageControl];

    [self insertImageToImageView];

    // 初始化定时器
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.5f target:self selector:@selector(respondsToTimer) userInfo:nil repeats:YES];
    // 启动定时器
    _timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:2.0];
}

#pragma mark *** Timer methods ***
- (void)respondsToTimer {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 改变偏移
    [_scrollView setContentOffset:CGPointMake(2 * ScrollView_Width, 0) animated:YES];
}

#pragma mark *** Private methods ***
- (void)insertImageToImageView {
    [_imageViews enumerateObjectsUsingBlock:^(UIImageView *  _Nonnull imageView, NSUInteger idx, BOOL * _Nonnull stop) {
        imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForAuxiliaryExecutable:_imageNames[idx]]];
    }];
}

#pragma mark *** UIScrollViewDelegate ***
// 滚动
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 实现无限滚动逻辑

    // 根据偏移量
    if (scrollView.contentOffset.x >= 2 * ScrollView_Width) {
        // 手势向左
        id firstObject = [_imageNames.firstObject mutableCopy];

        [_imageNames removeObjectAtIndex:0];

        [_imageNames addObject:firstObject];

        [firstObject release];

        // 更新分页控件指示
        _pageControl.currentPage = _pageControl.currentPage == 9 ? 0 : ++ _pageControl.currentPage;

    }else if (scrollView.contentOffset.x <= 0 ) {
        // 手势向右
        id lastObject = [_imageNames.lastObject mutableCopy];

        [_imageNames removeLastObject];

        [_imageNames insertObject:lastObject atIndex:0];

        [lastObject release];

        // 更新分页控件指示
        _pageControl.currentPage = _pageControl.currentPage == 0 ? 9 : -- _pageControl.currentPage;

    }else {
        // 停在中间
        return;
    }
    // 更新图片
    [self insertImageToImageView];
    // 更新偏移量
    scrollView.contentOffset = CGPointMake(ScrollView_Width, 0);


}
// 开始拖动
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 暂停timer
    _timer.fireDate = [NSDate distantFuture];
}

// 停止拖动
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    // 启动timer
    _timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:2.0f];
}

// 开始减速
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

// 停止减速
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

#pragma mark *** Getters ***
- (UIScrollView *)scrollView {
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc] init];
        _scrollView.bounds = CGRectMake(0, 0, ScrollView_Width, ScrollView_Height);
        _scrollView.center = self.view.center;
        _scrollView.backgroundColor = [UIColor redColor];
        // 设置内容大小
        _scrollView.contentSize = CGSizeMake(3 * ScrollView_Width, ScrollView_Height);
        // 设置偏移量
        _scrollView.contentOffset = CGPointMake(ScrollView_Width, 0);
        // 设置能否回弹
        _scrollView.bounces = NO;
        // 设置是否分页
        _scrollView.pagingEnabled = YES;
        // 设置能否滚动
        _scrollView.scrollEnabled = YES;
        // 设置是否点击状态栏是否回到顶部
        _scrollView.scrollsToTop = YES;
        // 设置是否显示横向指示器
        _scrollView.showsHorizontalScrollIndicator = YES;
        // 设置是否显示纵向指示器
        _scrollView.showsVerticalScrollIndicator = YES;
        // 设置代理
        _scrollView.delegate = self;
    }
    return _scrollView;
}

- (UIPageControl *)pageControl {
    if (!_pageControl) {
        _pageControl = [[UIPageControl alloc] init];
        _pageControl.bounds = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 40);
        _pageControl.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMaxY(self.scrollView.frame) - CGRectGetMidY(_pageControl.bounds));
        // 设置总页数
        _pageControl.numberOfPages = _imageNames.count;
        // 设置当前页数
        _pageControl.currentPage = 0;
        // 设置总页数指示色
        _pageControl.pageIndicatorTintColor = [UIColor darkGrayColor];
        // 设置当前页数指示色
        _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor];
    }
    return _pageControl;
}

@end