Android中怎么實(shí)現(xiàn)滑動,相信很多沒有經(jīng)驗(yàn)的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
我們提供的服務(wù)有:成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè)、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、貢井ssl等。為上千家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的貢井網(wǎng)站制作公司
(1)、Android的坐標(biāo)系
Android中將屏幕最左上角的頂點(diǎn)作為Android坐標(biāo)系的原點(diǎn),從這個(gè)點(diǎn)向右是X軸正方向,從這個(gè)點(diǎn)向下是Y軸正方向,如下圖:
系統(tǒng)提供了getLocationOnScreen(int location[])這樣的方法來獲取Android坐標(biāo)系中點(diǎn)的位置,即該視圖左上角在Android坐標(biāo)系中的坐標(biāo)。在觸控事件中使用getRawX()、getRawY()方法所獲得的坐標(biāo)同樣是Android坐標(biāo)系中的坐標(biāo)。
(2)、視圖坐標(biāo)系
Android中除了上面所說的這種坐標(biāo)系之外,還有一個(gè)視圖坐標(biāo)系,它描述了子視圖在父視圖中的位置關(guān)系。這兩種坐標(biāo)系并不矛盾也不復(fù)雜,他們的作用是相互相成的。與Android坐標(biāo)系類似,視圖坐標(biāo)系同樣是以原點(diǎn)向右為X軸正方向,以原點(diǎn)向下為Y軸正方向,只不過在視圖坐標(biāo)系中,原點(diǎn)不再是Android坐標(biāo)系中的屏幕最左上角,而是以父視圖左上角為坐標(biāo)原點(diǎn),如下圖:
在觸控事件中,通過getX()、getY()所獲得的坐標(biāo)系就是視圖坐標(biāo)系中的坐標(biāo)。
(3)、觸控事件——MotionEvent
觸控事件MotionEvent在用戶交互中,占著舉足輕重的地位。首先看看MotionEvent封裝的一些常用事件常量,定義了觸控事件的不同類型。
//單點(diǎn)觸摸按下動作 public static final int ACTION_DOWN = 0; //單點(diǎn)觸摸離開動作 public static final int ACTION_UP = 1; //觸摸點(diǎn)移動動作 public static final int ACTION_MOVE = 2; //觸摸動作取消 public static final int ACTION_CANCEL = 3; //觸摸動作超出邊界 public static final int ACTION_OUTSIDE = 4; //多點(diǎn)觸摸按下動作 public static final int ACTION_POINTER_DOWN = 5; //多點(diǎn)離開動作 public static final int ACTION_POINTER_UP = 6;
通常情況會在onTouchEvent(MotionEvent event)方法中通過event.getAction()方法來獲取觸控事件的類型,并使用switch-case方法來進(jìn)行篩選,這個(gè)代碼的模式基本固定:
@Override public boolean onTouchEvent(MotionEvent event) { //獲取當(dāng)前輸入點(diǎn)的X、Y坐標(biāo)(視圖坐標(biāo)) int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //處理按下事件 break; case MotionEvent.ACTION_MOVE: //處理移動事件 break; case MotionEvent.ACTION_UP: //處理離開事件 break; } return true; }
在不涉及多點(diǎn)操作的情況下,通??梢允褂靡陨洗a來完成觸控事件的監(jiān)聽。
在Android中系統(tǒng)提供了非常多的方法來獲取坐標(biāo)值、相對距離等。方法豐富固然好,下面對坐標(biāo)系的API進(jìn)行總結(jié),如下圖:
這些方法可以分為如下兩個(gè)類別:
View提供的獲取坐標(biāo)方法
getTop():獲取到的是View自身的頂邊到其父布局頂邊的距離。
getLeft():獲取到的是View自身的左邊到其父布局最左邊的距離。
getRight():獲取到的是View自身的右邊到其父布局左邊的距離。
getBottom():獲取到的是View自身的底邊到其父布局頂邊的距離。
MotionEvent提供的方法
getX():獲取點(diǎn)擊事件距離空間左邊的距離,即視圖坐標(biāo)。
getY():獲取點(diǎn)擊事件距離控件頂邊的距離,即視圖坐標(biāo)。
getRawX():獲取點(diǎn)擊事件距離整個(gè)屏幕左邊的距離,即絕對坐標(biāo)。
getRawY():獲取點(diǎn)擊事件距離整個(gè)屏幕頂邊的距離,即絕對坐標(biāo)。
二、實(shí)現(xiàn)滑動的七種方式
當(dāng)了解Android坐標(biāo)系和觸控事件后,我們再來看看如何使用系統(tǒng)提供的API來實(shí)現(xiàn)動態(tài)地修改一個(gè)View坐標(biāo),即實(shí)時(shí)滑動效果。而不管采用哪一種方式,其實(shí)現(xiàn)的思想基本是一致的,當(dāng)觸摸View時(shí),系統(tǒng)記下當(dāng)前觸摸點(diǎn)坐標(biāo),當(dāng)手指移動時(shí),系統(tǒng)記下移動后的觸摸點(diǎn)坐標(biāo),從而獲取到相對于前一次坐標(biāo)點(diǎn)的偏移量,并通過偏移量來修改View的坐標(biāo),這樣不斷重復(fù),實(shí)現(xiàn)滑動過程。
通過一個(gè)實(shí)例看看Android中該如何實(shí)現(xiàn)滑動效果,定義一個(gè)View,處于LinearLayout中,實(shí)現(xiàn)一個(gè)簡單布局:
我們的目的就是讓這個(gè)自定義的View隨著手指在屏幕上的滑動而滑動。初始化時(shí)顯示效果:
(1)、layout方法
在View繪制時(shí),會調(diào)用onLayout()方法來設(shè)置顯示的位置。同樣,可以通過修改View的left,top,right,bottom四個(gè)屬性來控制View的坐標(biāo)。與前面提供的模板代碼一樣,在每次回調(diào)onTouchEvent的時(shí)候,我們都來獲取一下觸摸點(diǎn)的坐標(biāo),代碼如下:
//獲取當(dāng)前輸入點(diǎn)的X、Y坐標(biāo)(視圖坐標(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方法中,在目前Layout的left,top,right,bottom基礎(chǔ)上,增加計(jì)算出來的偏移量,代碼如下所示:
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;
這樣沒錯(cuò)移動后,View都會調(diào)用Layout方法來對自己重新布局,從而達(dá)到移動View的效果。
上面的代碼中,使用的是getX()、getY()方法來獲取坐標(biāo)值,即通過視圖坐標(biāo)來獲取偏移量。當(dāng)然,同樣可以使用getRawX()、getRawY()來獲取坐標(biāo),并使用絕對坐標(biāo)來計(jì)算偏移量,代碼如下:
// 視圖坐標(biāo)方式 @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getRawX(); int y = (int) event.getRawY(); 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; // 在當(dāng)前l(fā)eft、top、right、bottom的基礎(chǔ)上加上偏移量 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); //重新設(shè)置初始化坐標(biāo) lastX = x; lastY = y; break; } return true; }
使用絕對坐標(biāo)系,有一點(diǎn)非常需要注意的地方,就是在每次執(zhí)行完ACTION_MOVE的邏輯后,一定要重新設(shè)置初始化坐標(biāo),這樣才能準(zhǔn)確地獲取偏移量。
(2)、offsetLeftAndRight()與offsetTopAndBottom()
這個(gè)方法相當(dāng)于系統(tǒng)提供的一個(gè)對左右、上下移動的API的封裝。當(dāng)計(jì)算出偏移量后,只需要使用如下代碼就可以完成View的重新布局,效果與使用Layout方法一樣,代碼如下所示:
//同時(shí)對left和right進(jìn)行偏移 offsetLeftAndRight(offsetX); //同時(shí)對top和bottom進(jìn)行偏移 offsetTopAndBottom(offsetY);
這里的offsetX、offsetY與在layout方法中計(jì)算offset方法一樣。
(3)、LayoutParams
LayoutParams保存了一個(gè)View的布局參數(shù)。因此可以在程序中,通過改變LayoutParams來動態(tài)地修改一個(gè)布局的位置參數(shù),從而達(dá)到改變View位置的效果。我們可以很方便在程序中使用getLayoutParams()來獲取一個(gè)View的LayoutParams。當(dāng)然,計(jì)算偏移量的方法與在Layout方法中計(jì)算offset也是一樣。當(dāng)獲取到偏移量之后,就可以通過setLayoutParams來改變其LayoutParams,代碼如下:
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams);
這里getLayoutParams()獲取LayoutParams時(shí),需要根據(jù)View所在View父布局的類型來設(shè)置不同的類型,比如這里將View放在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。這一切的前提是你必須要有一個(gè)父布局,不然系統(tǒng)無法獲取LayoutParams。
在通過改變LayoutParams來改變一個(gè)View的位置時(shí),通常改變的是這個(gè)View的Margin屬性,所以除了使用布局的LayoutParams之外,還可以使用ViewGroup.MarginLayoutParams來實(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更加的方便,不需要考慮父布局的類型,當(dāng)然它們的本質(zhì)都是一樣。
(4)、scrollTo與scrollBy
在一個(gè)View中,系統(tǒng)提供了scrollTo、scrollBy兩種方式來改變一個(gè)View的位置。這兩個(gè)方法的區(qū)別非常好理解,與英文中To與By的區(qū)別類似,scrollTo(x,y)表示移動到一個(gè)具體的坐標(biāo)點(diǎn)(x,y),而scrollBy(dx,dy)表示移動的增量為dx,dy。
與前面幾種方式相同,在獲取偏移量后使用scrollBy來移動View,代碼如下:
int offsetX = x - lastX; int offsetY = y - lastY; scrollBy(offsetX, offsetY);
但是,當(dāng)我們拖動View的時(shí)候,你會發(fā)現(xiàn)View并沒有移動,其實(shí)方法沒錯(cuò),View確實(shí)移動了,只是移動的并不是我們想要的東西。scrollTo、scrollBy方法移動的是View的content,即讓View的內(nèi)容移動,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移動的將是所有子View,如果在View中使用,那么移動的將是View的內(nèi)容,例如TextView,content就是它的文本,ImageView,content就是它的drawable對象。
通過以上的分析,現(xiàn)在知道為什么不能再View中使用這兩個(gè)方法來拖動這個(gè)View了。那么我們就該View所在的ViewGroup中來使用scrollBy方法,移動它的子View,代碼如下:
((View) getParent()).scrollBy(offsetX, offsetY);
但是再次拖動View的時(shí)候,你會發(fā)現(xiàn)View雖然移動了,但卻在亂動,并不是我們想要的跟隨觸摸點(diǎn)的移動而移動。這里先看一下視圖移動,不妨這樣想象一下手機(jī)屏幕是一個(gè)中空的蓋板,蓋板下面是一個(gè)巨大的畫布,也就是我們想要顯示的視圖。當(dāng)把這個(gè)蓋板蓋在畫布上的某一處時(shí),透過中間空的矩形,我們看見了手機(jī)屏幕上顯示的視圖,而畫布上其他地方的視圖,則被蓋板蓋住了無法看見。我們的視圖與這個(gè)例子非常類似,我們沒有看見視圖,并不代表它就不存在,有可能只是在屏幕外面而已。當(dāng)調(diào)用scrollBy方法時(shí),可以想象為外面的蓋板在移動,這么說比較抽象。
下圖一中間的矩形相當(dāng)于屏幕,及可視區(qū)域。后面的content就相當(dāng)于畫布,代表視圖??梢钥吹?,只有視圖的中間部分目前是可視的,其他部分都不可見。在可見區(qū)域中,我們設(shè)置了一個(gè)Button,它的坐標(biāo)為(20,10)。
下面使用scrollBy方法,將蓋板(屏幕、可視區(qū)域),在水平方向上向X軸正方向(向右)平移20,在豎直方向上向Y軸正方向(下方)平移10,那么平移之后的可視區(qū)域如圖二。
圖一
圖二、移動之后的可視區(qū)域
我們發(fā)現(xiàn),雖然設(shè)置scrollBy(20,10),偏移量均為X軸、Y軸正方向上的正數(shù),但是在屏幕的可視區(qū)域內(nèi),Button卻向X軸、Y軸負(fù)方向上移動了。這就是因?yàn)閰⒖枷颠x擇的不同,而產(chǎn)生的不同效果。
通過上面的分析可以發(fā)現(xiàn),如果講scrollBy中的參數(shù)dx和dy設(shè)置為正數(shù),那么content講向坐標(biāo)軸負(fù)方向移動,如果將scrollBy中的參數(shù)dx和dy設(shè)置為負(fù)數(shù),那么content將向坐標(biāo)軸正方向移動,因此回到前面的例子,要實(shí)現(xiàn)跟隨著手指移動而滑動的效果,就必須將偏移量改為負(fù)值,代碼如下:
int offsetX = x - lastX; int offsetY = y - lastY; ((View) getParent()).scrollBy(-offsetX, -offsetY);
現(xiàn)在在運(yùn)行一次發(fā)現(xiàn)和前面幾種方式效果相同了,類似地使用絕對坐標(biāo)時(shí),也可以通過使用scrollTo發(fā)方法來實(shí)現(xiàn)這一效果。
(5)、Scroller
前面提到了scrollBy、scrollTo方法,就不得不再來說一說Scroller類。Scroller類與scrollBy、scrollTo方法十分相似。什么區(qū)別?先看例子,如果要完成這樣一個(gè)效果;通過點(diǎn)擊按鈕,讓一個(gè)ViewGroup的子View向右移動100個(gè)像素。問題看起來很簡單,只要在按鈕的點(diǎn)擊事件中使用前面的scrollBy方法設(shè)置下偏移量就可以了嗎?確實(shí)這樣可以讓一個(gè)子ViewGroup中的子View平移,但是不管使用scrollBy還是scrollTo方法,子view的平移都是瞬間發(fā)生的,在事件執(zhí)行的時(shí)候平移就已經(jīng)完成了,這樣的效果會讓人感覺非常突然,Google建議使用自然的過度動畫來實(shí)現(xiàn)移動效果。因此Scroller類就這樣誕生了,通過Scroller類可以實(shí)現(xiàn)平滑移動的效果,而不是瞬間就完成移動。
Scroller類的實(shí)現(xiàn)原理,其實(shí)它與前面使用的scrollTo和scrollBy方法來實(shí)現(xiàn)子View跟隨手指移動的原理基本類似,雖然scrollBy芳芳法是讓子View瞬間從某點(diǎn)移動到另一個(gè)點(diǎn),但是由于在ACTION_MOVE事件中不斷獲取手指移動的微小的偏移量,這樣就將一段距離劃分成了N個(gè)非常小的偏移量。雖然每個(gè)偏移量里面,通過scrollBy方法進(jìn)行了瞬間移動,但是在整體上卻可以獲得一個(gè)平滑移動的效果。這個(gè)原理與動畫的實(shí)現(xiàn)原理也是基本類似的,它們都是利用了人眼的視覺暫留特性。
下面我們使用Scroller類實(shí)現(xiàn)平滑移動,在這個(gè)實(shí)例中,同樣讓子View跟隨手指的滑動而滑動,但是在手指離開屏蔽時(shí),讓子View平滑的移動到初始化位置,即屏幕左上角。使用Scroller類需要如下三個(gè)步驟:
初始化Scroller
首先通過它的構(gòu)造方法來創(chuàng)建一個(gè)Scroller對象,代碼如下所示:
// 初始化Scroller mScroller = new Scroller(context);
重寫computerScroller方法,實(shí)現(xiàn)模擬滑動
下面我們需要重寫computerScroller()芳芳法,它是使用Scroller類的核心,系統(tǒng)在繪制View的時(shí)候會在draw()方法中調(diào)用該方法。這個(gè)方法實(shí)際就是使用的scrollTo方法。再結(jié)合Scroller對象,幫助獲取到當(dāng)前滾動值。我們可以通過不斷地瞬間移動一個(gè)小的距離來實(shí)現(xiàn)整體上的平滑移動效果。代碼如下:
@Override public void computeScroll() { super.computeScroll(); // 判斷Scroller是否執(zhí)行完畢 if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY()); // 通過重繪來不斷調(diào)用computeScroll invalidate(); } }
Scroller類提供了computeScrollOffset()方法來判斷是否完成了整個(gè)滑動,同時(shí)也提供了getCurrX()、getCurrY()方法來獲得當(dāng)前的滑動坐標(biāo)。在上面的代碼中,唯一需要注意的是invalidate()方法,因?yàn)橹荒茉赾omputeScroller()方法中獲取模擬過程中的scrollX和scrollY坐標(biāo)。但computeScroll()方法是不會自動調(diào)用的,只能通過invalidate()->draw()->computeScroll()來間接調(diào)用compuetScroll()方法,所以需要在compuetScroll()方法中調(diào)用invaliDate()方法,實(shí)現(xiàn)循環(huán)獲取scrollX和scrollY的目的。而當(dāng)模擬過程結(jié)束后,scroller.compuetScrollOffset()方法會返回false,而中斷循環(huán),完成平滑移動過程。
startScroll開啟模擬過程
我們在需要使用平滑移動的事件中,使用Scroller類的startScroll()方法來開啟平滑移動過程。startScroll()方法具有兩個(gè)重載方法。
public void startScroll(int startX, int startY, int dx, int dy)
public void startScroll(int startX, int startY, int dx, int dy, int duration)
可以看到它們的區(qū)別就是一個(gè)具有指定的支持時(shí)長,而另一個(gè)沒有。很好理解,與在動畫中設(shè)置duration和使用默認(rèn)的顯示時(shí)長是一個(gè)道理。其他四個(gè)坐標(biāo),則與他們的命名含義相同,就是起始坐標(biāo)與偏移量。在獲取坐標(biāo)時(shí),通常可以使用getScrollX()和getScrollY()方法來獲取父視圖中content所滑動到的點(diǎn)的坐標(biāo),需要注意的是這個(gè)值的正負(fù),它與在scrollBy、scrollTo中講解的情況是一樣的。
根據(jù)以上三步,就可以使用Scroller類實(shí)現(xiàn)平滑移動,在構(gòu)造方法中初始化Scroller對象,重寫View的computerScroll()方法,***監(jiān)聽手指離開屏蔽的事件,并在該事件中調(diào)用startScroll()方法完成平滑移動。監(jiān)聽手指離開屏幕的事件,只需要在onTouchEvent中增加一個(gè)ACTION_UP監(jiān)聽選項(xiàng)即可,代碼如下所示:
case MotionEvent.ACTION_UP: // 手指離開時(shí),執(zhí)行滑動過程 View viewGroup = ((View) getParent()); mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY()); invalidate(); break;
在startScroll()方法中我們獲取子View移動的距離-getScrollX()、getScrollY(),并將偏移量設(shè)置為其相反數(shù),從而將子View滑動到原位置。這里的invalidate()方法是用來通知View進(jìn)行重繪,調(diào)用computeScroll()的模擬過程。當(dāng)然,也可以給startScroll()方法增加一個(gè)duration的參數(shù)來設(shè)置滑動的持續(xù)時(shí)長。
(6)、屬性動畫
屬性動畫請參見我的另一篇:Android全套動畫使用技巧
(7)、ViewDragHelper
Google在其support庫中為我們提供了DrawerLayout和SlidingPaneLayout兩個(gè)布局來幫助開發(fā)者實(shí)現(xiàn)側(cè)邊欄滑動的效果。這兩個(gè)新的布局方便我們創(chuàng)建自己的滑動布局界面,在這兩個(gè)強(qiáng)大布局背后有一個(gè)功能強(qiáng)大的類——ViewDragHelper。通過ViewDragHelper,基本可以實(shí)現(xiàn)各種不同的滑動、拖放需求,因此這個(gè)方法也是各種滑動解決方案中的終結(jié)絕招。
下面演示一個(gè)使用ViewDragHelper創(chuàng)建一個(gè)QQ側(cè)邊欄滑動的布局,如圖:
圖三
圖四
初始化ViewDragHelper
首先需要初始化ViewDragHelper,ViewDragHelper通常定義在一個(gè)ViewGroup的內(nèi)部,通過靜態(tài)工廠方法進(jìn)行初始化,代碼如下:
mViewDragHelper = ViewDragHelper.create(this, callback);
***個(gè)參數(shù)監(jiān)聽的View,通常需要一個(gè)ViewGroup,即parentView;第二個(gè)參數(shù)是一個(gè)Callback回調(diào),這個(gè)回調(diào)就是整個(gè)ViewDragHelper的邏輯核心。
攔截事件
重寫攔截事件,將事件傳遞給ViewDragHelper進(jìn)行處理;
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mViewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //將觸摸事件傳遞給ViewDragHelper,此操作必不可少 mViewDragHelper.processTouchEvent(event); return true; }
處理computeScroll()
使用ViewDragHelper同樣需要重寫computeScroll()方法,因?yàn)閂iewDragHelper內(nèi)部也是通過Scroller來實(shí)現(xiàn)平滑移動的。
@Override public void computeScroll() { if (mViewDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } }
處理回調(diào)Callback
創(chuàng)建一個(gè)ViewDragHelper.Callback
private ViewDragHelper.Callback getCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return false; } };
as自動重寫tryCaptureView()方法,通過這個(gè)方法可以指定在創(chuàng)建ViewDragHelper時(shí),參數(shù)parentView中的哪一個(gè)子Vieww可以被移動,例如我們在這個(gè)實(shí)例中自定義一個(gè)ViewGroup,里面定義了兩個(gè)子View——Menu View和MainView,如下代碼:
// 何時(shí)開始檢測觸摸事件 @Override public boolean tryCaptureView(View child, int pointerId) { //如果當(dāng)前觸摸的child是mMainView時(shí)開始檢測 return mMainView == child; }
具體垂直滑動方法clampViewPositionVertical()和水平滑動方法clampViewPositionHorizontal()。實(shí)現(xiàn)滑動這個(gè)兩個(gè)方法必須寫,默認(rèn)返回值是0,即不發(fā)生滑動,當(dāng)然如果只重寫clampViewPositionVertical()或clampViewPositionHorizontal()中的一個(gè),那么就只會實(shí)現(xiàn)該方向上的滑動效果。
// 處理垂直滑動 @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移動的距離,dy則表示比較前一次的增量。clampViewPositionHorizontal(View child, int left, int dx)也是類似的含義,通常情況下只需要返回top和left即可,但需要更加精確地計(jì)算padding等屬性的時(shí)候,就需要對left進(jìn)行一些處理,并返回合適大小的值。
通過重寫上面的三個(gè)方法,就可以實(shí)現(xiàn)基本的滑動效果。當(dāng)用手拖動MainView的時(shí)候,它就可有跟隨手指的滑動而滑動了,代碼:
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { // 何時(shí)開始檢測觸摸事件 @Override public boolean tryCaptureView(View child, int pointerId) { //如果當(dāng)前觸摸的child是mMainView時(shí)開始檢測 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; } };
在前面的Scroller中講解時(shí)實(shí)現(xiàn)一個(gè)效果——手指離開屏幕后,View滑動回到初始位置。現(xiàn)在使用ViewDragHelper實(shí)現(xiàn),在ViewDragHelper.Callback中,系統(tǒng)提供了這樣的方法——onViewReleased(),通過重寫這個(gè)方法,可以非常簡單地實(shí)現(xiàn)當(dāng)手指離開屏幕后實(shí)現(xiàn)的操作。這個(gè)方法內(nèi)部是使用Scroller類實(shí)現(xiàn)的,這也是前面重寫computeScroll()方法的原因。
@Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); //手指抬起后緩慢移動到指定位置 if (mMainView.getLeft() < 500) { //關(guān)閉菜單 //等同于Scroll的startScroll方法 mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } else { //打開菜單 mViewDragHelper.smoothSlideViewTo(mMainView,300,0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } }
設(shè)置讓MainView移動后左邊距小于500像素的時(shí)候,就使用smoothSlideViewTo()方法來講MainView還原到初始狀態(tài),即坐標(biāo)(0,0),左邊距大于500則將MainView移動到(300,0)坐標(biāo),即顯示MainView。
//ViewDragHelper mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
//Scroller mScroller.startScroll(x,y,dx,dy); invalidate();
滑動的時(shí)候,在自定義ViewGroup的onFinishInflate()方法中,按照順序?qū)⒆覸iew分別定義成MenuView和MainView,并在onSizeChanged方法中獲得View的寬度。如果需要根據(jù)View的寬度來處理滑動后的效果,就可以使用這個(gè)值判斷。
/*** * 加載完布局文件后調(diào)用 */ @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è)通過ViewDragHelper實(shí)現(xiàn)QQ側(cè)滑功能代碼:
package com.xjf.drawview; import android.content.Context; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; public class DragViewGroup extends FrameLayout { private ViewDragHelper mViewDragHelper; private View mMenuView, mMainView; private int mWidth; public DragViewGroup(Context context) { super(context); initView(); } public DragViewGroup(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } /*** * 加載完布局文件后調(diào)用 */ @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(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mViewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //將觸摸事件傳遞給ViewDragHelper,此操作必不可少 mViewDragHelper.processTouchEvent(event); return true; } private void initView() { mViewDragHelper = ViewDragHelper.create(this, callback); } private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { // 何時(shí)開始檢測觸摸事件 @Override public boolean tryCaptureView(View child, int pointerId) { //如果當(dāng)前觸摸的child是mMainView時(shí)開始檢測 return mMainView == child; } // 觸摸到View后回調(diào) @Override public void onViewCaptured(View capturedChild, int activePointerId) { super.onViewCaptured(capturedChild, activePointerId); } // 當(dāng)拖拽狀態(tài)改變,比如idle,dragging @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); } // 當(dāng)位置改變的時(shí)候調(diào)用,常用與滑動時(shí)更改scale等 @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); } // 處理垂直滑動 @Override public int clampViewPositionVertical(View child, int top, int dy) { return 0; } // 處理水平滑動 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } // 拖動結(jié)束后調(diào)用 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); //手指抬起后緩慢移動到指定位置 if (mMainView.getLeft() < 500) { //關(guān)閉菜單 //相當(dāng)于Scroller的startScroll方法 mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } else { //打開菜單 mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } } }; @Override public void computeScroll() { if (mViewDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } }
除此之外,ViewDragHelper很多強(qiáng)大的功能還沒得到展示,在ViewDragHelper.Callback中,系統(tǒng)定義了大量的監(jiān)聽事件來幫助我們處理各種事件,如下:
onViewCaptured()這個(gè)事件在用戶觸摸到View后回調(diào)
onViewDragStateChanged()這個(gè)事件在拖拽狀態(tài)改變時(shí)回調(diào),比如idle,dragging等狀態(tài)
STATE_IDLE:View當(dāng)前沒有被拖拽也沒執(zhí)行動畫,只是安靜地待在原地
STATE_DRAGGING:View當(dāng)前正在被拖動,由于用戶輸入或模擬用戶輸入導(dǎo)致View位置正在改變
STATE_SETTLING:View當(dāng)前正被安頓到指定位置,由fling手勢或預(yù)定義的非交互動作觸發(fā)
onViewPositionChanged()//view在拖動過程坐標(biāo)發(fā)生變化時(shí)會調(diào)用此方法,包括兩個(gè)時(shí)間段:手動拖動和自動滾動。
看完上述內(nèi)容,你們掌握Android中怎么實(shí)現(xiàn)滑動的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!