美文网首页Android 技术开发程序员Android开发
自定义无限循环ViewPager(二)――ViewPager滑动

自定义无限循环ViewPager(二)――ViewPager滑动

作者: time_fly | 来源:发表于2017-08-10 16:06 被阅读255次

    自定义无限循环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方法的改造了,最后一篇文章也会尽快发布出来。如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!

    相关文章

      网友评论

        本文标题:自定义无限循环ViewPager(二)――ViewPager滑动

        本文链接:https://www.haomeiwen.com/subject/foodlxtx.html