作者: ztelur
聯(lián)系方式:segmentfault,csdn,github
本文僅供個人學(xué)習(xí),不用于任何形式商業(yè)目的,轉(zhuǎn)載請注明原作者、文章來源,鏈接,版權(quán)歸原文作者所有。
本文是android滾動相關(guān)的系列文章的第二篇,主要總結(jié)一下使用手勢相關(guān)的代碼邏輯。主要是單點拖動,多點拖動,fling和OveScroll的實現(xiàn)。每個手勢都會有代碼片段。
?對android滾動相關(guān)的知識還不太了解的同學(xué)可以先閱讀一下文章:
為了節(jié)約你的時間,我特地將文章大致內(nèi)容總結(jié)如下:
- 手勢Drag的實現(xiàn)和原理
- 手勢Fling的實現(xiàn)和原理
- OverScroll效果和EdgeEffect效果的實現(xiàn)和原理。
詳細(xì)代碼請查看我的github
Drag
Drag是最為基本的手勢:用戶可以使用手指在屏幕上滑動,以拖動屏幕相應(yīng)內(nèi)容移動。實現(xiàn)Drag手勢其實很簡單,步驟如下:
- 在
ACTION_DOWN事件發(fā)生時,調(diào)用getX和getY函數(shù)獲得事件發(fā)生的x,y坐標(biāo)值,并記錄在mLastX和mLastY變量中。 - 在
ACTION_MOVE事件發(fā)生時,調(diào)用getX和getY函數(shù)獲得事件發(fā)生的x,y坐標(biāo)值,將其與mLastX和mLastY比較,如果二者差值大于一定限制(ScaledTouchSlop),就執(zhí)行scrollBy函數(shù),進行滾動,最后更新mLastX和mLastY的值。 - 在
ACTION_UP和ACTION_CANCEL事件發(fā)生時,清空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;
}
}
// 當(dāng)mIsBeingDragged為true時,就不用判斷> touchSlopg啦,不然會導(dǎo)致滾動是一段一段的
// 不是很連續(xù)
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;
}
多觸點Drag
上邊的代碼只適用于單點觸控的手勢,如果你是兩個手指觸摸屏幕,那么它只會根據(jù)你第一個手指滑動的情況來進行屏幕滾動。更為致命的是,當(dāng)你先松開第一個手指時,由于我們少監(jiān)聽了ACTION_POINTER_UP事件,將會導(dǎo)致屏幕突然滾動一大段距離,因為第二個手指移動事件的x,y值會和第一個手指移動時留下的mLastX和mLastY比較,導(dǎo)致屏幕滾動。
如果我們要監(jiān)聽并處理多觸點的事件,我們還需要對ACTION_POINTER_DOWN和ACTION_POINTER_UP事件進行監(jiān)聽,并且在ACTION_MOVE事件時,要記錄所有觸摸點事件發(fā)生的x,y值。
- 當(dāng)
ACTION_POINTER_DOWN事件發(fā)生時,我們要記錄第二觸摸點事件發(fā)生的x,y值為mSecondaryLastX和mSecondaryLastY,和第二觸摸點pointer的id為mSecondaryPointerId - 當(dāng)
ACTION_MOVE事件發(fā)生時,我們除了根據(jù)第一觸摸點pointer的x,y值進行滾動外,也要更新mSecondayLastX和mSecondaryLastY - 當(dāng)
ACTION_POINTER_UP事件發(fā)生時,我們要先判斷是哪個觸摸點手指被抬起來啦,如果是第一觸摸點,那么我們就將坐標(biāo)值和pointer的id都更換為第二觸摸點的數(shù)據(jù);如果是第二觸摸點,就只要重置一下數(shù)據(jù)即可。
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;
//重復(fù)代碼,為了讓邏輯看起來更加清晰
} 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
當(dāng)用戶手指快速劃過屏幕,然后快速立刻屏幕時,系統(tǒng)會判定用戶執(zhí)行了一個Fling手勢。視圖會快速滾動,并且在手指立刻屏幕之后也會滾動一段時間。Drag表示手指滑動多少距離,界面跟著顯示多少距離,而fling是根據(jù)你的滑動方向與輕重,還會自動滑動一段距離。Filing手勢在android交互設(shè)計中應(yīng)用非常廣泛:電子書的滑動翻頁、ListView滑動刪除item、滑動解鎖等。所以如何檢測用戶的fling手勢是非常重要的。
?在檢測Fling時,你需要檢測手指在屏幕上滑動的速度,這是你就需要VelocityTracker和Scroller這兩個類啦。
- 我們首先使用
VelocityTracker.obtain()這個方法獲得其實例 - 然后每次處理觸摸時間時,我們將觸摸事件通過
addMovement方法傳遞給它 - 最后在處理
ACTION_UP事件時,我們通過computeCurrentVelocity方法獲得滑動速度; - 我們判斷滑動速度是否大于一定數(shù)值(MinFlingSpeed),如果大于,那么我們調(diào)用
Scroller的fling方法。然后調(diào)用invalidate()函數(shù)。 - 我們需要重載
computeScroll方法,在這個方法內(nèi),我們調(diào)用Scroller的computeScrollOffset()方法啦計算當(dāng)前的偏移量,然后獲得偏移量,并調(diào)用scrollTo函數(shù),最后調(diào)用postInvalidate()函數(shù)。 - 除了上述的操作外,我們需要在處理
ACTION_DOWN事件時,對屏幕當(dāng)前狀態(tài)進行判斷,如果屏幕現(xiàn)在正在滾動(用戶剛進行了Fling手勢),我們需要停止屏幕滾動。
具體這一套流程是如何運轉(zhuǎn)的,我會在下一篇文章中詳細(xì)解釋,大家也可以自己查閱代碼或者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) {
//當(dāng)手指立刻屏幕時,獲得速度,作為fling的初始速度 mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinFlingSpeed) {
// 由于坐標(biāo)軸正方向問題,要加負(fù)號。
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();
}
}
OverScroll
在Android手機上,當(dāng)我們滾動屏幕內(nèi)容到達(dá)內(nèi)容邊界時,如果再滾動就會有一個發(fā)光效果。而且界面會進行滾動一小段距離之后再回復(fù)原位,這些效果是如何實現(xiàn)的呢?我們需要使用Scroller和scrollTo的升級版OverScroller和overScrollBy了,還有發(fā)光的EdgeEffect類。
?我們先來了解一下相關(guān)的API,理解了這些接口參數(shù)的含義,你就可以輕松使用這些接口來實現(xiàn)上述的效果啦。
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 : 偏移量,也就是當(dāng)前要滾動的x,y值。
- int scrollX,int scrollY : 當(dāng)前的mScrollX和mScrollY的值。
- int maxOverScrollX,int maxOverScrollY: 標(biāo)示可以滾動的最大的x,y值,也就是你視圖真實的長和寬。也就是說,你的視圖可視大小可能是100,100,但是視圖中的內(nèi)容的大小為200,200,所以,上述兩個值就為200,200
- int maxOverScrollX,int maxOverScrollY:允許超過滾動范圍的最大值,x方向的滾動范圍就是0maxOverScrollX,y方向的滾動范圍就是0maxOverScrollY。
- boolean isTouchEvent:是否在
onTouchEvent中調(diào)用的這個函數(shù)。所以,當(dāng)你在computeScroll中調(diào)用這個函數(shù)時,就可以傳入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
- int scrollX,int scrollY:就是x,y方向的滾動距離,就相當(dāng)于
mScrollX和mScrollY。你既可以直接把二者賦值給相應(yīng)的成員變量,也可以使用scrollTo函數(shù)。 - boolean clampedX,boolean clampY:表示是否到達(dá)超出滾動范圍的最大值。如果為true,就需要調(diào)用
OverScroll的springBack函數(shù)來讓視圖回復(fù)原來位置。
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
- int startX,int startY:標(biāo)示當(dāng)前的滾動值,也就是
mScrollX和mScrollY的值。 - int minX,int maxX:標(biāo)示x方向的合理滾動值
- int minY,int maxY:標(biāo)示y方向的合理滾動值。
相信看完上述的API之后,大家會有很多的疑惑,所以這里我來舉個例子。
?假設(shè)視圖大小為100*100。當(dāng)你一直下拉到視圖上邊緣,然后在下拉,這時,mScrollY已經(jīng)達(dá)到或者超過正常的滾動范圍的最小值了,也就是0,但是你的maxOverScrollY傳入的是10,所以,mScrollY最小可以到達(dá)-10,最大可以為110。所以,你可以繼續(xù)下拉。等到mScrollY到達(dá)或者超過-10時,clampedY就為true,標(biāo)示視圖已經(jīng)達(dá)到可以O(shè)verScroll的邊界,需要回滾到正常滾動范圍,所以你調(diào)用springBack(0,0,0,100)。
然后我們再來看一下發(fā)光效果是如何實現(xiàn)的。
?使用EdgeEffect類。一般來說,當(dāng)你只上下滾動時,你只需要兩個EdgeEffect實例,分別代表上邊界和下邊界的發(fā)光效果。你需要在下面兩個情景下改變EdgeEffect的狀態(tài),然后在draw()方法中繪制EdgeEffect
- 處理
ACTION_MOVE時,如果發(fā)現(xiàn)y方向的滾動值超過了正常范圍的最小值時,你需要調(diào)用上邊界實例的onPull方法。如果是超過最大值,那么就是調(diào)用下邊界的onPull方法。 - 在
computeScroll函數(shù)中,也就是說Fling手勢執(zhí)行過程中,如果發(fā)現(xiàn)y方向的滾動值超過正常范圍時的最小值時,調(diào)用onAbsorb函數(shù)。
然后就是重載draw方法,讓EdgeEffect實例在畫布上繪制自己。你會發(fā)現(xiàn),你必須對畫布進行移動或者旋轉(zhuǎn)來讓EdgeEffect繪制出上邊界或者下邊界的發(fā)光的效果,因為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調(diào)用
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());
}
}
}
}
后記
本篇文章是系列文章的第二篇,大家可能已經(jīng)知道如何實現(xiàn)各類手勢,但是對其中的機制和原理還不是很了解,之后的第三篇會講解從本篇代碼的視角講解一下android視圖繪制的原理和Scroller的機制,希望大家多多關(guān)注。
參考文章
http://stackoverflow.com/questions/22843671/android-swipe-vs-fling
https://www.google.com/design/spec/patterns/gestures.html#gestures-drag-swipe-or-fling-details
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1212/2145.html