Android 的滑動分析以及各種實現


一、滑動效果的產生

滑動一個View,本質區別就是移動一個View。改變當前View所在的坐標,原理和動畫相似不斷改變坐標位置實現。實現View的滑動就必須監聽滑動的事件,并且根據事件傳入的坐標,動態且不斷改變View的坐標,從而實現View跟隨用戶觸摸的滑動而滑動。

(1)、Android的坐標系

Android中將屏幕最左上角的頂點作為Android坐標系的原點,從這個點向右是X軸正方向,從這個點向下是Y軸正方向,如下圖:


系統提供了getLocationOnScreen(int location[])這樣的方法來獲取Android坐標系中點的位置,即該視圖左上角在Android坐標系中的坐標。在觸控事件中使用getRawX()、getRawY()方法所獲得的坐標同樣是Android坐標系中的坐標。

(2)、視圖坐標系

Android中除了上面所說的這種坐標系之外,還有一個視圖坐標系,它描述了子視圖在父視圖中的位置關系。這兩種坐標系并不矛盾也不復雜,他們的作用是相互相成的。與Android坐標系類似,視圖坐標系同樣是以原點向右為X軸正方向,以原點向下為Y軸正方向,只不過在視圖坐標系中,原點不再是Android坐標系中的屏幕最左上角,而是以父視圖左上角為坐標原點,如下圖:


在觸控事件中,通過getX()、getY()所獲得的坐標系就是視圖坐標系中的坐標。

(3)、觸控事件——MotionEvent

觸控事件MotionEvent在用戶交互中,占著舉足輕重的地位。首先看看MotionEvent封裝的一些常用事件常量,定義了觸控事件的不同類型。

//單點觸摸按下動作
public static final int ACTION_DOWN             = 0;//單點觸摸離開動作
public static final int ACTION_UP               = 1;//觸摸點移動動作
public static final int ACTION_MOVE             = 2;//觸摸動作取消
public static final int ACTION_CANCEL           = 3;//觸摸動作超出邊界
public static final int ACTION_OUTSIDE          = 4;//多點觸摸按下動作
public static final int ACTION_POINTER_DOWN     = 5;//多點離開動作
public static final int ACTION_POINTER_UP       = 6;復制代碼

通常情況會在onTouchEvent(MotionEvent event)方法中通過event.getAction()方法來獲取觸控事件的類型,并使用switch-case方法來進行篩選,這個代碼的模式基本固定:

@Override
public boolean onTouchEvent(MotionEvent event) {//獲取當前輸入點的X、Y坐標(視圖坐標)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;
}復制代碼

在不涉及多點操作的情況下,通常可以使用以上代碼來完成觸控事件的監聽。

在Android中系統提供了非常多的方法來獲取坐標值、相對距離等。方法豐富固然好,下面對坐標系的API進行總結,如下圖:


這些方法可以分為如下兩個類別:

  • View提供的獲取坐標方法
    • getTop():獲取到的是View自身的頂邊到其父布局頂邊的距離。
    • getLeft():獲取到的是View自身的左邊到其父布局最左邊的距離。
    • getRight():獲取到的是View自身的右邊到其父布局左邊的距離。
    • getBottom():獲取到的是View自身的底邊到其父布局頂邊的距離。
  • MotionEvent提供的方法
    • getX():獲取點擊事件距離空間左邊的距離,即視圖坐標。
    • getY():獲取點擊事件距離控件頂邊的距離,即視圖坐標。
    • getRawX():獲取點擊事件距離整個屏幕左邊的距離,即絕對坐標。
    • getRawY():獲取點擊事件距離整個屏幕頂邊的距離,即絕對坐標。

二、實現滑動的七種方式

當了解Android坐標系和觸控事件后,我們再來看看如何使用系統提供的API來實現動態地修改一個View坐標,即實時滑動效果。而不管采用哪一種方式,其實現的思想基本是一致的,當觸摸View時,系統記下當前觸摸點坐標,當手指移動時,系統記下移動后的觸摸點坐標,從而獲取到相對于前一次坐標點的偏移量,并通過偏移量來修改View的坐標,這樣不斷重復,實現滑動過程。

通過一個實例看看Android中該如何實現滑動效果,定義一個View,處于LinearLayout中,實現一個簡單布局:

<?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.xjf.drawview.DragView1android:layout_width="100dp"android:layout_height="100dp" /></LinearLayout>復制代碼

我們的目的就是讓這個自定義的View隨著手指在屏幕上的滑動而滑動。初始化時顯示效果:


(1)、layout方法

在View繪制時,會調用onLayout()方法來設置顯示的位置。同樣,可以通過修改View的left,top,right,bottom四個屬性來控制View的坐標。與前面提供的模板代碼一樣,在每次回調onTouchEvent的時候,我們都來獲取一下觸摸點的坐標,代碼如下:

//獲取當前輸入點的X、Y坐標(視圖坐標)
int x = (int) event.getX();
int y = (int) event.getY();復制代碼

接著,在Action_DOWN事件中記錄觸摸點的坐標,如下:

case MotionEvent.ACTION_DOWN:// 記錄觸摸點坐標lastX = x;lastY = y;break;復制代碼

最后,可以在Action_MOVE事件中計算偏移量,并將偏移量作用到Layout方法中,在目前Layout的left,top,right,bottom基礎上,增加計算出來的偏移量,代碼如下所示:

case MotionEvent.ACTION_MOVE:// 計算偏移量int offsetX = x - lastX;int offsetY = y - lastY;// 在當前left、top、right、bottom的基礎上加上偏移量layout(getLeft() + offsetX,getTop() + offsetY,getRight() + offsetX,getBottom() + offsetY);break;復制代碼

這樣沒錯移動后,View都會調用Layout方法來對自己重新布局,從而達到移動View的效果。

上面的代碼中,使用的是getX()、getY()方法來獲取坐標值,即通過視圖坐標來獲取偏移量。當然,同樣可以使用getRawX()、getRawY()來獲取坐標,并使用絕對坐標來計算偏移量,代碼如下:

// 視圖坐標方式
@Override
public boolean onTouchEvent(MotionEvent event) {int x = (int) event.getRawX();int y = (int) event.getRawY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:// 記錄觸摸點坐標lastX = x;lastY = y;break;case MotionEvent.ACTION_MOVE:// 計算偏移量int offsetX = x - lastX;int offsetY = y - lastY;// 在當前left、top、right、bottom的基礎上加上偏移量layout(getLeft() + offsetX,getTop() + offsetY,getRight() + offsetX,getBottom() + offsetY);//重新設置初始化坐標lastX = x;lastY = y;break;}return true;
}復制代碼

使用絕對坐標系,有一點非常需要注意的地方,就是在每次執行完ACTION_MOVE的邏輯后,一定要重新設置初始化坐標,這樣才能準確地獲取偏移量。

(2)、offsetLeftAndRight()與offsetTopAndBottom()

這個方法相當于系統提供的一個對左右、上下移動的API的封裝。當計算出偏移量后,只需要使用如下代碼就可以完成View的重新布局,效果與使用Layout方法一樣,代碼如下所示:

//同時對left和right進行偏移
offsetLeftAndRight(offsetX);
//同時對top和bottom進行偏移
offsetTopAndBottom(offsetY);復制代碼

這里的offsetX、offsetY與在layout方法中計算offset方法一樣。

(3)、LayoutParams

LayoutParams保存了一個View的布局參數。因此可以在程序中,通過改變LayoutParams來動態地修改一個布局的位置參數,從而達到改變View位置的效果。我們可以很方便在程序中使用getLayoutParams()來獲取一個View的LayoutParams。當然,計算偏移量的方法與在Layout方法中計算offset也是一樣。當獲取到偏移量之后,就可以通過setLayoutParams來改變其LayoutParams,代碼如下:

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);復制代碼

這里getLayoutParams()獲取LayoutParams時,需要根據View所在View父布局的類型來設置不同的類型,比如這里將View放在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。這一切的前提是你必須要有一個父布局,不然系統無法獲取LayoutParams。

在通過改變LayoutParams來改變一個View的位置時,通常改變的是這個View的Margin屬性,所以除了使用布局的LayoutParams之外,還可以使用ViewGroup.MarginLayoutParams來實現這一一個功能,代碼:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);復制代碼

我們可以發現,使用ViewGroup.MarginLayoutParams更加的方便,不需要考慮父布局的類型,當然它們的本質都是一樣。

(4)、scrollTo與scrollBy

在一個View中,系統提供了scrollTo、scrollBy兩種方式來改變一個View的位置。這兩個方法的區別非常好理解,與英文中To與By的區別類似,scrollTo(x,y)表示移動到一個具體的坐標點(x,y),而scrollBy(dx,dy)表示移動的增量為dx,dy。

與前面幾種方式相同,在獲取偏移量后使用scrollBy來移動View,代碼如下:

int offsetX = x - lastX;
int offsetY = y - lastY;
scrollBy(offsetX, offsetY);復制代碼

但是,當我們拖動View的時候,你會發現View并沒有移動,其實方法沒錯,View確實移動了,只是移動的并不是我們想要的東西。scrollTo、scrollBy方法移動的是View的content,即讓View的內容移動,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移動的將是所有子View,如果在View中使用,那么移動的將是View的內容,例如TextView,content就是它的文本,ImageView,content就是它的drawable對象。

通過以上的分析,現在知道為什么不能再View中使用這兩個方法來拖動這個View了。那么我們就該View所在的ViewGroup中來使用scrollBy方法,移動它的子View,代碼如下:

((View) getParent()).scrollBy(offsetX, offsetY);復制代碼

但是再次拖動View的時候,你會發現View雖然移動了,但卻在亂動,并不是我們想要的跟隨觸摸點的移動而移動。這里先看一下視圖移動,不妨這樣想象一下手機屏幕是一個中空的蓋板,蓋板下面是一個巨大的畫布,也就是我們想要顯示的視圖。當把這個蓋板蓋在畫布上的某一處時,透過中間空的矩形,我們看見了手機屏幕上顯示的視圖,而畫布上其他地方的視圖,則被蓋板蓋住了無法看見。我們的視圖與這個例子非常類似,我們沒有看見視圖,并不代表它就不存在,有可能只是在屏幕外面而已。當調用scrollBy方法時,可以想象為外面的蓋板在移動,這么說比較抽象。

下圖一中間的矩形相當于屏幕,及可視區域。后面的content就相當于畫布,代表視圖。可以看到,只有視圖的中間部分目前是可視的,其他部分都不可見。在可見區域中,我們設置了一個Button,它的坐標為(20,10)。

下面使用scrollBy方法,將蓋板(屏幕、可視區域),在水平方向上向X軸正方向(向右)平移20,在豎直方向上向Y軸正方向(下方)平移10,那么平移之后的可視區域如圖二。


圖一



圖二、移動之后的可視區域

我們發現,雖然設置scrollBy(20,10),偏移量均為X軸、Y軸正方向上的正數,但是在屏幕的可視區域內,Button卻向X軸、Y軸負方向上移動了。這就是因為參考系選擇的不同,而產生的不同效果。

通過上面的分析可以發現,如果講scrollBy中的參數dx和dy設置為正數,那么content講向坐標軸負方向移動,如果將scrollBy中的參數dx和dy設置為負數,那么content將向坐標軸正方向移動,因此回到前面的例子,要實現跟隨著手指移動而滑動的效果,就必須將偏移量改為負值,代碼如下:

int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);復制代碼

現在在運行一次發現和前面幾種方式效果相同了,類似地使用絕對坐標時,也可以通過使用scrollTo發方法來實現這一效果。

(5)、Scroller

前面提到了scrollBy、scrollTo方法,就不得不再來說一說Scroller類。Scroller類與scrollBy、scrollTo方法十分相似。什么區別?先看例子,如果要完成這樣一個效果;通過點擊按鈕,讓一個ViewGroup的子View向右移動100個像素。問題看起來很簡單,只要在按鈕的點擊事件中使用前面的scrollBy方法設置下偏移量就可以了嗎?確實這樣可以讓一個子ViewGroup中的子View平移,但是不管使用scrollBy還是scrollTo方法,子view的平移都是瞬間發生的,在事件執行的時候平移就已經完成了,這樣的效果會讓人感覺非常突然,Google建議使用自然的過度動畫來實現移動效果。因此Scroller類就這樣誕生了,通過Scroller類可以實現平滑移動的效果,而不是瞬間就完成移動。

Scroller類的實現原理,其實它與前面使用的scrollTo和scrollBy方法來實現子View跟隨手指移動的原理基本類似,雖然scrollBy芳芳法是讓子View瞬間從某點移動到另一個點,但是由于在ACTION_MOVE事件中不斷獲取手指移動的微小的偏移量,這樣就將一段距離劃分成了N個非常小的偏移量。雖然每個偏移量里面,通過scrollBy方法進行了瞬間移動,但是在整體上卻可以獲得一個平滑移動的效果。這個原理與動畫的實現原理也是基本類似的,它們都是利用了人眼的視覺暫留特性。

下面我們使用Scroller類實現平滑移動,在這個實例中,同樣讓子View跟隨手指的滑動而滑動,但是在手指離開屏蔽時,讓子View平滑的移動到初始化位置,即屏幕左上角。使用Scroller類需要如下三個步驟:

  • 初始化Scroller

首先通過它的構造方法來創建一個Scroller對象,代碼如下所示:

// 初始化Scroller
mScroller = new Scroller(context);復制代碼
  • 重寫computerScroller方法,實現模擬滑動

下面我們需要重寫computerScroller()芳芳法,它是使用Scroller類的核心,系統在繪制View的時候會在draw()方法中調用該方法。這個方法實際就是使用的scrollTo方法。再結合Scroller對象,幫助獲取到當前滾動值。我們可以通過不斷地瞬間移動一個小的距離來實現整體上的平滑移動效果。代碼如下:

@Override
public void computeScroll() {super.computeScroll();// 判斷Scroller是否執行完畢if (mScroller.computeScrollOffset()) {((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());// 通過重繪來不斷調用computeScrollinvalidate();}
}復制代碼

Scroller類提供了computeScrollOffset()方法來判斷是否完成了整個滑動,同時也提供了getCurrX()、getCurrY()方法來獲得當前的滑動坐標。在上面的代碼中,唯一需要注意的是invalidate()方法,因為只能在computeScroller()方法中獲取模擬過程中的scrollX和scrollY坐標。但computeScroll()方法是不會自動調用的,只能通過invalidate()->draw()->computeScroll()來間接調用compuetScroll()方法,所以需要在compuetScroll()方法中調用invaliDate()方法,實現循環獲取scrollX和scrollY的目的。而當模擬過程結束后,scroller.compuetScrollOffset()方法會返回false,而中斷循環,完成平滑移動過程。

  • startScroll開啟模擬過程

我們在需要使用平滑移動的事件中,使用Scroller類的startScroll()方法來開啟平滑移動過程。startScroll()方法具有兩個重載方法。

public void startScroll(int startX, int startY, int dx, int dy)復制代碼
public void startScroll(int startX, int startY, int dx, int dy, int duration)復制代碼

可以看到它們的區別就是一個具有指定的支持時長,而另一個沒有。很好理解,與在動畫中設置duration和使用默認的顯示時長是一個道理。其他四個坐標,則與他們的命名含義相同,就是起始坐標與偏移量。在獲取坐標時,通常可以使用getScrollX()和getScrollY()方法來獲取父視圖中content所滑動到的點的坐標,需要注意的是這個值的正負,它與在scrollBy、scrollTo中講解的情況是一樣的。

根據以上三步,就可以使用Scroller類實現平滑移動,在構造方法中初始化Scroller對象,重寫View的computerScroll()方法,最后監聽手指離開屏蔽的事件,并在該事件中調用startScroll()方法完成平滑移動。監聽手指離開屏幕的事件,只需要在onTouchEvent中增加一個ACTION_UP監聽選項即可,代碼如下所示:

case MotionEvent.ACTION_UP:// 手指離開時,執行滑動過程View viewGroup = ((View) getParent());mScroller.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),-viewGroup.getScrollX(),-viewGroup.getScrollY());invalidate();break;復制代碼

在startScroll()方法中我們獲取子View移動的距離-getScrollX()、getScrollY(),并將偏移量設置為其相反數,從而將子View滑動到原位置。這里的invalidate()方法是用來通知View進行重繪,調用computeScroll()的模擬過程。當然,也可以給startScroll()方法增加一個duration的參數來設置滑動的持續時長。

(6)、屬性動畫

屬性動畫請參見我的另一篇:Android全套動畫使用技巧

(7)、ViewDragHelper

Google在其support庫中為我們提供了DrawerLayout和SlidingPaneLayout兩個布局來幫助開發者實現側邊欄滑動的效果。這兩個新的布局方便我們創建自己的滑動布局界面,在這兩個強大布局背后有一個功能強大的類——ViewDragHelper。通過ViewDragHelper,基本可以實現各種不同的滑動、拖放需求,因此這個方法也是各種滑動解決方案中的終結絕招。

下面演示一個使用ViewDragHelper創建一個QQ側邊欄滑動的布局,如圖:


圖三




圖四

  • 初始化ViewDragHelper

首先需要初始化ViewDragHelper,ViewDragHelper通常定義在一個ViewGroup的內部,通過靜態工廠方法進行初始化,代碼如下:

mViewDragHelper = ViewDragHelper.create(this, callback);復制代碼

第一個參數監聽的View,通常需要一個ViewGroup,即parentView;第二個參數是一個Callback回調,這個回調就是整個ViewDragHelper的邏輯核心。

  • 攔截事件

重寫攔截事件,將事件傳遞給ViewDragHelper進行處理;

@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()方法,因為ViewDragHelper內部也是通過Scroller來實現平滑移動的。

@Override
public void computeScroll() {if (mViewDragHelper.continueSettling(true)) {ViewCompat.postInvalidateOnAnimation(this);}
}復制代碼
  • 處理回調Callback

創建一個ViewDragHelper.Callback

private ViewDragHelper.Callback getCallback = new ViewDragHelper.Callback() {@Overridepublic boolean tryCaptureView(View child, int pointerId) {return false;}
};復制代碼

as自動重寫tryCaptureView()方法,通過這個方法可以指定在創建ViewDragHelper時,參數parentView中的哪一個子Vieww可以被移動,例如我們在這個實例中自定義一個ViewGroup,里面定義了兩個子View——Menu View和MainView,如下代碼:

// 何時開始檢測觸摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {//如果當前觸摸的child是mMainView時開始檢測return mMainView == child;
}復制代碼

具體垂直滑動方法clampViewPositionVertical()和水平滑動方法clampViewPositionHorizontal()。實現滑動這個兩個方法必須寫,默認返回值是0,即不發生滑動,當然如果只重寫clampViewPositionVertical()或clampViewPositionHorizontal()中的一個,那么就只會實現該方向上的滑動效果。

// 處理垂直滑動
@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)中的參數top,代表在垂直方向上child移動的距離,dy則表示比較前一次的增量。clampViewPositionHorizontal(View child, int left, int dx)也是類似的含義,通常情況下只需要返回top和left即可,但需要更加精確地計算padding等屬性的時候,就需要對left進行一些處理,并返回合適大小的值。

通過重寫上面的三個方法,就可以實現基本的滑動效果。當用手拖動MainView的時候,它就可有跟隨手指的滑動而滑動了,代碼:

private ViewDragHelper.Callback callback =  new ViewDragHelper.Callback() {// 何時開始檢測觸摸事件@Overridepublic boolean tryCaptureView(View child, int pointerId) {//如果當前觸摸的child是mMainView時開始檢測return mMainView == child;}// 處理垂直滑動@Overridepublic int clampViewPositionVertical(View child, int top, int dy) {return 0;}// 處理水平滑動@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {return left;}};復制代碼

在前面的Scroller中講解時實現一個效果——手指離開屏幕后,View滑動回到初始位置。現在使用ViewDragHelper實現,在ViewDragHelper.Callback中,系統提供了這樣的方法——onViewReleased(),通過重寫這個方法,可以非常簡單地實現當手指離開屏幕后實現的操作。這個方法內部是使用Scroller類實現的,這也是前面重寫computeScroll()方法的原因。

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {super.onViewReleased(releasedChild, xvel, yvel);//手指抬起后緩慢移動到指定位置if (mMainView.getLeft() < 500) {//關閉菜單//等同于Scroll的startScroll方法mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);} else {//打開菜單mViewDragHelper.smoothSlideViewTo(mMainView,300,0);ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);}
}復制代碼

設置讓MainView移動后左邊距小于500像素的時候,就使用smoothSlideViewTo()方法來講MainView還原到初始狀態,即坐標(0,0),左邊距大于500則將MainView移動到(300,0)坐標,即顯示MainView。

//ViewDragHelper

mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);復制代碼

//Scroller

mScroller.startScroll(x,y,dx,dy);
invalidate();復制代碼

滑動的時候,在自定義ViewGroup的onFinishInflate()方法中,按照順序將子View分別定義成MenuView和MainView,并在onSizeChanged方法中獲得View的寬度。如果需要根據View的寬度來處理滑動后的效果,就可以使用這個值判斷。

/**** 加載完布局文件后調用*/
@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();
}復制代碼

最后,整個通過ViewDragHelper實現QQ側滑功能代碼:

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();}/**** 加載完布局文件后調用*/@Overrideprotected void onFinishInflate() {super.onFinishInflate();mMenuView = getChildAt(0);mMainView = getChildAt(1);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = mMenuView.getMeasuredWidth();}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return mViewDragHelper.shouldInterceptTouchEvent(ev);}@Overridepublic 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() {// 何時開始檢測觸摸事件@Overridepublic boolean tryCaptureView(View child, int pointerId) {//如果當前觸摸的child是mMainView時開始檢測return mMainView == child;}// 觸摸到View后回調@Overridepublic void onViewCaptured(View capturedChild,int activePointerId) {super.onViewCaptured(capturedChild, activePointerId);}// 當拖拽狀態改變,比如idle,dragging@Overridepublic void onViewDragStateChanged(int state) {super.onViewDragStateChanged(state);}// 當位置改變的時候調用,常用與滑動時更改scale等@Overridepublic void onViewPositionChanged(View changedView,int left, int top, int dx, int dy) {super.onViewPositionChanged(changedView, left, top, dx, dy);}// 處理垂直滑動@Overridepublic int clampViewPositionVertical(View child, int top, int dy) {return 0;}// 處理水平滑動@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {return left;}// 拖動結束后調用@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {super.onViewReleased(releasedChild, xvel, yvel);//手指抬起后緩慢移動到指定位置if (mMainView.getLeft() < 500) {//關閉菜單//相當于Scroller的startScroll方法mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);} else {//打開菜單mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);}}};@Overridepublic void computeScroll() {if (mViewDragHelper.continueSettling(true)) {ViewCompat.postInvalidateOnAnimation(this);}}
}復制代碼

除此之外,ViewDragHelper很多強大的功能還沒得到展示,在ViewDragHelper.Callback中,系統定義了大量的監聽事件來幫助我們處理各種事件,如下:

  • onViewCaptured()這個事件在用戶觸摸到View后回調
  • onViewDragStateChanged()這個事件在拖拽狀態改變時回調,比如idle,dragging等狀態

STATE_IDLE:View當前沒有被拖拽也沒執行動畫,只是安靜地待在原地

STATE_DRAGGING:View當前正在被拖動,由于用戶輸入或模擬用戶輸入導致View位置正在改變

STATE_SETTLING:View當前正被安頓到指定位置,由fling手勢或預定義的非交互動作觸發

  • onViewPositionChanged()//view在拖動過程坐標發生變化時會調用此方法,包括兩個時間段:手動拖動和自動滾動。



本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/282316.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/282316.shtml
英文地址,請注明出處:http://en.pswp.cn/news/282316.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

微軟產品 .NET 6 遷移之旅

“.NET性能不行&#xff01;”“.NET有什么像樣的產品嗎&#xff01;&#xff1f;”“升級到.NET 6有什么好處&#xff01;&#xff1f;”……聽人扯淡還不如看看微軟自己是怎么做的。本文將匯總一下微軟的開發博客——這些博客均涉及微軟將產品和服務遷移到.NET 6的成果。博客…

Navicat 連接 RDS數據庫

場景介紹&#xff1a; 隨著業務量的逐漸增加&#xff0c;公司的數據庫壓力也會逐漸增大&#xff0c;使用自己購買的esc創建的mysql的話&#xff0c;還得考慮相應的dba維護&#xff0c;也比較繁瑣&#xff0c;說不定還做的并不完美&#xff0c;這時&#xff0c;RDS就派上用場了&…

bzoj1045 糖果傳遞

Description 有n個小朋友坐成一圈&#xff0c;每人有ai個糖果。每人只能給左右兩人傳遞糖果。每人每次傳遞一個糖果代價為1。 Input 第一行一個正整數nn<1000000&#xff0c;表示小朋友的個數&#xff0e;接下來n行&#xff0c;每行一個整數ai&#xff0c;表示第i個小朋友得…

BEGINNING SHAREPOINT#174; 2013 DEVELOPMENT 第9章節--client對象模型和REST APIs概覽 client對象模型API范圍...

BEGINNING SHAREPOINT 2013 DEVELOPMENT 第9章節--client對象模型和REST APIs概覽 client對象模型API范圍 本章之前提到過。client對象模型應用中一個不足就是缺乏對SP APIs和訪問功能的支持不足。轉載于:https://www.cnblogs.com/yutingliuyl/p/6748382.html

為.NET應用添加截圖功能

本文介紹了 .NET 實現截圖功能的思路和過程&#xff0c;如果你僅想了解最后的解決方案&#xff0c;可以直接查看文章末尾。截圖的功能我們應該都經常使用&#xff0c;在開發軟件時&#xff0c;我們有時也或多或少需要提供這方面的功能&#xff0c;無論是為用戶更方便提供遠程診…

K8S集群Master高可用實踐

本文將在前文基礎上介紹k8s集群的高可用實踐&#xff0c;一般來講&#xff0c;k8s集群高可用主要包含以下幾個內容&#xff1a;1、etcd集群高可用2、集群dns服務高可用3、kube-apiserver、kube-controller-manager、kube-scheduler等master組件的高可用 其中etcd實現的辦法較為…

[轉載]智能科普:VR、AR、MR的區別

智能科普&#xff1a;VR、AR、MR的區別 http://news.zol.com.cn/553/5534833.html news.zol.com.cn 2015-11-23 16:00近日&#xff0c; 獲得谷歌5億美元融資的技術公司Magic Leap在WSJD展會中放出了一段實錄視頻&#xff0c;引起不小騷動。如今&#xff0c;也有媒體稱他們為MR公…

PHP項目中,記錄錯誤日志

一、場景介紹&#xff1a; 環境&#xff1a;LNMP 我們通常是通過nginx的錯誤日志來分析分錯的&#xff0c;也就是我們在各個server中定義的error_log。 比如下面這樣&#xff0c;就是將錯誤日志定義在/etc/nginx/logs/error/www.xiaobudiu.top.log&#xff0c;發生錯誤&#xf…

持續集成指南:GitLab 的 CI/CD 工具配置與使用

1前言寫代碼這項工作&#xff0c;本質就是將工作自動化&#xff0c;減少手工操作提供效率&#xff0c;因為人的本質都是懶狗&#xff0c;程序員也不能例外&#xff0c;為了各種意義的效率提升&#xff08;懶&#xff09;&#xff0c;我們需要持續集成工具&#xff0c;將代碼測試…

php 錯誤日志 redis' already loaded in Unknown on line 0

環境介紹&#xff1a;LNMP 報錯信息&#xff1a;注&#xff1a;這個php_errors.log 是我在php.ini 中定義的錯誤日志路徑 問題原因&#xff1a; 報錯信息給出的意思是&#xff1a;redis和memcache 模塊已經加載過問題解決&#xff1a; php加載模塊有兩種方式&#xff0c;一種是…

第一周作業

我的Git賬號&#xff1a;AI1452349541 和代碼圖 這是我在電腦和手機上下的網易有道詞典 &#xff0c; C也下了。 ***學習內容總結*** 感覺任務并不是很難&#xff0c;有些任務沒完成是 因為還沒買電腦不好弄&#xff0c;下周電腦一定到位。 ***遇到的問題…

升級MariaDB為10.1版本

2019獨角獸企業重金招聘Python工程師標準>>> CentOS中升級mariadb為10.1GA版本。 1、如果有&#xff0c;停止服務 systemctl stop mariadb 2、卸載原來的數據庫服務 yum -y remove mari* 3、刪除數據庫文件 rm -rf /var/lib/mysql/* 4.創建/etc/yum.repos.d/MariaDB…

第一篇文章

第一次寫博客。歡迎各位大牛捧場轉載于:https://www.cnblogs.com/clnchanpin/p/6753665.html

羊了個羊的Ignite大會又來啦

據說最近羊了個羊非常火啊&#xff5e;可惜沒有時間精力研究。不過&#xff0c;薅微軟羊毛的機會我是一定不會錯過的&#xff0c;這不&#xff0c;薅羊毛的機會來了&#xff0c;哈哈哈。作為經常薅微軟羊毛的老司機&#xff0c;今天收到了微軟的郵件&#xff0c;告知有新的羊毛…

清除谷歌瀏覽器的dns緩存

谷歌地址欄輸入&#xff1a; chrome://net-internals/#dns出現下面界面&#xff1a;找到DNS選項&#xff0c;選擇clear host cache即可效果&#xff1a;這樣&#xff0c;谷歌瀏覽器上的dns緩存就清理掉了。應用場景&#xff1a; 本地環境和線上環境用的是一個host&#xff0c;這…

生產YUM源搭建

企業內部YUM源搭建轉載于:https://www.cnblogs.com/xiangtanglaojing/p/7603581.html

什么樣的代碼稱得上是好代碼?

“軟件自有其美感所在” --《重構》圖片&#xff1a;崇禮瀚海梁的山花 拍攝于2022年8月13日 攝影師&#xff1a;劉先生這篇內容寫作于4年前&#xff08;2018年&#xff09;&#xff0c;是自己多年軟件開發工作的一點感悟&#xff0c;現在看來雖有偏頗&#xff0c;但總體思想方…

Coding and Paper Letter(十四)

2019獨角獸企業重金招聘Python工程師標準>>> 資源整理。 1 Coding: 1.R語言包ungeviz&#xff0c;ggplot2的拓展包&#xff0c;專門用來作不確定性的可視化。 ungeviz 2.計算機圖形學相關開源項目。 計算機圖形學光線追蹤開源項目C源碼。 computer graphics ray tra…

QBC運算符含義

HQL運算符 QBC運算符 含義 Restrictions.eq() 等于 <> Restrictions.not(Exprission.eq()) 不等于 > Restrictions.gt() …

eclipse安裝反編譯插件

一、下載插件 1、官方地址&#xff1a;http://jd.benow.ca/ 2、百度網盤&#xff1a;http://pan.baidu.com/s/1eSJ7Tiq 密碼&#xff1a;sr6p 二、打開eclipse&#xff0c;點擊“Help > Install New Software” 三、Name填&#xff1a;JD-Eclipse Update Site&#xff08;可…