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

大屏幕手機(jī)在返回前頁(yè)操作時(shí),點(diǎn)擊左上角的 APP 內(nèi)返回鍵或者手機(jī)自帶的返回按鍵都不是很方便,這時(shí)候能通過(guò)屏幕側(cè)滑退出當(dāng)前頁(yè)面體驗(yàn)就會(huì)好很多了。但是 Android 系統(tǒng)并沒(méi)有想 IOS 一樣自帶側(cè)滑返回,好在 Android 輪子比較多,本文記錄一下個(gè)人開(kāi)源項(xiàng)目 PandaEye 中使用的側(cè)滑返回庫(kù) SwipBackLayout 。該庫(kù)參考 github 上的開(kāi)源庫(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è)滑過(guò)程中返回界面會(huì)被默認(rèn)的窗口背景顏色覆蓋,因此我們需要把實(shí)現(xiàn)側(cè)滑返回的界面的 theme 做一些小小的優(yōu)化,將背景設(shè)置為透明狀態(tài),并設(shè)置進(jìn)入和退出的動(dòng)畫(huà)。
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)畫(huà)設(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)畫(huà)

<!--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)畫(huà)

<?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)過(guò)程中背景覆蓋問(wèn)題。

原理淺析

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,通過(guò) getWindow().getDecorView() 可以獲取到對(duì)象;如圖中 DecorView 有且僅有一個(gè) LinearLayout 子布局,即圖中的黃色部分。這個(gè) LinearLayout 一般情況下又包含有 ViewStub 和 FrameLayout 兩部分(不同的主題 Theme 可能會(huì)多處一些對(duì)象),ViewStub 即是應(yīng)用的 ActionBar,他會(huì)根據(jù) theme 來(lái)決定是否真正引入 ActionBar 到界面顯示。而這個(gè) FrameLayout 中的內(nèi)容即是我們寫(xiě)的 layout 布局文件中想要展示的內(nèi)容。需要注意如果 Activity 繼承自 AppComcatActivity 則這個(gè) FrameLayout 中還會(huì)有兩個(gè)子布局,第一個(gè)子布局中的內(nèi)容才是我們寫(xiě)的布局文件中的內(nèi)容

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

通過(guò) 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)抽屜,接下來(lái)我們看一下 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)來(lái)的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 中通過(guò)重寫(xiě) onInterceptTouchEvent(MotionEvent ev) 方法和 onTouchEvent(MotionEvent ev) 方法來(lái)實(shí)現(xiàn)側(cè)滑返回事件的處理及對(duì) ViewPager 滑動(dòng)的兼容的。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //處理ViewPager沖突問(wèn)題
        ViewPager mViewPager = getTouchViewPager(mViewPagers, ev);
        //當(dāng)無(wú)觸摸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)則記錄觸摸開(kā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 來(lái)決定是否按默認(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)距離超過(guò) 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方法來(lái)設(shè)置一些滾動(dòng)的參數(shù),我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來(lái)滾動(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方法來(lái)設(shè)置一些滾動(dòng)的參數(shù),我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來(lái)滾動(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é)語(yǔ)

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

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

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

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