You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于选中索引的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 13:01:11