系列导航 :一:全景概览与架构设计 | 二:启动流程深度解析 | 三: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里的KeyguardMonitor、SecurityController等控制器,放在状态栏包里就近取用这些依赖,比放在纯视图包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);
}
}
关键设计 :
mTiles用LinkedHashMap而非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)并不直接newTile,而是只改Settings.Secure,真正的创建由onTuningChanged()回调统一驱动------这套"写配置→回调重建"的机制让用户在 Tuner 里拖拽调整顺序后能自动生效。
四、QSTile------Tile 基类
所有 Tile 都继承自 QSTile<TState>,TState 是状态类型。基类用一个内部 Handler(H,跑在宿主提供的后台 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 子类需要实现 handleUpdateState、handleClick、getLongClickIntent、getMetricsCategory)。
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.label和State.icon(图标走ResourceIcon.get(...))。handleUpdateState()负责"算状态",handleClick()负责"改状态后触发重算",getLongClickIntent()负责"长按去哪"------三者职责清晰。
五、QSPanel------面板视图
5.1 QSPanel 的装配
QSPanel(extends LinearLayout)是网格的宿主,它实现 Tunable 和 QSTile.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 视图
QSTileView(extends 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,而是 TunerService(extends SystemUI,随 SystemUI 一起启动)。它把 Settings.Secure 里的一组键注册成 ContentObserver,键一变就回调所有监听它的 Tunable。QSTileHost 正是这样监听 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_BRIGHTNESS,QSPanel.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(如 wifi、bt)即可,不需要去动 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 锁屏模块深度解析