1、QS创建
QSPanel 创建是从 CentralSurfacesImpl#makeStatusBarView 开始的,Qs面板创建这块,与之前版本对比,没啥变化。
com.android.systemui.statusbar.phone.CentralSurfacesImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| protected void makeStatusBarView() { ...... final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame); if (container != null) { FragmentHostManager fragmentHostManager = FragmentHostManager.get(container); ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame, mExtensionController .newExtension(QS.class) .withPlugin(QS.class) .withDefault(this::createDefaultQSFragment) .build()); mBrightnessMirrorController = new BrightnessMirrorController( mNotificationShadeWindowView, mNotificationPanelViewController, mNotificationShadeDepthControllerLazy.get(), mBrightnessSliderFactory, (visible) -> { mBrightnessMirrorVisible = visible; updateScrimController(); }); fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> { QS qs = (QS) f; if (qs instanceof QSFragment) { mQSPanelController = ((QSFragment) qs).getQSPanelController(); ((QSFragment) qs).setBrightnessMirrorController(mBrightnessMirrorController); } }); } ...... }
|
接下来就先看看 QSFragment 的 onCreateView() 方法:
com.android.systemui.qs.QSFragment.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ...... @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { try { Trace.beginSection("QSFragment#onCreateView"); inflater = inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.Theme_SystemUI_QuickSettings)); return inflater.inflate(R.layout.qs_panel, container, false); } finally { Trace.endSection(); } } ......
|
再看 QSFragment 的构造函数:
com.android.systemui.qs.QSFragment.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ...... @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, QSTileHost qsTileHost, StatusBarStateController statusBarStateController, CommandQueue commandQueue, @Named(QS_PANEL) MediaHost qsMediaHost, @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, KeyguardBypassController keyguardBypassController, QSFragmentComponent.Factory qsComponentFactory, QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger, FalsingManager falsingManager, DumpManager dumpManager) { ...... mHost = qsTileHost; ...... } ......
|
这里注意 @Inject 注解,这个是 Android dagger里的一种注解。
在这里,与Android 9.0及其以下版本实例化 QSTileHost类的方式不一样,这里是通dagger来实例化的。所以这里实例化了QSTileHost 。
下面我们就进入到 QSTileHost 的构造方法:
com.android.systemui.qs.QSTileHost.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ...... @Inject public QSTileHost(Context context, StatusBarIconController iconController, QSFactory defaultFactory, @Main Handler mainHandler, @Background Looper bgLooper, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, Optional<CentralSurfaces> centralSurfacesOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory ) { ...... mainHandler.post(() -> { tunerService.addTunable(this, TILES_SETTING); mAutoTiles = autoTiles.get(); mTileServiceRequestController.init(); }); } ......
|
在 QSTileHost 的构造函数里,我们主要看 tunerService.addTunable(this, TILES_SETTING); 很明显,调用 tunerService 里的 addTunabe() 方法,跟进去会发现,最终的是调用的 TunerServiceImpl 里面的 addTunabe() 方法。
com.android.systemui.tuner.TunerServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ...... public void addTunable(Tunable tunable, String... keys) { for (String key : keys) { addTunable(tunable, key); } }
private void addTunable(Tunable tunable, String key) { ...... String value = DejankUtils.whitelistIpcs(() -> Settings.Secure .getStringForUser(mContentResolver, key, mCurrentUser)); tunable.onTuningChanged(key, value); } ......
|
tunable.onTuningChanged() 回调 QSTileHost#onTuningChanged():
com.android.systemui.qs.QSTileHost.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @Override public void onTuningChanged(String key, String newValue) { if (!TILES_SETTING.equals(key)) { return; } if (DEBUG) Log.d(TAG, "Recreating tiles"); if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); } final List<String> tileSpecs = loadTileSpecs(mContext, newValue); int currentUser = ActivityManager.getCurrentUser(); if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach( tile -> { if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); tile.getValue().destroy(); }); final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>(); for (String tileSpec : tileSpecs) { QSTile tile = mTiles.get(tileSpec); if (tile != null && (!(tile instanceof CustomTile) || ((CustomTile) tile).getUser() == currentUser)) { if (tile.isAvailable()) { if (DEBUG) Log.d(TAG, "Adding " + tile); tile.removeCallbacks(); if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) { tile.userSwitch(currentUser); } newTiles.put(tileSpec, tile); } else { tile.destroy(); } } else { if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec); try { tile = createTile(tileSpec); if (tile != null) { if (tile.isAvailable()) { tile.setTileSpec(tileSpec); newTiles.put(tileSpec, tile); } else { tile.destroy(); } } } catch (Throwable t) { Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); } } } mCurrentUser = currentUser; mTileSpecs.clear(); mTileSpecs.addAll(tileSpecs); mTiles.clear(); mTiles.putAll(newTiles); for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onTilesChanged(); } }
|
这里有两个重要的方法:一个是获取 config 里字符串信息 loadTileSpecs(mContext, newValue);一个实例化 Tile 的 createTile(tileSpec)。
先看第一个 QSTileHost#loadTileSpecs() 这里和Android 10 有点出入。
com.android.systemui.qs.QSTileHost.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| protected static List<String> loadTileSpecs(Context context, String tileList) { final Resources res = context.getResources(); if (TextUtils.isEmpty(tileList)) { tileList = res.getString(R.string.quick_settings_tiles); if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); } else { if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); } final ArrayList<String> tiles = new ArrayList<String>(); boolean addedDefault = false; Set<String> addedSpecs = new ArraySet<>(); for (String tile : tileList.split(",")) { tile = tile.trim(); if (tile.isEmpty()) continue; if (tile.equals("default")) { if (!addedDefault) { List<String> defaultSpecs = getDefaultSpecs(context); for (String spec : defaultSpecs) { if (!addedSpecs.contains(spec)) { tiles.add(spec); addedSpecs.add(spec); } } addedDefault = true; } } else { if (!addedSpecs.contains(tile)) { tiles.add(tile); addedSpecs.add(tile); } } } return tiles; }
|
上述代码中第一次 tileList 为空,调用了 getDefaultSpecs(context) 获取字符串,该方法比较简单,这里就不做分析了。
接着看第二个 QSTileHost#createTile(tileSpec) 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public QSTile createTile(String tileSpec) { for (int i = 0; i < mQsFactories.size(); i++) { QSTile t = mQsFactories.get(i).createTile(tileSpec); if (t != null) { return t; } } if (mQuickSettingsExt != null && mQuickSettingsExt.doOperatorSupportTile(tileSpec)) { return (QSTile) mQuickSettingsExt.createTile(this, tileSpec); } return null; }
|
这里调用 QSFactory#createTile(),而 QSFactory 接口又由 QSFactoryImpl 实现。所以这里直接看 QSFactoryImpl #createTile():
com.android.systemui.qs.tileimpl.QSFactoryImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public QSTile createTile(String tileSpec) { QSTileImpl tile = createTileInternal(tileSpec); if (tile != null) { tile.handleStale(); } return tile; } private QSTileImpl createTileInternal(String tileSpec) { switch (tileSpec) { case "wifi": return mWifiTileProvider.get(); case "internet": return mInternetTileProvider.get(); case "bt": return mBluetoothTileProvider.get(); case "cell": return mCellularTileProvider.get(); case "dnd": return mDndTileProvider.get(); case "inversion": return mColorInversionTileProvider.get(); case "airplane": return mAirplaneModeTileProvider.get(); case "work": return mWorkModeTileProvider.get(); case "rotation": return mRotationLockTileProvider.get(); case "flashlight": return mFlashlightTileProvider.get(); case "location": return mLocationTileProvider.get(); case "cast": return mCastTileProvider.get(); case "hotspot": return mHotspotTileProvider.get(); case "battery": return mBatterySaverTileProvider.get(); case "saver": return mDataSaverTileProvider.get(); case "night": return mNightDisplayTileProvider.get(); case "nfc": return mNfcTileProvider.get(); case "dark": return mUiModeNightTileProvider.get(); case "screenrecord": return mScreenRecordTileProvider.get(); } return null; }
|
看到这里通过对应的字符串分别实例化了对应的 Tile。
2、QS显示
以上涉及资源文件加载及对应实例化,接下来看看如何显示出来的。和Android 11 对比出入有点大,加了一个控制器。
这里要回到 QSFragment#onViewCreated() 方法:
com.android.systemui.qs.QSFragment.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this); mQSPanelController = qsFragmentComponent.getQSPanelController(); mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController(); mQSFooterActionController = qsFragmentComponent.getQSFooterActionController(); mQSPanelController.init(); mQuickQSPanelController.init(); mQSFooterActionController.init(); mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view); }
|
经过上述分析,我们来看看 ViewController#init():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void init() { if (mInited) { return; } onInit(); mInited = true; if (isAttachedToWindow()) { mOnAttachStateListener.onViewAttachedToWindow(mView); } addOnAttachStateChangeListener(mOnAttachStateListener); }
private OnAttachStateChangeListener mOnAttachStateListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { ViewController.this.onViewAttached(); } @Override public void onViewDetachedFromWindow(View v) { ViewController.this.onViewDetached(); } };
|
这个添加在 QSPanelController 的父类 QSPanelControllerBase 中的 onViewAttached() 方法中。
QSPanelControllerBase#onViewAttached()
com.android.systemui.qs.QSPanelControllerBase.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @Override protected void onViewAttached() { mQsTileRevealController = createTileRevealController(); if (mQsTileRevealController != null) { mQsTileRevealController.setExpansion(mRevealExpansion); } mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); mHost.addCallback(mQSHostCallback); setTiles(); mLastOrientation = getResources().getConfiguration().orientation; switchTileLayout(true); mDumpManager.registerDumpable(mView.getDumpableTag(), this); }
public void setTiles() { setTiles(mHost.getTiles(), false); }
public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { if (!collapsedView && mQsTileRevealController != null) { mQsTileRevealController.updateRevealedTiles(tiles); } for (QSPanelControllerBase.TileRecord record : mRecords) { mView.removeTile(record); record.tile.removeCallback(record.callback); } mRecords.clear(); mCachedSpecs = ""; for (QSTile tile : tiles) { addTile(tile, collapsedView); } } private void addTile(final QSTile tile, boolean collapsedView) { final TileRecord r = new TileRecord(tile, mHost.createTileView(getContext(), tile, collapsedView)); mView.addTile(r); mRecords.add(r); mCachedSpecs = getTilesSpecs(); }
|
这里只需关注QSPanel#addTile():
com.android.systemui.qs.QSPanel.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void addTile(QSPanelControllerBase.TileRecord tileRecord) { final QSTile.Callback callback = new QSTile.Callback() { @Override public void onStateChanged(QSTile.State state) { drawTile(tileRecord, state); } }; tileRecord.tile.addCallback(callback); tileRecord.callback = callback; tileRecord.tileView.init(tileRecord.tile); tileRecord.tile.refreshState(); if (mTileLayout != null) { mTileLayout.addTile(tileRecord); } }
|
由 TileLayout#addTile() 实现:
com.android.systemui.qs.TileLayout.java
1 2 3 4 5 6 7 8 9
| public void addTile(TileRecord tile) { mRecords.add(tile); tile.tile.setListening(this, mListening); addTileView(tile); } protected void addTileView(TileRecord tile) { addView(tile.tileView); }
|
至此,SystemUI 下拉状态栏快捷开关模块代码流程分析完毕。
QS一个有3种呈现方式,如图:

我这分析的是第 2 种。其他的展示方法也类似。
res/layout/qs_panel.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <com.android.systemui.qs.QSContainerImpl xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/quick_settings_container" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:clipChildren="false"> <com.android.systemui.qs.NonInterceptingScrollView android:id="@+id/expanded_qs_scroll_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:elevation="@dimen/qs_panel_elevation" android:importantForAccessibility="no" android:scrollbars="none" android:clipChildren="false" android:clipToPadding="false" android:layout_weight="1"> <com.android.systemui.qs.QSPanel android:id="@+id/quick_settings_panel" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/transparent" android:focusable="true" android:accessibilityTraversalBefore="@android:id/edit" android:clipToPadding="false" android:clipChildren="false"> <include layout="@layout/qs_footer_impl" /> </com.android.systemui.qs.QSPanel> </com.android.systemui.qs.NonInterceptingScrollView> <include layout="@layout/quick_status_bar_expanded_header" /> <include layout="@layout/footer_actions" android:id="@+id/qs_footer_actions" android:layout_height="@dimen/footer_actions_height" android:layout_width="match_parent" android:layout_gravity="bottom" /> <include android:id="@+id/qs_customize" layout="@layout/qs_customize_panel" android:visibility="gone" /> </com.android.systemui.qs.QSContainerImpl>
|
本文链接:
http://longzhiye.top/2023/05/22/2023-05-22/