使用 ViewDragHelper 实现左右侧滑
2016-04-24 09:36
645 查看
本文将一步步实现一个可左右侧滑的布局。
支持库里有个 SlidingPanelLayout ,实现了左侧侧滑。它是面面俱到的,1600+ 的源码。
ViewDragHelper 介绍:http://blog.csdn.net/lmj623565791/article/details/46858663。
ViewDragHelper 简化事件处理了 ,还是让事情变的更复杂?
但在实现后续功能时,可能受到极大的干扰。(精力是有限的,否则发明面向对象干嘛)
把 MainUI 换成 ScrollView,并改动两个地方:
情况一,水平拖拽的过程中,垂直移动不会影响到 ScrollView。
情况二,垂直滑动 ScrollView 的过程中,水平移动也不会产生拖拽效果。
shouldInterpcept 不会拦截 DOWN 事件,DOWN 事件被 ScrollView 消费。(ScrollView onTouchEvent 末尾返回 true 和这里的 onTouchEvent 一个道理)
MOVE 事件到来时,因为 mFirstTouchTarget 不为 null(即 ScrollView),也是由 shouldInterceptTouchEvent 来判断是否拦截。
首先,小于最小滑动距离 mTouchSlop (具体大小和 sensitivity 有关,这里假设为1)的 MOVE 事件,都不会被拦截。(ScrollView 对这种事件也不会有响应)
接着,大致就是:水平滑动会拦截,垂直滑动不拦截。(源码太长不看)
日志:
04-24 11:58:24.473 30948-30948/femade.viewdrag E/result: interceptForDrag false 0
04-24 11:58:24.548 30948-30948/femade.viewdrag E/result: interceptForDrag false 2
04-24 11:58:24.565 30948-30948/femade.viewdrag E/result: interceptForDrag true 2
第一个是 Down 事件,不拦截。
第二个滑动距离小于 TouchSlop,不拦截。
第三个滑动距离大于 TouchSlop,拦截。
如果拦截了,会给 ScrollView 发一个 Cancel 事件,就是说后续事件不用它操心了。
并且
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
会把 mFirstTouchEvent 重置为空。(单点触控的话,next 就是 null。多点触控,我脑子不好使,不管了。)
这样一来,下个 MOVE 事件到来时,不会再走 onInterceptTouchEvent ,而是直接 intercept = true。
所以 interceptForDrag 一共就只调用了 3 次。
从第三个 MOVE 事件起,所有后续事件,由 DragLayout 的 onTouchEvent,也就是 processEvent 处理,拖拽生效。
04-24 12:17:19.657 30948-30948/femade.viewdrag E/result: interceptForDrag false 0
04-24 12:17:19.693 30948-30948/femade.viewdrag E/result: interceptForDrag false 2
第一个,DOWN 不拦截。
第二个,MOVE 不拦截,小于 TouchSlop。
然后,就没后续了,这是怎么回事呢?
原因在 ScrollView 上,当然他确认了滑动产生时(垂直滑动距离大于 TouchSlop),会执行如下代码:
当 UP 事件到来时,会调用 resetTouchState 重置 disallowIntercept。
讲道理,Android 的事件分发是真的烦。
这两种情况,就像是 DragLayout 和 ScrollView 在抢夺事件处理权(先是垂直滑动,还是水平滑动),
一方成功时,另一方就没机会处理后续事件了。
侧滑栏有两种滑动方式。
一种是快速滑动,会使侧滑栏立即打开或者关闭。
一种是缓慢拖拽,松开手指后会根据滑动距离选择打开或关闭。
可能会想到这么个问题:
如果在自动滑动的过程中,再去拖拽 DragLayout 会不会产生问题。
实际上是不会的。
ViewDragHelper 中用 STATE_SETTLING 来描述自动滑动的状态。
SideMenu 展开时,ScrollView 依旧能正常工作,改进下。
这样 ScrollView 就没有等到垂直滑动距离大于 TouchSlop 的机会了。
当前侧栏展开时,“点击” Main UI 时,关闭侧滑栏。
把 sensitivity 适当调低即可。
、
当然是在处理我目前考虑不及的问题。。。。
代码还有 BUG,仍待完善。
写在前面
想实现左右侧滑,又不想去读 https://github.com/jfeinstein10/SlidingMenu 的源码。支持库里有个 SlidingPanelLayout ,实现了左侧侧滑。它是面面俱到的,1600+ 的源码。
ViewDragHelper 介绍:http://blog.csdn.net/lmj623565791/article/details/46858663。
ViewDragHelper 简化事件处理了 ,还是让事情变的更复杂?
最简单的版本
// 继承 FrameLayout,省去了 measure 和 layout 的烦恼 public class DragLayout extends FrameLayout { public DragLayout(Context context) { super(context); init(); } public DragLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @TargetApi(21) public DragLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } // ViewDragHelper dragHelper; // 可拖拽距离 int dragRange; @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); dragRange = getChildAt(0).getWidth(); } private void init() { dragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { // 只有 MainUI 可以拖拽 return indexOfChild(child) == getChildCount() - 1; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { // 限制水平拖拽范围 return Math.min(Math.max(left, 0), dragRange); } @Override public int clampViewPositionVertical(View child, int top, int dy) { // 竖直方向始终为 0 return 0; } }); } @Override public boolean onTouchEvent(MotionEvent ev) { dragHelper.processTouchEvent(ev); return true; }
<femade.viewdrag.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="800px" android:layout_height="match_parent" android:background="#222222" android:gravity="center" android:text="LeftSideMenu" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_blue_light" android:gravity="center" android:textColor="@android:color/white" android:text="MainUI" /> </femade.viewdrag.DragLayout>
ViewDragHelper 确实简化了事件处理
即使不用 ViewDragHelper,实现的拖拽也没什么难度。但在实现后续功能时,可能受到极大的干扰。(精力是有限的,否则发明面向对象干嘛)
事件分析
因为 TextView MainUI 不会消耗 DOWN 事件,DOWN 事件和可能的后续事件都由 DragLayout 的 onTouchEvent 处理:public boolean onTouchEvent(MotionEvent ev) { dragHelper.processTouchEvent(ev); return true; }return true 是为了接收后续事件。
版本二
MainUI 一般来说都会消费 DOWN 事件,那么 onTouchEvent 不会被执行,processTouchEvent 不会被执行,也就无法拖拽了。把 MainUI 换成 ScrollView,并改动两个地方:
@Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean interceptForDrag = dragHelper.shouldInterceptTouchEvent(event); Log.e("result","interceptForDrag " + interceptForDrag + " " +event.getAction()); return interceptForDrag; }
@Override public int getViewHorizontalDragRange(View child) { return dragRange;// 大于0即可 }
情况一,水平拖拽的过程中,垂直移动不会影响到 ScrollView。
情况二,垂直滑动 ScrollView 的过程中,水平移动也不会产生拖拽效果。
// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
事件分析 情况一 先水平滑动
DOWN 事件到来,由 onInterceptTouchEvent 判断是否拦截,即 shouldInterceptTouchEvent 判断是否拦截。shouldInterpcept 不会拦截 DOWN 事件,DOWN 事件被 ScrollView 消费。(ScrollView onTouchEvent 末尾返回 true 和这里的 onTouchEvent 一个道理)
MOVE 事件到来时,因为 mFirstTouchTarget 不为 null(即 ScrollView),也是由 shouldInterceptTouchEvent 来判断是否拦截。
首先,小于最小滑动距离 mTouchSlop (具体大小和 sensitivity 有关,这里假设为1)的 MOVE 事件,都不会被拦截。(ScrollView 对这种事件也不会有响应)
接着,大致就是:水平滑动会拦截,垂直滑动不拦截。(源码太长不看)
日志:
04-24 11:58:24.473 30948-30948/femade.viewdrag E/result: interceptForDrag false 0
04-24 11:58:24.548 30948-30948/femade.viewdrag E/result: interceptForDrag false 2
04-24 11:58:24.565 30948-30948/femade.viewdrag E/result: interceptForDrag true 2
第一个是 Down 事件,不拦截。
第二个滑动距离小于 TouchSlop,不拦截。
第三个滑动距离大于 TouchSlop,拦截。
如果拦截了,会给 ScrollView 发一个 Cancel 事件,就是说后续事件不用它操心了。
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; }
并且
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
会把 mFirstTouchEvent 重置为空。(单点触控的话,next 就是 null。多点触控,我脑子不好使,不管了。)
这样一来,下个 MOVE 事件到来时,不会再走 onInterceptTouchEvent ,而是直接 intercept = true。
所以 interceptForDrag 一共就只调用了 3 次。
从第三个 MOVE 事件起,所有后续事件,由 DragLayout 的 onTouchEvent,也就是 processEvent 处理,拖拽生效。
事件分析 情况二 先垂直滑动
先看日志:04-24 12:17:19.657 30948-30948/femade.viewdrag E/result: interceptForDrag false 0
04-24 12:17:19.693 30948-30948/femade.viewdrag E/result: interceptForDrag false 2
第一个,DOWN 不拦截。
第二个,MOVE 不拦截,小于 TouchSlop。
然后,就没后续了,这是怎么回事呢?
原因在 ScrollView 上,当然他确认了滑动产生时(垂直滑动距离大于 TouchSlop),会执行如下代码:
final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); }然后 DragLayout 的 onInterceptTouchEvent 就不会被执行了,后续事件都被分发给 ScrollView。
当 UP 事件到来时,会调用 resetTouchState 重置 disallowIntercept。
讲道理,Android 的事件分发是真的烦。
这两种情况,就像是 DragLayout 和 ScrollView 在抢夺事件处理权(先是垂直滑动,还是水平滑动),
一方成功时,另一方就没机会处理后续事件了。
版本三
侧滑栏有两种滑动方式。
一种是快速滑动,会使侧滑栏立即打开或者关闭。
一种是缓慢拖拽,松开手指后会根据滑动距离选择打开或关闭。
ViewDragHelper dragHelper; // 可拖拽距离 int dragRange; // 拖拽百分比 0~1 float dragOffset = 0.0f; @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); dragRange = getChildAt(0).getWidth(); } private void init() { dragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { // 只有 MainUI 可以拖拽 return indexOfChild(child) == getChildCount() - 1; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { // 限制水平拖拽范围 return Math.min(Math.max(left, 0), dragRange); } @Override public int clampViewPositionVertical(View child, int top, int dy) { // 竖直方向始终为 0 return 0; } @Override public int getViewHorizontalDragRange(View child) { return dragRange; } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { // 拖拽时更新拖拽百分比 dragOffset = (float) (left) / dragRange; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { int finalOffset; // xvel 是速度,DragViewHelper 可以设置最小捕捉速度(就像 TouchSlop 是最短滑动距离) // 如果 xvel 大于 0 ,则执行类似 fling 的行为。 if (Math.abs(xvel) > 0) { finalOffset = xvel > 0 ? 1 : 0; } // 没有捕捉到速度,则处于缓慢拖拽中。 // 如果释放时距离大于 0.5,则滑动到 1。反之滑动到 0。 else { finalOffset = dragOffset > 0.5 ? 1 : 0; } int finalLeft = finalOffset * dragRange; // 类似 Scroll 的用法 dragHelper.smoothSlideViewTo(releasedChild, finalLeft, releasedChild.getTop()); invalidate(); } }); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean interceptForDrag = dragHelper.shouldInterceptTouchEvent(event); Log.e("result","interceptForDrag " + interceptForDrag + " " +event.getAction()); return interceptForDrag; } @Override public boolean onTouchEvent(MotionEvent ev) { dragHelper.processTouchEvent(ev); return true; } // 类似 Scroll 的用法 @Override public void computeScroll() { if (dragHelper.continueSettling(true)) { invalidate(); } }
可能会想到这么个问题:
如果在自动滑动的过程中,再去拖拽 DragLayout 会不会产生问题。
实际上是不会的。
ViewDragHelper 中用 STATE_SETTLING 来描述自动滑动的状态。
// Catch a settling view if possible. if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); }ViewDragHelper 会特殊处理的,细节不看了。
版本四 这里完成三个优化,否则版本太多了。。。
优化一
SideMenu 展开时,ScrollView 依旧能正常工作,改进下。
@Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean interceptTap = false; if (dragOffset == 1 && dragHelper.isViewUnder(getChildAt(getChildCount() - 1), (int) event.getX(), (int) event.getY())) { interceptTap = true; } boolean interceptForDrag = dragHelper.shouldInterceptTouchEvent(event); return interceptTap || interceptForDrag; }当 dragOffset == 1(侧栏展开),落在 ScrollView 中的事件,一律拦截。
这样 ScrollView 就没有等到垂直滑动距离大于 TouchSlop 的机会了。
优化二
当前侧栏展开时,“点击” Main UI 时,关闭侧滑栏。
private float mInitialMotionX; private float mInitialMotionY; @Override public boolean onTouchEvent(MotionEvent ev) { dragHelper.processTouchEvent(ev); final int action = ev.getAction(); switch (action & MotionEventCompat.ACTION_MASK) { // 因为 interceptTap 的关系,DOWN 事件就被拦截了,交由 onTouchEvent 处理。 case MotionEvent.ACTION_DOWN: { mInitialMotionX = ev.getX(); mInitialMotionY = ev.getY(); break; } case MotionEvent.ACTION_UP: { if (dragOffset == 1) { final float x = ev.getX(); final float y = ev.getY(); final float dx = x - mInitialMotionX; final float dy = y - mInitialMotionY; final int slop = dragHelper.getTouchSlop(); View dragView = getChildAt(getChildCount() - 1); // 如果 UP 时,移动的距离足够小,则认为它是一个 “Tap” 事件 if (dx * dx + dy * dy < slop * slop && dragHelper.isViewUnder(dragView, (int) x, (int) y)) { dragHelper.smoothSlideViewTo(dragView, 0, dragView.getTop()); break; } } break; } } return true; }
优化三
因为 ViewDragHelper 太过敏感,不是很笔直的垂直滑动,会被误认为水平拖拽。把 sensitivity 适当调低即可。
dragHelper = ViewDragHelper.create(this, 0.5f, new ViewDragHelper.Callback()
版本五 实现左右侧滑
、
public class DragLayout extends FrameLayout { public DragLayout(Context context) { super(context); init(); } public DragLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @TargetApi(21) public DragLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } // ViewDragHelper dragHelper; View dragView; View leftSideMenu; View rightSideMenu; int dragRange; float dragOffset = 0.0f; @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); leftSideMenu = getChildAt(0); rightSideMenu = getChildAt(1); dragView = getChildAt(2); dragRange = leftSideMenu.getWidth(); } private void init() { dragHelper = ViewDragHelper.create(this, 0.5f, new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return child == dragView; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (left > 0) { leftSideMenu.setVisibility(VISIBLE); rightSideMenu.setVisibility(GONE); } else { leftSideMenu.setVisibility(GONE); rightSideMenu.setVisibility(VISIBLE); } return Math.min(Math.max(left, -dragRange), dragRange); } @Override public int clampViewPositionVertical(View child, int top, int dy) { return 0; } @Override public int getViewHorizontalDragRange(View child) { return dragRange; } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { dragOffset = (float) (left) / dragRange; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { int finalOffset; if (Math.abs(xvel) > 0) { if (xvel > 0) { if (dragOffset < 0) { finalOffset = 0; } else { finalOffset = 1; } } else { if (dragOffset > 0) { finalOffset = 0; } else { finalOffset = -1; } } } else { if (dragOffset < -0.5) { finalOffset = -1; } else if (dragOffset > 0.5) { finalOffset = 1; } else { finalOffset = 0; } } int finalLeft = finalOffset * dragRange; dragHelper.smoothSlideViewTo(releasedChild, finalLeft, releasedChild.getTop()); invalidate(); } }); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean interceptTap = false; if (Math.abs(dragOffset) == 1 && dragHelper.isViewUnder(dragView, (int) event.getX(), (int) event.getY())) { interceptTap = true; } boolean interceptForDrag = dragHelper.shouldInterceptTouchEvent(event); return interceptTap || interceptForDrag; } private float mInitialMotionX; private float mInitialMotionY; @Override public boolean onTouchEvent(MotionEvent ev) { dragHelper.processTouchEvent(ev); final int action = ev.getAction(); switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { mInitialMotionX = ev.getX(); mInitialMotionY = ev.getY(); break; } case MotionEvent.ACTION_UP: { if (Math.abs(dragOffset) == 1) { final float x = ev.getX(); final float y = ev.getY(); final float dx = x - mInitialMotionX; final float dy = y - mInitialMotionY; final int slop = dragHelper.getTouchSlop(); if (dx * dx + dy * dy < slop * slop && dragHelper.isViewUnder(dragView, (int) x, (int) y)) { dragHelper.smoothSlideViewTo(dragView, 0, dragView.getTop()); break; } } break; } } return true; } @Override public void computeScroll() { if (dragHelper.continueSettling(true)) { invalidate(); } } }
<femade.viewdrag.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/left" android:layout_width="800px" android:layout_height="match_parent" android:background="#222222" android:gravity="center" android:text="LeftSideMenu" /> <TextView android:id="@+id/right" android:layout_width="800px" android:layout_height="match_parent" android:layout_gravity="end" android:background="#222222" android:gravity="center" android:text="RightSideMenu" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content" android:overScrollMode="never" android:scrollbars="none"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="1000px" android:background="@android:color/holo_blue_light" android:gravity="center" android:text="MainUI" android:textColor="@android:color/white" /> <TextView android:layout_width="match_parent" android:layout_height="1000px" android:background="@android:color/holo_red_light" android:gravity="center" android:text="MainUI" android:textColor="@android:color/white" /> </LinearLayout> </ScrollView> </femade.viewdrag.DragLayout>
写在后面
总共 180 左右代码,那么 SlidingPanelLayout 的 1600+ 在干嘛。当然是在处理我目前考虑不及的问题。。。。
代码还有 BUG,仍待完善。
相关文章推荐
- Spark Shuffle系列-----1. Spark Shuffle与任务调度之间的关系
- getpid()函数
- The Lucky Week
- 第 9 章 路径分页标签和徽章组件
- Android ProgressBar使用
- "软件包 opera-stable 需要重新安装,但是我无法找到相应的安装文件 " 解决办法
- 【freemarker】使用模板导出word
- HBase 数据库检索性能优化策略
- 多进程DP
- linux硬链接和软链接的区别
- suid sgid sticky-bit 三种特殊权限简介
- centos svn server
- Android下拉刷新
- 机房重构——组合查询
- Android 从网络加载图片
- XDU-1139 猴子吃桃 II (水~斐波那契数列求和) From 西电校赛网络赛
- android Gradle的神奇之处
- BZOJ1338: Pku1981 Circle and Points单位圆覆盖
- 训练深度神经网络的时候需要注意的技巧
- XDU-1138 Z1+Z2 (水~复数相加) From 西电校赛网络赛