ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从ActionBar到Toolbar:操作栏原理、菜单集成与实战踩坑

从ActionBar到Toolbar:操作栏原理、菜单集成与实战踩坑 做Android开发谁没跟操作栏打过交道应用顶部那条放着标题、返回按钮和菜单入口的横条就是所谓的ActionBar或Toolbar。ActionBar是系统从Android 3.0开始内置的祖传动作栏Toolbar则是Material Design时代Google推出的自由版替代方案两者解决的是同一个问题给用户一个稳定的导航和操作入口。这篇内容不扯虚的直接讲清楚它们的核心区别、基本使用、菜单集成和真实踩坑记录适合刚入门的新手也适合想把老项目从ActionBar迁到Toolbar的老开发。你在Android Studio里敲完这几段代码就能明白三分真正把主题、菜单、返回键这几个环节串起来以后你才会理解这条横条到底是怎么工作的。1. 从ActionBar到Toolbar为什么必须掌握这两套体系1.1 ActionBar系统窗口装饰的一部分ActionBar在Android 3.0API 11被引入当时Android还在用物理菜单键应用的操作项藏在MENU键后面查找效率特别低。ActionBar的出现把导航图标、标题、菜单项直接放到了屏幕顶部成为系统级窗口装饰的一部分。它不是一个普通View而是由Window的DecorView承载的装饰元素。也就是说你只要在主题里声明item namewindowActionBartrue/item系统就会在窗口顶层画出一条操作栏Activity代码甚至不用写任何布局打开应用就能看到这条栏。但成也系统、败也系统。因为ActionBar被绑定在窗口装饰层开发者能控制的东西非常有限。背景颜色、高度、内边距能改但要操作主题属性而不是直接改一个View想把它放到屏幕中间或底部基本不可能想在它上面加一个子View参与动画联动也特别别扭。Google自己也清楚这套方案的局限所以在Android 5.0API 21推出独立的Toolbar控件并在AppCompat支持库里给旧系统提供了兼容方案。从兼容性角度说如果目标设备系统版本低于API 21原生Toolbar不可用只能依赖support库提供的近似版本。这也是为什么现在的Android项目大多统一用androidx.appcompat.widget.Toolbar——它把Toolbar的能力通过支持库带到了所有Android 4.4及以下的设备上做到真正的全覆盖。1.2 Toolbar被设计成普通View的操作栏Toolbar的本质是一个ViewGroupFrameLayout的子类它在布局文件里的角色和你平时写的TextView、LinearLayout没有本质区别。你可以把它放在布局树的任何位置给它任意设置背景、宽度、高度、margin、padding甚至用属性动画让它滑动、旋转、缩放。相比ActionBarToolbar有几个关键改进。第一它支持在XML里直接配置标题、副标题、logo、导航图标每一项都是Toolbar自己的属性不需要经过主题中转。第二菜单的弹出样式可以通过app:popupTheme单独控制不再强制跟随全局主题。第三它允许在同一条操作栏里放任意自定义子View比如搜索框、分段控件这些在ActionBar里很难做到在Toolbar里就是普通的addView操作。从项目选型角度看新项目建议直接以Toolbar为基准配合AppCompatActivity使用因为AppCompatActivity的supportActionBar本质上就是一个Toolbar的包装器setSupportActionBar(toolbar)之后你既能享受Toolbar的布局自由度又能沿用ActionBar的老接口控制标题、返回键和菜单。老项目如果还在用原生ActionBar迁到Toolbar的成本并不高新建布局、替换主题、改setContentView把onCreateOptionsMenu和onOptionsItemSelected的逻辑原样搬过来多花半天绝对能搞定。不过有一点要特别提醒如果主题里没有去掉windowActionBarToolbar和原生操作栏会同时出现屏幕上会叠着两条栏。所以用Toolbar时主题必须继承Theme.AppCompat.*NoActionBar或者手动把windowActionBar设为false。1.3 选型思路不是选择题是组合拳很多初学者把ActionBar和Toolbar当成两个互斥的选项其实更准确的说法是Toolbar是替代品ActionBar是兼容壳。官方推荐的现代写法是AppCompatActivity Theme.AppCompat.NoActionBar 布局里的Toolbar再用setSupportActionBar(toolbar)把Toolbar注册成伪ActionBar。这样代码里任何针对supportActionBar的调用设置标题、开关返回键、收起菜单都会映射到Toolbar上外部表现和ActionBar几乎一致。那么什么时候还需要原生ActionBar极少数情况比如你在做一个不依赖AppCompat的最小化SDK工具库targetSdk和minSdk都高代码量要求极低直接调windowActionBar也能用。但这类场景在实际业务项目中非常少见绝大多数团队最终都会走到Toolbar路线上。我见过不少项目组在维护遗留代码时为了省事把actionBar和toolbar混着用最后主题、图标、尺寸全乱成一锅粥所以选型这件事最好在项目第一天就定死。2. Toolbar基础使用从零开始搭一个能用的顶栏2.1 依赖和主题先扫清前置条件第一步是引入支持库。现在稳定版本都走AndroidX在模块级的build.gradle里加上dependencies { implementation androidx.appcompat:appcompat:1.6.1 }如果项目里用了Material Components库它本身已经传递依赖了appcompat不需要重复引入。但要注意Material库的主题继承体系和AppCompat有区别后面我会单独讲。第二步是配置主题。要让Toolbar作为唯一操作栏主题必须和NoActionBar家族挂钩。在res/values/themes.xml里把应用主题改成style nameTheme.MyApp parentTheme.AppCompat.Light.NoActionBar item namecolorPrimarycolor/colorPrimary/item item namecolorPrimaryDarkcolor/colorPrimaryDark/item item namecolorAccentcolor/colorAccent/item /style这里的关键点有两个。一是NoActionBar后缀没有它系统会在窗口顶层额外画一条操作栏二是colorPrimary等颜色属性的作用范围Toolbar默认不会自动读取colorPrimary作为背景所以通常我会在Toolbar的XML里手动设置android:background为?attr/colorPrimary或者用app:backgroundTint统一控制。如果你在AndroidManifest里指定的主题不是AppCompat的NoActionBar系列而是系统自带的android:Theme.Material.Light这类主题setSupportActionBar()就会抛异常因为AppCompatActivity要求主题必须基于AppCompat主题。这个坑很多新手会踩遇到时优先检查主题继承关系。2.2 布局和绑定三行代码接上操作栏完成主题配置后写布局。以一个典型页面为例LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:orientationvertical android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.appcompat.widget.Toolbar android:idid/toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize android:background?attr/colorPrimary android:elevation4dp app:title我的应用 app:titleTextColorandroid:color/white app:navigationIcondrawable/ic_arrow_back / FrameLayout android:idid/content_frame android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayout然后把Activity改成继承AppCompatActivity并在onCreate里做绑定class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val toolbar: Toolbar findViewById(R.id.toolbar) setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowTitleEnabled(true) supportActionBar?.title 首页 } }这一步的为什么我得说清楚。setSupportActionBar(toolbar)有两个作用一是让AppCompatActivity意识到操作栏由这个View承担二是把supportActionBar内部状态绑定到Toolbar上这样你之后调用的supportActionBar?.title 首页会直接修改Toolbar里的TextView文本。setDisplayHomeAsUpEnabled(true)则是打开左上角导航箭头。注意它只会绘制一个箭头图标并不会自动响应点击。返回逻辑需要你在onOptionsItemSelected里处理android.R.id.home或重写onSupportNavigateUp()。很多初学者以为开了就自动返回结果点了没反应原因就在这里。2.3 核心属性与它们背后的决策逻辑Toolbar的XML属性非常多我挑了日常最常用的一组列成表格对比说明属性作用说明app:title主标题文本默认显示在操作栏左侧app:subtitle副标题文本显示在主标题下方字号略小app:titleTextColor主标题颜色直接用color资源引用别硬编码十六进制值app:subtitleTextColor副标题颜色同理app:navigationIcon导航图标返回箭头、抽屉菜单图标等都从这里设置app:logologo图标设置了logo会显示在标题前面app:popupTheme菜单弹层主题控制更多菜单列表的外观风格app:menu直接关联menu资源不常用一般还是在onCreateOptionsMenu里inflateapp:titleMarginStart标题起始边距需要和logo间距统一时调整用它这里我想特别聊聊android:layout_height?attr/actionBarSize。?attr/actionBarSize是一个系统提供的高度参数默认是56dp手机竖屏时不同主题下会不同。直接引用这个值的好处是即使系统规范调整了操作栏高度你的Toolbar也会自动跟着变化不需要硬编码一个40dp或50dp。另一个容易被忽略的是app:popupTheme。当菜单项溢出到更多弹层里时弹层默认使用当前Activity的主题如果Activity是深色主题、弹层却用浅色列表视觉会很割裂。给Toolbar单独设置app:popupThemestyle/ThemeOverlay.AppCompat.Light可以把弹层固定成浅色风格避免被全局主题带偏。3. 菜单集成与事件交互让Toolbar真正能干事儿3.1 创建菜单资源showAsAction到底怎么写操作栏的作用不只是显示标题更重要的是承载操作入口。在Android里操作项以Menu资源的形式组织。先创建一个res/menu/main_menu.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:showAsActionifRoom / item android:idid/action_share android:title分享 app:showAsActionnever / /menushowAsAction的值决定了菜单项是直接显示在操作栏上还是折叠进溢出菜单。常用取值有always始终直接显示在操作栏上。图标区空间有限建议只给1-2个最重要的动作。ifRoom空间充足时显示不足时折叠进溢出菜单推荐大多数操作使用。never永远不进操作栏只出现在溢出菜单里。此时文字标题最重要不要只依赖图标。collapseActionView配合SearchView等可折叠控件使用需要与actionViewClass一起声明。可以用|组合比如ifRoom|withText表示同时显示图标和文字。我的习惯是编辑、保存这种高频操作用always搜索、设置这种中频操作用ifRoom剩下的统一丢给never。无脑全用always的下场很惨——操作栏一排图标挤到看不清长按才看到提示文字用户体验直接打折。3.2 菜单注入与点击分发别忽略android.R.id.home有了menu资源在Activity里重写两个方法就能接上操作栏override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main_menu, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_search - { Toast.makeText(this, 点了搜索, Toast.LENGTH_SHORT).show() true } R.id.action_settings - { // 跳转设置页 true } R.id.action_share - { // 调起分享 true } android.R.id.home - { onBackPressedDispatcher.onBackPressed() true } else - super.onOptionsItemSelected(item) } }先说onCreateOptionsMenu。这个方法在setSupportActionBar()之后的第一次绘制阶段被系统调用你在里面返回true表示有菜单要展示返回false表示这次不需要菜单。如果忘记写inflate操作栏右侧就会一片空白。再强调一次android.R.id.home。导航图标navigationIcon被点击时系统会把它包装成一个id为android.R.id.home的MenuItem抛给onOptionsItemSelected。所以如果你只是设置了setDisplayHomeAsUpEnabled(true)却没写处理逻辑返回箭头点了毫无反应如果你用了toolbar.setNavigationOnClickListener直接设置导航点击监听那android.R.id.home这个分支就不会触发两者二选一。如果你在Fragment里管理返回栈还可以在Activity里重写onSupportNavigateUp()统一处理导航逻辑override fun onSupportNavigateUp(): Boolean { return navController.navigateUp() || super.onSupportNavigateUp() }这种情况下setDisplayHomeAsUpEnabled(true)会自动让系统在导航图标点击时回调onSupportNavigateUp()比逐条处理android.R.id.home更省心。3.3 动态控制让操作栏随场景呼吸实际开发中操作栏经常需要跟随页面状态改变。比如滚动到列表尽头时隐藏操作栏或者某个页面进入编辑模式时临时隐藏菜单项。这些场景用Toolbar处理比ActionBar顺手很多。先看最基础的两个方法// 隐藏某个菜单项 toolbar.menu.findItem(R.id.action_search)?.isVisible false // 整个操作栏消失 toolbar.visibility View.GONE第一行代码只影响单项适合编辑态切换第二行代码直接移除整个操作栏的占位空间适合沉浸式浏览场景。如果你想做得更细腻可以用属性动画把操作栏滑出屏幕toolbar.animate() .translationY(-toolbar.height.toFloat()) .setDuration(200) .start() toolbar.animate() .translationY(0f) .setDuration(200) .start()这里的思路是让Toolbar沿Y轴平移移出屏幕后视觉上就像收缩了。注意一点translationY设置的是相对初始位置的偏移量第二个动画里要回到0而不是直接visibility GONE否则页面会出现闪跳。进阶用法里Toolbar还常配合CollapsingToolbarLayout、AppBarLayout做滚动折叠。这部分虽然用的还是Toolbar但已经进入了CoordinatorLayout的体系建议你把基础使用练熟之后再碰。4. 常见问题与实战避坑一份排查手册4.1 屏上冒出两条操作栏现象很直观页面顶部出现两条横条或者标题重复显示。原因几乎都是主题里忘了加NoActionBar。检查三个位置AndroidManifest.xml里Activity是否指定了带ActionBar的android:theme。values/themes.xml或values/styles.xml里应用主题是否继承了Theme.AppCompat.*NoActionBar。如果项目用了Theme.MaterialComponents.*注意它的命名和AppCompat不同但也有.NoActionBar变体例如Theme.MaterialComponents.Light.NoActionBar。排查顺序先看Manifest的theme被哪个Activity引用再看styles.xml的parent最后检查setContentView的布局根节点——Toolbar不要嵌套在窗口装饰自带的ActionBar下面两者是独立的。4.2 菜单项就是不出来遇到菜单不显示按以下顺序排查确认Activity继承的是AppCompatActivity而不是普通Activity。原生Activity不支持AppCompat的menuInflater包装。确认重写了onCreateOptionsMenu并返回true。如果返回false系统认为本次不需要菜单。确认menu的xmlns:app命名空间声明了。漏掉的话app:showAsAction会被解析成未知属性菜单项全部折叠进溢出菜单。确认showAsAction的值不是never。如果所有菜单项都设成never操作栏右侧只会出现一个三点入口看不到图标。如果图标是drawable引用的检查drawable文件是否存在且格式正确。资源解析失败时inflate可能抛异常但有时异常被吞掉最后菜单栏显示为空。我实际开发中遇到最多的是第二种和第四种基本都是笔误。所以别急着怀疑框架先把showAsAction和返回值检查一遍。4.3 返回箭头不响应这属于高频问题三种典型情况忘了写setDisplayHomeAsUpEnabled(true)箭头不会出现。写了开启但onOptionsItemSelected里没处理android.R.id.home点击无响应。同时设置了setNavigationOnClickListener和onOptionsItemSelected的home分支前者把点击事件消费掉后者永远执行不到。另外在某些Android系统版本上setSupportActionBar(toolbar)的调用顺序也会影响箭头显示。如果你在setSupportActionBar()之前调用setDisplayHomeAsUpEnabled(true)可能部分国产ROM上不生效稳妥做法是放在setSupportActionBar之后。4.4 主题混用导致崩溃或变样AppCompat的setSupportActionBar对主题有严格约束。如果AppCompatActivity遇到非AppCompat主题会抛出IllegalStateException: You need to use a Theme.AppCompat theme。这个异常信息很直白看到它就可以直奔主题文件。Material Components库的情况稍微复杂一点。Theme.MaterialComponents.DayNight.NoActionBar这类主题本身提供ActionBar逻辑但它和AppCompatActivity兼容良好如果你直接拿Theme.MaterialComponents配合AppCompatActivity某些变体可能会触发主题不兼容问题。我的建议是引入Material库的项目统一使用Material Components的.NoActionBar变体Activity继承AppCompatActivity菜单、返回键、主题表现都是最稳的。4.5 高度、阴影与低版本兼容在API 21以下Toolbar的android:elevation属性无效因为阴影依赖视图Z轴绘制老版本没有这个机制。需要阴影效果时可以给Toolbar外包一层android:layout_marginBottom配合渐变Drawable模拟阴影。在API 20及以下部分Toolbar属性也会失效比如app:titleCentered其实原生Toolbar根本没有这个属性要通过子View处理或借助第三方库实现。如果你在网上看到居中标题的写法注意先验证目标设备的最低系统版本。一个更稳妥的高度控制方式不要硬编码layout_height56dp用?attr/actionBarSize。原因前面提过它保证Toolbar高度和各种系统弹窗、DrawerLayout抽屉头的对齐一致。如果你硬编码高度在某些大屏设备或者特殊字体缩放下操作栏和系统组件之间可能出现明显的错位。5. 进阶与实操体会Toolbar只是起点5.1 与CoordinatorLayout的联动思路Toolbar最常用的进阶场景是作为AppBarLayout的一部分配合CoordinatorLayout实现滚动折叠、固定置顶等效果。基本结构androidx.coordinatorlayout.widget.CoordinatorLayout com.google.android.material.appbar.AppBarLayout androidx.appcompat.widget.Toolbar / /com.google.android.material.appbar.AppBarLayout ScrollView app:layout_behaviorstring/appbar_scrolling_view_behavior / /androidx.coordinatorlayout.widget.CoordinatorLayoutAppBarLayout给子View提供滚动行为的能力Toolbar在它里面可以响应RecyclerView的滚动事件实现向下滚动时收起操作栏、向上滚动时呼出操作栏的沉浸式体验。核心是给滚动容器设置app:layout_behaviorstring/appbar_scrolling_view_behavior给AppBarLayout设置app:liftOnScrolltrue。真要做滚出滚入效果还需要在AppBarLayout上声明app:layout_scrollFlagsscroll|enterAlways。这部分使用的虽然还是Toolbar但已经跨界到CoordinatorLayout体系。我的建议是先把基础Toolbar独立使用掌握牢再上组合否则菜单、返回、主题这些问题混在一起排查起来很难定位到底是谁出了问题。5.2 几条值得记进笔记的实操体会Activity里能少写的代码尽量少写。很多属性可以在XML里用app:定义好比如标题、图标、背景色代码里只需要setSupportActionBar(toolbar)和一句setDisplayHomeAsUpEnabled(true)。把样式留在XML里以后换肤或调整主题时只需要改一处资源。supportActionBar的接口和Toolbar的接口不要混着用。比如supportActionBar?.title和toolbar.setTitle()都能设置标题但Toolbar内部可能因为某个API调用重置另一个值出现莫名其妙的竞态。我给项目定过一个规范一律用supportActionBar控制标题和返回键只有菜单动态显隐这类ActionBar接口覆盖不到的操作才直接碰Toolbar。做沉浸式状态栏时不要简单地在setContentView后调用WindowCompat.setStatusBarColor硬套。Toolbar背景和状态栏颜色要一起处理通常我会给状态栏设置和Toolbar相同的颜色再用window.statusBarColor Color.TRANSPARENT配合fitsSystemWindows把Toolbar延伸到状态栏区域。这个布局稍复杂联动关系容易写乱建议在单独的模板页里调试好再复制到业务页面。善用theme和themeOverlay隔离样式。比如主标题要白色、副标题要浅灰不要在每个页面各自设置titleTextColor而是在values/themes.xml里定义一个主题覆盖style nameTheme.MyApp.Toolbar parentThemeOverlay.AppCompat.Dark.ActionBar item nameandroid:backgroundcolor/colorPrimary/item /style然后给Toolbar设置app:themestyle/Theme.MyApp.Toolbar。这比逐个设置属性好维护得多尤其当你在几十个页面统一调整操作栏样式时只改一处就能全局生效。5.3 别被过时言论带偏网上经常有人说ActionBar已经过时了完全不要碰。这话本身没错但容易误导新手——实际上Toolbar的很多接口写法依然沿用了ActionBar时代的命名比如setDisplayHomeAsUpEnabled、setDisplayShowTitleEnabled。学习路径上先理解ActionBar的职责模型再掌握Toolbar的实现方式反而是最快的一条路。而且项目里经常还会遇到历史代码比如activity.getActionBar()这种老写法。如果哪天维护代码时看到这个先别急着删它可能配合的是原生系统ActionBar而不是AppCompat。改Toolbar时要连同Manifest主题和Activity继承链一起替换只改一行代码会让异常继续潜伏在暗处。我个人在实际项目里的体会是操作栏这块的坑往往不是知识点太深而是半熟不熟。很多人知道ActionBar是旧的、Toolbar是新的但两个体系混用的边界在哪、主题之间怎么继承、菜单事件如何分发这些细节真正用起来才暴露问题。你只要把上面提到的主题、菜单、事件分发、尺寸规范这四个维度想清楚遇到任何操作栏相关的需求都能快速定位到根因。最后再分享一个小技巧调试操作栏问题时先开布局边界开发者选项里的显示布局边界一眼能看出Toolbar的实际占位和阴影范围比盯着log猜快得多。
返回列表