如需轉載請注明博客出處: 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滑動呼出效果 !