这个库本身并没有很难, 但是它在交互方面的思想是超前的, 虽然现在我们对滑动返回已经习惯了, 但是在当时还是很新鲜的交互方式.
如果要我实现滑动返回, 我第一个看的一定是DrawerLayout的实现, 它用了ViewDragHelper.
接下来我们结合源码解析SwipeBackLayout是如何做到让我们能使用滑动返回的手势的.
用过的都知道, 想让一个activity具有滑动返回的手势, 需要让它继承SwipeBackActivity.
public class SwipeBackActivity extends FragmentActivity implements SwipeBackActivityBase
SwipeBackActivity继承FragmentActivity, 其实就相当于在正常的继承关系之间插了它自己进去.
至于SwipeBackActivityBase则是一个定义好的接口, 在SwipeBackActivity中实现
public interface SwipeBackActivityBase {
/**
* @return the SwipeBackLayout associated with this activity.
*/
public abstract SwipeBackLayout getSwipeBackLayout();
public abstract void setSwipeBackEnable(boolean enable);
/**
* Scroll out contentView and finish the activity
*/
public abstract void scrollToFinishActivity();
}
这个些个方法在SwipeBackActivity里面都重写了, 都很简单, 没必要说, 我们看看它重写的其他三个方法
private SwipeBackActivityHelper mHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHelper = new SwipeBackActivityHelper(this);
mHelper.onActivityCreate();
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mHelper.onPostCreate();
}
@Override
public View findViewById(int id) {
View v = super.findViewById(id);
if (v == null && mHelper != null)
return mHelper.findViewById(id);
return v;
}
findViewById
就是先调用super.findViewById
, 没找到的情况下, 再到mHelper
中找. 至于mHelper
是什么, 等会儿再说, 先看其他两个方法.
其他两个方法相当于在activity的两个生命周期里面执行了mHelper
的两个函数, 那么mHelper
到底是什么呢, 我们看看SwipeBackActivityHelper
首先看它的构造函数和成员变量
private Activity mActivity;
private SwipeBackLayout mSwipeBackLayout;
public SwipeBackActivityHelper(Activity activity) {
mActivity = activity;
}
原来构造它的时候, 它立刻将构造它的activity的引用保存了起啦, 我们怀疑和它的两个类似生命周期的方法有关.
先看onActivityCreate
public void onActivityCreate() {
mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
mActivity.getWindow().getDecorView().setBackgroundDrawable(null);
mSwipeBackLayout = (SwipeBackLayout) LayoutInflater.from(mActivity).inflate(
me.imid.swipebacklayout.lib.R.layout.swipeback_layout, null);
mSwipeBackLayout.addSwipeListener(new SwipeBackLayout.SwipeListener() {
@Override
public void onScrollStateChange(int state, float scrollPercent) {
if (state == SwipeBackLayout.STATE_IDLE && scrollPercent == 0) {
Utils.convertActivityFromTranslucent(mActivity);
}
}
@Override
public void onEdgeTouch(int edgeFlag) {
Utils.convertActivityToTranslucent(mActivity);
}
@Override
public void onScrollOverThreshold() {
}
});
}
这个方法开头就把初始化它的activity的背景设为透明, 然后初始化了一个SwipeBackLayout
, 还和一个布局文件有关, 最后给它添加了一个监听器, 这个监听器无关紧要, 只是用来控制acitivty的透明的, 具体的代码就不说了.
我们可以看一下初始化SwipeBackLayout
用到的布局文件
<?xml version="1.0" encoding="utf-8"?>
<me.imid.swipebacklayout.lib.SwipeBackLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe"
android:layout_width="match_parent"
android:layout_height="match_parent" />
其实什么都没有, 看来刚才只是为了创建一个SwipeBackLayout
对象, 这个对象应该是ViewGroup
的子类.
这个方法本质上并没有什么异常, 改改activity的背景, 并不会有滑动返回的效果, 所以重点应该在onPostCreate
上, 它的代码也很简单
public void onPostCreate() {
mSwipeBackLayout.attachToActivity(mActivity);
Utils.convertActivityFromTranslucent(mActivity);
}
attachToActivity
是关键, 我们等会儿再分析, 先看完这个类.
SwipeBackActivityHelper
还有一个方法
public View findViewById(int id) {
if (mSwipeBackLayout != null) {
return mSwipeBackLayout.findViewById(id);
}
return null;
}
刚才在SwipeBackActivity
中重写的findViewById
会调用这个方法, 这个方法本质上是调用SwipeBackLayout.findViewById
, 现在比较奇怪的是SwipeBackLayout
里面是没有任何东西的, 何必要调用它的findViewById
呢? 所以SwipeBackLayout.attachToActivity
一定是一个非常关键的方法, 没有它就不可能有滑动返回的效果.
public class SwipeBackLayout extends FrameLayout
我们的猜测没有错, SwipeBackLayout
的确是ViewGroup
的子类, 我们暂时不看这个类的初始化, 先看attachToActivity
方法到底写了什么.
public void attachToActivity(Activity activity) {
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();
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
decorChild.setBackgroundResource(background);
decor.removeView(decorChild);
addView(decorChild);
setContentView(decorChild);
decor.addView(this);
}
我们看到这个方法中除了对背景资源的操作, 就是一些奇怪的View
操作了. 先是将activity的DecorView和DecorView的第一个child拿到, 然后将它的child从DecorView中删除, 将child添加到SwipeBackLayout
自身里面, 最后将SwipeBackLayout
添加回DecorView中, 这样就完成了一次狸猫换太子, 我们成功在DecorView和它的第一个child之间插入了我们自己控制的布局. 当然实际上DecorView只有一个child.
那么现在我们的SwipeBackLayout
成功打入了敌人内部, 而且扼守咽喉, SwipeBackLayout
的行为就是关键了.
我们先看它的构造函数
public SwipeBackLayout(Context context) {
this(context, null);
}
public SwipeBackLayout(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.SwipeBackLayoutStyle);
}
public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
mDragHelper = ViewDragHelper.create(this, new ViewDragCallback());
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle,
R.style.SwipeBackLayout);
int edgeSize = a.getDimensionPixelSize(R.styleable.SwipeBackLayout_edge_size, -1);
if (edgeSize > 0)
setEdgeSize(edgeSize);
int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)];
setEdgeTrackingEnabled(mode);
int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left,
R.drawable.shadow_left);
int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right,
R.drawable.shadow_right);
int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom,
R.drawable.shadow_bottom);
setShadow(shadowLeft, EDGE_LEFT);
setShadow(shadowRight, EDGE_RIGHT);
setShadow(shadowBottom, EDGE_BOTTOM);
a.recycle();
final float density = getResources().getDisplayMetrics().density;
final float minVel = MIN_FLING_VELOCITY * density;
mDragHelper.setMinVelocity(minVel);
mDragHelper.setMaxVelocity(minVel * 2f);
}
这些构造函数无非就是给一些参数赋默认值, 有一些参数是从style中读取的, 也就是说我们可以通过修改style来控制初始化的参数, 这些style分别定义在attrs.xml和styles.xml中
values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SwipeBackLayout">
<attr name="edge_size" format="dimension"/>
<attr name="edge_flag">
<enum name="left" value="0" />
<enum name="right" value="1" />
<enum name="bottom" value="2" />
<enum name="all" value="3" />
</attr>
<attr name="shadow_left" format="reference"/>
<attr name="shadow_right" format="reference"/>
<attr name="shadow_bottom" format="reference"/>
</declare-styleable>
<attr name="SwipeBackLayoutStyle" format="reference"/>
</resources>
values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="SwipeBackLayout">
<item name="edge_size">50dip</item>
<item name="shadow_left">@drawable/shadow_left</item>
<item name="shadow_right">@drawable/shadow_right</item>
<item name="shadow_bottom">@drawable/shadow_bottom</item>
</style>
</resources>
就是一些默认值的定义, 如果不懂可以参看我之前写的自定义控件和属性的文章.
初始化过程中有一个叫
mDragHelper = ViewDragHelper.create(this, new ViewDragCallback());
这个就是滑动返回的秘密了, 和我的想法一样, 就是使用ViewDragHelper来创造滑动整个activity的效果.
当然, 只是调用ViewDragHelper.create
是不够的, 还需要下面两个方法, 这样ViewDragHelper
才有能力对我们的事件做出反应.
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!mEnable) {
return false;
}
try {
return mDragHelper.shouldInterceptTouchEvent(event);
} catch (ArrayIndexOutOfBoundsException e) {
// FIXME: handle exception
// issues #9
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mEnable) {
return false;
}
mDragHelper.processTouchEvent(event);
return true;
}
其中的ArrayIndexOutOfBoundsException
是某种情况下会触发的bug, 这样写可以忽略掉那个bug造成的崩溃.
真正指导ViewDragHelper
的是初始化时传入的ViewDragCallback
, 代码如下
private class ViewDragCallback extends ViewDragHelper.Callback {
private boolean mIsScrollOverValid;
@Override
public boolean tryCaptureView(View view, int i) {
boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i);
if (ret) {
if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) {
mTrackingEdge = EDGE_LEFT;
} else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) {
mTrackingEdge = EDGE_RIGHT;
} else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) {
mTrackingEdge = EDGE_BOTTOM;
}
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onEdgeTouch(mTrackingEdge);
}
}
mIsScrollOverValid = true;
}
return ret;
}
@Override
public int getViewHorizontalDragRange(View child) {
return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
}
@Override
public int getViewVerticalDragRange(View child) {
return mEdgeFlag & EDGE_BOTTOM;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if ((mTrackingEdge & EDGE_LEFT) != 0) {
mScrollPercent = Math.abs((float) left
/ (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
mScrollPercent = Math.abs((float) left
/ (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
mScrollPercent = Math.abs((float) top
/ (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
}
mContentLeft = left;
mContentTop = top;
invalidate();
if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) {
mIsScrollOverValid = true;
}
if (mListeners != null && !mListeners.isEmpty()
&& mDragHelper.getViewDragState() == STATE_DRAGGING
&& mScrollPercent >= mScrollThreshold && mIsScrollOverValid) {
mIsScrollOverValid = false;
for (SwipeListener listener : mListeners) {
listener.onScrollOverThreshold();
}
}
if (mScrollPercent >= 1) {
if (!mActivity.isFinishing())
mActivity.finish();
}
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final int childWidth = releasedChild.getWidth();
final int childHeight = releasedChild.getHeight();
int left = 0, top = 0;
if ((mTrackingEdge & EDGE_LEFT) != 0) {
left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
+ mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
+ mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
+ mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
}
mDragHelper.settleCapturedViewAt(left, top);
invalidate();
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
int ret = 0;
if ((mTrackingEdge & EDGE_LEFT) != 0) {
ret = Math.min(child.getWidth(), Math.max(left, 0));
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
ret = Math.min(0, Math.max(left, -child.getWidth()));
}
return ret;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int ret = 0;
if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
ret = Math.min(0, Math.max(top, -child.getHeight()));
}
return ret;
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onScrollStateChange(state, mScrollPercent);
}
}
}
}
tryCaptureView
的返回值判断某个View是否可以滑动, 本来是用来判断某个子View是否可以滑动的, 这里这么写其实就是只要发现触摸了activity的边缘, 就会触发滑动.
clampViewPositionHorizontal
和clampViewPositionVertical
是分别计算水平和垂直方向上滑动坐标的方法, 默认返回0, 代表水平或垂直方向上无法滑动.
onViewPositionChanged
则是在被滑动的View的位置改变时被回调, 这个方法中主要计算各种状态, 包括滑动的百分比等等, 如果滑动百分比大于1的话, 也就是说我们已经把整个activity滑到屏幕外去了, 那么就finish这个activity. 由于SwipeBackLayout
本身是这个activity的一部分, 所以也接受activity的管理.
onViewReleased
是松手后的处理, 判断activity是应该回弹恢复原状还是继续滑出屏幕, 滑出屏幕会导致被finish.
松手后继续滑动的秘密在于SwipeBackLayout
重写的下面的方法
@Override
public void computeScroll() {
mScrimOpacity = 1 - mScrollPercent;
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
其他的方法都无关紧要, 主要是绘图, 比如显示边界阴影, 遮罩的效果等等, 所以就不介绍了.