內(nèi)容是博主照著書(shū)敲出來(lái)的,博主碼字挺辛苦的,轉(zhuǎn)載請(qǐng)注明出處,后序內(nèi)容陸續(xù)會(huì)碼出。
當(dāng)了解了Android坐標(biāo)系和觸控事件后,我們?cè)賮?lái)看看如何使用系統(tǒng)提供的API來(lái)實(shí)現(xiàn)動(dòng)態(tài)地修改一個(gè)View的坐標(biāo),即實(shí)現(xiàn)滑動(dòng)效果。而不管采用哪一種方式,其實(shí)現(xiàn)的思想基本是一致的,當(dāng)觸摸View時(shí),系統(tǒng)記下當(dāng)前觸摸點(diǎn)坐標(biāo);當(dāng)手指移動(dòng)時(shí),系統(tǒng)記下移動(dòng)后的觸摸點(diǎn)坐標(biāo),從而獲取到相對(duì)于前一次坐標(biāo)點(diǎn)的偏移量,并通過(guò)偏移量來(lái)修改View的坐標(biāo),這樣不斷重復(fù),從而實(shí)現(xiàn)滑動(dòng)過(guò)程。
下面我們就通過(guò)一個(gè)實(shí)例,來(lái)看看 在Android中該如何實(shí)現(xiàn)滑動(dòng)效果。定義一個(gè)View,并置于一個(gè)LinearLayout中,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的布局,代碼如下所示。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.blankj.achievescroll.DragView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#ff09cfb1"/>
</LinearLayout>
我們的目的就是讓這個(gè)自定義View隨著手指在屏幕上的滑動(dòng)而滑動(dòng)。初始化時(shí)顯示效果如下圖所示。

layout方法
我們知道,在View進(jìn)行繪制時(shí),會(huì)調(diào)用onLayout()方法來(lái)設(shè)置顯示的位置。同樣,可以通過(guò)修改View的left,top,right,bottom四個(gè)屬性來(lái)控制View的坐標(biāo)。與前面提供的模板代碼一樣,在每次回調(diào)onTouchEvent的時(shí)候,我們都來(lái)獲取一下觸摸點(diǎn)的坐標(biāo),代碼如下所示。
int x = (int) event.getX();
int y = (int) event.getY();
接著,在ACTION_DOWN事件中記錄觸摸點(diǎn)的坐標(biāo),代碼如下所示。
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點(diǎn)坐標(biāo)
lastX = x;
lastY = y;
break;
最后,可以在ACTION_MOVE事件中計(jì)算偏移量,并將偏移量作用到Layout的left,top,right,bottom基礎(chǔ)上,增加計(jì)算出來(lái)的偏移量,代碼如下所示。
case MotionEvent.ACTION_MOVE:
// 計(jì)算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在當(dāng)前l(fā)eft、top、right、bottom的基礎(chǔ)上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
break;
這樣每次移動(dòng)后,View都會(huì)調(diào)用Layout方法來(lái)對(duì)自己重新布局,從而達(dá)到移動(dòng)View的效果。
在上面的代碼中,使用的是getX()、getY()方法來(lái)獲取坐標(biāo)值,即通過(guò)視圖坐標(biāo)來(lái)獲取偏移量。當(dāng)然,同樣可以使用getRawX()、getRawY()來(lái)獲取坐標(biāo),并使用絕對(duì)坐標(biāo)來(lái)計(jì)算偏移量,代碼如下所示。
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點(diǎn)坐標(biāo)
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
// 計(jì)算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
// 在當(dāng)前l(fā)eft、top、right、bottom的基礎(chǔ)上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
// 重新設(shè)置初始坐標(biāo)
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
使用絕對(duì)坐標(biāo)系,有一點(diǎn)非常需要注意的地方,就是在每次執(zhí)行完ACTION_MOVE的邏輯后,一定要重新設(shè)置初始坐標(biāo),這樣才能準(zhǔn)確地獲取偏移量,兩種方式的不同點(diǎn)一定要自己想清楚原因哦。
offsetLeftAndRight()與offsetTopAndBottom()
這個(gè)方法相當(dāng)于系統(tǒng)提供的一個(gè)對(duì)左右、上下移動(dòng)的API的封裝。當(dāng)計(jì)算出偏移量后,只需要使用如下代碼就可以完成View的重新布局,效果與使用Layout方法一樣,代碼如下所示。
// 同時(shí)對(duì)left和right進(jìn)行偏移
offsetLeftAndRight(offsetX);
// 同時(shí)對(duì)top和bottom進(jìn)行偏移
offsetTopAndBottom(offsetY);
這里的offsetX、offSetY與在Layout方法中計(jì)算offset方法一樣,這里就不重復(fù)了。
LayoutParams
LayoutParams保存了一個(gè)View的布局參數(shù)。因此可以在程序中,通過(guò)改變LayoutParams來(lái)動(dòng)態(tài)地修改一個(gè)布局的位置參數(shù),從而達(dá)到改變View位置的效果。我們可以很方便地在程序中使用getLayoutParams()來(lái)獲取一個(gè)View的LayouParams。當(dāng)然,計(jì)算偏移量的方法與在Layout方法中計(jì)算offset也是一樣。當(dāng)獲取到偏移量之后,就可以通過(guò)setLayoutParams來(lái)改變其LayoutParams,代碼如下所示。
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
不過(guò)這里需要注意的是,通過(guò)getLayoutParams()獲取LayoutParams時(shí),需要根據(jù)View所在父布局的類(lèi)型來(lái)設(shè)置不同的類(lèi)型,比如這里將View放在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。類(lèi)似地,如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。當(dāng)然,這一切的前提是你必須要有一個(gè)父布局,不然系統(tǒng)不法獲取LayoutParams。
在通過(guò)改變LayoutParams來(lái)改變一個(gè)View的位置時(shí),通常改變的是這個(gè)View的Margin屬性,所以除了使用布局的LayoutParams之后,還可以使用ViewGroup.MarginLayoutParams來(lái)實(shí)現(xiàn)這樣一個(gè)功能,代碼如下所示。
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
我們可以發(fā)現(xiàn),使用ViewGroup.MarginLayoutParams更加的方便,不需要考慮父布局的類(lèi)型,當(dāng)然他們的本質(zhì)都是一樣的。
scrollTo與scrollBy
在一個(gè)View中,系統(tǒng)提供了scrollTo、scrollBy兩種方式來(lái)改變一個(gè)View的位置。這兩個(gè)方法的區(qū)別非常好理解,與英文的To與By的區(qū)別類(lèi)似,scrollTo(x, y)表示移動(dòng)到一個(gè)具體的坐標(biāo)點(diǎn)(x, y),而scrollBy(dx, dy) 表示移動(dòng)的增量為dx、dy。
與前面幾種方式不同,在獲取偏移量后使用scrollBy來(lái)移動(dòng)View,代碼如下所示。
int offsetX = x - lastX;
int offsetY = y - lastY;
scrollBy(offsetX, offsetY);
但是,當(dāng)我們拖動(dòng)View的時(shí)候,你會(huì)發(fā)現(xiàn)View并沒(méi)有移動(dòng)!難道是我們方法寫(xiě)錯(cuò)了嗎?其實(shí),方法沒(méi)有寫(xiě)錯(cuò),View也確實(shí)移動(dòng)了,只是它移動(dòng)的并不是我們想要移動(dòng)的東西。scrollTo、scrollBy方法移動(dòng)的都View的content,即讓View的內(nèi)容移動(dòng),如果在ViewGroup中使用scrollTo、scrollBy方法,那么移動(dòng)的將是所有子View,但如果在View中使用,那么移動(dòng)的將是View的內(nèi)容,例如TextView,content就是它的文本;ImageView,content就是它的drawable對(duì)象。
相信通過(guò)上面的分析,讀者朋友應(yīng)該知道為什么不能在View中使用這個(gè)兩個(gè)方法來(lái)拖動(dòng)這個(gè)View了。那么我們就該View所在的ViewGroup中來(lái)視同scrollBy方法,移動(dòng)它的子View,代碼如下所示。
((View) getParent()).scrollBy(offsetX, offsetY);
但是,當(dāng)再次拖動(dòng)View的時(shí)候,你會(huì)發(fā)現(xiàn)View雖然移動(dòng)了,但卻在亂動(dòng),并不是我們想要的跟隨觸摸點(diǎn)的移動(dòng)而移動(dòng)。這里需要先了解一下視圖移動(dòng)的一些知識(shí)。大家在理解這個(gè)問(wèn)題的時(shí)候,不妨這樣想象手機(jī)屏幕是一個(gè)中空的蓋板,蓋板下面是一個(gè)巨大的畫(huà)布,也就是我們想要顯示的視圖。當(dāng)把這個(gè)蓋板蓋在畫(huà)布上的某一處時(shí),透過(guò)中間空的矩形,我們看見(jiàn)了手機(jī)屏幕上的視圖,而畫(huà)布在其他地方的視圖,則被蓋板蓋住了無(wú)法看見(jiàn)。我們的視圖與這個(gè)例子非常類(lèi)似,我們沒(méi)有看見(jiàn)視圖,并不代表它就不存在,有可能只是在屏幕外面而已。當(dāng)調(diào)用scrollBy方法時(shí),可以想象為外面的蓋板在移動(dòng),這么說(shuō)比較抽象,來(lái)看一個(gè)具體的例子,如下圖所示。

在上圖中,中間的矩形相當(dāng)于屏幕,即可視區(qū)域。后面的content就相當(dāng)于畫(huà)布,代表視圖。大家可以看到,只有視圖的中間部分目前是可視的,其他部分都不可見(jiàn)。在可見(jiàn)區(qū)域中,我們?cè)O(shè)置了一個(gè)Button,它的坐標(biāo)是(20,10)。
下面使用scrollBy方法,將蓋板(屏幕、可視區(qū)域),在水平方向上向X軸正方向(右方)平移20,在豎直方向上向Y軸正方向(下方)平移10,那么平移后的可視區(qū)域如下圖所示。

我們可以發(fā)現(xiàn),雖然設(shè)置scrollBy(20, 10),偏移量均為X軸、Y軸正方向上的正數(shù),但是在屏幕的可視區(qū)域內(nèi),Button卻向X軸、Y軸負(fù)方向上移動(dòng)了。這就是因?yàn)閰⒖枷颠x擇的不同,而產(chǎn)生的不同效果。
通過(guò)上面的分析可以發(fā)現(xiàn),如果將scrollBy中的參數(shù)dx和dy設(shè)置為正數(shù),那么content將向坐標(biāo)軸負(fù)方向移動(dòng);如果將scrollBy中的參數(shù)dx和dy設(shè)置為負(fù)數(shù),那么content將向坐標(biāo)軸正方向移動(dòng)。因此回到前面的例子,要實(shí)現(xiàn)跟隨手指移動(dòng)而滑動(dòng)的效果,就必須將偏移量改為負(fù)值,代碼如下所示。
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
再去試驗(yàn)一下,大家就可以發(fā)現(xiàn),效果與前面幾種方式的效果相同了。類(lèi)似地,在使用絕對(duì)坐標(biāo)時(shí),也可以通過(guò)scrollTo方法來(lái)實(shí)現(xiàn)這一效果。
Scroller
既然提到了scrollTo、scrollBy方法,就不得不再來(lái)說(shuō)一說(shuō)Scroller類(lèi)。Scroller類(lèi)與scrollTo、scrollBy方法十分相似,有著千絲萬(wàn)縷的聯(lián)系。那么它們之間具體有什么區(qū)別呢?要解答這個(gè)問(wèn)題,首先來(lái)看一個(gè)小例子。假如要完成這樣一個(gè)效果:通過(guò)點(diǎn)擊按鈕,讓一個(gè)ViewGroup的子View向右移動(dòng)100個(gè)像素。問(wèn)題看似非常簡(jiǎn)單,只要在按鈕的點(diǎn)擊事件中使用前面講的scrollBy方法設(shè)置下偏移量不就可以了嗎?的確,通過(guò)這樣一個(gè)方法可以讓ViewGroup中的子View平移。但是讀者朋友可以發(fā)現(xiàn),不管使用scrollTo還是scrollBy方法,子View的平移都是瞬間發(fā)生的,在事件執(zhí)行的時(shí)候平移就已經(jīng)完成了,這樣的效果會(huì)讓人感覺(jué)非常突兀。Google建議使用自然的過(guò)度動(dòng)畫(huà)來(lái)實(shí)現(xiàn)移動(dòng)效果,當(dāng)然也要遵循這一原則。因此,Scroller類(lèi)就這樣應(yīng)運(yùn)而生了,通過(guò)Scroller類(lèi)可以實(shí)現(xiàn)平滑移動(dòng)的效果,而不再是瞬間完成的移動(dòng)。
說(shuō)到Scroller類(lèi)的實(shí)現(xiàn)原理,其實(shí)它與前面使用scrollTo和scrollBy方法來(lái)實(shí)現(xiàn)子View跟隨手指移動(dòng)的原理基本類(lèi)似。雖然scrollBy方法是讓子View瞬間從某點(diǎn)移動(dòng)到另一個(gè)點(diǎn),但是由于在ACTION_MOVE事件中不斷獲取手指移動(dòng)的微小的偏移量,這樣就將一段距離劃分成了N個(gè)非常小的偏移量。雖然在每個(gè)偏移量里面,通過(guò)scrollBy方法進(jìn)行了瞬間移動(dòng),但是在整體上卻可以獲得一個(gè)平滑移動(dòng)的效果。這個(gè)原理與動(dòng)畫(huà)的實(shí)現(xiàn)原理基本類(lèi)似,他們都是利用了人眼的視覺(jué)暫留特性。
下面我們就演示一下如何使用Scroller類(lèi)實(shí)現(xiàn)平滑移動(dòng)。在這個(gè)實(shí)例中,同樣讓子View跟隨手指的滑動(dòng)而滑動(dòng),但是在手指離開(kāi)屏幕時(shí),讓子View平滑地移動(dòng)到初始位置,即屏幕左上角。一般情況下,使用Scroller類(lèi)需要如下三個(gè)步驟。
◆ 初始化Scroller
首先,通過(guò)它的構(gòu)造方法來(lái)創(chuàng)建一個(gè)Scroller對(duì)象,代碼如下所示。
// 初始化Scroller
mScroller = new Scroller(context);
◆ 重寫(xiě)computeScroll()方法,實(shí)現(xiàn)模擬滾動(dòng)
下面我們需要重寫(xiě)computeScroll()方法,它是使用Scroller類(lèi)的核心,系統(tǒng)在繪制View的時(shí)候會(huì)在draw()方法中調(diào)用該方法。這個(gè)方法實(shí)際上就是使用scrollTo方法。再結(jié)合Scroller對(duì)象,幫助獲取到當(dāng)前的滾動(dòng)值。我們可以通過(guò)不斷地瞬間移動(dòng)一個(gè)小的距離來(lái)實(shí)現(xiàn)整體上的平滑移動(dòng)效果。通常情況下,computeScroll的代碼可以利用如下模板代碼來(lái)實(shí)現(xiàn)。
@Override
public void computeScroll() {
super.computeScroll();
// 判斷Scroller是否執(zhí)行完畢
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY());
// 通過(guò)重繪來(lái)不斷調(diào)用computeScroll
invalidate();
}
}
Scroller類(lèi)提供了computeScrollOffset()方法來(lái)判斷是否完成了整個(gè)滑動(dòng),同時(shí)也提供了getCurrX()、getCurrY()方法來(lái)獲取當(dāng)前滑動(dòng)坐標(biāo)。在上面的代碼中,唯一需要注意的是invalidate()方法,因?yàn)橹荒茉赾omputeScroll()方法中獲取模擬過(guò)程中的scrollX和scrollY坐標(biāo)。但computeScroll()方法是不會(huì)自動(dòng)調(diào)用的,只能通過(guò)invalidate()→draw()→computeScroll()來(lái)間接調(diào)用computeScroll()方法,所以需要在模板代碼中調(diào)用invalidate()方法,實(shí)現(xiàn)循環(huán)獲取scrollX和scrollY的目的。而當(dāng)模擬過(guò)程結(jié)束后,scroller.computeScrollOffset()方法會(huì)返回false,從而中斷循環(huán),完成整個(gè)平滑移動(dòng)過(guò)程。
◆ startScroll開(kāi)啟模擬過(guò)程
最后,萬(wàn)事俱備只欠東風(fēng)。我們?cè)谛枰褂闷交苿?dòng)事件中,使用Scroller類(lèi)的startScroll()方法來(lái)開(kāi)啟平滑移動(dòng)過(guò)程。startScroll()方法具有兩個(gè)重載方法。
public void startScroll(int startX, int startY, int dx, int dy, int duration)
public void startScroll(int startX, int startY, int dx, int dy)
可以看到他們的區(qū)別就是一個(gè)具有指定的持續(xù)時(shí)長(zhǎng),而另一個(gè)沒(méi)有。這個(gè)非常好理解,與在動(dòng)畫(huà)中設(shè)置durarion和使用默認(rèn)的顯示時(shí)長(zhǎng)是一個(gè)道理。而其他四個(gè)坐標(biāo),則與它們的命名含義相同,就是起始坐標(biāo)與偏移量。在獲取坐標(biāo)時(shí),通常可以使用getScrollX()和getScrollerY()方法來(lái)獲取父視圖中content所滑動(dòng)到的電的坐標(biāo),不過(guò)要注意的是這個(gè)值的正負(fù),它與在scrollBy和scrollTo中講解的情況是一樣的。
通過(guò)上面三個(gè)步驟,我們就可以使用Scroller類(lèi)來(lái)實(shí)現(xiàn)平滑移動(dòng)了,下面回到實(shí)例中,在構(gòu)造方法中初始化Scroller對(duì)象,并重寫(xiě)View的computeScroll()方法。最后,需要監(jiān)聽(tīng)手指離開(kāi)屏幕的事件,并在該事件中通過(guò)調(diào)用startScroll()方法完成平滑移動(dòng)。那么要監(jiān)聽(tīng)手指離開(kāi)屏幕的事件,只需要在onTouchEvent中增加一個(gè)ACTION_UP監(jiān)聽(tīng)選項(xiàng)即可,代碼如下所示。
case MotionEvent.ACTION_UP:
// 手指離開(kāi)時(shí),執(zhí)行滑動(dòng)過(guò)程
View viewGroup = ((View) getParent());
mScroller.startScroll(
viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY());
invalidate();
在startScroll()方法中,我們獲取子View移動(dòng)的距離——getScrollX()、getScrollY(),并將偏移量設(shè)置為其相反數(shù),從而將子View滑動(dòng)到原來(lái)位置。這里需要注意的還是invalidate()方法,需要使用這個(gè)方法來(lái)通知View進(jìn)行重繪,從而來(lái)調(diào)用conputeScroll()的模擬過(guò)程。當(dāng)然,也可以給startScroll()方法增加一個(gè)duration的參數(shù)來(lái)設(shè)置滑動(dòng)的持續(xù)時(shí)長(zhǎng)。
屬性動(dòng)畫(huà)
為視圖增加位移動(dòng)畫(huà),視圖進(jìn)行位移偏移后,利用視圖動(dòng)畫(huà)在松手后視圖回到原處,具體代碼如下所示。
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點(diǎn)坐標(biāo)
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計(jì)算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;// 同時(shí)對(duì)left和right進(jìn)行偏移
offsetLeftAndRight(offsetX);
// 同時(shí)對(duì)top和bottom進(jìn)行偏移
offsetTopAndBottom(offsetY);
break;
case MotionEvent.ACTION_UP:
// 手指離開(kāi)時(shí),執(zhí)行滑動(dòng)過(guò)程
ObjectAnimator animator1 = ObjectAnimator.ofFloat(this, "translationX", -getLeft());
ObjectAnimator animator2 = ObjectAnimator.ofFloat(this, "translationY", -getTop());
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2);
set.start();
break;
}
return true;
}
ViewDragHelper
Google在其support庫(kù)中為我們提供了DrawerLayout和SlidingPaneLayout兩個(gè)布局來(lái)幫助開(kāi)發(fā)者實(shí)現(xiàn)側(cè)邊欄滑動(dòng)的效果。這兩個(gè)新的布局,大大方便了我們創(chuàng)建自己的滑動(dòng)布局界面。然而,這兩個(gè)功能強(qiáng)大的布局背后,卻隱藏著一個(gè)鮮為人知卻功能強(qiáng)大的類(lèi)——ViewDragHelper。通過(guò)ViewDragHelper,基本可以實(shí)現(xiàn)各種不同的滑動(dòng)、拖放需求,因此這個(gè)方法也是各種滑動(dòng)方案中的終極絕招。
ViewDragHelper雖然功能強(qiáng)大,但其使用方法也是這次最復(fù)雜的。讀者朋友需要在理解ViewDragHelper基本使用方法的基礎(chǔ)上,通過(guò)不斷練習(xí)來(lái)掌握它的技巧。下面通過(guò)一個(gè)實(shí)例,來(lái)演示一下如何使用ViewDragHelper創(chuàng)建一個(gè)滑動(dòng)布局。在這個(gè)例子中,準(zhǔn)備實(shí)現(xiàn)類(lèi)似QQ滑動(dòng)側(cè)邊欄的布局,初始時(shí)顯示內(nèi)容界面,當(dāng)用戶(hù)手指滑動(dòng)超過(guò)一段距離時(shí),內(nèi)容界面?zhèn)然@示菜單界面,整個(gè)過(guò)程如下圖所示。

下面來(lái)看具體的代碼是如何實(shí)現(xiàn)的。
◆ 初始化ViewDragHelper
首先,自然是需要初始化ViewDragHelper。ViewDragHelper通常定義在一個(gè)ViewGroup的內(nèi)部,并通過(guò)其靜態(tài)工廠方法進(jìn)行初始化,代碼如下所示。
mViewDragHelper = ViewDragHelper.create(this, callback);
它的第一個(gè)參數(shù)是要監(jiān)聽(tīng)的View,通常需要是一個(gè)ViewGroup,即parentView;第二個(gè)參數(shù)是一個(gè)Callback回調(diào),這個(gè)回調(diào)就是整個(gè)ViewDragHelper的邏輯核心,后面再來(lái)詳細(xì)講解。
◆ 攔截事件
接下來(lái),要重寫(xiě)事件攔截方法,將事件傳遞給ViewDragHelper進(jìn)行處理,代碼如下所示。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 將觸摸事件傳遞給ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
這一點(diǎn)我們?cè)谥vAndroid事件機(jī)制的時(shí)候已經(jīng)進(jìn)行了詳細(xì)講解,這里就不再重復(fù)了。
◆ 處理computeScroll
沒(méi)錯(cuò),使用ViewDragHelper同樣需要重寫(xiě)下computeScroll()方法,因?yàn)閂iewDragHelper內(nèi)部也是通過(guò)Scroller來(lái)實(shí)現(xiàn)平滑移動(dòng)的。通常情況下,可以使用如下所示的模板代碼。
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
◆ 處理回調(diào)Callback
下面就是最關(guān)鍵的Callback實(shí)現(xiàn),通過(guò)如下所示代碼來(lái)創(chuàng)建一個(gè)ViewDragHelper.Callback。
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
};
IDE自動(dòng)幫我們重寫(xiě)了一個(gè)方法——tryCaptureView()。通過(guò)這個(gè)方法,我們可以指定在創(chuàng)建ViewDragHelper時(shí),參數(shù)parentView中哪一個(gè)子View可以被移動(dòng),例如在這個(gè)實(shí)例中自定義了一個(gè)ViewGroup,里面定義了兩個(gè)子View——MenuView和MainView,當(dāng)指定如下代碼時(shí),則只有MainView是可以被拖動(dòng)的。
// 何時(shí)開(kāi)始檢測(cè)觸摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 如果當(dāng)前觸摸的child是mMainView時(shí)開(kāi)始檢測(cè)
return mMainView == child;
}
下面來(lái)看具體的滑動(dòng)方法——clampViewPositionVertical()和clampViewPositionHorizontal(),分別對(duì)應(yīng)垂直和水平方向上的滑動(dòng)。如果要實(shí)現(xiàn)滑動(dòng)效果,那么這兩個(gè)方法是必須要重寫(xiě)的。因?yàn)樗J(rèn)的返回值是0,即不發(fā)生滑動(dòng)。當(dāng)然,如果只重寫(xiě)clampViewPositionVertical()或clampViewPositionHorizontal()中的一個(gè),那么就只會(huì)實(shí)現(xiàn)該方向上的滑動(dòng)效果了,代碼如下所示。
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
clampViewPositionVertical(View child, int top, int dy)中的參數(shù)top,代表在垂直方向上child移動(dòng)的距離,而dy則表示比較前一次的增量。同理,clampViewPositionHorizontal(View child, int left, int dx)也是類(lèi)似的含義。通常情況下,只需要返回top和left即可,但當(dāng)需要更加精確地計(jì)算padding等屬性的時(shí)候,就需要對(duì)left進(jìn)行一些處理,并返回合適大小的值。
僅僅是通過(guò)重寫(xiě)上面的這三個(gè)方法,就可以實(shí)現(xiàn)一個(gè)最基本的滑動(dòng)效果了。當(dāng)用手拖動(dòng)MainView的時(shí)候,它就可以跟隨手指的滑動(dòng)而滑動(dòng)了,代碼如下所示。
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 何時(shí)開(kāi)始檢測(cè)觸摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 如果當(dāng)前觸摸的child是mMainView時(shí)開(kāi)始檢測(cè)
return mMainView == child;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
};
下面繼續(xù)來(lái)優(yōu)化這個(gè)實(shí)例。在講解Scroller類(lèi)時(shí),曾實(shí)現(xiàn)了這樣一個(gè)效果——在手指離開(kāi)屏幕后,子View滑動(dòng)回初始位置。當(dāng)時(shí)我們是通過(guò)監(jiān)聽(tīng)ACTION_UP事件,并通過(guò)調(diào)用Scroller類(lèi)來(lái)實(shí)現(xiàn)的,這里使用ViewDragHelper來(lái)實(shí)現(xiàn)這樣的效果。在ViewDragHelper.Callback中,系統(tǒng)提供了這樣的方法——onViewReleased(),通過(guò)重寫(xiě)這個(gè)方法,可以非常簡(jiǎn)單地實(shí)現(xiàn)當(dāng)手指離開(kāi)屏幕后實(shí)現(xiàn)的操作。當(dāng)然,這個(gè)方法內(nèi)部是通過(guò)Scroller類(lèi)來(lái)實(shí)現(xiàn)的,這也是前面重寫(xiě)computeScroll()方法的原因,這部分代碼如下所示。
// 拖動(dòng)結(jié)束后調(diào)用
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
// 手指抬起后緩慢移動(dòng)到指定位置
if (mMainView.getLeft() < 500) {
// 關(guān)閉菜單
// 相當(dāng)于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
} else {
// 打開(kāi)菜單
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
}
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
設(shè)置讓MainView移動(dòng)后左邊距小于500像素的時(shí)候,就是用smoothSlideViewTo()方法來(lái)將MainView還原到初始狀態(tài),即坐標(biāo)為(0, 0)的點(diǎn)。而當(dāng)其左邊距大于500的時(shí)候,則將MainView移動(dòng)到(300, 0)坐標(biāo),即顯示MenuView。讀者朋友可以發(fā)現(xiàn)如下所示的這兩行代碼,與在使用Scroller類(lèi)的時(shí)候使用的startScroll()方法是不是非常像呢?
// ViewDragHelper
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
// Scroll
mScroller.startScroll(x, y, dx, dy);
invalidate();
通過(guò)前面一步步的分析,現(xiàn)在要實(shí)現(xiàn)類(lèi)似QQ側(cè)滑菜單的效果,是不是就非常簡(jiǎn)單了呢?下面自定義一個(gè)ViewGroup來(lái)完成整個(gè)實(shí)例的編寫(xiě)?;瑒?dòng)的處理部分前面已經(jīng)講解過(guò)了,在自定義ViewGroup的onFInishInflate()方法中,按順序?qū)⒆覸iew分別定義成MenuView和MainView,并在onSizeChanged()方法中獲得View的寬度。如果你需要根據(jù)View的寬度來(lái)處理滑動(dòng)后的效果,就可以使用這個(gè)值來(lái)進(jìn)行判斷。這部分代碼如下所示。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
}
最后,整個(gè)通過(guò)ViewDragHelper實(shí)現(xiàn)QQ側(cè)滑功能的完整代碼參考項(xiàng)目地址即可。
當(dāng)然,這里只是非常簡(jiǎn)單地模擬了QQ側(cè)滑菜單這個(gè)功能。ViewDragHelper的很多強(qiáng)大功能還沒(méi)能夠得到展示。在ViewDragHelper.Callback中,系統(tǒng)定義了大量的監(jiān)聽(tīng)事件來(lái)幫助我們吹各種事件,下面就列舉一些事件。
◆ onViewCaptured()
這個(gè)事件在用戶(hù)觸摸到View后調(diào)用。
◆ onViewDragStateChanged()
這個(gè)事件在拖拽狀態(tài)改變時(shí)回調(diào),比如idle,dragging等狀態(tài)。
◆ onViewPositionChanged()
這個(gè)事件在位置改變時(shí)回調(diào),常用于滑動(dòng)時(shí)更改scale進(jìn)行縮放等效果。
這個(gè)ViewDragHelper可以幫助我們非常好地處理程序中的滑動(dòng)效果。但同時(shí)ViewDragHelper的使用也比較復(fù)雜,需要開(kāi)發(fā)者對(duì)事件攔截、滑動(dòng)處理都有比較清楚的認(rèn)識(shí)。所以建議初學(xué)者循序漸進(jìn),在掌握前面幾種解決方案的基礎(chǔ)上,再來(lái)學(xué)習(xí)ViewDragHelper,以實(shí)現(xiàn)更加豐富的滑動(dòng)效果。
項(xiàng)目地址→AchieveScroll
原文地址實(shí)現(xiàn)滑動(dòng)的七種方法(Android群英傳)
我的自媒體博客blankj小站(OJ、LeetCode、Android開(kāi)發(fā)),歡迎來(lái)逛逛。