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

Swift中Tab Bar顶部线条添加与平滑滑动切换动画实现问题

Hey there! Let's walk through how to build those two Tab Bar features you're referencing from the Amazon mobile app—super common patterns that make navigation feel polished. Let's break it down step by step.

1. Adding a Top Line to Each Tab Bar Item

First up, adding that subtle top line above every tab. The idea is to either customize your Tab Bar component directly or inject small line views above each tab item. Here's how to do it cleanly for both iOS and Android:

iOS (Swift)

Create a custom UITabBar subclass to handle drawing the lines automatically:

class CustomTabBar: UITabBar {
    private let defaultLineHeight: CGFloat = 1.0
    private let defaultLineColor = UIColor.lightGray
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // Remove old lines to avoid duplicates on layout updates
        subviews.filter { $0.tag == 100 }.forEach { $0.removeFromSuperview() }
        
        guard let itemCount = items?.count, itemCount > 0 else { return }
        let itemWidth = bounds.width / CGFloat(itemCount)
        
        for index in 0..<itemCount {
            let lineFrame = CGRect(
                x: CGFloat(index) * itemWidth,
                y: 0,
                width: itemWidth,
                height: defaultLineHeight
            )
            let lineView = UIView(frame: lineFrame)
            lineView.backgroundColor = defaultLineColor
            lineView.tag = 100
            addSubview(lineView)
        }
    }
}

Then just set your tab bar controller's tab bar to use this custom class (either in Storyboard or code).

Android (Kotlin)

For BottomNavigationView, extend the component to add lines above each item:

class CustomBottomNavView(context: Context, attrs: AttributeSet?) : BottomNavigationView(context, attrs) {
    private val defaultLineHeight = 1.dpToPx()
    private val defaultLineColor = ContextCompat.getColor(context, R.color.light_gray)

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        if (changed) {
            addDefaultTabLines()
        }
    }

    private fun addDefaultTabLines() {
        // Clear existing lines first
        removeDefaultLines()
        
        val itemCount = menu.size()
        val itemWidth = width / itemCount
        
        for (index in 0 until itemCount) {
            val lineView = View(context).apply {
                setBackgroundColor(defaultLineColor)
                tag = "default_tab_line"
            }
            addView(lineView)
            lineView.layoutParams = LayoutParams(itemWidth, defaultLineHeight).apply {
                topMargin = 0
                leftMargin = index * itemWidth
            }
        }
    }

    private fun removeDefaultLines() {
        for (i in 0 until childCount) {
            getChildAt(i)?.takeIf { it.tag == "default_tab_line" }?.let { removeView(it) }
        }
    }
    
    private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
}

2. Smooth Sliding Selected Line Effect

This is the more engaging part—making that blue line glide from the current tab to the selected one instead of jumping. The core idea is to have a separate, prominent line view that we animate to the target position when the tab selection changes.

iOS Implementation

Update the custom UITabBar to include a sliding selected line, plus add code to listen for tab selection changes:

class CustomTabBar: UITabBar {
    private let defaultLineHeight: CGFloat = 1.0
    private let selectedLineHeight: CGFloat = 2.0
    private let defaultLineColor = UIColor.lightGray
    private let selectedLineColor = UIColor.systemBlue
    
    private let selectedLineView = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupSelectedLine()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupSelectedLine()
    }
    
    private func setupSelectedLine() {
        selectedLineView.backgroundColor = selectedLineColor
        selectedLineView.tag = 200
        addSubview(selectedLineView)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // Update default lines
        subviews.filter { $0.tag == 100 }.forEach { $0.removeFromSuperview() }
        
        guard let itemCount = items?.count, itemCount > 0 else { return }
        let itemWidth = bounds.width / CGFloat(itemCount)
        
        // Redraw default lines
        for index in 0..<itemCount {
            let lineFrame = CGRect(
                x: CGFloat(index) * itemWidth,
                y: 0,
                width: itemWidth,
                height: defaultLineHeight
            )
            let lineView = UIView(frame: lineFrame)
            lineView.backgroundColor = defaultLineColor
            lineView.tag = 100
            addSubview(lineView)
        }
        
        // Set initial position if not already set
        if selectedLineView.frame.width == 0 {
            selectedLineView.frame = CGRect(x: 0, y: 0, width: itemWidth, height: selectedLineHeight)
        }
        
        // Ensure selected line stays on top
        bringSubviewToFront(selectedLineView)
    }
    
    // Call this method when the selected tab changes
    func slideSelectedLine(to index: Int) {
        guard let itemCount = items?.count, index >= 0 && index < itemCount else { return }
        let itemWidth = bounds.width / CGFloat(itemCount)
        let targetX = CGFloat(index) * itemWidth
        
        // Animate the slide
        UIView.animate(withDuration: 0.3) {
            self.selectedLineView.frame = CGRect(
                x: targetX,
                y: 0,
                width: itemWidth,
                height: self.selectedLineHeight
            )
        }
    }
}

Then in your UITabBarController, hook up the selection listener:

class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
        
        // Initialize line position to first tab
        if let customTabBar = tabBar as? CustomTabBar {
            customTabBar.slideSelectedLine(to: selectedIndex)
        }
    }
    
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
        guard let customTabBar = tabBarController.tabBar as? CustomTabBar else { return }
        customTabBar.slideSelectedLine(to: tabBarController.selectedIndex)
    }
}

Scenarios Covered:

  • First Tab Selected: When the app loads, the blue line starts exactly aligned with the first tab's top line—clean and intentional.
  • Switching to Second Tab: When you tap the second tab, the blue line animates smoothly from its starting position to the top of the second tab, with a natural 0.3-second duration that feels responsive.

Android Implementation

For Android, use a ValueAnimator to animate the line's position:

class CustomBottomNavView(context: Context, attrs: AttributeSet?) : BottomNavigationView(context, attrs) {
    private val defaultLineHeight = 1.dpToPx()
    private val selectedLineHeight = 2.dpToPx()
    private val defaultLineColor = ContextCompat.getColor(context, R.color.light_gray)
    private val selectedLineColor = ContextCompat.getColor(context, R.color.blue)
    
    private val selectedLineView = View(context)
    private var itemWidth: Int = 0

    init {
        setupSelectedLine()
        setOnItemSelectedListener { menuItem ->
            val index = menu.items.indexOf(menuItem)
            animateLineToIndex(index)
            true
        }
    }

    private fun setupSelectedLine() {
        selectedLineView.setBackgroundColor(selectedLineColor)
        addView(selectedLineView)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        if (changed) {
            itemWidth = width / menu.size()
            // Set initial position to first tab
            selectedLineView.layoutParams = LayoutParams(itemWidth, selectedLineHeight).apply {
                topMargin = 0
                leftMargin = 0
            }
            addDefaultTabLines()
            bringChildToFront(selectedLineView)
        }
    }

    private fun addDefaultTabLines() {
        removeDefaultLines()
        val itemCount = menu.size()
        
        for (index in 0 until itemCount) {
            val lineView = View(context).apply {
                setBackgroundColor(defaultLineColor)
                tag = "default_tab_line"
            }
            addView(lineView)
            lineView.layoutParams = LayoutParams(itemWidth, defaultLineHeight).apply {
                topMargin = 0
                leftMargin = index * itemWidth
            }
        }
    }

    private fun removeDefaultLines() {
        for (i in 0 until childCount) {
            getChildAt(i)?.takeIf { it.tag == "default_tab_line" }?.let { removeView(it) }
        }
    }

    private fun animateLineToIndex(index: Int) {
        val targetLeft = index * itemWidth
        val animator = ValueAnimator.ofInt(selectedLineView.left, targetLeft)
        animator.duration = 300
        animator.addUpdateListener { animation ->
            val newLeft = animation.animatedValue as Int
            selectedLineView.layoutParams = (selectedLineView.layoutParams as LayoutParams).apply {
                leftMargin = newLeft
            }
        }
        animator.start()
    }
    
    private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
}

This works exactly like the iOS version: initial alignment with the first tab, smooth sliding to any selected tab, including the second one.

内容的提问来源于stack exchange,提问作者sait cihaner

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 13:17:40