優(yōu)雅的構(gòu)建 Android 項(xiàng)目——側(cè)滑返回使用及原理分析

大屏幕手機(jī)在返回前頁操作時(shí)窥突,點(diǎn)擊左上角的 APP 內(nèi)返回鍵或者手機(jī)自帶的返回按鍵都不是很方便蒂秘,這時(shí)候能通過屏幕側(cè)滑退出當(dāng)前頁面體驗(yàn)就會(huì)好很多了本今。但是 Android 系統(tǒng)并沒有想 IOS 一樣自帶側(cè)滑返回提岔,好在 Android 輪子比較多仙蛉,本文記錄一下個(gè)人開源項(xiàng)目 PandaEye 中使用的側(cè)滑返回庫(kù) SwipBackLayout 笋敞。該庫(kù)參考 github 上的開源庫(kù) SwipeBackLayout 做了一些簡(jiǎn)化碱蒙;

使用方式

定義側(cè)滑基礎(chǔ) Activity

側(cè)滑返回的實(shí)現(xiàn)是基于 Activity 的,可以直接繼承 Activity 或者繼承自己應(yīng)用實(shí)現(xiàn)的 BaseActivity 然后實(shí)現(xiàn) SwipeBackLayout.SwipeListener 接口即可.

public class SwipeBackActivity extends BaseActivity implements SwipeBackLayout.SwipeListener {
    protected SwipeBackLayout layout;
    private ArgbEvaluator argbEvaluator;

    @SuppressLint("InflateParams")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        layout = (SwipeBackLayout) LayoutInflater.from(this).inflate(
                R.layout.swipeback_base, null);
        layout.attachToActivity(this);
        argbEvaluator = new ArgbEvaluator();
        layout.addSwipeListener(this);
        if (Build.VERSION.SDK_INT >= 23) {
            currentStatusColor = getResources().getColor(R.color.colorPrimaryDark, null);
        } else {
            currentStatusColor = getResources().getColor(R.color.colorPrimaryDark);
        }
    }

    // 提供給子類設(shè)置 ViewPager 的接口夯巷,用于 SwipeLayout 中處理滑動(dòng)沖突
    public void addViewPager(ViewPager pager) {
        layout.addViewPager(pager);
    }

}

效果優(yōu)化

需要側(cè)滑返回的 Activity 繼承 SwipeBackActivity 即可實(shí)現(xiàn)側(cè)滑返回的功能了赛惩,但是側(cè)滑過程中返回界面會(huì)被默認(rèn)的窗口背景顏色覆蓋,因此我們需要把實(shí)現(xiàn)側(cè)滑返回的界面的 theme 做一些小小的優(yōu)化趁餐,將背景設(shè)置為透明狀態(tài)喷兼,并設(shè)置進(jìn)入和退出的動(dòng)畫。
style 中的屬性設(shè)置

    <!--全屏加透明-->
    <style name="TranslucentFullScreenTheme" parent="AppTheme">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <!--<item name="android:windowAnimationStyle">@android:style/Animation</item>-->
        <item name="android:windowAnimationStyle">@style/AnimationActivity</item>
    </style>
    <!--動(dòng)畫設(shè)置-->
        <style name="AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/base_slide_right_in</item>
        <item name="android:activityOpenExitAnimation">@anim/base_slide_right_out</item>
        <item name="android:activityCloseEnterAnimation">@anim/base_slide_right_in</item>
        <item name="android:activityCloseExitAnimation">@anim/base_slide_right_out</item>
    </style>

界面進(jìn)入動(dòng)畫

<!--base_slide_right_in-->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="300"
        android:fromXDelta="100.0%"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="0.0%" />

</set>

界面退出動(dòng)畫

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="300"
        android:fromXDelta="100.0%"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="0.0%" />

</set>

然后在 manifest 文件中將繼承 SwipeBackActivity 的 Activity 的 theme 設(shè)置為 TranslucentFullScreenTheme 即可解決滑動(dòng)過程中背景覆蓋問題后雷。

原理淺析

Activity 中 View 視圖層級(jí)

要明白側(cè)滑返回的原理我們得先明白 Android Activity 界面的視圖層級(jí)關(guān)系:

Activity 界面視圖層級(jí)
Activity 界面視圖層級(jí)

Activity 和 PhoneWindow 這里可以忽略季惯,重點(diǎn)在 DecorView上。這個(gè) DecorView 是 Activity 中 View 布局的祖宗級(jí)布局臀突,是一個(gè) FrameLayout勉抓,通過 getWindow().getDecorView() 可以獲取到對(duì)象;如圖中 DecorView 有且僅有一個(gè) LinearLayout 子布局候学,即圖中的黃色部分藕筋。這個(gè) LinearLayout 一般情況下又包含有 ViewStub 和 FrameLayout 兩部分(不同的主題 Theme 可能會(huì)多處一些對(duì)象),ViewStub 即是應(yīng)用的 ActionBar梳码,他會(huì)根據(jù) theme 來決定是否真正引入 ActionBar 到界面顯示纯出。而這個(gè) FrameLayout 中的內(nèi)容即是我們寫的 layout 布局文件中想要展示的內(nèi)容蹭越。需要注意如果 Activity 繼承自 AppComcatActivity 則這個(gè) FrameLayout 中還會(huì)有兩個(gè)子布局,第一個(gè)子布局中的內(nèi)容才是我們寫的布局文件中的內(nèi)容

實(shí)現(xiàn)原理

通過 SDK 自帶的視圖分析工具 Hierarchy View 我們可以看到視圖的如下分布:

DecorView 視圖節(jié)點(diǎn)
DecorView 視圖節(jié)點(diǎn)

界面所有顯示的內(nèi)容其實(shí)都在這個(gè) LinearLayout 中,如果我們給這個(gè) LinearLayout 增加一個(gè)父布局然后對(duì)這個(gè)父布局進(jìn)行滑動(dòng)處理就可以實(shí)現(xiàn)界面的整體滑動(dòng)务漩,即把整個(gè)可視界面放入一個(gè)滑動(dòng)抽屜。因此實(shí)現(xiàn)滑動(dòng)的界面視圖應(yīng)該變成如下的樣子:

可側(cè)滑的界面的 DecorView 試圖節(jié)點(diǎn)
可側(cè)滑的界面的 DecorView 試圖節(jié)點(diǎn)

如圖 SwipeBackLayout 即是添加的滑動(dòng)抽屜嗤练,接下來我們看一下 SwipeBackLayout 中是怎樣實(shí)現(xiàn)在 LinearLayout 上層插入一個(gè) SwipeBackLayout 布局的害碾。
在 SwipeBackActivity 中只調(diào)用了 attachToActivity() 方法,方法中代碼如下:

    public void attachToActivity(Activity activity) {
        mActivity = activity;
        TypedArray a = activity.getTheme().obtainStyledAttributes(
                new int[]{android.R.attr.windowBackground});
        int background = a.getResourceId(0, 0);
        a.recycle();
        //獲取到 DecorView 對(duì)象
        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        Log.i("decorChildCount", decor.getChildCount() + "");
        ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
        Log.i("decorChild", decorChild.toString());
        //重置背景色資源
        decorChild.setBackgroundResource(background);
        //decorView 中將子布局移除
        decor.removeView(decorChild);
        //SwipeBackLayout 添加從decorView中移除布局
        addView(decorChild);
        //將ContentView設(shè)置為decorChild的父布局即添加進(jìn)來的SwipeBackLayout
        setContentView(decorChild);
        //將SwipeBackLayout添加進(jìn)DecorView
        decor.addView(this);
    }

從中我添加的注釋不難看出,實(shí)現(xiàn)替換的流程:

  • 1甸各、傳入的 activity 對(duì)象獲取到 DecorView
  • 2垛贤、DecorView.getChildAt(0) 獲取到 LinearLayout 對(duì)象
  • 3、將 LinearLayout 背景資源重置趣倾,并從 DecorView 中移除
  • 4聘惦、將 LinearLayout 添加到自定義的 SwipeBackLayout 中
  • 5、將自定義的 SwipeBackLayout 添加到 DecorView 中

滑動(dòng)處理及 ViewPager 處理

在 SwipeBackLayout 中通過重寫 onInterceptTouchEvent(MotionEvent ev) 方法和 onTouchEvent(MotionEvent ev) 方法來實(shí)現(xiàn)側(cè)滑返回事件的處理及對(duì) ViewPager 滑動(dòng)的兼容的儒恋。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //處理ViewPager沖突問題
        ViewPager mViewPager = getTouchViewPager(mViewPagers, ev);
        //當(dāng)無觸摸ViewPager或者該ViewPager未滑動(dòng)到最左則不對(duì)滑動(dòng)時(shí)間進(jìn)行攔截
        if (mViewPager != null && mViewPager.getCurrentItem() != 0) {
            return super.onInterceptTouchEvent(ev);
        }
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = tempX = (int) ev.getRawX();
                downY = (int) ev.getRawY();
                canSwipe = downX <= viewWidth / 2;
                if (!canSwipe) {
                    return super.onInterceptTouchEvent(ev);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (!canSwipe) {
                    return super.onInterceptTouchEvent(ev);
                }
                int moveX = (int) ev.getRawX();
                // 滿足此條件屏蔽SildingFinishLayout里面子類的touch事件
                if (moveX - downX > mTouchSlop
                        && Math.abs((int) ev.getRawY() - downY) < mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

在手指按下的時(shí)候相較于 onTouchEvent() 方法 onInterceptTouchEvent() 方法會(huì)先執(zhí)行善绎,在此方法中先判斷當(dāng)前觸摸是否為 ViewPager,是 ViewPager 則判斷是否滑動(dòng)到了 ViewPager 的最左側(cè)诫尽。如果觸摸的 ViewPager 且未滑動(dòng)到最左側(cè)則不對(duì)事件進(jìn)行攔截交給 ViewPager 處理觸摸事件禀酱,否則觸摸位置進(jìn)行判斷,在有效區(qū)域內(nèi)則記錄觸摸開始點(diǎn)牧嫉,否則按系統(tǒng)默認(rèn)方式處理剂跟。在移動(dòng)事件中會(huì)根據(jù)按下事件的判斷結(jié)果決定是否按默認(rèn)方式處理,當(dāng)需要處理側(cè)滑時(shí)會(huì)再次判斷如果 X 方向的滑動(dòng)大于最小有效滑動(dòng)距離 Y方向滑動(dòng)距離小于最小有效滑動(dòng)距離則此次事件將會(huì)被 SwipeBackLayout 所消費(fèi)酣藻,將進(jìn)入 SwipeBackLayout 的 onTouchEvent() 方法中的處理邏輯曹洽。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if (!canSwipe) {
                    return super.onInterceptTouchEvent(event);
                }
                int moveX = (int) event.getRawX();
                int deltaX = tempX - moveX;
                tempX = moveX;
                if (moveX - downX > mTouchSlop
                        && Math.abs((int) event.getRawY() - downY) < mTouchSlop) {
                    isSilding = true;
                }
                if (moveX - downX >= 0 && isSilding) {
                    //deltaX 為單次移動(dòng)的距離向右滑為負(fù)數(shù)
                    // TODO: 2017/6/22 實(shí)現(xiàn) y 方向的移動(dòng),即向右任意方向滑出界面
                    mContentView.scrollBy(deltaX, 0);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!canSwipe) {
                    return super.onInterceptTouchEvent(event);
                }
                isSilding = false;
                if (mContentView.getScrollX() <= -viewWidth / 4) {
                    isFinish = true;
                    scrollRight();
                } else {
                    scrollOrigin();
                    isFinish = false;
                }
                break;
        }
        return true;
    }

同樣此方法中也會(huì)根據(jù) onInterceptTouchEvent() 中的 DOWN 事件的判定結(jié)果 canSwipe 來決定是否按默認(rèn)方式消費(fèi)事件辽剧,MOVE 事件中如果滿足側(cè)滑條件則會(huì)調(diào)用 scrollBy() 將 mContentView 按滑動(dòng)方向進(jìn)行移動(dòng)送淆,而此處的 mContentView 即是 SwipeBackLayout 自身,因此整個(gè)顯示的界面會(huì)被按照滑動(dòng)方向移動(dòng)怕轿。當(dāng)手指抬起時(shí)如果滑動(dòng)距離超過 1/4 界面寬度(可以按自己需求調(diào)整)偷崩,則視為側(cè)滑返回完成,讓 Scroller 自動(dòng)完成剩余距離的滑動(dòng)撞羽,否則讓 Scroller 恢復(fù)到滑動(dòng)起始位置

    /**
     * 滾動(dòng)出界面
     */
    private void scrollRight() {
        final int delta = (viewWidth + mContentView.getScrollX());
        // 調(diào)用startScroll方法來設(shè)置一些滾動(dòng)的參數(shù)阐斜,我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來滾動(dòng)item
        mScroller.startScroll(mContentView.getScrollX(), 0, -delta + 1, 0,
                Math.abs(delta));
        postInvalidate();
    }

    /**
     * 滾動(dòng)到起始位置
     */
    private void scrollOrigin() {
        int delta = mContentView.getScrollX();
                // 調(diào)用startScroll方法來設(shè)置一些滾動(dòng)的參數(shù),我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來滾動(dòng)item
        mScroller.startScroll(mContentView.getScrollX(), 0, -delta, 0,
                Math.abs(delta));
        postInvalidate();
    }
    
    /**
     * 具體執(zhí)行 Scroller 中的滾動(dòng)及將滑動(dòng)距離傳遞給外部接口
     */
     @Override
    public void computeScroll() {
        Log.i("computeScroll","computeScroll");
        if (mSwipeListener != null) {
            double scrollx = Math.abs(mContentView.getScrollX());
            double offset = scrollx / viewWidth;
            if (offset > 0.9) {
                offset = 1d;
            }
            mSwipeListener.swipeValue(offset);
        }
        if (mScroller.computeScrollOffset()) {
            Log.i("computeScroll","mScroller");
            mContentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
            if (mScroller.isFinished() && isFinish) {
                mActivity.finish();
            }
        }
    }

結(jié)語

以上就是簡(jiǎn)化后的側(cè)滑返回的基本使用和原理的簡(jiǎn)單分析放吩,完整代碼可以參考 PandaEye歡迎 Star智听。文章一遍過為反復(fù)檢查如有不妥之處歡迎大家踴躍交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末渡紫,一起剝皮案震驚了整個(gè)濱河市到推,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惕澎,老刑警劉巖莉测,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異唧喉,居然都是意外死亡捣卤,警方通過查閱死者的電腦和手機(jī)忍抽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來董朝,“玉大人鸠项,你說我怎么就攤上這事∽咏” “怎么了祟绊?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)哥捕。 經(jīng)常有香客問我牧抽,道長(zhǎng),這世上最難降的妖魔是什么遥赚? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任扬舒,我火速辦了婚禮,結(jié)果婚禮上凫佛,老公的妹妹穿的比我還像新娘讲坎。我一直安慰自己,他們只是感情好御蒲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布衣赶。 她就那樣靜靜地躺著,像睡著了一般厚满。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碧磅,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天碘箍,我揣著相機(jī)與錄音,去河邊找鬼鲸郊。 笑死丰榴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秆撮。 我是一名探鬼主播四濒,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼职辨!你這毒婦竟也來了盗蟆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤舒裤,失蹤者是張志新(化名)和其女友劉穎喳资,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腾供,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仆邓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年鲜滩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片节值。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡徙硅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搞疗,到底是詐尸還是另有隱情闷游,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布贴汪,位于F島的核電站脐往,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扳埂。R本人自食惡果不足惜业簿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阳懂。 院中可真熱鬧梅尤,春花似錦、人聲如沸岩调。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽号枕。三九已至缰揪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間葱淳,已是汗流浹背钝腺。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赞厕,地道東北人艳狐。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像皿桑,于是被迫代替她去往敵國(guó)和親毫目。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

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