您的位置:首页 > 其它

使用 ViewDragHelper 实现左右侧滑

2016-04-24 09:36 645 查看
本文将一步步实现一个可左右侧滑的布局。

写在前面

想实现左右侧滑,又不想去读  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,仍待完善。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: