使用贝塞尔曲线实现一个iOS时间轴

UI效果

实现的思路

就是通过贝塞尔曲线画出时间轴的圆环的路径,然后

使用CAShaper来渲染UI,再通过

animation.beginTime = [cilrclLayer convertTime:CACurrentMediaTime() fromLayer:nil] + circleTimeOffset 来设置每个圆环的动画开始时间,

,每个地方都是有两层layer的,一层是底部灰色样式的(即没有到达时候的颜色)一层是到达得阶段的颜色,

到达的layer在上面,如果要开启动画,我们就得先将

到达的layer隐藏掉,然后开始动画的时候,将对应的那一条展示,开启动画的时候将hidden置为NO,这时候,其实layer是展示不了的(不会整个展示),因为我们添加了strokeEnd的动画,他会随者动画的进行而逐渐展示,所以我们在动画代理方法animationDidStart中,将layer 设置为可见,(如果没有设置动画代理,也可以在添加动画的时候设置为可见)

代码

//
//  LBTimeView.m
//  LBTimeLine
//
//  Created by mac on 2024/6/9.
//

#import "LBTimeView.h"
#define ScreenWidth [UIScreen mainScreen].bounds.size.width
#define RGB(r, g, b)    [UIColor colorWithRed:(r)/255.f green:(g)/255.f blue:(b)/255.f alpha:1.f]
#define SizeScale (([UIScreen mainScreen].bounds.size.width > 320) ? [UIScreen mainScreen].bounds.size.width/320 : 1)

const float BETTWEEN_LABEL_OFFSET = 20;
const float LINE_WIDTH = 1.9;
const float CIRCLE_RADIUS = 3.7;
const float INITIAL_PROGRESS_CONTAINER_WIDTH = 20.0;
const float PROGRESS_VIEW_CONTAINER_LEFT = 51.0;
const float VIEW_WIDTH = 225.0;

@interface LBTimeView ()
{
    CGPoint lastpoint;
    NSMutableArray *layers;
    NSMutableArray *circleLayers;
    int layerCounter;
    int circleCounter;
    CGFloat timeOffset;
    CGFloat leftWidth;
    CGFloat rightWidth;
    CGFloat viewWidth;
}

@end

@implementation LBTimeView

-(id)initWithFrame:(CGRect)frame sum:(NSInteger)sum current:(NSInteger)current{
    
    self = [super initWithFrame:frame];
    if (self) {
        self.frame = frame;
        [self configureTimeLineWithNum:sum andCurrentNum:current];
    }
    return self;
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)configureTimeLineWithNum:(NSInteger)sum andCurrentNum:(NSInteger)currentStatus {
    // NSInteger  currentStatus = 3;
    
    circleLayers = [[NSMutableArray alloc] init];
    layers = [[NSMutableArray alloc] init];
    CGFloat U = (ScreenWidth - 80- sum+1)/(sum - 1);
    CGFloat betweenLineOffset = 0;
    //CGFloat totlaHeight = 8;
    
    // CGFloat yCenter = - 48 + (ScreenWidth - 248)/2;
    CGFloat yCenter = 40;
    // CGFloat xCenter;
    UIColor *strokeColor;
    CGPoint toPoint;
    CGPoint fromPoint;
    
    int i = 0;
    for (int j = 0;j < sum;j ++) {
        //configure circle
        strokeColor = i < currentStatus ? RGB(224, 0, 30) : RGB(233, 233, 233);
        UIBezierPath *circle = [UIBezierPath bezierPath];
        [self configureBezierCircle:circle withCenterY:yCenter];
        CAShapeLayer *circleLayer = [self getLayerWithCircle:circle andStrokeColor:strokeColor];
        //
        [circleLayers addObject:circleLayer];
        //add static background gray circle
        CAShapeLayer *grayStaticCircleLayer = [self getLayerWithCircle:circle andStrokeColor:RGB(233, 233, 233)];
        [self.layer addSublayer:grayStaticCircleLayer];
        [self.layer addSublayer:circleLayer];
        //configure line
        if (i > 0) {
            fromPoint = lastpoint;
            toPoint = CGPointMake(yCenter - CIRCLE_RADIUS,60*SizeScale);
            lastpoint = CGPointMake( yCenter + CIRCLE_RADIUS+ 1,60*SizeScale);
            
            UIBezierPath *line = [self getLineWithStartPoint:fromPoint endPoint:toPoint];
            CAShapeLayer *lineLayer = [self getLayerWithLine:line andStrokeColor:strokeColor];
            
            // CAShapeLayer *lineLayer2 = [self getLayerWithLine:line andStrokeColor:strokeColor];
            [layers addObject:lineLayer];
            //add static background gray line
            CAShapeLayer *grayStaticLineLayer = [self getLayerWithLine:line andStrokeColor:RGB(233, 233, 233)];
            
            [self.layer addSublayer:grayStaticLineLayer];
            [self.layer addSublayer:lineLayer];
        } else {
            lastpoint = CGPointMake( yCenter + CIRCLE_RADIUS+1,60*SizeScale);
        }
        betweenLineOffset = BETTWEEN_LABEL_OFFSET;
        yCenter += U;
        i++;
    }
}

- (CAShapeLayer *)getLayerWithLine:(UIBezierPath *)line andStrokeColor:(UIColor *)strokeColor {
    CAShapeLayer *lineLayer = [CAShapeLayer layer];
    lineLayer.path = line.CGPath;
    lineLayer.strokeColor = strokeColor.CGColor;
    lineLayer.fillColor = nil;
    lineLayer.lineWidth = 1.4;
    return lineLayer;
}

- (UIBezierPath *)getLineWithStartPoint:(CGPoint)start endPoint:(CGPoint)end {
    UIBezierPath *line = [UIBezierPath bezierPath];
    [line moveToPoint:start];
    [line addLineToPoint:end];
    
    return line;
}


- (CAShapeLayer *)getLayerWithCircle:(UIBezierPath *)circle andStrokeColor:(UIColor *)strokeColor {
    CAShapeLayer *circleLayer = [CAShapeLayer layer];
    circleLayer.path = circle.CGPath;
    circleLayer.strokeColor = strokeColor.CGColor;
    circleLayer.fillColor = nil;
    circleLayer.lineWidth = LINE_WIDTH;
    circleLayer.lineJoin = kCALineJoinBevel;
    return circleLayer;
}

- (void)configureBezierCircle:(UIBezierPath *)circle withCenterY:(CGFloat)centerY {
    [circle addArcWithCenter:CGPointMake( centerY,60*SizeScale)
                      radius:CIRCLE_RADIUS
                  startAngle:M_PI_2
                    endAngle:-M_PI_2
                   clockwise:YES];
    [circle addArcWithCenter:CGPointMake(centerY,60*SizeScale)
                      radius:CIRCLE_RADIUS
                  startAngle:-M_PI_2
                    endAngle:M_PI_2
                   clockwise:YES];
}

- (void)startAnimatingWithCurrentIndex:(NSInteger)index
{
    for (CAShapeLayer *layer in layers) {
        layer.hidden = YES;
    }
    [self startAnimatingLayers:circleLayers forStatus:index];
}

- (void)startAnimatingLayers:(NSArray *)layersToAnimate forStatus:(NSInteger)currentStatus {
    float circleTimeOffset = 1;
    circleCounter = 0;
    int i = 1;
    //add with animation
    for (CAShapeLayer *cilrclLayer in layersToAnimate) {
        [self.layer addSublayer:cilrclLayer];
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        animation.duration = 0.2;
        animation.beginTime = [cilrclLayer convertTime:CACurrentMediaTime() fromLayer:nil] + circleTimeOffset;
        animation.fromValue = [NSNumber numberWithFloat:0.0f];
        animation.toValue   = [NSNumber numberWithFloat:1.0f];
        animation.fillMode = kCAFillModeForwards;
        animation.delegate =(id <CAAnimationDelegate>) self;
        circleTimeOffset += .4;
        [cilrclLayer setHidden:YES];
        [cilrclLayer addAnimation:animation forKey:@"strokeCircleAnimation"];
        if (self.lastBlink && i == currentStatus && i != [layersToAnimate count]) {
            CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:@"strokeColor"];
            strokeAnim.fromValue         = (id) [UIColor orangeColor].CGColor;
            strokeAnim.toValue           = (id) [UIColor clearColor].CGColor;
            strokeAnim.duration          = 1.0;
            strokeAnim.repeatCount       = HUGE_VAL;
            strokeAnim.autoreverses      = NO;
            [cilrclLayer addAnimation:strokeAnim forKey:@"animateStrokeColor"];
        }
        i++;
    }
}

- (void)animationDidStart:(CAAnimation *)anim {
    if (circleCounter < circleLayers.count) {
        if (anim == [circleLayers[circleCounter] animationForKey:@"strokeCircleAnimation"]) {
            [circleLayers[circleCounter] setHidden:NO];
            circleCounter++;
        }
    }
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (layerCounter >= layers.count) {
        return;
    }
    CAShapeLayer *lineLayer = layers[layerCounter];
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    animation.duration = 0.200;
    
    animation.fromValue = [NSNumber numberWithFloat:0.0f];
    animation.toValue   = [NSNumber numberWithFloat:1.0f];
    animation.fillMode = kCAFillModeForwards;
    lineLayer.hidden = NO;
    [self.layer addSublayer:lineLayer];
    [lineLayer addAnimation:animation forKey:@"strokeEndAnimation"];
    layerCounter++;
}

@end

如果对您有帮助,欢迎给一个star
demo

相关推荐
SoraLuna9 小时前
「Mac玩转仓颉内测版1」入门篇1 - Cangjie环境的搭建
macos·cangjie
️ 邪神11 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】文本点击事件
flutter·ios·鸿蒙·reactnative·anroid
蚁景网络安全13 小时前
MacOS java多版本安装与管理-sdkman
java·macos·sdkman
️ 邪神13 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】文本Text显示
flutter·ios·鸿蒙·reactnative·anroid
袁代码13 小时前
Swift 开发教程系列 - 第11章:内存管理和 ARC(Automatic Reference Counting)
开发语言·ios·swift·ios开发
海绵不是宝宝81713 小时前
IOS开发之MapKit定位国内不准的问题
ios
SoraLuna15 小时前
「Mac玩转仓颉内测版5」入门篇5 - Cangjie控制结构(上)
算法·macos·cangjie
那就可爱多一点点16 小时前
如何处理 iOS 客户端内 Webview H5 中后台播放的音视频问题
ios·音视频
crasowas16 小时前
iOS问题记录 - 503 Service Temporarily Unavailable
ios·fastlane
货拉拉技术17 小时前
货拉拉是如何实现symbolic demangle?
ios·性能优化