iOS自定义滑动、拖拽时间轴。仿萤石

仿照着做了一个时间轴的控件,类似萤石的效果,先上图


时间轴效果.gif

实现如下:

1 - 整个控件是基于UIScrollView做的
2 - 初始化scrollView的时候,设置scrollView的contentSize为scrollView的2倍宽,高不变。
_scrollView.contentSize = CGSizeMake(2 * self.width, self.height);
3 - 接下来初始化contentView,我的代码中contentView是用的UIImageView(只有能实现,用UIView啥的都行),设置contentView的frame为scrollView的contentSize。
_contentView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 2 * self.width, self.height)];
        _contentView.userInteractionEnabled = YES; 
[self.scrollView addSubview:_contentView];

同时添加一个UIPinchGestureRecognizer手势

UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchAction:)];
pinch.delegate = self;
[_contentView addGestureRecognizer:pinch];
4 - 初始化刻度,最开始的时候,我设置的时间刻度最小单位是10分钟
//计算需要多个刻度线
#define kJCTimeLineMaxHour          24
//最小刻度为10min,也就是需要24*6个刻度
self.itemCount = 24 * 6;
//计算最小刻度宽,这里之所以减去self.width,是因为contentview上时间刻度绘制区域是需要去掉头部和尾部的空白区的,这个看控件的UI就能理解,不多累赘了
CGFloat itemWidth = (self.contentViewWidth - self.width) / self.itemCount;
//画刻度线
for (NSInteger i = 0; i < (self.itemCount+1); i++) {
  CALayer *lineLayer = [CALayer layer];
  lineLayer.backgroundColor = [self.timeLineDrawColor CGColor];
  [self.contentView.layer addSublayer:lineLayer];
        
  CGFloat height = 10;
  if (i % 6 == 0) {
    height = 25;//时刻度
  }else if (i % (6/2) == 0){
    height = 15;//中等刻度
  }else{
    height = 10;//最小刻度
  }
  lineLayer.frame = CGRectMake(self.startX + itemWidth * I,
                               self.height - kJCTimeLineBottomSpace - height,
                               1,
                               height);
}

接下来是绘制刻度文字

//因为初始显示区域较小,这里暂且设置成每3小时绘制一次时间
//从00:00开始一直到24:00,一共需要绘制9个时间点,即当时间分别为00:00、03:00、06:00...时需要绘制文字
//下面计算在什么时候需要绘制
//计算方法为:时间范围 / 最小刻度时间间隔
3小时*60分钟 / 10分钟 = 18格
//也就是每18格需要绘制一次
for (NSInteger i = 0; i < (self.itemCount+1); i++) {
        //绘制刻度线
        //...
        
        //绘制时间文字
        if (i % 18 == 0) {
            NSInteger sec = i * 600;
            CATextLayer *textLayer;
            NSInteger stringWidth = 0;
            
            NSString *string = [NSString stringWithFormat:@"%02ld:%02ld",(sec/3600),(sec%3600/60)];
            CGSize stringSize = [string boundingRectWithSize:CGSizeMake(30, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin) attributes:self.timeLineTextAttributes context:nil].size;
            stringWidth = stringSize.width;
            textLayer = [[CATextLayer alloc] init];
            textLayer.string = [[NSAttributedString alloc] initWithString:string
                                                               attributes:self.timeLineTextAttributes];
            textLayer.contentsScale = [UIScreen mainScreen].scale;//寄宿图的像素尺寸和视图大小的比例,不设置为屏幕比例文字就会像素化
            [self.contentView.layer addSublayer:textLayer];
            
            textLayer.frame = CGRectMake((self.startX + itemWidth * i) - (stringWidth * 0.5),
                                         self.height - kJCTimeLineBottomSpace,
                                         stringWidth,
                                         kJCTimeLineBottomSpace);
        }
    }

绘制已有时间区

//增加一个public属性,用于接受对外传经来的时间数组
/**
 需要绘制的已有的时间
 时间格式要求是xx:xx-xx:xx
 起点时间-终点时间
 */
@property (nonatomic, strong) NSArray <NSString *> *timePaintingArray;
@property (nonatomic, strong) UIColor *timePaintingColor;

//绘制已有时间区
for (NSInteger i = 0; i < self.timePaintingArray.count; i++) {
        NSString *timeRange = self.timePaintingArray[I];
        NSString *startTime = [timeRange componentsSeparatedByString:@"-"].firstObject;
        NSString *endTime = [timeRange componentsSeparatedByString:@"-"].lastObject;
        //将时间转成对应的坐标点
        NSInteger startHourSec = [startTime componentsSeparatedByString:@":"][0].integerValue * 3600;
        NSInteger startMinSec = [startTime componentsSeparatedByString:@":"][1].integerValue * 60;
        NSInteger startSec = [startTime componentsSeparatedByString:@":"][2].integerValue;
        startSec = startHourSec + startMinSec + startSec;
        
        NSInteger endHourSec = [endTime componentsSeparatedByString:@":"][0].integerValue * 3600;
        NSInteger endMinSec = [endTime componentsSeparatedByString:@":"][1].integerValue * 60;
        NSInteger endSec = [endTime componentsSeparatedByString:@":"][2].integerValue;
        endSec = endHourSec + endMinSec + endSec;
        
        CALayer *timelayer = [[CALayer alloc] init];
        timelayer.backgroundColor = [self.timePaintingColor CGColor];
        [self.contentView.layer addSublayer:timelayer];
        
        timelayer.frame = CGRectMake(self.startX + itemWidth * ((CGFloat)startSec / 600),
                                     0,
                                     (endSec - startSec) / (CGFloat)600 * itemWidth,
                                     2 * kJCTimeLineBottomSpace);
    }

此时刻度和时间文字都绘制出来了,至于中间的红色指示线、底部的黑色线,这个没什么好说的了,随便怎么实现都可以。


最小时间刻度为10分钟效果.jpg
5 - 手势捏合缩放

手势捏合这里,主要是获取到捏合缩放的系数后,直接改变contentView的frame,以及scrollView的contentSize

#pragma mark UIGestureRecognizerDelegate
// 允许多个手势并发
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

#pragma mark - PinchActionHandler
- (void)pinchAction:(UIPinchGestureRecognizer *) sender{
    if (sender.state == UIGestureRecognizerStateBegan ||
        sender.state == UIGestureRecognizerStateChanged){
        self.scrollView.scrollEnabled = NO;
        UIView *view = [sender view];
        //扩大、缩小倍数
        CGRect frame = view.frame;
        frame.size.width = sender.scale * frame.size.width;
        if (frame.size.width <= 2*self.width) {
            //最小是2倍,和初始化的时候一样
            frame.size.width = 2*self.width;
        }else if (frame.size.width >= 200*self.width){
            //最大限制是200倍宽
            frame.size.width = 200*self.width;
        }
        view.frame = frame;
        self.scrollView.contentSize = frame.size;
        self.contentViewWidth = frame.size.width;
        sender.scale = 1;
        //重新绘制刻度和时间文本等,最小刻度那些都要重新计算,具体代码看Demo
        [self reloadTimeLine];
        self.scrollView.scrollEnabled = YES;
    }
}

在缩放改变contentView的frame记忆scrollView的contentSize时,中间的红色位置也需要一起改变

//保持中间红线位置不变
    self.scrollView.contentOffset = CGPointMake(itemWidth * ((CGFloat)self.currentSec / (CGFloat)self.secUnit), 0);

这样就实现了捏合放大和缩小


捏合放大缩小.gif
6 - 滚动获取时间

这里稍微做点限制,在捏合手势的时候,不获取滚动时间,只有当拖拽时候再去获取滚动的时间,直接上代码。

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (self.isNeedScrollData) {
        self.currentSec = scrollView.contentOffset.x / (self.contentViewWidth - self.width) * 86400;
        if (self.currentSec <= 0) {
            self.currentSec = 0;
        }else if(self.currentSec >= 86400){
            self.currentSec = 86400;
        }
        self.timeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld:%02ld",self.currentSec/3600,self.currentSec%3600/60,self.currentSec%3600%60];
        if (self.delegate &&
            [self.delegate respondsToSelector:@selector(timeLine:scrollToTime:timeSecValue:)]) {
            [self.delegate timeLine:self scrollToTime:self.timeLabel.text timeSecValue:self.currentSec];
        }
    }
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    self.isNeedScrollData = YES;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    self.isNeedScrollData = decelerate;
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    self.isNeedScrollData = NO;
}
7 - 优化部分

主要是针对缩放时,重新绘制刻度线和时间文本的优化。
我将缩放分为了6个区间模式,最小的刻度单位是10分钟一格,最大的是15秒一格

if (self.contentViewWidth < self.width*4) {
        //每10分钟1格
        widthType = JCTimeLineWidthType10Min;
    }else if (self.contentViewWidth >= self.width * 4 &&
              self.contentViewWidth < self.width * 8){
        //每5分钟一格
        widthType = JCTimeLineWidthType5Min;
    }else if (self.contentViewWidth >= self.width * 8 &&
              self.contentViewWidth < self.width * 18){
        //每2分钟一格
        widthType = JCTimeLineWidthType2Min;
    }else if (self.contentViewWidth >= self.width * 18 &&
              self.contentViewWidth < self.width * 30){
        //每1分钟一格
        widthType = JCTimeLineWidthType1Min;
    }else if (self.contentViewWidth >= self.width * 30 &&
              self.contentViewWidth < self.width * 150){
        //每30秒一格
        widthType = JCTimeLineWidthType30Sec;
    }else{
        //每15秒一格
        widthType = JCTimeLineWidthType15Sec;
    }

每次产生缩放,重新绘制的时候,都会判断下是否模式改变了。
A - 对于刻度线和时间文本:
如果改变:

1 - 将已绘制添加上的刻度线和时间文字移除;
2 - 需要重新创建刻度线的CALAyer和文字的CATextLayer,重新计算相应的frame和时间文字;
3 - 缓存时间文字

没改变:

1 - 只需要重新计算下frame,改变frame就可以了

B - 对于已有的时间区

只需重新计算下frame就行了,在初始化的时候就已经创建好了layer

上述优化后,可以减少重新创建layer的开销。
此时,会发现在缩放重新绘制时,CPU开销还是比较高,特别是放到最大时,因为绘制的layer多。只有在绘制layer的时候将layer的隐式动画关了就行。

[CATransaction begin];
//这里执行的代码是没有隐式动画的
[CATransaction setDisableActions:YES];
[CATransaction commit];

Demo下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 81,885评论 1 180
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 28,826评论 1 144
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 33,671评论 0 105
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 18,390评论 0 90
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 23,693评论 0 149
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 19,563评论 1 90
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 12,252评论 2 165
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 11,638评论 0 80
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 10,162评论 5 114
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 13,398评论 0 130
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 12,140评论 1 129
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 13,004评论 0 134
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 7,777评论 0 18
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 10,579评论 2 119
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 13,771评论 3 129
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 9,366评论 0 3
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 9,671评论 0 80
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 14,343评论 2 138
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 14,808评论 2 135

推荐阅读更多精彩内容