掌握Android 7.0 新增特性 Quick Settings

Posted on Aug 1, 2017


本文中,我们详细了解一下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(); 
}

这段代码说明如下:

  1. 处理用户的点击事件
  2. 获取自身的Tile对象
  3. 设置Tile的状态,包括:Label,Icon,State
  4. 设置完成之后真正让状态生效

在实现完成这个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;
   }

这段代码说明如下:

  1. 通过PackageManager查询所有设置了TileService.ACTION_QS_TILE的组件。PackageManager负责了所有应用包信息的管理。
  2. 遍历查询到的所有组件
  3. 跳过系统预置的Tile
  4. 为每个Tile读取标签和图标

参考资料与推荐读物


 Contents