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。