作者: ztelur
聯(lián)系方式:segmentfault缓溅,csdn蛇损,github
本文僅供個(gè)人學(xué)習(xí),不用于任何形式商業(yè)目的坛怪,轉(zhuǎn)載請(qǐng)注明原作者淤齐、文章來源,鏈接袜匿,版權(quán)歸原文作者所有更啄。
本文是android滾動(dòng)相關(guān)的系列文章的第二篇,主要總結(jié)一下使用手勢(shì)相關(guān)的代碼邏輯居灯。主要是單點(diǎn)拖動(dòng)祭务,多點(diǎn)拖動(dòng)内狗,fling和OveScroll的實(shí)現(xiàn)。每個(gè)手勢(shì)都會(huì)有代碼片段义锥。
?對(duì)android滾動(dòng)相關(guān)的知識(shí)還不太了解的同學(xué)可以先閱讀一下文章:
為了節(jié)約你的時(shí)間其屏,我特地將文章大致內(nèi)容總結(jié)如下:
- 手勢(shì)Drag的實(shí)現(xiàn)和原理
- 手勢(shì)Fling的實(shí)現(xiàn)和原理
- OverScroll效果和EdgeEffect效果的實(shí)現(xiàn)和原理。
詳細(xì)代碼請(qǐng)查看我的github
Drag
Drag是最為基本的手勢(shì):用戶可以使用手指在屏幕上滑動(dòng)缨该,以拖動(dòng)屏幕相應(yīng)內(nèi)容移動(dòng)。實(shí)現(xiàn)Drag手勢(shì)其實(shí)很簡(jiǎn)單川背,步驟如下:
- 在
ACTION_DOWN
事件發(fā)生時(shí)贰拿,調(diào)用getX
和getY
函數(shù)獲得事件發(fā)生的x,y坐標(biāo)值,并記錄在mLastX
和mLastY
變量中熄云。 - 在
ACTION_MOVE
事件發(fā)生時(shí)膨更,調(diào)用getX
和getY
函數(shù)獲得事件發(fā)生的x,y坐標(biāo)值,將其與mLastX
和mLastY
比較,如果二者差值大于一定限制(ScaledTouchSlop),就執(zhí)行scrollBy
函數(shù)缴允,進(jìn)行滾動(dòng),最后更新mLastX
和mLastY
的值荚守。 - 在
ACTION_UP
和ACTION_CANCEL
事件發(fā)生時(shí),清空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;
// 讓第一次滑動(dòng)的距離和之后的距離不至于差距太大
// 因?yàn)榈谝淮伪仨?gt;TouchSlop,之后則是直接滑動(dòng)
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
// 當(dāng)mIsBeingDragged為true時(shí),就不用判斷> touchSlopg啦薄料,不然會(huì)導(dǎo)致滾動(dòng)是一段一段的
// 不是很連續(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;
}
多觸點(diǎn)Drag
上邊的代碼只適用于單點(diǎn)觸控的手勢(shì)敞贡,如果你是兩個(gè)手指觸摸屏幕,那么它只會(huì)根據(jù)你第一個(gè)手指滑動(dòng)的情況來進(jìn)行屏幕滾動(dòng)摄职。更為致命的是誊役,當(dāng)你先松開第一個(gè)手指時(shí),由于我們少監(jiān)聽了ACTION_POINTER_UP
事件谷市,將會(huì)導(dǎo)致屏幕突然滾動(dòng)一大段距離蛔垢,因?yàn)榈诙€(gè)手指移動(dòng)事件的x,y值會(huì)和第一個(gè)手指移動(dòng)時(shí)留下的mLastX
和mLastY
比較,導(dǎo)致屏幕滾動(dòng)迫悠。
如果我們要監(jiān)聽并處理多觸點(diǎn)的事件鹏漆,我們還需要對(duì)ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件進(jìn)行監(jiān)聽,并且在ACTION_MOVE
事件時(shí)及皂,要記錄所有觸摸點(diǎn)事件發(fā)生的x,y值甫男。
- 當(dāng)
ACTION_POINTER_DOWN
事件發(fā)生時(shí),我們要記錄第二觸摸點(diǎn)事件發(fā)生的x,y值為mSecondaryLastX
和mSecondaryLastY
验烧,和第二觸摸點(diǎn)pointer的id為mSecondaryPointerId
- 當(dāng)
ACTION_MOVE
事件發(fā)生時(shí)板驳,我們除了根據(jù)第一觸摸點(diǎn)pointer的x,y值進(jìn)行滾動(dòng)外碍拆,也要更新mSecondayLastX
和mSecondaryLastY
- 當(dāng)
ACTION_POINTER_UP
事件發(fā)生時(shí)若治,我們要先判斷是哪個(gè)觸摸點(diǎn)手指被抬起來啦慨蓝,如果是第一觸摸點(diǎn),那么我們就將坐標(biāo)值和pointer的id都更換為第二觸摸點(diǎn)的數(shù)據(jù)端幼;如果是第二觸摸點(diǎn)礼烈,就只要重置一下數(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)用戶手指快速劃過屏幕此熬,然后快速立刻屏幕時(shí),系統(tǒng)會(huì)判定用戶執(zhí)行了一個(gè)Fling手勢(shì)滑进。視圖會(huì)快速滾動(dòng)犀忱,并且在手指立刻屏幕之后也會(huì)滾動(dòng)一段時(shí)間。Drag表示手指滑動(dòng)多少距離扶关,界面跟著顯示多少距離阴汇,而fling是根據(jù)你的滑動(dòng)方向與輕重,還會(huì)自動(dòng)滑動(dòng)一段距離节槐。Filing手勢(shì)在android交互設(shè)計(jì)中應(yīng)用非常廣泛:電子書的滑動(dòng)翻頁(yè)搀庶、ListView滑動(dòng)刪除item、滑動(dòng)解鎖等铜异。所以如何檢測(cè)用戶的fling手勢(shì)是非常重要的哥倔。
?在檢測(cè)Fling時(shí),你需要檢測(cè)手指在屏幕上滑動(dòng)的速度熙掺,這是你就需要VelocityTracker
和Scroller
這兩個(gè)類啦未斑。
- 我們首先使用
VelocityTracker.obtain()
這個(gè)方法獲得其實(shí)例 - 然后每次處理觸摸時(shí)間時(shí),我們將觸摸事件通過
addMovement
方法傳遞給它 - 最后在處理
ACTION_UP
事件時(shí)币绩,我們通過computeCurrentVelocity
方法獲得滑動(dòng)速度; - 我們判斷滑動(dòng)速度是否大于一定數(shù)值(MinFlingSpeed),如果大于蜡秽,那么我們調(diào)用
Scroller
的fling
方法。然后調(diào)用invalidate()
函數(shù)缆镣。 - 我們需要重載
computeScroll
方法芽突,在這個(gè)方法內(nèi)袍患,我們調(diào)用Scroller
的computeScrollOffset()
方法啦計(jì)算當(dāng)前的偏移量牺汤,然后獲得偏移量,并調(diào)用scrollTo
函數(shù),最后調(diào)用postInvalidate()
函數(shù)麸折。 - 除了上述的操作外钠糊,我們需要在處理
ACTION_DOWN
事件時(shí)挟秤,對(duì)屏幕當(dāng)前狀態(tài)進(jìn)行判斷,如果屏幕現(xiàn)在正在滾動(dòng)(用戶剛進(jìn)行了Fling手勢(shì))抄伍,我們需要停止屏幕滾動(dòng)艘刚。
具體這一套流程是如何運(yùn)轉(zhuǎn)的,我會(huì)在下一篇文章中詳細(xì)解釋截珍,大家也可以自己查閱代碼或者google來搞懂其中的原理攀甚。
@Override
public boolean onTouchEvent(MotionEvent event) {
.....
if (mVelocityTracker == null) {
//檢查速度測(cè)量器箩朴,如果為null,獲得一個(gè)
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)手指立刻屏幕時(shí)秋度,獲得速度炸庞,作為fling的初始速度 mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinFlingSpeed) {
// 由于坐標(biāo)軸正方向問題,要加負(fù)號(hào)荚斯。
doFling(-initialVelocity);
}
endDrag();
}
break;
default:
}
//每次onTouchEvent處理Event時(shí)埠居,都將event交給時(shí)間
//測(cè)量器
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手機(jī)上,當(dāng)我們滾動(dòng)屏幕內(nèi)容到達(dá)內(nèi)容邊界時(shí)事期,如果再滾動(dòng)就會(huì)有一個(gè)發(fā)光效果拐格。而且界面會(huì)進(jìn)行滾動(dòng)一小段距離之后再回復(fù)原位,這些效果是如何實(shí)現(xiàn)的呢刑赶?我們需要使用Scroller
和scrollTo
的升級(jí)版OverScroller
和overScrollBy
了,還有發(fā)光的EdgeEffect
類懂衩。
?我們先來了解一下相關(guān)的API撞叨,理解了這些接口參數(shù)的含義,你就可以輕松使用這些接口來實(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)前要滾動(dòng)的x,y值法希。
- int scrollX,int scrollY : 當(dāng)前的mScrollX和mScrollY的值枷餐。
- int maxOverScrollX,int maxOverScrollY: 標(biāo)示可以滾動(dòng)的最大的x,y值,也就是你視圖真實(shí)的長(zhǎng)和寬苫亦。也就是說毛肋,你的視圖可視大小可能是100,100,但是視圖中的內(nèi)容的大小為200,200,所以,上述兩個(gè)值就為200,200
- int maxOverScrollX,int maxOverScrollY:允許超過滾動(dòng)范圍的最大值屋剑,x方向的滾動(dòng)范圍就是0maxOverScrollX,y方向的滾動(dòng)范圍就是0maxOverScrollY润匙。
- boolean isTouchEvent:是否在
onTouchEvent
中調(diào)用的這個(gè)函數(shù)。所以唉匾,當(dāng)你在computeScroll
中調(diào)用這個(gè)函數(shù)時(shí)孕讳,就可以傳入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
- int scrollX,int scrollY:就是x巍膘,y方向的滾動(dòng)距離厂财,就相當(dāng)于
mScrollX
和mScrollY
。你既可以直接把二者賦值給相應(yīng)的成員變量峡懈,也可以使用scrollTo
函數(shù)璃饱。 - boolean clampedX,boolean clampY:表示是否到達(dá)超出滾動(dòng)范圍的最大值。如果為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)前的滾動(dòng)值幽告,也就是
mScrollX
和mScrollY
的值。 - int minX,int maxX:標(biāo)示x方向的合理滾動(dòng)值
- int minY,int maxY:標(biāo)示y方向的合理滾動(dòng)值裆甩。
相信看完上述的API之后冗锁,大家會(huì)有很多的疑惑,所以這里我來舉個(gè)例子嗤栓。
?假設(shè)視圖大小為100*100冻河。當(dāng)你一直下拉到視圖上邊緣,然后在下拉茉帅,這時(shí)叨叙,mScrollY
已經(jīng)達(dá)到或者超過正常的滾動(dòng)范圍的最小值了,也就是0堪澎,但是你的maxOverScrollY傳入的是10,所以擂错,mScrollY
最小可以到達(dá)-10,最大可以為110。所以樱蛤,你可以繼續(xù)下拉钮呀。等到mScrollY
到達(dá)或者超過-10時(shí),clampedY就為true昨凡,標(biāo)示視圖已經(jīng)達(dá)到可以O(shè)verScroll的邊界爽醋,需要回滾到正常滾動(dòng)范圍,所以你調(diào)用springBack(0,0,0,100)便脊。
然后我們?cè)賮砜匆幌掳l(fā)光效果是如何實(shí)現(xiàn)的蚂四。
?使用EdgeEffect
類。一般來說哪痰,當(dāng)你只上下滾動(dòng)時(shí)遂赠,你只需要兩個(gè)EdgeEffect
實(shí)例,分別代表上邊界和下邊界的發(fā)光效果晌杰。你需要在下面兩個(gè)情景下改變EdgeEffect
的狀態(tài)解愤,然后在draw()
方法中繪制EdgeEffect
- 處理
ACTION_MOVE
時(shí),如果發(fā)現(xiàn)y方向的滾動(dòng)值超過了正常范圍的最小值時(shí)乎莉,你需要調(diào)用上邊界實(shí)例的onPull
方法送讲。如果是超過最大值,那么就是調(diào)用下邊界的onPull
方法惋啃。 - 在
computeScroll
函數(shù)中哼鬓,也就是說Fling手勢(shì)執(zhí)行過程中,如果發(fā)現(xiàn)y方向的滾動(dòng)值超過正常范圍時(shí)的最小值時(shí)边灭,調(diào)用onAbsorb
函數(shù)异希。
然后就是重載draw
方法,讓EdgeEffect
實(shí)例在畫布上繪制自己绒瘦。你會(huì)發(fā)現(xiàn)称簿,你必須對(duì)畫布進(jìn)行移動(dòng)或者旋轉(zhuǎn)來讓EdgeEffect
繪制出上邊界或者下邊界的發(fā)光的效果扣癣,因?yàn)?code>EdgeEffect對(duì)象自己是沒有上下左右的概念的。
@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)知道如何實(shí)現(xiàn)各類手勢(shì)父虑,但是對(duì)其中的機(jī)制和原理還不是很了解,之后的第三篇會(huì)講解從本篇代碼的視角講解一下android視圖繪制的原理和Scroller的機(jī)制授药,希望大家多多關(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