Android自定义快速设置

Android自定义快速设置

Customizer Quick Setting

自定义快速设置

Android N/7.0 牛轧糖

前言

Android N在2016年5月的Google I/O大会上发布,按照过往历史,同样会用一种水果的名称称呼这个新的Andrid版本,但是Google I/O上,并没有为Android N定好名字,直到6月份通过投票的形式正式命名为“牛轧糖”。Android N在安全和用户体验上都有较大的更新,如多窗口支持、通知增强功能、配置文件指导的 JIT/AOT 编译、随时随地低电耗模式和Quick Settings Tile等等。本文对Android N的新功能Quick Settings Tile(快速设置图块)的应用进行详解。

所谓快速设置图块,即下拉通知栏时的快速设置按钮,通常用于直接从通知栏显示关键设置和操作,非常简单。如Wifi的开关、数据连接、飞行模式和蓝牙等等。当然这个功能在Andorid中一直都有,Android N做了那些升级更新呢?如下:

  • 额外的“快速设置”图块添加了更多空间,用户可以通过向左或向右滑动跨分页的显示区域访问它们。我们还让用户可以控制显示哪些“快速设置”图块以及显示的位置 — 用户可以通过拖放图块来添加或移动图块。
  • 对于开发者,Android 7.0 还添加了一个新的 API,从而让您可以定义自己的“快速设置”图块,使用户可以轻松访问您应用中的关键控件和操作。

第一点更新,如下图,用户可以左右滑动快速设置面板,比之前的版本增大了一倍的空间:

20170221113312839.png

第二点更新,也就是本文要介绍的功能更新,它让APP开发者也可以为自己的应用定制一个快速设置图块,而之前的版本只能对系统的功能进行设置,这对用户常用的APP,可能会带来很大的方便性。

既然是快速设置,所以Google建议:对于急需或频繁使用的控件和操作,保留“快速设置”图块,且不应将其用作启动应用的快捷方式。所以APP开发者,应该遵行Google的功能设计初衷,以便让用户能够有更好地用户体验。

自定义快速设置图块

APP要实现自定义快速设置图块很简单,只需需要两步即可:

第一步:定义一个MyQSTileService继承frameworks/base/core/java/android/service/quicksettings/TileService.java,如下:

public class MyQSTileService extends TileService {
    //Called when the user adds this tile to Quick Settings.
    @Override
    public void onTileAdded() {
        super.onTileAdded();
    }

    //Called when the user removes this tile from Quick Settings.
    @Override
    public void onTileRemoved() {
        super.onTileRemoved();
    }

    //Called when this tile moves into a listening state.
    @Override
    public void onStartListening() {
        super.onStartListening();
    }

    //Called when this tile moves out of the listening state.
    @Override
    public void onStopListening() {
        super.onStopListening();
    }

    //Called when the user clicks on this tile.
    @Override
    public void onClick() {
        super.onClick();
    }
}

如上面的代码,创建一个MyQSTileService继承TileService,复写onTileAdded()、onTileRemoved()、onStartListening()、onStopListening()和onClick()方法。

第二步:在项目的AndroidManifest.xml文件中添加如下代码:

<service
    android:name=".MyQSTileService"
    android:label="@string/my_default_tile_label"
    android:icon="@drawable/my_default_icon_label"
    android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
    <intent-filter>
        <action android:name="android.service.quicksettings.action.QS_TILE" />
    </intent-filter>
</service>

上述代码,android:name和android:label就不多说了,这里侧重看android:icon,android:permission和action。android:icon虽然是老版本就有的,但是
TileService必须指明这个属性,快速设置才会生效。既然是快速设置图块,就需要一张图片,所以这个icon肯定不能为空啦,要不还怎么显示。本例子添加的图片如下高亮的图片:


20170221113346936.png

然后必须在TileService中声明权限android.permission.BIND_QUICK_SETTINGS_TILE,这是Android安全的套路。最后action也是必不可少,系统就是通过android.service.quicksettings.action.QS_TILE这个Action来寻找所有APP的TileService,后文会赘述这个过程。

只需两步,即可实现自定义的Tile显示在下拉菜单的快速设置面板中。如下图所以:

20170221113407593.png

如上图,最后一个便是本例子自定义的快速设置图块,当然,现在这个图块只是在备选区域,用户可以把这个图块拖动到快速设置面板,就可以实现真正的快速设置了,如下图中间的图块:


20170221113428497.png

实现自定义快速设置图块,就是这么简单,但是只是这样,对用户使用体验不是很舒服,因为这个图块只有一种颜色,用户不知道当前是关,或者是开的状态。因此,还需要根据实际开/关的状态,改变图块的颜色。当用户点击一次图块,状态就要发生变化。实现代码如下:

public class MyQSTileService extends TileService {
    @Override
    public void onTileAdded() {
        Log.d("MyQSTileService", "onTileAdded()");
    }
    ......
    
    @Override
    public void onClick() {
        Log.d("MyQSTileService", "onClick()");
        //获取自定义的Tile.
        Tile tile = getQsTile();
        if (tile == null) {
            return;
        }
        Log.d("MyQSTileService", "Tile state: " + tile.getState());
        switch (tile.getState()) {
            case Tile.STATE_ACTIVE:
                //当前状态是开,设置状态为关闭.
                tile.setState(Tile.STATE_INACTIVE);
                //更新快速设置面板上的图块的颜色,状态为关.
                tile.updateTile();
                //do close somethings.
                break;
            case Tile.STATE_UNAVAILABLE:
                break;
            case Tile.STATE_INACTIVE:
                //当前状态是关,设置状态为开.
                tile.setState(Tile.STATE_ACTIVE);
                //更新快速设置面板上的图块的颜色,状态为开.
                tile.updateTile();
                //do open somethings.
                break;
            default:
                break;
        }
    }
}

如上面的代码,当用户点击的自定义的图块,将会回调onClick()方法,在这个方法里,首先获取到当前的Tile,调用Tile的getState()方法获取当期Tile的状态,如果状态正处于打开的状态,调用setState()的方法改变状态为关闭,然后一定要调用updateTile()这个方法,快速设置面板上的图块颜色才会改变。如下图,CusTile为打开的状态:


20170221113458857.png

下图是CusTile是关闭的状态:


20170221113520639.png

除此之外,Tile还提供如下两个方法,以便APP可以给用户更为舒适的用户体验。检测当前设备是否处于锁屏状态,方法如下:

//Checks if the lock screen is showing.
public final boolean isLocked() {
    try {
        return mService.isLocked();
    } catch (RemoteException e) {
        return true;
    }
}

检测当期设备是否处于安全模式,如图案解锁,方法如下:

//Checks if the device is in a secure state.
public final boolean isSecure() {
    try {
        return mService.isSecure();
    } catch (RemoteException e) {
        return true;
    }
}

用户在操作时,APP应该使用这两个方法判断一下是否应该改变APP的行为。另外,TileService还提供startActivityAndCollapse()等方法让APP开发者可以用更少的代码实现超棒的用户体验,这也是一个非常棒的方法。读者可以自行阅读帮助文档了解TileService提供的功能,以便更好地开发自己的应用。

深入理解TileService

TileService的结构

快速设置面板是SystemUI提供的功能,如果读者不熟悉SystemUI的,可以阅读文章《SystemUI架构分析 》,也就是说自定义快速设置图块的点击等行为已经发生了跨进程通信,SystemUI是如果调用到APP的呢?以Anroid四大组件之一的Service如何建立IPC通信开始,从Service的生命周期onBind()说起:

public class TileService extends Service {
    public IBinder onBind(Intent intent) {
        mService = IQSService.Stub.asInterface(intent.getIBinderExtra(EXTRA_SERVICE));
        try {
            ComponentName component = intent.getParcelableExtra(EXTRA_COMPONENT);
            mTile = mService.getTile(component);
        } catch (RemoteException e) {
            throw new RuntimeException("Unable to reach IQSService", e);
        }
        if (mTile != null) {
            mTile.setService(mService);
            mHandler.sendEmptyMessage(H.MSG_START_SUCCESS);
        }
        return new IQSTileService.Stub() {
            @Override
            public void onTileRemoved() throws RemoteException {
                mHandler.sendEmptyMessage(H.MSG_TILE_REMOVED);
            }

            @Override
            public void onTileAdded() throws RemoteException {
                mHandler.sendEmptyMessage(H.MSG_TILE_ADDED);
            }

            @Override
            public void onStopListening() throws RemoteException {
                mHandler.sendEmptyMessage(H.MSG_STOP_LISTENING);
            }

            @Override
            public void onStartListening() throws RemoteException {
                mHandler.sendEmptyMessage(H.MSG_START_LISTENING);
            }

            @Override
            public void onClick(IBinder wtoken) throws RemoteException {
                mHandler.obtainMessage(H.MSG_TILE_CLICKED, wtoken).sendToTarget();
            }

            @Override
            public void onUnlockComplete() throws RemoteException{
                mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE);
            }
        };
    }
}

如上面的代码,如上这些方法是不是很熟悉呢。当SystemUI启动TileService时,回调onBind()方法,TileService创建一个IQSTileService.Stub的实例,IQSTileService正好就是实现了AIDL标准的IPC通信的接口,所以,onBind()方法把IQSTileService的句柄返回给SystemUI,因此SystemUI便可实现对TileService的远程调用。SystemUI通过桥梁IQSTileService便可和APP实现紧密快速的通信。

TileService的加载

通过文章《SystemUI架构分析 》可知,负责快速设置面板显示的控件是rameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java,自定义快速设置图块的APP安装到系统后,当用户点击编辑快速设置面板的编辑按钮时,调用QSPanel的showEdit()方法,如下:

public class QSPanel extends LinearLayout implements Tunable, Callback {
    private void showEdit(final View v) {
        v.post(new Runnable() {
            @Override
            public void run() {
                ......
                        mCustomizePanel.show(x, y);
                    }
                }
            }
        });
    }
}

接着上面的代码,然后调用通过QSCustomizer实例mCustomizePanel调用show()方法,后面接着会生成一个TileQueryHelper的对象,调用addSystemTiles()方法,这个过程就不赘述了,下面直接看addSystemTiles()的代码:

public class TileQueryHelper {
    private void addSystemTiles(final QSTileHost host) {
        ......
        qsHandler.post(new Runnable() {
            @Override
            public void run() {
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        new QueryTilesTask().execute(host.getTiles());
                    }
                });
            }
        });
    }
}

上面的代码启动了多线程任务类QueryTilesTask,看在新的线程里,如果加载自定义的快速设置图块,代码如下:

public class TileQueryHelper {
        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());
            for (ResolveInfo info : services) {
                String packageName = info.serviceInfo.packageName;
                ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
                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查询action为TileService.ACTION_QS_TILE的service,看TileService.ACTION_QS_TILE这个值:

public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE";

正好是上文中的例子中的APP中MyQSTileService在AndroidManifest.xml中声明的action一致,往下看一个if语句:

if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
    continue;
}

这条if语句就是判断TileService中是否声明了icon,如果没有,则无法添加快速设置图块,所以AndroidManifest.xml中对MyQSTileService一定要声明icon。再往下看另外一个if语句:

if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
    continue;
}

这里检测了permission.BIND_QUICK_SETTINGS_TILE全新,因此AndroidManifest.xml中对MyQSTileService一定要声明android.permission.BIND_QUICK_SETTINGS_TILE权限。再往下看,有icon.setTint(mContext.getColor(android.R.color.white)),这个方法就是把图块强制绘制成白色,这个在Android的扁平化设计中强制的行为,所以APP用一些五颜六色图片时没有用的,最终都会变成白色。最后调用addTile()方法添加到快速设置面板了。TileService的加载就分析到这里。下面看看TileService是如何启动的?什么时候启动?

TileService的启动

在frameworks/base/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java中,会注册一个监听APK安装与卸载的广播接收者,当自定义了快速设置图块的APK被安装时,会触发这个广播接收者:

public class TileLifecycleManager extends BroadcastReceiver ...... {
    public void onReceive(Context context, Intent intent) {
        ......
        if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) && mChangeListener != null) {
            mChangeListener.onTileChanged(mIntent.getComponent());
        }
        stopPackageListening();
        if (mBound) {
            // Trying to bind again will check the state of the package before bothering to bind.
            if (DEBUG) Log.d(TAG, "Trying to rebind");
            setBindService(true);
        }
    }
}

APK安装时触发广播接收器,调用setBindService(true)方法,如下:

    public void setBindService(boolean bind) {
        mBound = bind;
        ......
            try {
                mIsBound = mContext.bindServiceAsUser(mIntent, this,
                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
                        mUser);
            ......
        }
    }

如上面的代码,通过bindServiceAsUser()启动了TileService,Service连接后,回调ServiceConnection的onServiceConnected(),如下:

public void onServiceConnected(ComponentName name, IBinder service) {
    .....
    final QSTileServiceWrapper wrapper = new QSTileServiceWrapper(Stub.asInterface(service));
    .....
    mWrapper = wrapper;
    handlePendingMessages();
}

如上面的代码,IBinder保存到QSTileServiceWrapper中,Stub.asInterface(service)正是TileService的远程调用句柄,本质是上文中提到的IQSTileService.Stub。然后快速这只的一切事件将会通过QSTileServiceWrapper的实例wrapper调用,如下:

public class QSTileServiceWrapper {
    private static final String TAG = "IQSTileServiceWrapper";

    private final IQSTileService mService;

    public QSTileServiceWrapper(IQSTileService service) {
        mService = service;
    }

    public IBinder asBinder() {
        return mService.asBinder();
    }

    public boolean onTileAdded() {
        try {
            mService.onTileAdded();
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public boolean onTileRemoved() {
        ......
    }

    public boolean onStartListening() {
        ......
    }

    public boolean onStopListening() {
        ......
    }

    public boolean onClick(IBinder token) {
        ......
    }

    public boolean onUnlockComplete() {
        ......
    }
}

TileService的启动就分析到这里,SystemUI对快速设置图块的添加、移除和点击等等事件,通过wrapper最后调用到APP的TileService。

总结

本文详细描述了Android N的新功能自定义设置图块的实现方法,以及原理,这个功能的核心是TileService,提供了SystemUI和自定义设置图块的APP进行紧密的通信的基础。作为APP的开发者,或许快速设置图块将会是一个APP提升用户体验的一种重要途径。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容