本文中,我们详细了解一下Android N(7.0)上的新增特性:Quick Settings。
Quick Settings功能如下图所示:
该功能位于下拉的通知面板中,在用户单手指下拉通知面板的时候,Quick Settings区域显示成一个长条,用户可以点击右上角的尖号展开这个区域。
Quick Settings提供给用户非常便捷的按钮,用户甚至无需解锁就可以操作这个区域,通过点击Quick Settings中的Tile来切换某个功能的状态,例如打开/关闭手电筒,蓝牙,Wifi等功能。这对于用户来说是非常便捷的。
开发者API
使用Quick Settings功能非常的简单,只需要与Tile和TileService两个类打交道即可。它们的类图如下图所示:
TileService是android.app.Service的子类,开发者通过继承TileService并覆写其对应的方法来完成功能的实现。TileService中提供的状态回调方法如下:
方法名 | 说明 |
---|---|
onClick() | 当前Tile被点击了 |
onDestroy() | 当前Tile将要被销毁 |
onStartListening() | 当前Tile将要进入监听状态 |
onStopListening() | 当前Tile将要退出监听状态 |
onTileAdded() | 当前Tile被添加到Quick Settings中 |
onTileRemoved() | 当前Tile被从Quick Settings中删除 |
在这些状态变更的时候,开发者可以根据状态的不同来调整Tile的状态。调整的方法就是:先通过TileService.getQsTile()获取到当前Tile,然后通过Tile的setXXX方法来修改。最后调用Tile.updateTile()来使刚刚的设置生效。
下面是一段代码示例。这段代码的功能是根据用户点击来将Tile在Active和非Active状态之间进行切换。
private static final String SERVICE_STATUS_FLAG = "serviceStatus";
private static final String PREFERENCES_KEY =
"com.google.android_quick_settings";
@Override
public void onClick() { ①
Log.d("QS", "Tile tapped");
updateTile();
}
// Changes the appearance of the tile.
private void updateTile() {
Tile tile = this.getQsTile(); ②
boolean isActive = getServiceStatus();
Icon newIcon;
String newLabel;
int newState;
// Change the tile to match the service status.
if (isActive) {
newLabel = String.format(Locale.US,
"%s %s",
getString(R.string.tile_label),
getString(R.string.service_active));
newIcon = Icon.createWithResource(getApplicationContext(),
R.drawable.ic_android_black_24dp);
newState = Tile.STATE_ACTIVE;
} else {
newLabel = String.format(Locale.US,
"%s %s",
getString(R.string.tile_label),
getString(R.string.service_inactive));
newIcon =
Icon.createWithResource(getApplicationContext(),
android.R.drawable.ic_dialog_alert);
newState = Tile.STATE_INACTIVE;
}
// Change the UI of the tile.
tile.setLabel(newLabel); ③
tile.setIcon(newIcon);
tile.setState(newState);
// Need to call updateTile for the tile to pick up changes.
tile.updateTile(); ④
}
这段代码说明如下:
- 处理用户的点击事件
- 获取自身的Tile对象
- 设置Tile的状态,包括:Label,Icon,State
- 设置完成之后真正让状态生效
在实现完成这个TileService之后,我们还需要将其注册到Manifest中。TileService需要设置一个特殊的权限和Intent-Filter的Action,如下所示:
<service
android:name=".QuickSettingsService"
android:icon="@drawable/ic_android_black_dp"
android:label="@string/tile_label"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
当我们将包含这个TileService的应用安装到设备上之后,下划通知面板然后展开Quick Settings区域便可以看到我们开发的Tile了。
系统实现
我们可以通过前面提到的Layout Inspector工具来分析Quick Settings的结构。
Quick Settings位于下拉的通知面板中。在布局上,这个部分通过QSContainer作为外部的容器,其中包含了一个QSPanel。
QSPanel中,包含了一个调节屏幕亮度的控件,这是通过一个LinearLayout来进行布局的,接下来就是PagedTileLayout中包含的多个Tile了,每个Tile用一个QSTileView来进行布局。PagedTileLayout正如其名称所示,这是一个可以分页的Layout。
QSContainer中包含的元素如下图所示:
在Android系统中,包含两类Tile:
- 一类是系统预置的
- 另一类的第三方应用中包含的
Quick Settings功能实现主要位于这个目录中: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs。
系统预置Tile
qs目录下,包含了布局结构中用到的几个元素的实现类,包括:QSContainer,QSPanel,PagedTileLayout,QSTileView,QSIconView等。
系统本身包含了一些预装的Tile,例如:飞行模式的开关,位置信息的开关,热点功能的开关,手电筒功能开关等等。这些Tile的实现位于qs/tiles目录下,包含下面这些:
- AirplaneModeTile.java
- BatteryTile.java
- BluetoothTile.java
- CastTile.java
- CellularTile.java
- ColorInversionTile.java
- DataSaverTile.java
- DndTile.java
- FlashlightTile.java
- HotspotTile.java
- IntentTile.java
- LocationTile.java
- NightDisplayTile.java
- RotationLockTile.java
- UserTile.java
- WifiTile.java
- WorkModeTile.java
在res目录下,有一个名称为quick_settings_tiles_stock
的字符串列出了所有系统内置的Quick Setting的名称,它们通过逗号进行分隔。
<string name="quick_settings_tiles_stock" translatable="false">
wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,work,cast,night
</string>
QSTileHost中为这里的名称和实现类做了映射:
// QSTileHost.java
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("work")) return new WorkModeTile(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);
// Intent tiles.
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;
}
}
TileQueryHelper负责了Tile的初始化工作。在这个类中,会读取R.string.quick_settings_tiles_stock中的值,然后根据配置来初始化系统内置的Quick Setting:
// TileQueryHelper.java
String possible = mContext.getString(R.string.quick_settings_tiles_stock);
String[] possibleTiles = possible.split(",");
final Handler qsHandler = new Handler(host.getLooper());
final Handler mainHandler = new Handler(Looper.getMainLooper());
for (int i = 0; i < possibleTiles.length; i++) {
final String spec = possibleTiles[i];
final QSTile<?> tile = host.createTile(spec);
if (tile == null || !tile.isAvailable()) {
continue;
}
tile.setListening(this, true);
tile.clearState();
tile.refreshState();
tile.setListening(this, false);
qsHandler.post(new Runnable() {
@Override
public void run() {
final QSTile.State state = tile.newTileState();
tile.getState().copyTo(state);
// Ignore the current state and get the generic label instead.
state.label = tile.getTileLabel();
mainHandler.post(new Runnable() {
@Override
public void run() {
addTile(spec, null, state, true);
mListener.onTilesChanged(mTiles);
}
});
}
});
}
这段代码应该很简单,这里就不多做说明了。
第三方应用中包含的Tile
对于SystemUI来说,除了要列出系统内置的Quick Setting之外,还有开发者开发的Quick Setting也需要读取。这部分逻辑通过QueryTilesTask以一个异步的Task来完成,这在这个异步任务中,会通过PackageManager查询所有开发者开发的Quick Setting
// TileQueryHelper.java
private class QueryTilesTask extends
AsyncTask<Collection<QSTile<?>>, Void, Collection<TileInfo>> {
@Override
protected Collection<TileInfo> doInBackground(Collection<QSTile<?>>... params) {
List<TileInfo> tiles = new ArrayList<>();
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser()); ①
String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);
for (ResolveInfo info : services) { ②
String packageName = info.serviceInfo.packageName;
ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
// Don't include apps that are a part of the default tile set.
if (stockTiles.contains(componentName.flattenToString())) { ③
continue;
}
final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm); ④
String spec = CustomTile.toSpec(componentName);
State state = getState(params[0], spec);
if (state != null) {
addTile(spec, appLabel, state, false);
continue;
}
if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
continue;
}
Drawable icon = info.serviceInfo.loadIcon(pm);
if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
continue;
}
if (icon == null) {
continue;
}
icon.mutate();
icon.setTint(mContext.getColor(android.R.color.white));
CharSequence label = info.serviceInfo.loadLabel(pm);
addTile(spec, icon, label != null ? label.toString() : "null", appLabel, mContext);
}
return tiles;
}
这段代码说明如下:
- 通过PackageManager查询所有设置了TileService.ACTION_QS_TILE的组件。PackageManager负责了所有应用包信息的管理。
- 遍历查询到的所有组件
- 跳过系统预置的Tile
- 为每个Tile读取标签和图标