SystemUI 下拉状态栏快捷开关是 QSPanel,qs_panel.xml,@+id/quick_settings_panel,本篇文章就来看看这些快捷开关是如何呈现的以及如何新增一个快捷开关?基于 AOSP 9.0 分析。
QSPanel 创建是从 StatusBar#makeStatusBarView 开始的。
protected void makeStatusBarView() { //省略其他代码 final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this, mIconController); mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow, (visible) -> { mBrightnessMirrorVisible = visible; updateScrimController(); }); fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> { QS qs = (QS) f; if (qs instanceof QSFragment) { ((QSFragment) qs).setHost(qsh); mQSPanel = ((QSFragment) qs).getQsPanel(); mQSPanel.setBrightnessMirror(mBrightnessMirrorController); mKeyguardStatusBar.setQSPanel(mQSPanel); } }); //省略其他代码 }
先看 SystemUIFactory#createQSTileHost。
public QSTileHost createQSTileHost(Context context, StatusBar statusBar, StatusBarIconController iconController) { return new QSTileHost(context, statusBar, iconController); }
这里进行 QSTileHost 初始化。
public QSTileHost(Context context, StatusBar statusBar, StatusBarIconController iconController) { //省略其他代码 Dependency.get(TunerService.class).addTunable(this, TILES_SETTING); //省略其他代码 }
这里进行了 TunerService 注册,在 TunerServiceImpl#addTunable 重写。
@Override public void addTunable(Tunable tunable, String... keys) { for (String key : keys) { addTunable(tunable, key); } } private void addTunable(Tunable tunable, String key) { if (!mTunableLookup.containsKey(key)) { mTunableLookup.put(key, new ArraySet<Tunable>()); } mTunableLookup.get(key).add(tunable); if (LeakDetector.ENABLED) { mTunables.add(tunable); Dependency.get(LeakDetector.class).trackCollection(mTunables, "TunerService.mTunables"); } Uri uri = Settings.Secure.getUriFor(key); if (!mListeningUris.containsKey(uri)) { mListeningUris.put(uri, key); mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser); } // Send the first state. String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser); tunable.onTuningChanged(key, value); }
tunable.onTuningChanged
回调 QSTileHost#onTuningChanged。
@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 { //这里通过 字符串 一个个实例化 Tile 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(); } }
看下 QSTileHost#loadTileSpecs,是获得 config 里字符串信息。
protected List<String> loadTileSpecs(Context context, String tileList) { final Resources res = context.getResources(); final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); if (tileList == null) { 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; for (String tile : tileList.split(",")) { tile = tile.trim(); if (tile.isEmpty()) continue; if (tile.equals("default")) { if (!addedDefault) { tiles.addAll(Arrays.asList(defaultTileList.split(","))); addedDefault = true; } } else { tiles.add(tile); } } return tiles; }
其中 quick_settings_tiles_default 值在 AOSP/frameworks/base/packages/SystemUI/res/values/config.xml 里:
<string name="quick_settings_tiles_default" translatable="false"> wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast </string>
这里就是我们所看到的快捷开关的文本描述。
再看 QSTileHost#onTuningChanged 中的调用 QSTileHost#createTile 方法。
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; } } return null; }
调用 QSFactory#createTile,由 QSFactoryImpl#createTile 实现了。
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 new WifiTile(mHost); case "bt": return new BluetoothTile(mHost); case "cell": return new CellularTile(mHost); case "dnd": return new DndTile(mHost); case "inversion": return new ColorInversionTile(mHost); case "airplane": return new AirplaneModeTile(mHost); case "work": return new WorkModeTile(mHost); case "rotation": return new RotationLockTile(mHost); case "flashlight": return new FlashlightTile(mHost); case "location": return new LocationTile(mHost); case "cast": return new CastTile(mHost); case "hotspot": return new HotspotTile(mHost); case "user": return new UserTile(mHost); case "battery": return new BatterySaverTile(mHost); case "saver": return new DataSaverTile(mHost); case "night": return new NightDisplayTile(mHost); case "nfc": return new NfcTile(mHost); } // Intent tiles. if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec); if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec); // Debug tiles. if (Build.IS_DEBUGGABLE) { if (tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC)) { return new GarbageMonitor.MemoryTile(mHost); } } // Broken tiles. Log.w(TAG, "Bad tile spec: " + tileSpec); return null; }
看到这里通过对应的字符串分别实例化 Tile。
以上涉及资源文件加载及对应实例化,接下来看看代码如何加载的,看 QSPanel#onAttachedToWindow 方法。
@Override protected void onAttachedToWindow() { super.onAttachedToWindow(); final TunerService tunerService = Dependency.get(TunerService.class); tunerService.addTunable(this, QS_SHOW_BRIGHTNESS); if (mHost != null) { setTiles(mHost.getTiles()); } if (mBrightnessMirrorController != null) { mBrightnessMirrorController.addCallback(this); } } public void setTiles(Collection<QSTile> tiles) { setTiles(tiles, false); } public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { if (!collapsedView) { mQsTileRevealController.updateRevealedTiles(tiles); } for (TileRecord record : mRecords) { mTileLayout.removeTile(record); record.tile.removeCallback(record.callback); } mRecords.clear(); for (QSTile tile : tiles) { addTile(tile, collapsedView); } } protected TileRecord addTile(final QSTile tile, boolean collapsedView) { final TileRecord r = new TileRecord(); r.tile = tile; r.tileView = createTileView(tile, collapsedView); //省略其他代码 r.tileView.init(r.tile); r.tile.refreshState(); mRecords.add(r); if (mTileLayout != null) { mTileLayout.addTile(r); } return r; }
mTileLayout.addTile(r);由 PagedTileLayout#addTile 实现。
PagedTileLayout 是 ViewPager,重点看 setAdapter,看数据源如何 add 的。
@Override public void addTile(TileRecord tile) { mTiles.add(tile); postDistributeTiles(); } private void postDistributeTiles() { removeCallbacks(mDistribute); post(mDistribute); } private final Runnable mDistribute = new Runnable() { @Override public void run() { distributeTiles(); } }; private void distributeTiles() { if (DEBUG) Log.d(TAG, "Distributing tiles"); final int NP = mPages.size(); for (int i = 0; i < NP; i++) { mPages.get(i).removeAllViews(); } int index = 0; final int NT = mTiles.size(); for (int i = 0; i < NT; i++) { TileRecord tile = mTiles.get(i); if (mPages.get(index).isFull()) { if (++index == mPages.size()) { if (DEBUG) Log.d(TAG, "Adding page for " + tile.tile.getClass().getSimpleName()); mPages.add((TilePage) LayoutInflater.from(getContext()) .inflate(R.layout.qs_paged_page, this, false)); } } if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to " + index); mPages.get(index).addTile(tile); } if (mNumPages != index + 1) { mNumPages = index + 1; while (mPages.size() > mNumPages) { mPages.remove(mPages.size() - 1); } if (DEBUG) Log.d(TAG, "Size: " + mNumPages); mPageIndicator.setNumPages(mNumPages); setAdapter(mAdapter); mAdapter.notifyDataSetChanged(); setCurrentItem(0, false); } }
至此,SystemUI 下拉状态栏快捷开关模块代码流程分析完毕。
0、国际惯例,先上效果图,新增一个Camera,随便用了蓝牙的图标:
1、首先在 AOSP/frameworks/base/packages/SystemUI/res/values/config.xml 里面添加截屏 Camera 的选项
<string name="quick_settings_tiles_default" translatable="false"> wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast,camera </string>
2、在 AOSP/frameworks/base/packages/SystemUI/res/values/strings.xml 里面还要加一个字符串
1 | <string name="quick_settings_camera_label">Camera</string> |
---|
3、在 AOSP/frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/ 目录下创建 CameraTile.java,实现 QSTileImpl:
package com.android.systemui.qs.tiles; import android.content.Intent; import android.provider.MediaStore; import android.widget.Toast; //手动添加 import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.tileimpl.QSTileImpl; //手动添加 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; public class CameraTile extends QSTileImpl<QSTile.BooleanState> { public CameraTile(QSHost host) { super(host); } @Override public BooleanState newTileState() { return new BooleanState(); } @Override protected void handleClick() { Toast.makeText(mContext,"Camera Click",Toast.LENGTH_LONG).show(); } @Override protected void handleUpdateState(BooleanState state, Object arg) { state.label = mContext.getString(R.string.quick_settings_camera_label); //定义图标,随便用了蓝牙的图标 state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on); } @Override public int getMetricsCategory() { return MetricsEvent.QS_CAMERA; } @Override public Intent getLongClickIntent() { return new Intent(MediaStore.ACTION_IMAGE_CAPTURE); } @Override protected void handleSetListening(boolean listening) { } @Override public CharSequence getTileLabel() { return mContext.getString(R.string.quick_settings_camera_label); } }
4、在 AOSP/frameworks/base/proto/src/metrics_constants.proto,增加常量:
1 | QS_CAMERA = 1568; |
---|
5、在 AOSP/frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java,增加:
private QSTileImpl createTileInternal(String tileSpec) { // Stock tiles. switch (tileSpec) { case "wifi": return new WifiTile(mHost); // 省略部分代码 case "nfc": return new NfcTile(mHost); case "camera": return new CameraTile(mHost); } // 省略部分代码 }
6、整编代码,运行模拟器,有效果,棒棒哒。
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句