仿照着做了一个时间轴的控件,类似萤石的效果,先上图
实现如下:
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);
}
此时刻度和时间文字都绘制出来了,至于中间的红色指示线、底部的黑色线,这个没什么好说的了,随便怎么实现都可以。
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);
这样就实现了捏合放大和缩小
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];