Android13下拉状态栏QS面板的加载流程解析
1、QS创建
QSPanel 创建是从 CentralSurfacesImpl#makeStatusBarView 开始的,Qs面板创建这块,与之前版本对比,没啥变化。
com.android.systemui.statusbar.phone.CentralSurfacesImpl.java
protected void makeStatusBarView() {......// 设置快速设置面板// R.id.qs_frame 是一个 FrameLayout 布局,将 QSFragment 布局添加到其中。所以 R.id.qs_frame 最终显示的是 QSFragment 。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
......@Overridepublic 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));// 在这里返回了布局,R.layout.qs_panelreturn inflater.inflate(R.layout.qs_panel, container, false);} finally {Trace.endSection();}}......
再看 QSFragment 的构造函数:
com.android.systemui.qs.QSFragment.java
......@Injectpublic 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
......@Injectpublic 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(() -> {// This is technically a hack to avoid circular dependency of// QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation// 在创建任何图块之前完成。tunerService.addTunable(this, TILES_SETTING);// AutoTileManager 可以修改 mTiles,因此请确保 mTiles 已经初始化。mAutoTiles = autoTiles.get();mTileServiceRequestController.init();});}......
在 QSTileHost 的构造函数里,我们主要看 tunerService.addTunable(this, TILES_SETTING); 很明显,调用 tunerService 里的 addTunabe() 方法,跟进去会发现,最终的是调用的 TunerServiceImpl 里面的 addTunabe() 方法。
com.android.systemui.tuner.TunerServiceImpl.java
......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
@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);}//调用 QSTileHost#loadTileSpecs,获得 config 里字符串信息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 {//这里通过 字符串 一个个实例化 Tiletile = createTile(tileSpec);if (tile != null) {if (tile.isAvailable()) {tile.setTileSpec(tileSpec);// put 到 Map 中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();// put 到 Map 中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
protected static List<String> loadTileSpecs(Context context, String tileList) {final Resources res = context.getResources();// tileList 为空,则获取一个 “default” 字符串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;// 第一次 tileList 为空,取了默认值,if (tile.equals("default")) {if (!addedDefault) {// 从 config 文件获取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) 方法:
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;}}// M: @ {if (mQuickSettingsExt != null && mQuickSettingsExt.doOperatorSupportTile(tileSpec)) {// WifiCallingreturn (QSTile) mQuickSettingsExt.createTile(this, tileSpec);}// @ }return null;}
这里调用 QSFactory#createTile(),而 QSFactory 接口又由 QSFactoryImpl 实现。所以这里直接看 QSFactoryImpl #createTile():
com.android.systemui.qs.tileimpl.QSFactoryImpl.java
public QSTile createTile(String tileSpec) {QSTileImpl tile = createTileInternal(tileSpec);if (tile != null) {tile.handleStale(); // Tile was just created, must be stale.}return tile;
}
private QSTileImpl createTileInternal(String tileSpec) {// 省略其他代码......// Stock tiles.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
@Overridepublic void onViewCreated(View view, @Nullable Bundle savedInstanceState) {QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);mQSPanelController = qsFragmentComponent.getQSPanelController();mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController();mQSFooterActionController = qsFragmentComponent.getQSFooterActionController();// 一些初始化,init() 是 抽象类 ViewController 的 public 方法。mQSPanelController.init();mQuickQSPanelController.init();mQSFooterActionController.init();// 扩展的 qs 滚动视图mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view); // 省略其他代码......}
经过上述分析,我们来看看 ViewController#init():
public void init() {if (mInited) {return;}onInit(); // 要在 onViewAttached() 方法之前运行,mInited = true;if (isAttachedToWindow()) {// 调用内部 onViewAttachedToWindow() 方法,去添加视图。mOnAttachStateListener.onViewAttachedToWindow(mView);}addOnAttachStateChangeListener(mOnAttachStateListener);
}private OnAttachStateChangeListener mOnAttachStateListener = new OnAttachStateChangeListener() {@Overridepublic void onViewAttachedToWindow(View v) {// 调用自己的抽象方法 onViewAttached() ,在子类具体实现,添加 Tiles.ViewController.this.onViewAttached();}@Overridepublic void onViewDetachedFromWindow(View v) {ViewController.this.onViewDetached();}
};
这个添加在 QSPanelController 的父类 QSPanelControllerBase 中的 onViewAttached() 方法中。
QSPanelControllerBase#onViewAttached()
com.android.systemui.qs.QSPanelControllerBase.java
@Override
protected void onViewAttached() {mQsTileRevealController = createTileRevealController();if (mQsTileRevealController != null) {mQsTileRevealController.setExpansion(mRevealExpansion);}mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener);mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);mHost.addCallback(mQSHostCallback);// 这里设置 TilessetTiles();mLastOrientation = getResources().getConfiguration().orientation;switchTileLayout(true);mDumpManager.registerDumpable(mView.getDumpableTag(), this);
}
/** */
public void setTiles() {// 这里 getTiles() 就是获取,前面我们说的 Tiles 实例,在 QSTileHost 中。setTiles(mHost.getTiles(), false);
}
/** */
public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {// TODO(b/168904199): move this logic into QSPanelController.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 是 QSPanel,在 QSPanelController 的构造方法通过super传到 QSPanelControllerBase 的。这里也是视图的添加。mView.addTile(r);mRecords.add(r);mCachedSpecs = getTilesSpecs();
}
这里只需关注QSPanel#addTile():
com.android.systemui.qs.QSPanel.java
void addTile(QSPanelControllerBase.TileRecord tileRecord) {final QSTile.Callback callback = new QSTile.Callback() {@Overridepublic 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
public void addTile(TileRecord tile) {mRecords.add(tile);tile.tile.setListening(this, mListening);addTileView(tile);}protected void addTileView(TileRecord tile) {// 注:TileLayout 继承的是 ViewGroup。addView(tile.tileView);}
至此,SystemUI 下拉状态栏快捷开关模块代码流程分析完毕。
QS一个有3种呈现方式,如图:
我这分析的是第 2 种。其他的展示方法也类似。
res/layout/qs_panel.xml
<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.NonInterceptingScrollViewandroid: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.QSPanelandroid: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" /><includelayout="@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"/><!-- 第三种布局 --><includeandroid:id="@+id/qs_customize"layout="@layout/qs_customize_panel"android:visibility="gone" />
</com.android.systemui.qs.QSContainerImpl>