SystemUI之QuickStep探索(常規(guī)啟動篇)

引言

QuickStep是Android P中一大新特性质礼,在Android P中 Google將SystemUI中Recents在Launcher又實現(xiàn)了一遍服鹅,并且取名為QuickStep逻卖,其具有新的界面(如下圖)以及交互方式粘捎。


quickstep.png

QuickStep有兩種啟動方式形纺,一種為常規(guī)啟動方式——點(diǎn)擊recents鍵丘侠,一種則是通過手勢啟動(如下圖)。


640.gif

那么QuickStep是如何啟動的呢逐样?是如何加載數(shù)據(jù)的呢蜗字?今天就先來講講常規(guī)啟動的流程。

常規(guī)啟動

用戶通過導(dǎo)航欄Recents Button啟動脂新,首先來看下Recents Button的onClick事件如何實現(xiàn):

private void onRecentsClick(View v) {
        if (LatencyTracker.isEnabled(getContext())) {
            LatencyTracker.getInstance(getContext()).onActionStart(
                    LatencyTracker.ACTION_TOGGLE_RECENTS);
        }
        mStatusBar.awakenDreams();
        mCommandQueue.toggleRecentApps();
    }

這里通過CommandQueue發(fā)出啟動Recents的消息挪捕。而Recents.java使用了CommandQueue的callback接口:

    /**
     * Toggles the Recents activity.
     */
    @Override
    public void toggleRecentApps() {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isUserSetup()) {
            return;
        }

        // 啟動Launcher QuickStep 9.0新特性
        // If connected to launcher service, let it handle the toggle logic
        IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
        if (overviewProxy != null) {
            final Runnable toggleRecents = () -> {
                try {
                    if (mOverviewProxyService.getProxy() != null) {
                        mOverviewProxyService.getProxy().onOverviewToggle();
                    }
                } catch (RemoteException e) {
                    Log.e(TAG, "Cannot send toggle recents through proxy service.", e);
                }
            };
            // Preload only if device for current user is unlocked
            final StatusBar statusBar = getComponent(StatusBar.class);
            if (statusBar != null && statusBar.isKeyguardShowing()) {
                statusBar.executeRunnableDismissingKeyguard(() -> {
                        // Flush trustmanager before checking device locked per user
                        mTrustManager.reportKeyguardShowingChanged();
                        mHandler.post(toggleRecents);
                    }, null,  true/*dismissShade*/ , false/*afterKeyguardGone*/ ,
                    true /*deferred*/);
            } else {
                toggleRecents.run();
            }
            return;
        }

        // SystemUI 默認(rèn)啟動Recents
        int growTarget = getComponent(Divider.class).getView().growsRecents();
        int currentUser = sSystemServicesProxy.getCurrentUser();
        if (sSystemServicesProxy.isSystemUser(currentUser)) {
            mImpl.toggleRecents(growTarget);
        } else {
            if (mSystemToUserCallbacks != null) {
                IRecentsNonSystemUserCallbacks callbacks =
                        mSystemToUserCallbacks.getNonSystemUserRecentsForUser(currentUser);
                if (callbacks != null) {
                    try {
                        callbacks.toggleRecents(growTarget);
                    } catch (RemoteException e) {
                        Log.e(TAG, "Callback failed", e);
                    }
                } else {
                    Log.e(TAG, "No SystemUI callbacks found for user: " + currentUser);
                }
            }
        }
    }

可以看出在9.0中,SystemUI啟動Recents的地方會分成兩部分争便,一個是通過AIDL跨進(jìn)程啟動QuickStep级零,另外一種則是啟動SystemUI內(nèi)部默認(rèn)的Recents。

OverviewProxyService

OverviewProxyService就是實現(xiàn)與含有QuickStep功能的Launcher進(jìn)行數(shù)據(jù)傳輸?shù)墓δ茴愔鸵遥饕獙崿F(xiàn)了與Service(Launcher)端的綁定奏纪,并將SystemUIProxy傳給Launcher鉴嗤,供Launcher內(nèi)部調(diào)用
SystemUI接口。
OverviewProxyService開機(jī)時嘗試bindService:

private void internalConnectToCurrentUser() {
        disconnectFromLauncherService();

        // If user has not setup yet or already connected, do not try to connect
        if (!mDeviceProvisionedController.isCurrentUserSetup() || !isEnabled()) {
            Log.v(TAG_OPS, "Cannot attempt connection, is setup "
                + mDeviceProvisionedController.isCurrentUserSetup() + ", is enabled "
                + isEnabled());
            return;
        }
        mHandler.removeCallbacks(mConnectionRunnable);
        Intent launcherServiceIntent = new Intent(ACTION_QUICKSTEP)
                .setPackage(mRecentsComponentName.getPackageName());
        boolean bound = false;
        try {
            bound = mContext.bindServiceAsUser(launcherServiceIntent,
                    mOverviewServiceConnection, Context.BIND_AUTO_CREATE,
                    UserHandle.of(mDeviceProvisionedController.getCurrentUser()));
        } catch (SecurityException e) {
            Log.e(TAG_OPS, "Unable to bind because of security error", e);
        }
        if (bound) {
            // Ensure that connection has been established even if it thinks it is bound
            mHandler.postDelayed(mDeferredConnectionCallback, DEFERRED_CALLBACK_MILLIS);
        } else {
            // Retry after exponential backoff timeout
            final long timeoutMs = (long) Math.scalb(BACKOFF_MILLIS, mConnectionBackoffAttempts);
            mHandler.postDelayed(mConnectionRunnable, timeoutMs);
            mConnectionBackoffAttempts++;
            Log.w(TAG_OPS, "Failed to connect on attempt " + mConnectionBackoffAttempts
                    + " will try again in " + timeoutMs + "ms");
        }
    }

并在ServiceConnect之后將SystemUIProxy傳給Launcher端序调,SystemUIProxy主要實現(xiàn)功能接口如下:

private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() {

        public GraphicBufferCompat screenshot(Rect sourceCrop, int width, int height, int minLayer,
                int maxLayer, boolean useIdentityTransform, int rotation) {
            long token = Binder.clearCallingIdentity();
            try {
                return new GraphicBufferCompat(SurfaceControl.screenshotToBuffer(sourceCrop, width,
                        height, minLayer, maxLayer, useIdentityTransform, rotation));
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        public void startScreenPinning(int taskId) {
            long token = Binder.clearCallingIdentity();
            try {
                mHandler.post(() -> {
                    StatusBar statusBar = ((SystemUIApplication) mContext).getComponent(
                            StatusBar.class);
                    if (statusBar != null) {
                        statusBar.showScreenPinningRequest(taskId, false /* allowCancel */);
                    }
                });
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        public void onSplitScreenInvoked() {
            long token = Binder.clearCallingIdentity();
            try {
                EventBus.getDefault().post(new DockedFirstAnimationFrameEvent());
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        public void onOverviewShown(boolean fromHome) {
            long token = Binder.clearCallingIdentity();
            try {
                mHandler.post(() -> {
                    for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
                        mConnectionCallbacks.get(i).onOverviewShown(fromHome);
                    }
                });
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        public void setInteractionState(@InteractionType int flags) {
            long token = Binder.clearCallingIdentity();
            try {
                if (mInteractionFlags != flags) {
                    mInteractionFlags = flags;
                    mHandler.post(() -> {
                        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
                            mConnectionCallbacks.get(i).onInteractionFlagsChanged(flags);
                        }
                    });
                }
            } finally {
                Prefs.putInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, mInteractionFlags);
                Binder.restoreCallingIdentity(token);
            }
        }

        public Rect getNonMinimizedSplitScreenSecondaryBounds() {
            long token = Binder.clearCallingIdentity();
            try {
                Divider divider = ((SystemUIApplication) mContext).getComponent(Divider.class);
                if (divider != null) {
                    return divider.getView().getNonMinimizedSplitScreenSecondaryBounds();
                }
                return null;
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        public void setBackButtonAlpha(float alpha, boolean animate) {
            long token = Binder.clearCallingIdentity();
            try {
                mHandler.post(() -> {
                    notifyBackButtonAlphaChanged(alpha, animate);
                });
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }
    };

這里不過多解讀跨進(jìn)程的具體功能醉锅,后續(xù)對QuickStep單獨(dú)功介紹時進(jìn)行一一介紹。

TouchInteractionService

TouchInteractionService就是Launcher端的Service发绢,代碼路徑是:packages/apps/Launcher3/quickstep/src/com/android/quickstep/TouchInteractionService.java硬耍,通過SystemUI中bind啟動。前面SystemUI在Recents中通過AIDL調(diào)用Launcher端onOverviewToggle:

@Override
public void onOverviewToggle() {
     mOverviewCommandHelper.onOverviewToggle();
}

這里的mOverviewCommandHelper對應(yīng)的就是OverviewCommandHelper边酒,其代碼路徑是:
packages/apps/Launcher3/quickstep/src/com/android/quickstep/OverviewCommandHelper.java默垄,其作用主要是啟動Launcher中的QuickStep界面。代碼邏輯詳情如下:

public void onOverviewToggle() {
     // If currently screen pinning, do not enter overview
     if (mAM.isScreenPinningActive()) {
         return;
     }
     mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
     mMainThreadExecutor.execute(new RecentsActivityCommand<>());
}

這里最關(guān)鍵的邏輯就是mMainThreadExecutor.execute(new RecentsActivityCommand<>());這里RecentsActivityCommand是OverviewCommandHelper中的一個內(nèi)部類甚纲,因為implement了Runnable口锭,因此mMainThreadExecutor主要執(zhí)行的就是該類的run方法,其代碼邏輯如下:

@Override
public void run() {
    long elapsedTime = mCreateTime - mLastToggleTime;
    mLastToggleTime = mCreateTime;
    if (!handleCommand(elapsedTime)) {
        // Start overview
        if (!mHelper.switchToRecentsIfVisible(true)) {
             mListener = mHelper.createActivityInitListener(this::onActivityReady);
             mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation,
                     Context, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION);
             }
        }
}

這里邏輯分成兩塊介杆,一是連續(xù)點(diǎn)擊判斷和實現(xiàn)雙擊切換應(yīng)用鹃操,這塊邏輯在handleCommand(elapsedTime),二是啟動QuickStep對應(yīng)的Activity界面春哨,并執(zhí)行對應(yīng)的窗口動畫荆隘,這里的overviewIntent是在OverviewCommandHelper初始化的時候進(jìn)行初始化的,其關(guān)鍵代碼邏輯如下:

private void initOverviewTargets() {
        ComponentName defaultHome = PackageManagerWrapper.getInstance()
                .getHomeActivities(new ArrayList<>());
        final String overviewIntentCategory;
        //判斷默認(rèn)Launcher是否為自身
        if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) {
            // User default home is same as out home app. Use Overview integrated in Launcher.
            overviewComponent = mMyHomeComponent;
            mActivityControlHelper = new LauncherActivityControllerHelper();
            overviewIntentCategory = Intent.CATEGORY_HOME;
            if (mUpdateRegisteredPackage != null) {
                // Remove any update listener as we don't care about other packages.
                mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
                mUpdateRegisteredPackage = null;
            }
        } else {
            // The default home app is a different launcher. Use the fallback Overview instead.
            overviewComponent = new ComponentName(mContext, RecentsActivity.class);
            mActivityControlHelper = new FallbackActivityControllerHelper(defaultHome);
            overviewIntentCategory = Intent.CATEGORY_DEFAULT;
            // User's default home app can change as a result of package updates of this app (such
            // as uninstalling the app or removing the "Launcher" feature in an update).
            // Listen for package updates of this app (and remove any previously attached
            // package listener).
            if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) {
                if (mUpdateRegisteredPackage != null) {
                    mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
                }
                mUpdateRegisteredPackage = defaultHome.getPackageName();
                IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED);
                updateReceiver.addAction(ACTION_PACKAGE_CHANGED);
                updateReceiver.addAction(ACTION_PACKAGE_REMOVED);
                updateReceiver.addDataScheme("package");
                updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage,
                        PatternMatcher.PATTERN_LITERAL);
                mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver);
            }
        }
        overviewIntent = new Intent(Intent.ACTION_MAIN)
                .addCategory(overviewIntentCategory)
                .setComponent(overviewComponent)
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}

這里的邏輯主要為判斷當(dāng)前系統(tǒng)默認(rèn)Launcher是不是Launcher3自身赴背,如果是將自身component賦給
overviewComponent椰拒,并反注冊Launcher切換廣播接受者;如果不是凰荚,則將RecentsActivity的component賦給overviewComponent燃观,兩者的區(qū)別就是啟動的Recents界面一個帶有hotseat(Launcher3獨(dú)有),一個則是普通的Recents界面便瑟。

總結(jié)

到這里QuickStep的常規(guī)按鍵啟動流程介紹結(jié)束了缆毁,下面附上一張對應(yīng)的時序圖工大家參考:


quickstepNormalStart.png

下一篇,將給大家?guī)硎謩輪臃治觥?/p>

本篇文章已獨(dú)家授權(quán)公眾號ApeClub使用到涂,轉(zhuǎn)載請注明出處脊框!更多好文章請關(guān)注公眾號ApeClub。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末践啄,一起剝皮案震驚了整個濱河市浇雹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屿讽,老刑警劉巖昭灵,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡虎锚,警方通過查閱死者的電腦和手機(jī)硫痰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窜护,“玉大人效斑,你說我怎么就攤上這事≈悖” “怎么了缓屠?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長护侮。 經(jīng)常有香客問我敌完,道長,這世上最難降的妖魔是什么羊初? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任滨溉,我火速辦了婚禮,結(jié)果婚禮上长赞,老公的妹妹穿的比我還像新娘晦攒。我一直安慰自己,他們只是感情好得哆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布脯颜。 她就那樣靜靜地躺著,像睡著了一般贩据。 火紅的嫁衣襯著肌膚如雪捂齐。 梳的紋絲不亂的頭發(fā)上馁蒂,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天堵第,我揣著相機(jī)與錄音脖岛,去河邊找鬼。 笑死近尚,一個胖子當(dāng)著我的面吹牛蠕啄,可吹牛的內(nèi)容都是我干的场勤。 我是一名探鬼主播戈锻,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼和媳!你這毒婦竟也來了格遭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤留瞳,失蹤者是張志新(化名)和其女友劉穎拒迅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡璧微,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年作箍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片前硫。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡胞得,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屹电,到底是詐尸還是另有隱情阶剑,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布危号,位于F島的核電站牧愁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏外莲。R本人自食惡果不足惜猪半,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望偷线。 院中可真熱鬧办龄,春花似錦、人聲如沸淋昭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翔忽。三九已至英融,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間歇式,已是汗流浹背驶悟。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留材失,地道東北人痕鳍。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像龙巨,于是被迫代替她去往敵國和親笼呆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容