自定义无限循环ViewPager(二)――ViewPager滑动原理解析

自定义无限循环ViewPager分成以下三篇文章进行讲解:

  1. ViewPager初始化源码解析
  2. ViewPager滑动原理解析
  3. ViewPager方法改造实现无限循环

在前面一篇文章中,已经分析了ViewPager初始化的原理,而本篇文章开始分析ViewPager的滑动及页面切换的原理。在阅读本文之前,大家可以先去了解下Scroller的用法,以便大家更好的理解ViewPager的滑动原理。

关于ViewGroup的事件处理不外乎与dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()这三个方法有关,ViewPager重写了后面两个方法。而ViewPager根据手势产生页面移动也正是因为重写了这两个方法。ViewPager存在两种移动方式:

  1. 在MOVE触摸事件中,页面随手指的拖动而移动。
  2. 在UP事件后,页面滑动到指定页面(通过Scroller实现的)。

现在,我们先来看下onInterceptTouchEvent()方法。

onInterceptTouchEvent()

onInterceptTouchEvent()方法只是判断是否应该拦截这个触摸事件,如果返回true,则将事件交给onTouchEvent()进行滚动处理。在分析onInterceptTouchEvent()前,先介绍下页面的三个状态:

public static final int SCROLL_STATE_IDLE = 0;空闲状态 public static final int SCROLL_STATE_DRAGGING = 1;正在被拖拽的状态 public static final int SCROLL_STATE_SETTLING = 2;正在向最终位置移动的状态

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        //触摸动作
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

        // 如果事件取消或者触摸事件结束,返回false,不用拦截事件
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            //重置触摸相关变量
            resetTouch();
            return false;
        }

        // 如果当前不是按下事件,就判断一下,是否是在拖拽切换页面
        if (action != MotionEvent.ACTION_DOWN) {
            //如果正在被拖拽,拦截
            if (mIsBeingDragged) {
                return true;
            }
            //不允许拖拽,不拦截
            if (mIsUnableToDrag) {
                return false;
            }
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                // 记录按下触摸的位置。
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
               //获取第一个触摸点的id
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                //重置允许拖拽切换页面  
                mIsUnableToDrag = false;
                
                //计算滑动的偏移量 ,Scroller在初始化initViewPager()中创建
                mScroller.computeScrollOffset();
                //如果页面此时正在向最终位置移动并且离最终位置还有一定距离时
                if (mScrollState == SCROLL_STATE_SETTLING &&
                        Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                    //让用户感觉抓住了这个页面
                    //所以停止移动
                    mScroller.abortAnimation();
                    mPopulatePending = false;
                    //更新缓存page信息
                    populate();
                    //设置为正在拖拽
                    mIsBeingDragged = true;
                     //ViewPager向父View申请不要拦截自己的触摸事件
                    requestParentDisallowInterceptTouchEvent(true);
                    //设为拖拽状态
                    setScrollState(SCROLL_STATE_DRAGGING);
                } else {
                    //如果离最终的距离足够小,结束滚动
                    completeScroll(false);
                    mIsBeingDragged = false;
                }

                break;
            }

            case MotionEvent.ACTION_MOVE: {
                 //mIsBeingDragged == false, 否则事件已经在上面就被拦截
                //第一个触摸点的id,为了处理多点触摸 
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // 如果不是有效的触摸id,直接break,不做任何处理
                    break;
                }
                //根据第一个触摸点的id获取触摸点的序号
                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                //根据这个序号,获取这个触摸点的横坐标
                final float x = MotionEventCompat.getX(ev, pointerIndex);
                //得到水平方向移动距离
                final float dx = x - mLastMotionX;
               //水平方向移动距离绝对值
                final float xDiff = Math.abs(dx);
                 //根据这个序号,获取这个触摸点的纵坐标
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                //垂直方向移动距离的绝对值
                final float yDiff = Math.abs(y - mInitialMotionY);
               
                //判断当前显示的子view是否可以滑动,如果可以滑动,交给子view处理,不拦截
                //isGutterDrag是判断是否在两个子view之间的缝隙间滑动
                //canScroll是判断子view是否可以滑动
                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    mLastMotionX = x;
                    mLastMotionY = y;
                     //标记ViewPager不去拦截事件
                    mIsUnableToDrag = true;
                    return false;
                }

                 //如果水平方向移动绝对值大于最小距离, 且 yDiff/xDiff < 0.5f,表示在水平方向移动
                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                   //说明正在拖拽
                    mIsBeingDragged = true;
                    //ViewPager向父View申请不要拦截自己的触摸事件
                    requestParentDisallowInterceptTouchEvent(true);
                     //设置为拖拽的状态
                    setScrollState(SCROLL_STATE_DRAGGING);
                    //保存当前位置
                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                            mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    //启用缓存  
                    setScrollingCacheEnabled(true);
                } else if (yDiff > mTouchSlop) {
                     // 如果是垂直方向上的移动则不拦截
                    mIsUnableToDrag = true;
                }
                if (mIsBeingDragged) {
                    //如果在拖拽,让子view跟随手指进行移动
                    //performDrag(x)方法很重要,下面会有详细介绍
                    if (performDrag(x)) {
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }
                break;
            }

            case MotionEventCompat.ACTION_POINTER_UP:
                //此方法用于处理多点触摸,如果抬起的是第一个触摸点,则将mActivePointerId设为第二个触摸点
                onSecondaryPointerUp(ev);
                break;
        }

        //添加速度追踪类
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

       
        return mIsBeingDragged;
    }

onInterceptTouchEvent()主要作用就是判断各种情况是不是在拖拽,是否要拦截此事件。在MOVE事件中,如果在拖拽,会调用performDrag()方法让当前页面移动。下面便分析此此方法。

performDrag()
 private boolean performDrag(float x) {
        boolean needsInvalidate = false;
        
        //两次MOVE的移动距离
        final float deltaX = mLastMotionX - x;
        mLastMotionX = x;
        //viewpager滚动的距离
        float oldScrollX = getScrollX();
        //viewpager需要滚动的距离
        float scrollX = oldScrollX + deltaX;
        final int width = getClientWidth();
         
         //子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);
        
        //如果第一个缓存页面不是adapter中第一个页面,更新子view的左边界
        if (firstItem.position != 0) {
            leftAbsolute = false;
            leftBound = firstItem.offset * width;
        }
        //如果最后一个缓存页面不是adapter中最后一个页面,更新子view的右边界
        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;
        }
         
        mLastMotionX += scrollX - (int) scrollX;
        //滚动到相应的位置
        scrollTo((int) scrollX, getScrollY());
        pageScrolled((int) scrollX);

        return needsInvalidate;
    }

performDrag()方法做了这么几件事:首先得到viewpager需要滚动的距离,其次得到边界条件leftBoundrightBound,根据边界条件的约束得到真正的滚动距离,最后调用scrollTo()方法滚动到最终的位置。简单来说,performDrag()方法让ViewPager的视图滑动了。紧接着,再看看pageScrolled()方法到底做了那些操作。

pageScrolled()
private boolean pageScrolled(int xpos) {
        //如果没有任何的页面缓存信息
        if (mItems.size() == 0) {
             //mCalledSuper作用是:如果子类重写了onPageScrolled,
             // 那么子类的实现必须要先调用父类ViewPager的onPageScrolled
            //为了确保子类的实现中先调用了父类ViewPager的onPageScrolled,定义了mCalledSuper
            //并且在ViewPager类中的onPageScrolled将mCalledSuper设置为了true,用于判断子类有没有调用。
            mCalledSuper = false;
            onPageScrolled(0, 0, 0);
            //如果没有执行ViewPager的onPageScrolled,抛出异常
            if (!mCalledSuper) {
                throw new IllegalStateException(
                        "onPageScrolled did not call superclass implementation");
            }
            return false;
        }
        //根据当前滑动的位置,得到当前显示的子view的页面信息iteminfo
        final ItemInfo ii = infoForCurrentScrollPosition();
        final int width = getClientWidth();
        final int widthWithMargin = width + mPageMargin;
        final float marginOffset = (float) mPageMargin / width;
        //当前页面的position,在adapter数据中的位置
        final int currentPage = ii.position;
        //得到当前页面的偏移量
        final float pageOffset = (((float) xpos / width) - ii.offset) /
                (ii.widthFactor + marginOffset);
        //当前页面偏移的像素值
        final int offsetPixels = (int) (pageOffset * widthWithMargin);

        //以下几句代码跟上面的作用一样,都是如果子类重写了onPageScrolled,必须要先调用ViewPager的onPageScrolled
        mCalledSuper = false;
        onPageScrolled(currentPage, pageOffset, offsetPixels);
        if (!mCalledSuper) {
            throw new IllegalStateException(
                    "onPageScrolled did not call superclass implementation");
        }
        return true;
    }

pageScrolled(int xpos)简单来说就是根据当前的滑动位置,找到当前的页面信息,然后得到viewpager滑动距离,最后调用了onPageScrolled(currentPage, pageOffset, offsetPixels),此方法下面会有分析。值得注意的是,infoForCurrentScrollPosition()是符合找到当前显示的页面的?

    private ItemInfo infoForCurrentScrollPosition() {
        final int width = getClientWidth();
        //viewpager滑动距离比例
        final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0;
        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
        int lastPos = -1;
        float lastOffset = 0.f;
        float lastWidth = 0.f;
        boolean first = true;

        ItemInfo lastItem = null;
        // 遍历所有预存的页面
        for (int i = 0; i < mItems.size(); i++) {
            ItemInfo ii = mItems.get(i);
            float offset;
             //第一次first=true,不进入;第二次判断mitems中缓存的页面是不是有丢失
            if (!first && ii.position != lastPos + 1) {
                // Create a synthetic item for a missing page.
                ii = mTempItem;
                ii.offset = lastOffset + lastWidth + marginOffset;
                ii.position = lastPos + 1;
                ii.widthFactor = mAdapter.getPageWidth(ii.position);
                i--;
            }
            offset = ii.offset;
            //根据页面获取左右边界,然后通过与滑动比例的比较,找到当前显示的页面
            final float leftBound = offset;
            final float rightBound = offset + ii.widthFactor + marginOffset;
            if (first || scrollOffset >= leftBound) {
                if (scrollOffset < rightBound || i == mItems.size() - 1) {
                    return ii;
                }
            } else {
                return lastItem;
            }
            first = false;
            //存储检查过的页面信息
            lastPos = ii.position;
            lastOffset = offset;
            lastWidth = ii.widthFactor;
            lastItem = ii;
        }

        return lastItem;
    }

通过上面源码的分析,首先获得viewpager滑动过的距离比例,然后通过遍历mItems缓存列表,根据每个缓存页面的offset值得到改页面的左右边界,最后就是判断viewpager滑动过的距离比例在哪一个缓存页面的边界之内,这个缓存页面就是当前显示的页面。而如果viewpager显示区域内存在两个页面显示的时候,从缓存列表的遍历顺序就可以看出,返回的必然是最左边的页面。

onPageScrolled()

从上面的代码分析可以看出,pageScrolled()方法只是为了调用onPageScrolled()做传参的计算。其中,

position表示当前显示页面的位置 offset当前页面位置的偏移 offsetPixels当前页面偏移的像素大小。

 protected void onPageScrolled(int position, float offset, int offsetPixels) {
       //如果有DecorView,则需要使得它们一直显示在屏幕中,不移出屏幕
        if (mDecorChildCount > 0) {
            //根据Gravity将DecorView摆放到指定位置。可参考onMeasure的分析
            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);
                }
            }
        }

        //分发页面滚动事件,即回调监听的onPageScrolled方法
        dispatchOnPageScrolled(position, offset, offsetPixels);

        //如果mPageTransformer不为null,则不断去调用mPageTransformer的transformPage函数  
        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;
                //子view的位置
                final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
                //回到transformPage方法
                mPageTransformer.transformPage(child, transformPos);
            }
        }

        mCalledSuper = true;
    }

上面代码中回调transformPage()中的transformPos的取值范围如下:

(-∞ , -1) :表示左边 的View 且已经看不到了 [-1 , 0] :表示左边的 View ,且可以看见 ( 0 , 1] :表示右边的VIew , 且可以看见了 ( 1 , -∞) : 表示右边的 View 且已经看不见了

举个栗子:

如果a 是第一页,b 是第二页 当前页为 a, 当 a 向左滑动, 直到滑到 b 时: a 的position变化是 [-1 , 0] 由 0 慢慢变到 -1 b 的position变化是 ( 0 , 1] 由 1 慢慢变到 0 当前页为b, 当 b 向右滑动, 直到滑到a 时: a 的position变化是 [-1 , 0] 由 -1 慢慢变到 0 b 的position变化是 ( 0 , 1] 由 0 慢慢变到 1

onPageScrolled()方法就分析到这里,它其实就做了三件事:

  1. 将DecorView显示在屏幕中,不移除屏幕
  2. 回调接口的onPageScrolled()方法
  3. 回调接口的transformPage()方法,自定义实现页面转换动画

基本上到这里,onInterceptTouchEvent()流程中涉及的方法就分析完毕了。简单总结下,就是在onInterceptTouchEvent()方法中根据不同情况对mIsBeingDragged进行赋值,对触摸事件是否进行拦截;如果在MOVE事件中是可滑动的,就调用performDrag()让视图跟着滑动,当然此方法中是调用scrollTo()方法形成拖拽效果,接着调用pageScrolled()方法,获取得当前页面的信息和偏移量传入onPageScrolled()方法,再在onPageScrolled()中对DecorView固定显示,回调接口,回调转换动画接口。

虽然,onInterceptTouchEvent()中产生了拖动效果,但主要还是对是否拦截事件作出判断,关于页面的滑动还是在onTouchEvent()中进行处理。

onTouchEvent()
public boolean onTouchEvent(MotionEvent ev) {
        if (mFakeDragging) {
            //使用程序模拟拖拽事件,忽略真正的触摸事件
            return true;
        }

        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
           // 不立即处理边缘触摸事件
            return false;
        }

        if (mAdapter == null || mAdapter.getCount() == 0) {
            //mAdapter中数据为空,不处理事件
            return false;
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        //添加速度追踪
        mVelocityTracker.addMovement(ev);

       //获取触摸事件
        final int action = ev.getAction();
        boolean needsInvalidate = false;

        switch (action & MotionEventCompat.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                //如果按钮,立即停止滚动
                mScroller.abortAnimation();
                mPopulatePending = false;
                //根据mCurIndex更新需要缓存的页面信息
                populate();

                // 保存起始触摸点
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                //如果不在drag(这里有可能是因为没有消耗手势的子View,返回来让ViewPager处理)
                if (!mIsBeingDragged) {
                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    if (pointerIndex == -1) {
                        // A child has consumed some touch events and put us into an inconsistent state.
                        needsInvalidate = resetTouch();
                        break;
                    }
                    
                    //计算x和y方向的移动
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float xDiff = Math.abs(x - mLastMotionX);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    final float yDiff = Math.abs(y - mLastMotionY);
                    // 如果x方向移动足够大,且大于y方向的移动,则开始拖拽
                    if (xDiff > mTouchSlop && xDiff > yDiff) {
                        mIsBeingDragged = true;
                        //ViewPager向父View申请不要拦截自己的触摸事件
                        requestParentDisallowInterceptTouchEvent(true);
                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                                mInitialMotionX - mTouchSlop;
                        mLastMotionY = y;
                        //设置为拖拽的状态
                        setScrollState(SCROLL_STATE_DRAGGING);
                        //启用缓存  
                        setScrollingCacheEnabled(true);

                        // 感觉和 requestParentDisallowInterceptTouchEvent(true)重复了
                         //就是requestParentDisallowInterceptTouchEvent方法的具体实现
                        ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                // 在拖拽的状态
                if (mIsBeingDragged) {
                    // 调用performDrag(),实现页面的滑动,上面已经分析过了
                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
                            ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    needsInvalidate |= performDrag(x);
                }
                break;
            case MotionEvent.ACTION_UP:
                 //如果是在拖拽状态抬起手指
                if (mIsBeingDragged) {
                     //计算x方向的滑动速度
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                            velocityTracker, mActivePointerId);
                    mPopulatePending = true;
                    final int width = getClientWidth();
                    //获取viewpager横向滑动距离
                    final int scrollX = getScrollX();
                    //根据滑动位置得到当前显示的页面信息
                    final ItemInfo ii = infoForCurrentScrollPosition();
                    final int currentPage = ii.position;
                    //计算当前页面偏移
                    final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
                    final int activePointerIndex =
                            MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    //获取手指滑动的距离
                    final int totalDelta = (int) (x - mInitialMotionX);
                     // 通过手指滑动距离和速度计算会滑动到哪个页面
                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                            totalDelta);
                    // 滑动到 nextPage 页
                    setCurrentItemInternal(nextPage, true, true, initialVelocity);

                    needsInvalidate = resetTouch();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged) {
                    //滑动到当前的页面
                    scrollToItem(mCurItem, true, 0, false);
                    needsInvalidate = resetTouch();
                }
                break;
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index = MotionEventCompat.getActionIndex(ev);
                final float x = MotionEventCompat.getX(ev, index);
                mLastMotionX = x;
                //多点触摸,换了另外一个手指过后更新mLastMotionX和mActivePointerId
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                break;
            }
            case MotionEventCompat.ACTION_POINTER_UP:
                //多点触摸下一个手指抬起了,要更新mLastMotionX
                onSecondaryPointerUp(ev);
                mLastMotionX = MotionEventCompat.getX(ev,
                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));
                break;
        }
        if (needsInvalidate) {
            //如果需要重绘,重绘viewpager
            ViewCompat.postInvalidateOnAnimation(this);
        }
        return true;
    }

纵观整个方法,MOVE中调用performDrag()实现拖动,而UP的时候则根据计算出下一个应该显示的页面nextPage,接着调用setCurrentItemInternal()产生滑动。

关于onTouchEvent()方法的代码与onInterceptTouchEvent()有很多的相似之处,如果对onInterceptTouchEvent()有所理解的话,相信对onTouchEvent()的理解也会比较简单的。不过,在onTouchEvent()方法中关于抬起事件和事件取消中,调用了determineTargetPage()setCurrentItemInternal()scrollToItem()这三个方法。至于scrollToItem()方法,在上篇文章ViewPager初始化源码解析已经有过分析,其作用就是滑动mCurItem的目标页面。至于前两个方法,下面会一一进行讲解。

determineTargetPage()

determineTargetPage()方法通过滑动速度,滑动距离以及当前页面位置偏移计算出下一个页面的position。

 private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
        int targetPage;
        //如果滑动的距离大于最小的飞速滚动距离,且滑动速度大于最小的飞速滑动速度
        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
            //如果速度大于0,下个页面position则为当前显示页面的位置+1
            targetPage = velocity > 0 ? currentPage : currentPage + 1;
        } else {
            //如果滑动距离和滑动速度均不满足最小要求,则通过当前显示页面的偏移得到下个页面
            final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
            targetPage = (int) (currentPage + pageOffset + truncator);
        }

        if (mItems.size() > 0) {
            final ItemInfo firstItem = mItems.get(0);
            final ItemInfo lastItem = mItems.get(mItems.size() - 1);

            // 最后进行边界判断取值,下个页面的position在缓存页面的position之间
            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
        }

        return targetPage;
    }
setCurrentItemInternal()

viewpager可以调用setCurrentItem(int item)选中需要显示的页面,此方法最后也是调用的是setCurrentItemInternal()方法。

 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        if (mAdapter == null || mAdapter.getCount() <= 0) {
            setScrollingCacheEnabled(false);
            return;
        }
        if (!always && mCurItem == item && mItems.size() != 0) {
            setScrollingCacheEnabled(false);
            return;
        }

        if (item < 0) {
            item = 0;
        } else if (item >= mAdapter.getCount()) {
            item = mAdapter.getCount() - 1;
        }

        //以上代码都是一些代码健壮性检查,如果不满足条件,直接返回
        //缓存页面的数量
        final int pageLimit = mOffscreenPageLimit;
        //如果需要显示的页面超过了需要缓存的页面,将所有缓存页面的滚动状态设为true
        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
            for (int i=0; i<mItems.size(); i++) {
                mItems.get(i).scrolling = true;
            }
        }
        final boolean dispatchSelected = mCurItem != item;
        //如果是
        if (mFirstLayout) {
            //保存当前页面
            mCurItem = item;
            //要跳转的页面是不是当前页面,则回调onPageSelected方法
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            //请求绘制
            requestLayout();
        } else {
            //更新页面信息,并且滑动到目标页面
            populate(item);
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }

如果不是第一次布局,mFirstLayout=false,则会执行scrollToItem()方法,虽然此方法在上篇文章中有作分析,为了能连贯阅读这里在贴下源码。

 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();
             // 获取 item 的水平 方向的offset偏移值
            destX = (int) (width * Math.max(mFirstOffset,
                    Math.min(curInfo.offset, mLastOffset)));
        }
        if (smoothScroll) {
            // 平滑滚动到偏移位置
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
        } else {
            //是否需要分发OnPageSelected回调
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            //滑动结束后的清理复位
            completeScroll(false);
            //滚动到偏移的位置,结束滑动
            scrollTo(destX, 0);
            //最后会调用onPageScrolled(currentPage, pageOffset, offsetPixels)方法
            pageScrolled(destX);
        }
    }

滑动到目标页面存在两种方式,一种是平滑滑动到目标页面,一种是直接滑动动目标位置。如果是onTouchEvent()的Up事件滑动到目标页面则是第一种,而初始化完成之后通过调用setCurrentItem(int item)滑动到目标页面则是第二种。我们先看下,smoothScroll=true的平滑滑动的过程。

void smoothScrollTo(int x, int y, int velocity) {
        //没有子view直接返回
        if (getChildCount() == 0) {
            setScrollingCacheEnabled(false);
            return;
        }
        //获取viewpager滚动的距离
        int sx = getScrollX();
        int sy = getScrollY();
        //需要滚动的距离
        int dx = x - sx;
        int dy = y - sy;
        //如果需要滚动的距离为0,结束滚动,更新页面信息,设置空闲的滚动状态
        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);
        //smoothScrollTo并没有使用匀速滑动,而是通过distanceInfluenceForSnapDuration函数来实现变速,
        final float distance = halfWidth + halfWidth *
                distanceInfluenceForSnapDuration(distanceRatio);

        int duration = 0;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
             //如果手指滑动速度不为0,根据手指滑动速度计算滑动持续时间
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
              //如果手指滑动速度为0,即通过代码的方式滑动到指定位置,则使用下面的方式计算滑动持续时间
            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);
        //用scroller类开始平滑滑动
        mScroller.startScroll(sx, sy, dx, dy, duration);
        //重绘
        ViewCompat.postInvalidateOnAnimation(this);
    }

在上面的代码里mScroller.startScroll()开启了平滑滑动后,会不断的调用computeScroll()方法,然后重写此方法,完成视图的滑动。

public void computeScroll() {
        //确保mScroller还没有结束计算滑动位置
        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
              //获取当前所处的位置oldX,oldY
            int oldX = getScrollX();
            int oldY = getScrollY();
            //获取mScroller计算出来的位置
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            
              //只要x和y方向有一个发生了变化,就去滑动
            if (oldX != x || oldY != y) {
                //滑到mScroller计算出来的新位置
                scrollTo(x, y);
                //调用pageScrolled,此方法上面有分析,只有当ViewPager里面没有子View才会返回false
                if (!pageScrolled(x)) {
                     //结束动画,并使得当前位置处于最终的位置
                    mScroller.abortAnimation();
                     //没有子View,说明x方向无需滑动,再次确保y方向滑动
                    scrollTo(0, y);
                }
            }

            //不断的postInvalidate,不断重绘,达到动画效果
            ViewCompat.postInvalidateOnAnimation(this);
            return;
        }

        //如果滑动结束了,做一些结束后的清理相关操作
        completeScroll(true);
    }

Viewpager利用Scroller产生平滑滑动,其关键点在于启动滑动后,会不断回调computeScroll(),ViewPager重写了这个方法,然后调用scrollTo()滑动之后还调用了pageScrolled(x)对DecorView进行位置更新、回调接口、产生动画,最后申请重绘。 在computeScroll()方法的最后,如果滑动结束了,调用了completeScroll(true)方法,此方法在很多地方都用调用,我们来看下它究竟做了那些操作。

private void completeScroll(boolean postEvents) {
        //如果当前滑动状态是SCROLL_STATE_SETTLING
        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
        if (needPopulate) {
            setScrollingCacheEnabled(false);
            //停止滑动
            mScroller.abortAnimation();
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
             //如果还没滑动到目标位置,调用 scrollTo()确保滑动最终的位置
            if (oldX != x || oldY != y) {
                scrollTo(x, y);
                if (x != oldX) {
                    pageScrolled(x);
                }
            }
        }
        mPopulatePending = false;
        //将缓存页面的滚动状态设为false
        for (int i=0; i<mItems.size(); i++) {
            ItemInfo ii = mItems.get(i);
            if (ii.scrolling) {
                needPopulate = true;
                ii.scrolling = false;
            }
        }

        if (needPopulate) {
            //将状态设为空闲状态,并更新页面信息
            if (postEvents) {
                ViewCompat.postOnAnimation(this, mEndScrollRunnable);
            } else {
                mEndScrollRunnable.run();
            }
        }
    }

 private final Runnable mEndScrollRunnable = new Runnable() {
        public void run() {
            setScrollState(SCROLL_STATE_IDLE);
            populate();
        }
    };
小结

关于ViewPager的滑动以及页面切换的原理分析就到此结束了,关于ViewPager的两种移动方式所涉及到的相关方法也都有分析到,

  • 其中在onInterceptTouchEvent()onTouchEvent()的MOVE事件中,调用performDrag()对拖拽进行处理,通过scrollTo()方法完成页面的移动,期间通过pageScrolled()完成相关事情的处理,如DecorView显示、接口方法回调、动画接口回调等;
  • 而另外一种移动方式在onTouchEvent()的UP事件中,调用setCurrentItemInternal()对平滑滑动进行处理,通过最后调用smoothScrollTo()方法,利用Scroller达到目的,当然最后也调用了pageScrolled()进行接口的回调等操作,在滑动结束的最后,调用completeScroll(boolean postEvents)完成滑动结束后的相关清理工作。
最后

关于改造ViewPager变为无限循环的第二部分(ViewPager滑动原理解析)所有内容都已分析完毕了,只剩下最后一部分ViewPager方法的改造了,最后一篇文章也会尽快发布出来。如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏飞雪无情的博客

Android中为图标加上数字--用于未读短信数提醒,待更新应用数提醒等

在我们开发一些如短消息、应用商店等应用时,会考虑在短消息的图标上加上未读短信的数量,在应用商店上加上可以升级的应用数量,这样不占太大空间还能达到提示的目的。

1034
来自专栏Android开发小工

完全自定义样式的一句话实现RecyclerView的单选多选

今天的主题是封装RecyclerView的单选多选,现在大家应该都是用的RecyclerView开发列表数据吧。

1825
来自专栏james大数据架构

SwitchButton 开关按钮 的多种实现方式

刚开始接触开关样式的按钮是在IOS系统上面,它的切换以及滑动十分帅气,深入人心。 所谓的开关按钮,就是只有2个状态:on和off,下图就是系统IOS 7上开关按...

2857
来自专栏Android Note

Android上的自定义字体 - 通过XML进行动态字体选择

1286
来自专栏向治洪

Android系统服务之WindowManager整理

概述 WindowManager是Android中一个重要的服务(Service )。WindowManager Service 是全局的,是唯一的。它将用户的...

21210
来自专栏非著名程序员

Android学习第六弹之Touch事件的处理

在移动开发过程当中,我们经常会遇到手势处理和事件触摸的情况,如果不了解整个事件的处理机制,对于开发的同学和码农是非常痛苦的,但是事件触摸的处理确实是一个非常复杂...

1855
来自专栏Android机器圈

Achartengine.jar绘制动态图形一 --饼图

PS:我们在做安卓程序的时候,免不了会做一些图形,自己可以选择自定义view ,就是用Canvas画,也可以用写好的jar包,就是achartengine.ja...

4688
来自专栏Android机动车

Material Design整理(六)——SearchView及FlexboxLayout

1221
来自专栏Android相关

LinearLayout.onMeasure--事例说明

将LinearLayout中代码Copy了一份存在本地,然后再在里面加了几个子View,打印出来LinearLayout.onMeasure中的那些变量的值

1082
来自专栏肖蕾的博客

第三章:动画(Animation)动画是什么?原理?Animation图片裁剪方法StateTime

1.定义:管理游戏中动画实现的类,实现只需要两个参数,时间:duration,另外则是TextureRegion 即图片。 2.用途:管理动画,设置随即播放模...

954

扫码关注云+社区

领取腾讯云代金券