基于选中索引的UIBezierPath动画TabBar波形形状实现问题
解决方案
核心思路
- 动态计算Tab位置:不再硬编码固定比例,根据TabBar的item数量自动计算每个Tab的宽度和中心坐标,适配任意数量的Tab。
- 精准定位波峰:将波峰的控制点点直接对齐选中Tab的中心,确保波形和选中Tab完全对应。
- 添加平滑动画:通过
CABasicAnimation实现路径切换的过渡动画,提升交互体验。
修改后的完整代码
1. 自定义TabBar类修改
class CustomTabBar: UITabBar { private var shapeLayer: CAShapeLayer? private let waveHeight: CGFloat = 40.0 // 波峰/波谷的高度 private let extraTopOffset: CGFloat = -20.0 // TabBar顶部向外偏移量 override func layoutSubviews() { super.layoutSubviews() // 首次布局时初始化形状 if shapeLayer == nil { let initialPath = createPath(for: selectedItemIndex()) shapeLayer = CAShapeLayer() shapeLayer?.path = initialPath shapeLayer?.fillColor = UIColor.secondarySystemBackground.cgColor layer.insertSublayer(shapeLayer!, at: 0) } } // 获取当前选中的Tab索引 private func selectedItemIndex() -> Int { guard let items = items, let selectedItem = selectedItem else { return 0 } return items.firstIndex(of: selectedItem) ?? 0 } // 生成对应选中索引的路径 private func createPath(for selectedIndex: Int) -> CGPath { let path = UIBezierPath() let tabBarWidth = bounds.width guard let itemCount = items?.count, itemCount > 0 else { // 没有Tab时返回默认矩形路径 path.move(to: .zero) path.addLine(to: CGPoint(x: tabBarWidth, y: 0)) path.addLine(to: CGPoint(x: tabBarWidth, y: bounds.height)) path.addLine(to: CGPoint(x: 0, y: bounds.height)) path.close() return path.cgPath } let itemWidth = tabBarWidth / CGFloat(itemCount) // 选中Tab的中心X坐标 let selectedCenterX = (CGFloat(selectedIndex) + 0.5) * itemWidth // 起点:左上角偏移位置 path.move(to: CGPoint(x: 0, y: extraTopOffset)) // 第一段曲线:从左起点到选中Tab左侧的中间点,控制点点设为选中Tab中心上方(波峰) let leftMidPointX = selectedCenterX - itemWidth/2 path.addQuadCurve(to: CGPoint(x: leftMidPointX, y: extraTopOffset), controlPoint: CGPoint(x: selectedCenterX, y: extraTopOffset - waveHeight)) // 第二段曲线:从选中Tab左侧中间点到右终点,控制点点设为选中Tab中心上方(保持波峰) path.addQuadCurve(to: CGPoint(x: tabBarWidth, y: extraTopOffset), controlPoint: CGPoint(x: selectedCenterX, y: extraTopOffset - waveHeight)) // 绘制TabBar底部和左侧 path.addLine(to: CGPoint(x: tabBarWidth, y: bounds.height)) path.addLine(to: CGPoint(x: 0, y: bounds.height)) path.close() return path.cgPath } func updateShape(with selectedIndex: Int) { guard let shapeLayer = shapeLayer else { return } let newPath = createPath(for: selectedIndex) // 添加路径切换动画 let pathAnimation = CABasicAnimation(keyPath: "path") pathAnimation.fromValue = shapeLayer.path pathAnimation.toValue = newPath pathAnimation.duration = 0.3 // 动画时长 pathAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) shapeLayer.path = newPath shapeLayer.add(pathAnimation, forKey: "pathAnimation") } }
2. TabBarController调用代码
extension YourTabBarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { guard let tabBar = tabBarController.tabBar as? CustomTabBar, let index = tabBarController.viewControllers?.firstIndex(of: viewController) else { return } tabBar.updateShape(with: index) } }
关键改进点说明
- 动态适配Tab数量:通过
items?.count获取Tab总数,计算每个Tab的宽度itemWidth = tabBarWidth / CGFloat(itemCount),支持任意数量的Tab。 - 精准波峰定位:选中Tab的中心坐标
selectedCenterX直接作为贝塞尔曲线的控制点点X值,确保波峰严格对齐选中Tab正上方。 - 平滑动画过渡:使用
CABasicAnimation实现路径渐变动画,替换原生硬的图层切换效果。 - 鲁棒性处理:增加Tab数量为0时的默认路径处理,避免崩溃。
内容的提问来源于stack exchange,提问作者Mel
相关产品推荐
相关产品推荐

