優雅的構建 Android 專案——側滑返回使用及原理分析

PandaQ發表於2017-06-26

大螢幕手機在返回前頁操作時,點選左上角的 APP 內返回鍵或者手機自帶的返回按鍵都不是很方便,這時候能通過螢幕側滑退出當前頁面體驗就會好很多了。但是 Android 系統並沒有想 IOS 一樣自帶側滑返回,好在 Android 輪子比較多,本文記錄一下個人開源專案 PandaEye 中使用的側滑返回庫 SwipBackLayout 。該庫參考 github 上的開源庫 SwipeBackLayout 做了一些簡化;

使用方式

定義側滑基礎 Activity

側滑返回的實現是基於 Activity 的,可以直接繼承 Activity 或者繼承自己應用實現的 BaseActivity 然後實現 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);
        }
    }

    // 提供給子類設定 ViewPager 的介面,用於 SwipeLayout 中處理滑動衝突
    public void addViewPager(ViewPager pager) {
        layout.addViewPager(pager);
    }

}複製程式碼

效果優化

需要側滑返回的 Activity 繼承 SwipeBackActivity 即可實現側滑返回的功能了,但是側滑過程中返回介面會被預設的視窗背景顏色覆蓋,因此我們需要把實現側滑返回的介面的 theme 做一些小小的優化,將背景設定為透明狀態,並設定進入和退出的動畫。
style 中的屬性設定

    <!--全屏加透明-->
    <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>
    <!--動畫設定-->
        <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>複製程式碼

介面進入動畫

<!--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>複製程式碼

介面退出動畫

<?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 設定為 TranslucentFullScreenTheme 即可解決滑動過程中背景覆蓋問題。

原理淺析

Activity 中 View 檢視層級

要明白側滑返回的原理我們得先明白 Android Activity 介面的檢視層級關係:

Activity 介面檢視層級
Activity 介面檢視層級

Activity 和 PhoneWindow 這裡可以忽略,重點在 DecorView上。這個 DecorView 是 Activity 中 View 佈局的祖宗級佈局,是一個 FrameLayout,通過 getWindow().getDecorView() 可以獲取到物件;如圖中 DecorView 有且僅有一個 LinearLayout 子佈局,即圖中的黃色部分。這個 LinearLayout 一般情況下又包含有 ViewStub 和 FrameLayout 兩部分(不同的主題 Theme 可能會多處一些物件),ViewStub 即是應用的 ActionBar,他會根據 theme 來決定是否真正引入 ActionBar 到介面顯示。而這個 FrameLayout 中的內容即是我們寫的 layout 佈局檔案中想要展示的內容。需要注意如果 Activity 繼承自 AppComcatActivity 則這個 FrameLayout 中還會有兩個子佈局,第一個子佈局中的內容才是我們寫的佈局檔案中的內容

實現原理

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

DecorView 檢視節點
DecorView 檢視節點

介面所有顯示的內容其實都在這個 LinearLayout 中,如果我們給這個 LinearLayout 增加一個父佈局然後對這個父佈局進行滑動處理就可以實現介面的整體滑動,即把整個可視介面放入一個滑動抽屜。因此實現滑動的介面檢視應該變成如下的樣子:

可側滑的介面的 DecorView 試圖節點
可側滑的介面的 DecorView 試圖節點

如圖 SwipeBackLayout 即是新增的滑動抽屜,接下來我們看一下 SwipeBackLayout 中是怎樣實現在 LinearLayout 上層插入一個 SwipeBackLayout 佈局的。
在 SwipeBackActivity 中只呼叫了 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 物件
        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設定為decorChild的父佈局即新增進來的SwipeBackLayout
        setContentView(decorChild);
        //將SwipeBackLayout新增進DecorView
        decor.addView(this);
    }複製程式碼

從中我新增的註釋不難看出,實現替換的流程:

  • 1、傳入的 activity 物件獲取到 DecorView
  • 2、DecorView.getChildAt(0) 獲取到 LinearLayout 物件
  • 3、將 LinearLayout 背景資源重置,並從 DecorView 中移除
  • 4、將 LinearLayout 新增到自定義的 SwipeBackLayout 中
  • 5、將自定義的 SwipeBackLayout 新增到 DecorView 中

滑動處理及 ViewPager 處理

在 SwipeBackLayout 中通過重寫 onInterceptTouchEvent(MotionEvent ev) 方法和 onTouchEvent(MotionEvent ev) 方法來實現側滑返回事件的處理及對 ViewPager 滑動的相容的。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //處理ViewPager衝突問題
        ViewPager mViewPager = getTouchViewPager(mViewPagers, ev);
        //當無觸控ViewPager或者該ViewPager未滑動到最左則不對滑動時間進行攔截
        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);
    }複製程式碼

在手指按下的時候相較於 onTouchEvent() 方法 onInterceptTouchEvent() 方法會先執行,在此方法中先判斷當前觸控是否為 ViewPager,是 ViewPager 則判斷是否滑動到了 ViewPager 的最左側。如果觸控的 ViewPager 且未滑動到最左側則不對事件進行攔截交給 ViewPager 處理觸控事件,否則觸控位置進行判斷,在有效區域內則記錄觸控開始點,否則按系統預設方式處理。在移動事件中會根據按下事件的判斷結果決定是否按預設方式處理,當需要處理側滑時會再次判斷如果 X 方向的滑動大於最小有效滑動距離 Y方向滑動距離小於最小有效滑動距離則此次事件將會被 SwipeBackLayout 所消費,將進入 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 為單次移動的距離向右滑為負數
                    // TODO: 2017/6/22 實現 y 方向的移動,即向右任意方向滑出介面
                    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;
    }複製程式碼

同樣此方法中也會根據 onInterceptTouchEvent() 中的 DOWN 事件的判定結果 canSwipe 來決定是否按預設方式消費事件,MOVE 事件中如果滿足側滑條件則會呼叫 scrollBy() 將 mContentView 按滑動方向進行移動,而此處的 mContentView 即是 SwipeBackLayout 自身,因此整個顯示的介面會被按照滑動方向移動。當手指抬起時如果滑動距離超過 1/4 介面寬度(可以按自己需求調整),則視為側滑返回完成,讓 Scroller 自動完成剩餘距離的滑動,否則讓 Scroller 恢復到滑動起始位置

    /**
     * 滾動出介面
     */
    private void scrollRight() {
        final int delta = (viewWidth + mContentView.getScrollX());
        // 呼叫startScroll方法來設定一些滾動的引數,我們在computeScroll()方法中呼叫scrollTo來滾動item
        mScroller.startScroll(mContentView.getScrollX(), 0, -delta + 1, 0,
                Math.abs(delta));
        postInvalidate();
    }

    /**
     * 滾動到起始位置
     */
    private void scrollOrigin() {
        int delta = mContentView.getScrollX();
                // 呼叫startScroll方法來設定一些滾動的引數,我們在computeScroll()方法中呼叫scrollTo來滾動item
        mScroller.startScroll(mContentView.getScrollX(), 0, -delta, 0,
                Math.abs(delta));
        postInvalidate();
    }

    /**
     * 具體執行 Scroller 中的滾動及將滑動距離傳遞給外部介面
     */
     @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();
            }
        }
    }複製程式碼

結語

以上就是簡化後的側滑返回的基本使用和原理的簡單分析,完整程式碼可以參考 PandaEye歡迎 Star。文章一遍過為反覆檢查如有不妥之處歡迎大家踴躍交流。

相關文章