Android activity滑動返回

前言

activity的滑動返回也是個常用的功能,網上有很多介紹怎么去實現的庫,我測過其中幾個,包括star比較多的

  1. SwipeBackLayout
  2. BGASwipeBackLayout-Android

但是這兩個庫各有各的缺點,SwipeBackLayout沒有實現上一個activity跟隨滑動的效果,這也是網上大多數介紹滑動返回文章的問題,只是粗略實現了當前界面滑動后 finish的效果。BGASwipeBackLayout-Android實現了跟隨滑動的效果,但是其一是跟隨滑動不流暢,并且在將activity的主題設置為透明之后,將activity主題設置為透明后,會有bug(前一個activity在滑動的過程中,底部會有黑背景);第二個問題是不夠輕量,仔細去看源代碼,有很多不需要的代碼邏輯,包括measure和layout等不需要的設計。

于是我決定在前人的基礎上重寫一個滑動返回的控件

思路

  • Android的activity滑動的實現的設計思路大部分都是借助ViewDragHelper這個類實現的,因為這個類可以幫我們處理很多的手勢檢測和尺寸計算。
  • 上個activity跟隨滑動的實現,當手指開始從左側邊緣滑動的時候,通過將上個activity的contentView暫時添加到當前activity的contentView下方,通過屬性動畫讓它跟隨當前activity的滑動而滑動

整個過程入下圖展示

初始狀態
拖拽過程轉臺
結束拖拽

上圖中viewDragHelper其實是一個包含viewDragHelperFramLayout,我取名為SlidebackLayout

這里之所以不是直接添加preContentView而是用一個preWrapper來包裝,是因為不能在SlidebackLayout初始化的時候去獲取preContentView,因為這個時候activity整window沒有繪制,獲取的preContentView會是一片空白,所以是在activity初始化好展示給用戶,用戶要開始拖拽的時候才去添加preContentView。因此我們在SlidebackLayout初始化的時候用一個preWrapper來占位,它存在于當前contentView的下方.

代碼

SlideBackLayout

這是整個拖拽的核心,他包含一個viewDragHelperviewDragHelper的使用創建方法不多做解釋,我們主要看一下它的ViewDragHelper.Callback,因為主要的處理邏輯都在這里

    public SlideBackLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.mShawDrawable = ContextCompat.getDrawable(getContext(), R.drawable.bga_sbl_shadow);
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, mDragCallback);
         //支持左側邊緣滑動
        mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }


private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return false;
        }

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            super.onEdgeDragStarted(edgeFlags, pointerId);
            mViewDragHelper.captureChildView(mCurrentContentView, pointerId);
            if (!mPreContentViewWrapper.isBindPreActivity())
                mPreContentViewWrapper.bindPreActivity(mCurrentActivity);
            if (mSlideListener != null)
                mSlideListener.onSlideStart();
        }

        @Override
        public void onViewDragStateChanged(int state) {
            if (state == ViewDragHelper.STATE_IDLE) {
                //返回上個界面
                if (mCurrentContentView.getLeft() >= mWidth) {
                    if (mSlideListener != null) {
                        mSlideListener.onSlideComplete();
                    }
                    mCurrentActivity.finish();
                    mCurrentActivity.overridePendingTransition(0,0);
                    mCurrentActivity.getWindow().getDecorView().setVisibility(GONE);
                    removeView(mPreContentViewWrapper);
                    mPreContentViewWrapper.unBindPreActivity();
                } else {
                    //返回當前界面
                    if (mSlideListener != null)
                        mSlideListener.onSlideCancel();
                }
            }
        }

        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            mDragLeftX = capturedChild.getLeft();
            mDragTopY = capturedChild.getTop();
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left < 0 ? 0 : left;
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (releasedChild.getLeft() > mWidth * BACK_THRESHOLD_RATIO) {
                mViewDragHelper.settleCapturedViewAt(mWidth, mDragTopY);
            } else {
                mViewDragHelper.settleCapturedViewAt(mDragLeftX, mDragTopY);
            }
            invalidate();
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            if (mPreContentViewWrapper != null && mPreContentViewWrapper.isBindPreActivity()) {
                float ratio = left * 1.0f / mWidth;
                mPreContentViewWrapper.onSlideChange(ratio);
                mShawDrawable.setBounds(left - SHADOW_WIDTH, 0, left, mHeight);
                if (mSlideListener != null)
                    mSlideListener.onSliding(ratio);
                invalidate();
            }
        }
    };

注意到上述代碼中有個mPreContentViewWrapper,這個就是上面我們提到的包裝preContentView的容器。我將它抽取成一個自定義view,看下它的代碼

 /*============================上一個界面的容器=============================*/
    public static class PreContentViewWrapper extends FrameLayout {

        private static final float TRANSLATE_X_RATIO = 0.3f;//當前頁面在滑動的時候,前一個界面初始被隱藏的寬度為0.3*width

        private WeakReference<Activity> mPreActivityRef;
        private ViewGroup mPreDecorView;
        private ViewGroup mPreContentView;

        private ViewGroup.LayoutParams mPreLayoutParams;

        private boolean isBindPreActivity;
        private int mHideWidth;

        public PreContentViewWrapper(Context context) {
            this(context, null);
        }

        public PreContentViewWrapper(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mHideWidth = (int) (TRANSLATE_X_RATIO * w);
        }

        /**
         * 綁定上一個activity的ContenView
         * @param currentActivity
         */
        public void bindPreActivity(Activity currentActivity) {
            Activity preActivity = ActivityStackUtil.getInstance().getPreActivity(currentActivity);
            if (!preActivity.isDestroyed() && !preActivity.isFinishing()) {
                //創建一個軟連接指向上個activity
                mPreActivityRef = new WeakReference<Activity>(preActivity);

                mPreDecorView = (ViewGroup) preActivity.getWindow().getDecorView();
                mPreContentView = (ViewGroup) mPreDecorView.getChildAt(0);
                mPreLayoutParams = mPreContentView.getLayoutParams();
                mPreDecorView.removeView(mPreContentView);
                addView(mPreContentView, 0, mPreLayoutParams);
                this.isBindPreActivity = true;
            }
        }


        /**
         * 解除綁定,將preContentView歸還給上個activity
         */
        public void unBindPreActivity() {
            if (!isBindPreActivity) return;
            if (mPreActivityRef == null || mPreActivityRef.get() == null) return;
            if (mPreContentView != null && mPreDecorView != null) {
                this.removeView(mPreContentView);
                mPreDecorView.addView(mPreContentView, 0, mPreLayoutParams);
                mPreContentView = null;
                mPreActivityRef.clear();
                mPreActivityRef = null;
            }
            this.isBindPreActivity = false;
        }

        @Override
        protected void dispatchDraw(Canvas canvas) {
            super.dispatchDraw(canvas);
            if (mPreDecorView != null && mPreContentView == null) {
                mPreDecorView.draw(canvas);
            }
        }

        /**
         * 前一個界面跟隨當前頁面滑動而滑動
         *
         * @param ratio
         */
        public void onSlideChange(float ratio) {
            this.setTranslationX(mHideWidth * (ratio - 1));
        }


        public boolean isBindPreActivity() {
            return isBindPreActivity;
        }
    }

ActivityStackUtil類用于獲取當前activity的前一個activity,需要在application中調用ActivityStackUtil getInstance().init(this);

package lu.basetool.util;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import java.util.Stack;

/**
 * @Author: luqihua
 * @Time: 2018/5/29
 * @Description: ActivityUtil
 */

public class ActivityStackUtil implements Application.ActivityLifecycleCallbacks {

    private Stack<Activity> mActivityStack = new Stack<>();

    private static class Holder {
        private static ActivityStackUtil sInstance = new ActivityStackUtil();
    }

    public static ActivityStackUtil getInstance() {
        return Holder.sInstance;
    }


    public void init(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        mActivityStack.push(activity);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
        mActivityStack.remove(activity);
    }


    /**
     * 獲取相對于當前activity前一個activity
     *
     * @param mCurrentActivity
     * @return
     */
    public Activity getPreActivity(Activity mCurrentActivity) {
        Activity preActivity = null;
        if (mActivityStack.size() > 1) {
            int index = mActivityStack.lastIndexOf(mCurrentActivity);
            if (index > 0) {
                preActivity = mActivityStack.get(index - 1);
            } else {
                preActivity = mActivityStack.lastElement();
            }
        }
        return preActivity;
    }
}

callback的onViewDragStateChanged方法中處理滑動結束的操作,除了mCurrentActivity.finish()結束當前activity之外,還處理滑動退出后閃屏的幾個點

//1.由于我們使用了滑動退出,因此不需要activity之間默認的切換動畫

  mCurrentActivity.overridePendingTransition(0,0);
//2. 由于盡管取消了activity切換動畫,但是activity的消失可能任然會有閃一下的可能,于是我們干脆把當前的視圖隱藏
mCurrentActivity.getWindow().getDecorView().setVisibility(GONE);
removeView(mPreContentViewWrapper);

3.將當前mCurrentActivity的主題設置為透明

在style.xml中新建個主題,并在AndroidManifest.xml中設置給需要滑動返回的activity

  <style name="AppTheme.transparent" parent="AppTheme">
        <!-- Customize your theme here. -->
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
  </style>

使用 代碼git地址

編寫好的代碼有2個類,ActivityStackUtilSlideBackLayout

//1.在application中初始化ActivityStackUtil

  ActivityStackUtil.getInstance().init(this);
  
//2.給需要滑動返回的activity的(style.xml)theme添加如下兩行代碼
   <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>

//3.在需要滑動返回的activity的onCreate()方法中調用

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //在 super.onCreate(savedInstanceState);之前調用此方法
  //第二個參數是一個滑動的監聽,一般情況下設置為null即可
        new SlideBackLayout(this).attach2Activity(this, null);
        super.onCreate(savedInstanceState);
    }

可能出現的錯誤:

ViewDragHelper在處理動態變化的子view的時候,可能會出現已經拖拽的子view自動回到原位,所謂動態變化的子view例如:輪播圖動畫等,所以盡量確保在啟動可以滑動返回的activity之后,上一個activity的一些定時改變視圖(例如輪播圖定時翻頁)的效果暫停掉。

[異常]Only fullscreen opaque activities can request orientation;出現該異常的話,將activity中的android:screenOrientation=""屬性去掉。因為當activity的theme中設置了透明之后,不允許再設置該屬性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容