考拉Android全域性滑動返回及聯動效果的實現

網易考拉移動端團隊發表於2018-02-06

作者:許方鎮

前言

首次通過右滑來返回到上一個頁面的操作是在 IOS7上出現。到目前android應用上支援這種操作的依然不多。分析其主要原因應該是android已有實體的返回按鍵,這樣的功能變得不重要,但我覺得有這樣的功能便於單手操作,能提升app的使用者體驗,特別是從ios轉到android的使用者。寫這篇博文希望可以對大家有所幫助,希望自己的app上有滑動返回功能的可以參考下。

原理的簡單描述

Android系統裡有很多滑動相關的API和類,比如ViewDragHelper就是一個很好的滑動助手類。首先設定Window的背景為透明,再通過ViewDragHelper對Activity上DecorView的子view進行滑動,當滑動到一定距離,手指離開後就自動滑到最右側,然後finish當前的activity,這樣即可實現滑動返回效果。為了能夠 “全域性的”、“聯動的” 實現滑動返回效果,在每個activity的DecorView下插入了SwipeBackLayout,當前activity滑動和下層activity的聯動都在該類中完成。

效果圖

下層聯動滑動返回

佈局圖

Hierarchy View

實現主要類:

SwipeBackActivity //滑動返回基類
SwipeBackLayout //滑動返回佈局類
SwipeBackLayoutDragHelper //修改ViewDragHelper後助手類
TranslucentHelper //程式碼中修改透明或者不透明的助手類

##程式碼層面的講解

一. 設定activity為透明、activity跳轉動畫(TranslucentHelper 講解)

這個看起來很簡單,但如果要相容到API16及以下,會遇到過一個比較麻煩的頁面切換動畫問題:

1.1、通過activity的主題style進行設定

<item name="android:windowBackground">@color/transparent</item>
<item name="android:windowIsTranslucent">true</item>```

**遇到問題:**如果在某個activity的主題style中設定了android:windowIsTranslucent屬性為true,那麼該activity切換動畫與沒設定之前是不同的,有些手機切換動畫會變得非常跳。所以需要自定義activity的切換動畫。
接下來我們會想到通過主題style裡的windowAnimationStyle來設定切換動畫
複製程式碼

@anim/activity_open_enter @anim/activity_open_exit @anim/activity_close_enter @anim/activity_close_exit``` **實踐證明:**當android:windowIsTranslucent為true時,以上幾個屬性是無效的,而下面兩個屬性還是可以用。但是這兩個屬性一個是視窗進來動畫,一個是視窗退出動畫,明顯是不夠。

<item name="android:windowEnterAnimation">@anim/***</item>
<item name="android:windowExitAnimation">@anim/***</item>```

結合overridePendingTransition(int enterAnim, int exitAnim)可以複寫視窗進來動畫和視窗退出動畫,這種我覺得最終可能是可以實現的,不過控制起來比較複雜:
比如有A、B、C三個頁面:
A跳到B,進場頁面B動畫從右進來,出場頁面A動畫從左出去,可以直接在style中寫死
複製程式碼

@anim/*** @anim/***```

如果B返回到A,進場頁面A動畫從左進來,出場頁面B動畫從右出去,此時需要通過複寫onBackPressed() 方法,
在其中新增overridePendingTransition(int enterAnim, int exitAnim)方法來改變動畫。

如果B是finish()後到A頁面,在finish()後面加上overridePendingTransition ……
由於onBackPressed() 方法最終會調finish(),所以實際上只需要複寫finish(),在其中新增overridePendingTransition……

但是假如B finish()後跳到C,則又不應該執行overridePendingTransition……,那麼就需要判斷finish執行後是否要加 overridePendingTransition……
對於一個較為龐大的專案,採取這種方法需要對每個頁面進行排查,因此是不可行的,而對於剛剛起步的應用來說則是一個選擇。

1.2、通過透明助手類(TranslucentHelper)進行設定

透明助手類(TranslucentHelper)裡主要又有兩個方法,一個是讓activity變不透明,一個是讓activity變透明,這兩個都是通過反射來呼叫隱藏的系統api來實現的。因為較低的版本不支援程式碼中修改背景透明不透明,所以在類中有個靜態變數mTranslucentState 來記錄是否可以切換背景,這樣低版本就不需要每次都反射通過捕獲到的異常來做相容方案。 另外:發現有些手機支援背景變黑,但不支援背景變透明(中興z9 mini 5.0.2系統)

public class TranslucentHelper {
    private static final String TRANSLUCENT_STATE = "translucentState";
    private static final int INIT = 0;//表示初始
    private static final int CHANGE_STATE_FAIL = INIT + 1;//表示確認不可以切換透明狀態
    private static final int CHANGE_STATE_SUCCEED = CHANGE_STATE_FAIL + 1;//表示確認可以切換透明狀態
    private static int mTranslucentState = INIT;

    interface TranslucentListener {
        void onTranslucent();
    }

    private static class MyInvocationHandler implements InvocationHandler {
        private TranslucentListener listener;

        MyInvocationHandler(TranslucentListener listener) {
            this.listener = listener;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                boolean success = (boolean) args[0];
                if (success && listener != null) {
                    listener.onTranslucent();
                }
            } catch (Exception ignored) {
            }
            return null;
        }
    }

    static boolean convertActivityFromTranslucent(Activity activity) {
        if (mTranslucentState == INIT) {
            mTranslucentState = PreferencesUtils.getInt(TRANSLUCENT_STATE, INIT);
        }
        if (mTranslucentState == INIT) {
            convertActivityToTranslucent(activity, null);
        } else if (mTranslucentState == CHANGE_STATE_FAIL) {
            return false;
        }

        try {
            Method method = Activity.class.getDeclaredMethod("convertFromTranslucent");
            method.setAccessible(true);
            method.invoke(activity);
            mTranslucentState = CHANGE_STATE_SUCCEED;
            return true;
        } catch (Throwable t) {
            mTranslucentState = CHANGE_STATE_FAIL;
            PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
            return false;
        }
    }

    static void convertActivityToTranslucent(Activity activity, final TranslucentListener listener) {
        if (mTranslucentState == CHANGE_STATE_FAIL) {
            if (listener != null) {
                listener.onTranslucent();
            }
            return;
        }

        try {
            Class<?>[] classes = Activity.class.getDeclaredClasses();
            Class<?> translucentConversionListenerClazz = null;
            for (Class clazz : classes) {
                if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
                    translucentConversionListenerClazz = clazz;
                }
            }

            MyInvocationHandler myInvocationHandler = new MyInvocationHandler(listener);
            Object obj = Proxy.newProxyInstance(Activity.class.getClassLoader(),
                    new Class[] { translucentConversionListenerClazz }, myInvocationHandler);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
                getActivityOptions.setAccessible(true);
                Object options = getActivityOptions.invoke(activity);

                Method method = Activity.class.getDeclaredMethod("convertToTranslucent",
                        translucentConversionListenerClazz, ActivityOptions.class);
                method.setAccessible(true);
                method.invoke(activity, obj, options);
            } else {
                Method method =
                        Activity.class.getDeclaredMethod("convertToTranslucent", translucentConversionListenerClazz);
                method.setAccessible(true);
                method.invoke(activity, obj);
            }
            mTranslucentState = CHANGE_STATE_SUCCEED;
        } catch (Throwable t) {
            mTranslucentState = CHANGE_STATE_FAIL;
            PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (listener != null) {
                        listener.onTranslucent();
                    }
                }
            }, 100);
        }
    }
}
複製程式碼

讓activity變不透明的方法比較簡單;讓activity變透明的方法引數裡傳入了一個listener介面 ,主要是當antivity變透明後會回撥,因為這個介面也在activity裡,而且是私有的,所以我們只能通過動態代理去獲取這個回撥。最後如果版本大於等於5.0,還需要再傳入一個ActivityOptions引數。

在實際開發中,這兩個方法在android 5.0以上是有效的,在5.0以下需要當android:windowIsTranslucent為true時才有效,這樣又回到了之前的問題activity切換動畫異常。

**最終決解方法:**setContentView之前就呼叫 convertActivityFromTranslucent方法,讓activity背景變黑,這樣activity切換效果就正常。

**總結:**在style中設定android:windowIsTranslucent為true ,setContentView之前就呼叫 convertActivityFromTranslucent方法,當觸發右滑時呼叫convertActivityToTranslucent,通過動態代理獲取activity變透明後的回撥,在回撥後允許開始滑動。

二. 讓BaseActivity繼承SwipeBackActivity(SwipeBackActivity講解)

先直接看程式碼,比較少

public abstract class SwipeBackActivity extends CoreBaseActivity {
    /**
     * 滑動返回View
     */
    private SwipeBackLayout mSwipeBackLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!isSwipeBackDisableForever()) {
            TranslucentHelper.convertActivityFromTranslucent(this);
            mSwipeBackLayout = new SwipeBackLayout(this);
        }
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        if (!isSwipeBackDisableForever()) {
            mSwipeBackLayout.attachToActivity(this);
            mSwipeBackLayout.setOnSwipeBackListener(new SwipeBackLayout.onSwipeBackListener() {
                @Override
                public void onStart() {
                    onSwipeBackStart();
                }

                @Override
                public void onEnd() {
                    onSwipeBackEnd();
                }
            });
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (!isSwipeBackDisableForever() && hasFocus) {
            getSwipeBackLayout().recovery();
        }
    }

    /**
     * 滑動返回開始時的回撥
     */
    protected void onSwipeBackStart() {}

    /**
     * 滑動返回結束時的回撥
     */
    protected void onSwipeBackEnd() {}

    /**
     * 設定是否可以邊緣滑動返回,需要在onCreate方法呼叫
     */
    public void setSwipeBackEnable(boolean enable) {
        if (mSwipeBackLayout != null) {
            mSwipeBackLayout.setSwipeBackEnable(enable);
        }
    }

    public boolean isSwipeBackDisableForever() {
        return false;
    }

    public SwipeBackLayout getSwipeBackLayout() {
        return mSwipeBackLayout;
    }
}
複製程式碼

SwipeBackActivity中包含了一個SwipeBackLayout物件

在onCreate方法中:
1.activity轉化為不透明。
2.new了一個SwipeBackLayout。

在onPostCreate方法中:
1.attachToActivity主要是插入SwipeBackLayout、視窗背景設定……
2.設定了滑動返回開始和結束的監聽介面,建議在滑動返回開始時,把PopupWindow給dismiss掉。

onWindowFocusChanged 方法中
如果是hasFocus == true,就recovery()這個SwipeBackLayout,這個也是因為下層activity有聯動效果而移動了SwipeBackLayout,所以需要recovery()下,防止異常情況。

isSwipeBackDisableForever 方法是一個大開關,預設返回false,在程式碼中複寫後返回 true,則相當於直接繼承了SwipeBackActivity的父類。

setSwipeBackEnable 方法是一個小開關,設定了false之後就暫時不能滑動返回了,可以在特定的時機設定為true,就恢復滑動返回的功能。

**總結說明:**下層activity設定了setSwipeBackEnable 為 false,上層activity滑動時還是可以聯動的,比如MainActivity。而isSwipeBackDisableForever 返回true就不會聯動了,而且一些仿PopupWindow的activity需要複寫這個方法,因為activity需要透明。

三、滑動助手類的使用和滑動返回佈局類的實現(SwipeBackLayout講解)

直接貼SwipeBackLayout原始碼:

/**
 * 滑動返回容器類
 */
public class SwipeBackLayout extends FrameLayout {
    /**
     * 滑動銷燬距離比例界限,滑動部分的比例超過這個就銷燬
     */
    private static final float DEFAULT_SCROLL_THRESHOLD = 0.5f;
    /**
     * 滑動銷燬速度界限,超過這個速度就銷燬
     */
    private static final float DEFAULT_VELOCITY_THRESHOLD = ScreenUtils.dpToPx(250);
    /**
     * 最小滑動速度
     */
    private static final int MIN_FLING_VELOCITY = ScreenUtils.dpToPx(200);
    /**
     * 左邊移動的畫素值
     */
    private int mContentLeft;
    /**
     * 左邊移動的畫素值 / (ContentView的寬+陰影)
     */
    private float mScrollPercent;
    /**
     * (ContentView可見部分+陰影)的比例 (即1 - mScrollPercent)
     */
    private float mContentPercent;
    /**
     * 陰影圖
     */
    private Drawable mShadowDrawable;
    /**
     * 陰影圖的寬
     */
    private int mShadowWidth;
    /**
     * 內容view,DecorView的原第一個子view
     */
    private View mContentView;
    /**
     * 用於記錄ContentView所在的矩形
     */
    private Rect mContentViewRect = new Rect();
    /**
     * 設定是否可滑動
     */
    private boolean mIsSwipeBackEnable = true;
    /**
     * 是否正在放置
     */
    private boolean mIsLayout = true;
    /**
     * 判斷背景Activity是否啟動進入動畫
     */
    private boolean mIsEnterAnimRunning = false;
    /**
     * 是否是透明的
     */
    private boolean mIsActivityTranslucent = false;
    /**
     * 進入動畫(只在釋放手指時使用)
     */
    private ObjectAnimator mEnterAnim;
    /**
     * 退拽助手類
     */
    private SwipeBackLayoutDragHelper mViewDragHelper;
    /**
     * 執行滑動時的最頂層Activity
     */
    private Activity mTopActivity;
    /**
     * 後面的Activity的弱引用
     */
    private WeakReference<Activity> mBackActivityWeakRf;
    /**
     * 監聽滑動開始和結束
     */
    private onSwipeBackListener mListener;

    public SwipeBackLayout(Context context) {
        super(context);
        init(context);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mViewDragHelper = SwipeBackLayoutDragHelper.create(SwipeBackLayout.this, new ViewDragCallback());
        mViewDragHelper.setEdgeTrackingEnabled(SwipeBackLayoutDragHelper.EDGE_LEFT);
        mViewDragHelper.setMinVelocity(MIN_FLING_VELOCITY);
        mViewDragHelper.setMaxVelocity(MIN_FLING_VELOCITY * 2);
        try {
            mShadowDrawable = context.getResources().getDrawable(R.drawable.swipeback_shadow_left);
        } catch (Exception ignored) {
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        try {
            if (!mIsSwipeBackEnable) {
                super.onLayout(changed, left, top, right, bottom);
                return;
            }
            mIsLayout = true;
            if (mContentView != null) {
                mContentView.layout(mContentLeft, top, mContentLeft + mContentView.getMeasuredWidth(),
                        mContentView.getMeasuredHeight());
            }
            mIsLayout = false;
        } catch (Exception e) {
            super.onLayout(changed, left, top, right, bottom);
        }
    }

    @Override
    public void requestLayout() {
        if (!mIsLayout || !mIsSwipeBackEnable) {
            super.requestLayout();
        }
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        try {
            //繪製陰影
            if (mContentPercent > 0
                    && mShadowDrawable != null
                    && child == mContentView
                    && mViewDragHelper.getViewDragState() != SwipeBackLayoutDragHelper.STATE_IDLE) {
                child.getHitRect(mContentViewRect);
                mShadowWidth = mShadowDrawable.getIntrinsicWidth();
                mShadowDrawable.setBounds(mContentViewRect.left - mShadowWidth, mContentViewRect.top,
                        mContentViewRect.left, mContentViewRect.bottom);
                mShadowDrawable.draw(canvas);
            }
            return super.drawChild(canvas, child, drawingTime);
        } catch (Exception e) {
            return super.drawChild(canvas, child, drawingTime);
        }
    }

    @Override
    public void computeScroll() {
        mContentPercent = 1 - mScrollPercent;
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!mIsSwipeBackEnable) {
            return false;
        }
        try {
            return mViewDragHelper.shouldInterceptTouchEvent(event);
        } catch (ArrayIndexOutOfBoundsException e) {
            return super.onInterceptTouchEvent(event);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mIsSwipeBackEnable) {
            return false;
        }
        try {
            mViewDragHelper.processTouchEvent(event);
            return true;
        } catch (Exception e) {
            return super.onTouchEvent(event);
        }
    }

    /**
     * 將View新增到Activity
     */
   public void attachToActivity(Activity activity) {
        //插入SwipeBackLayout
        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
        decor.removeView(decorChild);
        if (getParent() != null) {
            decor.removeView(this);
        }
        decor.addView(this);
        this.removeAllViews();
        this.addView(decorChild);

        //mContentView為SwipeBackLayout的直接子view,獲取window背景色進行賦值
        activity.getWindow().setBackgroundDrawableResource(R.color.transparent_white);
        TypedArray a = activity.getTheme().obtainStyledAttributes(new int[] {
                android.R.attr.windowBackground
        });
        mContentView = decorChild;
        mContentView.setBackgroundResource(a.getResourceId(0, 0));
        a.recycle();

        //拿到頂層activity和下層activity,做聯動操作
        mTopActivity = activity;
        Activity backActivity = ActivityUtils.getSecondTopActivity();
        if (backActivity != null && backActivity instanceof SwipeBackActivity) {
            if (!((SwipeBackActivity) backActivity).isSwipeBackDisableForever()) {
                mBackActivityWeakRf = new WeakReference<>(backActivity);
            }
        }
    }

    /**
     * 設定是否可以滑動返回
     */
    public void setSwipeBackEnable(boolean enable) {
        mIsSwipeBackEnable = enable;
    }

    public boolean isActivityTranslucent() {
        return mIsActivityTranslucent;
    }

    /**
     * 啟動進入動畫
     */
    private void startEnterAnim() {
        if (mContentView != null) {
            ObjectAnimator anim =
                    ObjectAnimator.ofFloat(mContentView, "TranslationX", mContentView.getTranslationX(), 0f);
            anim.setDuration((long) (125 * mContentPercent));
            mEnterAnim = anim;
            mEnterAnim.start();
        }
    }

    protected View getContentView() {
        return mContentView;
    }

    private class ViewDragCallback extends SwipeBackLayoutDragHelper.Callback {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            if (mIsSwipeBackEnable && mViewDragHelper.isEdgeTouched(SwipeBackLayoutDragHelper.EDGE_LEFT, pointerId)) {
                TranslucentHelper.convertActivityToTranslucent(mTopActivity,
                        new TranslucentHelper.TranslucentListener() {
                            @Override
                            public void onTranslucent() {
                                if (mListener != null) {
                                    mListener.onStart();
                                }
                                mIsActivityTranslucent = true;
                            }
                        });
                return true;
            }
            return false;
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            return mIsSwipeBackEnable ? SwipeBackLayoutDragHelper.EDGE_LEFT : 0;
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            if (changedView == mContentView) {
                mScrollPercent = Math.abs((float) left / mContentView.getWidth());
                mContentLeft = left;
                //未執行動畫就平移
                if (!mIsEnterAnimRunning) {
                    moveBackActivity();
                }
                invalidate();
                if (mScrollPercent >= 1 && !mTopActivity.isFinishing()) {
                    if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
                        ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().invalidate();
                    }
                    mTopActivity.finish();
                    mTopActivity.overridePendingTransition(0, 0);
                }
            }
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (xvel > DEFAULT_VELOCITY_THRESHOLD || mScrollPercent > DEFAULT_SCROLL_THRESHOLD) {
                if (mIsActivityTranslucent) {
                    mViewDragHelper.settleCapturedViewAt(releasedChild.getWidth() + mShadowWidth, 0);
                    if (mContentPercent < 0.85f) {
                        startAnimOfBackActivity();
                    }
                }
            } else {
                mViewDragHelper.settleCapturedViewAt(0, 0);
            }
            if (mListener != null) {
                mListener.onEnd();
            }
            invalidate();
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return Math.min(child.getWidth(), Math.max(left, 0));
        }

        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);

            if (state == SwipeBackLayoutDragHelper.STATE_IDLE && mScrollPercent < 1f) {
                TranslucentHelper.convertActivityFromTranslucent(mTopActivity);
                mIsActivityTranslucent = false;
            }
        }

        @Override
        public boolean isTranslucent() {
            return SwipeBackLayout.this.isActivityTranslucent();
        }
    }

    /**
     * 背景Activity開始進入動畫
     */
    private void startAnimOfBackActivity() {
        if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
            mIsEnterAnimRunning = true;
            SwipeBackLayout swipeBackLayout = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout();
            swipeBackLayout.startEnterAnim();
        }
    }

    /**
     * 移動背景Activity
     */
    private void moveBackActivity() {
        if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
            View view = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().getContentView();
            if (view != null) {
                int width = view.getWidth();
                view.setTranslationX(-width * 0.3f * Math.max(0f, mContentPercent - 0.15f));
            }
        }
    }

    /**
     * 回覆介面的平移到初始位置
     */
    public void recovery() {
        if (mEnterAnim != null && mEnterAnim.isRunning()) {
            mEnterAnim.end();
        } else {
            mContentView.setTranslationX(0);
        }
    }

    interface onSwipeBackListener {
        void onStart();

        void onEnd();
    }

    public void setOnSwipeBackListener(onSwipeBackListener listener) {
        mListener = listener;
    }
}
複製程式碼
  • attachToActivity
    上面講到SwipeBackLayout是在activity的onCreate時被建立,在onPostCreate是插入到DecorView裡,主要是因為DecorView是在setContentView時與Window關聯起來插入SwipeBackLayout方法如程式碼所示,不難,然後是設定了window的背景色為透明色,mContentView為SwipeBackLayout的直接子view,獲取window背景色進行賦值。由於考拉專案已經有很多activity,而這些activity中android:windowBackground設定的顏色大部分是白色,少部分是灰色和透明的,所以需要在程式碼中設定統一設定一遍透明的,原來的背景色則賦值給SwipeBackLayout的子View就可以達到最少修改程式碼的目的。最後拿到頂層activity和下層activity,做聯動操作

  • SwipeBackLayoutDragHelper 和 ViewDragCallback
    SwipeBackLayout中包含了一個滑動助手類(SwipeBackLayoutDragHelper)的物件,該類是在ViewDragHelper的基礎上進行修改得來的。
    修改點:
    1.右側觸發區域 EDGE_SIZE由 20dp 改到 25dp
    2.提供滑動最大速度 的設定方法
    3.在ViewDragHelper 的內部類Callback方法中提供是否activity為透明的回撥介面
    4.在最終呼叫滑動的方法dragTo中新增判斷邏輯,activity為透明時才支援滑動
    SwipeBackLayoutDragHelper 在init 方法中初始化,通過onInterceptTouchEvent和onTouchEvent拿到滑動事件,通過ViewDragCallback的一些方法返回相應的滑動回撥, ViewDragCallback實現了SwipeBackLayoutDragHelper.Callback裡的以下幾個介面,其中其中isTranslucent()是自己新增進去的。

    ViewDragCallback

  • tryCaptureView方法當觸控到SwipeBackLayout裡的子View時觸發的,當返回true,表示捕捉成功,否則失敗。判斷條件是如果支援滑動返回並且是左側邊距被觸控時才可以,我們知道這個時候的的背景色是不透明的,如果直接開始滑動則是黑色的,所以需要在這裡背景色改成透明的,如果直接呼叫 TranslucentHelper.convertActivityToTranslucent(mTopActivity, null)後直接返回true,會出現一個異常情況,就是滑動過快時會導致背景還來不及變成黑色就滑動出來了,之後才變成透明的,從而導致了會從黑色到透明的一個閃爍現象,解決的辦法是在程式碼中用了一個回撥和標記,當變成透明後設定了mIsActivityTranslucent = true;通過mIsActivityTranslucent 這個變數來判斷是否進行移動的操作。由於修改activity變透明的方法是通過反射的,不能簡單的設定一個介面後進行回撥,而是通過動態代理的方式來實現的(InvocationHandler),在convertToTranslucent方法的第一個引數剛好是一個判斷activity是否已經變成透明的回撥,看下面程式碼中 if 語句裡的註釋和回撥,如果視窗已經變成透明的話,就傳了一個drawComplete (true)。通過動態代理,將translucentConversionListenerClazz 執行其方法onTranslucentConversionComplete的替換成myInvocationHandler中執行invoke方法。其中賦值給success的args[0]正是 drawComplete。

  • isTranslucent是自己新增了一個方法,主要是返回activity是否是透明的預設為true,在SwipeBackLayout重寫後將mIsActivityTranslucent返回。仔細看SwipeBackLayoutDragHelper方法的話,會發現最後通過dragTo方法對view進行移動,因此在進行水平移動前判斷下是否是透明的,只有透明瞭才能移動

  • onViewPositionChanged view移動過程中會持續呼叫,這裡面的邏輯主要有這幾個: 1.實時計算滑動了多少距離,用於繪製左側陰影等
    2.使下面的activity進行移動moveBackActivity();
    3.當view完全移出螢幕後,銷燬當前的activity

  • onViewReleased是手指釋放後觸發的一個方法。如果滑動速度大於最大速度或者滑動的距離大於設定的閾值距離,則直接移到螢幕外,同時觸發下層activity的復位動畫,否則移會到原來位置 。

  • onViewDragStateChanged當滑動的狀態發生改變時的回撥,主要是停止滑動後,將背景改成不透明,這樣跳到別的頁面是動畫就是正常的。

  • clampViewPositionHorizontal 返回水平移動距離,防止滑出父 view。

  • getViewHorizontalDragRange對於clickable=true的子view,需要返回大於0的數字才能正常捕獲。

其他方法都較為簡單,註釋也寫了,就不多說了,最後毫不吝嗇的貼上SwipeBackLayoutDragHelper的dragTo程式碼,就多了if (mCallback.isTranslucent())

private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            if (mCallback.isTranslucent()) {
                ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
            }
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            if (mCallback.isTranslucent()) {
                mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                        clampedDx, clampedDy);
            }
        }
    }

複製程式碼

相關文章