搞 Android 开发的朋友十有八九都跟“顶部那条 bar”打过交道。动作栏ActionBar和工具栏Toolbar这两个名字经常混在一起出现尤其是新手刚打开 Android Studio 新建项目默认模板里明明写着 Toolbar网上一搜又全是 ActionBar谁能不懵这篇文章我不想讲太虚的概念就按我平时在项目里的真实使用流程来聊先从它俩的来龙去脉说起再给你一套可以直接抄的基础配置、菜单联动、样式定制最后把我在真机和模拟器上踩过的坑列一遍。想快速给页面加一个稳定、可定制、不闹脾气的顶部栏看完这篇基本就够了。我也见过不少老项目维护者代码里全是ActionBar突然换到Toolbar后各种怪问题。为什么怪因为很多人不了解两者在系统里的“待遇”完全不同。所以不管是新入门还是要做迁移先把概念理顺后面每一步都是顺水推舟。下面进入正题。1. 动作栏和工具栏两个名字四种关系1.1 ActionBar 的出身与局限ActionBar 是 Android 3.0API 11引入的“系统级”组件。那时候手机屏幕小、返回键是物理键顶部栏的主要职责就是显示应用标题和少量操作按钮所以 Google 把标题、图标、菜单、Tab 导航全塞进了 ActionBar省得每个 App 自己画一条不规范的顶栏。但问题也出在“系统级”上。ActionBar 不是一个普通 View它由 Window 直接管理位置固定、高度固定背景和文字颜色都得靠主题属性去绕想塞一个自定义搜索框进去要用ActionBar.LayoutParams这种别扭的 API。更要命的是国内很多定制 ROM 对 ActionBar 的渲染有差异同一个样式在不同手机上经常出现标题偏移、菜单失效。所以在我刚接手项目的那几年遇到 ActionBar 需求第一反应就是“能不碰就不碰”。1.2 Toolbar 的设计思路为什么更现代Toolbar 是 Android 5.0API 21跟随 Material Design 一起发布的组件但它的定位很聪明它压根不是“系统栏”而是一个普通的ViewGroup可以放在布局里的任何位置。你可以把它放顶部也可以放底部甚至可以嵌进折叠布局里做滚动动画。因为它是普通 View所以常规 View 能做的事它都能做改背景、改高度、加内边距、塞子 View、写点击事件全部直接操作。配合 AndroidX 里的 AppCompat 库Toolbar 还能兼容到很老的系统版本。我自己在低版本设备上实测过除了某些控件样式需要额外注意整体行为非常稳定。这也是后来所有 Material 组件比如AppBarLayout、CollapsingToolbarLayout都以 Toolbar 作为核心的原因。1.3 选型对比什么时候坚持用 ActionBar什么时候无脑上 Toolbar直接给结论新项目一律 Toolbar老项目如果只是简单维护、不便大改继续留着 ActionBar 也不是不能跑但只要是新写的页面强烈建议用 Toolbar。我在项目里判断依据通常是下面这张表维度ActionBarToolbar组件性质Window 管理的系统栏普通 ViewGroup摆放位置固定顶部任意位置定制能力较弱靠主题属性强直接设置布局属性折叠/隐藏动画不支持支持配合 AppBarLayout兼容性推荐用 AppCompat 包装AppCompat 自带兼容老系统学习成本低但上限低基础使用不复杂如果你已经在用CoordinatorLayout做页面联动或者想要沉浸式状态栏、banner 渐变折叠Toolbar 没有悬念。如果没有这些需求直接用 NoActionBar 主题 Toolbar也省得以后迁移时再翻旧账。2. 基础配置从依赖、主题到第一个可用的 Toolbar2.1 先把依赖和主题理顺否则后面全是坑现在用 Android Studio 新建工程默认依赖里一般已经有androidx.appcompat:appcompatToolbar 就包含在这个库里。如果你是自己拼的构建文件至少要有implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0依赖到位后最关键的步骤是改主题。Manifest 或themes.xml里Activity 的主题必须使用NoActionBar后缀比如style nameAppTheme parentTheme.AppCompat.Light.NoActionBar /style这里用了NoActionBar系统才不会自带一个 ActionBar。很多新手第一步就错在这自己明明在布局里加了 Toolbar主题却还是默认的带 ActionBar 主题结果界面上出现两条栏。后面我会再列这个问题但先记住Toolbar 和系统 ActionBar 不是叠加关系是替代关系。2.2 在布局文件里放一个标准 Toolbar布局里最基础的样子长这样LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical androidx.appcompat.widget.Toolbar android:idid/toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize android:background?attr/colorPrimary app:titleTextColorandroid:color/white app:subtitleTextColorandroid:color/white app:popupThemestyle/ThemeOverlay.AppCompat.Light app:themestyle/ThemeOverlay.AppCompat.Dark.ActionBar / /LinearLayout高度用?attr/actionBarSize而不是写死 48dp 或 56dp是因为不同设备、不同系统版本下系统推荐值可能不同。你写死一种尺寸很容易在平板上显得小气在带手势导航的设备上挤压内容区。app:theme控制 Toolbar 内部文字、图标的亮色还是暗色app:popupTheme控制溢出菜单弹窗的样式。这两个很容易混淆简单记theme管 bar 自己popupTheme管弹出的菜单。2.3 在 Activity 里绑定并设置基础属性Kotlin 绑定代码很简洁class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val toolbar findViewByIdandroidx.appcompat.widget.Toolbar(R.id.toolbar) setSupportActionBar(toolbar) supportActionBar?.title 主页 supportActionBar?.subtitle 今日进度70% supportActionBar?.setDisplayHomeAsUpEnabled(true) } }这里有一个非常关键的点setSupportActionBar(toolbar)之后Toolbar 才被“录入”系统的 ActionBar 管理机制后续的菜单回调、返回键回调才会走 Activity 的onCreateOptionsMenu/onOptionsItemSelected。如果你只是把 Toolbar 当普通 View 摆着不调用这一行那菜单和返回箭头都不会正常工作。supportActionBar是 Kotlin 里对getSupportActionBar()的扩展属性老代码里如果看到的是getSupportActionBar().setTitle(...)效果完全一样。3. 菜单和事件让 Toolbar 不只是装饰3.1 创建 menu 资源别漏了 app 命名空间菜单资源放在res/menu/目录比如menu_main.xmlmenu xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto item android:idid/action_search android:icondrawable/ic_search android:title搜索 app:showAsActionalways / item android:idid/action_settings android:icondrawable/ic_settings android:title设置 app:showAsActionnever / /menuapp:showAsAction决定这个菜单项是直接显示在 Toolbar 上还是折叠进右侧的“三个点”溢出菜单。取值有always、ifRoom、never还可以加withText让文字一起显示。我个人的习惯是常用操作给 1 个always图标其余全部never。一个页面上塞五六个图标在高分辨率屏幕上还好在全面屏小设备上很挤。特别注意app前缀不要写成android:showAsAction否则在低版本系统上可能不生效。这个坑我替你们踩过写反了真机跑起来一点反应没有。3.2 用 onCreateOptionsMenu 和 onOptionsItemSelected 接住事件既然调用了setSupportActionBar就用 Activity 的标准回调override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_search - { // 打开搜索 true } R.id.action_settings - { // 打开设置 true } else - super.onOptionsItemSelected(item) } }如果你没有把 Toolbar 交给setSupportActionBar管理而是把它当普通 View 用那就得用toolbar.setOnMenuItemClickListener处理菜单点击。两条线路不要混用否则会出现点击一次触发两次。菜单项显示后有时候需要根据页面状态动态地隐藏或置灰某个按钮。我一般会在onPrepareOptionsMenu里更新override fun onPrepareOptionsMenu(menu: Menu?) { val searchItem menu?.findItem(R.id.action_search) searchItem?.isVisible !isLoading searchItem?.isEnabled !isLoading super.onPrepareOptionsMenu(menu) }而且最好在数据状态变化后调用invalidateOptionsMenu()强制菜单重新 prepare不然你发现按钮明明改成了不可见界面却半天没反应。3.3 导航按钮、返回键与 DrawerLayout 的联系当setDisplayHomeAsUpEnabled(true)开启后Toolbar 最左边会显示返回箭头。这个返回箭头的点击事件在新版 AndroidX 里最省事的写法是toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }但如果你的页面用到了DrawerLayout我不建议在这手动调用返回。更标准的是配合ActionBarDrawerToggle让图标在菜单和返回之间自动切换val toggle ActionBarDrawerToggle( this, drawerLayout, toolbar, R.string.open_drawer, R.string.close_drawer ) drawerLayout.addDrawerListener(toggle) toggle.syncState()这样做的好处是打开侧边栏的时候图标自动变成关闭样式关闭时自动恢复。如果还叠加了返回键逻辑建议统一在onSupportNavigateUp()里写否则几个回调互相干扰状态很容易乱。3.4 标题、副标题和菜单的动态改法除了静态 XML 里的app:title代码里随时可以改toolbar.title 订单详情 toolbar.subtitle 订单号A123456注意如果调用了setSupportActionBar(toolbar)toolbar.title和supportActionBar?.title是同步的但supportActionBar?.title更“官方”。遇到 Fragment 切换时标题自动变成 Fragment 的标签这是 AppCompatActivity 的行为。如果你不想让 Fragment 里的setTitle覆盖你手动设置的标题建议在 Fragment 里显式调用(activity as AppCompatActivity).supportActionBar?.setTitle(...)。4. 视觉定制状态栏、主题与协调布局4.1 用主题统一所有页面的顶部栏外观直接在每个 Toolbar 上写android:backgroundcolor/xxx当然可以但一个项目几十个页面每个页面复制粘贴后面要改品牌色就会想骂人。更推荐的方式是在主题里统一style nameAppTheme parentTheme.AppCompat.Light.NoActionBar item namecolorPrimarycolor/brand_blue/item item namecolorPrimaryDarkcolor/brand_blue_dark/item item namecolorAccentcolor/colorAccent/item /stylecolorPrimary是 Material Design 里的顶栏背景色很多自定义主题组件都会读取它。所以只要布局里 Toolbar 的背景写的是?attr/colorPrimary换主题的时候所有页面一起变。Toolbar 上的文字颜色如果项目里大多数页面的顶栏都是深色背景、白色文字可以在themes.xml里加上item nametoolbarStylestyle/Widget.AppCompat.Toolbar/item item nameandroid:titleTextColorandroid:color/white/item但实操下来直接在 Toolbar 布局里写app:titleTextColor更直观少一层间接跳转。两者冲突时布局里的属性优先。4.2 沉浸式状态栏和刘海屏适配沉浸式状态栏是很多 App 的标配顶部背景色和 Toolbar 背景融为一体界面看起来更连贯。实现上最常用的方式是把状态栏设成透明或匹配顶栏背景色if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { window.statusBarColor ContextCompat.getColor(this, R.color.brand_blue) }如果想做成“延伸到状态栏后面”的效果就要用WindowCompat.setDecorFitsSystemWindows(window, false)再让布局根节点配合android:fitsSystemWindowstrue。这样系统会把状态栏高度当 padding 传给根布局Toolbar 就不会被摄像头挡住。刘海屏设备上如果发现 Toolbar 被刘海遮住优先检查根布局是否有fitsSystemWindows。如果是自己手动写 padding可以用ViewCompat.setOnApplyWindowInsetsListener动态获取状态栏高度ViewCompat.setOnApplyWindowInsetsListener(toolbar) { view, insets - view.updatePadding(top insets.systemWindowInsetTop) insets }这种方法比硬编码一个status_bar_height稳得多因为不同设备返回值不一样硬编码迟早要踩雷。4.3 在 AppBarLayout 里实现 banner 折叠效果“协调布局 banner”这个需求我经常在项目里遇到页面顶部是一张大图向下滚动时图片慢慢折叠成一条标准 Toolbar。实现它三件套CoordinatorLayoutAppBarLayoutCollapsingToolbarLayout。布局骨架androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_widthmatch_parent android:layout_heightmatch_parent com.google.android.material.appbar.AppBarLayout android:layout_widthmatch_parent android:layout_height200dp com.google.android.material.appbar.CollapsingToolbarLayout android:layout_widthmatch_parent android:layout_heightmatch_parent app:contentScrim?attr/colorPrimary app:layout_scrollFlagsscroll|exitUntilCollapsed ImageView android:layout_widthmatch_parent android:layout_heightmatch_parent android:scaleTypecenterCrop android:srcdrawable/banner app:layout_collapseModeparallax / androidx.appcompat.widget.Toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize app:layout_collapseModepin / /com.google.android.material.appbar.CollapsingToolbarLayout /com.google.android.material.appbar.AppBarLayout androidx.core.widget.NestedScrollView android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_behaviorstring/appbar_scrolling_view_behavior !-- 页面内容 -- /androidx.core.widget.NestedScrollView /androidx.coordinatorlayout.widget.CoordinatorLayout关键有两个layout_scrollFlags里的scroll|exitUntilCollapsed决定向上滚动时 AppBar 整体滚出屏幕最后留下 Toolbarlayout_collapseMode里 banner 用parallaxToolbar 用pin这样图片滚得比页面慢Toolbar 固定在顶部。这里有个隐藏坑CollapsingToolbarLayout自己有一套展开/折叠标题动画所以不要让 Toolbar 再用setSupportActionBar设置标题否则两张标题叠在一起。要么用collapsingToolbar.title要么用supportActionBar?.title二选一。5. 常见问题与避坑指南5.1 顶部出现了两个栏怎么都去不掉这是所有问题里最常被问的。现象布局里已经放了 Toolbar运行时屏幕顶部还是冒出一根系统标题栏。原因就是主题。检查AndroidManifest.xml里这个 Activity 的android:theme再看values/themes.xml和values-v21/themes.xml有没有被覆盖。我遇到过最离谱的情况是values-v21里写了一个带 ActionBar 的主题而主themes.xml没问题结果测试的旧手机兼容新手机全出了两栏。排查方法很简单全局搜索Theme.后缀里有没有ActionBar有就换成NoActionBar。5.2 Toolbar 标题不显示或者一直显示应用名标题不显示通常有三个原因一是在setContentView之前就调了setSupportActionBar(toolbar)和title此时布局还没测量完二是标题被 Fragment 或intent带来的android:label覆盖了三是用错了组件比如你用的是androidx.appcompat.widget.Toolbar但代码里 import 成了android.widget.Toolbar后者不支持 AppCompat 的标题管理。最稳的做法是在setSupportActionBar(toolbar)之后立刻supportActionBar?.title 我的标题如果页面里还有 Fragment 切换最好在 Fragment 的onResume里再设置一次避免返回栈切换后标题没刷新。5.3 溢出菜单三个点点了没反应或者菜单项不出现先说最常见的如果 Toolbar 没有调用setSupportActionBar(toolbar)onCreateOptionsMenu不会触发。其次如果你重写了onCreateOptionsMenu但忘了return true菜单也不会显示。第三菜单项showAsAction都设成always但图标没有设置或图标资源缺失看起来就像“点了个寂寞”。溢出菜单一点开就闪退多半是popupTheme和theme搭配出问题。我常用的方案是Toolbar 本身用暗色主题弹出菜单用亮色主题即app:themestyle/ThemeOverlay.AppCompat.Dark.ActionBar app:popupThemestyle/ThemeOverlay.AppCompat.Light这样菜单弹出后是白底黑字不会出现白底白字。5.4 Toolbar 被状态栏遮住或者布局整体下移很多做沉浸式状态栏的人都会遇到“Toolbar 跑到状态栏后面”。如果用的是CoordinatorLayout这个行为可能是预期内的因为 AppBarLayout 会自己处理系统栏。普通LinearLayout根节点下最简单的修复是给根布局加android:fitsSystemWindowstrue但要记住fitsSystemWindows是“从根布局内部消费系统窗口 insets”多个嵌套视图同时设置就会混乱。我一般只在最外层根布局设置一次。如果页面里还嵌了键盘弹起逻辑键盘导致的 padding 变化也要一并考虑必要时用专门处理WindowInsets的监听。5.5 老项目从 ActionBar 迁移到 Toolbar 时容易忽略的细节迁移不是把布局替换一下就行。我总结过一套常规动作先全局搜onCreateOptionsMenu、getActionBar()、android.app.ActionBar这些关键点把主题换成Theme.AppCompat.Light.NoActionBar或 Material 的 NoActionBar把原来ActionBar的自定义 View 换成在 Toolbar 里addView处理PopupMenu时用 Toolbar 的上下文而不是getActionBar().getThemedContext()。迁移完别忘了在真机上测这几件事横竖屏切换时标题是否丢失、抽屉图标是否正常、溢出菜单在深色皮肤下是否看得清。老项目往往藏着历史遗留代码搜索actionBar时顺便看看有没有反射调用这种问题最难查。最后再分享一个我在实际项目里养成的习惯我会在BaseActivity里封装一个统一的setupToolbar()包括设置标题、返回键、菜单点击回调甚至把状态栏颜色也一起管了。新页面只需要传标题文字和菜单资源 ID不用每个页面都重复写主题和导航逻辑。这样做以后我们项目里因为顶部栏导致的问题从一个月好几条崩溃和 UI 反馈降到了大半年才遇到一两次。Toolbar 的灵活是好事但灵活也意味着容易各自为政统一封装一下后面会省心很多。