ViewDragHelper出来这么久了,今天终于回想起它了,看过国外的相关blog资料,也看过翔哥blog,清晰的说明了使用方法,知乎上也有不少的资料,所以呢决定重新整理一下,顺便梳理知识。(只有自己亲自动手写过,才能更好的掌握,只看别人的blog,只能说你了解过,能使用,但是遇到需求变更,就只能各个群里拜大神)
/** * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number * of useful operations and state tracking for allowing a user to drag and reposition * views within their parent ViewGroup. */
ViewDragHelper类对于ViewGroup自定义是有非常大的帮助的,他可以跟踪用户的拖拽轨迹,并重新定位。
public class ViewDragHelper {
/** * Apps should use ViewDragHelper.create() to get a new instance. * This will allow VDH to use internal compatibility implementations for different * platform versions. * 如果你不想new 出实例对象,也可以通过ViewDragHelper.create(context,callback)方法获得实例。 * @param context Context to initialize config-dependent params from * @param forParent Parent view to monitor */
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
//传入的ViewGroup和CallBack回调实例不能为空
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
//初始化变量
mParentView = forParent;
mCallback = cb;
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = ScrollerCompat.create(context, sInterpolator);
}
}
ViewConfiguration 类中的值一遍在自定义高级控件都会用到,内部定义很多变量值,各有其用途,比如上例代码块中的mTouchSlop:在可滑动的控件中用于区别单击子控件和滑动操作的一个值,mMaxVelocity、mMinVelocity 最大Fling滑动速度和最大Fling滑动速度等。传入Scroller的动画曲线差值器,初始化一个Scroller类
/** * Interpolator defining the animation curve for mScroller */
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
ViewDragHelper提供的create方法有两个,第二个方法多传入了sensitivity影响拖拽的敏感系数。sensitivity值越大,mTouchSlop 就越小越敏感,从而影响到滑动控件的滑动和点击事件的分发。
/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param sensitivity Multiplier for how sensitive the helper should be about detecting * the start of a drag. Larger values are more sensitive. 1.0f is normal. * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
再来了解静态抽象类ViewDragHelper.Callback的定义
public static abstract class Callback {
/** * Called when the drag state changes. See the <code>STATE_*</code> constants * for more information. * 拖动状态改变时调用的方法 * @param state The new drag state * * @see #STATE_IDLE * @see #STATE_DRAGGING * @see #STATE_SETTLING */
public void onViewDragStateChanged(int state) {}
/** * Called when the captured view's position changes as the result of a drag or settle. * 视图位置发生了变化,捕获到相关的数据,并回调相关数值 * @param changedView View whose position changed(发生变化的视图) * @param left New X coordinate of the left edge of the view(视图左边缘坐标) * @param top New Y coordinate of the top edge of the view(视图上边缘坐标) * @param dx Change in X position from the last call(拖拽的x距离) * @param dy Change in Y position from the last call(拖拽的Y距离) */
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
/** * 当子view被拖曳或被settle, 而被捕获时回调的方法. * @param capturedChild Child view that was captured * @param activePointerId Pointer id tracking the child capture(跟踪子View捕捉到的指针标识) */
public void onViewCaptured(View capturedChild, int activePointerId) {}
/** * 当子view不再被拖曳时调用.如果有需要,fling滑动的速度也会被提供.速度值会介于 * 系统最小化和最大值之间.(也就是构造函数里面初始化的两个值) * @param releasedChild The captured child view now being released * @param xvel X x轴离开的速率 * @param yvel Y y轴离开的速率 */
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
/** * 当父view其中一个被标记可拖曳的边缘被用户触摸, 同时父view里没有子view被捕获响应时回调该方法. * * edgeFlags 受到拖拽影响的边缘 * pointerId 跟踪View拖拽边缘是的指针标识 * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM * @see #EDGE_ALL */
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/** * 当原来可以拖曳的边缘被锁定不可拖曳时回调,比如抽屉控件+ViewPager嵌套 * ViewPager左右滑动选择是否锁定抽屉控件的滑动 * @param edgeFlags A combination of edge flags describing the edge(s) locked * @return true to lock the edge, false to leave it unlocked */
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/** * 当用户用开始从屏幕边缘拖曳,并且父view中没有子view影响时调用. */
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/** * 子视图的z轴的顺序值。 */
public int getOrderedChildIndex(int index) {
return index;
}
/** * 返回一个子视图的水平拖动范围值,如果值为0,则不能水平拖动 * @param child Child view to check * @return range of horizontal motion in pixels */
public int getViewHorizontalDragRange(View child) {
return 0;
}
/** * 返回一个子视图的垂直拖动范围值,如果值为0,则不能垂直拖动 * @param child Child view to check * @return range of vertical motion in pixels */
public int getViewVerticalDragRange(View child) {
return 0;
}
/** * 当我们通过指针标识移动子View,会回调该函数,如果该函数返回为true,则允许我们移动子View位置。 * 如果子View已经被捕获,那么就会导致重复调用,从而指针标识控制了移动。 * 如果该方法返回为true,捕获到了子View,onViewCaptured该方法随即被调用, * * @param child Child the user is attempting to capture * @param pointerId ID of the pointer attempting the capture * @return true if capture should be allowed, false otherwise */
public abstract boolean tryCaptureView(View child, int pointerId);
/** * 限制的沿水平轴拖子视图 * 默认实现不允许水平拖拽 * 扩展类必须覆盖该方法,并提供所需的阀值。 * @param child Child view being dragged * @param left Attempted motion along the X axis * @param dx Proposed change in position for left * @return The new clamped position for left */
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
/** * 限制的沿垂直轴拖子视图 * 默认实现不允许垂直拖拽 * 扩展类必须覆盖该方法,并提供所需的阀值。 * @param child Child view being dragged * @param top Attempted motion along the Y axis * @param dy Proposed change in position for top * @return The new clamped position for top */
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
ViewDragHelper类里面也有许多常量,下面来逐一了解。
/** * A null/invalid pointer ID.无效ID */
public static final int INVALID_POINTER = -1;
/** * A view is not currently being dragged or animating as a result of a fling/snap. * 没有被拖拽或没有拖拽相关动画执行的状态 */
public static final int STATE_IDLE = 0;
/** * A view is currently being dragged. The position is currently changing as a result * of user input or simulated user input. * 子View根据用户拖拽改变位置的状态 */
public static final int STATE_DRAGGING = 1;
/** * A view is currently settling into place as a result of a fling or * predefined non-interactive motion. * 根据标识设置改变view的位置的过程,simple:A------>>------B这个过程 */
public static final int STATE_SETTLING = 2;
/** * Edge flag indicating that the left edge should be affected. * 拖拽会影响到的左侧边缘 */
public static final int EDGE_LEFT = 1 << 0;
/** * Edge flag indicating that the right edge should be affected. * 拖拽会影响到的右侧边缘 */
public static final int EDGE_RIGHT = 1 << 1;
/** * Edge flag indicating that the top edge should be affected. * 拖拽会影响到的顶部边缘 */
public static final int EDGE_TOP = 1 << 2;
/** * Edge flag indicating that the bottom edge should be affected. * 拖拽会影响到的底部边缘 */
public static final int EDGE_BOTTOM = 1 << 3;
/** * Edge flag set indicating all edges should be affected. * 四周都可以被拖拽 */
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
/** * Indicates that a check should occur along the horizontal axis * 拖拽方向:水平 */
public static final int DIRECTION_HORIZONTAL = 1 << 0;
/** * Indicates that a check should occur along the vertical axis * 拖拽方向:垂直 */
public static final int DIRECTION_VERTICAL = 1 << 1;
/** * Indicates that a check should occur along all axes * 拖拽方向:水平和垂直皆可 */
public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
private static final int EDGE_SIZE = 20; // 边缘值大小 20dp
private static final int BASE_SETTLE_DURATION = 256; //settle基本的时间值 256ms
private static final int MAX_SETTLE_DURATION = 600; //settle的最大时间值 600ms
// Current drag state; idle, dragging or settling 拖拽有三个状态,当前的拖拽状态变量
private int mDragState;
// Distance to travel before a drag may begin 触发拖拽的最大值
private int mTouchSlop;
// Last known position/pointer tracking 跟踪子拖拽View的指针标识
private int mActivePointerId = INVALID_POINTER;
//初始化记录拖拽的x坐标值
private float[] mInitialMotionX;
//初始化记录拖拽的y坐标值
private float[] mInitialMotionY;
private float[] mLastMotionX;
private float[] mLastMotionY;
private int[] mInitialEdgesTouched;
//边缘拖拽的距离变化
private int[] mEdgeDragsInProgress;
//边缘拖拽被锁定的集合
private int[] mEdgeDragsLocked;
private int mPointersDown;
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
private float mMinVelocity;
//边缘距离大小
private int mEdgeSize;
private int mTrackingEdges;
//兼容的Scroller
private ScrollerCompat mScroller;
private final Callback mCallback;
private View mCapturedView;
private boolean mReleaseInProgress;
private final ViewGroup mParentView;
再来细看ViewDragHelper内部一些基本的set get方法定义
/** * 设置最小的滑动速度 * * @param minVel Minimum velocity to detect */
public void setMinVelocity(float minVel) {
mMinVelocity = minVel;
}
public float getMinVelocity() {
return mMinVelocity;
}
/** * 获取当前的拖拽状态 * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. * @return The current drag state */
public int getViewDragState() {
return mDragState;
}
/** * 设置能被跟踪的边缘 * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */
public void setEdgeTrackingEnabled(int edgeFlags) {
mTrackingEdges = edgeFlags;
}
/** * 边缘距离大小 * @return The size of an edge in pixels * @see #setEdgeTrackingEnabled(int) */
public int getEdgeSize() {
return mEdgeSize;
}
/** * @return 当前跟踪捕获的子视图 */
public View getCapturedView() {
return mCapturedView;
}
/** * @return 当天捕获到的拖拽的子View对应的指针标识 * or {@link #INVALID_POINTER}. */
public int getActivePointerId() {
return mActivePointerId;
}
//****************此处略********************
上面两个方法的mCapturedView、mActivePointerId变量,在调用captureChildView()方法时初始化,同时改变拖拽状态,
/** * @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;
//子view被拖曳或被settle, 而被捕获时Callback回调方法.
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
VelocityTracker主要用跟踪触摸屏事件的速率(滑动速度),你可以调用getXVelocity() 、getXVelocity()获得横、竖方向的速率,下面的cancel方法,对速率跟踪的VelocityTracker进行了回收。
/** * 这个方法等同于MotionEvent.ACTION_CANCEL一样 * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. */
public void cancel() {
//跟踪的指针标识变成无效的
mActivePointerId = INVALID_POINTER;
//清空跟踪的历史记录
clearMotionHistory();
if (mVelocityTracker != null) {
//释放
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void clearMotionHistory() {
if (mInitialMotionX == null) {
return;
}
/** * 这是我以前没用过的,在这里做个备注吧 * * Arrays.fill(float[] array, float value); * simple: boolean [] flags = new boolean[2]; * Arrays.fill( flags, true); * result: flags={true,true} * * / Arrays.fill(mInitialMotionX, 0); //............略............... mPointersDown = 0; }
abort方法先调用了上面的cancel方法,随即改变拖拽状态,如果当前状态是settling,还需要停止滑动动画,并且执行Callback回调函数onViewPositionChanged(),通过scroller计算出滑动的距离变化
/** * {@link #cancel()}, but also abort all motion in progress and snap to the end of any * animation. */
public void abort() {
cancel();
if (mDragState == STATE_SETTLING) {
final int oldX = mScroller.getCurrX();
final int oldY = mScroller.getCurrY();
mScroller.abortAnimation();
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
}
setDragState(STATE_IDLE);
}
当你拖拽的子View完成后MotionEvent.ACTION_UP时,子View所在的位置不是他自身应该处的位置,会调用Callback.onViewReleased()方法回调,进行相应的位置变换(例如:DrawLayout拖拽画出距离过小,会让滑出视图回到原来的位置),onViewReleased()方法回调后我们一般会用到下面这个方法smoothSlideViewTo(),
/** * @param 要移动的View * @param 移动View到距离屏幕左侧的距离 * @param 移动View到距离屏幕顶部的距离。 */
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
//根据距离判断能否继续滑动(如果能继续滑动在forceSettleCapturedView()方法里面调用scroller.startScroll()继续滑动)
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
/** * Settle the captured view at the given (left, top) position. * * @param finalLeft Target left position for the captured view * @param finalTop Target top position for the captured view * @param xvel Horizontal velocity * @param yvel Vertical velocity * @return true if animation should continue through {@link #continueSettling(boolean)} calls */
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;
}
粗略了解了Callback.onViewReleased()相关方法,再来看看clampMag(),该方法用途是保证参数中给定的速率是正确的值。从滑动时间的计算可以看出,滑动速率也是滑动时间影响之一。
/** * * @param value Value to clamp * @param absMin Absolute value of the minimum significant value to return * @param absMax Absolute value of the maximum value to return * @return The clamped value with the same sign as <code>value</code> */
private int clampMag(int value, int absMin, int absMax) {
final int absValue = Math.abs(value);
if (absValue < absMin) return 0;
if (absValue > absMax) return value > 0 ? absMax : -absMax;
return value;
}
ViewDragHelper的内部方法是在太多了,就不挨着细看了,说几个重要方法开始demo吧,在onInterceptTouchEvent()方法里调ViewDragHelper的shouldInterceptTouchEvent()方法选择是否拦截事件分发,在onTouchEvent()方法里调用ViewDragHelper()的processTouchEvent()方法,ACTION_DOWN时返回true,则可以继续接收后续事件(drag),对于drag的实现ViewHelper已经帮我们实现了,如果你对事件分发不太了解的建议先去看看ViewGroup View 相关的资料。
下面来简单实践一下拖拽效果,如下图
/** * Created by LanYan on 2016/1/19. */
public class DragLinearLayout extends LinearLayout{
private TextView textView1,textView2;
private final ViewDragHelper mViewDragHelper;
private Point textViewOldOptions = new Point();
public DragLinearLayout(Context context) {
this(context, null);
}
public DragLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
根据ViewDragHelper说明,我们需要重写onInterceptTouchEvent拦截方法和onTouchEvent触摸方法,交给ViewDragHelper处理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
接着我们来初始化我们的需要被拖拽的视图控件,onFinishInflate()该方法调用时机在系统解析XML完成,把子View全部添加完成后,一般在自定义ViewGroup常用到,在这个方法中初始化自己需要用到的控件。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
textView1 = (TextView) getChildAt(0);
textView2 = (TextView) getChildAt(1);
}
在构造函数里面需要初始化ViewDragHelper类,设置他的触摸边界、范围等。
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
Log.i("info", "DragStatus:"+state);
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
Log.i("info", "left:" + left+",top:"+top+",distanceX:"+dx+",distanceY:"+dy);
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
//允许被捕获拖拽的view视图
return child == textView1||child == textView2;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
Log.i("info", "xvel:" + xvel+",yvel"+yvel);
if(releasedChild==textView1){
//回到初始位置,并且锁定边缘不能再被拖拽 mViewDragHelper.settleCapturedViewAt(textViewOldOptions.x,textViewOldOptions.y);
invalidate();
mViewDragHelper.setEdgeTrackingEnabled(0);
}
}
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
super.onEdgeTouched(edgeFlags, pointerId);
Log.i("info", "EdgeFlags onTouch:" + edgeFlags);
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
//边缘部分拖拽尝试捕获跟踪
mViewDragHelper.captureChildView(textView1,pointerId);
}
@Override
public boolean onEdgeLock(int edgeFlags) {
Log.i("info", "EdgeFlags lock:" + edgeFlags);
return false;
}
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth()-child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight()-child.getMeasuredHeight();
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
});
//触摸边界为左侧,其他边界参照ViewDragHelper常量定义
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
就上面这样看似没问题的代码,其实还存在一个bug,在onViewReleased方法中调用了settleCapturedViewAt方法,如果你看过我上面贴的代码不难发现,settleCapturedViewAt方法内部还调用了下面一段代码
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
//....................此处略.................
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
调用mScroller.startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,做出滚动的响应,而computeScroll在父控件执行drawChild时,会调用这个方法。
@Override
public void computeScroll() {
super.computeScroll();
if(mViewDragHelper.continueSettling(true)){
invalidate();
}
}
对于该效果的简单自定义,源码已上传:http://download.csdn.net/detail/analyzesystem/9411522
ViewDragHelper对我们自定义ViewGroup的帮助是相当大的,想当初我看了翔哥的blog自定义横向的ViewPager,我也学着去写了个支持横纵向的GuideViewPager,都要自己去检测滑动速率方向之类,哎,往事不堪回首,一个速率bug让我调试了半天时间。关于ViewDragHelper的相关自定义翔哥有个LeftDrawLayout,看完之后决定加深理解,于是乎自己写了一个RightDrawLayout,鉴于时间关系,不能在加班了,就放到下一篇DrawLayout blog。