自定义ToolbarView实战指南(Kotlin版)

一、为什么我们需要造轮子?

看到标题你可能会问:系统自带Toolbar不香吗?确实香,但遇到这些场景就抓瞎了:

  • 设计稿要求标题栏带渐变背景+动态波浪线
  • 产品经理非要搞个不对称的返回按钮布局
  • UI设计师坚持标题和副标题要45度角重叠
    这时候再不自己动手撸View,就只能等着加班掉头发了!

二、从零打造ToolbarView

2.1 骨架搭建

kotlin 复制代码
class ToolbarView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
    
    // 三大核心组件
    private lateinit var backButton: ImageView
    private lateinit var titleView: TextView
    private lateinit var actionMenu: LinearLayout
    
    // 初始化三连
    init {
        initAttrs(attrs)
        initViews()
        setupClickListeners()
    }
}

关键点解析:

  • 继承ViewGroup而不是直接继承Toolbar(保持最大自由度)
  • 采用组合模式而不是继承(方便后续扩展)
  • 初始化拆分为属性解析、视图创建、事件绑定三步

2.2 测量布局实战

kotlin 复制代码
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val maxWidth = MeasureSpec.getSize(widthMeasureSpec)
    var totalHeight = 0
    
    // 测量返回按钮
    backButton.measure(
        MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
    )
    totalHeight = max(totalHeight, backButton.measuredHeight)
    
    // 测量标题(最多占用剩余宽度的70%)
    val titleMaxWidth = (maxWidth - backButton.measuredWidth) * 0.7f
    titleView.measure(
        MeasureSpec.makeMeasureSpec(titleMaxWidth.toInt(), MeasureSpec.AT_MOST),
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
    )
    totalHeight = max(totalHeight, titleView.measuredHeight)
    
    // 设置最终尺寸
    setMeasuredDimension(maxWidth, resolveSize(totalHeight + paddingTop + paddingBottom, heightMeasureSpec))
}

避坑指南:

  1. 处理wrap_content时需要特别注意AT_MOST模式
  2. 带IconFont的TextView需要单独处理CompoundDrawable测量
  3. 多语言文本可能导致测量抖动,需要添加layout稳定机制

2.3 自定义属性大全

xml 复制代码
<!-- res/values/attrs.xml -->
<declare-styleable name="ToolbarView">
    <!-- 背景相关 -->
    <attr name="toolbarBackground" format="color|reference" />
    <attr name="cornerRadius" format="dimension" />
    
    <!-- 标题样式 -->
    <attr name="titleText" format="string" />
    <attr name="titleTextColor" format="color" />
    <attr name="titleTextSize" format="dimension" />
    
    <!-- 返回按钮特殊配置 -->
    <attr name="backIcon" format="reference" />
    <attr name="backIconTint" format="color" />
</declare-styleable>

属性解析黑科技:

kotlin 复制代码
private fun initAttrs(attrs: AttributeSet?) {
    context.obtainStyledAttributes(attrs, R.styleable.ToolbarView).apply {
        // 解析渐变背景
        getDrawable(R.styleable.ToolbarView_toolbarBackground)?.let {
            background = if (it is GradientDrawable) {
                it.apply { cornerRadius = getDimension(...) }
            } else it
        }
        
        // 动态创建标题View
        titleView.text = getString(R.styleable.ToolbarView_titleText)
        titleView.setTextColor(getColor(R.styleable.ToolbarView_titleTextColor, Color.BLACK))
        
        // 返回按钮图标处理
        backButton.setImageDrawable(getDrawable(R.styleable.ToolbarView_backIcon))
        DrawableCompat.setTint(backButton.drawable, getColor(...))
        
        recycle()
    }
}

三、高级技巧加持

3.1 沉浸式状态栏适配

kotlin 复制代码
fun fitsSystemWindow() {
    val statusBarHeight = getStatusBarHeight()
    setPadding(paddingLeft, paddingTop + statusBarHeight, paddingRight, paddingBottom)
    layoutParams = layoutParams.apply { height += statusBarHeight }
}

private fun getStatusBarHeight(): Int {
    val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
    return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
}

3.2 动态主题切换

kotlin 复制代码
fun applyDarkTheme(isDark: Boolean) {
    val textColor = if (isDark) Color.WHITE else Color.BLACK
    val iconTint = if (isDark) Color.WHITE else Color.DKGRAY
    
    titleView.setTextColor(textColor)
    backButton.drawable.setTint(iconTint)
    actionMenu.children.forEach { (it as? ImageView)?.drawable?.setTint(iconTint) }
    
    // 带动画效果更丝滑
    animate().setDuration(300).alpha(0.8f).withEndAction { animate().alpha(1f) }
}

四、调试踩坑记录

4.1 触摸事件冲突

症状 :滑动返回手势和按钮点击冲突
处方:重写onInterceptTouchEvent

kotlin 复制代码
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    return when {
        // 在左右边缘30dp内时交给系统处理滑动返回
        ev.x < 30.dp || ev.x > width - 30.dp -> false
        // 其他区域自己处理点击
        else -> super.onInterceptTouchEvent(ev)
    }
}

4.2 内存泄漏检测

在onDetachedFromWindow中释放资源:

kotlin 复制代码
override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    // 清除动画
    clearAnimation()
    // 解绑回调
    backButton.setOnClickListener(null)
    // 释放大图资源
    backButton.setImageDrawable(null)
}

经验之谈:自定义View就像搭积木,先拆解设计稿,再组合基础组件,最后打磨细节。记得多用Canvas.saveLayer()来调试绘制范围!

相关推荐
walkskyer5 分钟前
Golang `testing`包使用指南:单元测试、性能测试与并发测试
开发语言·golang·单元测试
飞升不如收破烂~22 分钟前
WebSocketHandler 是 Spring Framework 中用于处理 WebSocket 通信的接口
开发语言
爱吃柠檬呀34 分钟前
C语言中的内存函数使用与模拟实现
c语言·开发语言
奔跑吧邓邓子42 分钟前
【Python爬虫(67)】Python爬虫实战:探秘旅游网站数据宝藏
开发语言·爬虫·python·旅游网站
土豆炒马铃薯。1 小时前
彻底卸载MySQL
java·开发语言·前端·数据库·mysql·删除·数据
卓大胖_1 小时前
SEO炼金术(4)| Next.js SEO 全攻略
开发语言·javascript·dreamweaver
数据小小爬虫1 小时前
如何使用Java爬虫按关键字搜索VIP商品实践指南
java·开发语言·爬虫
智想天开1 小时前
工厂方法模式:思考与解读
开发语言·c#·工厂方法模式
攻城狮_Dream2 小时前
基于 Python 的项目管理系统开发
android·数据库·python
啥都不懂的小小白2 小时前
Java常见设计模式(上):创建型模式
java·开发语言·设计模式