iOS自定義滑動、拖拽時(shí)間軸。仿螢石

仿照著做了一個(gè)時(shí)間軸的控件,類似螢石的效果,先上圖


時(shí)間軸效果.gif

實(shí)現(xiàn)如下:

1 - 整個(gè)控件是基于UIScrollView做的
2 - 初始化scrollView的時(shí)候,設(shè)置scrollView的contentSize為scrollView的2倍寬,高不變。
_scrollView.contentSize = CGSizeMake(2 * self.width, self.height);
3 - 接下來初始化contentView,我的代碼中contentView是用的UIImageView(只有能實(shí)現(xiàn),用UIView啥的都行),設(shè)置contentView的frame為scrollView的contentSize。
_contentView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 2 * self.width, self.height)];
        _contentView.userInteractionEnabled = YES; 
[self.scrollView addSubview:_contentView];

同時(shí)添加一個(gè)UIPinchGestureRecognizer手勢

UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchAction:)];
pinch.delegate = self;
[_contentView addGestureRecognizer:pinch];
4 - 初始化刻度,最開始的時(shí)候,我設(shè)置的時(shí)間刻度最小單位是10分鐘
//計(jì)算需要多個(gè)刻度線
#define kJCTimeLineMaxHour          24
//最小刻度為10min,也就是需要24*6個(gè)刻度
self.itemCount = 24 * 6;
//計(jì)算最小刻度寬,這里之所以減去self.width,是因?yàn)閏ontentview上時(shí)間刻度繪制區(qū)域是需要去掉頭部和尾部的空白區(qū)的,這個(gè)看控件的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;//時(shí)刻度
  }else if (i % (6/2) == 0){
    height = 15;//中等刻度
  }else{
    height = 10;//最小刻度
  }
  lineLayer.frame = CGRectMake(self.startX + itemWidth * I,
                               self.height - kJCTimeLineBottomSpace - height,
                               1,
                               height);
}

接下來是繪制刻度文字

//因?yàn)槌跏硷@示區(qū)域較小,這里暫且設(shè)置成每3小時(shí)繪制一次時(shí)間
//從00:00開始一直到24:00,一共需要繪制9個(gè)時(shí)間點(diǎn),即當(dāng)時(shí)間分別為00:00、03:00、06:00...時(shí)需要繪制文字
//下面計(jì)算在什么時(shí)候需要繪制
//計(jì)算方法為:時(shí)間范圍 / 最小刻度時(shí)間間隔
3小時(shí)*60分鐘 / 10分鐘 = 18格
//也就是每18格需要繪制一次
for (NSInteger i = 0; i < (self.itemCount+1); i++) {
        //繪制刻度線
        //...
        
        //繪制時(shí)間文字
        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;//寄宿圖的像素尺寸和視圖大小的比例,不設(shè)置為屏幕比例文字就會像素化
            [self.contentView.layer addSublayer:textLayer];
            
            textLayer.frame = CGRectMake((self.startX + itemWidth * i) - (stringWidth * 0.5),
                                         self.height - kJCTimeLineBottomSpace,
                                         stringWidth,
                                         kJCTimeLineBottomSpace);
        }
    }

繪制已有時(shí)間區(qū)

//增加一個(gè)public屬性,用于接受對外傳經(jīng)來的時(shí)間數(shù)組
/**
 需要繪制的已有的時(shí)間
 時(shí)間格式要求是xx:xx-xx:xx
 起點(diǎn)時(shí)間-終點(diǎn)時(shí)間
 */
@property (nonatomic, strong) NSArray <NSString *> *timePaintingArray;
@property (nonatomic, strong) UIColor *timePaintingColor;

//繪制已有時(shí)間區(qū)
for (NSInteger i = 0; i < self.timePaintingArray.count; i++) {
        NSString *timeRange = self.timePaintingArray[I];
        NSString *startTime = [timeRange componentsSeparatedByString:@"-"].firstObject;
        NSString *endTime = [timeRange componentsSeparatedByString:@"-"].lastObject;
        //將時(shí)間轉(zhuǎn)成對應(yīng)的坐標(biāo)點(diǎn)
        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);
    }

此時(shí)刻度和時(shí)間文字都繪制出來了,至于中間的紅色指示線、底部的黑色線,這個(gè)沒什么好說的了,隨便怎么實(shí)現(xiàn)都可以。


最小時(shí)間刻度為10分鐘效果.jpg
5 - 手勢捏合縮放

手勢捏合這里,主要是獲取到捏合縮放的系數(shù)后,直接改變contentView的frame,以及scrollView的contentSize

#pragma mark UIGestureRecognizerDelegate
// 允許多個(gè)手勢并發(fā)
- (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];
        //擴(kuò)大、縮小倍數(shù)
        CGRect frame = view.frame;
        frame.size.width = sender.scale * frame.size.width;
        if (frame.size.width <= 2*self.width) {
            //最小是2倍,和初始化的時(shí)候一樣
            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;
        //重新繪制刻度和時(shí)間文本等,最小刻度那些都要重新計(jì)算,具體代碼看Demo
        [self reloadTimeLine];
        self.scrollView.scrollEnabled = YES;
    }
}

在縮放改變contentView的frame記憶scrollView的contentSize時(shí),中間的紅色位置也需要一起改變

//保持中間紅線位置不變
    self.scrollView.contentOffset = CGPointMake(itemWidth * ((CGFloat)self.currentSec / (CGFloat)self.secUnit), 0);

這樣就實(shí)現(xiàn)了捏合放大和縮小


捏合放大縮小.gif
6 - 滾動獲取時(shí)間

這里稍微做點(diǎn)限制,在捏合手勢的時(shí)候,不獲取滾動時(shí)間,只有當(dāng)拖拽時(shí)候再去獲取滾動的時(shí)間,直接上代碼。

#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 - 優(yōu)化部分

主要是針對縮放時(shí),重新繪制刻度線和時(shí)間文本的優(yōu)化。
我將縮放分為了6個(gè)區(qū)間模式,最小的刻度單位是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;
    }

每次產(chǎn)生縮放,重新繪制的時(shí)候,都會判斷下是否模式改變了。
A - 對于刻度線和時(shí)間文本:
如果改變:

1 - 將已繪制添加上的刻度線和時(shí)間文字移除;
2 - 需要重新創(chuàng)建刻度線的CALAyer和文字的CATextLayer,重新計(jì)算相應(yīng)的frame和時(shí)間文字;
3 - 緩存時(shí)間文字

沒改變:

1 - 只需要重新計(jì)算下frame,改變frame就可以了

B - 對于已有的時(shí)間區(qū)

只需重新計(jì)算下frame就行了,在初始化的時(shí)候就已經(jīng)創(chuàng)建好了layer

上述優(yōu)化后,可以減少重新創(chuàng)建layer的開銷。
此時(shí),會發(fā)現(xiàn)在縮放重新繪制時(shí),CPU開銷還是比較高,特別是放到最大時(shí),因?yàn)槔L制的layer多。只有在繪制layer的時(shí)候?qū)ayer的隱式動畫關(guān)了就行。

[CATransaction begin];
//這里執(zhí)行的代碼是沒有隱式動畫的
[CATransaction setDisableActions:YES];
[CATransaction commit];

Demo下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容