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

本文缘起

因为我做的app里使用了SwipeBackHelper的开源库来实现Activity的侧滑后退,本来使用起来一直没什么问题,但在新版本中接入了腾讯x5内核的WebView后就出现了一个小问题。看下图:

Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第1张图片
图1
Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第2张图片
图2

图2中两条黑线之间就是图1中所展示的视频播放的区域,但图2中显示的不是视频内容,而是当前的WebActivity下层的MainActivity的部分视图。因为当进入网页播放页面点击视频播放按钮后,视频播放区域会突然变成透明的,直到视频加载出来之后才会开始显示视频内容,该过程持续1秒到数秒不等。本来如果只是闪现一下就消失也没什么大问题,但有的网页中的视频加载过慢,导致这个透明现象出现的时间过长,所以app运营渠道提出需要解决该问题。

问题分析

经测试,该问题出现是因为满足了两个条件:
1.Activity的主题style中满足属性:true (这也是使用SwipeBackHelper的必要条件);
2.使用x5内核的WebView播放视频。
对于我们的项目来说,x5是不能放弃的,但侧滑退出的效果在三个版本之前就加入了,现在要针对某些页面去掉,也让我觉得很不爽。此时当然是参考微信的效果喽,结果微信给我的结果是这样的:

Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第3张图片
微信x5内核WebView播放视频效果

微信同样是使用x5内核,同样具有侧滑退出得效果,当播放相同视频时,本该显示透明的区域却显示的是黑色的背景。微信究竟是如何解决的呢?
我尝试了给WebView增加背景色,给WebView增加父容器后再增加背景色,给Activity的Window和DecorView设置背景色,但没有作用。只要Activity的主题style中设置了窗体透明,该问题无论如何都会出现。

问题解决

无奈之下,我尝试解决这个问题,虽然说是个小问题,着实花了一番功夫。下面我会从三个方面来说明我在寻求解决方案的过程中学习和总结到的一些东西。因为这个问题遇到的人不多,而且我只是在SwipeBackHelper的源码基础上做了一些修改,所以就不上传代码到github了,但我会详细说明我修改的过程和原理,相信读完本文,你会对SwipeBackHelper的工作原理有更多地了解,也会了解到通过反编译成熟apk寻找解决方案的学习方法。

一. SwipeBackHelper的实现原理

其实我搜索了很久找其他实现侧滑后退的方案,但发现不管什么方案,设置true这一条件都被声明为必要条件,否则就会出现侧滑时出现下层背景为黑的bug。所以最终我只有阅读一下源码来看看侧滑后退的原理究竟是什么。大家搜索时会发现github上有一个star数量更多的相关项目SwipeBackLayout,我看了两个项目各自的代码,从github分支推送的时间来看,SwipeBackLayout是最先出现的。两者的代码80%的代码是相似的,SwipeBackHelper只是在SwipeBackLayout的基础上对其中的主要控件进行了解耦,提取出来了一个SwipeBackHelper和SwipeBackPage两个管理类,使用法更加清晰明了,同时实现了当前Activity侧滑关闭时与下层Activity的联动效果,跟微信已经99%相似了(是的,我要解决的就是那1%的问题)。因为我项目用的是SwipeBackHelper项目,所以我也是在它的源码基础上进行修改的。

Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第4张图片
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 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,看看能不能找到一些端倪,没想到,还真被我找到了。

Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第5张图片
微信的SwipeBackLayout

在反编译后的java代码中,我找到了一个SwipeBackLayout的类,很明显,微信侧滑后退的实现方式跟上面开源库的差不多,只不过人家自己做了整合和优化。我一眼看到"convertToTranslucent",就知道这个肯定跟处理透明问题有关,后来我才发现原来同时出现在SwipeBackHelper和SwipeBackLayout项目中的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()}.
     * 

* 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. *

* 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 true if Window was opaque and will become translucent or * false 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.
     * 

* 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. *

* 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,我在微信上进行了尝试,不得不说我机智地发现微信并没有处理这种情况:

Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第6张图片

上图中视频区域显示的是下层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 listener;

        public MyInvocationHandler(WeakReference 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(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代码:




为什么说取巧呢,因为我这里用Activity退出的动画以假乱真模拟了侧滑退出的效果。那凭什么说微信也是用这种方式呢,请看我的证据:

Android版与微信Activity侧滑后退效果完全相同的SwipeBackLayout_第7张图片
微信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:
true

这里还要说明一点,就是在更低版本的手机上或者被定制了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的进场和退场动画:

    

但不够完善,可以看到android:activityOpenExitAnimation之类的动画是没有定义的,android:activityOpenExitAnimation指定的是当执行打开一个Activity的动画时,即将退出的那个Activity的退场动画,比如我当前在ActivityB,要打开ActivityA,那么当我打开ActivityA的一瞬间会发生两个动作:一是ActivityA被打开并执行它的进场动画(slide_in_right),一是ActivityB被关闭并执行它的退场动画(当前是null)。因为不同手机的Activity的动画被进行了不同的定制,有的是左滑退出,有的是直接缩小退出,有的是快速滑向底部退出。提出这个问题是因为我发现在某些测试机上,当上层Activity执行侧滑退出时,下层Activity的顶部连接状态栏的地方会闪一下,研究半天才明白原来是因为的下层Activity的退场动画是系统默认的(刷的一下往下消失),所以会有一条阴影在状态栏附近快速地闪一下。解决方案就是在上面style的基础上把android:activityOpenExitAnimation属性也指定清楚: