前言
activity的滑動返回也是個常用的功能,網上有很多介紹怎么去實現的庫,我測過其中幾個,包括star比較多的
但是這兩個庫各有各的缺點,
SwipeBackLayout
沒有實現上一個activity跟隨滑動的效果,這也是網上大多數介紹滑動返回文章的問題,只是粗略實現了當前界面滑動后 finish的效果。BGASwipeBackLayout-Android
實現了跟隨滑動的效果,但是其一是跟隨滑動不流暢,并且在將activity的主題設置為透明之后,將activity主題設置為透明后,會有bug(前一個activity在滑動的過程中,底部會有黑背景);第二個問題是不夠輕量,仔細去看源代碼,有很多不需要的代碼邏輯,包括measure和layout等不需要的設計。
于是我決定在前人的基礎上重寫一個滑動返回的控件
思路
- Android的activity滑動的實現的設計思路大部分都是借助
ViewDragHelper
這個類實現的,因為這個類可以幫我們處理很多的手勢檢測和尺寸計算。 - 上個activity跟隨滑動的實現,當手指開始從左側邊緣滑動的時候,通過將上個activity的contentView暫時添加到當前activity的contentView下方,通過屬性動畫讓它跟隨當前activity的滑動而滑動
整個過程入下圖展示
上圖中
viewDragHelper
其實是一個包含viewDragHelper
的FramLayout
,我取名為SlidebackLayout
這里之所以不是直接添加
preContentView
而是用一個preWrapper
來包裝,是因為不能在SlidebackLayout
初始化的時候去獲取preContentView
,因為這個時候activity整window沒有繪制,獲取的preContentView
會是一片空白,所以是在activity初始化好展示給用戶,用戶要開始拖拽的時候才去添加preContentView
。因此我們在SlidebackLayout
初始化的時候用一個preWrapper
來占位,它存在于當前contentView
的下方.
代碼
SlideBackLayout
這是整個拖拽的核心,他包含一個
viewDragHelper
。viewDragHelper的使用創建方法不多做解釋,我們主要看一下它的ViewDragHelper.Callback,因為主要的處理邏輯都在這里
public SlideBackLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.mShawDrawable = ContextCompat.getDrawable(getContext(), R.drawable.bga_sbl_shadow);
mViewDragHelper = ViewDragHelper.create(this, 1.0f, mDragCallback);
//支持左側邊緣滑動
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
mViewDragHelper.captureChildView(mCurrentContentView, pointerId);
if (!mPreContentViewWrapper.isBindPreActivity())
mPreContentViewWrapper.bindPreActivity(mCurrentActivity);
if (mSlideListener != null)
mSlideListener.onSlideStart();
}
@Override
public void onViewDragStateChanged(int state) {
if (state == ViewDragHelper.STATE_IDLE) {
//返回上個界面
if (mCurrentContentView.getLeft() >= mWidth) {
if (mSlideListener != null) {
mSlideListener.onSlideComplete();
}
mCurrentActivity.finish();
mCurrentActivity.overridePendingTransition(0,0);
mCurrentActivity.getWindow().getDecorView().setVisibility(GONE);
removeView(mPreContentViewWrapper);
mPreContentViewWrapper.unBindPreActivity();
} else {
//返回當前界面
if (mSlideListener != null)
mSlideListener.onSlideCancel();
}
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
mDragLeftX = capturedChild.getLeft();
mDragTopY = capturedChild.getTop();
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left < 0 ? 0 : left;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (releasedChild.getLeft() > mWidth * BACK_THRESHOLD_RATIO) {
mViewDragHelper.settleCapturedViewAt(mWidth, mDragTopY);
} else {
mViewDragHelper.settleCapturedViewAt(mDragLeftX, mDragTopY);
}
invalidate();
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if (mPreContentViewWrapper != null && mPreContentViewWrapper.isBindPreActivity()) {
float ratio = left * 1.0f / mWidth;
mPreContentViewWrapper.onSlideChange(ratio);
mShawDrawable.setBounds(left - SHADOW_WIDTH, 0, left, mHeight);
if (mSlideListener != null)
mSlideListener.onSliding(ratio);
invalidate();
}
}
};
注意到上述代碼中有個
mPreContentViewWrapper
,這個就是上面我們提到的包裝preContentView
的容器。我將它抽取成一個自定義view,看下它的代碼
/*============================上一個界面的容器=============================*/
public static class PreContentViewWrapper extends FrameLayout {
private static final float TRANSLATE_X_RATIO = 0.3f;//當前頁面在滑動的時候,前一個界面初始被隱藏的寬度為0.3*width
private WeakReference<Activity> mPreActivityRef;
private ViewGroup mPreDecorView;
private ViewGroup mPreContentView;
private ViewGroup.LayoutParams mPreLayoutParams;
private boolean isBindPreActivity;
private int mHideWidth;
public PreContentViewWrapper(Context context) {
this(context, null);
}
public PreContentViewWrapper(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHideWidth = (int) (TRANSLATE_X_RATIO * w);
}
/**
* 綁定上一個activity的ContenView
* @param currentActivity
*/
public void bindPreActivity(Activity currentActivity) {
Activity preActivity = ActivityStackUtil.getInstance().getPreActivity(currentActivity);
if (!preActivity.isDestroyed() && !preActivity.isFinishing()) {
//創建一個軟連接指向上個activity
mPreActivityRef = new WeakReference<Activity>(preActivity);
mPreDecorView = (ViewGroup) preActivity.getWindow().getDecorView();
mPreContentView = (ViewGroup) mPreDecorView.getChildAt(0);
mPreLayoutParams = mPreContentView.getLayoutParams();
mPreDecorView.removeView(mPreContentView);
addView(mPreContentView, 0, mPreLayoutParams);
this.isBindPreActivity = true;
}
}
/**
* 解除綁定,將preContentView歸還給上個activity
*/
public void unBindPreActivity() {
if (!isBindPreActivity) return;
if (mPreActivityRef == null || mPreActivityRef.get() == null) return;
if (mPreContentView != null && mPreDecorView != null) {
this.removeView(mPreContentView);
mPreDecorView.addView(mPreContentView, 0, mPreLayoutParams);
mPreContentView = null;
mPreActivityRef.clear();
mPreActivityRef = null;
}
this.isBindPreActivity = false;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mPreDecorView != null && mPreContentView == null) {
mPreDecorView.draw(canvas);
}
}
/**
* 前一個界面跟隨當前頁面滑動而滑動
*
* @param ratio
*/
public void onSlideChange(float ratio) {
this.setTranslationX(mHideWidth * (ratio - 1));
}
public boolean isBindPreActivity() {
return isBindPreActivity;
}
}
ActivityStackUtil
類用于獲取當前activity的前一個activity,需要在application中調用ActivityStackUtil getInstance().init(this);
package lu.basetool.util;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import java.util.Stack;
/**
* @Author: luqihua
* @Time: 2018/5/29
* @Description: ActivityUtil
*/
public class ActivityStackUtil implements Application.ActivityLifecycleCallbacks {
private Stack<Activity> mActivityStack = new Stack<>();
private static class Holder {
private static ActivityStackUtil sInstance = new ActivityStackUtil();
}
public static ActivityStackUtil getInstance() {
return Holder.sInstance;
}
public void init(Application application) {
application.registerActivityLifecycleCallbacks(this);
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
mActivityStack.push(activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
mActivityStack.remove(activity);
}
/**
* 獲取相對于當前activity前一個activity
*
* @param mCurrentActivity
* @return
*/
public Activity getPreActivity(Activity mCurrentActivity) {
Activity preActivity = null;
if (mActivityStack.size() > 1) {
int index = mActivityStack.lastIndexOf(mCurrentActivity);
if (index > 0) {
preActivity = mActivityStack.get(index - 1);
} else {
preActivity = mActivityStack.lastElement();
}
}
return preActivity;
}
}
callback的
onViewDragStateChanged
方法中處理滑動結束的操作,除了mCurrentActivity.finish()
結束當前activity之外,還處理滑動退出后閃屏的幾個點
//1.由于我們使用了滑動退出,因此不需要activity之間默認的切換動畫
mCurrentActivity.overridePendingTransition(0,0);
//2. 由于盡管取消了activity切換動畫,但是activity的消失可能任然會有閃一下的可能,于是我們干脆把當前的視圖隱藏
mCurrentActivity.getWindow().getDecorView().setVisibility(GONE);
removeView(mPreContentViewWrapper);
3.將當前mCurrentActivity的主題設置為透明
在style.xml中新建個主題,并在AndroidManifest.xml中設置給需要滑動返回的activity
<style name="AppTheme.transparent" parent="AppTheme">
<!-- Customize your theme here. -->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
使用 代碼git地址
編寫好的代碼有2個類,
ActivityStackUtil
和SlideBackLayout
//1.在application中初始化ActivityStackUtil
ActivityStackUtil.getInstance().init(this);
//2.給需要滑動返回的activity的(style.xml)theme添加如下兩行代碼
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
//3.在需要滑動返回的activity的onCreate()方法中調用
@Override
protected void onCreate(Bundle savedInstanceState) {
//在 super.onCreate(savedInstanceState);之前調用此方法
//第二個參數是一個滑動的監聽,一般情況下設置為null即可
new SlideBackLayout(this).attach2Activity(this, null);
super.onCreate(savedInstanceState);
}
可能出現的錯誤:
ViewDragHelper
在處理動態變化的子view的時候,可能會出現已經拖拽的子view自動回到原位,所謂動態變化的子view例如:輪播圖,動畫等,所以盡量確保在啟動可以滑動返回的activity之后,上一個activity的一些定時改變視圖(例如輪播圖定時翻頁)的效果暫停掉。
[異常]Only fullscreen opaque activities can request orientation;
出現該異常的話,將activity中的android:screenOrientation=""
屬性去掉。因為當activity的theme
中設置了透明之后,不允許再設置該屬性。