前言
activity的滑动返回也是个常用的功能,网上有很多介绍怎么去实现的库,我测过其中几个,包括star比较多的
- SwipeBackLayout
- BGASwipeBackLayout-Android
但是这两个库各有各的缺点,
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 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(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 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
使用 代码git地址
编写好的代码有2个类,
ActivityStackUtil
和SlideBackLayout
//1.在application中初始化ActivityStackUtil
ActivityStackUtil.getInstance().init(this);
//2.给需要滑动返回的activity的(style.xml)theme添加如下两行代码
- true
- @android:color/transparent
//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
中设置了透明之后,不允许再设置该属性。