使用贝塞尔曲线实现一个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

相关推荐
DevLoom_11 分钟前
macOS 27 升级后 Time Capsule 无法备份?AFP 协议废弃后的完整解决方案与备份迁移教程
macos·时间机器·macos27·macos 27备份·macos实用技巧
andyweike44 分钟前
Swift
开发语言·ios·swift
金海星科技1 小时前
船舶交通管理中心的人工智能值守:从“听、说、读、写“看人机协同
人工智能·macos·xcode
茶底世界之下1 小时前
白杯偏蓝,色温调到哪里该停?Awaking 两版正式导出
ios
小相会自律4 小时前
全平台远程控制软件推荐2026:Win/Mac/Linux/手机/鸿蒙互通哪款好
linux·macos·智能手机·todesk·teamviewer·向日葵
IsITGirl4 小时前
VMware Fusion(Mac)Windows ARM 虚拟机 NAT (vmnet8) 报错解决记录
windows·macos·.net·桥接模式
2501_915921435 小时前
KXBuild CI 实操,Windows/Linux 服务器接入 Jenkins 打包 iOS IPA
服务器·windows·vscode·ios·ci/cd·个人开发·敏捷流程
狗凯之家源码网5 小时前
iOS C2 V2 与 Python FastAPI 全链路远控系统深度评测
python·ios·fastapi
看浪的路人5 小时前
第6讲:Streaming 优化——告别“转圈圈“的用户体验
ios·iphone·xcode
传奇开心果编程16 小时前
【Flutter踩坑排雷】第7课:巨型 build 函数坑|全部 UI 写在同一个 build,维护难、隐性异常
android·flutter·ios·移动开发·跨平台·dart·声明式ui