4、状态栏(StatusBar)架构:布局、显示隐藏与触摸事件

状态栏这东西,说白了就是手机最顶上那一小条。别小看它,用户每天看手机,第一眼扫到的就是这里。时间、信号、电量,全挤在这几十个像素里。我刚开始看SystemUI源码时,觉得状态栏应该挺简单——不就是显示几个图标嘛。结果一深入,发现里面的门道比我想象的多得多。

今天我们就来拆解StatusBar的三个核心问题:布局怎么搭的、什么时候显示什么时候隐藏、触摸事件怎么处理。嗯,这三个点搞明白了,状态栏这块你基本就通了。

4.1 布局结构:层层嵌套的“三明治”

先看布局。StatusBar的布局文件在SystemUI/res/layout/status_bar.xml。我习惯把它理解成一个三明治结构:

<!-- 简化后的status_bar.xml结构 -->
<com.android.systemui.statusbar.phone.PhoneStatusBarView>
    <!-- 第一层:通知图标区域 -->
    <LinearLayout android:id="@+id/notification_icon_area">
        <!-- 这里放通知图标,比如微信消息 -->
    </LinearLayout>

    <!-- 第二层:系统图标区域 -->
    <LinearLayout android:id="@+id/system_icon_area">
        <!-- 信号、WiFi、蓝牙、电量等 -->
    </LinearLayout>

    <!-- 第三层:时钟 -->
    <TextView android:id="@+id/clock" />
</com.android.systemui.statusbar.phone.PhoneStatusBarView>

你可能会问:为什么是这种顺序?其实这是有讲究的。通知图标在左边,系统图标在右边,时钟在最右边。这是Android从早期就定下的设计规范。我在项目中遇到过一个问题:某些定制ROM把时钟挪到了左边,结果用户反馈说“不习惯”,最后又改回去了。你看,UI布局有时候不只是技术问题,还有用户习惯的考量。

核心要点:StatusBar的布局是PhoneStatusBarView作为根容器,内部通过LinearLayout划分区域。每个区域又由具体的StatusBarIconView来承载图标。

再往下挖一层。每个系统图标,比如信号强度,其实是一个StatusBarIconView。这个View会监听对应的状态变化,然后更新图标。举个例子:

// StatusBarIconView 的核心逻辑
public void setIcon(StatusBarIcon icon) {
    // 更新drawable
    setImageDrawable(icon.getDrawable());
    // 更新内容描述(无障碍用)
    setContentDescription(icon.contentDescription);
    // 更新可见性
    setVisibility(icon.visible ? VISIBLE : GONE);
}

这里有个小细节:StatusBarIcon是一个数据类,它封装了图标的所有状态。View层只负责渲染,数据层负责逻辑。嗯,典型的MVC模式。

4.2 显示与隐藏逻辑:谁在控制状态栏的“生杀大权”

状态栏什么时候显示?什么时候隐藏?这个问题看似简单,但实际场景很复杂。比如:

  • 全屏播放视频时,状态栏要隐藏
  • 下拉通知栏时,状态栏要显示
  • 来电时,状态栏要显示
  • 某些应用可以控制状态栏的显示状态

我当初看这部分代码时,第一反应是:这肯定有个全局的状态机。结果还真让我猜对了。核心逻辑在StatusBarStateController里。

// StatusBarState 的几种状态
public static final int STATUS_BAR_STATE_SHOWING = 0;   // 显示
public static final int STATUS_BAR_STATE_HIDING = 1;    // 正在隐藏
public static final int STATUS_BAR_STATE_HIDDEN = 2;    // 已隐藏

状态切换的触发点主要有两个:

  1. 系统窗口标志(Window Flags):应用可以通过WindowManager.LayoutParams.FLAG_FULLSCREEN来请求隐藏状态栏。SystemUI会监听这个标志的变化。
  2. 系统UI可见性(SystemUiVisibility):这是更细粒度的控制。比如SYSTEM_UI_FLAG_FULLSCREENSYSTEM_UI_FLAG_HIDE_NAVIGATION

避坑指南:我曾经遇到过一个bug:某个应用设置了FLAG_FULLSCREEN,但状态栏还是显示出来了。查了半天发现,是因为该应用在onResume里设置了标志,但onPause时没有清除。结果状态栏一直处于“显示”状态。所以,状态栏的显示隐藏一定要成对处理

具体到代码层面,状态栏的显示隐藏是通过StatusBarWindowManager来控制的。它会根据当前状态,调整状态栏窗口的y坐标偏移量:

// StatusBarWindowManager 中的核心方法
public void setBarState(int state) {
    switch (state) {
        case STATUS_BAR_STATE_SHOWING:
            // 将状态栏移回原位
            mStatusBarView.setTranslationY(0);
            break;
        case STATUS_BAR_STATE_HIDING:
            // 开始动画,将状态栏移出屏幕
            mStatusBarView.animate().translationY(-mStatusBarHeight);
            break;
        case STATUS_BAR_STATE_HIDDEN:
            // 直接移出屏幕,不带动画
            mStatusBarView.setTranslationY(-mStatusBarHeight);
            break;
    }
}

这里有个细节:隐藏时为什么用translationY而不是setVisibility(GONE)?因为translationY可以配合动画,实现平滑的滑入滑出效果。而setVisibility(GONE)会直接触发布局重绘,导致闪烁。嗯,这就是为什么你看到的隐藏动画是“滑上去”而不是“突然消失”。

4.3 触摸事件处理:状态栏的“触觉神经”

状态栏的触摸事件处理,说白了就是解决一个问题:谁该响应这次触摸

你想想看,状态栏上同时有:

  • 通知图标(点击进入通知详情)
  • 系统图标(点击进入设置)
  • 时钟(点击进入闹钟设置)
  • 空白区域(下拉展开通知栏)

这么多目标挤在一条窄窄的栏里,触摸事件的分发就变得很关键。SystemUI的做法是:先拦截,再分发

核心类PhoneStatusBarView重写了onInterceptTouchEvent方法:

// PhoneStatusBarView 的触摸拦截逻辑
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        // 判断触摸点是否在某个可点击的图标上
        View target = findTargetView(event.getX(), event.getY());
        if (target != null) {
            // 如果点在图标上,不拦截,让子View处理
            return false;
        } else {
            // 如果点在空白区域,拦截下来,准备处理下拉手势
            return true;
        }
    }
    return super.onInterceptTouchEvent(event);
}

这里有个关键点:findTargetView方法会遍历所有子View,判断触摸点是否落在某个View的边界内。如果落在图标上,就不拦截,让图标自己处理点击事件。如果落在空白区域,就拦截下来,交给StatusBaronTouchEvent处理下拉手势。

注意:这里有一个常见的坑。如果某个图标的clickable属性没有设置为true,那么即使触摸点落在它上面,onInterceptTouchEvent也不会认为它是“可点击的”,从而把事件交给空白区域处理。结果就是:你明明点在了WiFi图标上,却触发了下拉通知栏。我曾经排查过一个类似的bug,最后发现是某个第三方图标没有设置clickable

再往下看,如果事件被拦截了,会进入StatusBaronTouchEvent

// StatusBar 的触摸事件处理
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 记录起始位置
            mTouchDownY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            // 计算滑动距离
            float dy = event.getY() - mTouchDownY;
            if (dy > mTouchSlop) {
                // 如果向下滑动超过阈值,开始展开通知栏
                startExpand();
                return true;
            }
            break;
        case MotionEvent.ACTION_UP:
            // 如果滑动距离不够,不展开
            break;
    }
    return false;
}

这里有个小技巧:mTouchSlop是系统定义的触摸滑动阈值,通常为8像素。为什么要设这个阈值?因为用户可能只是不小心碰了一下状态栏,并不是想下拉。设个阈值可以避免误触。嗯,用户体验的细节就在这里。

4.4 总结一下

状态栏的架构,说白了就是三件事:

模块 核心类 关键方法
布局结构 PhoneStatusBarView onFinishInflate()
显示隐藏 StatusBarWindowManager setBarState()
触摸事件 PhoneStatusBarView + StatusBar onInterceptTouchEvent() + onTouchEvent()

我个人习惯把状态栏看作一个“微型窗口管理器”。它既要处理自身的显示状态,又要响应触摸事件,还要协调各个图标的更新。说实话,刚开始看这部分代码时,我觉得挺乱的。但后来发现,只要抓住“布局-状态-事件”这条主线,一切都清晰了。

下一章我们会聊到通知栏的展开与折叠,那又是另一个有趣的话题。到时候见。