2018年首篇,带大家一起学习视图拖拽功能的源码

原创 2018年01月02日 00:00:00



今日科技快讯


今天是2018年的第一个工作日,今天的快讯我们对2017年的重大事件进行一下回顾吧。

乐视事件

回溯乐视的2017,“眼看他起朱楼,眼看他宴宾客,眼看他楼塌了”。乐视历经了来自融创中国百亿融资、高层相继离职、裁员风波、资金链危机、业务亏损,以及旗下的手机业务停摆、供应商讨债等一系列危机,乐视生态几近崩溃。贾跃亭则赴美专注法拉第未来,称会将全部精力放在造车项目上。12月25日,北京证监局发布通告,责令贾跃亭于2017年12月31日前回国,切实履行公司实际控制人应尽义务,配合解决公司问题。截至目前,贾跃亭妻子甘薇在微博宣称已经回国到达北京,但并未提及贾跃亭是否回国。这边厢,贾跃亭的造车梦仍在质疑中继续;那边厢,来自蔚来、威马、小鹏等造车新势力不断崛起,科技巨头巨额资金投资布局,互联网造车愈发热闹。

单车业洗牌

历经2016年的繁荣扩张后,共享单车行业在2017下半年迎来洗牌期,共享单车企业频现“倒闭潮”。悟空单车、小鸣单车、酷奇单车、小蓝单车等第二梯队共享单车接连倒闭。在多个城市限制新增车辆投放之后,头部共享单车的竞争也尤为胶着。摩拜、ofo合并传闻下,投资人与管理层未能达成一致。不论摩拜单车、ofo还是哈罗单车,几家的竞争已经从铺量到精细化运营比拼,并开始将目光转向大出行领域。这意味着共享单车的游戏规则已经改变。整个行业已从砸钱这一粗放的增长方式转变成组建出行服务生态。

AI发展

2017年5月,中国棋手柯洁在与AlphaGo的三番棋较量中,连输三局完败。在击败柯洁后, DeepMind宣布AlphaGo从竞技比赛退役。 5个月后,DeepMind在《Nature》上发布了第四篇论文,介绍了AlphaGo系列的新版本—— AlphaGo Zero,它学习围棋技能不需要任何人类的知识,完全靠自我对弈学习下棋,不依赖人类围棋经验,仅训练3天就战胜了AlphaGo Lee(战败李世石的版本),比分100:0。经过40天训练后,Alpha Zero又以89:11战胜了Alpha Master(战胜柯洁的版本)。在上百万局游戏中,这个系统逐渐学会了从零开始的游戏,在短短几天内积累了数千年的知识。在此过程中,它还发现了非传统的策略,并揭示了这个古老游戏的新知识。DeepMind相信AI在更复杂的问题上也能起到同样的作用,它可以是科学技术工具,也可以是人类创造力的倍增器。目前AlphaGo团队已经在着手处理下一组重大挑战。DeepMind希望AI系统可以在气候变化、新药研发、到发现新型复合材料、降低现有医疗系统的运转压力等挑战上帮助人类做出进展。

新式零售

2017年可谓是“新零售元年”,行业集中涌现出AR(增强现实)购物、虚拟试妆等由新技术支撑的玩法,并且由于盒马鲜生、无人便利店、无人值守货架等新业态的崛起,带动了背后资本层面的涌动。阿里入股高鑫零售、腾讯入股永辉超市、猩便利获得3.8亿元A1轮融资,红杉资本、华兴资本、光速中国创业投资基金等都加入其中。而美国零售变革的脚步对比之下则显得有些缓慢,亚马逊147亿美元收购美国有机食品连锁超市WholeFoods后,除了把智能音响Echo等“网站爆款”商品放进超市里销售,并在亚马逊网站上低价销售超市的自有品牌食品,以及Prime会员权益合作外,目前仍未看出大的变革动作。

比特币关闭

2017年9月4日下午,央行等七部门联合发布公告,正式叫停包括ICO (虚拟货币融资)在内的“代币发行融资”。公告发布之日起,各类代币发行融资活动应当立即停止。自2009年比特币诞生以来,全球数字货币已超过一百种。尽管比特币等数字货币交易量快速增长,相关的交易却仍缺乏监管,一些国家仍不认同比特币是一种金融工具。9月15日,北京市互金风险专项整治办下发《北京地区虚拟货币交易场所清理整治工作要求》),各交易场所在当天24点前发布公告,明确停止所有虚拟货币交易的最终时间,并宣布立即停止新用户注册,国内比特币交易平台应声宣布将关闭交易平台。与此同时,今年11月28日,包括GDAX、CoinDesk等多家比特币交易平台的报价显示,比特币兑美元突破10000美元关口。


作者简介


放假回来直接进入了2018年的第一个工作日,整顿心情之后,新的一年需要新的态度和目标,一起努力吧!

本篇文章来自 丶小嵩 的投稿。主要介绍了ViewDragHelper源码相关知识,希望对大家有所帮助!

丶小嵩 的博客地址

http://blog.csdn.net/qq_22393017


ViewDragHelper介绍


ViewDragHelper它是Google官方推出的手势滑动辅助类,极大程度地简化了我们对控件的手势滑动跟踪及处理。让我们能够更加便捷地开发自定义ViewGroup控件,实现拖拽以及弹性滚动等功能。事实上,官方的SlidingPaneLayout和DrawerLayout都是利用ViewDragHelper实现的。掌握它,可以一定程度地减轻我们开发工作难度以及投入精力。

本篇文章讲的是关于ViewDragHelper的源码解析部分,如果对ViewDragHelper的基础用法还并不熟悉的朋友,可以去参考作者博客的前面一篇文章,文章的链接是:

http://blog.csdn.net/qq_22393017/article/details/78045810


UML类图及流程图


ViewDragHelper的UML类图如下所示

在使用ViewDragHelper过程中,主要涉及到如下四个类: 

MyDraggableView 

我们自定义的ViewGroup类。 

ViewDragHelper 

帮助类,是我们本篇文章主要分析的对象。 

Callback 

ViewDragHelper的内部抽象静态类,主要用于事件处理结果的回调及事件监听。 

DraggableViewCallback 

继承于Callback,是它的实现类,ViewDragHelper里面处理的事件,我们可以通过该实现类进行监听回调。

ViewDragHelper的事件流程图如下所示

MotionEvent事件是从上往下传递的,如果其中的一个onInterceptTouchEvent返回了true,则表示该View拦截此事件系列,此后的MOVE,UP都不会再调用onInterceptTouchEvent,而是会直接调用自己的onTouchEvent方法。

之前文章里面提及的,我们自定义的ViewGroup控件的 onInterceptTouchEvent 方法,是通过 viewDragHelper.shouldInterceptTouchEvent(ev) 方法的返回值来决定是否拦截,当它返回 true 时,会直接触发该类自己的onTouchEvent方法;在onTouchEvent事件里面通过viewDragHelper 的 processTouchEvent(ev) 方法,将MotionEvent传递给viewDragHelper 内部,让viewDragHelper 对事件进行分析处理。以上就是在使用viewDragHelper时,事件分发的大概流程以及它的处理过程了,接下来将分析我们在onTouch 方法里将事件传递给viewDragHelper之后 ,它内部是如何对事件进行分析处理的。

本文由于篇幅关系,重点讲解的是以下几个部分: 

  • 抽象内部静态类 ViewDrageHelper .Callback。 

  • ViewDrageHelper 内部部分源码逻辑。 

  • VelocityTracker。 

  • ScrollerCompat。


ViewDragHelper源码


由UML类图我们不难看出,ViewDragHelper 是在我们自定义ViewGroup类的构造方法中初始化的,而Callback 是一个ViewDrageHelper 的内部静态抽象类。在创建ViewDragHelper 对象时,我们需要传入一个继承自Callback 的实现类实例对象进去。下面我们一步一步来剖析它的内部逻辑。

构造器

 /** 
     * Apps should use ViewDragHelper.create() to get a new instance. 
     * This will allow VDH to use internal compatibility implementations for different 
     * platform versions. 
     * 
     * @param context Context to initialize config-dependent params from 
     * @param forParent Parent view to monitor 
     */ 
    private ViewDragHelper(Context context, ViewGroup forParent, Callback cb)

由以上源码我们看到,它的构造器是私有的,也就是说我们并不能直接在外部通过new ViewDragHelper()的方式来创建对象。那么我们需要如何创建一个新的ViewDragHelper对象呢?不急,我们接着往下看。

创建对象

我们贴上关于创建对象以及初始化相关的完整源代码,其实,通过构造方法上面的英文注释可以知道,Google提供了两个工厂方法,让开发者去创建一个新的ViewDragHelper对象。如下所示:

  • create(ViewGroup forParent, Callback cb) 

该方法在return 时,利用构造器创建了一个新的ViewDragHelper实例。

  • create(ViewGroup forParent, float sensitivity, Callback cb) 

该方法内部,先调用了第一个工厂方法,得到新ViewDragHelper实例,之后又初始化了 mTouchSlop、mMaxVelocity 、mMinVelocity 、mScroller 等数据和对象。

不难看出含有sensitivity 这个参数的create方法,内部也是调用了create(forParent, cb)方法,只是它对mTouchSlop做了一下处理,传入的灵敏度(sensitivity值)越大,mTouchSlop的值越小。假设当前手机的系统mTouchSlop 大小为24dp, 若我们传入的sensitivity = 3.0f ,则mTouchSlop = 8 dp,即单次滑动距离超过8dp,就会触发系统的 MOVE事件。它的源码如下:

    /** 
     * 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; 
    } 

    /** 
     * Apps should use ViewDragHelper.create() to get a new instance. 
     * This will allow VDH to use internal compatibility implementations for different 
     * platform versions. 
     * 
     * @param context Context to initialize config-dependent params from 
     * @param forParent Parent view to monitor 
     */ 
    private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { 
        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); 
    }

滑动相关

smoothSlideViewTo方法

该方法用于平顺地滑动控件到指定位置。 child代表子控件对象, finalLeft代表滑动结束时,子控件左边所处的位置, finalTop 代表子控件顶部的位置。

那么,smoothSlideViewTo方法内部做了哪些操作呢?下面我们来看一看源代码:

    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) { 
            mCapturedView = null; 
        } 

        return continueSliding; 
    }

我们可以看到,它是一个布尔型的方法,如果此方法返回 true,则我们应该调用continueSettling方法让它继续滑动,直到返回false,这次滑动才算完成。

settleCapturedViewAt方法

该方法是以松手前的滑动速度为初值,让捕获到的子View自动滑动到指定位置,它只能在Callback的onViewReleased()中使用,若mReleaseInProgress不为True,则会抛出IllegalStateException异常。传递的两个参数分别是结束时子控件的位置,其内部最终调用的是forceSettleCapturedViewAt 方法。

    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)); 
    }
forceSettleCapturedViewAt 方法
    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; 
    }

由以上可看出,最终它是交给Scroller去处理滑动的,并且,滑动的时长是通过computeSettleDuration方法计算得到。那么computeSettleDuration内部又做了什么呢?我们继续往下看:

 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); 
    }

通过上面的一系列计算过后,得到的就是自动滑动所需的时间(毫秒)。

MotionEvent 相关

processTouchEvent 方法

若ViewDragHelper接受并处理父控件传递过来的触摸事件,则该方法内部会分析MotionEvent 事件,并根据需要,触发监听回调事件。需要强调的是:父控件的onTouchEvent实现方法需要调用processTouchEvent 方法,才能将事件传递给ViewDragHelper让其分析处理。

我们阅读其源码发现,首先,它做了如下操作:

public void processTouchEvent(MotionEvent ev) { 
        final int action = MotionEventCompat.getActionMasked(ev); 
        final int actionIndex = MotionEventCompat.getActionIndex(ev); 

        if (action == MotionEvent.ACTION_DOWN) { 
            // Reset things for a new event stream, just in case we didn't get 
            // the whole previous stream. 
            cancel(); 
        } 

        if (mVelocityTracker == null) { 
            mVelocityTracker = VelocityTracker.obtain(); 
        } 
        mVelocityTracker.addMovement(ev); 
        ... 
}

很显然,在ACTION_DOWN 即手指开始按下时,调用cancel方法重置了一下状态,以防以没有得到当前事件序列的完整事件输入流,而导致出错。

紧接着,若mVelocityTracker(速度跟踪器)对象为空,则通过VelocityTracker 的内部静态方法obtain 来创建一个新的对象,并通过addMovement将触摸事件添加监听,用于捕获用户手指滑动屏幕的速度。

然后通过switch 语句处理各种类型的ACTION事件,具体如下:

ACTION_DOWN 事件

case MotionEvent.ACTION_DOWN: { 
                final float x = ev.getX(); 
                final float y = ev.getY(); 
                final int pointerId = ev.getPointerId(0); 
                final View toCapture = findTopChildUnder((int) x, (int) y); 

                saveInitialMotion(x, y, pointerId); 

                // Since the parent is already directly processing this touch event, 
                // there is no reason to delay for a slop before dragging. 
                // Start immediately if possible. 
                tryCaptureViewForDrag(toCapture, pointerId); 

                final int edgesTouched = mInitialEdgesTouched[pointerId]; 
                if ((edgesTouched & mTrackingEdges) != 0) { 
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 
                } 
                break; 
            }

ACTION_DOWN是在第一个手指按下时触发,ViewDragHelper内部做了如下操作: 

  • 保存初始化x、y位置及pointerId。 

  • 调用tryCaptureViewForDrag 方法。直接回调true,因为父控件已经处理了ACTION_DOWN 事件。 

  • 若按下区域是在边缘,则触发onEdgeTouched 回调。

ACTION_POINTER_DOWN 事件

case MotionEventCompat.ACTION_POINTER_DOWN: { 
                final int pointerId = ev.getPointerId(actionIndex); 
                final float x = ev.getX(actionIndex); 
                final float y = ev.getY(actionIndex); 

                saveInitialMotion(x, y, pointerId); 

                // A ViewDragHelper can only manipulate one view at a time. 
                if (mDragState == STATE_IDLE) { 
                    // If we're idle we can do anything! Treat it like a normal down event. 

                    final View toCapture = findTopChildUnder((int) x, (int) y); 
                    tryCaptureViewForDrag(toCapture, pointerId); 

                    final int edgesTouched = mInitialEdgesTouched[pointerId]; 
                    if ((edgesTouched & mTrackingEdges) != 0) { 
                        mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 
                    } 
                } else if (isCapturedViewUnder((int) x, (int) y)) { 
                    // We're still tracking a captured view. If the same view is under this 
                    // point, we'll swap to controlling it with this pointer instead. 
                    // (This will still work if we're "catching" a settling view.) 

                    tryCaptureViewForDrag(mCapturedView, pointerId); 
                } 
                break; 
            }

由以上源码我们可以看出: 

  • 若mDragState 状态 为STATE_IDLE ,即处于闲置状态,则处理逻辑同ACTION_DOWN。 

  • 否则 直接调用tryCaptureViewForDrag 处理拖拽动作。

ACTION_MOVE 事件

            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,即拖拽状态。判断pointerId是否为无效id, 是则跳过。

获取触摸的x、y 位置,并调用dragTo 处理拖拽事件,然后调用saveLastMotion保存一下当前Motion。

若mDragState 状态不是 STATE_DRAGGING,则检查一遍 pointerId列表,看是否有Id处于可拖动状态并进行处理。

ACTION_POINTER_UP 事件

 case MotionEventCompat.ACTION_POINTER_UP: { 
                final int pointerId = ev.getPointerId(actionIndex); 
                if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { 
                    // Try to find another pointer that's still holding on to the captured view. 
                    int newActivePointer = INVALID_POINTER; 
                    final int pointerCount = ev.getPointerCount(); 
                    for (int i = 0; i < pointerCount; i++) { 
                        final int id = ev.getPointerId(i); 
                        if (id == mActivePointerId) { 
                            // This one's going away, skip. 
                            continue; 
                        } 

                        final float x = ev.getX(i); 
                        final float y = ev.getY(i); 
                        if (findTopChildUnder((int) x, (int) y) == mCapturedView 
                                && tryCaptureViewForDrag(mCapturedView, id)) { 
                            newActivePointer = mActivePointerId; 
                            break; 
                        } 
                    } 

                    if (newActivePointer == INVALID_POINTER) { 
                        // We didn't find another pointer still touching the view, release it. 
                        releaseViewForPointerUp(); 
                    } 
                } 
                clearMotionHistory(pointerId); 
                break; 
            }

如果mDragState 状态为 STATE_DRAGGING ,并且 pointerId 为当前行动的Id,则遍历一次pointerId 列表并进行处理,最后调用clearMotionHistory清除事件的历史记录。

ACTION_UP 事件

 case MotionEvent.ACTION_UP: { 
                if (mDragState == STATE_DRAGGING) { 
                    releaseViewForPointerUp(); 
                } 
                cancel(); 
                break; 
            }

如果 mDragState 状态为 STATE_DRAGGING, 则调用releaseViewForPointerUp方法,该方法会计算当前滑动速度,并调用dispatchViewReleased方法,计算松开手指时的X、Y轴的速度,并通过mCallback的onViewReleased方法回调出去。然后调用cancel重置状态。

ACTION_CANCEL 事件

  case MotionEvent.ACTION_CANCEL: { 
                if (mDragState == STATE_DRAGGING) { 
                    dispatchViewReleased(0, 0); 
                } 
                cancel(); 
                break; 
            }

如果mDragState 状态为 STATE_DRAGGING,则直接调用dispatchViewReleased方法,传递的初始X、Y轴速度为0;然后调用cancel重置状态。


ViewDragHelper.Callback部分解读


以上介绍了ViewDragHelper 类内部对MotionEvent事件处理的逻辑,那么它在处理完成后,是如何通知ViewGroup的呢? 很明显,ViewDragHelper 的静态内部抽象类Callback ,它的职责就是将触发的事件及结果返回给ViewGroup的。前面我们已经讲过了,我们在创建ViewDragHelper的过程中,需要实例化一个继承自ViewDragHelper.Callback的实现类,并将这个实现类的实例对象传入了ViewDragHelper,因此ViewDragHelper通过create方法传递进来的参数,持有实现类的对象实例。

在我们的实现类 DraggableViewCallback 中,我们可根据需求来覆盖父类Callback所提供的方法以实现相关监听。其中,抽象类Callback的抽象方法: tryCaptureView() 是必须要在DraggableViewCallback 中实现的。

首先我们看这个抽象内部静态类的完整源代码:

  public abstract static 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 
         * @param dy Change in Y position from the last call 
         */ 
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} 

        /** 
         * Called when a child view is captured for dragging or settling. The ID of the pointer 
         * currently dragging the captured view is supplied. If activePointerId is 
         * identified as {@link #INVALID_POINTER} the capture is programmatic instead of 
         * pointer-initiated. 
         * 
         * @param capturedChild Child view that was captured 
         * @param activePointerId Pointer id tracking the child capture 
         */ 
        public void onViewCaptured(View capturedChild, int activePointerId) {} 

        /** 
         * Called when the child view is no longer being actively dragged. 
         * The fling velocity is also supplied, if relevant. The velocity values may 
         * be clamped to system minimums or maximums. 
         * 
         * <p>Calling code may decide to fling or otherwise release the view to let it 
         * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} 
         * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes 
         * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} 
         * and the view capture will not fully end until it comes to a complete stop. 
         * If neither of these methods is invoked before <code>onViewReleased</code> returns, 
         * the view will stop in place and the ViewDragHelper will return to 
         * {@link #STATE_IDLE}.</p> 
         * 
         * @param releasedChild The captured child view now being released 
         * @param xvel X velocity of the pointer as it left the screen in pixels per second. 
         * @param yvel Y velocity of the pointer as it left the screen in pixels per second. 
         */ 
        public void onViewReleased(View releasedChild, float xvel, float yvel) {} 

        /** 
         * Called when one of the subscribed edges in the parent view has been touched 
         * by the user while no child view is currently captured. 
         * 
         * @param edgeFlags A combination of edge flags describing the edge(s) currently touched 
         * @param pointerId ID of the pointer touching the described edge(s) 
         * @see #EDGE_LEFT 
         * @see #EDGE_TOP 
         * @see #EDGE_RIGHT 
         * @see #EDGE_BOTTOM 
         */ 
        public void onEdgeTouched(int edgeFlags, int pointerId) {} 

        /** 
         * Called when the given edge may become locked. This can happen if an edge drag 
         * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} 
         * was called. This method should return true to lock this edge or false to leave it 
         * unlocked. The default behavior is to leave edges unlocked. 
         * 
         * @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; 
        } 

        /** 
         * Called when the user has started a deliberate drag away from one 
         * of the subscribed edges in the parent view while no child view is currently captured. 
         * 
         * @param edgeFlags A combination of edge flags describing the edge(s) dragged 
         * @param pointerId ID of the pointer touching the described edge(s) 
         * @see #EDGE_LEFT 
         * @see #EDGE_TOP 
         * @see #EDGE_RIGHT 
         * @see #EDGE_BOTTOM 
         */ 
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {} 

        /** 
         * Called to determine the Z-order of child views. 
         * 
         * @param index the ordered position to query for 
         * @return index of the view that should be ordered at position <code>index</code> 
         */ 
        public int getOrderedChildIndex(int index) { 
            return index; 
        } 

        /** 
         * Return the magnitude of a draggable child view's horizontal range of motion in pixels. 
         * This method should return 0 for views that cannot move horizontally. 
         * 
         * @param child Child view to check 
         * @return range of horizontal motion in pixels 
         */ 
        public int getViewHorizontalDragRange(View child) { 
            return 0; 
        } 

        /** 
         * Return the magnitude of a draggable child view's vertical range of motion in pixels. 
         * This method should return 0 for views that cannot move vertically. 
         * 
         * @param child Child view to check 
         * @return range of vertical motion in pixels 
         */ 
        public int getViewVerticalDragRange(View child) { 
            return 0; 
        } 

        /** 
         * Called when the user's input indicates that they want to capture the given child view 
         * with the pointer indicated by pointerId. The callback should return true if the user 
         * is permitted to drag the given view with the indicated pointer. 
         * 
         * <p>ViewDragHelper may call this method multiple times for the same view even if 
         * the view is already captured; this indicates that a new pointer is trying to take 
         * control of the view.</p> 
         * 
         * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} 
         * will follow if the capture is successful.</p> 
         * 
         * @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); 

        /** 
         * Restrict the motion of the dragged child view along the horizontal axis. 
         * The default implementation does not allow horizontal motion; the extending 
         * class must override this method and provide the desired clamping. 
         * 
         * 
         * @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; 
        } 

        /** 
         * Restrict the motion of the dragged child view along the vertical axis. 
         * The default implementation does not allow vertical motion; the extending 
         * class must override this method and provide the desired clamping. 
         * 
         * 
         * @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; 
        } 
    }

英文水平不赖的朋友也可以直接阅读英文源码注释,下面是我对这些方法的一些个人理解及总结,用中文写出来以方便快速阅读:

onViewDragStateChanged(int state) 方法

当View的拖拽状态改变时,回调该方法。state有三种状态:

  • STATE_IDLE = 0    当前处于闲置状态

  • STATE_DRAGGING = 1   正在被拖拽的状态

  • STATE_SETTLING = 2   拖拽后被安放到一个位置中的状态

onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 方法

View被拖拽,位置发生改变时回调

changedView :被拖拽的View

left : 被拖拽后 View的 left 坐标

top : 被拖拽后 View的 top 坐标 

dx :  拖动的x偏移量

dy :  拖动的y偏移量

public void onViewCaptured(View capturedChild, int activePointerId) 方法

当子控件被捕获到准备开始拖动时回调

capturedChild : 捕获的View

activePointerId : 对应的PointerId

public void onViewReleased(View releasedChild, float xvel, float yvel) 方法

当被捕获拖拽的View被释放时回调

releasedChild : 被释放的View

xvel : 释放View的x方向上的加速度

yvel : 释放View的y方向上的加速度

public void onEdgeTouched(int edgeFlags, int pointerId) 方法

如果parentView订阅了边缘触摸,则如果有边缘触摸就回调的接口

edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM

pointerId : 用来描述边缘触摸操作的id

public boolean onEdgeLock(int edgeFlags) 方法

是否锁定该边缘的触摸,默认返回false,返回true表示锁定

public void onEdgeDragStarted(int edgeFlags, int pointerId)

边缘触摸开始时回调

edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM

pointerId : 用来描述边缘触摸操作的id

public int getOrderedChildIndex(int index)

在寻找当前触摸点下的子View时会调用此方法,寻找到的View会提供给tryCaptureViewForDrag()来尝试捕获。

如果需要改变子View的遍历查询顺序可改写此方法,例如让下层的View优先于上层的View被选中。

public int getViewHorizontalDragRange(View child)

获取被拖拽View child 的水平拖拽范围,返回0表示无法被水平拖拽

public int getViewVerticalDragRange(View child)

获取被拖拽View child 的竖直拖拽范围,返回0表示无法被竖直拖拽

public abstract boolean tryCaptureView(View child, int pointerId);

是否捕获被拖拽的子View,child 为被触摸的子控件, 返回 true则表示允许拖拽,返回false则表示禁止。

public int clampViewPositionHorizontal(View child, int left, int dx)

该方法决定被拖拽的View在水平方向上应该移动到的位置。

child : 被拖拽的View

left : 期望移动到位置的View的left值

dx : 移动的水平距离

返回值 : 直接决定View在水平方向的位置

public int clampViewPositionVertical(View child, int top, int dy)

该方法决定被拖拽的View在垂直方向上应该移动到的位置。

child : 被拖拽的View

top : 期望移动到位置的View的top值

dy : 移动的垂直距离

返回值 : 直接决定View在垂直方向的位置

总之就是根据request和response的header的cache-control来做缓存,我们可以严格按照http协议的来做缓存策略,而不用去看okhttp协议怎么实现的(嗯,okhttp应该是严格按照http协议来写的吧?)


ScrollerCompat


ScrollerCompat是一个实现View平滑滚动的Helper类。从ScrollerCompat的源码我们可以看出,它其实就是封装了OverScroller。ScrollerCompat类的内部截图如下:

事实上,我们常用的ScrollView,它内部也是通过OverScroller 来实现的。有图有真相:

说到OverScroller,我们可能立马会想起Scroller,那么OverScroller和Scroller有什么区别呢?

事实上,这两个类它都属于Scrollers,Scroller属于早期的API,在API 11所提供的。而OverScroller是在API 19才新增的。翻阅他们内部源码我们不难看出,这两个类大部分的API是一致的。从字面上我们可以看出,Over的意思就是超出,即OverScroller提供了对超出滑动边界情况的处理逻辑,OverScroller的功能及逻辑相对而言比较完善。关于ScrollerCompat、Scroller、OverScroller的解读,大家有兴趣可自行查阅相关资料,这里就不作深入讨论了。

ScrollerCompat 在 ViewDragHelper 类中使用到的地方有如下几处:

abort () 方法

    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); 
    }

不难看出,该方法主要利用mScroller来获取当前X、Y位置以及动画终止后的X、Y位置。并通过onViewPositionChanged 回调外部newX、newY,以及dx、dy。

forceSettleCapturedViewAt () 方法

 final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); 
 mScroller.startScroll(startLeft, startTop, dx, dy, duration);

主要用于平顺滑动处理,duration 时长取决于初始速度及终点距离长短。

flingCapturedView() 方法

 public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { 
        if (!mReleaseInProgress) { 
            throw new IllegalStateException("Cannot flingCapturedView outside of a call to " 
                    + "Callback#onViewReleased"); 
        } 

        mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), 
                (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 
                (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), 
                minLeft, maxLeft, minTop, maxTop); 

        setDragState(STATE_SETTLING); 
    }

可以看出其实就是对 mScroller.fling () 方法的封装。

continueSettling() 方法

该方法主要利用mScroller 获取当前位置CurrX、CurrY,以及最终滑动停留的位置FinalX、FinalY。然后处理动画,生成惯性滑动的效果。


总结


到此ViewDragHelper的源码就解析完了,我们由此可知,ViewDragHelper本质上是对MotionEvent的分析及处理,并提供了一系列的监听回调方法,来帮助我们减轻开发负担,更为方便地处理控件的滑动拖拽逻辑。总而言之,深入阅读源码,过程虽然会有点辛苦,但理解程度会有很大的提升~ 有兴趣的朋友可自行查看它的源代码,第三篇将会是深入实战篇。后续有时间会陆续写好分享出来。感谢支持~ 希望能帮助到有需要的人。


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

版权声明:本文为博主原创文章,未经博主允许不得转载。

2018年首篇,带大家一起学习视图拖拽功能的源码

今天的快讯我们对2017年的重大事件进行一下回顾吧。 乐视事件 回溯乐视的2017,“眼看他起朱楼,眼看他宴宾客,眼看他楼塌了”。乐视历经了来自融创中国百亿融资、高层相继离职、裁员风波、资...
  • zz901214
  • zz901214
  • 2018年01月04日 16:17
  • 572

树形结构的增删改拖拽

  • 2016年01月04日 13:18
  • 358KB
  • 下载

二级目录拖拽排序实现之演示源码

  • 2012年09月05日 12:08
  • 75KB
  • 下载

VS2017下安装fltk库——C++程序设计原理与实践图形编程指南

VS2017下安装fltk库——C++程序设计原理与实践图形编程指南前言最近,我在学习《C++程序设计原理与实践》(原书第一版)遇到了安装图形库的问题,我花了两天时间,通过各种途径查找解决办法,终于成...
  • wyf12138
  • wyf12138
  • 2017年07月08日 11:11
  • 1637

MFC----文件拖拽的实现OnDropFiles

接着上篇的讲http://blog.csdn.net/yf210yf/article/details/7853741 这篇实现文件的拖拽功能。 实现的消息是WM_DROPFILES------>O...
  • yf210yf
  • yf210yf
  • 2012年08月11日 09:21
  • 12627

MFC开发实用教程1:CListCtrl控件实现Item项拖拽效果

CListCtrl控件拖拽Item项,闲话少说,先上效果图给大家看看 看到效果图后,大家是不是觉得很赞,是不是很想知道,这样的效果是怎样实现的呢?ok 直奔主题,下面我说说实现步骤 一、添加一个...
  • d704791892
  • d704791892
  • 2014年03月30日 19:14
  • 2669

2018年第一个学习计划

2018年第一个学习计划 本学期总结 看完的书只有《Head First Java》,这个学期还有两周,把testbed跑起来,把java视频还有10天的份额看完,视频看累了就看下计算机网络和操作系...
  • Liuss2
  • Liuss2
  • 2018年01月21日 19:56
  • 278

2017年,感谢你们,2018年,我们继续努力前行

昨晚在看罗胖《时间的朋友》跨年演讲,微信上收到几个读者的红包,其中有一位是去年6月份无意中关注了公众号,加了微信号,接触了 Python,按照他自己的话说:某渣二本学校,土木工程专业大四学生,非常想进...
  • zV3e189oS5c0tSknrBCL
  • zV3e189oS5c0tSknrBCL
  • 2018年01月01日 00:00
  • 579

CListCtrl实现拖拽 效果

方法1:void ClistOx::OnLvnBegindrag(NMHDR *pNMHDR, LRESULT *pResult){LPNMLISTVIEW pNMLV = reinterpret_c...
  • lin_angle
  • lin_angle
  • 2011年02月11日 11:52
  • 3341

VC之列表控件点击事件处理

本文简单介绍下VC列表控件的点击事件。 1.新建对话框应用程序 新建对话框应用程序,并且添加List列表控件。 2.设置相关变量 class CListClickDlg : public ...
  • bingdianlanxin
  • bingdianlanxin
  • 2015年04月05日 19:47
  • 816
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:2018年首篇,带大家一起学习视图拖拽功能的源码
举报原因:
原因补充:

(最多只允许输入30个字)