ViewDragHelper详解

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/0911/1680.html


编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!

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作为成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
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是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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遇到边界之后就不在拖动,对返回的值做了更多的考虑。

1
2
3
4
5
6
7
8
@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

1
2
3
4
5
6
7
@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是可以拖动的。

1
2
3
4
@Override
public boolean tryCaptureView(View child, int pointerId) {
   return  child == mDragView1;
}

滑动边缘:

分为滑动左边缘还是右边缘:EDGE_LEFT和EDGE_RIGHT,下面的代码设置了可以处理滑动左边缘:

1
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

假如如上设置,onEdgeTouched方法会在左边缘滑动的时候被调用,这种情况下一般都是没有和子view接触的情况。

1
2
3
4
5
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
     super .onEdgeTouched(edgeFlags, pointerId);
     Toast.makeText(getContext(),  "edgeTouched" , Toast.LENGTH_SHORT).show();
}

如果你想在边缘滑动的时候根据滑动距离移动一个子view,可以通过实现onEdgeDragStarted方法,并在onEdgeDragStarted方法中手动指定要移动的子View

1
2
3
4
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<FrameLayout
         xmlns:android= "http://schemas.android.com/apk/res/android"
         android:layout_width= "match_parent"
         android:layout_height= "match_parent" >
     <ListView
             android:id= "@+id/listView"
             android:layout_width= "match_parent"
             android:layout_height= "match_parent"
             android:tag= "list"
             />
     <com.example.vdh.YoutubeLayout
             android:layout_width= "match_parent"
             android:layout_height= "match_parent"
             android:id= "@+id/youtubeLayout"
             android:orientation= "vertical"
             android:visibility= "visible" >
         <TextView
                 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" />
         <TextView
                 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" />
     </com.example.vdh.YoutubeLayout>
</FrameLayout>

YoutubeLayout.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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);
}

代码下载地址:https://github.com/flavienlaurent/flavienlaurent.com


不管是menudrawer 还是本文实现的DragLayout都体现了一种设计哲学,即可拖动的控件都是封装在一个自定义的Layout中的,为什么这样做?为什么不直接将ViewDragHelper.create(this, 1f, new DragHelperCallback())中的this替换成任何已经布局好的容器,这样这个容器中的子View就能被拖动了,而往往是单独定义一个Layout来处理?个人认为如果在一般的布局中去拖动子view并不会出现什么问题,只是原本规则的世界被打乱了,而单独一个Layout来完成拖动,无非是说,他本来就没有什么规则可言,拖动一下也无妨。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值