为何说Android ViewDragHelper是神器 (二)
来源:互联网 发布:python json模块 编辑:程序博客网 时间:2024/04/29 05:46
前言: 通过上一篇的为何说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 ,都记得重写下面这两个方法:
@Overridepublic int getViewHorizontalDragRange(View child){ return getMeasuredWidth()-child.getMeasuredWidth();}@Overridepublic 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
- 为何说Android ViewDragHelper是神器 (二)
- 为何说Android ViewDragHelper是神器 (一)
- 为何说Android ViewDragHelper是神器 (实战)
- Android 神器ViewDragHelper(实战二)
- Android自定义ViewGroup神器-ViewDragHelper
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android--ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- Android ViewDragHelper完全解析 自定义ViewGroup神器
- 接口
- PHP排序算法及查找算法
- 数据类型和变量
- 40个Java多线程问题总结
- 通过右键安装inf文件来安装驱动程序这种方式可取吗?
- 为何说Android ViewDragHelper是神器 (二)
- table
- 用Android Studio 调试
- Java 8: Stream map method
- leetcode 15. 3Sum 题解
- 恒生电子怎样——应届生眼中的恒生
- KMP算法的简单版本(MP)的浅略分析
- Android中webView和WebSettings
- c++ 继承 总结