Swift5+UIKit实现2-3个UIView间连线绘制、动画及分支功能咨询
iOS 容器视图父子控件连线动画实现方案(基于UIBezierPath+CAShapeLayer)
核心实现逻辑
- 所有坐标计算统一基于容器视图的坐标系,避免坐标转换错误
- 连线图层固定添加到容器视图的layer最底层,避免遮挡子视图
- 每次子视图布局完成后重新计算路径,天然适配不同屏幕尺寸
步骤1:获取关键坐标点
在容器视图的layoutSubviews(自定义View场景)或ViewController的viewDidLayoutSubviews(AutoLayout场景)中获取坐标,所有坐标均为容器自身坐标系下的相对值:
// 筛选视图,默认第一个子视图为顶部视图,剩余为底部视图,可根据实际业务调整筛选规则 guard let topView = subviews.first else { return } let bottomViews = Array(subviews.dropFirst()) // 顶部起点:顶部视图底部中点 let startPoint = CGPoint(x: topView.frame.midX, y: topView.frame.maxY) // 分叉点Y坐标:取顶部视图底部和底部视图顶部的中间位置,自动适配不同屏幕高度 let forkY = startPoint.y + ((bottomViews.first?.frame.minY ?? 0) - startPoint.y)/2
// 筛选视图,默认第一个子视图为顶部视图,剩余为底部视图,可根据实际业务调整筛选规则 UIView *topView = self.subviews.firstObject; if (!topView) return; NSArray *bottomViews = [self.subviews subarrayWithRange:NSMakeRange(1, self.subviews.count-1)]; // 顶部起点:顶部视图底部中点 CGPoint startPoint = CGPointMake(CGRectGetMidX(topView.frame), CGRectGetMaxY(topView.frame)); // 分叉点Y坐标:取顶部视图底部和底部视图顶部的中间位置,自动适配不同屏幕高度 CGFloat forkY = startPoint.y + (CGRectGetMinY([bottomViews.firstObject frame]) - startPoint.y)/2;
步骤2:根据底部视图数量生成路径
分两种场景处理:
场景1:总共有2个视图(1个顶部+1个底部)
let path = UIBezierPath() path.move(to: startPoint) let endPoint = CGPoint(x: bottomViews[0].frame.midX, y: bottomViews[0].frame.minY) path.addLine(to: endPoint)
UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:startPoint]; CGPoint endPoint = CGPointMake(CGRectGetMidX(bottomViews[0].frame), CGRectGetMinY(bottomViews[0].frame)); [path addLineToPoint:endPoint];
场景2:总共有3个视图(1个顶部+2个底部)
let path = UIBezierPath() path.move(to: startPoint) // 先画到中间分叉点 let forkPoint = CGPoint(x: startPoint.x, y: forkY) path.addLine(to: forkPoint) // 分叉连接第一个底部视图 let endPoint1 = CGPoint(x: bottomViews[0].frame.midX, y: bottomViews[0].frame.minY) path.addLine(to: endPoint1) // 回到分叉点连接第二个底部视图 path.move(to: forkPoint) let endPoint2 = CGPoint(x: bottomViews[1].frame.midX, y: bottomViews[1].frame.minY) path.addLine(to: endPoint2)
UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:startPoint]; // 先画到中间分叉点 CGPoint forkPoint = CGPointMake(startPoint.x, forkY); [path addLineToPoint:forkPoint]; // 分叉连接第一个底部视图 CGPoint endPoint1 = CGPointMake(CGRectGetMidX(bottomViews[0].frame), CGRectGetMinY(bottomViews[0].frame)); [path addLineToPoint:endPoint1]; // 回到分叉点连接第二个底部视图 [path moveToPoint:forkPoint]; CGPoint endPoint2 = CGPointMake(CGRectGetMidX(bottomViews[1].frame), CGRectGetMinY(bottomViews[1].frame)); [path addLineToPoint:endPoint2];
步骤3:配置CAShapeLayer
提前声明全局CAShapeLayer属性避免重复创建:
lazy var connectLineLayer: CAShapeLayer = { let layer = CAShapeLayer() layer.strokeColor = UIColor.systemBlue.cgColor // 自定义连线颜色 layer.lineWidth = 2 // 自定义线宽 layer.fillColor = UIColor.clear.cgColor layer.lineCap = .round // 线头顶点圆角 return layer }() // 初始化时添加到容器layer最底层 layer.insertSublayer(connectLineLayer, at: 0) // 每次路径更新后赋值 connectLineLayer.path = path.cgPath
@property (nonatomic, strong) CAShapeLayer *connectLineLayer; // 初始化时配置 if (!_connectLineLayer) { _connectLineLayer = [CAShapeLayer layer]; _connectLineLayer.strokeColor = [UIColor systemBlueColor].CGColor; // 自定义连线颜色 _connectLineLayer.lineWidth = 2; // 自定义线宽 _connectLineLayer.fillColor = [UIColor clearColor].CGColor; _connectLineLayer.lineCap = kCALineCapRound; // 线头顶点圆角 [self.layer insertSublayer:_connectLineLayer atIndex:0]; // 添加到容器layer最底层 } // 每次路径更新后赋值 _connectLineLayer.path = path.CGPath;
步骤4:添加路径延伸动画
通过strokeEnd属性实现从无到有的延伸效果:
let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = 0.8 // 自定义动画时长 animation.fromValue = 0 animation.toValue = 1 animation.fillMode = .forwards animation.isRemovedOnCompletion = false connectLineLayer.add(animation, forKey: "lineStrokeAnimation")
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; animation.duration = 0.8; // 自定义动画时长 animation.fromValue = @(0); animation.toValue = @(1); animation.fillMode = kCAFillModeForwards; animation.removedOnCompletion = NO; [_connectLineLayer addAnimation:animation forKey:@"lineStrokeAnimation"];
适配注意事项
- 所有坐标均基于子视图的相对位置计算,没有写死固定数值,4.7英寸到6.2英寸设备只要子视图布局正确,连线会自动适配
- 子视图布局发生变化(比如屏幕旋转、动态调整子视图位置)时,需要重新调用路径生成代码更新
connectLineLayer.path,保证连线对齐 - 如果需要圆角折线效果,可将
addLineToPoint替换为addQuadCurveToPoint实现平滑过渡
内容的提问来源于stack exchange,提问作者rony_y
相关产品推荐
相关产品推荐

