Android自定義快速設(shè)置

Android自定義快速設(shè)置

Customizer Quick Setting

自定義快速設(shè)置

Android N/7.0 牛軋?zhí)?/p>

前言

Android N在2016年5月的Google I/O大會上發(fā)布,按照過往歷史,同樣會用一種水果的名稱稱呼這個新的Andrid版本,但是Google I/O上,并沒有為Android N定好名字,直到6月份通過投票的形式正式命名為“牛軋?zhí)恰?。Android N在安全和用戶體驗(yàn)上都有較大的更新,如多窗口支持、通知增強(qiáng)功能、配置文件指導(dǎo)的 JIT/AOT 編譯、隨時隨地低電耗模式和Quick Settings Tile等等。本文對Android N的新功能Quick Settings Tile(快速設(shè)置圖塊)的應(yīng)用進(jìn)行詳解。

所謂快速設(shè)置圖塊,即下拉通知欄時的快速設(shè)置按鈕,通常用于直接從通知欄顯示關(guān)鍵設(shè)置和操作,非常簡單。如Wifi的開關(guān)、數(shù)據(jù)連接、飛行模式和藍(lán)牙等等。當(dāng)然這個功能在Andorid中一直都有,Android N做了那些升級更新呢?如下:

  • 額外的“快速設(shè)置”圖塊添加了更多空間,用戶可以通過向左或向右滑動跨分頁的顯示區(qū)域訪問它們。我們還讓用戶可以控制顯示哪些“快速設(shè)置”圖塊以及顯示的位置 — 用戶可以通過拖放圖塊來添加或移動圖塊。
  • 對于開發(fā)者,Android 7.0 還添加了一個新的 API,從而讓您可以定義自己的“快速設(shè)置”圖塊,使用戶可以輕松訪問您應(yīng)用中的關(guān)鍵控件和操作。

第一點(diǎn)更新,如下圖,用戶可以左右滑動快速設(shè)置面板,比之前的版本增大了一倍的空間:

20170221113312839.png

第二點(diǎn)更新,也就是本文要介紹的功能更新,它讓APP開發(fā)者也可以為自己的應(yīng)用定制一個快速設(shè)置圖塊,而之前的版本只能對系統(tǒng)的功能進(jìn)行設(shè)置,這對用戶常用的APP,可能會帶來很大的方便性。

既然是快速設(shè)置,所以Google建議:對于急需或頻繁使用的控件和操作,保留“快速設(shè)置”圖塊,且不應(yīng)將其用作啟動應(yīng)用的快捷方式。所以APP開發(fā)者,應(yīng)該遵行Google的功能設(shè)計初衷,以便讓用戶能夠有更好地用戶體驗(yàn)。

自定義快速設(shè)置圖塊

APP要實(shí)現(xiàn)自定義快速設(shè)置圖塊很簡單,只需需要兩步即可:

第一步:定義一個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)建一個MyQSTileService繼承TileService,復(fù)寫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就不多說了,這里側(cè)重看android:icon,android:permission和action。android:icon雖然是老版本就有的,但是
TileService必須指明這個屬性,快速設(shè)置才會生效。既然是快速設(shè)置圖塊,就需要一張圖片,所以這個icon肯定不能為空啦,要不還怎么顯示。本例子添加的圖片如下高亮的圖片:


20170221113346936.png

然后必須在TileService中聲明權(quán)限android.permission.BIND_QUICK_SETTINGS_TILE,這是Android安全的套路。最后action也是必不可少,系統(tǒng)就是通過android.service.quicksettings.action.QS_TILE這個Action來尋找所有APP的TileService,后文會贅述這個過程。

只需兩步,即可實(shí)現(xiàn)自定義的Tile顯示在下拉菜單的快速設(shè)置面板中。如下圖所以:

20170221113407593.png

如上圖,最后一個便是本例子自定義的快速設(shè)置圖塊,當(dāng)然,現(xiàn)在這個圖塊只是在備選區(qū)域,用戶可以把這個圖塊拖動到快速設(shè)置面板,就可以實(shí)現(xiàn)真正的快速設(shè)置了,如下圖中間的圖塊:


20170221113428497.png

實(shí)現(xiàn)自定義快速設(shè)置圖塊,就是這么簡單,但是只是這樣,對用戶使用體驗(yàn)不是很舒服,因?yàn)檫@個圖塊只有一種顏色,用戶不知道當(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)擊的自定義的圖塊,將會回調(diào)onClick()方法,在這個方法里,首先獲取到當(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()這個方法,快速設(shè)置面板上的圖塊顏色才會改變。如下圖,CusTile為打開的狀態(tài):


20170221113458857.png

下圖是CusTile是關(guān)閉的狀態(tài):


20170221113520639.png

除此之外,Tile還提供如下兩個方法,以便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;
    }
}

用戶在操作時,APP應(yīng)該使用這兩個方法判斷一下是否應(yīng)該改變APP的行為。另外,TileService還提供startActivityAndCollapse()等方法讓APP開發(fā)者可以用更少的代碼實(shí)現(xiàn)超棒的用戶體驗(yàn),這也是一個非常棒的方法。讀者可以自行閱讀幫助文檔了解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啟動TileService時,回調(diào)onBind()方法,TileService創(chuàng)建一個IQSTileService.Stub的實(shí)例,IQSTileService正好就是實(shí)現(xiàn)了AIDL標(biāo)準(zhǔn)的IPC通信的接口,所以,onBind()方法把IQSTileService的句柄返回給SystemUI,因此SystemUI便可實(shí)現(xiàn)對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è)置面板的編輯按鈕時,調(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()方法,后面接著會生成一個TileQueryHelper的對象,調(diào)用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());
                    }
                });
            }
        });
    }
}

上面的代碼啟動了多線程任務(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這個值:

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,如果沒有,則無法添加快速設(shè)置圖塊,所以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權(quán)限。再往下看,有icon.setTint(mContext.getColor(android.R.color.white)),這個方法就是把圖塊強(qiáng)制繪制成白色,這個在Android的扁平化設(shè)計中強(qiáng)制的行為,所以APP用一些五顏六色圖片時沒有用的,最終都會變成白色。最后調(diào)用addTile()方法添加到快速設(shè)置面板了。TileService的加載就分析到這里。下面看看TileService是如何啟動的?什么時候啟動?

TileService的啟動

在frameworks/base/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java中,會注冊一個監(jiān)聽APK安裝與卸載的廣播接收者,當(dāng)自定義了快速設(shè)置圖塊的APK被安裝時,會觸發(fā)這個廣播接收者:

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安裝時觸發(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()啟動了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。然后快速這只的一切事件將會通過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的啟動就分析到這里,SystemUI對快速設(shè)置圖塊的添加、移除和點(diǎn)擊等等事件,通過wrapper最后調(diào)用到APP的TileService。

總結(jié)

本文詳細(xì)描述了Android N的新功能自定義設(shè)置圖塊的實(shí)現(xiàn)方法,以及原理,這個功能的核心是TileService,提供了SystemUI和自定義設(shè)置圖塊的APP進(jìn)行緊密的通信的基礎(chǔ)。作為APP的開發(fā)者,或許快速設(shè)置圖塊將會是一個APP提升用戶體驗(yàn)的一種重要途徑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容