利用UIScrollView实现自定义的PickerView

​ 偶然的在街上(虎扑)闲逛的时候,然后上看到了一个长得很好看的时间选择器,然后想着我也能实现一个类似的,于是就有了这篇文章。
先上个效果吧:

复刻效果.gif

​ 习惯上,涉及到自定义UI的时候,我们先去看看系统所提供的UI库到底满不满足自定义编写的需要,但十有八九都是不满足的。这次做的是一个时间选择器,于是我去翻阅了UIDatePicker以及UIPickerView ,发现提供的借口不能满足实现这个UI,所以顺带看了一下UIScrollView 的接口,想起前一阵子遇到的自定义UICollectionView,尝试使用Delegate和DataSource的方式来实现这个功能。总的来说这里模仿UITableView的实现大于要实现一个选择器吧。

​ 出于模仿UITableView,先定义一下DataSource所需要的内容如下:

@protocol ECPickerViewDataSourse <NSObject>
///返回有多少个列内容
-(NSInteger)numberOfItemsInSection:(ECPickerView *)view;
///对应每一列的行数组
-(NSArray *)pickerView:(ECPickerView *)view withSection:(NSInteger)section;
///对应每一行所显示的UI内容
-(UIView *)pickerView:(ECPickerView *)view withSection:(NSInteger)section indexPath:(NSInteger)index;
///返回实现行高度
-(CGFloat)pickerView:(ECPickerView *)view setHightForCell:(NSInteger)section indexPath:(NSInteger)index;
@end

接着定义一下Delegate的返回内容:

@protocol ECPickerViewDelegate <NSObject>
///返回滑动时的每一个行的内容,对应返回的是一组数组
-(void)cpickerViewMoveToItem:(NSArray *)selectArray;
///返回滑动结束的时候每一个行的内容,对应返回的是一组数组
-(void)cpickerViewMoveToItemEndAnimation:(NSArray *)selectArray;
@end

那么我们这个UIView所包含的属性应该是这样的:

@protocol ECPickerViewDataSourse,ECPickerViewDelegate;
@interface ECPickerView : UIView
///DataSourse
@property(nonatomic,unsafe_unretained)IBOutlet __nullable id<ECPickerViewDataSourse> datasourse;
///Delegate
@property(nonatomic,unsafe_unretained)IBOutlet __nullable id<ECPickerViewDelegate> delegate;
///列总数
@property (nonatomic, readonly) NSInteger numberOfSections;
///由于这里自定义的是一个时间选择器,所以这里提供的是时间设置
-(void)scrollToDate:(NSDate *)date;
@end

那么接下来实现的首先是DataSource的处理咯,因为后续的初始化UI数据还是后来的刷新数据多是通过他来实现的,所以我们先重定义了DataSource的set方法和初始化UIView:

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initWithData];
        [self reloadData];
    }
    return self;
}

-(void)setDatasourse:(id<ECPickerViewDataSourse>)datasourse{
    if (_datasourse != datasourse) {
        _datasourse = datasourse;
        if (_datasourse) {
            [self reloadData];///刷新数据的显示
            [self moveToNowTime];///移动到指定默认的时间戳位置
        }
    }
}

移动到指定的时间戳位置选取的方法如下:

-(void)scrollToDate:(NSDate *)date{
    NSString *dateStr = [formatter stringFromDate:date];
    NSArray *newArr = [dateStr componentsSeparatedByString:@":"];
    for (int i = 0; i<newArr.count; i++) {
        int number = [newArr[i] intValue];
        UIScrollView *scrollview = scrViewArray[I];
        CGFloat hight = [_datasourse pickerView:self setHightForCell:i indexPath:0];
        [scrollview setContentOffset:CGPointMake(0, number*hight) animated:YES];
        [scrollview setBouncesZoom:NO];
    }
}

-(void)moveToNowTime{
    [self scrollToDate:[NSDate new]];
}

初始化用来记录数据的数组

-(void)initWithData{
    scrViewArray = [NSMutableArray new];
    numberArray = [NSMutableArray new];
    formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"HH:mm";
}

刷新数据,初始化数据的代码:

-(void)reloadData{
    _numberOfSections = [_datasourse numberOfItemsInSection:self];///通过反向代理的方式来获取到外面DataSource所设置的值
    CGFloat width = self.frame.size.width/_numberOfSections;
    CGFloat height = self.frame.size.height;
    for (NSMutableArray *item in numberArray) {
        for (UIView *view in item) {
            [view removeFromSuperview];
        }
    }
    [numberArray removeAllObjects];
    
    for (UIScrollView *view in scrViewArray) {
        [view removeFromSuperview];
    }
    [scrViewArray removeAllObjects];
    for (int i = 0; i<self.numberOfSections; i++) {
        NSMutableArray *viewItemArr = [NSMutableArray new];
        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(i*width, 0, width, height)];
        scrollView.tag = I;
        scrollView.delegate = self;
        scrollView.pagingEnabled = NO;//分页滚动
        [scrollView setBounces:YES];//到了边缘以后不回弹
        scrollView.showsVerticalScrollIndicator = NO;
        scrollView.showsHorizontalScrollIndicator = NO;
        
        NSArray *itemArray = [_datasourse pickerView:self withSection:i];
        CGFloat countHight = 0;
        CGFloat itemHight1 = 0;
        for (int k = 0; k<itemArray.count; k++) {
            CGFloat itemHight = [_datasourse pickerView:self setHightForCell:i indexPath:k];
            itemHight1 = itemHight;
            countHight+=itemHight;
            UIView *view = [_datasourse pickerView:self withSection:i indexPath:k];
            view.frame = CGRectMake(0, k*itemHight+height/2-itemHight/2, width, itemHight);
            [viewItemArr addObject:view];
            [scrollView addSubview:view];
        }
        scrollView.contentSize = CGSizeMake(width, countHight+height-itemHight1);
        
        [self addSubview:scrollView];
        
        UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(i*width+width/2-(25/2), height/2-24, 25, 2)];
        view1.backgroundColor = [UIColor colorWithRed:128/255.0 green:91.0/255.0 blue:235.0/255.0 alpha:1.0];
        [self addSubview:view1];
        
        UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(i*width+width/2-(25/2), height/2+22, 25, 2)];
        view2.backgroundColor = [UIColor colorWithRed:128/255.0 green:91.0/255.0 blue:235.0/255.0 alpha:1.0];
        [self addSubview:view2];
        
        [numberArray addObject:viewItemArr];
        [scrViewArray addObject:scrollView];
    }
    CGFloat itemHight0 = [_datasourse pickerView:self setHightForCell:0 indexPath:0];
    UILabel *labCenter = [[UILabel alloc] initWithFrame:CGRectMake(self.frame.size.width/2-5, height/2-itemHight0/2-5, 10, itemHight0)];
    labCenter.font = [UIFont systemFontOfSize:40];
    labCenter.textColor = [UIColor blackColor];
    labCenter.text = @":";
    [self addSubview:labCenter];
    
    [self setNeedsDisplay];
}

为了让整个PickerView在用户操作的时候看起来更容易选择和更像系统所自带的UIDatePicker,所以这里需要利用好UIScrollView的代理,主要是滑动的起止以及惯性滑动这些。


-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    CGFloat hight = [_datasourse pickerView:self setHightForCell:scrollView.tag indexPath:0];
    if ((int)scrollView.contentOffset.y%(int)hight != 0) {
        int k = scrollView.contentOffset.y/hight;
        float t = scrollView.contentOffset.y/hight;
        NSString *f1 = [NSString stringWithFormat:@"%.4f",t];
        NSArray *tmpA = [f1 componentsSeparatedByString:@"."];
        NSString *f2 = tmpA[1];
        float f2f = [f2 floatValue]/10000;
        if (f2f >= 0.5) {
            k+=1;
        }
        [scrollView setContentOffset:CGPointMake(0, k*hight) animated:YES];
        scrollView.bouncesZoom = NO;
        [self labTextSet:scrollView];
    }
    [self shaker:scrollView withShake:YES];
    if ([_delegate respondsToSelector:@selector(cpickerViewMoveToItemEndAnimation:)]) {
        [_delegate cpickerViewMoveToItemEndAnimation:[self didSelectWithScroller]];
    }
    
}

-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    
}

-(void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView{
    
}

-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
    [self labTextSet:scrollView];
    if ([_delegate respondsToSelector:@selector(cpickerViewMoveToItem:)]) {
          [_delegate cpickerViewMoveToItem:[self didSelectWithScroller]];
      }
}
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    
    CGFloat hight = [_datasourse pickerView:self setHightForCell:scrollView.tag indexPath:0];
    if ((int)scrollView.contentOffset.y%(int)hight != 0) {
        int k = scrollView.contentOffset.y/hight;
        float t = scrollView.contentOffset.y/hight;
        NSString *f1 = [NSString stringWithFormat:@"%.4f",t];
        NSArray *tmpA = [f1 componentsSeparatedByString:@"."];
        NSString *f2 = tmpA[1];
        float f2f = [f2 floatValue]/10000;
        if (f2f >= 0.5) {
            k+=1;
        }
        [scrollView setContentOffset:CGPointMake(0, k*hight) animated:YES];
        scrollView.bouncesZoom = NO;
        
        [self labTextSet:scrollView];
    }
    
    [self shaker:scrollView withShake:YES];
    if ([_delegate respondsToSelector:@selector(cpickerViewMoveToItemEndAnimation:)]) {
        [_delegate cpickerViewMoveToItemEndAnimation:[self didSelectWithScroller]];
    }
    
}


-(void)labTextSet:(UIScrollView *)scrollView{
    NSArray *labArray = numberArray[scrollView.tag];
    CGFloat hight = [_datasourse pickerView:self setHightForCell:scrollView.tag indexPath:0];
    int k = scrollView.contentOffset.y/hight;
    float t = scrollView.contentOffset.y/hight;
    NSString *f1 = [NSString stringWithFormat:@"%.4f",t];
    NSArray *tmpA = [f1 componentsSeparatedByString:@"."];
    NSString *f2 = tmpA[1];
    float f2f = [f2 floatValue]/10000;
    if (f2f >= 0.5) {
        k+=1;
    }
    if (k>-1 && k<labArray.count) {
        for (int i = 0; i<labArray.count; i++) {
            if (i==k) {
                UILabel *lab = labArray[k];
                lab.font = [UIFont systemFontOfSize:40.0];
            }else{
                UILabel *lab = labArray[I];
                lab.font = [UIFont systemFontOfSize:25.0];
            }
        }
    }
}

-(void)shaker:(UIScrollView *)scrollView withShake:(BOOL)shake{
    CGFloat hight = [_datasourse pickerView:self setHightForCell:scrollView.tag indexPath:0];
    int k = scrollView.contentOffset.y/hight;
    if (oldIndex != k) {
        oldIndex = k;
        AudioServicesPlaySystemSoundWithCompletion(1157, nil);
        if (shake) {
            if (@available(iOS 10.0, *)) {
                UIImpactFeedbackGenerator *impactFeedBack = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy];
                [impactFeedBack prepare];
                [impactFeedBack impactOccurred];
            }
        }
        
    }
}



-(NSArray *)didSelectWithScroller{
    NSMutableArray *newItem = [NSMutableArray new];
    for (int i = 0;i<scrViewArray.count;i++){
        UIScrollView *scrollView = scrViewArray[I];
        NSArray *labArray = numberArray[scrollView.tag];
        CGFloat hight = [_datasourse pickerView:self setHightForCell:scrollView.tag indexPath:0];
        int k = scrollView.contentOffset.y/hight;
        if (k>-1 && k<labArray.count) {
            UILabel *lab = labArray[k];
            [newItem addObject:lab.text];
        }
    }
    return newItem;
}

为了让他看起来更像是渐入渐出的效果,于是我这里给他添加了遮罩,通过这样的方式来模仿UIDatePicker。

#pragma mark /// draw
-(void)drawRect:(CGRect)rect{
    CAGradientLayer *gradient = [CAGradientLayer layer];
    gradient.frame = self.bounds;
    gradient.colors = [NSArray arrayWithObjects:
                       (id)[UIColor colorWithRed:1 green:1 blue:1 alpha:1.0].CGColor,
                       (id)[UIColor colorWithRed:1 green:1 blue:1.0 alpha:0.0].CGColor,
                       (id)[UIColor colorWithRed:1 green:1 blue:1 alpha:1.0].CGColor, nil];
    [self.layer addSublayer:gradient];
}

@end

最后,调用起来也是参考着UITableView的实现,更贴近系统的思想:

//
//  ViewController.m
//  PickerView
//
//  Created by JLee on 2020/7/14.
//  Copyright © 2020 Eziochan. All rights reserved.
//

#import "ViewController.h"
#import "ECPickerView.h"


@interface ViewController ()<ECPickerViewDataSourse,ECPickerViewDelegate>{
    ECPickerView *pickerView;
    NSArray *array1;
    NSArray *array2;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSMutableArray *ar = [NSMutableArray new];
     NSMutableArray *ar1 = [NSMutableArray new];
     for (int i = 0; i<60; i++) {
         NSString *str;
         if (i<10) {
             str = [NSString stringWithFormat:@"0%d",I];
         }else{
             str = [NSString stringWithFormat:@"%d",I];
         }
         [ar addObject:str];
         if (i<24) {
             [ar1 addObject:str];
         }
     }
     array1 = ar;
     array2 = ar1;
    
    
    pickerView = [[ECPickerView alloc] initWithFrame:CGRectMake(self.view.frame.size.width/4, 100, self.view.frame.size.width/2, 200)];
    pickerView.datasourse = self;
    pickerView.delegate = self;
    [self.view addSubview:pickerView];
}

-(NSInteger)numberOfItemsInSection:(ECPickerView *)view{
    return 2;
}
-(NSArray *)pickerView:(ECPickerView *)view withSection:(NSInteger)section{
    if (section == 0) {
        return array2;
    }else{
        return array1;
    }
}
-(UIView *)pickerView:(ECPickerView *)view withSection:(NSInteger)section indexPath:(NSInteger)index{
        NSString *str1;
      if (section == 0) {
          str1 = array2[index];
      }else{
          str1 = array1[index];
      }
      CGFloat width = view.frame.size.width/2;
      UILabel *lab = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, width, 40)];
      lab.text = str1;
      lab.font = [UIFont systemFontOfSize:25];
      lab.textColor = [UIColor blackColor];
      lab.textAlignment = NSTextAlignmentCenter;
      return lab;
}

-(CGFloat)pickerView:(ECPickerView *)view setHightForCell:(NSInteger)section indexPath:(NSInteger)index{
    return 40;
}


-(void)cpickerViewMoveToItem:(NSArray *)selectArray{
    NSLog(@"%s selectArray:%@ ",__func__,selectArray);
}
-(void)cpickerViewMoveToItemEndAnimation:(NSArray *)selectArray{
    NSLog(@"%s selectArray:%@ ",__func__,selectArray);
}

@end

写在最后:

相对系统的UIPickerView,这里还差一个滚轮式淡出的效果,这里的遮罩效果只是个赝品,写在这里也是想着能抛转引玉,希望能引来其它同行的指导意见,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345