做者:ztelur
聯繫方式:segmentfault,csdn,github
本文僅供我的學習,不用於任何形式商業目的,轉載請註明原做者、文章來源,連接,版權歸原文做者全部。html
本文是android滾動相關的系列文章的第二篇,主要總結一下使用手勢相關的代碼邏輯。主要是單點拖動,多點拖動,fling和OveScroll的實現。每一個手勢都會有代碼片斷。java
對android滾動相關的知識還不太瞭解的同窗能夠先閱讀一下文章:android
爲了節約你的時間,我特意將文章大體內容總結以下:spring
手勢Drag的實現和原理canvas
手勢Fling的實現和原理segmentfault
OverScroll效果和EdgeEffect效果的實現和原理。ide
詳細代碼請查看個人github。函數
Drag是最爲基本的手勢:用戶可使用手指在屏幕上滑動,以拖動屏幕相應內容移動。實現Drag手勢其實很簡單,步驟以下:
在ACTION_DOWN
事件發生時,調用getX
和getY
函數得到事件發生的x,y座標值,並記錄在mLastX
和mLastY
變量中。
在ACTION_MOVE
事件發生時,調用getX
和getY
函數得到事件發生的x,y座標值,將其與mLastX
和mLastY
比較,若是兩者差值大於必定限制(ScaledTouchSlop),就執行scrollBy
函數,進行滾動,最後更新mLastX
和mLastY
的值。
在ACTION_UP
和ACTION_CANCEL
事件發生時,清空mLastX
,mLastY
。
@Override public boolean onTouchEvent(MotionEvent event) { int actionId = MotionEventCompat.getActionMasked(event); switch (actionId) { case MotionEvent.ACTION_DOWN: mLastX = event.getX(); mLastY = event.getY(); mIsBeingDragged = true; if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } break; case MotionEvent.ACTION_MOVE: float curX = event.getX(); float curY = event.getY(); int deltaX = (int) (mLastX - curX); int deltaY = (int) (mLastY - curY); if (!mIsBeingDragged && (Math.abs(deltaX)> mTouchSlop || Math.abs(deltaY)> mTouchSlop)) { mIsBeingDragged = true; // 讓第一次滑動的距離和以後的距離不至於差距太大 // 由於第一次必須>TouchSlop,以後則是直接滑動 if (deltaX > 0) { deltaX -= mTouchSlop; } else { deltaX += mTouchSlop; } if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } // 當mIsBeingDragged爲true時,就不用判斷> touchSlopg啦,否則會致使滾動是一段一段的 // 不是很連續 if (mIsBeingDragged) { scrollBy(deltaX, deltaY); mLastX = curX; mLastY = curY; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsBeingDragged = false; mLastY = 0; mLastX = 0; break; default: } return mIsBeingDragged; }
上邊的代碼只適用於單點觸控的手勢,若是你是兩個手指觸摸屏幕,那麼它只會根據你第一個手指滑動的狀況來進行屏幕滾動。更爲致命的是,當你先鬆開第一個手指時,因爲咱們少監聽了ACTION_POINTER_UP
事件,將會致使屏幕忽然滾動一大段距離,由於第二個手指移動事件的x,y值會和第一個手指移動時留下的mLastX
和mLastY
比較,致使屏幕滾動。
若是咱們要監聽並處理多觸點的事件,咱們還須要對ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件進行監聽,而且在ACTION_MOVE
事件時,要記錄全部觸摸點事件發生的x,y值。
當ACTION_POINTER_DOWN
事件發生時,咱們要記錄第二觸摸點事件發生的x,y值爲mSecondaryLastX
和mSecondaryLastY
,和第二觸摸點pointer的id爲mSecondaryPointerId
當ACTION_MOVE
事件發生時,咱們除了根據第一觸摸點pointer的x,y值進行滾動外,也要更新mSecondayLastX
和mSecondaryLastY
當ACTION_POINTER_UP
事件發生時,咱們要先判斷是哪一個觸摸點手指被擡起來啦,若是是第一觸摸點,那麼咱們就將座標值和pointer的id都更換爲第二觸摸點的數據;若是是第二觸摸點,就只要重置一下數據便可。
switch (actionId) { ..... case MotionEvent.ACTION_POINTER_DOWN: activePointerIndex = MotionEventCompat.getActionIndex(event); mSecondaryPointerId = MotionEventCompat.findPointerIndex(event,activePointerIndex); mSecondaryLastX = MotionEventCompat.getX(event,activePointerIndex); mSecondaryLastY = MotionEventCompat.getY(event,mActivePointerId); break; case MotionEvent.ACTION_MOVE: ...... // handle secondary pointer move if (mSecondaryPointerId != INVALID_ID) { int mSecondaryPointerIndex = MotionEventCompat.findPointerIndex(event, mSecondaryPointerId); mSecondaryLastX = MotionEventCompat.getX(event, mSecondaryPointerIndex); mSecondaryLastY = MotionEventCompat.getY(event, mSecondaryPointerIndex); } break; case MotionEvent.ACTION_POINTER_UP: //判斷是不是activePointer up了 activePointerIndex = MotionEventCompat.getActionIndex(event); int curPointerId = MotionEventCompat.getPointerId(event,activePointerIndex); Log.e(TAG, "onTouchEvent: "+activePointerIndex +" "+curPointerId +" activeId"+mActivePointerId+ "secondaryId"+mSecondaryPointerId); if (curPointerId == mActivePointerId) { // active pointer up mActivePointerId = mSecondaryPointerId; mLastX = mSecondaryLastX; mLastY = mSecondaryLastY; mSecondaryPointerId = INVALID_ID; mSecondaryLastY = 0; mSecondaryLastX = 0; //重複代碼,爲了讓邏輯看起來更加清晰 } else{ //若是是secondary pointer up mSecondaryPointerId = INVALID_ID; mSecondaryLastY = 0; mSecondaryLastX = 0; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsBeingDragged = false; mActivePointerId = INVALID_ID; mLastY = 0; mLastX = 0; break; default: }
當用戶手指快速劃過屏幕,而後快速馬上屏幕時,系統會斷定用戶執行了一個Fling手勢。視圖會快速滾動,而且在手指馬上屏幕以後也會滾動一段時間。Drag表示手指滑動多少距離,界面跟着顯示多少距離,而fling是根據你的滑動方向與輕重,還會自動滑動一段距離。Filing手勢在android交互設計中應用很是普遍:電子書的滑動翻頁、ListView滑動刪除item、滑動解鎖等。因此如何檢測用戶的fling手勢是很是重要的。
在檢測Fling時,你須要檢測手指在屏幕上滑動的速度,這是你就須要VelocityTracker
和Scroller
這兩個類啦。
咱們首先使用VelocityTracker.obtain()
這個方法得到其實例
而後每次處理觸摸時間時,咱們將觸摸事件經過addMovement
方法傳遞給它
最後在處理ACTION_UP
事件時,咱們經過computeCurrentVelocity
方法得到滑動速度;
咱們判斷滑動速度是否大於必定數值(MinFlingSpeed),若是大於,那麼咱們調用Scroller
的fling
方法。而後調用invalidate()
函數。
咱們須要重載computeScroll
方法,在這個方法內,咱們調用Scroller
的computeScrollOffset()
方法啦計算當前的偏移量,而後得到偏移量,並調用scrollTo
函數,最後調用postInvalidate()
函數。
除了上述的操做外,咱們須要在處理ACTION_DOWN
事件時,對屏幕當前狀態進行判斷,若是屏幕如今正在滾動(用戶剛進行了Fling手勢),咱們須要中止屏幕滾動。
具體這一套流程是如何運轉的,我會在下一篇文章中詳細解釋,你們也能夠本身查閱代碼或者google來搞懂其中的原理。
@Override public boolean onTouchEvent(MotionEvent event) { ..... if (mVelocityTracker == null) { //檢查速度測量器,若是爲null,得到一個 mVelocityTracker = VelocityTracker.obtain(); } int action = MotionEventCompat.getActionMasked(event); int index = -1; switch (action) { case MotionEvent.ACTION_DOWN: ...... if (!mScroller.isFinished()) { //fling mScroller.abortAnimation(); } ..... break; case MotionEvent.ACTION_MOVE: ...... break; case MotionEvent.ACTION_CANCEL: endDrag(); break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { //當手指馬上屏幕時,得到速度,做爲fling的初始速度 mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed); int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId); if (Math.abs(initialVelocity) > mMinFlingSpeed) { // 因爲座標軸正方向問題,要加負號。 doFling(-initialVelocity); } endDrag(); } break; default: } //每次onTouchEvent處理Event時,都將event交給時間 //測量器 if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } return true; } private void doFling(int speed) { if (mScroller == null) { return; } mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
在Android手機上,當咱們滾動屏幕內容到達內容邊界時,若是再滾動就會有一個發光效果。並且界面會進行滾動一小段距離以後再回復原位,這些效果是如何實現的呢?咱們須要使用Scroller
和scrollTo
的升級版OverScroller
和overScrollBy
了,還有發光的EdgeEffect
類。
咱們先來了解一下相關的API,理解了這些接口參數的含義,你就能夠輕鬆使用這些接口來實現上述的效果啦。
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
int deltaX,int deltaY : 偏移量,也就是當前要滾動的x,y值。
int scrollX,int scrollY : 當前的mScrollX和mScrollY的值。
int maxOverScrollX,int maxOverScrollY: 標示能夠滾動的最大的x,y值,也就是你視圖真實的長和寬。也就是說,你的視圖可視大小多是100,100,可是視圖中的內容的大小爲200,200,因此,上述兩個值就爲200,200
int maxOverScrollX,int maxOverScrollY:容許超過滾動範圍的最大值,x方向的滾動範圍就是0~maxOverScrollX,y方向的滾動範圍就是0~maxOverScrollY。
boolean isTouchEvent:是否在onTouchEvent
中調用的這個函數。因此,當你在computeScroll
中調用這個函數時,就能夠傳入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
int scrollX,int scrollY:就是x,y方向的滾動距離,就至關於mScrollX
和mScrollY
。你既能夠直接把兩者賦值給相應的成員變量,也可使用scrollTo
函數。
boolean clampedX,boolean clampY:表示是否到達超出滾動範圍的最大值。若是爲true,就須要調用OverScroll
的springBack
函數來讓視圖回覆原來位置。
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
int startX,int startY:標示當前的滾動值,也就是mScrollX
和mScrollY
的值
int minX,int maxX:標示x方向的合理滾動值
int minY,int maxY:標示y方向的合理滾動值
相信看完上述的API以後,你們會有不少的疑惑,因此這裏我來舉個例子。
假設視圖大小爲100*100。當你一直下拉到視圖上邊緣,而後在下拉,這時,mScrollY
已經達到或者超過正常的滾動範圍的最小值了,也就是0,可是你的maxOverScrollY傳入的是10,因此,mScrollY
最小能夠到達-10,最大能夠爲110。因此,你能夠繼續下拉。等到mScrollY
到達或者超過-10時,clampedY就爲true,標示視圖已經達到能夠OverScroll的邊界,須要回滾到正常滾動範圍,因此你調用springBack(0,0,0,100)。
而後咱們再來看一下發光效果是如何實現的。
使用EdgeEffect
類。通常來講,當你只上下滾動時,你只須要兩個EdgeEffect
實例,分別表明上邊界和下邊界的發光效果。你須要在下面兩個情景下改變EdgeEffect
的狀態,而後在draw()
方法中繪製EdgeEffect
處理ACTION_MOVE
時,若是發現y方向的滾動值超過了正常範圍的最小值時,你須要調用上邊界實例的onPull
方法。若是是超過最大值,那麼就是調用下邊界的onPull
方法。
在computeScroll
函數中,也就是說Fling手勢執行過程當中,若是發現y方向的滾動值超過正常範圍時的最小值時,調用onAbsorb
函數。
而後就是重載draw
方法,讓EdgeEffect
實例在畫布上繪製本身。你會發現,你必須對畫布進行移動或者旋轉來讓EdgeEffect
繪製出上邊界或者下邊界的發光的效果,由於EdgeEffect
對象本身是沒有上下左右的概念的。
@Override public void draw(Canvas canvas) { super.draw(canvas); if (mEdgeEffectTop != null) { final int scrollY = getScrollY(); if (!mEdgeEffectTop.isFinished()) { final int count = canvas.save(); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); canvas.translate(getPaddingLeft(),Math.min(0,scrollY)); mEdgeEffectTop.setSize(width,getHeight()); if (mEdgeEffectTop.draw(canvas)) { postInvalidate(); } canvas.restoreToCount(count); } } if (mEdgeEffectBottom != null) { final int scrollY = getScrollY(); if (!mEdgeEffectBottom.isFinished()) { final int count = canvas.save(); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); canvas.translate(-width+getPaddingLeft(),Math.max(getScrollRange(),scrollY)+getHeight()); canvas.rotate(180,width,0); mEdgeEffectBottom.setSize(width,getHeight()); if (mEdgeEffectBottom.draw(canvas)) { postInvalidate(); } canvas.restoreToCount(count); } } } @Override public boolean onTouchEvent(MotionEvent event) { ...... case MotionEvent.ACTION_MOVE: ..... if (mIsBeingDragged) { overScrollBy(0,(int)deltaY,0,getScrollY(),0,getScrollRange(),0,mOverScrollDistance,true); final int pulledToY = (int)(getScrollY()+deltaY); mLastY = y; if (pulledToY<0) { mEdgeEffectTop.onPull(deltaY/getHeight(),event.getX(mActivePointerId)/getWidth()); if (!mEdgeEffectBottom.isFinished()) { mEdgeEffectBottom.onRelease(); } } else if(pulledToY> getScrollRange()) { mEdgeEffectBottom.onPull(deltaY/getHeight(),1.0f-event.getX(mActivePointerId)/getWidth()); if (!mEdgeEffectTop.isFinished()) { mEdgeEffectTop.onRelease(); } } if (mEdgeEffectTop != null && mEdgeEffectBottom != null &&(!mEdgeEffectTop.isFinished() || !mEdgeEffectBottom.isFinished())) { postInvalidate(); } } ..... } .... } @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { if (!mScroller.isFinished()) { int oldX = getScrollX(); int oldY = getScrollY(); scrollTo(scrollX,scrollY); onScrollChanged(scrollX,scrollY,oldX,oldY); if (clampedY) { Log.e("TEST1","springBack"); mScroller.springBack(getScrollX(),getScrollY(),0,0,0,getScrollRange()); } } else { // TouchEvent中的overScroll調用 super.scrollTo(scrollX,scrollY); } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); int range = getScrollRange(); if (oldX != x || oldY != y) { overScrollBy(x-oldX,y-oldY,oldX,oldY,0,range,0,mOverFlingDistance,false); } final int overScrollMode = getOverScrollMode(); final boolean canOverScroll = overScrollMode == OVER_SCROLL_ALWAYS || (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverScroll) { if (y<0 && oldY >= 0) { mEdgeEffectTop.onAbsorb((int)mScroller.getCurrVelocity()); } else if (y> range && oldY < range) { mEdgeEffectBottom.onAbsorb((int)mScroller.getCurrVelocity()); } } } }
本篇文章是系列文章的第二篇,你們可能已經知道如何實現各種手勢,可是對其中的機制和原理還不是很瞭解,以後的第三篇會講解從本篇代碼的視角講解一下android視圖繪製的原理和Scroller的機制,但願你們多多關注。