Android系統(tǒng)中SwipeDismissLayout(右滑退出)

背景

最近在做一個手表項目, Android 7.1.1系統(tǒng), 系統(tǒng)中有個全局從左向右滑動退出當前Activity功能, 本以為是哪位同事添加的功能, 后來看了下代碼才發(fā)現(xiàn)是Android系統(tǒng)本身就有的功能(Android 5.0加入的), 使用也非常方便, 下面就來講一下這個功能如何啟用和基本原理.

右滑退出原理

右滑退出基本原理很簡單, 在某個ViewGroup中, 攔截onTouch事件(onInterceptTouchEvent()), 根據(jù)滑動手勢改變View或者Window的偏移量, 在達到某個閾值后, 判定當前手勢為退出, 調(diào)用Activity退出方法(finish() onBackPressed())即可.

但是如果你只是這樣操作的話,會發(fā)現(xiàn)滑動過程中的背景是黑的, 而不是顯示當前Activity后面的Activity內(nèi)容, 這是因為, Activity執(zhí)行onStop()后, 處于一種不可見狀態(tài), 要想讓當前Activity后面的Activity被繪制出來, 需要用到Activity的兩個函數(shù): convertFromTranslucent() 和 convertToTranslucent()
我們來看下對應(yīng)的函數(shù)解釋:
frameworks/base/core/java/android/app/Activity.java

/**
 * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} to a
 * fullscreen opaque Activity.
 * <p>
 * Call this whenever the background of a translucent Activity has changed to become opaque.
 * Doing so will allow the {@link android.view.Surface} of the Activity behind to be released.
 * <p>
 * This call has no effect on non-translucent activities or on activities with the
 * {@link android.R.attr#windowIsFloating} attribute.
 *
 * @see #convertToTranslucent(android.app.Activity.TranslucentConversionListener,
 * ActivityOptions)
 * @see TranslucentConversionListener
 *
 * @hide
 */
@SystemApi
public void convertFromTranslucent() {
    ......
}

/**
 * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} back from
 * opaque to translucent following a call to {@link #convertFromTranslucent()}.
 * <p>
 * Calling this allows the Activity behind this one to be seen again. Once all such Activities
 * have been redrawn {@link TranslucentConversionListener#onTranslucentConversionComplete} will
 * be called indicating that it is safe to make this activity translucent again. Until
 * {@link TranslucentConversionListener#onTranslucentConversionComplete} is called the image
 * behind the frontmost Activity will be indeterminate.
 * <p>
 * This call has no effect on non-translucent activities or on activities with the
 * {@link android.R.attr#windowIsFloating} attribute.
 *
 * @param callback the method to call when all visible Activities behind this one have been
 * drawn and it is safe to make this Activity translucent again.
 * @param options activity options delivered to the activity below this one. The options
 * are retrieved using {@link #getActivityOptions}.
 * @return <code>true</code> if Window was opaque and will become translucent or
 * <code>false</code> if window was translucent and no change needed to be made.
 *
 * @see #convertFromTranslucent()
 * @see TranslucentConversionListener
 *
 * @hide
 */
@SystemApi
public boolean convertToTranslucent(TranslucentConversionListener callback,
        ActivityOptions options) {
    ......
}

簡單解釋就是調(diào)用當前Activity的convertToTranslucent(), 會導(dǎo)致其后面的Activity變?yōu)榭梢? 這正是我們想要的效果, convertFromTranslucent()則相反, 讓后面Activity不可見.
知道這些內(nèi)容以后, 我們就可以在滑動開始的時候調(diào)用convertToTranslucent()來讓后面的Activity可見. 基本原理了解后,下面看下具體代碼實現(xiàn).

代碼實現(xiàn)

frameworks/base/core/java/com/android/internal/widget/SwipeDismissLayout.java
首先在SwipeDismissLayout.java中攔截觸摸事件:

    @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            resetMembers();
            mDownX = ev.getRawX();
            mDownY = ev.getRawY();
            mActiveTouchId = ev.getPointerId(0);
            mVelocityTracker = VelocityTracker.obtain();
            mVelocityTracker.addMovement(ev);
            break;
         //部分代碼省略...
        case MotionEvent.ACTION_POINTER_UP:
            actionIndex = ev.getActionIndex();
            int pointerId = ev.getPointerId(actionIndex);
            if (pointerId == mActiveTouchId) {
                // This was our active pointer going up. Choose a new active pointer.
                int newActionIndex = actionIndex == 0 ? 1 : 0;
                mActiveTouchId = ev.getPointerId(newActionIndex);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            //部分代碼省略...
            float dx = ev.getRawX() - mDownX;
            float x = ev.getX(pointerIndex);
            float y = ev.getY(pointerIndex);
            if (dx != 0 && canScroll(this, false, dx, x, y)) {
                mDiscardIntercept = true;
                break;
            }
            updateSwiping(ev);
            break;
    }

    return !mDiscardIntercept && mSwiping;
}

這里面就是一些滑動邏輯判斷, 主要判斷是否是右滑, 如果是就攔截當前事件, 這樣后續(xù)事件的onTouchEvent()就不會傳到子View中, 而是在當前View中的onTouchEvent()中進行處理.

onTouchEvent():

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // 部分代碼省略...
    ev.offsetLocation(mTranslationX, 0);
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_UP:
            updateDismiss(ev);
            //判斷當前動作是取消右滑還是結(jié)束Activity
            if (mDismissed) {
                dismiss();
            } else if (mSwiping) {
                cancel();
            }
            resetMembers();
            break;
        // 部分代碼省略....
        case MotionEvent.ACTION_MOVE:
            mVelocityTracker.addMovement(ev);
            mLastX = ev.getRawX();
            updateSwiping(ev);
            if (mSwiping) {
                if (mUseDynamicTranslucency && getContext() instanceof Activity) {
                    //如果是右滑并且是Activity, 調(diào)用convertToTranslucent() 讓后面Activity可見
                    ((Activity) getContext()).convertToTranslucent(null, null);
                }
                //此處會調(diào)用到PhoneWindow.java中, 來讓W(xué)indow偏移
                setProgress(ev.getRawX() - mDownX);
                break;
            }
    }
    return true;
}

此部分代碼主要包括右滑動作,取消右滑(cancel())以及右滑手勢完成后結(jié)束Activity(dismiss()). 同時, 如果開始右滑, 則調(diào)用 convertToTranslucent(), 讓后面Activity可見, 這樣當前Activity向右偏移后, 才能正常看到后面的Activity內(nèi)容. 滑動過程中Activity的偏移, 結(jié)束, 是否啟用右滑等代碼的實現(xiàn)在PhoneWindow.java中,下面繼續(xù)看源碼.

啟用/禁用右滑退出功能

SwipeDismissLayout是在什么時候被加載的呢? 這部分是在調(diào)用setContentView()之后的流程中來實現(xiàn)的. Activity的setContentView()最終會調(diào)用到PhoneWindow中, 在PhoneWindow中的generateLayout()函數(shù)中, 會根據(jù)一些條件, 來決定加載那個布局, 代碼如下:
frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java


protected ViewGroup generateLayout(DecorView decor) {
    //部分代碼省略...
    //如果主題中windowSwipeToDismiss為true, 添加FEATURE_SWIPE_TO_DISMISS
    if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
        requestFeature(FEATURE_SWIPE_TO_DISMISS);
    }
    //部分代碼省略...
    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        // 如果包含 FEATURE_SWIPE_TO_DISMISS,則加載的布局是screen_swipe_dismiss.xml
        layoutResource = R.layout.screen_swipe_dismiss;
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
        // System.out.println("Title Icons!");
    }
    //部分代碼省略...
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        //注冊滑動相關(guān)回調(diào)
        registerSwipeCallbacks();
    }
    //部分代碼省略...
    return contentParent;
}

這個函數(shù)內(nèi)容很多, 我只挑了關(guān)鍵代碼, 可以看到, 關(guān)鍵點即加載對應(yīng)的layout文件:

layoutResource = R.layout.screen_swipe_dismiss;

screen_swipe_dismiss.xml的路徑為:
frameworks/base/core/res/res/layout/screen_swipe_dismiss.xml

內(nèi)容就一個SwipeDismissLayout布局:

<com.android.internal.widget.SwipeDismissLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/content"
    android:fitsSystemWindows="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

使用這個布局后, setContentView的內(nèi)容就會加入到此布局之中.

因此,啟用右滑功能可以通過兩種方式實現(xiàn):

  1. 調(diào)用函數(shù)方式(Activity中) : getWindow().requestFeature(Window.FEATURE_SWIPE_TO_DISMISS);, 必須在setContentView()之前進行設(shè)置
  2. 通過主題配置, 在主題樣式中加入:<item name="android:windowSwipeToDismiss">true</item>

加入這個Feature后, PhoneWindow.java中就會加載screen_swipe_dismiss.xml這個布局, APP布局的"content"就會成為SwipeDismissLayout的子View, 從而達到攔截事件以及實現(xiàn)右滑功能.

另外我們可以看到, 滑動的回調(diào)也在PhoneWindow中實現(xiàn),代碼如下:

private void registerSwipeCallbacks() {
    SwipeDismissLayout swipeDismiss =
            (SwipeDismissLayout) findViewById(R.id.content);
    swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() {
        @Override
        public void onDismissed(SwipeDismissLayout layout) {
            //此處最終會調(diào)到Activity的onBackPressed(), 從而結(jié)束當前Activity
            dispatchOnWindowDismissed(false /*finishTask*/);
        }
    });
    swipeDismiss.setOnSwipeProgressChangedListener(
            new SwipeDismissLayout.OnSwipeProgressChangedListener() {
                private static final float ALPHA_DECREASE = 0.5f;
                private boolean mIsTranslucent = false;
                @Override
                public void onSwipeProgressChanged(
                        SwipeDismissLayout layout, float progress, float translate) {
                    //通過設(shè)置WindowManager.LayoutParams來實現(xiàn)滑動偏移效果
                    WindowManager.LayoutParams newParams = getAttributes();
                    newParams.x = (int) translate;
                    newParams.alpha = 1 - (progress * ALPHA_DECREASE);
                    setAttributes(newParams);
                    //部分代碼省略...
                }

                @Override
                public void onSwipeCancelled(SwipeDismissLayout layout) {
                    //取消滑動后重置相關(guān)參數(shù)
                    WindowManager.LayoutParams newParams = getAttributes();
                    newParams.x = 0;
                    newParams.alpha = 1;
                    setAttributes(newParams);
                    setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
                }
            });
}

邏輯也比較簡單, 即Activity的關(guān)閉, 滑動偏移效果和取消滑動這三個關(guān)鍵邏輯,都是在這里實現(xiàn)的.

實際測試

既然是Android 5.0就加入的功能, 想必一般廠商不會沒事去掉這個功能,我拿我手上的SONY Xperia Z5(Android 7.1.1)試了下, 隨便寫一個Activity進行測試:
setContentView()之前調(diào)用 getWindow().requestFeature(Window.FEATURE_SWIPE_TO_DISMISS);
或者在主題中加入 <item name="android:windowSwipeToDismiss">true</item>
都能實現(xiàn)右滑退出功.

swipe.png

但是由于本身實現(xiàn)邏輯問題, 取消滑動默認會進入全屏狀態(tài), 如果做系統(tǒng)開發(fā)的, 需要用這個功能的話, 可以根據(jù)需求進行修改.

總結(jié)

關(guān)于右滑退出這個系統(tǒng)功能, 關(guān)鍵點如下:

  1. 通過convertFromTranslucent() 和 convertToTranslucent()來實現(xiàn)讓背后的Activity可見和不可見
  2. 在ViewGroup中攔截onTouchEvent事件, 通過手勢實現(xiàn)右滑.
  3. 給Window添加Feature "FEATURE_SWIPE_TO_DISMISS", 會讓系統(tǒng)加載SwipeDismissLayout來作為App布局的父View.
  4. 右滑偏移效果, 取消右滑, 關(guān)閉Activity都在PhoneWindow中進行處理

存在的問題:
從上面代碼中可以看到, 攔截onTouch事件是判讀是不是向右滑動了,并且會判斷字View是否可以滑動, 如果不可以滑動, 右滑事件就會被攔截, 因此當App中有右滑的需求, 就會產(chǎn)生手勢沖突, App的右滑事件會被攔截, 所以如果實際要用這個功能, 還需進行優(yōu)化, 比如只在邊緣向右滑動的時候才攔截事件, 這樣就不會產(chǎn)生手勢沖突了, 或者App自己處理這種類型的沖突, 調(diào)用requestDisallowInterceptTouchEvent(boolean disallowIntercept)根據(jù)需求禁用攔截.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捧灰,隨后出現(xiàn)的幾起案子濒募,更是在濱河造成了極大的恐慌麻汰,老刑警劉巖坯钦,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異阱缓,居然都是意外死亡戳护,警方通過查閱死者的電腦和手機金抡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門瀑焦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梗肝,你說我怎么就攤上這事榛瓮。” “怎么了巫击?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵禀晓,是天一觀的道長。 經(jīng)常有香客問我坝锰,道長粹懒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任顷级,我火速辦了婚禮凫乖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘弓颈。我一直安慰自己帽芽,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布翔冀。 她就那樣靜靜地躺著嚣镜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪橘蜜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天付呕,我揣著相機與錄音计福,去河邊找鬼。 笑死徽职,一個胖子當著我的面吹牛象颖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姆钉,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼说订,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了潮瓶?” 一聲冷哼從身側(cè)響起陶冷,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毯辅,沒想到半個月后埂伦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡思恐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年沾谜,在試婚紗的時候發(fā)現(xiàn)自己被綠了膊毁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡基跑,死狀恐怖婚温,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情媳否,我是刑警寧澤栅螟,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站逆日,受9級特大地震影響嵌巷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜室抽,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一搪哪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坪圾,春花似錦晓折、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掠河。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蓝牲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工徐许, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留含友,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓钮热,卻偏偏與公主長得像填抬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子隧期,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,522評論 25 707
  • afinalAfinal是一個android的ioc飒责,orm框架 https://github.com/yangf...
    wgl0419閱讀 6,263評論 1 9
  • afinalAfinal是一個android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,401評論 2 45
  • 才一天不送我仆潮,我就有些難過宏蛉,我也不清楚我到底在難過什么,或許是他昨天蹭答應(yīng)我的沒有辦到鸵闪?或許我習(xí)慣了有他的陪伴檐晕,可...
  • 兩天畫了一枝花
    宛茹閱讀 260評論 2 5