Android7 SystemUI 源码解析(六)QuickSettings快捷设置面板

系列导航一:全景概览与架构设计 | 二:启动流程深度解析 | 三:StatusBar 状态栏 | 四:NavigationBar 导航栏 | 五:通知系统 | 六:QuickSettings(本文) | 七:Keyguard 锁屏 | 八:Recents 与其他子系统


一、概述

下拉通知栏后弹出的那一片网格开关------Wi-Fi、蓝牙、手电筒、飞行模式------就是 QuickSettings 面板。它由两部分组成:状态模型QSTile 及其子类,描述每个开关的图标、文案、开关值)和视图层QSPanel / QSTileView,负责把状态画出来并转发点击)。QSTileHost 则夹在两者中间,负责按配置批量创建和回收 Tile。

AOSP7 中这个模块的源码横跨两个包,初学者最容易踩的第一个坑就是找不到 QSTileHost

复制代码
frameworks/base/packages/SystemUI/src/com/android/systemui/
├── statusbar/phone/
│   └── QSTileHost.java             # ★ Tile 宿主(在 statusbar/phone,不在 qs/)
├── qs/
│   ├── QSPanel.java                # ★ 面板视图
│   ├── QSTile.java                 # ★ Tile 基类(含 State / BooleanState)
│   ├── QSTileView.java             # Tile 视图(TouchAnimator 风格)
│   ├── QSTileBaseView.java         # QSTileView 的基类
│   ├── QSDetailItems.java          # Tile 详情列表
│   ├── QSFooter.java               # 面板底部(编辑入口)
│   ├── QSContainer.java            # QS 容器
│   ├── TileLayout.java             # 网格布局(extends ViewGroup)
│   ├── QuickTileLayout.java        # 单列布局(extends LinearLayout)
│   ├── PagedTileLayout.java        # 分页布局(extends ViewPager)
│   ├── tiles/                      # 各种内置 Tile 实现
│   │   ├── WifiTile.java
│   │   ├── BluetoothTile.java
│   │   ├── FlashlightTile.java
│   │   ├── DndTile.java
│   │   ├── LocationTile.java
│   │   ├── HotspotTile.java
│   │   ├── AirplaneModeTile.java
│   │   ├── CellularTile.java
│   │   ├── BatteryTile.java
│   │   ├── RotationLockTile.java
│   │   ├── CastTile.java
│   │   ├── DataSaverTile.java
│   │   ├── ColorInversionTile.java
│   │   ├── NightDisplayTile.java
│   │   ├── IntentTile.java
│   │   ├── UserTile.java
│   │   └── WorkModeTile.java
│   └── external/                   # 第三方 TileService 接入
│       ├── TileService.java
│       ├── CustomTile.java
│       ├── TileLifecycleManager.java
│       └── TileServices.java
└── tuner/
    └── TunerService.java           # ★ 用户调参(含 QS Tile 列表)

关键设计QSTileHost 之所以放在 statusbar/phone/,是因为它要把 Tile 的状态回调转发给 PhoneStatusBar 里的 KeyguardMonitorSecurityController 等控制器,放在状态栏包里就近取用这些依赖,比放在纯视图包 qs/ 里更合适。


二、QSPanel 的创建

2.1 在 makeStatusBarView() 中的装配

回顾系列第三篇,PhoneStatusBar 的初始化流程里会装配 QS。QS 面板并不是 makeStatusBarView() 直接 new 出来的,而是通过一个 AutoReinflateContainer 延迟 inflate 的,QSTileHost 则经由 SystemUIFactory 工厂方法创建。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java

java 复制代码
// ... makeStatusBarView() 内,装配 QS 面板(简化,删除无关初始化)...
final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(
        mContext, this, mBluetoothController, mLocationController,
        mRotationLockController, mNetworkController, mZenModeController,
        mHotspotController, mCastController, mFlashlightController,
        mUserSwitcherController, mUserInfoController, mKeyguardMonitor,
        mSecurityController, mBatteryController, mIconController,
        mNextAlarmController);
mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow);
container.addInflateListener(new InflateListener() {
    @Override
    public void onInflated(View v) {
        QSContainer qsContainer = (QSContainer) v.findViewById(
                R.id.quick_settings_container);
        qsContainer.setHost(qsh);                    // ← 把宿主交给容器
        mQSPanel = qsContainer.getQsPanel();         // ← 取出 QSPanel
        mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
        mKeyguardStatusBar.setQSPanel(mQSPanel);
        mHeader = qsContainer.getHeader();
    }
});

关键设计QSTileHost 的真实构造参数有一长串控制器(蓝牙、定位、旋转、网络、勿扰......),这些依赖在面板真正 inflate 之前就备好。QS 面板本身是延迟 inflate 的,所以 QSPanel 的取用被放进 InflateListener.onInflated(),等容器展开后才拿到视图。

2.2 QSPanel 布局文件

面板的布局由 qs_panel.xml 描述,最外层是 QSContainer,内部嵌套了真正的 QSPanel。注意亮度滑块不在这个文件里------它属于 QS 展开区域(quick_tile_layout)的一部分,见第七节。

源码路径frameworks/base/packages/SystemUI/res/layout/qs_panel.xml

xml 复制代码
<com.android.systemui.qs.QSContainer
        android:id="@+id/quick_settings_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        android:clipChildren="false"
        android:elevation="4dp">

    <!-- 真正的 QS 面板 -->
    <com.android.systemui.qs.QSPanel
            android:id="@+id/quick_settings_panel"
            android:background="#0000"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="8dp" />

    <!-- 顶部信号/时间区 -->
    <include layout="@layout/quick_status_bar_expanded_header" />

    <!-- Tile 详情区 -->
    <include android:id="@+id/qs_detail" layout="@layout/qs_detail" />

    <!-- 编辑面板(默认隐藏) -->
    <include android:id="@+id/qs_customize" layout="@layout/qs_customize_panel"
        android:visibility="gone" />
</com.android.systemui.qs.QSContainer>

关键设计QSPanel 在 XML 里只是个空壳容器,真正的 Tile 网格是运行期由 QSPanel 动态 addView 进去的------这就是为什么布局文件里看不到任何 QSTileView


三、QSTileHost------Tile 管理器

QSTileHost 是 QuickSettings 的核心管理类 ,负责按配置创建、回收和管理所有 Tile。注意它的类声明:QSTileHost 实现了 QSTile.Host(给 Tile 提供运行环境)和 TunerService.Tunable(监听用户调参)。

3.1 构造与初始化

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java

java 复制代码
public class QSTileHost implements QSTile.Host, Tunable {
    public static final String TILES_SETTING = Secure.QS_TILES;  // 持久化键

    private final Context mContext;
    private final PhoneStatusBar mStatusBar;
    // 用 LinkedHashMap 保持 Tile 的排列顺序
    private final LinkedHashMap<String, QSTile<?>> mTiles = new LinkedHashMap<>();
    protected final ArrayList<String> mTileSpecs = new ArrayList<>();
    // ... 一长串控制器字段(蓝牙/定位/网络/勿扰...)与 Looper、TileServices ...

    public QSTileHost(Context context, PhoneStatusBar statusBar,
            // ... 一长串控制器参数(蓝牙/定位/网络/勿扰...)...
            BatteryController battery, StatusBarIconController iconController,
            NextAlarmController nextAlarmController) {
        mContext = context;
        mStatusBar = statusBar;
        // ... 保存各控制器 ...

        final HandlerThread ht = new HandlerThread(
                QSTileHost.class.getSimpleName(), Process.THREAD_PRIORITY_BACKGROUND);
        ht.start();
        mLooper = ht.getLooper();                 // Tile 的状态刷新跑在这条后台线程

        mServices = new TileServices(this, mLooper);

        TunerService.get(mContext).addTunable(this, TILES_SETTING);  // ★ 监听用户调参
        mAutoTiles = new AutoTileManager(context, this);
    }
}

关键设计mTilesLinkedHashMap 而非 HashMap,因为 QS 的展示顺序必须稳定,LinkedHashMap 能按插入顺序迭代。Tile 的状态刷新统一派发到构造时启动的后台 HandlerThread,避免在 UI 线程上做网络等耗时操作。

3.2 Tile 的添加、移除与创建

addTile 有两个重载:addTile(String spec) 把内置 Tile 写进 Settings.Secure(触发 onTuningChanged 真正创建),addTile(ComponentName tile) 用于第三方 TileService,插入到列表最前面。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java

java 复制代码
public class QSTileHost implements QSTile.Host, Tunable {
    // ...
    private final LinkedHashMap<String, QSTile<?>> mTiles = new LinkedHashMap<>();
    protected final ArrayList<String> mTileSpecs = new ArrayList<>();

    public void addTile(String spec) {
        final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                TILES_SETTING, ActivityManager.getCurrentUser());
        final List<String> tileSpecs = loadTileSpecs(mContext, setting);
        if (tileSpecs.contains(spec)) return;
        tileSpecs.add(spec);
        // 写回 Settings.Secure → 触发 TunerService → onTuningChanged() 真正建 Tile
        Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING,
                TextUtils.join(",", tileSpecs), ActivityManager.getCurrentUser());
    }

    public void addTile(ComponentName tile) {
        List<String> newSpecs = new ArrayList<>(mTileSpecs);
        newSpecs.add(0, CustomTile.toSpec(tile));   // ← 第三方 Tile 插到最前
        changeTiles(mTileSpecs, newSpecs);
    }

    public QSTile<?> createTile(String tileSpec) {
        if (tileSpec.equals("wifi")) return new WifiTile(this);
        else if (tileSpec.equals("bt")) return new BluetoothTile(this);
        else if (tileSpec.equals("cell")) return new CellularTile(this);
        else if (tileSpec.equals("dnd")) return new DndTile(this);
        else if (tileSpec.equals("inversion")) return new ColorInversionTile(this);
        else if (tileSpec.equals("airplane")) return new AirplaneModeTile(this);
        else if (tileSpec.equals("rotation")) return new RotationLockTile(this);
        else if (tileSpec.equals("flashlight")) return new FlashlightTile(this);
        else if (tileSpec.equals("location")) return new LocationTile(this);
        else if (tileSpec.equals("cast")) return new CastTile(this);
        else if (tileSpec.equals("hotspot")) return new HotspotTile(this);
        else if (tileSpec.equals("user")) return new UserTile(this);
        else if (tileSpec.equals("battery")) return new BatteryTile(this);
        else if (tileSpec.equals("saver")) return new DataSaverTile(this);
        else if (tileSpec.equals("night")) return new NightDisplayTile(this);
        else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(this, tileSpec);
        else if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(this, tileSpec);
        else {
            Log.w(TAG, "Bad tile spec: " + tileSpec);
            return null;
        }
    }
}

关键设计createTile() 把字符串 spec("wifi""bt"......)映射到具体 Tile 类,是内置 Tile 的唯一工厂入口。addTile(String) 并不直接 new Tile,而是只改 Settings.Secure,真正的创建由 onTuningChanged() 回调统一驱动------这套"写配置→回调重建"的机制让用户在 Tuner 里拖拽调整顺序后能自动生效。


四、QSTile------Tile 基类

所有 Tile 都继承自 QSTile<TState>TState 是状态类型。基类用一个内部 HandlerH,跑在宿主提供的后台 Looper 上)把点击、刷新状态等消息串行化,避免多线程乱改状态。

4.1 QSTile 核心结构与 State

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSTile.java

java 复制代码
public abstract class QSTile<TState extends State> {
    protected final Host mHost;
    protected final Context mContext;
    protected final H mHandler;                 // 跑在宿主后台 Looper 上的 Handler
    protected TState mState = newTileState();

    // 子类必须实现的核心方法
    public abstract TState newTileState();      // 创建状态对象
    abstract protected void handleClick();      // 处理点击
    abstract protected void handleUpdateState(TState state, Object arg);  // 刷新状态
    abstract public int getMetricsCategory();   // 统计分类
    public abstract Intent getLongClickIntent(); // 长按跳转的 Intent(基类 handleLongClick() 默认用它)

    // 状态对象------描述一个 Tile 在某一时刻的外观
    public static class State {
        public Icon icon;                              // 图标(Icon 对象,不是 int 资源 id)
        public CharSequence label;                     // 主标签
        public CharSequence contentDescription;        // 无障碍描述
        public CharSequence dualLabelContentDescription;
        public CharSequence minimalContentDescription;
        public boolean autoMirrorDrawable = true;
        public boolean disabledByPolicy;              // 是否被策略禁用(如 MDM 限制)
        public EnforcedAdmin enforcedAdmin;
        public String minimalAccessibilityClassName;
        public String expandedAccessibilityClassName;

        public boolean copyTo(State other) { /* 比较字段是否变化,变了则复制并返回 true */ }
    }
}

关键设计State 里没有 value/enabled/visible 这类字段,也没有 int iconId------图标是一个 Icon 对象(ResourceIcon / DrawableIcon 等实现),开关值则由子类继承 BooleanState(多出一个 boolean value 字段)来表达。这正是很多旧文章把 QS Tile 写错的地方。

4.2 状态刷新与点击的分发

点击和刷新都不会在调用线程直接执行,而是先 sendToTarget() 派发到 Tile 自己的后台 Looper,由内部 Handler H 统一处理。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSTile.java

java 复制代码
public abstract class QSTile<TState extends State> {
    // ...
    protected TState mState = newTileState();
    private TState mTmpState = newTileState();
    private final ArrayList<Callback> mCallbacks = new ArrayList<>();

    public void click() {
        mHandler.sendEmptyMessage(H.CLICK);                 // ← 点击消息化
    }

    protected final void refreshState(Object arg) {
        mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
    }

    // 以下都在 Tile 后台 Looper 线程执行
    protected void handleRefreshState(Object arg) {
        handleUpdateState(mTmpState, arg);                  // ← 子类计算新状态
        final boolean changed = mTmpState.copyTo(mState);   // ← 比较并写回
        if (changed) handleStateChanged();
    }

    protected void handleLongClick() {
        MetricsLogger.action(mContext, MetricsEvent.ACTION_QS_LONG_PRESS, getTileSpec());
        mHost.startActivityDismissingKeyguard(getLongClickIntent());
    }

    protected final class H extends Handler {
        // ...
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == CLICK) {
                if (mState.disabledByPolicy) {
                    // 被策略禁用 → 跳到管理后台说明
                } else {
                    mAnnounceNextStateChange = true;
                    handleClick();                          // ← 子类处理点击
                }
            } else if (msg.what == REFRESH_STATE) {
                handleRefreshState(msg.obj);
            }
            // ... LONG_CLICK / SHOW_DETAIL / USER_SWITCH / DESTROY ...
        }
    }
}

关键设计refreshState() 先用 mTmpState 算新值、再 copyTo() 比较,只有真变了才回调 onStateChanged 通知视图重绘------这个"双状态对象 + 脏检查"的写法避免了无意义的 UI 刷新。点击时若 disabledByPolicy 为真(被 MDM/策略禁用),不会执行子类的 handleClick(),而是直接跳到管理后台。

4.3 一个 Tile 实现长什么样(示例)

真实的 WifiTile 依赖 WifiSignalController 等一大套逻辑,篇幅较长,这里用一个示例代码 说明一个开关类 Tile 的最小骨架(BooleanState 子类需要实现 handleUpdateStatehandleClickgetLongClickIntentgetMetricsCategory)。

java 复制代码
// ★ 示例代码(非 AOSP 源码,仅演示 Tile 骨架)
public class ScreenOnTile extends QSTile<QSTile.BooleanState> {
    public ScreenOnTile(QSTile.Host host) { super(host); }

    @Override
    public BooleanState newTileState() {
        return new BooleanState();
    }

    @Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        state.label = "屏幕常亮";
        state.icon = ResourceIcon.get(R.drawable.ic_qs_screen_on);  // Icon 走 ResourceIcon,不是 int
        state.contentDescription = "屏幕常亮开关";
        // 用一个 boolean value 表达开关状态
        state.value = false;
    }

    @Override
    protected void handleClick() {
        // ... 翻转开关状态 ...
        refreshState(null);          // ← 触发一次状态重算 + 重绘
    }

    @Override
    public Intent getLongClickIntent() {
        return new Intent(Settings.ACTION_DISPLAY_SETTINGS);  // ← 长按跳转显示设置
    }

    @Override
    public int getMetricsCategory() {
        return MetricsEvent.QS_CUSTOM;   // 示例用自定义 Tile 的统计分类
    }
}

关键设计 :开关类 Tile 的状态用一个 boolean value 表达(继承 BooleanState),文案和图标则分别放进 State.labelState.icon(图标走 ResourceIcon.get(...))。handleUpdateState() 负责"算状态",handleClick() 负责"改状态后触发重算",getLongClickIntent() 负责"长按去哪"------三者职责清晰。


五、QSPanel------面板视图

5.1 QSPanel 的装配

QSPanelextends LinearLayout)是网格的宿主,它实现 TunableQSTile.Host.Callback。面板 inflate 后,QSPanel 会遍历 QSTileHost.getTiles(),为每个 Tile 通过 tile.createTileView() 生成视图,再交给 TileLayout 摆放。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java

java 复制代码
public class QSPanel extends LinearLayout implements Tunable, Callback {
    protected final Context mContext;
    protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
    protected QSTileHost mHost;

    // 宿主回调:Tile 集合变化后重建网格
    @Override
    public void onTilesChanged() {
        setTiles(mHost.getTiles());
    }

    public void setTiles(Collection<QSTile<?>> tiles, boolean collapsedView) {
        for (TileRecord record : mRecords) {          // ← 先拆旧网格
            mTileLayout.removeTile(record);
            record.tile.removeCallback(record.callback);
        }
        mRecords.clear();
        for (QSTile<?> tile : tiles) {
            addTile(tile, collapsedView);              // ← 为每个 Tile 建视图 + 回调
        }
    }

    protected void addTile(final QSTile<?> tile, boolean collapsedView) {
        final TileRecord r = new TileRecord();
        r.tile = tile;
        r.tileView = createTileView(tile, collapsedView);
        final QSTile.Callback callback = new QSTile.Callback() {
            @Override
            public void onStateChanged(QSTile.State state) {
                drawTile(r, state);                    // ← 状态变了就重绘该 Tile
            }
            // ... onShowDetail / onToggleStateChanged / onScanStateChanged ...
        };
        r.tile.addCallback(callback);
        r.callback = callback;
        // ... 加点击监听、把 r 放进 mRecords、addView 到布局 ...
    }
}

关键设计QSPanel 自己不关心 Tile 怎么排版------它只负责"为每个 Tile 建一个视图、挂一个 QSTile.Callback、塞进布局"。状态一变,回调里的 drawTile() 只重绘那一个 Tile;真正的网格计算交给 TileLayout,这也是 TileLayout / QuickTileLayout / PagedTileLayout 可以互换(分别对应网格、单列、分页)的原因。

5.2 Tile 的网格布局

TileLayout 是网格的核心,列数不是写死的常量,而是从资源 R.integer.quick_settings_num_columns 读出来。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java

java 复制代码
public class TileLayout extends ViewGroup implements QSTileLayout {
    protected int mColumns;
    protected int mCellWidth;
    protected int mCellHeight;
    protected final ArrayList<TileRecord> mRecords = new ArrayList<>();

    public void updateResources() {
        final Resources res = mContext.getResources();
        // ★ 列数来自资源,竖屏 3 列、横屏/宽屏 4 列(见 config.xml / dimens.xml)
        final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
        mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height);
        mCellMargin = res.getDimensionPixelSize(R.dimen.qs_tile_margin);
        if (mColumns != columns) {
            mColumns = columns;
            requestLayout();
            return true;
        }
        return false;
    }
}

关键设计 :列数走资源而不是常量,因此竖屏(values/config.xml,3 列)和横屏/宽屏(values-land/dimens.xml,4 列)能自动切换,改列数只需改资源,不用动 Java 代码。

5.3 QSTileView------Tile 视图

QSTileViewextends QSTileBaseView)是单个 Tile 的视图。它和早期文档里"简单 ImageView + TextView + 触摸监听"的想象不同,实际是 QSTileBaseView 里基于 TouchAnimator 的一套触摸/按压状态机来处理的,点击、长按、按压动画都由基类统一驱动。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSTileView.java

java 复制代码
public class QSTileView extends QSTileBaseView {
    protected final Context mContext;
    protected TextView mLabel;

    public QSTileView(Context context, QSIconView icon, boolean collapsedView) {
        super(context, icon, collapsedView);
        mContext = context;
        setClipChildren(false);
        setClickable(true);
        createLabel();
        setOrientation(VERTICAL);
        setGravity(Gravity.CENTER);
        // ... 尺寸/间距相关初始化 ...
    }
}

关键设计QSTileView 把触摸和按压动画下沉到 QSTileBaseView(TouchAnimator 风格状态机),自己只负责"标签 + 图标"的摆放。点击事件最终通过 QSTile.Callback 派发到 Tile 的 click(),再走第四节的后台 Looper 分发链。


六、TunerService------用户自定义 Tile 配置

AOSP7 里用户通过 SystemUI Tuner 调整 QS Tile 列表,走的不是私有 API,而是 TunerServiceextends SystemUI,随 SystemUI 一起启动)。它把 Settings.Secure 里的一组键注册成 ContentObserver,键一变就回调所有监听它的 TunableQSTileHost 正是这样监听 QS_TILES 键的。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java

java 复制代码
public class TunerService extends SystemUI {
    private final ArrayMap<Uri, String> mListeningUris = new ArrayMap<>();
    private final HashMap<String, Set<Tunable>> mTunableLookup = new HashMap<>();

    private void addTunable(Tunable tunable, String key) {
        if (!mTunableLookup.containsKey(key)) {
            mTunableLookup.put(key, new ArraySet<Tunable>());
        }
        mTunableLookup.get(key).add(tunable);
        Uri uri = Settings.Secure.getUriFor(key);
        if (!mListeningUris.containsKey(uri)) {
            mListeningUris.put(uri, key);
            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
        }
        // 注册后立即发一次当前值
        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
        tunable.onTuningChanged(key, value);
    }

    public void reloadSetting(Uri uri) {
        String key = mListeningUris.get(uri);
        Set<Tunable> tunables = mTunableLookup.get(key);
        if (tunables == null) return;
        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
        for (Tunable tunable : tunables) {
            tunable.onTuningChanged(key, value);     // ← 回调监听者
        }
    }
}

关键设计TunerService 是"键 → 监听者集合"的注册中心,注册时立刻回灌一次当前值(保证冷启动不丢状态)。QSTileHost.onTuningChanged() 收到 QS_TILES 的变化后,会对比新旧 spec 列表,销毁多余的、复用或新建剩下的,最后回调 onTilesChanged() 让面板重建。


七、亮度调节

QS 展开区域里的亮度滑块是 com.android.systemui.settings.BrightnessSlider,它挂在 QS 展开区域(quick_tile_layout)里,而不是 qs_panel.xml 中。面板展开时滑块的当前亮度由 SystemBar 侧的 BrightnessController / BrightnessDialog 负责读取与回写 Settings.System.SCREEN_BRIGHTNESSQSPanel.setBrightnessMirror(...) 接的 BrightnessMirrorController 则负责在状态栏上"镜像"显示这条滑块的可见性动画(showMirror() / hideMirror())。

关键设计 :亮度这条链路里 BrightnessSlider 负责"画和取值",BrightnessController 负责"写回系统设置",BrightnessMirrorController 只负责"在状态栏顶部镜像滑块的显隐"------三者各司其职,不要把滑块逻辑和镜像动画混为一谈。


八、定制开发提示

8.1 新增自定义 Tile

步骤 说明
继承基类 extends QSTile<QSTile.BooleanState>(开关类)或自定义 State 子类
实现抽象方法 newTileState() / handleClick() / handleUpdateState() / getMetricsCategory() / getLongClickIntent()
注册 spec QSTileHost.createTile() 的 if/else 链里加一条映射,并在默认 spec 字符串中追加

关键 :AOSP7 的 Tile 列表是数据驱动的,新 Tile 必须同时改两处------createTile() 的映射和默认 spec(R.string.quick_settings_tiles / quick_settings_tiles_default),缺一不可。

8.2 修改 Tile 默认排列顺序

默认顺序由 R.string.quick_settings_tiles(可含 default 占位)和 R.string.quick_settings_tiles_default 两个字符串资源决定,loadTileSpecs() 负责把 default 展开成实际 spec。改这两个字符串就能调整出厂顺序。

8.3 修改 Tile 网格列数

列数不是 Java 常量,而是资源 R.integer.quick_settings_num_columns

xml 复制代码
<!-- res/values/config.xml:竖屏默认 3 列 -->
<integer name="quick_settings_num_columns">3</integer>
xml 复制代码
<!-- res/values-land/dimens.xml:横屏 4 列(宽屏 values-w550dp-land 同理) -->
<integer name="quick_settings_num_columns">4</integer>

关键设计 :要改列数改资源,别去找什么 NUM_COLUMNS 常量------TileLayout.updateResources() 每次只从 R.integer.quick_settings_num_columns 读值。

8.4 移除内置 Tile

在默认 spec 字符串(R.string.quick_settings_tiles / quick_settings_tiles_default)里去掉对应的 spec(如 wifibt)即可,不需要去动 createTile()------后者只是"spec → 类"的映射表,留着无害。

8.5 常见踩坑点

现象 排查方向
Tile 不显示 确认 spec 已在默认列表里,且 isAvailable() 返回 true
点击无反应 确认 handleClick() 里改了状态后调用了 refreshState()
自定义 Tile 不生效 确认 createTile() 映射 + 默认 spec 两处都改了
权限问题 写系统设置需要 WRITE_SETTINGS,SystemUI 以 system UID 运行,天然具备

九、小结

本文拆解了 QuickSettings 面板的完整链路:

模块 角色
QSTileHost Tile 宿主,按 spec 创建/回收 Tile,监听 TunerService 调参
QSTile<TState> Tile 基类,后台 Looper 串行化状态刷新,State 描述外观
QSPanel / QSTileView 面板视图与单个 Tile 视图(TouchAnimator 触摸)
TileLayout 网格布局,列数走 R.integer.quick_settings_num_columns
TunerService 用户调参中心,QS_TILES 变化驱动 Tile 重建
BrightnessSlider 亮度滑块,与 BrightnessController / BrightnessMirrorController 分工协作

下一篇文章将分析 Keyguard 锁屏模块,包括 KeyguardViewMediator 的调度逻辑、安全锁屏与滑动锁屏的创建,以及锁屏与状态栏的交互机制。


下一篇:AOSP7 SystemUI 源码解析(七):Keyguard 锁屏模块深度解析

相关推荐
Kapaseker1 小时前
你有搞明白 Volatile 什么意思吗?
android·kotlin
美狐美颜SDK开放平台1 小时前
直播APP源码与视频美颜sdk如何配合?一套完整开发思路
android·人工智能·计算机视觉·音视频·直播美颜sdk
心平气和量大福大1 小时前
android-权限
android·java
2501_916008892 小时前
怎么用 Godot 导出 iOS 应用并上架 App Store?签名字段该填什么?
android·ios·小程序·https·uni-app·iphone·webview
智慧物业老杨4 小时前
物业日常巡查的数智化重构:从“打卡式巡检“到“闭环式风控“
android·java·人工智能·系统架构·rxjava
alexhilton10 小时前
藏在设备上的秘密,终究藏不住
android·kotlin·android jetpack
hai_android16 小时前
Kotlin / Android 常用函数使用示例手册
android·java·kotlin
hai_android17 小时前
Android MeasureSpec 详解
android·java·kotlin
又见情义19 小时前
RK3568 Android 13 本地 U 盘 OTA 升级实战:基于 RKUpdateService 的完整流程
android