前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自定义无限循环ViewPager(二)――ViewPager滑动原理解析

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

作者头像
用户3106371
发布2018-09-12 10:13:52
2.1K0
发布2018-09-12 10:13:52
举报
文章被收录于专栏:lzj_learn_notelzj_learn_note

自定义无限循环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;正在向最终位置移动的状态

代码语言:javascript
复制
 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()
代码语言:javascript
复制
 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()
代码语言:javascript
复制
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()是符合找到当前显示的页面的?

代码语言:javascript
复制
    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当前页面偏移的像素大小。

代码语言:javascript
复制
 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()
代码语言:javascript
复制
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。

代码语言:javascript
复制
 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()方法。

代码语言:javascript
复制
 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()方法,虽然此方法在上篇文章中有作分析,为了能连贯阅读这里在贴下源码。

代码语言:javascript
复制
 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的平滑滑动的过程。

代码语言:javascript
复制
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()方法,然后重写此方法,完成视图的滑动。

代码语言:javascript
复制
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)方法,此方法在很多地方都用调用,我们来看下它究竟做了那些操作。

代码语言:javascript
复制
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方法的改造了,最后一篇文章也会尽快发布出来。如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.08.10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • onInterceptTouchEvent()
  • performDrag()
  • pageScrolled()
  • onPageScrolled()
  • onTouchEvent()
  • determineTargetPage()
  • setCurrentItemInternal()
  • 小结
  • 最后
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档