iOS实战-自定义的横向滚动控件CustomScrollView

CustomScrollView

  使用官方UIScrollView组件定制的一个横向滚动的视图。由于能力有限,暂没有抽象成一个UI组件,如果有大神能进行抽象封装,非常欢迎,大家多多交流!

说明

  CustomScrollView包括诺干个子视图,可以横向滚动,滚动过程中会根据子视图所在位置进行大小缩放。即最中间的视图最大,两边呈对称状态逐渐减小。且可以通过点击按钮进行滚动,选定某个子视图居中。还可以动态进行新增和删除子视图的操作,其中删除操作为在子视图上进行上滑手势操作。

Github 传送门

截图

具体实现

接下来我们来看看是怎么一步一步实现这种效果的。

模型

这里的模型只是我们简单定义的一个数据模型,模型包含了一个名称和对应的logo图标的名字。

//YSModel.h

#import <Foundation/Foundation.h>

@interface YSModel : NSObject

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *logoName;

- (instancetype)initWithName:(NSString *)name logoName:(NSString *)logoName;

@end

//YSModel.m

#import "YSModel.h"

@implementation YSModel

- (instancetype)init
{
    return [self initWithName:@"自定义" logoName:@"custom"];
}

- (instancetype)initWithName:(NSString *)name logoName:(NSString *)logoName
{
    self = [super init];
    if (self)
    {
        _name = name;
        _logoName = logoName;
    }
    return self;
}

@end

界面实现和控件绑定

  界面直接在xib文件里实现。只需要一个UIScrollView和UILabel就可以了,UILabel是为了当UIScrollView中的子视图滚动式,也会跟着切换。效果如图:

ViewController的实现

首先我们需要一些宏定义的常量:

//默认scrollView显示的模型数目
#define MODEL_NUMBER 5
//屏幕宽度
#define UISCREEN_WIDTH [[UIScreen mainScreen] bounds].size.width
//默认图标缩放比率
#define SCALE_RATE 0.6

其次我们需要一些变量,比如Outlet变量,数据模型数组等变量,详见demon里的代码。接下来我们主要详细介绍几个重点方法。

计算每个cell的宽高,以及初始化数据和UI视图

- (void)viewDidLoad
{
    [super viewDidLoad];

    //计算ScrollView中每个cell的宽高
    self.cellWidth = self.scrollView.frame.size.width / MODEL_NUMBER;
    self.cellHeight = self.scrollView.frame.size.height;

    [self initModels];
    [self initScrollView];
}

1.[self initModels] 方法主要是初始化模型数据,比较简单。
2.其主要的ScrollView初始化实现为方法[self initScrollView]。
该方法中,主要设置了ScrollView的初始化属性,例如禁用水平,垂直方向的滚动条,设定ScrollView初始位置等。

- (void)initScrollView
{
    //设定scrollView的contentSize,即scrollView中包含的cell个数计算出来的内容大小
    //+4是因为前后分别有两个空白的cell视图
    self.scrollView.contentSize = CGSizeMake(self.cellWidth * (self.models.count + 4), self.cellHeight);

    //清除scrollView的子视图
    //[self.scrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];

    //设置scrollView的委托对象
    self.scrollView.delegate = self;
    //隐藏水平和竖直方向的滚动条
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.showsVerticalScrollIndicator = YES;
    //设置scrollView滚动的减速速率
    self.scrollView.decelerationRate = 0.95f;

    if (!self.cellView)
    {
        self.cellView = [NSMutableArray array];
    }
    else
    {
        [self.cellView removeAllObjects];
    }

    //添加两个空白的cell块
    for (int i = 0; i < 2; i++)
    {
        UIView *view = [self createEmptyCell:CGRectMake(self.cellWidth * i, 0, self.cellWidth, self.cellHeight)];
        [self.scrollView addSubview:view];
    }

    //默认的六个块
    for (int i = 2; i < self.models.count + 2; i++)
    {
        UIView *view = [[UIView alloc] initWithFrame:CGRectMake(self.cellWidth * i, 0, self.cellWidth, self.cellHeight)];

        //创建一个ImageView用于显示图标logo
        UIImageView *image = [[UIImageView alloc] initWithFrame:CGRectMake(5, 5, self.cellWidth - 10, self.cellWidth - 10)];
        //设置图片为logo图片
        image.image = [UIImage imageNamed:[self.models[i - 2] logoName]];
        //开启可交互模式
        [image setUserInteractionEnabled:YES];

        image.tag = i - 2;
        view.tag = i -2;


        //最后一个"自定义"按钮添加特定触摸手势
        if (i == self.models.count + 1)
        {
            UITapGestureRecognizer *tapAddModel = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                                          action:@selector(tapToAddModel:)];
            [image addGestureRecognizer:tapAddModel];
        }
        //别的模型添加点击手势和向上滑动删除手势
        else
        {
            //添加点击手势
            UITapGestureRecognizer *tapEditModel = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                                           action:@selector(tapToEditModel:)];
            [image addGestureRecognizer:tapEditModel];

            //添加滑动手势
            UISwipeGestureRecognizer *swipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self
                                                                                               action:@selector(swipeToDeleteModel:)];
            //设置滑动方向为向上
            [swipeGesture setDirection:UISwipeGestureRecognizerDirectionUp];
            [image addGestureRecognizer:swipeGesture];
        }

        [view addSubview:image];
        //记录下对应的cell视图
        [self.cellView addObject:view];
        [self.scrollView addSubview:view];
    }

    //添加两个空白的块
    for (long i = self.models.count + 2; i < self.models.count + 4; i++)
    {
        UIView *view = [self createEmptyCell:CGRectMake(self.cellWidth * i, 0, self.cellWidth, self.cellHeight)];
        [self.scrollView addSubview:view];
    }

    //设置默认居中为第三个模型
    [self.scrollView setContentOffset:CGPointMake(self.cellWidth * 2, 0) animated:YES];
    self.cellIndex = 2;
    //设置背景颜色和文字
    [self updateCellBackground:(int)self.cellIndex];
}

//创建空白cell视图
- (UIView *)createEmptyCell:(CGRect)frame
{
    UIView *view = [[UIView alloc] initWithFrame:frame];
    //设置背景透明
    view.backgroundColor = [UIColor clearColor];
    return view;
}

此时我们应该会得到这样一个界面了。

实现UIScrollViewDelegate

  我们的很多滚动动画效果都是基于UIScrollViewDelegate中的回调方法的。接下来我们就看看如何实现这些效果。

首先我们看看官方API中UIScrollView有哪些协议方法。

我们这里用的主要是这几个方法。

  1. (void)scrollViewDidScroll:(UIScrollView *)scrollView
  2. (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  3. (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
  4. (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView

接下来我们详细看一下每个方法的实现。

(void)scrollViewDidScroll:(UIScrollView *)scrollView

//滑动过程中回调的函数,无论是手动滑动的,还是代码动画滑动都会回调该方法
//在这里计算那个cell是可见的,然后计算缩放比例,进行动画缩放
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //处理每一个cell,计算它的缩放比例
    for (int i = 0; i < self.models.count; i++)
    {
        //cell左侧x位置
        float lead = self.cellWidth * (i + 2);
        //cell右侧x位置
        float tail = self.cellWidth * (i + 3);

        float rate = SCALE_RATE;

        //cell在屏幕左,右侧,不可见,设置为默认缩放比例0.6
        if (self.scrollView.contentOffset.x >= tail || (self.scrollView.contentOffset.x + UISCREEN_WIDTH) <= lead)
        {
            //暂时啥都不干
        }
        //cell在屏幕上
        else
        {
            float sub = lead - self.scrollView.contentOffset.x;
            //前半部分
            if (sub <= 2 * self.cellWidth)
            {
                rate = sub / (2 * self.cellWidth) * SCALE_RATE + SCALE_RATE;
            }
            else
            {
                rate = (UISCREEN_WIDTH - sub - self.cellWidth) / (2 * self.cellWidth) * SCALE_RATE + SCALE_RATE;
            }
        }
        //缩放该cell的视图
        [self viewToScale:rate target:self.cellView[i]];
    }
}

(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

//scrollView 拖拽操作结束   
//判断接下来是否会进行减速操作,如果不需要减速则在这里进行计算,得出当前那个cell最靠近中间位置,并把该cell滑动到居中的位置
//否则,不做任何处理。其实则就是要进行减速,减速完毕会回调scrollViewDidEndDecelerating。
//综上,都会计算需要居中哪个cell
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (!decelerate)
    {
        [self cellJumpToIndex:scrollView];
    }
}

(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

//scrollView 滑动过程减速完毕后回调的方法
//在这里进行计算,得出当前那个cell最靠近中间位置,并把该cell滑动到居中的位置
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self cellJumpToIndex:scrollView];
}

(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView

//滑动动画结束时调用的函数
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    //根据居中的选项更新背景和文字
    [self updateCellBackground:(int)self.cellIndex];
    [self.scrollView setUserInteractionEnabled:YES];
}

我们要实现自动滑动居中的效果,这里有一个很关键的方法。该方法用于计算当前最靠近居中位置的是哪一个cell子视图。我们首先计算当前ScrollView的contentOffset.x的位置,从而得知当前显示的cell有哪些,然后计算出处于中间位置的cell的索引下标,再计算出该cell居中时的contentOffset.x位置,再进行动画移动到该位置即可。

- (void)cellJumpToIndex:(UIScrollView *)scrollView
{
    if (self.scrollView.contentOffset.x < self.cellWidth * 0.5)
    {
        [self.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
    }
    else if (self.scrollView.contentOffset.x > self.cellWidth * (self.models.count + 1.5))
    {
        [self.scrollView setContentOffset:CGPointMake(self.cellWidth * (self.models.count + 1), 0) animated:YES];
    }

    int index = (int)(self.scrollView.contentOffset.x / self.cellWidth + 0.5);
    [self.scrollView setContentOffset:CGPointMake(self.cellWidth * index, 0) animated:YES];

    //选定某个模式,进行模式更新等操作
    self.cellIndex = index;
}

最后就是一个是进行缩放的方法,还有一个更新cell对应的视图的方法。

//按比例缩放视图
- (void)viewToScale:(float)scale target:(UIView *)view
{
    UIImageView *image = [[view subviews] lastObject];
    [UIView beginAnimations:@"scale" context:nil];
    image.transform = CGAffineTransformMakeScale(scale, scale);
    [UIView commitAnimations];
}

//滑动到某个cell时更新视图的方法
- (void)updateCellBackground:(int)index
{
    self.name.text = [self.models[index] name];
}

此时我们基本可以实现一开始希望得到的滚动缩放效果了。接下来我们的任务就是实现动态的cell增加和删除等功能。

期待ing…

个人博客

林友松。一个逗比的开发者。
Email:lysongzi.hnu@gmail.com
博客地址:lysongzi.com