ViewDragHelper源码分析

来源:互联网 发布:ubuntu grub2引导win7 编辑:程序博客网 时间:2024/06/09 22:27

ViewDragHelper的源码分析

注1:本文内容纯作者手打原创,如需转载请注明出处。

注2:作者目前在找工作,初级安卓工程师的水平如有需要的请留言或私信,万分感谢。

开篇

先说一下为什么会关注到这个类,也许好多人都没听过这个是干嘛的,不过对于DrawerLayout大家应该都很熟悉,是官方出的一个用于实现抽屉效果的布局控件,虽然在效果上可能没有SlidingMenu炫,不过胜在简单好用,而DrawerLayout的底层拖动就是由ViewDragHelper来实现的,顾名思义就知道是一个View的拖动帮助类,其实这个类是在android2.0的时候就存在了,不过关注度真的好低,直到DrawerLayout出现,大家才注意到有这么个能能够非常方便的帮助我能去实现各种View的拖动和边缘的检测。
特别感谢郭神,姜糖水,还有一位没有署名的作者的帮助。文末会有相应的连接。写的都太棒了。

基本使用

ViewDragHelper对象的构建通常在viewGroup的内部,用于实现一个自定义的布局的时候,对布局内部的View进行拖动,可用的构造方法有两种。

/** * 两个工厂方法,通常使用第一个 * forParent 表示所在的ViewGroup * sensitivity 表示拖动的灵敏度 * cb 表示我们需要实现的拖动的各种监听 * */public static ViewDragHelper create(ViewGroup forParent, Callback cb) {    return new ViewDragHelper(forParent.getContext(), forParent, cb);}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;}//这个是在父View的构造方法中进行实例化。public MyLinearLayout(Context context, AttributeSet attrs, int defStyle) {    super(context, attrs, defStyle);    initView();}private void initView() {    mHelper=ViewDragHelper.create(this, new myCallBack());}

其中mTouchSlop为能够被系统识别为滑动的移动距离。两个构建方法并不难,我们继续,看下在ViewGroup中还需要做些什么。

 /** * Check if this event as provided to the parent view's onInterceptTouchEvent should * cause the parent to intercept the touch event stream. *检测这个作为被提供给父view的onInterceptTouchEvent的事件是否令父view拦截到当前的触摸事件流. * @param ev MotionEvent provided to onInterceptTouchEvent * 如果父View在onInterceptTouchEvent方法中应该返回true的话, 则返回true * 意思就是如果父控件决定拦截,就返回true。 * @return true if the parent view should return true from onInterceptTouchEvent */public boolean shouldInterceptTouchEvent(MotionEvent ev) {}

后面的代码实在是太多就没有全贴,我们只需要知道这个方法是使用就一个了,当然还是推荐大家去看一下里面都做了什么,便于更好的理解和使用这个类。shouldInterceptTouchEvent()方法在ViewGroup的onInterceptTouchEvent()方法中调用,直接把事件传过去,跟GestureDetector用法差不多。接下来还有。

   /** * Process a touch event received by the parent view. This method will dispatch callback events * as needed before returning. The parent view's onTouchEvent implementation should call this. *  处理从父view中获取的触摸事件.这个方法将分发callback回调事件.父view的onTouchEvent方法中应该调用该方法. * @param ev The touch event received by the parent view */public void processTouchEvent(MotionEvent ev) {}

processTouchEvent则在父View的onTouchEvent方法中被调用,用于分发CallBack的回调事件。
onInterceptTouchEvent中通过使用mHelper.shouldInterceptTouchEvent(event)来决定我们是否应该拦截当前的事件。onTouchEvent中通过mHelper.processTouchEvent(event)处理事件。(来自郭神博客)

/** * Move the captured settling view by the appropriate amount for the current time. * If <code>continueSettling</code> returns true, the caller should call it again * on the next frame to continue. * * @param deferCallbacks true if state callbacks should be deferred via posted message. *                       Set this to true if you are calling this method from *                       {@link android.view.View#computeScroll()} or similar methods *                       invoked as part of layout or drawing. * @return true if settle is still in progress */public boolean continueSettling(boolean deferCallbacks) {}//在父View中的使用@Overridepublic void computeScroll() {    if(mHelper.continueSettling(true)){        ViewCompat.postInvalidateOnAnimation(this);    }}

本人英文不是很好,初步理解就是如果要延迟刷新的话就传入true,返回值如果为True则会重新调用这个方法,直到结束。通常的使用方式就是如上文在父View中使用。至此在父View中我们需要做的就基本结束了。

接下来就是常用方法

      * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}     * will follow if the capture is successful.</p>     *  决定是否捕获传进来的View     * @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;    }

三个最常用的方法,在实现拖动的时候基本上都要重写的(第一个是实现),注释已经很明确了,通常在第二,三个方法中直接返回 left,top就可以。

自动复位和边缘拖动

   //手指释放的时候回调        @Override        public void onViewReleased(View releasedChild, float xvel, float yvel)        {            //mAutoBackView手指释放时可以自动回去            if (releasedChild == mAutoBackView)            {                mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);                invalidate();            }        }

代码来自郭神的博客,当你需要实现自动复位的功能的时候需要重写onViewReleased方法,其中可以对View进行判定(在多个View可以拖动的时候)其中可以实现自动复位的方法有一下三种。

settleCapturedViewAt()是指直接回到初始位置,这个方法的执行过程与松手时的速度有关。如果方法的返回值为true的话,那么continueSettling(boolean)在执行期间会被一直调用,直到方法的返回值为false。

smoothSlideViewTo()是指具有动画效果的回到初始位置。返回值的作用同上。
flingCapturedView()也是一种回到初始位置的方法。不过底层是自己调用的Scroller.fling方法实现的,没有经过forceSettleCapturedViewAt()方法

参考源码我们可以发现,在两种的复位的方法中都使用了forceSettleCapturedViewAt()这个方法来实现具体的复位工作,这个方法的实现是靠Scroller的scroll方法实现的,对于上文中的settle与slide的区别就在于forceSettleCapturedViewAt在被smoothSlideViewTo调用时初始的速度被默认设置为0,看一下具体的实现。
//具有动画效果的复位方法,速度为0
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;

    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;}//直接恢复到初始位置的复位方法,速度会影响复位的过程public boolean settleCapturedViewAt(int finalLeft, int finalTop) {    if (!mReleaseInProgress) {        throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +                "Callback#onViewReleased");    }    return forceSettleCapturedViewAt(finalLeft, finalTop,            (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),            (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));}//两种复位方法的实现。底层的实现是用Scrollerprivate 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中被调用,用于计算复位的时间private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {    xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);    yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);    final int absDx = Math.abs(dx);    final int absDy = Math.abs(dy);    final int absXVel = Math.abs(xvel);    final int absYVel = Math.abs(yvel);    final int addedVel = absXVel + absYVel;    final int addedDistance = absDx + absDy;    final float xweight = xvel != 0 ? (float) absXVel / addedVel :            (float) absDx / addedDistance;    final float yweight = yvel != 0 ? (float) absYVel / addedVel :            (float) absDy / addedDistance;    int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));    int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));    return (int) (xduration * xweight + yduration * yweight);}//用于计算复位动画的执行时间(单一轴方向上的)private int computeAxisDuration(int delta, int velocity, int motionRange) {    if (delta == 0) {        return 0;    }    final int width = mParentView.getWidth();    final int halfWidth = width / 2;    final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);    final float distance = halfWidth + halfWidth *            distanceInfluenceForSnapDuration(distanceRatio);    int duration;    velocity = Math.abs(velocity);    if (velocity > 0) {        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));    } else {        final float range = (float) Math.abs(delta) / motionRange;        duration = (int) ((range + 1) * BASE_SETTLE_DURATION);    }    return Math.min(duration, MAX_SETTLE_DURATION);}//计算区间值()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;}private float clampMag(float value, float absMin, float absMax) {    final float absValue = Math.abs(value);    if (absValue < absMin) return 0;    if (absValue > absMax) return value > 0 ? absMax : -absMax;    return value;}//用于在复位之后释放被捕获的View对象private float distanceInfluenceForSnapDuration(float f) {    f -= 0.5f; // center the values about 0.    f *= 0.3f * Math.PI / 2.0f;    return (float) Math.sin(f);}

看了这么多,基本上对于复位就了解的差不多了,一共就有三种选择,可以根据个人喜好和业务需要来选择不同的,其他的一些计算方法,基本不用管,稍微了解就可以,对于前两种方法在使用中的区别明显的在于松手时的速度,一个是有速度变化的复位,一个是平滑的复位。第三种没尝试过。接下来就是边缘拖动所涉及到的方法。

    //设置可以拖动的边缘    public void setEdgeTrackingEnabled(int edgeFlags) {        mTrackingEdges = edgeFlags;    }    //当父View中一个被设置为可拖动的边界被点击时,并且子view中没有被捕获是调用    //就是说如果点的位置是可拖动的边缘而且没有在点的位置没有控件被捕获的话会调用此方法。    public void onEdgeTouched(int edgeFlags, int pointerId) {}    //该方法当原来可以拖曳的边缘被锁定不可拖曳时回调.如果边缘在初始化开始拖曳前被拒绝拖曳,就会发生前面说的这种情况.    //但这个方法会在{@link #onEdgeTouched(int, int)}之后才会被回调.这个方法会返回true来锁定该边缘.或者    //返回false来释放解锁该屏幕.默认的行为是后者(返回false来释放解锁该屏幕)    //(不是很理解什么时候回被调用,返回值就是值是否锁住当前的边缘,使其变得不可用)    public boolean onEdgeLock(int edgeFlags) {        return false;    }    //在拖动开始的时候调用,通常用于在此方法中对要施加边缘拖动的View就行确定    public void onEdgeDragStarted(int edgeFlags, int pointerId) {}    //在onEdgeDragStarted方法中显示的调用,用来确定要施加边缘拖动的View    //不受tryCaptureView方法中的返回值限制    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);    }

上面的注释已经把每个方法的作用说过了,通常我们在使用边缘拖动的时候先设置要实现拖动的边缘,之后在start方法中对要被拖动的View进行显示的确定就行。

在郭神的博客中还对子View是否具有消耗事件的能力进行了一些解析,可以去看一下。

到这基本的使用也就全部解释完了,实际情况中可能别这要复杂得多,比如各种事件的传递与拦截,还有就是与其他的控件间进行适合,都需要大家去尝试。感觉在整体的ViewDragHelper这个类中,最主要的两个方法还是最开始介绍那两个用于传入事件的方法,在其中去有选择性的执行动作和产生各种回调。剩下的一些方Callback中方法在郭神的博客中都有注释,至于ViewDragHelper中的其他方法,可以自行去看源代码,大部分都是set,get方法,判断方法等。其中比较重要的如findTopChildUnder()用于确定拖动View的上下查找顺序(好像不应该这么说,不过作用差不多,底部有调了一个其他的方法去实现的,可以自行查源代码),cancel和abort,用于取消和终止ViewDragHelper。

总结

关于侧滑,从郭神的slidingmenu,到Drawerlayout,再到ViewDragHelper,之前也独立用代码在自定义的布局去根据传入事件的信息来实现View的拖动,但是确实计算量很大而且判定条件都需要自己去写,,动画的实现也需要自己去写,现在有了ViewDragHelper就再也不怕了。弄明白这个类对我们之后实现自定义的ViewGroup有很大的帮助,共勉吧!

最后打个广告。。在找工作。。

参考资料

郭神的两篇博客
http://blog.csdn.net/lmj623565791/article/details/46858663
http://blog.csdn.net/lmj623565791/article/details/47396187

超详细的ViewDragHelper源码解析
http://www.it165.net/pro/html/201505/40127.html

这个人写的也特别的好(中午上还能上去的,晚上就不行了)友情参考吧。
http://www.cnphp6.com/archives/87727

0 0
原创粉丝点击