如需转载请注明博客出处: http://www.cnblogs.com/wondertwo/p/5525670.html
开源库AndroidSwipeLayout地址请戳: https://github.com/daimajia/AndroidSwipeLayout 开源库作者 @代码家
从上面的开源库目录可以看出,SwipeLayout是整个swipe效果实现的基石,我们就从源码着手,SwipeLayout.java文件的源码较为复杂庞大,有1600+行代码,所以阅读源码的正确的打开姿势是这样的:分清主线,只抓重点!所以我们重点去看SwipeLayout在TouchEvent事件处理方面是怎样实现的。本篇博客分三个部分来写:
- SurfaceView和BottomView绘制();
- TouchEvent事件传递(多层嵌套不破坏事件传递是怎样做到的?);
- Listener监听与回调(设置隐藏百分比、过渡动画效果);
第一部分 SurfaceView和BottomView绘制
SwipeLayout从本质上来说,可以看作一个自定义的View控件,继承自帧布局FrameLayout。通过我的前一篇博客,相信同学们已经了解了,它的布局其实是由上层的SurfaceViews和下层的BottomViews叠加在一起的。先来看构造方法,SwipeLayout有3个重载的构造方法,主要做一些初始化工作。我们需要关注一下mDragEdges这个成员变量,mDragEdges是一个记录滑动方向DragEdge的哈希表,一共记录Left,Right,Top,Bottom四个方向值。
有过自定义View经验的同学都很清楚要处理好两件事情:View绘制和事件处理。ViewGroup需要先遍历View树中所有子View,通过onMeasure()方法对其大小进行测量,调用onLayout()方法把它显示出来。按照这个思路去看源码,果然,SwipeLayout重写了onLayout()方法,由于Android原生android.view.View.OnLayoutChangeListener is added in API 11,为了兼容到api 8,作者做了OnLayout接口抽象,一共涉及到一个接口和四个成员方法,把这部分代码单独拿出来如下:
/**
* {@link android.view.View.OnLayoutChangeListener} added in API 11. I need
* to support it from API 8.
*/
public interface OnLayout {
void onLayout(SwipeLayout v);
}
private List<OnLayout> mOnLayoutListeners;
public void addOnLayoutListener(OnLayout l) {
if (mOnLayoutListeners == null) mOnLayoutListeners = new ArrayList<OnLayout>();
mOnLayoutListeners.add(l);
}
public void removeOnLayoutListener(OnLayout l) {
if (mOnLayoutListeners != null) mOnLayoutListeners.remove(l);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
updateBottomViews();
if (mOnLayoutListeners != null) for (int i = 0; i < mOnLayoutListeners.size(); i++) {
mOnLayoutListeners.get(i).onLayout(this);
}
}
private void updateBottomViews() {
View currentBottomView = getCurrentBottomView();
if (currentBottomView != null) {
if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
mDragDistance = currentBottomView.getMeasuredWidth() - dp2px(getCurrentOffset());
} else {
mDragDistance = currentBottomView.getMeasuredHeight() - dp2px(getCurrentOffset());
}
}
if (mShowMode == ShowMode.PullOut) {
layoutPullOut();
} else if (mShowMode == ShowMode.LayDown) {
layoutLayDown();
}
safeBottomView();
}
- addOnLayoutListener(OnLayout l)和removeOnLayoutListener(OnLayout l)这两个方法是添加和移除布局监听;
- 在重写的父类onLayout()方法中,我们看到先调用了updateBottomViews(),那么updateBottomViews()的作用字面上理解就是更新底层View,当CurrentBottomView非空,根据拖动方向(左右方向为一组,上下方向为一组)来计算拖动距离mDragDistance,接着根据两种不同的模式执行对应的方法把表层View移开,
- 在onLayout()方法的最后,调用safeBottomView()来拦截掉底层View的所有事件;safeBottomView()方法在第二部分会出现;
- 到这里我们很熟悉,遍历所有子View,回调onLayout(),将其布局显示在SwipeLayout这个父控件中;
第二部分 TouchEvent传递(多层嵌套不破坏事件传递是怎样做到的?)
其实自定义View说到底就是在View绘制和事件处理这两块大做文章!在第一部分分析了SwipeLayout的View绘制过程后,我们接着来看它的事件传递过程,哈哈,原作者提到它的特性:“可以嵌套在任何地方而不破坏触摸事件传递(这是最难的地方)”,我想说这也是这个开源库最精华的地方,以前自己也写过View控件,在触摸事件处理上遇到了各种问题,SwipeLayout在这方面确实值得借鉴。
所以很快的我们就会想到要去SwipeLayout中找三个很重要很重要的重写方法:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()!他们都只接收一个MotionEvent对象,分别负责事件分发、事件拦截、事件消耗。把这部分相关的代码单独拿出来如下:
private int mEventCounter = 0;
protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, int dx, int dy) {
DragEdge edge = getDragEdge();
boolean open = true;
if (edge == DragEdge.Left) {
if (dx < 0) open = false;
} else if (edge == DragEdge.Right) {
if (dx > 0) open = false;
} else if (edge == DragEdge.Top) {
if (dy < 0) open = false;
} else if (edge == DragEdge.Bottom) {
if (dy > 0) open = false;
}
dispatchSwipeEvent(surfaceLeft, surfaceTop, open);
}
protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, boolean open) {
safeBottomView();
Status status = getOpenStatus();
if (!mSwipeListeners.isEmpty()) {
mEventCounter++;
for (SwipeListener l : mSwipeListeners) {
if (mEventCounter == 1) {
if (open) {
l.onStartOpen(this);
} else {
l.onStartClose(this);
}
}
l.onUpdate(SwipeLayout.this, surfaceLeft - getPaddingLeft(), surfaceTop - getPaddingTop());
}
if (status == Status.Close) {
for (SwipeListener l : mSwipeListeners) {
l.onClose(SwipeLayout.this);
}
mEventCounter = 0;
}
if (status == Status.Open) {
View currentBottomView = getCurrentBottomView();
if (currentBottomView != null) {
currentBottomView.setEnabled(true);
}
for (SwipeListener l : mSwipeListeners) {
l.onOpen(SwipeLayout.this);
}
mEventCounter = 0;
}
}
}
/**
* prevent bottom view get any touch event. Especially in LayDown mode.
*/
private void safeBottomView() {
Status status = getOpenStatus();
List<View> bottoms = getBottomViews();
if (status == Status.Close) {
for (View bottom : bottoms) {
if (bottom != null && bottom.getVisibility() != INVISIBLE) {
bottom.setVisibility(INVISIBLE);
}
}
} else {
View currentBottomView = getCurrentBottomView();
if (currentBottomView != null && currentBottomView.getVisibility() != VISIBLE) {
currentBottomView.setVisibility(VISIBLE);
}
}
}
protected void dispatchRevealEvent(final int surfaceLeft, final int surfaceTop, final int surfaceRight, final int surfaceBottom) {
if (mRevealListeners.isEmpty()) return;
for (Map.Entry<View, ArrayList<OnRevealListener>> entry : mRevealListeners.entrySet()) {
View child = entry.getKey();
Rect rect = getRelativePosition(child);
if (isViewShowing(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop,
surfaceRight, surfaceBottom)) {
mShowEntirely.put(child, false);
int distance = 0;
float fraction = 0f;
if (getShowMode() == ShowMode.LayDown) {
switch (mCurrentDragEdge) {
case Left:
distance = rect.left - surfaceLeft;
fraction = distance / (float) child.getWidth();
break;
case Right:
distance = rect.right - surfaceRight;
fraction = distance / (float) child.getWidth();
break;
case Top:
distance = rect.top - surfaceTop;
fraction = distance / (float) child.getHeight();
break;
case Bottom:
distance = rect.bottom - surfaceBottom;
fraction = distance / (float) child.getHeight();
break;
}
} else if (getShowMode() == ShowMode.PullOut) {
switch (mCurrentDragEdge) {
case Left:
distance = rect.right - getPaddingLeft();
fraction = distance / (float) child.getWidth();
break;
case Right:
distance = rect.left - getWidth();
fraction = distance / (float) child.getWidth();
break;
case Top:
distance = rect.bottom - getPaddingTop();
fraction = distance / (float) child.getHeight();
break;
case Bottom:
distance = rect.top - getHeight();
fraction = distance / (float) child.getHeight();
break;
}
}
for (OnRevealListener l : entry.getValue()) {
l.onReveal(child, mCurrentDragEdge, Math.abs(fraction), distance);
if (Math.abs(fraction) == 1) {
mShowEntirely.put(child, true);
}
}
}
if (isViewTotallyFirstShowed(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop,
surfaceRight, surfaceBottom)) {
mShowEntirely.put(child, true);
for (OnRevealListener l : entry.getValue()) {
if (mCurrentDragEdge == DragEdge.Left
|| mCurrentDragEdge == DragEdge.Right)
l.onReveal(child, mCurrentDragEdge, 1, child.getWidth());
else
l.onReveal(child, mCurrentDragEdge, 1, child.getHeight());
}
}
}
}
- mEventCounter,顾名思义,事件计数器,只要SwipeListener监听到有事件触发,mEventCounter++;
- 第一个重载的dispatchSwipeEvent()方法,屏蔽掉了一些无效的触摸事件,也就是屏蔽掉了SwipeLayout(也就是ItemView)边界以外触发的所有事件,当事件无效,则把布尔型标记位open变量置为false,这个标记位直接控制着是否回调事件的监听;
- 第二个重载的dispatchSwipeEvent()方法,则是真正的事件分发逻辑所在。safeBottomView(),在第一部分出现过,拦截底层View的所有事件;然后通过getOpenStatus()拿到表层View的打开状态status,表层View有三种状态:Middle,Open,Close表示半打开状态,完全打开(此时只能看底层View),关闭(此时只能看到表层View)。
- 循环遍历mSwipeListeners,拿到每一个触摸事件,根据标记位open判断是否执行事件的监听回调;
- 根据表层View的打开状态status,通过回调方法执行关闭或者打开操作;并把mEventCounter重新置零。到此整个触摸事件的分发,似乎已经完成;但是你发现没有?上面的这些事件分发都是一些SwipeListener的方法回调,其具体逻辑是交由用户去实现的;那么问题来了,我们的ItemView总得响应用户的触摸事件,进行相应的UI更新吧,比如表层View的Open和Close,你会发现这些事件是不能交给用户去处理的,所以这里涉及到了另外一个很重要的接口OnRevealListener(关于接口的监听回调,会在第三部分细讲),负责重绘ItenView以更新用户界面显示的UI。这是整个开源库的精髓所在;
- 此时看到最后还有一个dispatchRevealEvent(),那么这个方法是干嘛的呢?dispatchRevealEvent()就负责分发
UI更新相关事件。逻辑也很简单,通过getShowMode() == ShowMode.LayDown | ShowMode.PullOut判断用户的滑动操作模式是LayDown模式还是PullOut模式,根据不同的模式计算两个不同的参数distance和fraction,这两个参数就是直接控制着表层View的滑动行为; - 最后,遍历所有的OnRevealListener接口对象,回调onReveal()更新用户界面UI。
上面说完了SwipeLayout的事件分发,那么SwipeLayout事件是如何拦截以及消耗的呢?把相关的代码单独拿出来如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!isSwipeEnabled()) {
return false;
}
if (mClickToClose && getOpenStatus() == Status.Open && isTouchOnSurface(ev)) {
return true;
}
for (SwipeDenier denier : mSwipeDeniers) {
if (denier != null && denier.shouldDenySwipe(ev)) {
return false;
}
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDragHelper.processTouchEvent(ev);
mIsBeingDragged = false;
sX = ev.getRawX();
sY = ev.getRawY();
//if the swipe is in middle state(scrolling), should intercept the touch
if (getOpenStatus() == Status.Middle) {
mIsBeingDragged = true;
}
break;
case MotionEvent.ACTION_MOVE:
boolean beforeCheck = mIsBeingDragged;
checkCanDrag(ev);
if (mIsBeingDragged) {
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
if (!beforeCheck && mIsBeingDragged) {
//let children has one chance to catch the touch, and request the swipe not intercept
//useful when swipeLayout wrap a swipeLayout or other gestural layout
return false;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mDragHelper.processTouchEvent(ev);
break;
default://handle other action, such as ACTION_POINTER_DOWN/UP
mDragHelper.processTouchEvent(ev);
}
return mIsBeingDragged;
}
private float sX = -1, sY = -1;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isSwipeEnabled()) return super.onTouchEvent(event);
int action = event.getActionMasked();
gestureDetector.onTouchEvent(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
mDragHelper.processTouchEvent(event);
sX = event.getRawX();
sY = event.getRawY();
case MotionEvent.ACTION_MOVE: {
//the drag state and the direction are already judged at onInterceptTouchEvent
checkCanDrag(event);
if (mIsBeingDragged) {
getParent().requestDisallowInterceptTouchEvent(true);
mDragHelper.processTouchEvent(event);
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mDragHelper.processTouchEvent(event);
break;
default://handle other action, such as ACTION_POINTER_DOWN/UP
mDragHelper.processTouchEvent(event);
}
return super.onTouchEvent(event) || mIsBeingDragged || action == MotionEvent.ACTION_DOWN;
}
上面这段代码很简单,重写父类的onInterceptTouchEvent()方法和onTouchEvent(),和我们预想的一模一样。那么什么情况下,SwipeLayout拦截掉了子View的触摸事件?当SurfaceView处于打开状态,想要点击关闭,却点击在SurfaceView上时,SwipeLayout会拦截掉SurfaceView的触摸事件。至于事件拦截和处理的具体逻辑,就在上面这段代码中了。
第三部分 Listener监听与回调(设置隐藏百分比、过渡动画效果)
第三部分我们来分析Listener监听与回调,包括怎样设置子View显示隐藏百分比,以及更新UI的过渡动画效果。前文提到了两个监听接口OnLayout和OnRevealListener,前者的毁掉方法,主要响应一些用户的自定义操作,比如手机QQ聊天列表的ItenView左滑呼出删除、置顶、标为未读等操作功能;后者在第二部分已经提及,主要处理更新UI等功能,而这些功能已经在SwpieLayout中封装好了,不需要用户关心;当然,作为一个小巧但并不影响它强大存在的开源库,用户的可定制性还是非常高的,比如可以实现一些ItemView呼出或者关闭的动画。
关于SwipeLayout开源库的使用方法,点击阅读我的上一篇博客:开源库AndroidSwipeLayout分析(一),炫酷ItemView滑动呼出效果!