ViewPager源码解析之拖动和滑动

来源:互联网 发布:mac os关闭窗口快捷键 编辑:程序博客网 时间:2024/06/07 15:03

概述

一个ViewGroup的滑动肯定和它的dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()有关。ViewPager重写了后两者,我们一个个来看。首先说明一下,ViewPager根据手势产生视图移动的方式有两种,一种是MOVE的时候随手指的拖动,一种是UP之后滑动到指定页面,而滑动是通过Scroller + computeScroll()实现的。

onInterceptTouchEvent()

Viewpager有一个mScrollState成员维护着ViewPager当前页面的状态,它可能被赋三个值。

/**     * Indicates that the pager is in an idle, settled state. The current page     * is fully in view and no animation is in progress.     */    //当前page空闲,没有动画    public static final int SCROLL_STATE_IDLE = 0;    /**     * Indicates that the pager is currently being dragged by the user.     *///正在被拖动    public static final int SCROLL_STATE_DRAGGING = 1;    /**     * Indicates that the pager is in the process of settling to a final position.     */    //正在向最终位置移动    public static final int SCROLL_STATE_SETTLING = 2;

正如onInterceptTouchEvent()注释所说的,方法只是判断我们是否应该拦截这个Touch事件,scrolling都交给onTouchEvent()去做。因为在switch中每一个分支都有break,所以我调换了一下源码的顺序把DOWN放在了MOVE前面,这样更加清晰。

   @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        /*         * This method JUST determines whether we want to intercept the motion.         * If we return true, onMotionEvent will be called and we do the actual         * scrolling there.         */        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;        // Always take care of the touch gesture being complete.      //如果一套手势结束,返回false        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {            // Release the drag.            if (DEBUG) Log.v(TAG, "Intercept done!");            resetTouch();            return false;        }        // Nothing more to do here if we have decided whether or not we        // are dragging.        if (action != MotionEvent.ACTION_DOWN) {          //如果正在被drag,拦截            if (mIsBeingDragged) {                if (DEBUG) Log.v(TAG, "Intercept returning true!");                return true;            }          //不允许drag,不拦截            if (mIsUnableToDrag) {                if (DEBUG) Log.v(TAG, "Intercept returning false!");                return false;            }        }        switch (action) {            case MotionEvent.ACTION_DOWN: {                /*                 * Remember location of down touch.                 * ACTION_DOWN always refers to pointer index 0.                 */              //重新给这四个变量赋值,表示一套手势的开始                mLastMotionX = mInitialMotionX = ev.getX();                mLastMotionY = mInitialMotionY = ev.getY();                //获取第一个触摸点的id                mActivePointerId = ev.getPointerId(0);                //设置允许拖拽为false                mIsUnableToDrag = false;                //标记为开始滚动                mIsScrollStarted = true;                //这个Scroller是在initViewPager()中创建的,这里手动调用计算一下                    //Scroller中的x,y值                mScroller.computeScrollOffset();              //如果此时正在向终态靠拢,并且离最终位置还有一定距离                if (mScrollState == SCROLL_STATE_SETTLING                        && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {                    // Let the user 'catch' the pager as it animates.                  //让用户抓住这个Pager                  //停止滚动                    mScroller.abortAnimation();                  //???                    mPopulatePending = false;                  //更新缓存page信息(滚动的时候mCurItem会改变?)                    populate();                  //表示在拖动                    mIsBeingDragged = true;                  //不允许父ViewGroup拦截                    requestParentDisallowInterceptTouchEvent(true);                  //设置新的状态                    setScrollState(SCROLL_STATE_DRAGGING);                } else {                  //这里我理解的是正在向终态靠近且距离足够小了,所以不能干涉移动                    completeScroll(false);                    mIsBeingDragged = false;                }                ....                break;            }            case MotionEvent.ACTION_MOVE: {                /*                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check                 * whether the user has moved far enough from his original down touch.                 */                //注释的很清楚,如果能进入这个地方说明mIsBeingDragged == false                //这里要检查用户是否已经从原始位置移动的够远,以给mIsBeingDragged赋值                //第一个触摸点的id                final int activePointerId = mActivePointerId;                if (activePointerId == INVALID_POINTER) {                    // If we don't have a valid id, the touch down wasn't on content.                    break;                }                //不太懂,貌似对第一个触摸点id做了一个转换                final int pointerIndex = ev.findPointerIndex(activePointerId);               //触摸点横坐标                final float x = ev.getX(pointerIndex);                //横向偏移                final float dx = x - mLastMotionX;                //横向偏移绝对值                final float xDiff = Math.abs(dx);                //纵向                final float y = ev.getY(pointerIndex);                final float yDiff = Math.abs(y - mInitialMotionY);                ...                  //这里是说如果在这个区域子View可以滑动,交给子View处理,不拦截                  //canScroll的源码贴在后面,在子View中寻找可以滑动的                if (dx != 0 && !isGutterDrag(mLastMotionX, dx)                        && canScroll(this, false, (int) dx, (int) x, (int) y)) {                    // Nested view has scrollable area under this point. Let it be handled there.                    mLastMotionX = x;                    mLastMotionY = y;                    mIsUnableToDrag = true;                    return false;                }                //如果横向偏移绝对值大于最小值 且 yDiff/xDiff < 0.5f                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {                    if (DEBUG) Log.v(TAG, "Starting drag!");                    //拦截!                    mIsBeingDragged = true;                    requestParentDisallowInterceptTouchEvent(true);                    setScrollState(SCROLL_STATE_DRAGGING);                   //保存当前位置                    mLastMotionX = dx > 0                            ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;                    mLastMotionY = y;                    setScrollingCacheEnabled(true);                } else if (yDiff > mTouchSlop) {                    //如果在纵向移动了足够的距离,不拦截                    // The finger has moved enough in the vertical                    // direction to be counted as a drag...  abort                    // any attempt to drag horizontally, to work correctly                    // with children that have scrolling containers.                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");                    mIsUnableToDrag = true;                }              //                if (mIsBeingDragged) {                    // Scroll to follow the motion event                    //如果能拖拽,这里产生拖拽,很重要,后面分析。                    if (performDrag(x)) {                        ViewCompat.postInvalidateOnAnimation(this);                    }                }                break;            }            case MotionEventCompat.ACTION_POINTER_UP:                onSecondaryPointerUp(ev);                break;        }        ...        return mIsBeingDragged;    }

遍历子View,触点在子View的边界内,且子View可以滑动,返回true

    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {        if (v instanceof ViewGroup) {            final ViewGroup group = (ViewGroup) v;            final int scrollX = v.getScrollX();            final int scrollY = v.getScrollY();            final int count = group.getChildCount();            // Count backwards - let topmost views consume scroll distance first.            for (int i = count - 1; i >= 0; i--) {                // TODO: Add versioned support here for transformed views.                // This will not work for transformed views in Honeycomb+                final View child = group.getChildAt(i);                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()                        && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()                        && canScroll(child, true, dx, x + scrollX - child.getLeft(),                                y + scrollY - child.getTop())) {                    return true;                }            }        }        return checkV && ViewCompat.canScrollHorizontally(v, -dx);    }

performDrag()

前面onInterceptTouchEvent()判断了很多情况,基本都是根据情况判断能不能drag,然后给mIsBeingDragged这个变量赋值,代表最终是否拦截接下来的一串手势。在MOVE的末尾,在可以drag的情况下,我们会进入这个方法来让页面跟随手指的手势。

    private boolean performDrag(float x) {        boolean needsInvalidate = false;        final float deltaX = mLastMotionX - x;        mLastMotionX = x;        float oldScrollX = getScrollX();        //ViewPager的视图横坐标        float scrollX = oldScrollX + deltaX;        final int width = getClientWidth();        //子View左边界和子View右边界        float leftBound = width * mFirstOffset;        float rightBound = width * mLastOffset;        boolean leftAbsolute = true;        boolean rightAbsolute = true;        //当前第一个和最后一个页面信息        final ItemInfo firstItem = mItems.get(0);        final ItemInfo lastItem = mItems.get(mItems.size() - 1);        //如果第一个页面信息不是数据的第0项,更新一下leftBound        if (firstItem.position != 0) {            leftAbsolute = false;            leftBound = firstItem.offset * width;        }      //同理        if (lastItem.position != mAdapter.getCount() - 1) {            rightAbsolute = false;            rightBound = lastItem.offset * width;        }        //边界条件        if (scrollX < leftBound) {            if (leftAbsolute) {                float over = leftBound - scrollX;                needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);            }            scrollX = leftBound;        } else if (scrollX > rightBound) {            if (rightAbsolute) {                float over = scrollX - rightBound;                needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);            }            scrollX = rightBound;        }        // Don't lose the rounded component        mLastMotionX += scrollX - (int) scrollX;      //滑动视图        scrollTo((int) scrollX, getScrollY());        //重要方法        pageScrolled((int) scrollX);        return needsInvalidate;    }

pageScrolled()

performDrag()方法让ViewPager的视图滑动了(通过scrollTo()方法),并且调用了这个方法,现在我们来看一下这个方法。从整体上来看,这个方法做了这么几件事:

1.根据视图的scrollX获得了当前的页面信息。

2.计算了视图滑动距离的比例和像素。

3.onPageScrolled(currentPage, pageOffset, offsetPixels)

这里说一下,如果滑动造成ViewPager显示区域内有两个Page可以显示,infoForCurrentScrollPosition()返回的是左边那个的ItemInfo。

    private boolean pageScrolled(int xpos) {        //传进来的参数指的是ViewPager视图滑动的距离。        ...         //根据scrollX获取当前应该显示的页面信息        final ItemInfo ii = infoForCurrentScrollPosition();        final int width = getClientWidth();        final int widthWithMargin = width + mPageMargin;        final float marginOffset = (float) mPageMargin / width;        //当前页面的position(这个position是指在数据列表中的位置)        final int currentPage = ii.position;        //这里计算的是 当前页面视图滑动距离的比例 和 当前页面宽度比例  的比        //如果第二项是1的话,也就代表了 当前页面视图滑动距离(和页面宽度)的比例        final float pageOffset = (((float) xpos / width) - ii.offset)                / (ii.widthFactor + marginOffset);      //表示视图滑动的像素        final int offsetPixels = (int) (pageOffset * widthWithMargin);        mCalledSuper = false;        onPageScrolled(currentPage, pageOffset, offsetPixels);        if (!mCalledSuper) {            throw new IllegalStateException(                    "onPageScrolled did not call superclass implementation");        }        return true;    }

onPageScrolled(currentPage, pageOffset, offsetPixels)

这个方法在很多滚动的地方都会被调用,我们可以重写这个方法实现一些动画效果。纵观这个方法,做了这么几件事:

1.对DecorView进行滚动

2.回调接口的onPageScrolled(),就是我们自己添加的接口。

3.实现动画

protected void onPageScrolled(int position, float offset, int offsetPixels) {        // Offset any decor views if needed - keep them on-screen at all times.        //1        if (mDecorChildCount > 0) {            final int scrollX = getScrollX();            int paddingLeft = getPaddingLeft();            int paddingRight = getPaddingRight();            final int width = getWidth();            final int childCount = getChildCount();            for (int i = 0; i < childCount; i++) {                final View child = getChildAt(i);                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                if (!lp.isDecor) continue;                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;                int childLeft = 0;                switch (hgrav) {                    default:                        childLeft = paddingLeft;                        break;                    case Gravity.LEFT:                        childLeft = paddingLeft;                        paddingLeft += child.getWidth();                        break;                    case Gravity.CENTER_HORIZONTAL:                        childLeft = Math.max((width - child.getMeasuredWidth()) / 2,                                paddingLeft);                        break;                    case Gravity.RIGHT:                        childLeft = width - paddingRight - child.getMeasuredWidth();                        paddingRight += child.getMeasuredWidth();                        break;                }                childLeft += scrollX;                final int childOffset = childLeft - child.getLeft();                if (childOffset != 0) {                    child.offsetLeftAndRight(childOffset);                }            }        }        //2        dispatchOnPageScrolled(position, offset, offsetPixels);        //3        if (mPageTransformer != null) {            final int scrollX = getScrollX();            final int childCount = getChildCount();            for (int i = 0; i < childCount; i++) {                final View child = getChildAt(i);                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                if (lp.isDecor) continue;                final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();                mPageTransformer.transformPage(child, transformPos);            }        }        mCalledSuper = true;    }

回过头来小结一下,onInterceptTouchEvent()中会根据不同情况对mIsBeingDragged进行赋值,在MOVE中如果是可滑动的,就调用scrollTo对视图进行滑动形成拖拽效果,接着pageScrolled()中获得当前页面的信息和偏移量传入onPageScrolled(),onPageScrolled()中对Decor View进行位移,回调接口,产生动画。

onInterceptTouchEvent()产生了拖动效果,但主要还是对是否拦截作出判断,接下来我们看一下onTouchEvent()。

onTouchEvent()

纵观整个方法,MOVE中依旧是拖动,而UP的时候会根据 当前页面、当前页面的offset、速度、横向移动距离 计算出下一个应该显示的页面nextPage,接着调用setCurrentItemInternal()产生滑动。

    @Override    public boolean onTouchEvent(MotionEvent ev) {        //一些情况的判断,省略        ...        mVelocityTracker.addMovement(ev);        final int action = ev.getAction();        boolean needsInvalidate = false;        switch (action & MotionEventCompat.ACTION_MASK) {            case MotionEvent.ACTION_DOWN: {                //立即将Scroller中的x,y设为终值                mScroller.abortAnimation();                mPopulatePending = false;                //根据mCurIndex更新需要缓存的页面信息                populate();                // Remember where the motion event started                //记录                mLastMotionX = mInitialMotionX = ev.getX();                mLastMotionY = mInitialMotionY = ev.getY();                mActivePointerId = ev.getPointerId(0);                break;            }            case MotionEvent.ACTION_MOVE:                //如果不在drag(这里有可能是因为没有消耗手势的子View,所以返回来让ViewPager处理)                if (!mIsBeingDragged) {                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);                    if (pointerIndex == -1) {                        // A child has consumed some touch events and put us into an inconsistent                        // state.                        needsInvalidate = resetTouch();                        break;                    }                    //计算横向和纵向偏移                    final float x = ev.getX(pointerIndex);                    final float xDiff = Math.abs(x - mLastMotionX);                    final float y = ev.getY(pointerIndex);                    final float yDiff = Math.abs(y - mLastMotionY);                    //如果横向偏移足够大 且 横向偏移大于纵向偏移则可以开始drag                    if (xDiff > mTouchSlop && xDiff > yDiff) {                        if (DEBUG) Log.v(TAG, "Starting drag!");                        mIsBeingDragged = true;                        requestParentDisallowInterceptTouchEvent(true);                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :                                mInitialMotionX - mTouchSlop;                        mLastMotionY = y;                        setScrollState(SCROLL_STATE_DRAGGING);                        setScrollingCacheEnabled(true);                        // Disallow Parent Intercept, just in case                        ViewParent parent = getParent();                        if (parent != null) {                            parent.requestDisallowInterceptTouchEvent(true);                        }                    }                }                // Not else! Note that mIsBeingDragged can be set above.                //如果可以drag                if (mIsBeingDragged) {                    // Scroll to follow the motion event                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);                    final float x = ev.getX(activePointerIndex);                    //实现拖动,performDrag上面已经分析过了                    needsInvalidate |= performDrag(x);                }                break;            case MotionEvent.ACTION_UP:                //如果是拖动的时候up                if (mIsBeingDragged) {                    //计算x速度                    final VelocityTracker velocityTracker = mVelocityTracker;                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(                            velocityTracker, mActivePointerId);                    mPopulatePending = true;                    //ViewPager显示宽度                    final int width = getClientWidth();                    //视图横向滑动距离                    final int scrollX = getScrollX();                    //根据scrollX计算出当前的页面信息。                    final ItemInfo ii = infoForCurrentScrollPosition();                    //边缘占比                    final float marginOffset = (float) mPageMargin / width;                    //当前页面在数据列表中的位置                    final int currentPage = ii.position;                    //计算当前页面偏移                    final float pageOffset = (((float) scrollX / width) - ii.offset)                            / (ii.widthFactor + marginOffset);                    //横向偏移                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);                    final float x = ev.getX(activePointerIndex);                    final int totalDelta = (int) (x - mInitialMotionX);                   //确定up后终态所在的页面                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,                            totalDelta);                  //重要,这里是产生滑动的关键                    setCurrentItemInternal(nextPage, true, true, initialVelocity);                    needsInvalidate = resetTouch();                }                break;            ...            case MotionEventCompat.ACTION_POINTER_DOWN: {                final int index = MotionEventCompat.getActionIndex(ev);                final float x = ev.getX(index);                //多点触摸,换了另外一个手指过后更新mLastMotionX和mActivePointerId                mLastMotionX = x;                mActivePointerId = ev.getPointerId(index);                break;            }            case MotionEventCompat.ACTION_POINTER_UP:                //貌似是多点触摸下一个手指抬起了,要更新mLastMotionX                onSecondaryPointerUp(ev);                mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));                break;        }        if (needsInvalidate) {            ViewCompat.postInvalidateOnAnimation(this);        }        return true;    }

setCurrentItemInternal()

这个方法的作用是判断应该交给onMeasure()还是scrollToItem()去完成页面的set。

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {       //一些健壮性判断,省略        ...        final int pageLimit = mOffscreenPageLimit;       //有关跳跃性滑动,跳跃的过程中我们不会让涉及到的页面被更新        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {            // We are doing a jump by more than one page.  To avoid            // glitches, we want to keep all current pages in the view            // until the scroll ends.            for (int i = 0; i < mItems.size(); i++) {                mItems.get(i).scrolling = true;            }        }        final boolean dispatchSelected = mCurItem != item;        //还记得我们上一篇开始的时候我们会进入到这个分支,但是这里不会了。        if (mFirstLayout) {            mCurItem = item;            if (dispatchSelected) {                dispatchOnPageSelected(item);            }            requestLayout();        } else {//这里我们更新页面信息,并且滑动到目标页面            populate(item);            scrollToItem(item, smoothScroll, velocity, dispatchSelected);        }    }

scrollToItem()

根据是否为smoothScroll来进行不同的滑动,smoothScrollTo()或者直接scrollTo()。

    private void scrollToItem(int item, boolean smoothScroll, int velocity,            boolean dispatchSelected) {        final ItemInfo curInfo = infoForPosition(item);        int destX = 0;        if (curInfo != null) {            final int width = getClientWidth();            //计算偏移量            destX = (int) (width * Math.max(mFirstOffset,                    Math.min(curInfo.offset, mLastOffset)));        }        //如果是平滑滑动        if (smoothScroll) {            //后面具体解析            smoothScrollTo(destX, 0, velocity);            //回调接口            if (dispatchSelected) {                dispatchOnPageSelected(item);            }        } else {          //回调接口            if (dispatchSelected) {                dispatchOnPageSelected(item);            }            //直接用scrollTo的方式结束滑动            completeScroll(false);            scrollTo(destX, 0);            pageScrolled(destX);        }    }

smoothScrollTo()

   void smoothScrollTo(int x, int y, int velocity) {        ...        //x轴滑动起始位置        int sx;        boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();        //如果此时在滚动        if (wasScrolling) {          //更新起始位置            sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();            mScroller.abortAnimation();            setScrollingCacheEnabled(false);        } else {            sx = getScrollX();        }        //y轴滑动起始位置        int sy = getScrollY();        int dx = x - sx;        int dy = y - sy;        if (dx == 0 && dy == 0) {            completeScroll(false);            populate();            setScrollState(SCROLL_STATE_IDLE);            return;        }        setScrollingCacheEnabled(true);        setScrollState(SCROLL_STATE_SETTLING);        final int width = getClientWidth();        final int halfWidth = width / 2;        //滑动距离,距离影响时间        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / 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 pageWidth = width * mAdapter.getPageWidth(mCurItem);            final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);            duration = (int) ((pageDelta + 1) * 100);        }        duration = Math.min(duration, MAX_SETTLE_DURATION);        // Reset the "scroll started" flag. It will be flipped to true in all places        // where we call computeScrollOffset().        mIsScrollStarted = false;        //重要的地方,就是利用Scroller产生弹性滑动        mScroller.startScroll(sx, sy, dx, dy, duration);       //重绘以便回调        ViewCompat.postInvalidateOnAnimation(this);    }

computeScroll()

通过前面的分析我们知道了ViewPager是利用Scroller产生谈性滑动,Scroller产生弹性滑动的关键在于onDraw()中会回调computeScroll(),然后在这个方法里用scrollTo()滑动并再次申请重绘。ViewPager重写了这个方法,在调用scrollTo()之后还调用了pageScrolled(x)对Decor View进行更新、回调接口、产生动画。之后申请重绘。

    @Override    public void computeScroll() {        mIsScrollStarted = true;        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {            int oldX = getScrollX();            int oldY = getScrollY();            int x = mScroller.getCurrX();            int y = mScroller.getCurrY();            if (oldX != x || oldY != y) {                scrollTo(x, y);                if (!pageScrolled(x)) {                    mScroller.abortAnimation();                    scrollTo(0, y);                }            }            // Keep on drawing until the animation has finished.            ViewCompat.postInvalidateOnAnimation(this);            return;        }        // Done with scroll, clean up state.        completeScroll(true);    }

小结

ViewPager的拖动和滑动都看完了,onInterceptTouchEvent()和onTouchEvent()的Move中都会对drag进行响应,通过scrollTo方法形成视图的移动,期间通过pageScrolled()完成相关事情的处理,包括Decor View、接口方法回调、动画;onTouchEvent()的Up中可能会产生平滑滑动,利用的是初始化时候定义的Scroller。

到这里对ViewPager的视图的移动流程就有了一个整体上的了解,一些细节我们就可以很快的定位了,接下来一篇想看一下ViewPager和Fragment的交互。

原创粉丝点击