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


圖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)核,同樣具有側(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)行修改的。

源碼并不復(fù)雜,具體用法我就不解釋了,項(xiàng)目github上說得很詳細(xì)。我簡單說下每個(gè)類的主要功能:
- 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);
}
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源碼解析SwipeBackPage,每個(gè)滑動(dòng)頁面的管理類,該類持有當(dāng)前Activity、與Activity關(guān)聯(lián)的SwipeBackLayout和一個(gè)RelateSlider的引用,并提供一系列鏈?zhǔn)秸{(diào)用的方法設(shè)置SwipeBackLayout的相關(guān)屬性;
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();
}
SwipeListener,簡單的接口,提供了觸摸和滑動(dòng)SwipeBackLayout時(shí)的三個(gè)回調(diào)方法;
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)的效果。
Utils 最不起眼的一個(gè)類,在這個(gè)項(xiàng)目中都沒用到好伐。不過正是這個(gè)類,才是我解決問題的關(guān)鍵,這個(gè)類的源碼不太對(duì),后面我會(huì)貼出修改后的代碼。
二. 反編譯微信apk尋找靈感
雖然了解了SwipeBackHelper的實(shí)現(xiàn)原理,但剛開始我還是想不通微信是如何處理我開頭提出的問題。我Google了大半天都找不出有人有類似的問題,索性直接反編譯微信apk,看看能不能找到一些端倪,沒想到,還真被我找到了。

在反編譯后的java代碼中,我找到了一個(gè)SwipeBackLayout的類,很明顯,微信側(cè)滑后退的實(shí)現(xiàn)方式跟上面開源庫的差不多,只不過人家自己做了整合和優(yōu)化。我一眼看到"convertToTranslucent",就知道這個(gè)肯定跟處理透明問題有關(guān),后來我才發(fā)現(xiàn)原來同時(shí)出現(xiàn)在SwipeBackHelper和SwipeBackLayout項(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è)明顯不好的地方在于:
- 反射調(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í)得到了印證:

首先在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è)地方需要處理:
- 在手指嘗試滑動(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ù):

這兩張圖,左邊的是慢速滑動(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ì)是:
- 按照我以上所述正確修改SwipeBackHelper的源碼;
- 首先將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的功能。
- 在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)目中就是這樣使用的。