Each Navigation Drawer Hides a ViewDragHelper

本文介绍如何使用ViewDragHelper简化自定义布局中视图的拖动操作。通过实例演示了配置拖动方向、边界限制及触摸边缘触发等功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自 http://flavienlaurent.com/blog/2013/08/28/each-navigation-drawer-hides-a-viewdraghelper/

Recently, at the Google I/O 2013, two new layouts have been introduced: SlidingPaneLayout, a view that can be dragged from bottom to top and vice versa and the DrawerLayout, now used in almost all Google applications. Both of these use a new concept to more easily manage dragging: the ViewDragHelper.

In this article, I’m going to talk about the ViewDragHelper (aka VDH) because making a custom layout with dragging child view may be pain sometimes. First, I will show you how to use it and how it works (the main lines). Secondly, I will expose you a use case where the VDH is really useful.

You can download & install the sample application.

API design

In a few words

There are some important points to remember about VDH:

  • a (ViewDragHelper.Callback) is used as a communication channel between parent view and VDH
  • there is a static factory method to create a VDH instance
  • you can configure the drag direction as you want
  • a drag can be detected from edge even if there is no view to capture (left, right, top, bottom)

Remember to read the official documentation: ViewDragHelper and ViewDragHelper.Callback

Reading the source code

The VDH and its callback are available in the support-v4 library. You can read the source code: ViewDragHelper and ViewDragHelper.Callback.

It uses some common classes of the framework : – a VelocityTracker for>Scroller to scroll views when it’s needed.

You must read the source code as much as possible because first, it’s very interesting and then if you know how it works, you will be able to use it in a better way.

Using the VDH

In this section, I’m going to show you a few examples of what is possible to configure on a VDH. Let’s begin with some initializations and then, I will explain a few possible configurations.

VDH’s initialization

A custom ViewGroup extending a LinearLayout (DragLayout) with a simple child View (named mDragView).

code>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}

Create a VDH with its callback. Note that you can specify the sensivity (official documentation says Multiplier for how sensitive the helper should be about detecting the start of a drag. Larger values are more sensitive. 1.0f is normal.)

code>
1
2
3
4
5
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}

The most important part is to call VDH methods in onInterceptTouch and onTouch.

code>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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;
}

Now, you can change the VDH behavior only by configuring the callback.

Horizontal only

Implements clampViewPositionHorizontal to allow horizontal drag and to bound the drag motion. Note that documentation says The default implementation does not allow horizontal motion.

You have to take margins and parent padding into consideration. Not like in the code below

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

Vertical only

Implements clampViewPositionVertical to allow horizontal drag and to bound the drag motion. Note that documentation says The default implementation does not allow vertical motion.

You have to take margins and parent padding into consideration. Not like in the code below

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

Capture or not capture a view

Implements tryCaptureView to allow a child view to be captured. Here, there are two child views (mDragView1 and mDragView2) but only one (mDragView1) is draggable.

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

DragRange

Implements getViewHorizontalDragRange or getViewVerticalDragRange to returns the range of horizontal|vertical drag in pixels. This range is used by the VDH when you call smoothSlideViewTo or settleCapturedViewAt to calculate the scroll duration. Also, it’s used to check the horizontal|vertical touch slop.

Edge dragging

This feature is used in the DrawerLayout with EDGE_LEFT and EDGE_RIGHT.

Configure VDH to enable edge tracking.

code>
1
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

Implements onEdgeTouched called when the configured edge is touched. At this time, no child view is currently captured.

code>
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();
}

Implements onEdgeDragStarted called when a real drag from the configured edge has started. At this time, no child view is currently captured. In this method, you have to capture a child view manually.

code>
1
2
3
4
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    mDragHelper.captureChildView(mDragView2, pointerId);
}

A real example, the Youtube while playing layout

Recently, I’ve received an update of the Youtube app on my phone. Before this update, the most annoying thing was to not be able to watch a video and search the next video at the same time. They fixed this by implementing a nice layout in which you can minimize the video view from top to bottom.

I’m going to show how to do it and how it’s simple thanks to VDH.

Here is the expected result

Key points:

  • tryCaptureView returns true only for the header view
  • drag range is calculated onLayout
  • use VDH’s methods in onInterceptTouchEvent and onTouchEvent
  • call continueSettling in computeScroll (because VDH uses a scroller)
  • use smoothSlideViewTo to finish the drag motion

Be careful, this layout is not well made: it’s a draft. There is still work to do; on the scale part, touch event when the header is scaled, onLayout and onMeasure are badly written too. Also, I don’t know if calling requestLayout in onViewPositionChanged is >please tell me!).

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
39
40
41
42
43
44
<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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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);
}

Conclusion

The VDH is one of the useful but unknown class in the framework. Don’t hesitate to try it, use it and to appreciate it because it saves a lot of time and a lot of code!


1. 用户与权限管理模块 角色管理: 学生:查看实验室信息、预约设备、提交耗材申请、参与安全考核 教师:管理课题组预约、审批学生耗材申请、查看本课题组使用记录 管理员:设备全生命周期管理、审核预约、耗材采购与分发、安全检查 用户操作: 登录认证:统一身份认证(对接学号 / 工号系统,模拟实现),支持密码重置 信息管理:学生 / 教师维护个人信息(联系方式、所属院系),管理员管理所有用户 权限控制:不同角色仅可见对应功能(如学生不可删除设备信息) 2. 实验室与设备管理模块 实验室信息管理: 基础信息:实验室编号、名称、位置、容纳人数、开放时间、负责人 功能分类:按学科(计算机实验室 / 电子实验室 / 化学实验室)标记,关联可开展实验类型 状态展示:实时显示当前使用人数、设备运行状态(正常 / 故障) 设备管理: 设备档案:名称、型号、规格、购置日期、单价、生产厂家、存放位置、责任人 全生命周期管理: 入库登记:管理员录入新设备信息,生成唯一资产编号 维护记录:记录维修、校准、保养信息(时间、内容、执行人) 报废处理:登记报废原因、时间,更新设备状态为 "已报废" 设备查询:支持按名称、型号、状态多条件检索,显示设备当前可用情况 3. 预约与使用模块 预约管理: 预约规则:学生可预约未来 7 天内的设备 / 实验室,单次最长 4 小时(可设置) 预约流程:选择实验室→选择设备→选择时间段→提交申请(需填写实验目的) 审核机制:普通实验自动通过,高危实验(如化学实验)需教师审核 使用记录: 签到 / 签退:到达实验室后扫码签到,离开时签退,系统自动记录实际使用时长 使用登记:填写实验内容、设备运行情况(正常 / 异常),异常情况需详细描述 违规管理:迟到 15 分钟自动取消预约,多次违规限制预约权限 4. 耗材与安全管理模块 耗材管理: 耗材档案:名称、规格、数量、存放位置、
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值