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

Android开发:如何实现带指示点、聚焦按钮居中的Horizontal Scroll导航布局

Solution for Centered Horizontal Navigation with Indicator Dots

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 (or scrollToPositionWithOffset for 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:15:45