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

本文緣起

因?yàn)槲易龅腶pp里使用了SwipeBackHelper的開源庫來實(shí)現(xiàn)Activity的側(cè)滑后退,本來使用起來一直沒什么問題,但在新版本中接入了騰訊x5內(nèi)核的WebView后就出現(xiàn)了一個(gè)小問題。看下圖:

圖1
圖2

圖2中兩條黑線之間就是圖1中所展示的視頻播放的區(qū)域,但圖2中顯示的不是視頻內(nèi)容,而是當(dāng)前的WebActivity下層的MainActivity的部分視圖。因?yàn)楫?dāng)進(jìn)入網(wǎng)頁播放頁面點(diǎn)擊視頻播放按鈕后,視頻播放區(qū)域會(huì)突然變成透明的,直到視頻加載出來之后才會(huì)開始顯示視頻內(nèi)容,該過程持續(xù)1秒到數(shù)秒不等。本來如果只是閃現(xiàn)一下就消失也沒什么大問題,但有的網(wǎng)頁中的視頻加載過慢,導(dǎo)致這個(gè)透明現(xiàn)象出現(xiàn)的時(shí)間過長,所以app運(yùn)營渠道提出需要解決該問題。

問題分析

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

微信x5內(nèi)核WebView播放視頻效果

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

問題解決

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

一. SwipeBackHelper的實(shí)現(xiàn)原理

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

SwipeBackHelper源碼文件

源碼并不復(fù)雜,具體用法我就不解釋了,項(xiàng)目github上說得很詳細(xì)。我簡單說下每個(gè)類的主要功能:

  1. SwipeBackLayout,是一個(gè)繼承自FrameLayout的ViewGroup,我們側(cè)滑后退時(shí)滑動(dòng)的就是這個(gè)ViewGroup,需要側(cè)滑的Activity執(zhí)行onCreate時(shí),需要設(shè)置setSwipeBackEnable(true),這句代碼執(zhí)行時(shí)會(huì)調(diào)用SwipeBackLayout的attachToActivity,如下所示,該方法會(huì)找到Activity的Window界面的最頂層View,即DecorView,并找到DecorView的直接子view將它替換為SwipeBackLayout,同時(shí)將原來的子view添加到SwipeBackLayout中。這樣一來,SwipeBackLayout就會(huì)在Activity的所有布局(我們自己寫得xml所生成的布局)之上了),當(dāng)我們滑動(dòng)Activity時(shí),如果是在側(cè)邊(一般是屏幕左側(cè))可以觸發(fā)側(cè)滑后退動(dòng)作的區(qū)域內(nèi),SwipeBackLayout就會(huì)攔截觸摸事件,自己進(jìn)行處理,執(zhí)行被拖動(dòng)或滑動(dòng)退出的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,實(shí)現(xiàn)滑動(dòng)和拖動(dòng)的輔助類,其實(shí)就是在Android原生的ViewDragHelper上進(jìn)行了小小的修改,ViewDragHelper是一個(gè)非常強(qiáng)大的類,簡單的調(diào)用就可以幫我們實(shí)現(xiàn)View的滑動(dòng)和拖動(dòng)效果,SwipeBackLayout的onInterceptTouchEvent和onTouchEvent的處理都是交給ViewDragHelper來做的,所以要深入理解側(cè)滑的實(shí)現(xiàn)機(jī)制,需要知道ViewDragHelper是如何工作的,感興趣的同學(xué)可以直接讀下面兩篇博客,讀完應(yīng)該就理解得差不多了:
    Android ViewDragHelper完全解析 自定義ViewGroup神器
    Android ViewDragHelper源碼解析

  2. SwipeBackPage,每個(gè)滑動(dòng)頁面的管理類,該類持有當(dāng)前Activity、與Activity關(guān)聯(lián)的SwipeBackLayout和一個(gè)RelateSlider的引用,并提供一系列鏈?zhǔn)秸{(diào)用的方法設(shè)置SwipeBackLayout的相關(guān)屬性;

  3. SwipeBackHelper,滑動(dòng)的全局管理類,也是提供給我們?cè)贏ctivity中開啟側(cè)滑退出功能的工具類。在Activity的onCreate中調(diào)用SwipeBackHelper的onCreate方法時(shí),其內(nèi)部會(huì)創(chuàng)建一個(gè)與該Activity關(guān)聯(lián)的SwipeBackPage,并通過一個(gè)Stack集合記錄管理所有關(guān)聯(lián)過Activity的SwipeBackPage,需要下層Activity聯(lián)動(dòng)時(shí)就可以通過該類的getPrePage獲取到下層Activity相關(guān)聯(lián)的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,簡單的接口,提供了觸摸和滑動(dòng)SwipeBackLayout時(shí)的三個(gè)回調(diào)方法;

  2. RelateSlider,有下層Activity聯(lián)動(dòng)時(shí)需要用到的一個(gè)類,它實(shí)現(xiàn)了SwipeListener接口,在上層Activity的SwipeBackLayout被滑動(dòng)時(shí),會(huì)回調(diào)到它實(shí)現(xiàn)的onScroll和onScrollToClose方法,從而實(shí)現(xiàn)下層Activity的SwipeBackLayout位置的改變,達(dá)到聯(lián)動(dòng)的效果。

  3. Utils 最不起眼的一個(gè)類,在這個(gè)項(xiàng)目中都沒用到好伐。不過正是這個(gè)類,才是我解決問題的關(guān)鍵,這個(gè)類的源碼不太對(duì),后面我會(huì)貼出修改后的代碼。

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

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

微信的SwipeBackLayout

在反編譯后的java代碼中,我找到了一個(gè)SwipeBackLayout的類,很明顯,微信側(cè)滑后退的實(shí)現(xiàn)方式跟上面開源庫的差不多,只不過人家自己做了整合和優(yōu)化。我一眼看到"convertToTranslucent",就知道這個(gè)肯定跟處理透明問題有關(guān),后來我才發(fā)現(xiàn)原來同時(shí)出現(xiàn)在SwipeBackHelperSwipeBackLayout項(xiàng)目中的Utils中寫的正是反射調(diào)用Activity的"convertToTranslucent"方法,而且在SwipeBackLayout中的Utils是被使用過的,使用時(shí)機(jī)是在SwipeBackLayout的onEdgeTouch回掉中,也就是在側(cè)滑動(dòng)作觸發(fā)之前。而這個(gè)"convertToTranslucent"方法的作用正是讓不透明的Activity轉(zhuǎn)為透明。
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設(shè)置為false,然后在側(cè)滑被觸發(fā)之前調(diào)用convertToTranslucent不就好了。
事實(shí)證明的確是可以的,但有兩個(gè)明顯不好的地方在于:

  1. 反射調(diào)用convertToTranslucent方法會(huì)使相關(guān)聯(lián)的Activity重繪,測(cè)試發(fā)現(xiàn)這個(gè)過程需要100ms的時(shí)間,所以如果側(cè)滑動(dòng)作很快,就會(huì)出現(xiàn)黑邊閃現(xiàn),體驗(yàn)不太好;
    2.如果側(cè)滑動(dòng)作進(jìn)行一半,用戶又滑回去了選擇暫時(shí)不關(guān)閉Activity,其實(shí)Activity已經(jīng)轉(zhuǎn)換成透明了,再播放視頻的話透明現(xiàn)象還會(huì)出現(xiàn)。對(duì)于這個(gè)問題,我本來覺得可以在它滑回的時(shí)候調(diào)用Utils中的convertActivityFromTranslucent再將Activity轉(zhuǎn)為不透明,但測(cè)試發(fā)現(xiàn),這樣反轉(zhuǎn)一下后,視頻播放區(qū)域就直接全黑了,再也不出現(xiàn)視頻內(nèi)容了。

對(duì)于問題2,我在微信上進(jìn)行了嘗試,不得不說我機(jī)智地發(fā)現(xiàn)微信并沒有處理這種情況:


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

三. 解決問題的終極姿勢(shì)

快速滑動(dòng)出現(xiàn)黑邊問題的根本原因是convertToTranslucent是需要100ms左右的時(shí)間的,而且這個(gè)事件不固定跟手機(jī)的硬件配置有關(guān),所以思路是先等待convertToTranslucent成功的回調(diào),然后再觸發(fā)Activity的側(cè)滑。

 /**
     * 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) {
        }
    }

然而調(diào)用Activity的convertToTranslucent方法本來就是通過反射的方式,無法直接傳入回調(diào)接口。這樣一來只有通過動(dòng)態(tài)代理的方式了。我的這個(gè)想法在我重新看微信反編譯代碼時(shí)得到了印證:

微信也是通過動(dòng)態(tài)代理獲取convertToTranslucent成功的回調(diào)

首先在Utils中增加一個(gè)繼承自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: 被回調(diào)了");
            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) {
        }
    }

原來調(diào)用convertToTranslucent的時(shí)機(jī)是在onEdgeTouch回調(diào)中,但這樣會(huì)導(dǎo)致只要觸摸到屏幕左側(cè)就會(huì)執(zhí)行convertToTranslucent而且觸摸事件會(huì)不止一次回調(diào)。所以這里調(diào)用時(shí)機(jī)改到ViewDragHelper.Callback的onEdgeDragStarted回調(diào)中,只有當(dāng)SwipeBackLayout開始動(dòng)了才調(diào)用,并且只會(huì)調(diào)用一次:

        @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和兩個(gè)方法以作設(shè)置和標(biāo)識(shí),pageTranslucent默認(rèn)值為true:

    private boolean pageTranslucent = true;

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

    public boolean isPageTranslucent() {
        return pageTranslucent;
    }

有了上述標(biāo)識(shí),我們就可以知道當(dāng)前的Activity是否是透明的。
有兩個(gè)地方需要處理:

  1. 在手指嘗試滑動(dòng)SwipeBackLayout時(shí),判斷pageTranslucent是否為true,為true才允許被滑動(dòng)。而通過分析ViewDragHelper的源碼可知,它的dragTo()方法是唯一觸發(fā)拖動(dòng)行為的方法。所以在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中增加回調(diào)方法isPageTranslucent()并在SwipeBackLayout中如下實(shí)現(xiàn)即可:

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

2.在手指松開時(shí),會(huì)回調(diào)CallBack的onViewReleased()方法,SwipeBackLayout實(shí)現(xiàn)了此方法,判斷滑回左邊還是滑到最右邊關(guān)閉Activity:


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

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

            // settleCapturedViewAt中調(diào)用了ViewDragHelper內(nèi)部mScroller的startScroll()方法,然后通過invalidate刷新就可以觸發(fā)SwipeBackLayout的自行滾動(dòng)
            mDragHelper.settleCapturedViewAt(left, top);
            invalidate();
        }

所以在這里還是要判斷一下,如果當(dāng)前Activity不透明,那么手指松開后也不進(jìn)行滑動(dòng)。
但改完這里測(cè)試時(shí)發(fā)現(xiàn)了一個(gè)問題,就是低于21版本的手機(jī)執(zhí)行convertActivityToTranslucentBeforeL()方法時(shí)怎么也不起作用,經(jīng)過一番折騰我找到了原因。原來我一直忽略了Activity的convertToTranslucent方法的真正用法,關(guān)于這個(gè)方法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方法轉(zhuǎn)為不透明之后,將其再從不透明轉(zhuǎn)為透明。而且該方法對(duì)本來不透明的Activity是沒有作用的。所以我們只有在本身就為透明的Activity中調(diào)用convertFromTranslucent將其轉(zhuǎn)為不透明之后才可以通過convertToTranslucent方法將其再轉(zhuǎn)為透明。
雖說如此,但api21以上的手機(jī)確實(shí)是可以直接將本身主題不透明的Activity轉(zhuǎn)為透明的,21一下的就不行。所以為了兼容,我還是統(tǒng)一將Activity的主題設(shè)置為透明,而針對(duì)還有web頁面的Activity,再它的onCreate方法中先調(diào)用convertFromTranslucent轉(zhuǎn)為不透明,設(shè)置其SwipeBackLayout的pageTranslucent為false,再在側(cè)滑開始時(shí)調(diào)用convertToTranslucent將其轉(zhuǎn)為透明.

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

Utils中的convertActivityFromTranslucent我也做了點(diǎn)改動(dòng):

      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;
        }
    }

鏈?zhǔn)秸{(diào)用中的setPageTranslucent(!opaque)方法是我新增在SwipeBackPage類中的:

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

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

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

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

            if (isPageTranslucent()) {
                // 當(dāng)前page背景是透明時(shí),釋放手指后才可以滑動(dòng)
                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" />

為什么說取巧呢,因?yàn)槲疫@里用Activity退出的動(dòng)畫以假亂真模擬了側(cè)滑退出的效果。那憑什么說微信也是用這種方式呢,請(qǐng)看我的證據(jù):

微信web界面?zhèn)然顺龅膬煞N效果

這兩張圖,左邊的是慢速滑動(dòng)時(shí)的效果,右邊是快速滑動(dòng)時(shí)的效果。相信大家已經(jīng)看出不一致的地方了,那就是滑動(dòng)層左側(cè)的陰影。側(cè)滑時(shí)是上層Activity的SwipeBackLayout不停改變坐標(biāo)平移產(chǎn)生的效果,而陰影是在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) {
             // 畫側(cè)邊陰影
            drawShadow(canvas, child);
            // 畫覆蓋在可見的下層Activity區(qū)域之上的灰色半透明蒙層
            // 將這句代碼注釋掉,就是像微信一樣只要側(cè)邊一點(diǎn)陰影的效果
            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設(shè)置的Activity退出的動(dòng)畫的話,是無法繪制出陰影的,因?yàn)檫@種情況只出現(xiàn)在快速滑動(dòng)的情況下,所以也很難被看出。大家可以試試微信的側(cè)滑,當(dāng)你快速滑動(dòng)含web界面的Activity時(shí),明顯可以看出是手指松開后,Activiy才動(dòng)的,而其他不含web界面的Activity就不會(huì)如此。還有一點(diǎn)細(xì)節(jié),就是微信的側(cè)滑一般都是上下Activity聯(lián)動(dòng)的,細(xì)心的朋友會(huì)發(fā)現(xiàn)含web界面的Activity的側(cè)滑偏偏沒有聯(lián)動(dòng),為什么呢?就是因?yàn)樗焖倩瑒?dòng)時(shí)使用的通過overridePendingTransition設(shè)置的Activity退出動(dòng)畫,是無法設(shè)置聯(lián)動(dòng)的,所以索性把聯(lián)動(dòng)給取消了。
個(gè)人覺得微信對(duì)這種UI細(xì)節(jié)的處理真得打磨得特別用心,佩服!
如此,不管是快速滑動(dòng)還是convertToTranslucent出現(xiàn)異常導(dǎo)致pageTranslucent為false,都不會(huì)讓用戶突然滑不動(dòng)。

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

最后簡短總結(jié)一下吧

解決本文所述問題的終極姿勢(shì)是:

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

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

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

4.(12月30日)補(bǔ)充:
SwipeBackHelper的源碼中定義了統(tǒng)一的當(dāng)前打開的Activity的進(jìn)場(chǎng)和退場(chǎng)動(dòng)畫:

    <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之類的動(dòng)畫是沒有定義的,android:activityOpenExitAnimation指定的是當(dāng)執(zhí)行打開一個(gè)Activity的動(dòng)畫時(shí),即將退出的那個(gè)Activity的退場(chǎng)動(dòng)畫,比如我當(dāng)前在ActivityB,要打開ActivityA,那么當(dāng)我打開ActivityA的一瞬間會(huì)發(fā)生兩個(gè)動(dòng)作:一是ActivityA被打開并執(zhí)行它的進(jìn)場(chǎng)動(dòng)畫(slide_in_right),一是ActivityB被關(guān)閉并執(zhí)行它的退場(chǎng)動(dòng)畫(當(dāng)前是null)。因?yàn)椴煌謾C(jī)的Activity的動(dòng)畫被進(jìn)行了不同的定制,有的是左滑退出,有的是直接縮小退出,有的是快速滑向底部退出。提出這個(gè)問題是因?yàn)槲野l(fā)現(xiàn)在某些測(cè)試機(jī)上,當(dāng)上層Activity執(zhí)行側(cè)滑退出時(shí),下層Activity的頂部連接狀態(tài)欄的地方會(huì)閃一下,研究半天才明白原來是因?yàn)榈南聦覣ctivity的退場(chǎng)動(dòng)畫是系統(tǒng)默認(rèn)的(刷的一下往下消失),所以會(huì)有一條陰影在狀態(tài)欄附近快速地閃一下。解決方案就是在上面style的基礎(chǔ)上把a(bǔ)ndroid: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" />

這個(gè)style的效果也是跟微信差不多的,目前我項(xiàng)目中就是這樣使用的。

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 前言 側(cè)滑手勢(shì)在Android App應(yīng)用得非常廣泛,常見的使用場(chǎng)景包括:滑動(dòng)抽屜、側(cè)滑刪除、側(cè)滑返回、下拉刷新以...
    billy05閱讀 3,639評(píng)論 1 19
  • 這些年的習(xí)慣,節(jié)假日去個(gè)園林式的酒店住幾天,以酒店為原點(diǎn),活動(dòng)范圍不超過2公里,聽聽鳥叫,看看花草,喝喝茶。老了。
    明月百年心閱讀 288評(píng)論 0 0
  • 聽書譜講解
    靜思宅閱讀 328評(píng)論 0 2
  • windows 7版本的: 1.官網(wǎng)下載安裝系統(tǒng)版本的mysql壓縮版本的 2.解壓 3.mysqld --ini...
    夕陽_好閱讀 298評(píng)論 1 1

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