Android版與微信Activity側滑后退效果完全相同的SwipeBackLayout

本文緣起

因為我做的app里使用了SwipeBackHelper的開源庫來實現Activity的側滑后退,本來使用起來一直沒什么問題,但在新版本中接入了騰訊x5內核的WebView后就出現了一個小問題。看下圖:

圖1
圖2

圖2中兩條黑線之間就是圖1中所展示的視頻播放的區域,但圖2中顯示的不是視頻內容,而是當前的WebActivity下層的MainActivity的部分視圖。因為當進入網頁播放頁面點擊視頻播放按鈕后,視頻播放區域會突然變成透明的,直到視頻加載出來之后才會開始顯示視頻內容,該過程持續1秒到數秒不等。本來如果只是閃現一下就消失也沒什么大問題,但有的網頁中的視頻加載過慢,導致這個透明現象出現的時間過長,所以app運營渠道提出需要解決該問題。

問題分析

經測試,該問題出現是因為滿足了兩個條件:
1.Activity的主題style中滿足屬性:<item name="android:windowIsTranslucent">true</item> (這也是使用SwipeBackHelper的必要條件);
2.使用x5內核的WebView播放視頻。
對于我們的項目來說,x5是不能放棄的,但側滑退出的效果在三個版本之前就加入了,現在要針對某些頁面去掉,也讓我覺得很不爽。此時當然是參考微信的效果嘍,結果微信給我的結果是這樣的:

微信x5內核WebView播放視頻效果

微信同樣是使用x5內核,同樣具有側滑退出得效果,當播放相同視頻時,本該顯示透明的區域卻顯示的是黑色的背景。微信究竟是如何解決的呢?
我嘗試了給WebView增加背景色,給WebView增加父容器后再增加背景色,給Activity的Window和DecorView設置背景色,但沒有作用。只要Activity的主題style中設置了窗體透明,該問題無論如何都會出現。

問題解決

無奈之下,我嘗試解決這個問題,雖然說是個小問題,著實花了一番功夫。下面我會從三個方面來說明我在尋求解決方案的過程中學習和總結到的一些東西。因為這個問題遇到的人不多,而且我只是在SwipeBackHelper的源碼基礎上做了一些修改,所以就不上傳代碼到github了,但我會詳細說明我修改的過程和原理,相信讀完本文,你會對SwipeBackHelper的工作原理有更多地了解,也會了解到通過反編譯成熟apk尋找解決方案的學習方法。

一. SwipeBackHelper的實現原理

其實我搜索了很久找其他實現側滑后退的方案,但發現不管什么方案,設置<item name="android:windowIsTranslucent">true</item>這一條件都被聲明為必要條件,否則就會出現側滑時出現下層背景為黑的bug。所以最終我只有閱讀一下源碼來看看側滑后退的原理究竟是什么。大家搜索時會發現github上有一個star數量更多的相關項目SwipeBackLayout,我看了兩個項目各自的代碼,從github分支推送的時間來看,SwipeBackLayout是最先出現的。兩者的代碼80%的代碼是相似的,SwipeBackHelper只是在SwipeBackLayout的基礎上對其中的主要控件進行了解耦,提取出來了一個SwipeBackHelper和SwipeBackPage兩個管理類,使用法更加清晰明了,同時實現了當前Activity側滑關閉時與下層Activity的聯動效果,跟微信已經99%相似了(是的,我要解決的就是那1%的問題)。因為我項目用的是SwipeBackHelper項目,所以我也是在它的源碼基礎上進行修改的。

SwipeBackHelper源碼文件

源碼并不復雜,具體用法我就不解釋了,項目github上說得很詳細。我簡單說下每個類的主要功能:

  1. SwipeBackLayout,是一個繼承自FrameLayout的ViewGroup,我們側滑后退時滑動的就是這個ViewGroup,需要側滑的Activity執行onCreate時,需要設置setSwipeBackEnable(true),這句代碼執行時會調用SwipeBackLayout的attachToActivity,如下所示,該方法會找到Activity的Window界面的最頂層View,即DecorView,并找到DecorView的直接子view將它替換為SwipeBackLayout,同時將原來的子view添加到SwipeBackLayout中。這樣一來,SwipeBackLayout就會在Activity的所有布局(我們自己寫得xml所生成的布局)之上了),當我們滑動Activity時,如果是在側邊(一般是屏幕左側)可以觸發側滑后退動作的區域內,SwipeBackLayout就會攔截觸摸事件,自己進行處理,執行被拖動或滑動退出的UI效果;
public void attachToActivity(Activity activity) {
        if (getParent() != null) {
            return;
        }
        mActivity = activity;
        TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
                android.R.attr.windowBackground
        });
        int background = a.getResourceId(0, 0);
        a.recycle();

        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        View decorChild = decor.findViewById(android.R.id.content);
        while (decorChild.getParent() != decor) {
            decorChild = (View) decorChild.getParent();
        }
        decorChild.setBackgroundResource(background);
        decor.removeView(decorChild);
        addView(decorChild);
        setContentView(decorChild);
        decor.addView(this);
    }
  1. ViewDragHelper,實現滑動和拖動的輔助類,其實就是在Android原生的ViewDragHelper上進行了小小的修改,ViewDragHelper是一個非常強大的類,簡單的調用就可以幫我們實現View的滑動和拖動效果,SwipeBackLayout的onInterceptTouchEvent和onTouchEvent的處理都是交給ViewDragHelper來做的,所以要深入理解側滑的實現機制,需要知道ViewDragHelper是如何工作的,感興趣的同學可以直接讀下面兩篇博客,讀完應該就理解得差不多了:
    Android ViewDragHelper完全解析 自定義ViewGroup神器
    Android ViewDragHelper源碼解析

  2. SwipeBackPage,每個滑動頁面的管理類,該類持有當前Activity、與Activity關聯的SwipeBackLayout和一個RelateSlider的引用,并提供一系列鏈式調用的方法設置SwipeBackLayout的相關屬性;

  3. SwipeBackHelper,滑動的全局管理類,也是提供給我們在Activity中開啟側滑退出功能的工具類。在Activity的onCreate中調用SwipeBackHelper的onCreate方法時,其內部會創建一個與該Activity關聯的SwipeBackPage,并通過一個Stack集合記錄管理所有關聯過Activity的SwipeBackPage,需要下層Activity聯動時就可以通過該類的getPrePage獲取到下層Activity相關聯的SwipeBackPage類;

private static final Stack<SwipeBackPage> mPageStack = new Stack<>();
……

    public static void onCreate(Activity activity) {
        SwipeBackPage page;
        if ((page = findHelperByActivity(activity)) == null){
            page = mPageStack.push(new SwipeBackPage(activity));
        }
        page.onCreate();
    }
  1. SwipeListener,簡單的接口,提供了觸摸和滑動SwipeBackLayout時的三個回調方法;

  2. RelateSlider,有下層Activity聯動時需要用到的一個類,它實現了SwipeListener接口,在上層Activity的SwipeBackLayout被滑動時,會回調到它實現的onScroll和onScrollToClose方法,從而實現下層Activity的SwipeBackLayout位置的改變,達到聯動的效果。

  3. Utils 最不起眼的一個類,在這個項目中都沒用到好伐。不過正是這個類,才是我解決問題的關鍵,這個類的源碼不太對,后面我會貼出修改后的代碼。

二. 反編譯微信apk尋找靈感

雖然了解了SwipeBackHelper的實現原理,但剛開始我還是想不通微信是如何處理我開頭提出的問題。我Google了大半天都找不出有人有類似的問題,索性直接反編譯微信apk,看看能不能找到一些端倪,沒想到,還真被我找到了。

微信的SwipeBackLayout

在反編譯后的java代碼中,我找到了一個SwipeBackLayout的類,很明顯,微信側滑后退的實現方式跟上面開源庫的差不多,只不過人家自己做了整合和優化。我一眼看到"convertToTranslucent",就知道這個肯定跟處理透明問題有關,后來我才發現原來同時出現在SwipeBackHelperSwipeBackLayout項目中的Utils中寫的正是反射調用Activity的"convertToTranslucent"方法,而且在SwipeBackLayout中的Utils是被使用過的,使用時機是在SwipeBackLayout的onEdgeTouch回掉中,也就是在側滑動作觸發之前。而這個"convertToTranslucent"方法的作用正是讓不透明的Activity轉為透明。
5.0及其以上版本的Activity中的convertToTranslucent方法:

  /**
     * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} back from
     * opaque to translucent following a call to {@link #convertFromTranslucent()}.
     * <p>
     * Calling this allows the Activity behind this one to be seen again. Once all such Activities
     * have been redrawn {@link TranslucentConversionListener#onTranslucentConversionComplete} will
     * be called indicating that it is safe to make this activity translucent again. Until
     * {@link TranslucentConversionListener#onTranslucentConversionComplete} is called the image
     * behind the frontmost Activity will be indeterminate.
     * <p>
     * This call has no effect on non-translucent activities or on activities with the
     * {@link android.R.attr#windowIsFloating} attribute.
     *
     * @param callback the method to call when all visible Activities behind this one have been
     * drawn and it is safe to make this Activity translucent again.
     * @param options activity options delivered to the activity below this one. The options
     * are retrieved using {@link #getActivityOptions}.
     * @return <code>true</code> if Window was opaque and will become translucent or
     * <code>false</code> if window was translucent and no change needed to be made.
     *
     * @see #convertFromTranslucent()
     * @see TranslucentConversionListener
     *
     * @hide
     */
    @SystemApi
    public boolean convertToTranslucent(TranslucentConversionListener callback,
            ActivityOptions options) {
        boolean drawComplete;
        try {
            mTranslucentCallback = callback;
            mChangeCanvasToTranslucent =
                    ActivityManagerNative.getDefault().convertToTranslucent(mToken, options);
            WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, false);
            drawComplete = true;
        } catch (RemoteException e) {
            // Make callback return as though it timed out.
            mChangeCanvasToTranslucent = false;
            drawComplete = false;
        }
        if (!mChangeCanvasToTranslucent && mTranslucentCallback != null) {
            // Window is already translucent.
            mTranslucentCallback.onTranslucentConversionComplete(drawComplete);
        }
        return mChangeCanvasToTranslucent;
    }

5.0以下版本的Activity中的convertToTranslucent方法:

/**
     * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} to a
     * fullscreen opaque Activity.
     * <p>
     * Call this whenever the background of a translucent Activity has changed to become opaque.
     * Doing so will allow the {@link android.view.Surface} of the Activity behind to be released.
     * <p>
     * This call has no effect on non-translucent activities or on activities with the
     * {@link android.R.attr#windowIsFloating} attribute.
     *
     * @see #convertToTranslucent(android.app.Activity.TranslucentConversionListener,
     * ActivityOptions)
     * @see TranslucentConversionListener
     *
     * @hide
     */
    @SystemApi
    public void convertFromTranslucent() {
        try {
            mTranslucentCallback = null;
            if (ActivityManagerNative.getDefault().convertFromTranslucent(mToken)) {
                WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, true);
            }
        } catch (RemoteException e) {
            // pass
        }
    }

既然如此,那么我將我的WebActivity主題的android:windowIsTranslucent設置為false,然后在側滑被觸發之前調用convertToTranslucent不就好了。
事實證明的確是可以的,但有兩個明顯不好的地方在于:

  1. 反射調用convertToTranslucent方法會使相關聯的Activity重繪,測試發現這個過程需要100ms的時間,所以如果側滑動作很快,就會出現黑邊閃現,體驗不太好;
    2.如果側滑動作進行一半,用戶又滑回去了選擇暫時不關閉Activity,其實Activity已經轉換成透明了,再播放視頻的話透明現象還會出現。對于這個問題,我本來覺得可以在它滑回的時候調用Utils中的convertActivityFromTranslucent再將Activity轉為不透明,但測試發現,這樣反轉一下后,視頻播放區域就直接全黑了,再也不出現視頻內容了。

對于問題2,我在微信上進行了嘗試,不得不說我機智地發現微信并沒有處理這種情況:


上圖中視頻區域顯示的是下層Activity的內容(我的聊天窗口)。
一方面這個問題確實難以解決,另一方面用戶進行問題2所述操作的概率并不會很高,所以這種問題暫時就參考微信,不去解決了。
真正讓我郁悶的還是問題1,看到微信怎么滑都不會有黑邊的效果,我還是決定嘗試將它徹底解決。

三. 解決問題的終極姿勢

快速滑動出現黑邊問題的根本原因是convertToTranslucent是需要100ms左右的時間的,而且這個事件不固定跟手機的硬件配置有關,所以思路是先等待convertToTranslucent成功的回調,然后再觸發Activity的側滑。

 /**
     * Calling the convertToTranslucent method on platforms after Android 5.0
     */
    private static void convertActivityToTranslucentAfterL(Activity activity) {
        try {
            Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
            getActivityOptions.setAccessible(true);
            Object options = getActivityOptions.invoke(activity);

            Class<?>[] classes = Activity.class.getDeclaredClasses();
            Class<?> translucentConversionListenerClazz = null;
            for (Class clazz : classes) {
                if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
                    translucentConversionListenerClazz = clazz;
                }
            }
            Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent",
                    translucentConversionListenerClazz, ActivityOptions.class);
            convertToTranslucent.setAccessible(true);
            convertToTranslucent.invoke(activity, null, options);
        } catch (Throwable t) {
        }
    }

然而調用Activity的convertToTranslucent方法本來就是通過反射的方式,無法直接傳入回調接口。這樣一來只有通過動態代理的方式了。我的這個想法在我重新看微信反編譯代碼時得到了印證:

微信也是通過動態代理獲取convertToTranslucent成功的回調

首先在Utils中增加一個繼承自InvocationHandler的類:

    public interface PageTranslucentListener {
        void onPageTranslucent();
    }

    static class MyInvocationHandler implements InvocationHandler {
        private static final String TAG = "MyInvocationHandler";
        private WeakReference<PageTranslucentListener> listener;

        public MyInvocationHandler(WeakReference<PageTranslucentListener> listener) {
            this.listener = listener;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Log.d(TAG, "invoke: end time: " + System.currentTimeMillis());
            Log.d(TAG, "invoke: 被回調了");
            try {
                boolean success = (boolean) args[0];
                if (success && listener.get() != null) {
                    listener.get().onPageTranslucent();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

然后改造一下原來的convertActivityToTranslucentAfterL方法,convertActivityToTranslucentBeforeL同理:

    private static void convertActivityToTranslucentAfterL(Activity activity, PageTranslucentListener listener) {
        try {
            Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
            getActivityOptions.setAccessible(true);
            Object options = getActivityOptions.invoke(activity);

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


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

            Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent",
                    translucentConversionListenerClazz, ActivityOptions.class);
            convertToTranslucent.setAccessible(true);
            Log.d("MyInvocationHandler", "start time: " + System.currentTimeMillis());
            convertToTranslucent.invoke(activity, obj, options);
        } catch (Throwable t) {
        }
    }

原來調用convertToTranslucent的時機是在onEdgeTouch回調中,但這樣會導致只要觸摸到屏幕左側就會執行convertToTranslucent而且觸摸事件會不止一次回調。所以這里調用時機改到ViewDragHelper.Callback的onEdgeDragStarted回調中,只有當SwipeBackLayout開始動了才調用,并且只會調用一次:

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            super.onEdgeDragStarted(edgeFlags, pointerId);
            Log.d("translucentTest", "onEdgeDragStarted");
            Utils.convertActivityToTranslucent(mActivity, new Utils.PageTranslucentListener() {
                @Override
                public void onPageTranslucent() {
                    setPageTranslucent(true);
                    Log.d("translucentTest", "onPageTranslucent: ");
                }
            });
        }

SwipeBackLayout中增加下面的成員pageTranslucent和兩個方法以作設置和標識,pageTranslucent默認值為true:

    private boolean pageTranslucent = true;

    public void setPageTranslucent(boolean pageTranslucent) {
        this.pageTranslucent = pageTranslucent;
    }

    public boolean isPageTranslucent() {
        return pageTranslucent;
    }

有了上述標識,我們就可以知道當前的Activity是否是透明的。
有兩個地方需要處理:

  1. 在手指嘗試滑動SwipeBackLayout時,判斷pageTranslucent是否為true,為true才允許被滑動。而通過分析ViewDragHelper的源碼可知,它的dragTo()方法是唯一觸發拖動行為的方法。所以在dragTo()方法中加入如下兩處判斷:
    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);
            Log.d("translucentTest", "dragTo: mCallback.isPageTranslucent()-->" + mCallback.isPageTranslucent());
            //增加是否透明的判斷
            if (mCallback.isPageTranslucent()) {
                mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
            }
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            mCapturedView.offsetTopAndBottom(clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            //增加是否透明的判斷
            if (mCallback.isPageTranslucent()) {
                mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy);
            }
        }
    }

在Callback中增加回調方法isPageTranslucent()并在SwipeBackLayout中如下實現即可:

        public boolean isPageTranslucent() {
            return SwipeBackLayout.this.isPageTranslucent();
        }

2.在手指松開時,會回調CallBack的onViewReleased()方法,SwipeBackLayout實現了此方法,判斷滑回左邊還是滑到最右邊關閉Activity:


        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            final int childWidth = releasedChild.getWidth();

            int left = 0, top = 0;
            //判斷釋放以后是應該滑到最右邊(關閉),還是最左邊(還原)
            left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
                    + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;

            // settleCapturedViewAt中調用了ViewDragHelper內部mScroller的startScroll()方法,然后通過invalidate刷新就可以觸發SwipeBackLayout的自行滾動
            mDragHelper.settleCapturedViewAt(left, top);
            invalidate();
        }

所以在這里還是要判斷一下,如果當前Activity不透明,那么手指松開后也不進行滑動。
但改完這里測試時發現了一個問題,就是低于21版本的手機執行convertActivityToTranslucentBeforeL()方法時怎么也不起作用,經過一番折騰我找到了原因。原來我一直忽略了Activity的convertToTranslucent方法的真正用法,關于這個方法Activity源碼中有注釋說明,高低版本中均有提到:

Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} back from opaque to translucent following a call to {@link #convertFromTranslucent()}.
……
This call has no effect on non-translucent activities or on activities with the {@link android.R.attr#windowIsFloating} attribute.

意思是說該方法的作用是,在Activity被convertFromTranslucent方法轉為不透明之后,將其再從不透明轉為透明。而且該方法對本來不透明的Activity是沒有作用的。所以我們只有在本身就為透明的Activity中調用convertFromTranslucent將其轉為不透明之后才可以通過convertToTranslucent方法將其再轉為透明。
雖說如此,但api21以上的手機確實是可以直接將本身主題不透明的Activity轉為透明的,21一下的就不行。所以為了兼容,我還是統一將Activity的主題設置為透明,而針對還有web頁面的Activity,再它的onCreate方法中先調用convertFromTranslucent轉為不透明,設置其SwipeBackLayout的pageTranslucent為false,再在側滑開始時調用convertToTranslucent將其轉為透明.

        //在Activity的onCreate中做如下設置
        //將Activity轉為不透明,設置成功,則pageTranslucent為false,否則為true
        boolean opaque = Utils.convertActivityFromTranslucent(this); 
        SwipeBackHelper.onCreate(this);
        SwipeBackHelper.getCurrentPage(this)
                .setSwipeBackEnable(true)
                .setPageTranslucent(!opaque);

Utils中的convertActivityFromTranslucent我也做了點改動:

      public static boolean convertActivityFromTranslucent(Activity activity) {
        try {
            Method method = Activity.class.getDeclaredMethod("convertFromTranslucent");
            method.setAccessible(true);
            method.invoke(activity);
            return true;
        } catch (Throwable t) {
            return false;
        }
    }

鏈式調用中的setPageTranslucent(!opaque)方法是我新增在SwipeBackPage類中的:

public void setPageTranslucent(boolean pageTranslucent) {
    mSwipeBackLayout.setPageTranslucent(pageTranslucent);
}

還有一點可能有人會注意到,就是既然調用convertToTranslucent后到接受到回調需要100ms的時間(如果本身是透明,又調用convertToTranslucent,只需要2ms),那么如果我快速的側滑,在100ms之前就松開手指了,豈不是側滑無法響應了,這樣就會出現慢速地話可以滑動,快速滑不能滑動的情況。還有,如果convertToTranslucent出現異常了,pageTranslucent始終為false,豈不是也滑不動了。
確實,這兩個問題也著實讓我頭疼了兩個小時。最終我找到了一個取巧的方式解決了,更巧的事,我發現微信也是這樣整的。先看我的代碼:

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            final int childWidth = releasedChild.getWidth();

            int left = 0, top = 0;
            //判斷釋放以后是應該滑到最右邊(關閉),還是最左邊(還原)
            left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
                    + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;

            if (isPageTranslucent()) {
                // 當前page背景是透明時,釋放手指后才可以滑動
                mDragHelper.settleCapturedViewAt(left, top);
                invalidate();
            } else {
                if (left > 0 && !mActivity.isFinishing()) {
                    mActivity.finish();
                    mActivity.overridePendingTransition(0, R.anim.slide_out_right);
                }
            }
        }

R.anim.slide_out_right的xml代碼:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:fromXDelta="0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXDelta="100%p" />

為什么說取巧呢,因為我這里用Activity退出的動畫以假亂真模擬了側滑退出的效果。那憑什么說微信也是用這種方式呢,請看我的證據:

微信web界面側滑退出的兩種效果

這兩張圖,左邊的是慢速滑動時的效果,右邊是快速滑動時的效果。相信大家已經看出不一致的地方了,那就是滑動層左側的陰影。側滑時是上層Activity的SwipeBackLayout不停改變坐標平移產生的效果,而陰影是在SwipeBackLayout不停重繪的過程中畫上去的:

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        final boolean drawContent = child == mContentView;

        boolean ret = super.drawChild(canvas, child, drawingTime);
        if (mScrimOpacity > 0 && drawContent
                && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
             // 畫側邊陰影
            drawShadow(canvas, child);
            // 畫覆蓋在可見的下層Activity區域之上的灰色半透明蒙層
            // 將這句代碼注釋掉,就是像微信一樣只要側邊一點陰影的效果
            drawScrim(canvas, child);  
        }
        return ret;
    }

    private void drawScrim(Canvas canvas, View child) {
        final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
        final int alpha = (int) (baseAlpha * mScrimOpacity);
        final int color = alpha << 24 | (mScrimColor & 0xffffff);
        canvas.clipRect(0, 0, child.getLeft(), getHeight());
        canvas.drawColor(color);
    }

    private void drawShadow(Canvas canvas, View child) {
        final Rect childRect = mTmpRect;
        child.getHitRect(childRect);

        mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top,
                childRect.left, childRect.bottom);
        mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
        mShadowLeft.draw(canvas);
    }

而如果是通過overridePendingTransition設置的Activity退出的動畫的話,是無法繪制出陰影的,因為這種情況只出現在快速滑動的情況下,所以也很難被看出。大家可以試試微信的側滑,當你快速滑動含web界面的Activity時,明顯可以看出是手指松開后,Activiy才動的,而其他不含web界面的Activity就不會如此。還有一點細節,就是微信的側滑一般都是上下Activity聯動的,細心的朋友會發現含web界面的Activity的側滑偏偏沒有聯動,為什么呢?就是因為它快速滑動時使用的通過overridePendingTransition設置的Activity退出動畫,是無法設置聯動的,所以索性把聯動給取消了。
個人覺得微信對這種UI細節的處理真得打磨得特別用心,佩服!
如此,不管是快速滑動還是convertToTranslucent出現異常導致pageTranslucent為false,都不會讓用戶突然滑不動。

好了,啰哩啰嗦說了這么多,不知道會不會有人碰到這樣的問題。

最后簡短總結一下吧

解決本文所述問題的終極姿勢是:

  1. 按照我以上所述正確修改SwipeBackHelper的源碼;
  2. 首先將Activity主題style中的window透明屬性設置為true:
<item name="android:windowIsTranslucent">true</item>

這里還要說明一點,就是在更低版本的手機上或者被定制了UI的手機上,會出現反射獲取方法時根本找不到convertFromTranslucent和convertToTranslucent方法的情況,那么有兩種處理方案:要么不處理,convertFromTranslucent沒有調用成功,pageTranslucent會被設置為true,不影響側滑,webActivity透明問題出現也不用管,畢竟低版本的手機也不是很多了;要么分版本設置style,低于某個版本(微信是17)的話,就直接設置android:windowIsTranslucent為false,并且全部禁用側滑退出Activity的功能。

  1. 在Activity的onCreate()中設置透明屬性和側滑功能:
 boolean opaque = Utils.convertActivityFromTranslucent(this);
 SwipeBackHelper.onCreate(this);
 SwipeBackHelper.getCurrentPage(this) 
                  .setSwipeBackEnable(true) 
                  .setSwipeRelateEnable(false)
                  .setPageTranslucent(!opaque);

4.(12月30日)補充:
SwipeBackHelper的源碼中定義了統一的當前打開的Activity的進場和退場動畫:

    <style name="SlideRightAnimation" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item>
        <item name="android:activityOpenExitAnimation">@null</item>
        <item name="android:activityCloseEnterAnimation">@null</item>
        <item name="android:activityCloseExitAnimation">@anim/slide_out_right</item>
        <item name="android:taskOpenEnterAnimation">@anim/slide_in_right</item>
        <item name="android:taskOpenExitAnimation">@null</item>
        <item name="android:taskCloseEnterAnimation">@null</item>
        <item name="android:taskCloseExitAnimation">@anim/slide_out_right</item>
        <item name="android:taskToFrontEnterAnimation">@anim/slide_in_right</item>
        <item name="android:taskToFrontExitAnimation">@null</item>
        <item name="android:taskToBackEnterAnimation">@null</item>
        <item name="android:taskToBackExitAnimation">@anim/slide_out_right</item>
    </style>

但不夠完善,可以看到android:activityOpenExitAnimation之類的動畫是沒有定義的,android:activityOpenExitAnimation指定的是當執行打開一個Activity的動畫時,即將退出的那個Activity的退場動畫,比如我當前在ActivityB,要打開ActivityA,那么當我打開ActivityA的一瞬間會發生兩個動作:一是ActivityA被打開并執行它的進場動畫(slide_in_right),一是ActivityB被關閉并執行它的退場動畫(當前是null)。因為不同手機的Activity的動畫被進行了不同的定制,有的是左滑退出,有的是直接縮小退出,有的是快速滑向底部退出。提出這個問題是因為我發現在某些測試機上,當上層Activity執行側滑退出時,下層Activity的頂部連接狀態欄的地方會閃一下,研究半天才明白原來是因為的下層Activity的退場動畫是系統默認的(刷的一下往下消失),所以會有一條陰影在狀態欄附近快速地閃一下。解決方案就是在上面style的基礎上把android:activityOpenExitAnimation屬性也指定清楚:

    <style name="BaseSlideAnimation" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item>
        <item name="android:activityOpenExitAnimation">@anim/slide_out_left</item>
        <item name="android:activityCloseEnterAnimation">@anim/slide_in_left</item>
        <item name="android:activityCloseExitAnimation">@anim/slide_out_right</item>
        <item name="android:taskOpenEnterAnimation">@anim/slide_in_right</item>
        <item name="android:taskOpenExitAnimation">@anim/slide_out_left</item>
        <item name="android:taskCloseEnterAnimation">@anim/slide_in_left</item>
        <item name="android:taskCloseExitAnimation">@anim/slide_out_right</item>
        <item name="android:taskToFrontEnterAnimation">@anim/slide_in_right</item>
        <item name="android:taskToFrontExitAnimation">@anim/slide_out_left</item>
        <item name="android:taskToBackEnterAnimation">@anim/slide_in_left</item>
        <item name="android:taskToBackExitAnimation">@anim/slide_out_right</item>

slide_in_right.xml:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:fromXDelta="100%p"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXDelta="0" />

slide_out_right.xml:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:fromXDelta="0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXDelta="100%p" />

slide_in_left.xml:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:fromXDelta="-30%p"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXDelta="0" />

slide_out_left.xml:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:fromXDelta="0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXDelta="-30%p" />

這個style的效果也是跟微信差不多的,目前我項目中就是這樣使用的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 前言 側滑手勢在Android App應用得非常廣泛,常見的使用場景包括:滑動抽屜、側滑刪除、側滑返回、下拉刷新以...
    billy05閱讀 3,505評論 1 19
  • 這些年的習慣,節假日去個園林式的酒店住幾天,以酒店為原點,活動范圍不超過2公里,聽聽鳥叫,看看花草,喝喝茶。老了。
    明月百年心閱讀 256評論 0 0
  • 聽書譜講解
    靜思宅閱讀 309評論 0 2
  • windows 7版本的: 1.官網下載安裝系統版本的mysql壓縮版本的 2.解壓 3.mysqld --ini...
    夕陽_好閱讀 283評論 1 1