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

