1. 为什么需要统一封装ToolBar?
在Android应用开发中,ToolBar作为Material Design的核心组件之一,几乎出现在每个页面的顶部。我接手过不少项目,发现很多团队对ToolBar的使用存在以下典型问题:
- 每个Activity/Fragment重复编写相似的ToolBar配置代码
- 样式不统一导致视觉体验割裂
- 基础功能(如返回按钮、标题居中)需要反复实现
- 后期修改主题色或交互逻辑需要全局搜索替换
以最近参与的电商App为例,38个页面中有29个独立实现了ToolBar逻辑,仅修改导航图标就花费了2个工作日。这种重复劳动完全可以通过封装避免。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Kotlin实现基础封装方案
2.1 创建BaseToolBarHelper
首先定义基础工具类,用Kotlin的扩展函数增强可读性:
kotlin复制object ToolBarHelper {
// 基础配置扩展函数
fun ToolBar.setupBase(
title: String,
showBack: Boolean = true,
backIconRes: Int = R.drawable.ic_back
) {
// 标题文本配置
this.title = title
setTitleTextColor(ContextCompat.getColor(context, R.color.white))
// 返回按钮配置
if (showBack) {
navigationIcon = ContextCompat.getDrawable(context, backIconRes)
setNavigationOnClickListener {
(context as? Activity)?.finish()
}
}
}
}
关键技巧:使用默认参数减少重载方法,通过类型安全转换( as? )避免类型转换异常
2.2 主题样式统一方案
在styles.xml中定义基础样式:
xml复制<style name="BaseToolBar" parent="Widget.MaterialComponents.Toolbar">
<item name="android:minHeight">?attr/actionBarSize</item>
<item name="titleTextAppearance">@style/ToolbarTitleAppearance</item>
<item name="android:background">@color/colorPrimary</item>
</style>
<style name="ToolbarTitleAppearance">
<item name="android:textSize">18sp</item>
<item name="android:textStyle">bold</item>
</style>
在布局中统一引用:
xml复制<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BaseToolBar"
app:layout_constraintTop_toTopOf="parent" />
3. 高级功能封装实践
3.1 动态菜单配置
通过DSL风格配置菜单项:
kotlin复制class DynamicMenuBuilder {
private val menuItems = mutableListOf<MenuItemData>()
fun item(
id: Int,
icon: Int,
title: String = "",
showAsAction: Int = MenuItem.SHOW_AS_ACTION_IF_ROOM,
action: (MenuItem) -> Unit = {}
) {
menuItems.add(MenuItemData(id, icon, title, showAsAction, action))
}
fun applyTo(toolbar: ToolBar) {
toolbar.inflateMenu(R.menu.empty_menu)
menuItems.forEach { data ->
toolbar.menu.add(0, data.id, 0, data.title).apply {
icon = ContextCompat.getDrawable(toolbar.context, data.icon)
setShowAsAction(data.showAsAction)
setOnMenuItemClickListener {
data.action(it)
true
}
}
}
}
}
// 使用示例
ToolBarHelper.setupDynamicMenu(toolbar) {
item(R.id.action_search, R.drawable.ic_search) {
startActivity(Intent(context, SearchActivity::class.java))
}
item(R.id.action_share, R.drawable.ic_share, "分享") {
// 分享逻辑
}
}
3.2 沉浸式状态栏适配
结合系统Bar处理:
kotlin复制fun ToolBar.setupWithStatusBar(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.apply {
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = Color.TRANSPARENT
}
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
val systemWindowInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemWindowInsets.top
}
insets
}
}
}
4. 常见问题解决方案
4.1 菜单点击事件冲突
现象:动态添加的MenuItem点击无响应
解决方案:
- 确保setShowAsAction正确配置
- 检查MenuItem的groupId和itemId是否唯一
- 在Activity中重写onOptionsItemSelected时调用super
4.2 样式不生效排查步骤
- 检查父主题是否继承自Theme.MaterialComponents
- 确认没有在代码中覆盖样式属性
- 使用Layout Inspector检查最终应用的属性
4.3 与Fragment的配合问题
推荐在BaseFragment中封装:
kotlin复制abstract class BaseFragment : Fragment() {
protected open fun setupToolbar(toolbar: ToolBar) {
(requireActivity() as? AppCompatActivity)?.setSupportActionBar(toolbar)
ToolBarHelper.setupBase(toolbar, getToolbarTitle(), showBack = true)
}
abstract fun getToolbarTitle(): String
}
5. 性能优化建议
- 避免在ToolBar中使用重量级自定义View
- 菜单图标建议使用VectorDrawable
- 频繁变化的ToolBar考虑使用ViewStub延迟加载
- 使用合并标签减少布局层级:
xml复制<merge xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BaseToolBar" />
</merge>
6. 扩展功能实现
6.1 动态变色效果
实现滚动渐变效果:
kotlin复制fun ToolBar.setupScrollColorTransition(
scrollView: NestedScrollView,
startColor: Int,
endColor: Int
) {
val evaluator = ArgbEvaluator()
scrollView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
val ratio = min(1f, scrollY.toFloat() / height)
val color = evaluator.evaluate(ratio, startColor, endColor) as Int
setBackgroundColor(color)
}
}
6.2 搜索框动态展开
实现Material风格的搜索交互:
kotlin复制fun ToolBar.setupSearchTransition(searchView: SearchView) {
val animator = ValueAnimator.ofInt(0, 1).apply {
duration = 300
addUpdateListener {
val value = it.animatedValue as Int
// 实现宽度/透明度动画
}
}
menu.findItem(R.id.action_search).setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
animator.start()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
animator.reverse()
return true
}
}
)
}
7. 测试验证方案
7.1 单元测试要点
kotlin复制@RunWith(AndroidJUnit4::class)
class ToolBarHelperTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testBaseSetup() {
activityRule.scenario.onActivity { activity ->
val toolbar = activity.findViewById<ToolBar>(R.id.toolbar)
ToolBarHelper.setupBase(toolbar, "Test")
// 验证标题
assertEquals("Test", toolbar.title)
// 验证返回按钮
assertNotNull(toolbar.navigationIcon)
}
}
}
7.2 UI自动化测试
使用Espresso编写交互测试:
kotlin复制@RunWith(AndroidJUnit4::class)
class ToolBarBehaviorTest {
@Test
fun testMenuClick() {
onView(withId(R.id.toolbar)).perform(click())
onView(withContentDescription("Search")).perform(click())
intended(hasComponent(SearchActivity::class.java.name))
}
}
8. 兼容性处理
8.1 低版本适配方案
针对API 21以下设备:
kotlin复制fun setupCompat(toolbar: ToolBar) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
toolbar.setPadding(
toolbar.paddingLeft,
toolbar.paddingTop + getStatusBarHeight(toolbar.context),
toolbar.paddingRight,
toolbar.paddingBottom
)
}
}
private fun getStatusBarHeight(context: Context): Int {
val resourceId = context.resources.getIdentifier(
"status_bar_height", "dimen", "android"
)
return if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else 0
}
8.2 深色模式适配
在values-night中配置夜间模式样式:
xml复制<style name="BaseToolBar" parent="Widget.MaterialComponents.Toolbar">
<item name="android:background">@color/nightColorPrimary</item>
<item name="titleTextColor">@color/nightTextColor</item>
</style>
动态切换检测:
kotlin复制fun checkDarkMode(toolbar: ToolBar) {
val nightMode = toolbar.context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK
if (nightMode == Configuration.UI_MODE_NIGHT_YES) {
toolbar.setBackgroundColor(
ContextCompat.getColor(toolbar.context, R.color.nightColorPrimary)
)
}
}
9. 模块化设计方案
9.1 作为独立模块发布
在build.gradle中配置:
groovy复制android {
namespace 'com.your.lib.toolbar'
// 启用资源混淆
buildFeatures {
androidResources = true
}
}
dependencies {
api 'androidx.appcompat:appcompat:1.6.1'
api 'com.google.android.material:material:1.9.0'
}
9.2 自定义属性支持
在attrs.xml中定义:
xml复制<declare-styleable name="SmartToolBar">
<attr name="stb_title" format="string|reference" />
<attr name="stb_backIcon" format="reference" />
<attr name="stb_menuConfig" format="reference" />
</declare-styleable>
在代码中解析:
kotlin复制class SmartToolBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.toolbarStyle
) : ToolBar(context, attrs, defStyleAttr) {
init {
context.obtainStyledAttributes(attrs, R.styleable.SmartToolBar).apply {
val title = getString(R.styleable.SmartToolBar_stb_title)
val backIcon = getResourceId(R.styleable.SmartToolBar_stb_backIcon, 0)
if (!title.isNullOrEmpty()) {
this@SmartToolBar.title = title
}
if (backIcon != 0) {
navigationIcon = ContextCompat.getDrawable(context, backIcon)
}
recycle()
}
}
}
10. 实际项目集成案例
10.1 多模块项目集成
在基础模块中声明:
kotlin复制interface ToolBarService {
fun setupDefault(toolbar: ToolBar, title: String)
fun setupWithMenu(toolbar: ToolBar, config: MenuConfig)
}
// 在app模块实现
class ToolBarServiceImpl : ToolBarService {
override fun setupDefault(toolbar: ToolBar, title: String) {
ToolBarHelper.setupBase(toolbar, title)
}
}
通过DI框架注入:
kotlin复制@Module
@InstallIn(SingletonComponent::class)
abstract class ToolBarModule {
@Binds
abstract fun bindService(impl: ToolBarServiceImpl): ToolBarService
}
10.2 A/B测试方案
实现动态样式切换:
kotlin复制fun applyVariant(toolbar: ToolBar, variant: ToolBarVariant) {
when (variant) {
ToolBarVariant.A -> {
toolbar.setBackgroundColor(Color.RED)
toolbar.layoutParams.height = dpToPx(56)
}
ToolBarVariant.B -> {
toolbar.setBackgroundColor(Color.BLUE)
toolbar.layoutParams.height = dpToPx(72)
}
}
}
private fun dpToPx(dp: Int): Int {
return (dp * Resources.getSystem().displayMetrics.density).toInt()
}
通过远程配置控制:
kotlin复制Firebase.remoteConfig.fetchAndActivate().addOnCompleteListener {
val variant = Firebase.remoteConfig.getString("toolbar_variant")
applyVariant(toolbar, ToolBarVariant.valueOf(variant))
}
