ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Android 9.0 SystemUI 下拉状态栏快捷开关

2021-05-21 11:04:56  阅读:335  来源: 互联网

标签:tiles return 状态栏 mHost tileSpec tile new Android 9.0


SystemUI 下拉状态栏快捷开关是 QSPanel,qs_panel.xml,@+id/quick_settings_panel,本篇文章就来看看这些快捷开关是如何呈现的以及如何新增一个快捷开关?基于 AOSP 9.0 分析。

SystemUI 下拉状态栏快捷开关

QSPanel 创建是从 StatusBar#makeStatusBarView 开始的。

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。

SystemUIFactory#createQSTileHost

public QSTileHost createQSTileHost(Context context, StatusBar statusBar,
       StatusBarIconController iconController) {
   return new QSTileHost(context, statusBar, iconController);
}

这里进行 QSTileHost 初始化。

QSTileHost#构造函数

public QSTileHost(Context context, StatusBar statusBar,
       StatusBarIconController iconController) {
   //省略其他代码
   Dependency.get(TunerService.class).addTunable(this, TILES_SETTING);
   //省略其他代码
}

这里进行了 TunerService 注册,在 TunerServiceImpl#addTunable 重写。

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。

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 里字符串信息。

QSTileHost#loadTileSpecs

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 方法。

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 实现了。

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 方法。

图片

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#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 里面还要加一个字符串

<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,增加常量:

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、整编代码,运行模拟器,有效果,棒棒哒。


标签:tiles,return,状态栏,mHost,tileSpec,tile,new,Android,9.0
来源: https://blog.51cto.com/u_15127678/2800491

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有