編輯:關於Android編程
很久沒有更新博客了,忙裡偷閒產出一篇。寫這片文章主要是去年項目中的一個需求,當時三下五除二的將其實現了,但是源碼的閱讀卻一直扔在那遲遲沒有時間理會,現在揀起來看看吧,否則心裡一直不踏實。
關於啥是ViewDragHelper,這裡不再解釋,官方下面這個解釋已經很牛逼了,如下:
/**
* ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
* of useful operations and state tracking for allowing a user to drag and reposition
* views within their parent ViewGroup.
*/
下面我們先從一個例子說起,然後再進行源碼淺析。duang、duang、duang!!!
在開始源碼淺析之前有必要先通過一個實例來認識下他的功效,下面呈上的效果圖就是他的功效之一:
Android-Blog-Source/tree/master/ViewDragHelper-Demo">該效果實例源碼點我下載
這個效果圖其實和去年那個項目中的需求十分接近了,為了保密期間才修改為此;當時的需求是這樣的:
能夠上下平滑收縮; 能夠上下手勢收縮; 能夠上下跟隨手指手勢收縮一定比例; 上下兩層裡下層是一個ScrollView,上層是一個ListView的業務邏輯交互;當時拿到這個需求時我的內心是崩潰的(哈哈,當然不會崩潰,網絡用語而已);起初構思時我心裡萌生了三套方案,是用屬性動畫來做呢,還是滑動來做啊,還是用ViewDragHelper來做啊,記得當時思考了一會決定用ViewDragHelper來做,因為他實現起來比較簡單,其他的比較麻煩。哈哈,下面給出ViewDragHelper的實現代碼:
/**
* 垂直DrawerLayout
* 實現步驟:
* 1.使用靜態方法來構ViewDragHelper,需要傳入一個ViewDragHelper.Callback對象.
* 2.重寫onInterceptTouchEvent和onTouchEvent回調ViewDragHelper中對應方法.
* 3.在ViewDragHelper.Callback中對視圖做操作.
* 4.使用ViewDragHelper.smoothSlideViewTo()方法平滑滾動.
* 5.自定義一些交互邏輯的自由實現.
*/
public class VerticalDrawerLayout extends ViewGroup {
private ViewDragHelper mTopViewDragHelper;
private View mContentView;
private View mDrawerView;
private int mCurTop = 0;
private boolean mIsOpen = true;
public VerticalDrawerLayout(Context context) {
super(context);
init();
}
public VerticalDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public VerticalDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//Step1:使用靜態方法構造ViewDragHelper,其中需要傳入一個ViewDragHelper.Callback回調對象.
mTopViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack());
mTopViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP);
}
//Step2:定義一個ViewDragHelper.Callback回調實現類
private class ViewDragHelperCallBack extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//返回ture則表示可以捕獲該view,手指摸上一瞬間調運
return child == mDrawerView;
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
//setEdgeTrackingEnabled設置的邊界滑動時觸發
//captureChildView是為了讓tryCaptureView返回false依舊生效
mTopViewDragHelper.captureChildView(mDrawerView, pointerId);
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//手指觸摸移動時實時回調, left表示要到的x位置
return super.clampViewPositionHorizontal(child, left, dx);
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//手指觸摸移動時實時回調, top表示要到的y位置
//保證手指挪動時只能向上,向下最大到0
return Math.max(Math.min(top, 0), -mDrawerView.getHeight());
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//手指釋放時回調
float movePrecent = (releasedChild.getHeight() + releasedChild.getTop()) / (float) releasedChild.getHeight();
int finalTop = (xvel >= 0 && movePrecent > 0.5f) ? 0 : -releasedChild.getHeight();
mTopViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), finalTop);
invalidate();
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//mDrawerView完全挪出屏幕則防止過度繪制
mDrawerView.setVisibility((changedView.getHeight()+top == 0)? View.GONE : View.VISIBLE);
mCurTop = top;
requestLayout();
}
@Override
public int getViewVerticalDragRange(View child) {
if (mDrawerView == null) return 0;
return (mDrawerView == child) ? mDrawerView.getHeight() : 0;
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
if (state == ViewDragHelper.STATE_IDLE) {
mIsOpen = (mDrawerView.getTop() == 0);
}
}
}
@Override
public void computeScroll() {
if (mTopViewDragHelper.continueSettling(true)) {
invalidate();
}
}
public void closeDrawer() {
if (mIsOpen) {
mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), -mDrawerView.getHeight());
invalidate();
}
}
public void openDrawer() {
if (!mIsOpen) {
mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), 0);
invalidate();
}
}
public boolean isDrawerOpened() {
return mIsOpen;
}
//Step3:重寫onInterceptTouchEvent回調ViewDragHelper中對應的方法.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mTopViewDragHelper.shouldInterceptTouchEvent(ev);
}
//Step3:重寫onTouchEvent回調ViewDragHelper中對應的方法.
@Override
public boolean onTouchEvent(MotionEvent event) {
mTopViewDragHelper.processTouchEvent(event);
return true;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
mContentView = getChildAt(0);
mDrawerView = getChildAt(1);
MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
measureWidth- (params.leftMargin + params.rightMargin), MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
measureHeight - (params.topMargin + params.bottomMargin), MeasureSpec.EXACTLY);
mContentView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
mDrawerView.measure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();
mContentView.layout(params.leftMargin, params.topMargin,
mContentView.getMeasuredWidth() + params.leftMargin,
mContentView.getMeasuredHeight() + params.topMargin);
params = (MarginLayoutParams) mDrawerView.getLayoutParams();
mDrawerView.layout(params.leftMargin, mCurTop + params.topMargin,
mDrawerView.getMeasuredWidth() + params.leftMargin,
mCurTop + mDrawerView.getMeasuredHeight() + params.topMargin);
}
}
}
怎麼樣,簡單吧。效果也有了,代碼也有了,ViewDragHelper也體驗了,接下來就該苦逼的看源碼了。
上面的例子中我們可以知道,使用ViewDragHelper的第一步就是通過他提供的靜態工廠方法create獲取實例,因為ViewDragHelper的構造方法是私有的。既然這樣那我們先看下這些靜態工廠方法,如下:
public class ViewDragHelper {
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
}
可以看見,三個參數的create方法實質調運的還是兩個參數的create。其中forParent一般是我們自定義的ViewGroup,cb是控制子View相關狀態的回調抽象類實現對象,sensitivity是用來調節mTouchSlop的,至於mTouchSlop是啥以及sensitivity的作用下面會有解釋。接著可以發現兩個參數的create實質是調運了ViewDragHelper的構造函數,那我們就來分析一下這個構造函數,如下源碼:
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
......
//對參數進行賦值
mParentView = forParent;
mCallback = cb;
//通過ViewConfiguration等將dp轉px得到mEdgeSize
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
//通過ViewConfiguration獲取TouchSlop,默認為8
mTouchSlop = vc.getScaledTouchSlop();
//獲得允許執行一個fling手勢動作的最大速度值
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
//獲得允許執行一個fling手勢動作的最小速度值
mMinVelocity = vc.getScaledMinimumFlingVelocity();
//通過兼容包的ScrollerCompat實例化Scroller,動畫插值器為sInterpolator
mScroller = ScrollerCompat.create(context, sInterpolator);
}
可以看見,構造函數其實沒有做啥特別的事情,主要就是一些參數的實例化,最主要的就是實例化了一個Scroller的內部成員,而且還在ViewDragHelper中重寫了插值器,如下:
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
關於動畫插值器這裡不再說了,之前博文有講過。我們還是把視線回到上面實例部分,可以看見,在獲取ViewDragHelper實例之後我們接著重寫了ViewGroup的onInterceptTouchEvent和onTouchEvent方法,在其中觸發了ViewDragHelper的shouldInterceptTouchEvent和processTouchEvent方法。所以下面我們就來分析這兩個方法,首先我們看下shouldInterceptTouchEvent方法,如下:
//這玩意返回值的作用在前面博客中有分析,我們先來看下ACTION_DOWN事件
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
//每次ACTION_DOWN都會調用cancel(),該方法中mVelocityTracker被清空,故mVelocityTracker記錄的是本次ACTION_DOWN到ACTION_UP的觸摸信息
cancel();
}
//獲取VelocityTracker實例,記錄下各個觸摸點信息用來計算本次滑動速率等
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
//Step 1
saveInitialMotion(x, y, pointerId);
//Step 2
final View toCapture = findTopChildUnder((int) x, (int) y);
//Step 3
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
//Step 4
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
//暫時忽略
......
}
//Step 5
return mDragState == STATE_DRAGGING;
}
可以看見,上面代碼我們只列出了shouldInterceptTouchEvent關於ACTION_DOWN部分的代碼,在注釋裡我將他分為了5步來敘述。我們先來看當ACTION_DOWN觸發時Step 1的代碼,他通過saveInitialMotion(x, y, pointerId)保存了事件的初始信息,如下是saveInitialMotion方法代碼:
private void saveInitialMotion(float x, float y, int pointerId) {
ensureMotionHistorySizeForId(pointerId);
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
//getEdgesTouched就是通過mEdgeSize去判斷觸摸邊沿方向是否OK
mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
mPointersDown |= 1 << pointerId;
}
接著目光再回到shouldInterceptTouchEvent方法的Step 2,可以發現他嘗試通過findTopChildUnder()方法來獲取當前觸摸點下最頂層的子View,我們可以看下這個方法的源碼,如下:
public View findTopChildUnder(int x, int y) {
//獲取mParentView中子View個數
final int childCount = mParentView.getChildCount();
//倒序遍歷整個子View,因為最上面的子View最後插入
for (int i = childCount - 1; i >= 0; i--) {
//遍歷拿到最靠上且獲得觸摸焦點的那個子View
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
//判斷當前DOWN的觸摸點是否在該子View范圍,也就是說是不是摸上了該子View
if (x >= child.getLeft() && x < child.getRight() &&
y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}
這個方法你不覺的奇怪麼?怎麼遍歷時getChildAt傳入的不是index,而是mCallback.getOrderedChildIndex(i)啊,我勒個去,mCallback不就是我們創建ViewDragHelper實例時傳入的CallBack對象麼?我們還是跳過去看下getOrderedChildIndex()方法吧,如下:
public static abstract class Callback {
......
public int getOrderedChildIndex(int index) {
return index;
}
......
}
貓了個咪的,這玩意默認啥也沒干啊,看樣子是某些情況下給我們用來重寫的,啥情況?原來在上面findTopChildUnder()方法中返回當前被觸摸的View時會有一種坑爹的情況出現,那就是如果在mParentView的同一個位置有多個子View是重疊的,此時又想讓重疊的View中下面指定的那個被選中(默認for循環是倒序額)時getOrderedChildIndex()方法的默認實現就搞不定了,所以就需要我們自己去實現Callback裡的getOrderedChildIndex()方法來改變查找子View的順序。譬如:
public int getOrderedChildIndex(int index) {
//實現重疊View時讓下面的View獲得選中
int topIndex = mParentView.indexOfChild(your_top_view);
int BottomSelectedIndex = mParentView.indexOfChild(blow_your_top_view_selected);
return ((index == topIndex) ? indexBottom : index);
}
好了,扯遠了,我們還是把目光回到shouldInterceptTouchEvent方法的Step 3,可以發現這裡有一個判斷,因為第一次觸摸屏幕mCapturedView默認為null,所以一開始不會執行這個判斷裡的代碼,同時因為mDragState第一次也不處於STATE_SETTLING狀態,所以不執行,等執行時再分析。我們繼續往下看Step 4,可以發現這裡首先拿了saveInitialMotion方法賦值的結果,然後判斷設置的邊沿方向進行Callback的onEdgeTouched()方法回調。到這裡boolean shouldInterceptTouchEvent()方法的第一次觸摸按下ACTION_DOWN所干的事就完了,接著Step 5時直接return了mDragState == STATE_DRAGGING;,因為上面說了,在ACTION_DOWN時mDragState還是STATE_IDLE狀態,所以這裡返回了false。
至此第一次手指觸摸mParentView上時shouldInterceptTouchEvent的ACTION_DOWN流程就結束了,接著我們就是依據這個返回值的情況進行分析(具體參見之前博文關於Android觸摸事件傳遞的分析)。這裡返回false就表示mParentView沒有攔截這次事件,所以接下來會在mParentView中觸發每個子View的boolean dispatchTouchEvent()方法,這時依據Android觸摸事件處理機制又分為了兩大類情況來處理,一類就是子View消費了這個ACTION_DOWN,一類是沒有消費的情況,而這些情況下又分很多中情況,譬如子View消費了本次ACTION_DOWN,mParentView的onTouchEvent()就不會收到ACTION_DOWN了(即ViewDragHelper的processTouchEvent()方法也就收不到ACTION_DOWN了);這時候又有很多情況,譬如當前子View如果調運了requestDisallowInterceptTouchEvent(true),則ACTION_MOVE等來時mParentView的onInterceptTouchEvent()方法就不會被回調(即ViewDragHelper的相關方法也就沒意義了);當前子View沒有調用requestDisallowInterceptTouchEvent(true),則ACTION_MOVE等來時mParentView的onInterceptTouchEvent()方法還會被執行,此時若onInterceptTouchEvent()方法返回true,則mParentView的onTouchEvent()就會被調運(即ViewDragHelper的processTouchEvent()會被執行)。
額額額,觸摸事件傳遞本來就很復雜,這裡情況又很多,所以我們還是不分情況來說了,淺析源碼我們牽一條主線走就行,其它的用到時再分析即可。
所以我們來看下子View沒有消費這次ACTION_DOWN事件(即子View的dispatchTouchEvent()方法返回false)的流程。此時mParentView的dispatchTouchEvent()方法會調用自己的super.dispatchTouchEvent()方法(即View的dispatchTouchEvent()),然後super會調用mParentView的onTouchEvent()方法(即調用ViewDragHelper的processTouchEvent()方法)。這時onTouchEvent()方法需要返回true(只用在ACTION_DOWN時返回true,否則onTouchEvent()方法無法接收接下來的ACTION_MOVE等事件),當onTouchEvent()返回true以後ACTION_MOVE、ACTION_UP等事件再來時就不會再執行mParentView的onInterceptTouchEvent()了。
那就牽著主線走吧,我們可以看下processTouchEvent()方法的ACTION_DOWN部份源碼,如下:
public void processTouchEvent(MotionEvent ev) {
//和shouldInterceptTouchEvent相似,省略
......
switch (action) {
case MotionEvent.ACTION_DOWN: {
//和shouldInterceptTouchEvent相似,省略解釋
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
final View toCapture = findTopChildUnder((int) x, (int) y);
saveInitialMotion(x, y, pointerId);
//Step 1 重點!!!!
tryCaptureViewForDrag(toCapture, pointerId);
//和shouldInterceptTouchEvent相似,省略解釋
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
//省略其他ACTION
......
}
}
可以看見,該方法裡最核心的東西估計就是tryCaptureViewForDrag()方法了,下面我們看下這個方法,如下:
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
//正在拽就不管了
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
//調用了Callback的tryCaptureView()方法,傳遞觸摸到的View和觸摸點編號
//Callback的tryCaptureView()決定是否能夠拖動當前觸摸的View
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
//重點
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
可以看見,通過Callback的tryCaptureView()重寫設置是否可以挪動該View,若可以挪動(返回true)則又調運了captureChildView()方法,繼續看下captureChildView()方法源碼:
public void captureChildView(View childView, int activePointerId) {
......
//暫存被捕獲的這個View的相關信息及觸摸信息
mCapturedView = childView;
mActivePointerId = activePointerId;
//通過Callback的onViewCaptured()方法回調當前View被捕獲了
mCallback.onViewCaptured(childView, activePointerId);
//設置當前被捕獲的子View狀態為STATE_DRAGGING
//裡面會通過mCallback.onViewDragStateChanged(state)回調告知狀態
setDragState(STATE_DRAGGING);
}
到此一次mParentView自己消費事件,子View無攔截ACTION_DOWN的事件處理就徹底結束了。接著就是主流程的ACTION_MOVE事件了,這玩意由於mParentView的onTouchEvent消費了事件且沒進行攔截ACTION_DOWN,所以一旦觸發時就直接走進了processTouchEvent()方法裡,下面是ACTION_MOVE代碼:
public void processTouchEvent(MotionEvent ev) {
......
switch (action) {
......
case MotionEvent.ACTION_MOVE: {
//分兩種情況,依賴上一個ACTION_DOWN事件
if (mDragState == STATE_DRAGGING) {
//ACTION_DOWN時CallBack的tryCaptureView()返回true時對mDragState賦值了STATE_DRAGGING,故此流程
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
//Step 1 重點!!!!!!
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
//Step 2 重點!!!!!!
saveLastMotion(ev);
} else {
//ACTION_DOWN時CallBack的tryCaptureView()返回false時對mDragState沒進行賦值,故此流程
// Check to see if any pointer is now over a draggable view.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
//Step 3 重點!!!!!!
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
//Step 4 重點!!!!!!
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
......
}
}
可以看見,當ACTION_MOVE事件多次觸發時該段代碼會依據我們重寫CallBack的代碼分為可以托拽當前View和不能托拽兩種情況。
我們先來看下不能托拽的情況,這種相對比較簡單,可以看到上面代碼的Step 3部分,reportNewEdgeDrags()方法是我們的重點,如下:
//在托拽時該方法會被多次調運
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
......//四個方向,省略三個
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
//該方法只會被調運一次,checkNewEdgeDrag方法中有處理
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
可以發現,當我們在ACTION_DOWN觸發時重寫CallBack的tryCaptureView()方法返回false(當前View不能托拽)且是邊沿觸摸時移動時首先會回調Callback的onEdgeDragStarted()方法通知自定義ViewGroup開始邊沿托拽。接著我們把目光投向Step 4部分,if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId))
判斷是我們關注的核心,toCapture其實就是當前捕獲的View(這個View在邊沿模式時一般摸不到,所以其實拿到的不是想要的childView,所以一般我們會在回調onEdgeDragStarted()方法中重寫手動調用captureChildView()方法,傳入我們摸不到的View,這樣就相當於繞過tryCaptureView將狀態設置為STATE_DRAGGING了),下面我們先看下checkTouchSlop()方法,如下:
//檢查手指移動的距離有沒有超過觸發處理移動事件的最短距離mTouchSlop
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
//如果想讓某個View滑動,就要返回大於0,否則processTouchEvent()的ACTION_MOVE就不會調用tryCaptureViewForDrag()來捕獲當前觸摸的View
final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
if (checkHorizontal && checkVertical) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
} else if (checkHorizontal) {
return Math.abs(dx) > mTouchSlop;
} else if (checkVertical) {
return Math.abs(dy) > mTouchSlop;
}
return false;
}
到此ACTION_MOVE不能托拽的情況就分析完畢了,我們再來看下可以托拽的情況,請看上面processTouchEvent()代碼的Step 1,2部分,重點在於dragTo()方法,當我們正常捕獲到View時ACTION_MOVE就不停的調用dragTo()對mCaptureView進行拖動,源碼如下:
//left、top為mCapturedView.getLeft()+dx、mCapturedView.getTop()+dy,即期望目標坐標
//dx、dy為前後兩次ACTION_MOVE移動的距離
private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
//重寫固定橫坐標移動到的位置
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
//這是View中定義的方法,實質是改變View的mLeft、mRight、mTop、mBottom達到移動View的效果,類似layout()方法的效果
//clampedX為新位置,oldLeft為舊位置,若想不動保證插值為0即可!!!!
mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
}
if (dy != 0) {
//重寫固定縱坐標移動到的位置
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
//這是View中定義的方法,實質是改變View的mLeft、mRight、mTop、mBottom達到移動View的效果,類似layout()方法的效果
mCapturedView.offsetTopAndBottom(clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
//當位置有變化時回調Callback的onViewPositionChanged方法實時通知
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
到此可以發現ACTION_MOVE時如果可以托拽則會實時挪動View的位置,同時回調很多方法。具體移動到哪和范圍由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()來決定。到此一次ACTION_MOVE事件的觸發處理也就分析完畢了。下面就該是松手時ACTION_UP或者ACTION_MOVE被mParentView的上級View攔截觸發的ACTION_CANCEL事件了,他們與ACTION_MOVE類似,直接觸發processTouchEvent()的ACTION_UP或者ACTION_CANCEL,如下:
public void processTouchEvent(MotionEvent ev) {
......
switch (action) {
......
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
//重置所有的狀態記錄
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
//重置所有的狀態記錄
cancel();
break;
}
}
}
可以看見,ACTION_UP和ACTION_CANCEL的實質都是重置資源和通知View觸摸被釋放,一個調運了releaseViewForPointerUp方法,另一個調運了dispatchViewReleased方法而已。那我們先看下releaseViewForPointerUp方法,源碼如下:
private void releaseViewForPointerUp() {
//獲得相關速率
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(
VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
final float yvel = clampMag(
VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
//傳入速率
dispatchViewReleased(xvel, yvel);
}
哎喲,握草,dispatchViewReleased(xvel, yvel)不就是ACTION_CANCEL傳參為0調運的方法嗎?也就是說ACTION_CANCEL與ACTION_UP邏輯一致,只是傳入的速率不同而已。那我們就來看看這個方法,如下:
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
//通知外部View被釋放了
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;
//如果之前是STATE_DRAGGING狀態,則復位狀態為STATE_IDLE
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
可以看見dispatchViewReleased()方法主要就是通過CallBack通知手指松開了,同時將狀態置位為STATE_IDLE。但是如果你留意該方法的注釋和方法裡的mReleaseInProgress變量的話,你一定會有疑惑。下面我就從注釋和該變量出現的地方進行下簡單分析,然後回過頭就知道咋回事了,該方法注釋提到了mReleaseInProgress變量與settleCapturedViewAt()和flingCapturedView()方法有關,那我們就來看下是啥關系,查看這兩個方法的開頭可以發現一個共性如下:
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot XXXXXXXXXX outside of a call to " +
"Callback#onViewReleased");
}
我靠,默認mReleaseInProgress是false,在dispatchViewReleased()中CallBack回調onViewReleased()方法前把他置位了true,onViewReleased()後置位了false。這就是為啥注釋裡說唯一可以調用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()裡,這下你指定就明白了,因為別的地方會拋出異常哇。
握草,這兩方法為啥這麼神奇,為啥只能在CallBack回調onViewReleased()中使用啊,我們先來分析一下就知道了,如下先看settleCapturedViewAt()方法源碼:
//限制最終慣性滾動到的終極位置及滾動過去
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
//表明只能在CallBack回調onViewReleased()中使用
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
"Callback#onViewReleased");
}
return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
......
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
//直接用過Scroller滾動到指定位置
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
//滾動時設置狀態為STATE_SETTLING
setDragState(STATE_SETTLING);
return true;
}
再看下flingCapturedView()方法,如下:
//不限制終點,由松手時加速度決定慣性滾動過去,fling效果
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
"Callback#onViewReleased");
}
//直接用過Scroller滾動
mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
minLeft, maxLeft, minTop, maxTop);
//滾動時設置狀態為STATE_SETTLING
setDragState(STATE_SETTLING);
}
到此確實很奇怪,為啥這兩個方法要這麼限制設計?我還是沒想明白,請教大神指點一下!但是我全局搜索mScroller的相關調運又發現了新的方法,如下:
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
這貨也能達到上面受限的settleCapturedViewAt()方法的效果,額額額,我凌亂了,這麼設計是為啥額,不清楚。不過到此一次完整的ViewDragHelper的觸摸移動主流程就分析完成了,其他相關狀態請自行分析,對我來說有這些就差不多夠了,主流程掌握了基本就能明白他提供的相關API含義了,其它的遇到問題再現查即可。
經過上面的淺析與注釋總結歸納如下總結方便日後使用;同時我們可以發現ViewDragHelper的本質其實涉及的知識點還是很多的,主要在事件處理上,不得不佩服該工具類考慮的強大。
//常用核心API歸納總結
public class ViewDragHelper {
//當前View處於空閒狀態,靜止
public static final int STATE_IDLE = 0;
//當前View處於托動狀態中
public static final int STATE_DRAGGING = 1;
//當前View處於滾動慣性到settling坐標間的狀態
public static final int STATE_SETTLING = 2;
//可托拽邊緣方向常量
public static final int EDGE_LEFT = 1 << 0;
public static final int EDGE_RIGHT = 1 << 1;
public static final int EDGE_TOP = 1 << 2;
public static final int EDGE_BOTTOM = 1 << 3;
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
...
//公有靜態內部抽象回調類,當ViewDragHelper控制的ViewGroup中View變化時會被回調
public static abstract class Callback {
//當托拽狀態變化時回調,譬如動畫結束後回調為STATE_IDLE等
//state有三種狀態,均以STATE_XXXX模式
public void onViewDragStateChanged(int state) {}
//當前被觸摸的View位置變化時回調
//changedView為位置變化的View,left/top變化時新的x左/y頂坐標,dx/dy為從舊到新的偏移量
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
//tryCaptureViewForDrag()成功捕獲到子View時或者手動調用captureChildView()時回調
public void onViewCaptured(View capturedChild, int activePointerId) {}
//當子View被松手或者ACTION_CANCEL時時回調,xvel/yvel為離開屏幕時各方向每秒運動的速率,為px
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
//當觸摸ACTION_DOWN或ACTION_POINTER_DOWN邊沿時回調
public void onEdgeTouched(int edgeFlags, int pointerId) {}
//返回true鎖定edgeFlags對應的邊緣,鎖定後的邊緣就不會回調onEdgeDragStarted()
public boolean onEdgeLock(int edgeFlags) {
return false;
}
//ACTION_MOVE且沒有鎖定邊緣時觸發
//可在此手動調用captureChildView()觸發從邊緣拖動子View,有點類似略過tryCaptureView返回false響應重定向其他View的效果
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
//尋找當前觸摸點View時回調此方法
//如果需要改變子View的倒序遍歷查詢順序則可改寫此方法,譬如讓重疊的下層View先於上層View被捕獲
public int getOrderedChildIndex(int index) {
return index;
}
//返回給定子View在相應方向上可以被拖動的最遠距離,默認為0,一般是可被挪動View時指定為指定View的大小等
public int getViewHorizontalDragRange(View child) {
return 0;
}
public int getViewVerticalDragRange(View child) {
return 0;
}
//傳遞當前觸摸上的子View,如果需要當前觸摸的子View進行拖拽移動就返回true,否則返回false
public abstract boolean tryCaptureView(View child, int pointerId);
//決定要拖拽的子View在所屬方向上應該移動到的位置
//child為拖拽的子View,left為期望值,dx為挪動差值
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
...
//構造工廠方法,sensitivity用來調節mTouchSlop的值,默認一般傳遞1即可
//sensitivity越大,mTouchSlop越小,對滑動的檢測就越敏感,譬如手指move多少才算滑動,否則忽略
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {...}
//設置允許父View的某個邊緣可以用來響應托拽
//相當於控制了CallBack對象的onEdgeTouched()和onEdgeDragStarted()方法是否被回調
public void setEdgeTrackingEnabled(int edgeFlags) {...}
//兩個傳遞MotionEvent的方法
public boolean shouldInterceptTouchEvent(MotionEvent ev) {...}
public void processTouchEvent(MotionEvent ev) {...}
//主動在父View內捕獲指定的子view用於拖曳,會回調tryCaptureView()
public void captureChildView(View childView, int activePointerId) {...}
//指定某個View自動滾動到指定的位置,初速度為0,可在任何地方調用
//如果這個方法返回true,那麼在接下來動畫移動的每一幀中都會回調continueSettling(boolean)方法,直到結束
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {...}
//以松手前的滑動速度為初值,讓捕獲到的子View自動滾動到指定位置,只能在Callback的onViewReleased()中使用
//如果這個方法返回true,那麼在接下來動畫移動的每一幀中都會回調continueSettling(boolean)方法,直到結束
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {...}
//以松手前的滑動速度為初值,讓捕獲到的子View在指定范圍內fling慣性運動,只能在Callback的onViewReleased()中使用
//如果這個方法返回true,那麼在接下來動畫移動的每一幀中都會回調continueSettling(boolean)方法,直到結束
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {...}
/**
* 在整個settle狀態中,這個方法會返回true,deferCallbacks決定滑動是否Runnable推遲,一般推遲
* 在調用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()時,
* 需要實現mParentView的computeScroll()方法,如下:
* @Override
* public void computeScroll() {
* if (mDragHelper.continueSettling(true)) {
* ViewCompat.postInvalidateOnAnimation(this);
* }
* }
*/
public boolean continueSettling(boolean deferCallbacks) {...}
...
//設置與獲取最小速率,一般保持默認
public void setMinVelocity(float minVel) {...}
public float getMinVelocity() {...}
//獲取當前子View所處狀態
public int getViewDragState() {...}
//返回可觸摸反饋區域邊緣大小,單位為px
public int getEdgeSize() {...}
//返回當前捕獲的子View,如果沒有則為null
public View getCapturedView() {...}
//獲取當前拖曳的View的Pointer ID
public int getActivePointerId() {...}
//獲取最小觸發拖曳動作的靈敏度差值,單位為px
public int getTouchSlop() {...}
//類似ACTION_CANCEL事件的觸發調運
public void cancel() {...}
//終止手勢,結束動畫滾動等,恢復初始STATE_IDLE狀態
public void abort() {...}
...
}
可以看見,上面基本就是整個ViewDragHelper的相關public等可控制的API解釋了,我們基本上可以通過他們的組合搞出各種自定義的控件來玩玩的。下面我們再粗略給出ViewDragHelper使用的流程,如下:
自定義ViewGroup裡通過ViewDragHelper靜態工廠方法create()創建實例並實現ViewDragHelper.CallBack抽象類。
在自定義ViewGroup的onInterceptTouchEvent()方法裡調用並返回ViewDragHelper的shouldInterceptTouchEvent()方法,在onTouchEvent()方法裡調用ViewDragHelper()的processTouchEvent()方法,且返回true(因為ACTION_DOWN時如果子View沒有消費事件,我們需要在onTouchEvent()中返回true,否則收不到後續的事件,從而不會產生拖動等效果)。
依據自己需求實現ViewDragHelper.CallBack中相關方法即可。
至此已經實現了子View拖動效果,如果需要Fling或者慣性滾動效果則還需要實現自定義ViewGroup的computeScroll()方法進行手動刷幀。
以上就是使用ViewDragHelper的全過程,可以發現其真的很牛逼。
扯了上面那麼多,下面我們給出一個使用ViewDragHelper搞出來的東東,大家一看就知道他很牛逼了。如下是效果圖(Ubuntu下面,GIF圖Low爆了,請諒解):
該控件效果就不解釋了,代碼也不多,源碼可以點我下載即可。
至此整個ViewDragHelper進階就介紹完了,如果你還認為不夠的話,那就可以自行看看官方的DrawerLayout控件實現即可,可以把它當作該文的進階實例,這裡我就不再分析了,我們的項目中重寫了DrawerLayout,因為一些特殊的交互需求。
一、調用Android lint命令查找出沒有用到的資源,並生成一個清單列表:命令:lint –check “UnusedResources” [project_path
zip操作符概述官方文檔描述:Returns an Observable that emits the results of a specified combiner f
上次我們講到了使用URLConnection的網絡編程,URLConnection已經可以非常方便地與指定站點交換信息,URLConnection下還有一個子類:Http
Intent意圖是android中非常重要的部分,他在Activity,service中有較為廣泛的應用。 1 public void startActiv