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

相关推荐
ssxueyi11 分钟前
Git 完整安装与环境配置教程(Windows/macOS/Linux 通用)
windows·git·macos·项目管理·git教程·代码管理
2501_915918413 小时前
Wireshark、Fiddler、Charles抓包工具详细使用指南
android·ios·小程序·https·uni-app·iphone·webview
SoraLuna3 小时前
KuiklyUI for OpenHarmony 实战 01:源码构建与运行(Mac)
macos·ui·鸿蒙
Wcowin4 小时前
为macOS Finder提供直观的剪切粘贴体验
macos
JERRY. LIU5 小时前
Mac 笔记本通用快捷键大全
linux·macos
all79807969712 小时前
黑苹果macos 15 Sequoia升级 macos 26.1 Tahoe小结
macos
唯一浩哥14 小时前
2026 年,macbook air 2015 升级注意事项
macos·macbook·macbook air 2015·升级硬盘
TheNextByte115 小时前
如何将文件从Android无线传输到 iPad
android·ios·ipad
2501_9151063217 小时前
如何在iPad上高效管理本地文件的完整指南
android·ios·小程序·uni-app·iphone·webview·ipad
2501_9151063218 小时前
iOS 成品包加固,在只有 IPA 的情况下,能做那些操作
android·ios·小程序·https·uni-app·iphone·webview