Android自定義快速設(shè)置
Customizer Quick Setting
自定義快速設(shè)置
Android N/7.0 牛軋?zhí)?/p>
前言
Android N在2016年5月的Google I/O大會(huì)上發(fā)布,按照過往歷史赦邻,同樣會(huì)用一種水果的名稱稱呼這個(gè)新的Andrid版本设塔,但是Google I/O上,并沒有為Android N定好名字匣吊,直到6月份通過投票的形式正式命名為“牛軋?zhí)恰薄ndroid N在安全和用戶體驗(yàn)上都有較大的更新,如多窗口支持、通知增強(qiáng)功能宗收、配置文件指導(dǎo)的 JIT/AOT 編譯、隨時(shí)隨地低電耗模式和Quick Settings Tile等等亚兄。本文對(duì)Android N的新功能Quick Settings Tile(快速設(shè)置圖塊)的應(yīng)用進(jìn)行詳解混稽。
所謂快速設(shè)置圖塊,即下拉通知欄時(shí)的快速設(shè)置按鈕审胚,通常用于直接從通知欄顯示關(guān)鍵設(shè)置和操作匈勋,非常簡單。如Wifi的開關(guān)膳叨、數(shù)據(jù)連接洽洁、飛行模式和藍(lán)牙等等。當(dāng)然這個(gè)功能在Andorid中一直都有菲嘴,Android N做了那些升級(jí)更新呢饿自?如下:
- 額外的“快速設(shè)置”圖塊添加了更多空間,用戶可以通過向左或向右滑動(dòng)跨分頁的顯示區(qū)域訪問它們龄坪。我們還讓用戶可以控制顯示哪些“快速設(shè)置”圖塊以及顯示的位置 — 用戶可以通過拖放圖塊來添加或移動(dòng)圖塊昭雌。
- 對(duì)于開發(fā)者,Android 7.0 還添加了一個(gè)新的 API健田,從而讓您可以定義自己的“快速設(shè)置”圖塊烛卧,使用戶可以輕松訪問您應(yīng)用中的關(guān)鍵控件和操作。
第一點(diǎn)更新妓局,如下圖总放,用戶可以左右滑動(dòng)快速設(shè)置面板,比之前的版本增大了一倍的空間:
第二點(diǎn)更新好爬,也就是本文要介紹的功能更新局雄,它讓APP開發(fā)者也可以為自己的應(yīng)用定制一個(gè)快速設(shè)置圖塊,而之前的版本只能對(duì)系統(tǒng)的功能進(jìn)行設(shè)置抵拘,這對(duì)用戶常用的APP哎榴,可能會(huì)帶來很大的方便性型豁。
既然是快速設(shè)置,所以Google建議:對(duì)于急需或頻繁使用的控件和操作尚蝌,保留“快速設(shè)置”圖塊迎变,且不應(yīng)將其用作啟動(dòng)應(yīng)用的快捷方式。所以APP開發(fā)者飘言,應(yīng)該遵行Google的功能設(shè)計(jì)初衷衣形,以便讓用戶能夠有更好地用戶體驗(yàn)。
自定義快速設(shè)置圖塊
APP要實(shí)現(xiàn)自定義快速設(shè)置圖塊很簡單姿鸿,只需需要兩步即可:
第一步:定義一個(gè)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();
}
}
如上面的代碼,創(chuàng)建一個(gè)MyQSTileService繼承TileService苛预,復(fù)寫onTileAdded()句狼、onTileRemoved()、onStartListening()热某、onStopListening()和onClick()方法腻菇。
第二步:在項(xiàng)目的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就不多說了昔馋,這里側(cè)重看android:icon筹吐,android:permission和action。android:icon雖然是老版本就有的秘遏,但是
TileService必須指明這個(gè)屬性丘薛,快速設(shè)置才會(huì)生效。既然是快速設(shè)置圖塊邦危,就需要一張圖片洋侨,所以這個(gè)icon肯定不能為空啦,要不還怎么顯示铡俐。本例子添加的圖片如下高亮的圖片:
然后必須在TileService中聲明權(quán)限android.permission.BIND_QUICK_SETTINGS_TILE凰兑,這是Android安全的套路妥粟。最后action也是必不可少审丘,系統(tǒng)就是通過android.service.quicksettings.action.QS_TILE這個(gè)Action來尋找所有APP的TileService,后文會(huì)贅述這個(gè)過程勾给。
只需兩步滩报,即可實(shí)現(xiàn)自定義的Tile顯示在下拉菜單的快速設(shè)置面板中。如下圖所以:
如上圖播急,最后一個(gè)便是本例子自定義的快速設(shè)置圖塊脓钾,當(dāng)然,現(xiàn)在這個(gè)圖塊只是在備選區(qū)域桩警,用戶可以把這個(gè)圖塊拖動(dòng)到快速設(shè)置面板可训,就可以實(shí)現(xiàn)真正的快速設(shè)置了,如下圖中間的圖塊:
實(shí)現(xiàn)自定義快速設(shè)置圖塊,就是這么簡單握截,但是只是這樣飞崖,對(duì)用戶使用體驗(yàn)不是很舒服,因?yàn)檫@個(gè)圖塊只有一種顏色谨胞,用戶不知道當(dāng)前是關(guān)固歪,或者是開的狀態(tài)。因此胯努,還需要根據(jù)實(shí)際開/關(guān)的狀態(tài)牢裳,改變圖塊的顏色。當(dāng)用戶點(diǎn)擊一次圖塊叶沛,狀態(tài)就要發(fā)生變化蒲讯。實(shí)現(xiàn)代碼如下:
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:
//當(dāng)前狀態(tài)是開,設(shè)置狀態(tài)為關(guān)閉.
tile.setState(Tile.STATE_INACTIVE);
//更新快速設(shè)置面板上的圖塊的顏色灰署,狀態(tài)為關(guān).
tile.updateTile();
//do close somethings.
break;
case Tile.STATE_UNAVAILABLE:
break;
case Tile.STATE_INACTIVE:
//當(dāng)前狀態(tài)是關(guān)伶椿,設(shè)置狀態(tài)為開.
tile.setState(Tile.STATE_ACTIVE);
//更新快速設(shè)置面板上的圖塊的顏色,狀態(tài)為開.
tile.updateTile();
//do open somethings.
break;
default:
break;
}
}
}
如上面的代碼氓侧,當(dāng)用戶點(diǎn)擊的自定義的圖塊脊另,將會(huì)回調(diào)onClick()方法,在這個(gè)方法里约巷,首先獲取到當(dāng)前的Tile偎痛,調(diào)用Tile的getState()方法獲取當(dāng)期Tile的狀態(tài),如果狀態(tài)正處于打開的狀態(tài)独郎,調(diào)用setState()的方法改變狀態(tài)為關(guān)閉踩麦,然后一定要調(diào)用updateTile()這個(gè)方法,快速設(shè)置面板上的圖塊顏色才會(huì)改變氓癌。如下圖谓谦,CusTile為打開的狀態(tài):
下圖是CusTile是關(guān)閉的狀態(tài):
除此之外,Tile還提供如下兩個(gè)方法贪婉,以便APP可以給用戶更為舒適的用戶體驗(yàn)反粥。檢測當(dāng)前設(shè)備是否處于鎖屏狀態(tài),方法如下:
//Checks if the lock screen is showing.
public final boolean isLocked() {
try {
return mService.isLocked();
} catch (RemoteException e) {
return true;
}
}
檢測當(dāng)期設(shè)備是否處于安全模式疲迂,如圖案解鎖才顿,方法如下:
//Checks if the device is in a secure state.
public final boolean isSecure() {
try {
return mService.isSecure();
} catch (RemoteException e) {
return true;
}
}
用戶在操作時(shí),APP應(yīng)該使用這兩個(gè)方法判斷一下是否應(yīng)該改變APP的行為尤蒿。另外郑气,TileService還提供startActivityAndCollapse()等方法讓APP開發(fā)者可以用更少的代碼實(shí)現(xiàn)超棒的用戶體驗(yàn),這也是一個(gè)非常棒的方法腰池。讀者可以自行閱讀幫助文檔了解TileService提供的功能尾组,以便更好地開發(fā)自己的應(yīng)用忙芒。
深入理解TileService
TileService的結(jié)構(gòu)
快速設(shè)置面板是SystemUI提供的功能,如果讀者不熟悉SystemUI的讳侨,可以閱讀文章《SystemUI架構(gòu)分析 》匕争,也就是說自定義快速設(shè)置圖塊的點(diǎn)擊等行為已經(jīng)發(fā)生了跨進(jìn)程通信,SystemUI是如果調(diào)用到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);
}
};
}
}
如上面的代碼,如上這些方法是不是很熟悉呢歹叮。當(dāng)SystemUI啟動(dòng)TileService時(shí)跑杭,回調(diào)onBind()方法,TileService創(chuàng)建一個(gè)IQSTileService.Stub的實(shí)例咆耿,IQSTileService正好就是實(shí)現(xiàn)了AIDL標(biāo)準(zhǔn)的IPC通信的接口德谅,所以,onBind()方法把IQSTileService的句柄返回給SystemUI萨螺,因此SystemUI便可實(shí)現(xiàn)對(duì)TileService的遠(yuǎn)程調(diào)用窄做。SystemUI通過橋梁IQSTileService便可和APP實(shí)現(xiàn)緊密快速的通信。
TileService的加載
通過文章《SystemUI架構(gòu)分析 》可知慰技,負(fù)責(zé)快速設(shè)置面板顯示的控件是rameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java椭盏,自定義快速設(shè)置圖塊的APP安裝到系統(tǒng)后,當(dāng)用戶點(diǎn)擊編輯快速設(shè)置面板的編輯按鈕時(shí)吻商,調(diào)用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);
}
}
}
});
}
}
接著上面的代碼,然后調(diào)用通過QSCustomizer實(shí)例mCustomizePanel調(diào)用show()方法艾帐,后面接著會(huì)生成一個(gè)TileQueryHelper的對(duì)象乌叶,調(diào)用addSystemTiles()方法,這個(gè)過程就不贅述了柒爸,下面直接看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());
}
});
}
});
}
}
上面的代碼啟動(dòng)了多線程任務(wù)類QueryTilesTask准浴,看在新的線程里,如果加載自定義的快速設(shè)置圖塊捎稚,代碼如下:
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;
}
}
上面的代碼有點(diǎn)多乐横,從上到下,慢慢來看阳藻,首先晰奖,是通過PackageManager查詢action為TileService.ACTION_QS_TILE的service,看TileService.ACTION_QS_TILE這個(gè)值:
public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE";
正好是上文中的例子中的APP中MyQSTileService在AndroidManifest.xml中聲明的action一致腥泥,往下看一個(gè)if語句:
if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
continue;
}
這條if語句就是判斷TileService中是否聲明了icon,如果沒有啃匿,則無法添加快速設(shè)置圖塊蛔外,所以AndroidManifest.xml中對(duì)MyQSTileService一定要聲明icon蛆楞。再往下看另外一個(gè)if語句:
if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
continue;
}
這里檢測了permission.BIND_QUICK_SETTINGS_TILE全新,因此AndroidManifest.xml中對(duì)MyQSTileService一定要聲明android.permission.BIND_QUICK_SETTINGS_TILE權(quán)限夹厌。再往下看豹爹,有icon.setTint(mContext.getColor(android.R.color.white)),這個(gè)方法就是把圖塊強(qiáng)制繪制成白色矛纹,這個(gè)在Android的扁平化設(shè)計(jì)中強(qiáng)制的行為臂聋,所以APP用一些五顏六色圖片時(shí)沒有用的,最終都會(huì)變成白色或南。最后調(diào)用addTile()方法添加到快速設(shè)置面板了孩等。TileService的加載就分析到這里。下面看看TileService是如何啟動(dòng)的采够?什么時(shí)候啟動(dòng)肄方?
TileService的啟動(dòng)
在frameworks/base/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java中,會(huì)注冊(cè)一個(gè)監(jiān)聽APK安裝與卸載的廣播接收者蹬癌,當(dāng)自定義了快速設(shè)置圖塊的APK被安裝時(shí)权她,會(huì)觸發(fā)這個(gè)廣播接收者:
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安裝時(shí)觸發(fā)廣播接收器,調(diào)用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()啟動(dòng)了TileService,Service連接后董济,回調(diào)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的遠(yuǎn)程調(diào)用句柄尼啡,本質(zhì)是上文中提到的IQSTileService.Stub。然后快速這只的一切事件將會(huì)通過QSTileServiceWrapper的實(shí)例wrapper調(diào)用询微,如下:
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的啟動(dòng)就分析到這里崖瞭,SystemUI對(duì)快速設(shè)置圖塊的添加、移除和點(diǎn)擊等等事件撑毛,通過wrapper最后調(diào)用到APP的TileService书聚。
總結(jié)
本文詳細(xì)描述了Android N的新功能自定義設(shè)置圖塊的實(shí)現(xiàn)方法,以及原理藻雌,這個(gè)功能的核心是TileService雌续,提供了SystemUI和自定義設(shè)置圖塊的APP進(jìn)行緊密的通信的基礎(chǔ)。作為APP的開發(fā)者胯杭,或許快速設(shè)置圖塊將會(huì)是一個(gè)APP提升用戶體驗(yàn)的一種重要途徑驯杜。