前言: 通过上一篇的为何说Android ViewDragHelper是神器 (一)中我们简单了解了ViewDragHelper的用法,然后实现了一个“view随手指滑动而滑动”的效果,代码很简单,但是VDH中处理的逻辑却很多很多,不得不说VDH真的是神器,要我们自己写的话得写一段时间了,接下来我们继续往下研究研究VDH,加油吧!骚年(^__^) !!!
以下demo内容大致参考鸿阳博客中的Android ViewDragHelper解析 一文,阳神一直是我崇拜的一个偶像(^__^) 。
ViewDragHelper还能做以下的一些操作:
边界检测、加速度检测(eg:DrawerLayout边界触发拉出)
移动到某个指定的位置(eg:点击Button,展开/关闭Drawerlayout)
回调Drag Release(eg:DrawerLayout部分,手指抬起,自动展开/收缩)
那么我们接下来对我们最基本的例子进行改造,包含上述的几个操作。
我们再创建两个view,id叫autobackview(拖动后手指一抬起返回初始位置),edgeview(滑动边缘开始滑动的view):
ids.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--可以被拖动的View-->
<item name="draggedview" type="id"/>
<!--拖动后手指一抬起返回初始位置-->
<item name="autobackview" type="id"/>
<!--滑动边缘开始滑动的view-->
<item name="edgeview" type="id"/>
</resources>
text_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.cisetech.demo.MainActivity">
<com.cisetech.demo.DragView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true">
<TextView
android:id="@id/draggedview"
android:layout_margin="10dp"
android:gravity="center"
android:layout_gravity="center"
android:background="#44ff00ff"
android:text="我可以被拖动"
android:layout_width="100dp"
android:layout_height="100dp"/>
<TextView
android:id="@id/autobackview"
android:layout_margin="10dp"
android:layout_gravity="center"
android:gravity="center"
android:background="#44ff"
android:text="我可以自动回到初始位置"
android:layout_width="100dp"
android:layout_height="100dp"/>
<TextView
android:id="@id/edgeview"
android:layout_margin="10dp"
android:layout_gravity="center"
android:gravity="center"
android:background="#44ff00"
android:text="滑动边缘移动我"
android:layout_width="100dp"
android:layout_height="100dp"/>
</com.cisetech.demo.DragView>
</RelativeLayout>
我们改改DragView:
package com.cisetech.demo;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
/**
* author:yinqingy
* date:2016-11-06 13:49
* blog:http://blog.csdn.net/vv_bug
* desc:
*/
public class DragView extends LinearLayout{
private ViewDragHelper mDragger;
private View mEdgeView,mAutoBackView;
private Point mAutoBackOriginPos=new Point();
public DragView(Context context) {
super(context);
init();
}
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mDragger=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//当id为edgeview的时候,不允许其滑动
return child.getId()==R.id.draggedview||child.getId()==R.id.autobackview;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
/**
* 当手指在边缘拖动的时候回调此方法
* edgeFlags分为left、top、right、bottom
*/
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
//当在边缘滑动的时候
mDragger.captureChildView(mEdgeView, pointerId);
}
//手指释放的时候回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//mAutoBackView手指释放时可以自动回去
if (releasedChild.getId()==R.id.autobackview) {
mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
invalidate();
}
}
});
//一定要加上这句代码,不然就checkNewEdgeDrag就不会进入判断了
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//获取autoBackView的初始位置
mAutoBackOriginPos.x = mAutoBackView.getLeft();
mAutoBackOriginPos.y = mAutoBackView.getTop();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mEdgeView=findViewById(R.id.edgeview);
mAutoBackView=findViewById(R.id.autobackview);
}
@Override
public void computeScroll() {
if(mDragger.continueSettling(true))
invalidate();
}
}
运行代码:
我们来分析下代码:
代码中都有注释,我就不一一解释了,先解释下边缘滑动的代码:
首先:
@Override
public boolean tryCaptureView(View child, int pointerId) {
//当id为edgeview的时候,不允许其滑动
return child.getId()==R.id.draggedview||child.getId()==R.id.autobackview;
}
不允许edgeview直接滑动,所以返回的是false,
然后:
/**
* 当手指在边缘拖动的时候回调此方法
* edgeFlags分为left、top、right、bottom
*/
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
//当在边缘滑动的时候
mDragger.captureChildView(mEdgeView, pointerId);
}
在当手指触碰到ViewGroup的边缘的时候,调用了mDragger.captureChildView方法,
最后:
//一定要加上这句代码,不然就checkNewEdgeDrag就不会进入判断了
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
很少的代码,我们的view就可以实现边缘滑动了(不了解边缘滑动的可以想象下侧滑菜单(^__^) 嘻嘻……),那么我们进入到VDH源码中看看为什么可以边缘滑动?
首先看看onEdgeDragStarted在哪调用的?
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
我们可以看到是在一个叫reportNewEdgeDrags的方法中调用的,那么reportNewEdgeDrags又是在哪调用的呢?
在VDH中的processTouchEvent方法中我们看到:
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
final float x = ev.getX(i);
final float y = ev.getY(i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy)
&& tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
当mDragState != STATE_DRAGGING的时候会调用reportNewEdgeDrags方法,在VDH中只有当mDragState ==STATE_DRAGGING的时候才能对view进行拖动,那么我们看看mDragState 在什么地方被置成STATE_DRAGGING标记的?
/**
* Capture a specific child view for dragging within the parent. The callback will be notified
* but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
* capture this view.
*
* @param childView Child view to capture
* @param activePointerId ID of the pointer that is dragging the captured child view
*/
public void captureChildView(View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
+ "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
在captureChildView中,我们很清晰的看到setDragState(STATE_DRAGGING);这么一段代码,当mDragg为STATE_DRAGGING状态的时候,当进入到processTouchEvent方法的ACTION_MOVE时,就会走:
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
}
调了dragTo方法就可以拖动了,dragTo在前一篇博客中有提及,我就不再说明了,到此,边界拖动的代码已经解析完毕了。
接下来看看松手自动返回的代码:
}
//手指释放的时候回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//mAutoBackView手指释放时可以自动回去
if (releasedChild.getId()==R.id.autobackview) {
mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
invalidate();
}
}
});
在手指松开的时候会调用onViewReleased方法,然后我们调用了VDH的settleCapturedViewAt方法,我们看看settleCapturedViewAt内部:
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
其内部主要调用了forceSettleCapturedViewAt方法,说到底还是调用了mScroller.startScroll(startLeft, startTop, dx, dy, duration);方法,Scroller的用法不懂的自己去脑补啊,还是很重要的一个组件的(^__^) 嘻嘻……既然有Scroller,我们就要重写View的computeScroll方法,所以我们在DragView中有重写:
@Override
public void computeScroll() {
if(mDragger.continueSettling(true))
invalidate();
}
}
其实其continueSettling的内部想必知道Scroller的童鞋应该猜得出干了什么:
boolean keepGoing = mScroller.computeScrollOffset();
到此手指松开回到原来位置的代码也分析完毕了。
细心的童鞋可以发现,我们做测试用的View都是TextView,因为TextView本身就不具备可点击性,如果换成本身具有可点击性的Button,那么还会有一样的效果吗?
我们试试:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.cisetech.demo.MainActivity">
<com.cisetech.demo.DragView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true">
<Button
android:id="@id/draggedview"
android:layout_margin="10dp"
android:gravity="center"
android:layout_gravity="center"
android:background="#44ff00ff"
android:text="我可以被拖动"
android:layout_width="100dp"
android:layout_height="100dp"/>
<Button
android:id="@id/autobackview"
android:layout_margin="10dp"
android:layout_gravity="center"
android:gravity="center"
android:background="#44ff"
android:text="我可以自动回到初始位置"
android:layout_width="100dp"
android:layout_height="100dp"/>
<Button
android:id="@id/edgeview"
android:layout_margin="10dp"
android:layout_gravity="center"
android:gravity="center"
android:background="#44ff00"
android:text="滑动边缘移动我"
android:layout_width="100dp"
android:layout_height="100dp"/>
</com.cisetech.demo.DragView>
</RelativeLayout>
当我们换成Button后,我们运行发现,只有边界移动的view可以移动,其它两个view不管怎么滑动都没效果哦,为什么呢?
主要是因为,如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,在onTouchEvent的DOWN的时候就确定了captureView。如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获。
所以,如果你用Button测试,或者给TextView添加了clickable = true ,都记得重写下面这两个方法:
@Override
public int getViewHorizontalDragRange(View child){
return getMeasuredWidth()-child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child){
return getMeasuredHeight()-child.getMeasuredHeight();
}
方法的返回值应当是该childView横向或者纵向的移动的范围,当前如果只需要一个方向移动,可以只复写一个。
这个时候你肯定又会问“为什么重写这两个方法就可以了呢?”
(涉及到事件分发的知识,不懂的童鞋还是得脑补一下哈(^__^) 嘻嘻……)我们来看看原因:
在VDH中的shouldInterceptTouchEvent方法中我们看到这么一段代码:
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
final float x = ev.getX(i);
final float y = ev.getY(i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback's
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can't move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
代码有点长,我们看重点,我们看到这么一段代码:
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback's
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can't move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
当pastSlop为true的时候,才会去跑:
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
当跑了tryCaptureViewForDrag的时候就会去走captureChildView方法:
*/
public void captureChildView(View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
+ "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
而这个时候setDragState(STATE_DRAGGING);会给mDragState设置成STATE_DRAGGING,当设置成了STATE_DRAGGING,在shouldInterceptTouchEvent的最后会返回true:
return mDragState == STATE_DRAGGING;
当shouldInterceptTouchEvent返回true以后,我们自定义的ViewGroup中的onInterceptTouchEvent也就返回true了,因此直接拦截了子View的事件,所以接下来才会进ViewGoup的onTouchEvent方法,所以才可以滑动。
有点复杂的感觉额,但是如果很清晰的掌握了事件分发流程,还是很好理解的。
细心的童鞋会发现,我们的View拖动的边界没有限制,以至于都拖到ViewGroup外面去了,好吧,我就直接贴代码了。
左右的边界:
左边为getPaddingLeft(),右边为getWidth() - mDragView.getWidth() - getPaddingRight():
public int clampViewPositionHorizontal(View child, int left, int dx)
{
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight();
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
上下的的边界: 上边为getPaddingTop(),下边为 getHeight() - child.getHeight() - getPaddingBottom():
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
好吧!到此,VDH的基本用法就介绍到这里了,接下来会进入到VDH的实战部分,有兴趣的童鞋可以跟我一起进入VDH实战部分哦!!!
未完待续………….
本文部分内容来自:
http://blog.csdn.net/lmj623565791/article/details/46858663
最后附上demo的git链接:
https://github.com/913453448/SwipeBackLayout