ViewDragHelper詳解

2013年谷歌i/o大會上介紹了兩個新的layout: SlidingPaneLayout和DrawerLayout,現在這倆個類被廣泛的運用,其實研究他們的源碼你會發現這兩個類都運用了ViewDragHelper來處理拖動。ViewDragHelper是framework中不為人知卻非常有用的一個工具

ViewDragHelper解決了android中手勢處理過于復雜的問題,在DrawerLayout出現之前,側滑菜單都是由第三方開源代碼實現的,其中著名的當屬?MenuDrawer?,MenuDrawer重寫onTouchEvent方法來實現側滑效果,代碼量很大,實現邏輯也需要很大的耐心才能看懂。如果每個開發人員都從這么原始的步奏開始做起,那對于安卓生態是相當不利的。所以說ViewDragHelper等的出現反映了安卓開發框架已經開始向成熟的方向邁進。

本文先介紹ViewDragHelper的基本用法,然后介紹一個能真正體現ViewDragHelper實用性的例子。

ViewDragHelper

其實ViewDragHelper并不是第一個用于分析手勢處理的類,gesturedetector也是,但是在和拖動相關的手勢分析方面gesturedetector只能說是勉為其難。

關于ViewDragHelper有如下幾點:

? ?ViewDragHelper.Callback是連接ViewDragHelper與view之間的橋梁(這個view一般是指擁子view的容器即parentView);

? ?ViewDragHelper的實例是通過靜態工廠方法創建的;

? ?你能夠指定拖動的方向;

? ?ViewDragHelper可以檢測到是否觸及到邊緣;

? ?ViewDragHelper并不是直接作用于要被拖動的View,而是使其控制的視圖容器中的子View可以被拖動,如果要指定某個子view的行為,需要在Callback中想辦法;

? ?ViewDragHelper的本質其實是分析onInterceptTouchEvent和onTouchEvent的MotionEvent參數,然后根據分析的結果去改變一個容器中被拖動子View的位置(?通過offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法?),他能在觸摸的時候判斷當前拖動的是哪個子View;

? ?雖然ViewDragHelper的實例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一個被ViewDragHelper處理拖動事件的對象 。

用法:

下面部分內容基本是Each Navigation Drawer Hides a ViewDragHelper一文的翻譯。

1.ViewDragHelper的初始化

ViewDragHelper一般用在一個自定義ViewGroup的內部,比如下面自定義了一個繼承于LinearLayout的DragLayout,DragLayout內部有一個子view mDragView作為成員變量:

public?class?DragLayout?extends?LinearLayout?{

private?final?ViewDragHelper?mDragHelper;

private?View?mDragView;

public?DragLayout(Context?context)?{

??this(context,?null);

}

public?DragLayout(Context?context,?AttributeSet?attrs)?{

??this(context,?attrs,?0);

}

public?DragLayout(Context?context,?AttributeSet?attrs,?int?defStyle)?{

??super(context,?attrs,?defStyle);

}

創建一個帶有回調接口的ViewDragHelper

public?DragLayout(Context?context,?AttributeSet?attrs,?int?defStyle)?{

??super(context,?attrs,?defStyle);

??mDragHelper?=?ViewDragHelper.create(this,?1.0f,?new?DragHelperCallback());

}

其中1.0f是敏感度參數參數越大越敏感。第一個參數為this,表示該類生成的對象,他是ViewDragHelper的拖動處理對象,必須為ViewGroup。

要讓ViewDragHelper能夠處理拖動需要將觸摸事件傳遞給ViewDragHelper,這點和gesturedetector是一樣的:

@Override

public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{

??final?int?action?=?MotionEventCompat.getActionMasked(ev);

??if?(action?==?MotionEvent.ACTION_CANCEL?||?action?==?MotionEvent.ACTION_UP)?{

??????mDragHelper.cancel();

??????return?false;

??}

??return?mDragHelper.shouldInterceptTouchEvent(ev);

}

@Override

public?boolean?onTouchEvent(MotionEvent?ev)?{

??mDragHelper.processTouchEvent(ev);

??return?true;

}

接下來,你就可以在回調中處理各種拖動行為了。

2.拖動行為的處理

處理橫向的拖動:

在DragHelperCallback中實現clampViewPositionHorizontal方法, 并且返回一個適當的數值就能實現橫向拖動效果,clampViewPositionHorizontal的第二個參數是指當前拖動子view應該到達的x坐標。所以按照常理這個方法原封返回第二個參數就可以了,但為了讓被拖動的view遇到邊界之后就不在拖動,對返回的值做了更多的考慮。

@Override

public?int?clampViewPositionHorizontal(View?child,?int?left,?int?dx)?{

??Log.d("DragLayout",?"clampViewPositionHorizontal?"?+?left?+?","?+?dx);

??final?int?leftBound?=?getPaddingLeft();

??final?int?rightBound?=?getWidth()?-?mDragView.getWidth();

??final?int?newLeft?=?Math.min(Math.max(left,?leftBound),?rightBound);

??return?newLeft;

}

同上,處理縱向的拖動:

在DragHelperCallback中實現clampViewPositionVertical方法,實現過程同clampViewPositionHorizontal

@Override

public?int?clampViewPositionVertical(View?child,?int?top,?int?dy)?{

??final?int?topBound?=?getPaddingTop();

??final?int?bottomBound?=?getHeight()?-?mDragView.getHeight();

??final?int?newTop?=?Math.min(Math.max(top,?topBound),?bottomBound);

??return?newTop;

}

clampViewPositionHorizontal 和 clampViewPositionVertical必須要重寫,因為默認它返回的是0。事實上我們在這兩個方法中所能做的事情很有限。 個人覺得這兩個方法的作用就是給了我們重新定義目的坐標的機會。

通過DragHelperCallback的tryCaptureView方法的返回值可以決定一個parentview中哪個子view可以拖動,現在假設有兩個子views (mDragView1和mDragView2) ?,如下實現tryCaptureView之后,則只有mDragView1是可以拖動的。

@Override

public?boolean?tryCaptureView(View?child,?int?pointerId)?{

??return?child?==?mDragView1;

}

滑動邊緣:

分為滑動左邊緣還是右邊緣:EDGE_LEFT和EDGE_RIGHT,下面的代碼設置了可以處理滑動左邊緣:

mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

假如如上設置,onEdgeTouched方法會在左邊緣滑動的時候被調用,這種情況下一般都是沒有和子view接觸的情況。

@Override

public?void?onEdgeTouched(int?edgeFlags,?int?pointerId)?{

????super.onEdgeTouched(edgeFlags,?pointerId);

????Toast.makeText(getContext(),?"edgeTouched",?Toast.LENGTH_SHORT).show();

}

如果你想在邊緣滑動的時候根據滑動距離移動一個子view,可以通過實現onEdgeDragStarted方法,并在onEdgeDragStarted方法中手動指定要移動的子View

@Override

public?void?onEdgeDragStarted(int?edgeFlags,?int?pointerId)?{

????mDragHelper.captureChildView(mDragView2,?pointerId);

}

ViewDragHelper讓我們很容易實現一個類似于YouTube視頻瀏覽效果的控件,效果如下:

代碼中的關鍵點:

1.tryCaptureView返回了唯一可以被拖動的header view;

2.拖動范圍drag range的計算是在onLayout中完成的;

3.注意在onInterceptTouchEvent和onTouchEvent中使用的ViewDragHelper的若干方法;

4.在computeScroll中使用continueSettling方法(因為ViewDragHelper使用了scroller)?

5.smoothSlideViewTo方法來完成拖動結束后的慣性操作。

需要注意的是代碼仍然有很大改進空間。

activity_main.xml

????????xmlns:android="http://schemas.android.com/apk/res/android"

????????android:layout_width="match_parent"

????????android:layout_height="match_parent">


????????????android:id="@+id/listView"

????????????android:layout_width="match_parent"

????????????android:layout_height="match_parent"

????????????android:tag="list"

????????????/>


????????????android:layout_width="match_parent"

????????????android:layout_height="match_parent"

????????????android:id="@+id/youtubeLayout"

????????????android:orientation="vertical"

????????????android:visibility="visible">


????????????????android:id="@+id/viewHeader"

????????????????android:layout_width="match_parent"

????????????????android:layout_height="128dp"

????????????????android:fontFamily="sans-serif-thin"

????????????????android:textSize="25sp"

????????????????android:tag="text"

????????????????android:gravity="center"

????????????????android:textColor="@android:color/white"

????????????????android:background="#AD78CC"/>


????????????????android:id="@+id/viewDesc"

????????????????android:tag="desc"

????????????????android:textSize="35sp"

????????????????android:gravity="center"

????????????????android:text="Loreum?Loreum"

????????????????android:textColor="@android:color/white"

????????????????android:layout_width="match_parent"

????????????????android:layout_height="match_parent"

????????????????android:background="#FF00FF"/>


YoutubeLayout.java

public?class?YoutubeLayout?extends?ViewGroup?{

private?final?ViewDragHelper?mDragHelper;

private?View?mHeaderView;

private?View?mDescView;

private?float?mInitialMotionX;

private?float?mInitialMotionY;

private?int?mDragRange;

private?int?mTop;

private?float?mDragOffset;

public?YoutubeLayout(Context?context)?{

??this(context,?null);

}

public?YoutubeLayout(Context?context,?AttributeSet?attrs)?{

??this(context,?attrs,?0);

}

@Override

protected?void?onFinishInflate()?{

????mHeaderView?=?findViewById(R.id.viewHeader);

????mDescView?=?findViewById(R.id.viewDesc);

}

public?YoutubeLayout(Context?context,?AttributeSet?attrs,?int?defStyle)?{

??super(context,?attrs,?defStyle);

??mDragHelper?=?ViewDragHelper.create(this,?1f,?new?DragHelperCallback());

}

public?void?maximize()?{

????smoothSlideTo(0f);

}

boolean?smoothSlideTo(float?slideOffset)?{

????final?int?topBound?=?getPaddingTop();

????int?y?=?(int)?(topBound?+?slideOffset?*?mDragRange);

????if?(mDragHelper.smoothSlideViewTo(mHeaderView,?mHeaderView.getLeft(),?y))?{

????????ViewCompat.postInvalidateOnAnimation(this);

????????return?true;

????}

????return?false;

}

private?class?DragHelperCallback?extends?ViewDragHelper.Callback?{

??@Override

??public?boolean?tryCaptureView(View?child,?int?pointerId)?{

????????return?child?==?mHeaderView;

??}

????@Override

??public?void?onViewPositionChanged(View?changedView,?int?left,?int?top,?int?dx,?int?dy)?{

??????mTop?=?top;

??????mDragOffset?=?(float)?top?/?mDragRange;

????????mHeaderView.setPivotX(mHeaderView.getWidth());

????????mHeaderView.setPivotY(mHeaderView.getHeight());

????????mHeaderView.setScaleX(1?-?mDragOffset?/?2);

????????mHeaderView.setScaleY(1?-?mDragOffset?/?2);

????????mDescView.setAlpha(1?-?mDragOffset);

????????requestLayout();

??}

??@Override

??public?void?onViewReleased(View?releasedChild,?float?xvel,?float?yvel)?{

??????int?top?=?getPaddingTop();

??????if?(yvel?>?0?||?(yvel?==?0?&&?mDragOffset?>?0.5f))?{

??????????top?+=?mDragRange;

??????}

??????mDragHelper.settleCapturedViewAt(releasedChild.getLeft(),?top);

??}

??@Override

??public?int?getViewVerticalDragRange(View?child)?{

??????return?mDragRange;

??}

??@Override

??public?int?clampViewPositionVertical(View?child,?int?top,?int?dy)?{

??????final?int?topBound?=?getPaddingTop();

??????final?int?bottomBound?=?getHeight()?-?mHeaderView.getHeight()?-?mHeaderView.getPaddingBottom();

??????final?int?newTop?=?Math.min(Math.max(top,?topBound),?bottomBound);

??????return?newTop;

??}

}

@Override

public?void?computeScroll()?{

??if?(mDragHelper.continueSettling(true))?{

??????ViewCompat.postInvalidateOnAnimation(this);

??}

}

@Override

public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{

??final?int?action?=?MotionEventCompat.getActionMasked(ev);

??if?((?action?!=?MotionEvent.ACTION_DOWN))?{

??????mDragHelper.cancel();

??????return?super.onInterceptTouchEvent(ev);

??}

??if?(action?==?MotionEvent.ACTION_CANCEL?||?action?==?MotionEvent.ACTION_UP)?{

??????mDragHelper.cancel();

??????return?false;

??}

??final?float?x?=?ev.getX();

??final?float?y?=?ev.getY();

??boolean?interceptTap?=?false;

??switch?(action)?{

??????case?MotionEvent.ACTION_DOWN:?{

??????????mInitialMotionX?=?x;

??????????mInitialMotionY?=?y;

????????????interceptTap?=?mDragHelper.isViewUnder(mHeaderView,?(int)?x,?(int)?y);

??????????break;

??????}

??????case?MotionEvent.ACTION_MOVE:?{

??????????final?float?adx?=?Math.abs(x?-?mInitialMotionX);

??????????final?float?ady?=?Math.abs(y?-?mInitialMotionY);

??????????final?int?slop?=?mDragHelper.getTouchSlop();

??????????if?(ady?>?slop?&&?adx?>?ady)?{

??????????????mDragHelper.cancel();

??????????????return?false;

??????????}

??????}

??}

??return?mDragHelper.shouldInterceptTouchEvent(ev)?||?interceptTap;

}

@Override

public?boolean?onTouchEvent(MotionEvent?ev)?{

??mDragHelper.processTouchEvent(ev);

??final?int?action?=?ev.getAction();

????final?float?x?=?ev.getX();

????final?float?y?=?ev.getY();

????boolean?isHeaderViewUnder?=?mDragHelper.isViewUnder(mHeaderView,?(int)?x,?(int)?y);

????switch?(action?&?MotionEventCompat.ACTION_MASK)?{

??????case?MotionEvent.ACTION_DOWN:?{

??????????mInitialMotionX?=?x;

??????????mInitialMotionY?=?y;

??????????break;

??????}

??????case?MotionEvent.ACTION_UP:?{

??????????final?float?dx?=?x?-?mInitialMotionX;

??????????final?float?dy?=?y?-?mInitialMotionY;

??????????final?int?slop?=?mDragHelper.getTouchSlop();

??????????if?(dx?*?dx?+?dy?*?dy?<?slop?*?slop?&&?isHeaderViewUnder)?{

??????????????if?(mDragOffset?==?0)?{

??????????????????smoothSlideTo(1f);

??????????????}?else?{

??????????????????smoothSlideTo(0f);

??????????????}

??????????}

??????????break;

??????}

??}

??return?isHeaderViewUnder?&&?isViewHit(mHeaderView,?(int)?x,?(int)?y)?||?isViewHit(mDescView,?(int)?x,?(int)?y);

}

private?boolean?isViewHit(View?view,?int?x,?int?y)?{

????int[]?viewLocation?=?new?int[2];

????view.getLocationOnScreen(viewLocation);

????int[]?parentLocation?=?new?int[2];

????this.getLocationOnScreen(parentLocation);

????int?screenX?=?parentLocation[0]?+?x;

????int?screenY?=?parentLocation[1]?+?y;

????return?screenX?>=?viewLocation[0]?&&?screenX?<?viewLocation[0]?+?view.getWidth()?&&

????????????screenY?>=?viewLocation[1]?&&?screenY?<?viewLocation[1]?+?view.getHeight();

}

@Override

protected?void?onMeasure(int?widthMeasureSpec,?int?heightMeasureSpec)?{

????measureChildren(widthMeasureSpec,?heightMeasureSpec);

????int?maxWidth?=?MeasureSpec.getSize(widthMeasureSpec);

????int?maxHeight?=?MeasureSpec.getSize(heightMeasureSpec);

????setMeasuredDimension(resolveSizeAndState(maxWidth,?widthMeasureSpec,?0),

????????????resolveSizeAndState(maxHeight,?heightMeasureSpec,?0));

}

@Override

protected?void?onLayout(boolean?changed,?int?l,?int?t,?int?r,?int?b)?{

??mDragRange?=?getHeight()?-?mHeaderView.getHeight();

????mHeaderView.layout(

????????????0,

????????????mTop,

????????????r,

????????????mTop?+?mHeaderView.getMeasuredHeight());

????mDescView.layout(

????????????0,

????????????mTop?+?mHeaderView.getMeasuredHeight(),

????????????r,

????????????mTop??+?b);

}

不管是menudrawer 還是本文實現的DragLayout都體現了一種設計哲學,即可拖動的控件都是封裝在一個自定義的Layout中的,為什么這樣做?為什么不直接將ViewDragHelper.create(this, 1f, new DragHelperCallback())中的this替換成任何已經布局好的容器,這樣這個容器中的子View就能被拖動了,而往往是單獨定義一個Layout來處理?個人認為如果在一般的布局中去拖動子view并不會出現什么問題,只是原本規則的世界被打亂了,而單獨一個Layout來完成拖動,無非是說,他本來就沒有什么規則可言,拖動一下也無妨。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容