ARTICLE DETAIL

资讯详情

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

从ActionBar到Toolbar:Android顶部操作栏的接入与避坑

从ActionBar到Toolbar:Android顶部操作栏的接入与避坑 1. 为什么ActionBar逐渐被Toolbar取代两个组件的定位与差异如果说你刚开始接触Android开发多半会在各种教程里看到“ActionBar”和“Toolbar”这两个词混着出现。老实说我刚入行的时候也蒙过一阵明明Activity自带标题栏为什么还要新增一个Toolbar后来翻源码、改项目、踩了几次坑才算把这两者的关系和定位彻底理清楚。1.1 ActionBar是Activity“自带”的标题栏ActionBar是Android 3.0API 11开始引入的它的出现本来是为了统一应用顶部的操作栏样式把标题、菜单按钮、返回按钮等元素集中管理。如果你不特别处理直接继承AppCompatActivity后Activity的顶部就会显示一条ActionBar。它看起来像一把“内置的刀”你不需要在布局里写任何控件Activity本身就带着它。系统默认主题决定它的高度、背景和文字样式然后在主题里通过windowActionBar等属性控制显示或隐藏。1.2 Toolbar是“放在布局里”的独立组件Toolbar由Android 5.0API 21引入并在AndroidX AppCompat库中被广泛使用核心思路是把“顶部操作栏”从Activity中解耦出来变成一个普通控件。也就是说你可以像写TextView、Button一样在布局文件里声明一个Toolbar摆放在任意位置设置任意背景、高度、内边距甚至扔到列表头部或底部。它不再被Activity“绑架”所以灵活性完全取决于你。1.3 一张表看懂两者的关键差异对比项ActionBarToolbar存在方式由Activity/主题内置布局中不可见必须写在布局中是一个View定制能力受主题和Window特性限制高度、背景、位置、内边距基本全可配导航图标通过setDisplayHomeAsUpEnabled控制通过setNavigationIcon或者XML指定菜单承载自动承载选项菜单配合setSupportActionBar后承载菜单多实例一个Activity通常只有一个同一布局可以放多个Toolbar动画/联动配合属性动画较麻烦可自由配合动画和CoordinatorLayout也就是说ActionBar解决的是“有没有标题栏”的问题Toolbar解决的是“标题栏听不听话”的问题。很多老项目里开发者为了省事直接沿用ActionBar一旦遇到要定制背景渐变、折叠效果、悬浮按钮就开始头疼。这也是我写这篇文章的初衷把ActionBar与Toolbar的基本使用链路讲清楚帮助你在项目初期就选对方向。2. 实际项目接入把Toolbar作为主标题栏的完整操作我在自己的项目重构中做过一次对比实验同一个页面先用ActionBar实现再用Toolbar实现。结果发现除了代码量差不多Toolbar的样式可控性和后续扩展性明显胜出。接下来我按实际接入顺序把操作步骤拆解给你看。2.1 第一步切换主题去掉默认ActionBar很多人会在布局里写一个Toolbar结果运行后顶部出现两条栏上面是ActionBar下面是Toolbar。根本原因就是主题没有关闭默认ActionBar。所以接入Toolbar后第一步就是把Activity或Application的主题改成NoActionBar风格。style nameAppTheme parentTheme.AppCompat.Light.NoActionBar !-- 这里可以继续配状态栏、颜色等 -- /style注意主题名称里的NoActionBar对AppCompatActivity是生效的如果你的Activity继承的是普通Activity那可能要走原生主题例如Theme.MaterialComponents.DayNight.NoActionBar。总之保证当前Activity使用的主题中不带ActionBar才轮到Toolbar“上位”。2.2 第二步在布局中放置Toolbar控件最简单的方式是在页面布局顶部放入Toolbar。由于它本质是个View你可以自由控制layout_width、layout_height和背景不像ActionBar会受系统高度约束。?xml version1.0 encodingutf-8? 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:backgroundcolor/colorPrimary app:title首页 app:titleTextColorandroid:color/white app:subtitle副标题 app:subtitleTextColor#CCFFFFFF app:logomipmap/ic_logo app:navigationIcondrawable/ic_back / /LinearLayout这里我故意把title、subtitle、logo、navigationIcon都写在XML里方便你直观感受Toolbar的XML属性有多丰富。实际开发中subtitle不一定常用但navigationIcon几乎都会用到。2.3 第三步在Activity中绑定Toolbar布局完成后需要在Java/Kotlin代码里把Toolbar“交”给Activity。这时需调用setSupportActionBar让Toolbar成为当前页面的ActionBar替身从而让菜单、标题等逻辑可以复用系统内部的ActionBar机制。public class MainActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar findViewById(R.id.toolbar); setSupportActionBar(toolbar); // 显示返回键 if (getSupportActionBar() ! null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(false); } } }这里有一个常见疑问setSupportActionBar之后getSupportActionBar()返回的对象实际的载体就是刚才的Toolbar。所以后续操作返回键、修改标题、修改左上角图标都是通过getSupportActionBar()来操作。而Toolbar本身仍然是一个普通View你依然可以调用toolbar.setNavigationIcon、toolbar.setTitle等方法。提示如果你在XML里设置了app:title为“首页”又在代码里调用了setSupportActionBar并setTitle(另一个标题)以后者为准。如果不想让Toolbar显示XML里写死的标题可以用setDisplayShowTitleEnabled(false)关掉标题显示然后自己加一个TextView放在旁边自由度更高。2.4 第四步把Toolbar接入抽屉导航常见App里左上角是一个汉堡图标点击后拉出侧边抽屉。这个过程用Toolbar实现并不复杂。关键在于DrawerLayout要与Toolbar联动而不是直接操作ActionBar。androidx.drawerlayout.widget.DrawerLayout LinearLayout androidx.appcompat.widget.Toolbar ... / !-- 主内容 -- /LinearLayout !-- 侧边栏 -- NavigationView ... / /androidx.drawerlayout.widget.DrawerLayout代码层面我习惯借助ActionBarDrawerToggle来同步图标与抽屉状态ActionBarDrawerToggle toggle new ActionBarDrawerToggle( this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ); drawerLayout.addDrawerListener(toggle); toggle.syncState();ActionBarDrawerToggle会替你自动处理汉堡图标和旋转动画。如果你不小心把DrawerLayout放在Toolbar上面可能会发现抽屉把Toolbar盖住这个要注意布局层级。3. 菜单、标题和导航图标的协同工作我在前面代码里已经把Toolbar绑定成ActionBar替身。但菜单相关的方法名和回调仍然沿用以往ActionBar时代的第一直觉。很多人写菜单时会疑惑到底用android:showAsAction还是app:showAsAction我在这节给你说明白。3.1 菜单资源文件怎么组织在res/menu目录下新建菜单文件例如menu_main.xml?xml version1.0 encodingutf-8? menu 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:showAsActionifRoom / item android:idid/action_settings android:title设置 app:showAsActionnever / /menu关键在于showAsActionifRoom表示空间够就显示为图标不够就收进“三个点”的溢出菜单always表示总是显示为图标但实际使用时不建议太多never表示只显示在溢出菜单里。为什么这里要用app:showAsAction而不是android:showAsAction因为Toolbar和AppCompat库的菜单机制会优先解析app命名空间下的showAsAction用android命名空间可能在部分旧版本Android上失效。3.2 处理菜单点击事件菜单点击逻辑还是那两个回调Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } Override public boolean onOptionsItemSelected(NonNull MenuItem item) { if (item.getItemId() R.id.action_search) { // 处理搜索 return true; } else if (item.getItemId() R.id.action_settings) { // 处理设置 return true; } return super.onOptionsItemSelected(item); }这里有个细节当Toolbar通过setSupportActionBar绑定后菜单项的显示排列顺序、图标颜色、溢出按钮样式其实交由主题中的actionMenuTextColor、actionOverflowButtonStyle等属性控制。如果你只是修改Toolbar的背景但菜单文字颜色仍然是默认深色在深色背景上就会看不清。因此要么设主题的颜色要么自定义菜单的样式。3.3 控制标题、返回键和图标显示的常用API我整理一下常用API方便你对照查阅setTitle(CharSequence title)直接修改标题文字。setSubtitle(CharSequence subtitle)设置或清除副标题。getSupportActionBar().setDisplayHomeAsUpEnabled(true)显示返回箭头。在Toolbar绑定后等价于让Toolbar显示navigationIcon并让点击事件走onOptionsItemSelected。setNavigationIcon(Drawable icon)设置左上角图标。如果同时设置了NavigationOnClickListener点击会优先走这个监听器。getSupportActionBar().setDisplayShowTitleEnabled(false)隐藏标题与副标题适合你自己想在右侧放自定义文字场景。提示当没有调用setSupportActionBar时Toolbar只能算一个普通的View容器它不参与菜单和Window特性。你如果单独使用Toolbar而不绑定菜单依然不显示title可以通过setTitle设置但onCreateOptionsMenu里的菜单不会自动映射进去。这点我在开发中确认过新手容易拿一个Toolbar说“我菜单怎么没有”。4. 避坑记录从“无菜单”到“滚动渐变”的实测心得技术类的博客光讲步骤不够还得把坑提前摆出来。下面几条都是我接过的真实问题产生原因不复杂但会把第一次接触Toolbar的人卡上半天。4.1 两栏标题问题现象页面顶部出现两个标题栏。原因几乎都是主题没有用NoActionBar系统自带ActionBar还在你又加了一个Toolbar。判断的办法很简单看主题parent里有没有NoActionBar关键词。如果你用了MaterialComponents主题也要确认风格为*.NoActionBar。还有一种情况是XML里漏了app:theme导致Toolbar自己使用了Light.DarkActionBar主题也会出现额外背景条。4.2 菜单不显示现象onCreateOptionsMenu里已经inflate了menu但页面顶部没有任何图标也没有三个点。思路排查先确认Activity是否继承的是AppCompatActivity以及setContentView之后是否调用了setSupportActionBar。如果在这两个前提下检查menu根标签的命名空间和item里的showAsAction是否写了app前缀。只要有一个item的showAsAction为never并且没有设置icon则它只会出现在溢出菜单中屏幕上当然看不到。4.3 标题文字和图标颜色变成白色ActionBar通常会把标题文字自动设为浅色因为主题背景默认是深色。但当你把Toolbar背景设成一个非常浅的颜色标题文字仍保持白色就会看不清。原因是标题文字颜色不是自动根据背景计算的而是依赖主题属性。解决办法有两种一是直接在Toolbar上设置titleTextColorandroidx.appcompat.widget.Toolbar android:background#FFFFFF app:titleTextColor#333333 app:subtitleTextColor#666666 /二是设置主题中的actionMenuTextColor和android:textColorPrimary等属性影响菜单文字和溢出按钮图标颜色。实测来看如果只是临时改某个页面直接在Toolbar上设置最快且不会影响全局。4.4 沉浸式状态栏下的高度适配现在很多App喜欢把状态栏改成透明或半透明让内容沉浸到状态栏下面。此时如果Toolbar放在线性布局顶部系统状态栏依然独立占用高度页面顶部会出现一条突兀的颜色块。我常用的做法是给Toolbar设置一个fitsSystemWindows属性或者手动设置状态栏高度padding。简单方案是在根部布局使用LinearLayout android:fitsSystemWindowstrue ... /LinearLayout但fitsSystemWindows在一些复杂布局中会出现奇怪效果比如背景被撑大、内容被顶起。如果你只想要Toolbar避开状态栏更可控的方式是代码动态设置高度int statusBarHeight 0; int resourceId getResources().getIdentifier(status_bar_height, dimen, android); if (resourceId 0) { statusBarHeight getResources().getDimensionPixelSize(resourceId); } toolbar.setPadding(0, statusBarHeight, 0, 0);注意这个方案会改变Toolbar的实际高度所以布局中要合理设置layout_height。如果后续还需要处理折叠效果就得结合CoordinatorLayout和AppBarLayout那就不是“基本使用”能覆盖的了。5. 进阶扩展让Toolbar真正“融入”页面视觉Toolbar最让我觉得值回票价的地方不是它能替代ActionBar而是它能把顶部操作栏和页面其他元素组合成一种交互关系。5.1 滚动时动态改变Toolbar背景透明度配合CoordinatorLayout和AppBarLayout你能让Toolbar在页面滚动时从透明渐变成不透明。这种效果在个人主页、详情页非常常见。最简单的写法是给Toolbar设置一个带透明度的背景然后监听滚动偏移量实时更新alpha。appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) - { int totalRange appBarLayout.getTotalScrollRange(); float alpha 1.0f - (Math.abs(verticalOffset) / (float) totalRange); toolbar.setAlpha(alpha); });当然更成熟的做法是让Toolbar直接作为AppBarLayout的子View然后通过app:layout_scrollFlagsscroll|enterAlways控制滚动行为。有兴趣的话这套联动逻辑值得单独写一篇但前提是你先理解了Toolbar作为View的定位才能把各种动画放在它身上。5.2 自定义导航按钮与Fragment返回栈的联动当页面中包含Fragment时顶部返回键的行为需要统一处理。你可以给Toolbar设置NavigationOnClickListener然后在监听器里判断FragmentManager的回退栈是否为空toolbar.setNavigationOnClickListener(v - { if (getSupportFragmentManager().getBackStackEntryCount() 0) { getSupportFragmentManager().popBackStack(); } else { finish(); } });用这个方法可以避免返回键直接关闭整个Activity造成体验断裂。很多项目把这一逻辑封装到BaseActivity中让所有子页面统一遵守。5.3 自定义View嵌入Toolbar因为Toolbar本质是ViewGroup所以允许在它内部添加子View比如搜索框、标题居中、红点提示等。如果你想让标题居中常见做法就是把Toolbar里自带标题隐藏然后塞一个TextView让它layout_gravitycenter。androidx.appcompat.widget.Toolbar android:idid/toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize TextView android:idid/tv_center_title android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter android:text居中标题 android:textSize18sp android:textColorandroid:color/white / /androidx.appcompat.widget.Toolbar这样做有个小坑子View默认对齐方式受Toolbar的contentInset影响也就是左边会被logo、navigationIcon预留的区域挤压。如果你想把TextView真正居中可以调整app:contentInsetStartWithNavigation0dp或者干脆不要navigationIcon改用纯按钮。实际体验来说contentInset在复杂页面上确实会造成“看似居中实则偏右”的情况需要自己多调几个值。我把ActionBar和Toolbar的基本使用拆成定位、接入、菜单、避坑和进阶五个部分从选择原因到实际踩坑尽量把文档里不写的内容也补上了。个人体感是新项目用Toolbar老项目没有必要大动干戈迁移除非你确实遇到样式定制瓶颈或者要接入AppBarLayout做复杂联动。如果你第一次上手照着这节里的代码从NoActionBar主题开始跑再逐个试XML属性大概一个下午就能把两者差异摸清楚。
返回列表