Android开发:如何实现带指示点、聚焦按钮居中的Horizontal Scroll导航布局
Hey there! Let's build that horizontal navigation bar with centered selected items and indicator dots you need for your Android app. I'll walk you through a complete, customizable solution using either HorizontalScrollView (for small datasets) or RecyclerView (for larger, more efficient lists).
Option 1: Basic Implementation with HorizontalScrollView
This works great if you have a small number of navigation items.
Step 1: Layout XML
First, create the layout structure with a scroll container for buttons and a container for indicator dots:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingVertical="8dp"> <!-- Horizontal Scroll Container for Navigation Buttons --> <HorizontalScrollView android:id="@+id/navScrollView" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="none"> <LinearLayout android:id="@+id/navBtnContainer" android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal" android:gravity="center_vertical" android:paddingHorizontal="16dp"/> </HorizontalScrollView> <!-- Indicator Dots Container --> <LinearLayout android:id="@+id/indicatorContainer" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/navScrollView" android:orientation="horizontal" android:gravity="center" android:paddingTop="8dp"/> </RelativeLayout>
Step 2: Kotlin Logic
Add the code to populate buttons, handle clicks, center the selected item, and manage indicators:
class MainActivity : AppCompatActivity() { private lateinit var navScrollView: HorizontalScrollView private lateinit var navBtnContainer: LinearLayout private lateinit var indicatorContainer: LinearLayout // Sample navigation items private val navItems = listOf( "Home", "Discover", "Categories", "Favorites", "Profile", "Settings", "Help" ) private var currentSelectedPosition = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initViews() setupNavButtons() setupIndicators() selectNavItem(0) // Default to first item } private fun initViews() { navScrollView = findViewById(R.id.navScrollView) navBtnContainer = findViewById(R.id.navBtnContainer) indicatorContainer = findViewById(R.id.indicatorContainer) } private fun setupNavButtons() { navItems.forEachIndexed { index, title -> val navBtn = TextView(this).apply { text = title setTextColor(resources.getColor(R.color.nav_text_unselected, theme)) textSize = 16f setPadding(24, 12, 24, 12) layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ) setOnClickListener { selectNavItem(index) } } navBtnContainer.addView(navBtn) } } private fun setupIndicators() { navItems.forEachIndexed { index, _ -> val dot = View(this).apply { layoutParams = LinearLayout.LayoutParams(12, 12).apply { marginHorizontal = 4.dpToPx() } background = if (index == currentSelectedPosition) { resources.getDrawable(R.drawable.dot_selected, theme) } else { resources.getDrawable(R.drawable.dot_unselected, theme) } } indicatorContainer.addView(dot) } } private fun selectNavItem(position: Int) { if (position == currentSelectedPosition) return // Update button styles val prevBtn = navBtnContainer.getChildAt(currentSelectedPosition) as TextView prevBtn.setTextColor(resources.getColor(R.color.nav_text_unselected, theme)) prevBtn.textSize = 16f val currentBtn = navBtnContainer.getChildAt(position) as TextView currentBtn.setTextColor(resources.getColor(R.color.nav_text_selected, theme)) currentBtn.textSize = 18f // Emphasize selected item // Update indicator dots val prevDot = indicatorContainer.getChildAt(currentSelectedPosition) prevDot.background = resources.getDrawable(R.drawable.dot_unselected, theme) val currentDot = indicatorContainer.getChildAt(position) currentDot.background = resources.getDrawable(R.drawable.dot_selected, theme) // Scroll to center the selected button centerSelectedItem(currentBtn) currentSelectedPosition = position } private fun centerSelectedItem(view: View) { val scrollViewWidth = navScrollView.width val viewWidth = view.width val viewLeft = view.left // Calculate scroll position to center the view val targetScrollX = viewLeft - (scrollViewWidth / 2) + (viewWidth / 2) navScrollView.smoothScrollTo(targetScrollX, 0) } // Helper to convert dp to pixels private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt() }
Step 3: Indicator Dot Drawables
Create two drawable files for selected/unselected dots:
dot_selected.xml:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@color/primary_color"/> <!-- Use your app's primary color --> </shape>
dot_unselected.xml:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@color/grey_400"/> <!-- Use a muted color for unselected --> </shape>
Option 2: Efficient Implementation with RecyclerView
For larger datasets, use RecyclerView with a horizontal layout manager for better performance and view reuse.
Step 1: Update Layout XML
Replace the HorizontalScrollView with a RecyclerView:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/navRecyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="none"/>
Step 2: Navigation Item Adapter
Create an adapter to handle navigation items and selection state:
class NavAdapter( private val items: List<String>, private val onItemClick: (Int) -> Unit ) : RecyclerView.Adapter<NavAdapter.NavViewHolder>() { private var selectedPosition = 0 inner class NavViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val titleTv: TextView = itemView.findViewById(R.id.navTitleTv) init { itemView.setOnClickListener { val pos = adapterPosition if (pos != RecyclerView.NO_POSITION) { selectedPosition = pos notifyDataSetChanged() onItemClick(pos) } } } fun bind(title: String) { titleTv.text = title val isSelected = adapterPosition == selectedPosition titleTv.setTextColor( if (isSelected) { itemView.context.resources.getColor(R.color.nav_text_selected, itemView.context.theme) } else { itemView.context.resources.getColor(R.color.nav_text_unselected, itemView.context.theme) } ) titleTv.textSize = if (isSelected) 18f else 16f } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NavViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.nav_item_layout, parent, false) return NavViewHolder(view) } override fun onBindViewHolder(holder: NavViewHolder, position: Int) { holder.bind(items[position]) } override fun getItemCount() = items.size fun setSelectedPosition(pos: Int) { selectedPosition = pos notifyDataSetChanged() } }
Step 3: Nav Item Layout (nav_item_layout.xml)
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/navTitleTv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingHorizontal="24dp" android:paddingVertical="12dp" android:textSize="16sp"/>
Step 4: Activity Logic for RecyclerView
Update your activity code to use the RecyclerView:
class MainActivity : AppCompatActivity() { private lateinit var navRecyclerView: RecyclerView private lateinit var navAdapter: NavAdapter private lateinit var indicatorContainer: LinearLayout private val navItems = listOf( "Home", "Discover", "Categories", "Favorites", "Profile", "Settings", "Help" ) private var currentSelectedPosition = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initViews() setupRecyclerView() setupIndicators() selectNavItem(0) } private fun initViews() { navRecyclerView = findViewById(R.id.navRecyclerView) indicatorContainer = findViewById(R.id.indicatorContainer) } private fun setupRecyclerView() { navAdapter = NavAdapter(navItems) { position -> selectNavItem(position) } navRecyclerView.apply { adapter = navAdapter layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false) // Add padding to first/last items addItemDecoration(object : RecyclerView.ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val pos = parent.getChildAdapterPosition(view) if (pos == 0) outRect.left = 16.dpToPx() if (pos == navItems.lastIndex) outRect.right = 16.dpToPx() } }) } } private fun selectNavItem(position: Int) { if (position == currentSelectedPosition) return navAdapter.setSelectedPosition(position) // Center the selected item val layoutManager = navRecyclerView.layoutManager as LinearLayoutManager val selectedView = navRecyclerView.getChildAt(position) selectedView?.let { val offset = (navRecyclerView.width / 2) - (it.width / 2) layoutManager.scrollToPositionWithOffset(position, offset) } // Update indicators (same as HorizontalScrollView version) val prevDot = indicatorContainer.getChildAt(currentSelectedPosition) prevDot.background = resources.getDrawable(R.drawable.dot_unselected, theme) val currentDot = indicatorContainer.getChildAt(position) currentDot.background = resources.getDrawable(R.drawable.dot_selected, theme) currentSelectedPosition = position } // Helper methods (setupIndicators and dpToPx same as before) private fun setupIndicators() { navItems.forEachIndexed { index, _ -> val dot = View(this).apply { layoutParams = LinearLayout.LayoutParams(12, 12).apply { marginHorizontal = 4.dpToPx() } background = if (index == currentSelectedPosition) { resources.getDrawable(R.drawable.dot_selected, theme) } else { resources.getDrawable(R.drawable.dot_unselected, theme) } } indicatorContainer.addView(dot) } } private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt() }
Key Features Explained
- Centered Selection: The
centerSelectedItem(orscrollToPositionWithOffsetfor RecyclerView) calculates the exact scroll position to align the selected button with the center of the screen. - Indicator Dots: Dynamic dots are created based on the number of navigation items, with visual feedback for the selected position.
- Visual Highlighting: Selected buttons get a larger text size and different color to stand out.
内容的提问来源于stack exchange,提问作者Android

