自定义无限循环ViewPager(一)――ViewPager初始化源码解析

无限循环viewpager

大部分app首页一般都会有个无限循环的广告轮播位,通常都是采用ViewPager来实现的,对此大家肯定不会感到陌生。而关于无限循环的ViewPager的实现,一般有下面三种实现方式。

  • 1.将PagerAdaptergetCount()方法中返回的值设为Integer.MAX_VALUE,然后ViewPager调用setCurrentItem设置到中间的位置开始,达到无限循环的目的。
  • 2.第二种是getCount()返回值不为Integer.MAX_VALUE,只需返回数据.size()+2即可。具体实现可以参考Viewpager实现真正的无限滑动,拒绝Integer.MAX_VALUE这篇文章。
  • 3.第三种方法就是自定义View。

本文介绍的就是通过自定义View实现无限循环。不过此方法是在ViewPager源码的基础上进行改造实现的。要知道如何改造ViewPager,就需要了解ViewPager的原理。关于如何自定义无限循环ViewPager,由于篇幅实在太长,准备分成三篇文章进行讲解。

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

前两篇关于ViewPager的源码分析,如果大家觉得比较枯燥,可以直接阅读第三篇文章。接下来将分析下ViewPager最重要的几个方法。首先看下ViewPager初始化几个方法的调用顺序。然后按照调用顺序逐个分析。

initViewPager()
void initViewPager() {
        //为了能够执行重写后的onDraw()方法
        setWillNotDraw(false);
        //只有当其子类控件不需要获取焦点时才获取焦点
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        setFocusable(true);
        final Context context = getContext();
        ////创建Scroller对象
        mScroller = new Scroller(context, sInterpolator);
        final ViewConfiguration configuration = ViewConfiguration.get(context);
         // 屏幕密度
        final float density = context.getResources().getDisplayMetrics().density;

        //系统所能识别的被认为是滑动的最小距离  
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        //最小速度
        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
        //获取允许执行一个fling手势的最大速度值  
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
        //定义左右两边边缘效应类
        mLeftEdge = new EdgeEffectCompat(context);
        mRightEdge = new EdgeEffectCompat(context);
        
        //fling飞速滑动的距离
        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
        //停止滑动的最小距离
        mCloseEnough = (int) (CLOSE_ENOUGH * density);
        //页面边缘大小
        mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);

        ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());

        if (ViewCompat.getImportantForAccessibility(this)
                == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            ViewCompat.setImportantForAccessibility(this,
                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
        }
    }

initViewPager()方法很简单,就是初始化了滑动最小距离、最小速度等值。而关于Scroller的用法大家可以去搜索下具体使用方法。这里就不做介绍了。

Tips:

  • ViewGroup默认情况下,会被设置成WILL_NOT_DRAW,这是从性能考虑,这样一来,onDraw就不会被调用了。如果想要调用重写的onDraw,就要调用setWillNotDraw(false)
  • FOCUS_AFTER_DESCENDANTS:viewgroup只有当其子类控件不需要获取焦点时才获取焦点 FOCUS_BEFORE_DESCENDANTS:viewgroup会优先其子类控件而获取到焦点 FOCUS_BLOCK_DESCENDANTS:viewgroup会覆盖子类控件而直接获得焦点
setAdapter()
public void setAdapter(PagerAdapter adapter) {
         //如果之前设置过adapter,那么就进入if语句进行一些清理工作
        //因为是第一次创建,所以mAdapter =null
        if (mAdapter != null) {
            //如果mAdapter != null,清除上一次adapter的观察者
            mAdapter.unregisterDataSetObserver(mObserver);
            //回调startUpdate函数,告诉PagerAdapter开始更新要显示的页面
            mAdapter.startUpdate(this);
            //将之前缓存的页面,通过回到destroyItem函数,将页面destroy掉
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
             //回调finishUpdate,告诉PagerAdapter结束更新
            mAdapter.finishUpdate(this);
            //清除缓存页面列表
            mItems.clear();
            //将viewpager的非Decor View的子View移除
            removeNonDecorViews();
            //将当前的显示页面重置到第一个 
            mCurItem = 0;
            //滑动重置到(0,0)位置  
            scrollTo(0, 0);
        }

        //保存上一次的PagerAdapter  
        final PagerAdapter oldAdapter = mAdapter;
        //设置为新的adapter
        mAdapter = adapter;
        //设置页面数量为0个
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            //确保观察者不为null,观察者主要用于监视数据源的内容变化
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            //adapter注册观察者
            mAdapter.registerDataSetObserver(mObserver);
            mPopulatePending = false;
           //保存上一次是否是第一次layout
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            //获取页面的数量
            mExpectedAdapterCount = mAdapter.getCount();
            //如果有数据需要恢复
            if (mRestoredCurItem >= 0) {
                //回调restoreState函数
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                //将当前页面选中为恢复前的页面
                setCurrentItemInternal(mRestoredCurItem, false, true);
                //重置恢复的标记
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                //如果不是第一次布局,那么只需要更新页面缓存列表中的数据,确保显示的页面得到创建
               //这是populate()最主要的工作
                populate();
            } else {
                 //重新布局
                requestLayout();
            }
        }
        
         //如果前后两次设置的adapter不一致的话,回调onAdapterChanged函数
        if (mAdapterChangeListener != null && oldAdapter != adapter) {
            mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
        }
    }

因为是初始化就设置了adapter,所以mFirstLayout=true是第一次布局,所以最后调用requestLayout()方法。

onMeasure()
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //设置尺寸信息,默认大小为0  
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));
         //获取viewper的测量宽度
        final int measuredWidth = getMeasuredWidth();
        final int maxGutterSize = measuredWidth / 10;
       //获取mGutterSize的值,即页面边缘大小
        mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

        // 获取子View的可用宽高的大小,即viewpager宽高除去内边距
        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        //遍历viewpager的子view,找出DecorView进行测量
        int size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                /如果该View是DecorView,对Decor进行测量
                if (lp != null && lp.isDecor) {
                    //获取Decor View的在水平方向和竖直方向上的Gravity
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    //默认DedorView模式对应的宽高是wrap_content  
                    int widthMode = MeasureSpec.AT_MOST;
                    int heightMode = MeasureSpec.AT_MOST;
                        
                     //判断DecorView是在垂直方向上还是在水平方向上占用空间 
                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                    boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;

                   //如果是在垂直方向上占用空间,那么水平方向就是match_parent,即EXACTLY  
                  //而垂直方向上具体占用多少空间,即wrap_content ,还得由DecorView自己决定
                  //如果是水平方向上占用空间同理  
                    if (consumeVertical) {
                        widthMode = MeasureSpec.EXACTLY;
                    } else if (consumeHorizontal) {
                        heightMode = MeasureSpec.EXACTLY;
                    }

                     //DecorView宽高大小,初始化为ViewPager子view可用宽高
                    int widthSize = childWidthSize;
                    int heightSize = childHeightSize;

                    //如果DecorView宽度不是wrap_content,那么width的测量模式就是EXACTLY  
                    //如果宽度既不是wrap_content又不是match_parent,那么说明是用户  
                    //在布局文件写的具体的尺寸,直接将widthSize设置为这个具体尺寸  
                    if (lp.width != LayoutParams.WRAP_CONTENT) {
                        widthMode = MeasureSpec.EXACTLY;
                        if (lp.width != LayoutParams.FILL_PARENT) {
                            widthSize = lp.width;
                        }
                    }
                    //同宽度一样
                    if (lp.height != LayoutParams.WRAP_CONTENT) {
                        heightMode = MeasureSpec.EXACTLY;
                        if (lp.height != LayoutParams.FILL_PARENT) {
                            heightSize = lp.height;
                        }
                    }

                    //确定宽高的测量规格MeasureSpec(包含尺寸和模式的整数)  
                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                    //DecorView进行测量
                    child.measure(widthSpec, heightSpec);
                    
                    //如果DecorView占用了ViewPager的垂直方向的空间,那么竖直方向可用空间将减去减去DecorView的高度
                    //水平方向上同理
                    if (consumeVertical) {
                        childHeightSize -= child.getMeasuredHeight();
                    } else if (consumeHorizontal) {
                        childWidthSize -= child.getMeasuredWidth();
                    }
                }
            }
        }
        
        //确定非DecorView宽高的测量规格MeasureSpec(包含尺寸和模式的整数) 
        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

        //通过Adapter中获取childView,确保需要显示的页面得到创建
        mInLayout = true;
        populate();
        mInLayout = false;

        // 测量非DecorView
        size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
                        + ": " + mChildWidthMeasureSpec);

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //只针对非DecorView测量  
                if (lp == null || !lp.isDecor) {
                    //LayoutParams的widthFactor是取值为[0,1]的浮点数,  
                    // 用于表示子view占ViewPager显示区域可用宽度的比例,  
                    // 即(childWidthSize * lp.widthFactor)表示子view的实际宽度  
                    final int widthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                    //非DecorView的子view进行测量  
                    child.measure(widthSpec, mChildHeightMeasureSpec);
                }
            }
        }
    }

onMeasure()方法其实主要就做了下面三件事:

  • 测量DecorView
  • 通过Adapter中获取childView,确保需要显示的页面得到创建,该步骤主要通过populate()实现,下面有具体分析
  • 测量非DecorView的子view

而关于onMeasure()中什么是DecorView的问题,这里作一点简单的介绍。ViewPager内部定义了一个Decor接口,而且该接口中没有定义任何的内容,唯一的作用就是如果自定义控件实现了Decor接口,那么该控件就属于ViewPagerDecorView。关于DecorView具体的使用以及分析与本文没有关系,所以就不作讲解了。大家有兴趣的话可以自行去尝试使用和阅读源码。

populate()
void populate() {
        populate(mCurItem);
    }

在分析populate(int newCurrentItem),先了解下ViewPager的静态内部类ItemInfo,该类用于保存页面的一些信息。

 static class ItemInfo {
        Object object;//childview对象
        int position;//childView在Adapter中的位置,即第几个页面
        boolean scrolling;//是否在滚动
        float widthFactor;//表示加载的页面占ViewPager可用宽度的比例[0~1](默认返回1) ,这个值可以设置一个屏幕显示多少个页面
        float offset;//childview偏移量,
    }

而且在ViewPager内部还维护了一个由ItemInfo对象组成的缓存列表mItems。 列表的长度是由mOffscreenPageLimit(当前页左右两边缓存的页面数量)来决定,这个在后面的代码分析中会看到。 populate(int newCurrentItem)方法代码比较多,而且也比较难理解, 将进行分段讲解。

  • 获取当前需要展示的ItemInfo对象
void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        //newCurrentItem 与mCurItem的值就是adapter中的位置
        //如果前后两个位置不相等
        if (mCurItem != newCurrentItem) {
            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            //获取旧的ItemInfo对象
            oldCurInfo = infoForPosition(mCurItem);
            //更新mCurItem的值,为新的当前页面的position
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            //对子view绘制顺序进行排序,优先绘制DecorView,再按照position从小到大排序  
            sortChildDrawingOrder();
            return;
        }

         //在用户手指抬起切换到新的位置期间应该推迟创建view,直到滚动到最终位置再去创建,以免在这个期间出现差错  
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

 
        //在ViewPager没有attached到window之前,不要populate.  
        // 因为如果我们在恢复重建View之前进行populate的话,可能会与要恢复的内容有冲突  
        if (getWindowToken() == null) {
            return;
        }

        //回调PagerAdapter的startUpdate函数
        mAdapter.startUpdate(this);

        final int pageLimit = mOffscreenPageLimit;
        //预加载页面的起始位置为当前页面减去缓存页面数量 ,>=0
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        //预加载页面的结束位置为当前页面减去缓存页面数量 ,<=mAdapter.getCount()-1;
        final int endPos = Math.min(N-1, mCurItem + pageLimit);

        //判断用户是否增减了数据源的元素,如果增减了且没有调用notifyDataSetChanged,则抛出异常
        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                    " Pager id: " + resName +
                    " Pager class: " + getClass() +
                    " Problematic adapter: " + mAdapter.getClass());
        }

        // 定位当前获焦页面,即当前需要展示的页面
        int curIndex = -1;
        ItemInfo curItem = null;
        //遍历页面缓存列表,根据position找出获焦的页面
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            //因为列表是按照position从大到小排序的
            //所以如果获焦页面position小于缓存的第一个页面的position,那么直接跳出循环, curIndex=0,curItem =null
           //如果获焦页面position大于缓存的最后一个页面的position,最终curIndex=mItems.size(),curItem =null
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

        //curItem == nul,说明mItems列表里面没有保存获焦页面,  
        // 需要将获焦页面加入到mItems里面  
        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }

    .......
    }

需要注意的是,在新建ItemInfo对象时,调用的了addNewItem方法,代码如下所示:

 ItemInfo addNewItem(int position, int index) {
        //新建一个ItemInfo对象
        ItemInfo ii = new ItemInfo();
        //保存位置信息
        ii.position = position;
        //用Adapter创建一个childView
        ii.object = mAdapter.instantiateItem(this, position);
        //默认返回1.0f
        ii.widthFactor = mAdapter.getPageWidth(position);
        //如果curIndex>= mItems.size(),即获焦页面position大于缓存的最后一个页面的position的时候,新建的iteminfo添加到最后
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
                //如果获焦页面position小于缓存的第一个页面的position的时候,curIndex=0,添加到列表的第一位
            mItems.add(index, ii);
        }
        return ii;
    }

不管是从mItems中提取还是新建一个ItemInfo对象,通过上面的代码,已经得到了当前ItemInfo对象curItem。

  • 更新mItems中的其余对象

mItems的长度为 2 * mOffscreenPageLimit+ 1,每次获取到当前curItem后,需要根据mOffscreenPageLimit的值,将当前View前后页面缓存进mItems中去,所以需要将一些ItemInfo添加进来,将另一些ItemInfo移除。保证我们的mItems中的ItemInfo.position在 [ startPos … mCurItem … endPos ] 之间。其中:

mCurItem = curItem.position startPos = mCurItem - pagLimit endPos = mCurItem + pagLimit

void populate(int newCurrentItem) {
        ....

         //当获取了确认了当前页面后,开始根据缓存页面数量,缓存当前页面左右两边的页面
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            //curIndex是curItem在mItems中的索引
            int itemIndex = curIndex - 1;
            //获取左边的ItemInfo对象
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            //可用宽度
            final int clientWidth = getClientWidth();
            //curItem左边需要的宽度,即实际宽度与可用区域宽度比例, 实际宽度=leftWidthNeeded*clientWidth
           //  curItem.widthFactor默认为1.0f,getPaddingLeft()一般为0,所以 leftWidthNeeded一般为1.0f
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;

             //遍历当前页面在adapter中左边的所有页面,如果是在预加载的范围类,那么如果本身就在mItems数组中,则不用移除;
            //如果不在mItems数组中创建并保存该页面,添加到数组中去,最后移除mItems数组中范围外的页面
            //curIndex是当前页面在mItems数组中的位置索引,mCurItem是viewpager中需要显示页面的位置索引,即adapter中的数据的索引
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                     //如果左边的宽度超过了所需的宽度,并且pos比缓存的起始位置还小,说明是在预加载页面的范围外
                    //如果这时ii不为空需要Destroy掉  
                    //为空说明mItems中左边已经没有页面了,跳出循环  
                    if (ii == null) {
                        break;
                    }
                    //如果startPos左边还有对象,需要从mItems中移除
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                         //回调PagerAdapter的destroyItem  
                        mAdapter.destroyItem(this, pos, ii.object);
                         //由于mItems删除了一个元素  
                        //需要将索引减一  
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    //如果当前页面左边缓存对象不为空,即ii!=null,并且该对象的position正好是此次需要缓存的位置
                    //累加curItem左边需要的宽度
                    extraWidthLeft += ii.widthFactor;
                    //将mItems索引减一 ,获取mItems再左边的对象
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    //如果左边对象为空,或者不为空但是不属于预加载的页面范围
                    //新建一个ItemInfo对象,添加到ii的右边
                    ii = addNewItem(pos, itemIndex + 1);
                    //累加curItem左边需要的宽度
                    extraWidthLeft += ii.widthFactor;
                    //由于往mItems中新插入了一个对象,curIndex需要加1
                    curIndex++;
                    //重新获取左边的对象
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
            
            //下面这块代码是更新添加当前页面右边需要预加载的页面
            //extraWidthRight =1.0f
            float extraWidthRight = curItem.widthFactor;
            //当前页面右边页面的索引
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                //在mItems中获取当前页面右边的对象,如果右边索引值大于列表长度,返回null
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                //rightWidthNeeded =2.0f
                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
                        (float) getPaddingRight() / (float) clientWidth + 2.f;
                    //遍历当前页面在adapter中右边的所有页面,如果是在预加载的范围类,那么如果本身就在mItems数组中,则不用移除;
                  //如果不在mItems数组中创建并保存该页面,添加到数组中去,最后移除mItems数组中范围外的页面
                //N=adpater.getcount
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                    //如果右边的宽度超过了所需的宽度,并且pos比需要缓存的终止位置还大,说明是在预加载页面的范围外
                    //如果这时ii不为空需要Destroy掉  
                    //为空说明mItems中右边已经没有页面了,跳出循环  
                        if (ii == null) {
                            break;
                        }
                        //后面的逻辑跟上面添加左边页面的逻辑是类似的,就不再重复了
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }
            // 计算mItems中的偏移参数
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

      .....见下文
    }

根据上文的执行顺序看出,初始化的时候会执行两次populate(int newCurrentItem),至于执行两次的原因下文会有讲到。现在看第一次执行打印的log就可以了。

  1. 初始化默认当前页面为0,缓存数量为1,即左右两边各缓存一个页面,所以starpos=0,endpos=1;
  2. 因为缓存数组为空,所以创建第一个页面对象,添加到数组中去,且在数组中对应的位置索引curIndex=0;
  3. 添加左边页面的时候,由于pos=mCurItem - 1=-1,所以没有进入循环,直接跳过,进入添加右边页面的逻辑;
  4. 添加右边页面的第一次循环, ii=null,直接进入最后的else语句中去,创建新页面对象添加进去;
  5. 第二次循环的时候,extraWidthRight = rightWidthNeeded=2.0,pos=2,endpos=1,进入第一个判断,但是ii=null,所以跳出循环,结束此次的右边页面的更新;
  6. 最后缓存列表mItems中保存了positon=0和position=1两个页面对象。

至于第二次调用populate(int newCurrentItem),当前页面mCurItem=0,然后大家可以按照源码逻辑顺序自行去推敲一遍,这里就不再赘述了。下面再贴下当mCurItem=1和mCurItem=2时,mItems中数据的变化工程的log打印。

mCurItem=1

mCurItem=2

  • 更新页面的偏移参数 在更新完所有的缓存页面后,会调用calculatePageOffsets()方法,对所有的缓存页面对象的偏移量offset值进行更新。
   private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
        final int N = mAdapter.getCount();
        final int width = getClientWidth();
        //mPageMargin是页面之间的间隔,marginOffset间隔比例
        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
        //根据上一次展示的页面,来确认此次当前页面的offset
        if (oldCurInfo != null) {
            final int oldCurPosition = oldCurInfo.position;
            //如果上一次展示页面的位置小于此次当前页面的位置,说明两个页面中间间隔了一些页面
            //下面就是以上一次展示页面的offset为基准,加上中间页面的宽度和marginOffset作为当前页面的offset
            //具体的实现大家自行阅读体会
            if (oldCurPosition < curItem.position) {
                int itemIndex = 0;
                ItemInfo ii = null;
                //oldCurInfo.widthFactor 默认为1.0,
                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                for (int pos = oldCurPosition + 1;
                        pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                    ii = mItems.get(itemIndex);
                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
                        itemIndex++;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos < ii.position) { 
                        offset += mAdapter.getPageWidth(pos) + marginOffset;
                        pos++;
                    }
                    ii.offset = offset;
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
                //这部分是一次展示页面的位置大于此次当前页面的位置,
               //然后以上一次展示页面的offset为基准,减去中间页面的宽度和marginOffset作为当前页面的offset
                //实现逻辑和上面类似
                int itemIndex = mItems.size() - 1;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset;
                for (int pos = oldCurPosition - 1;
                        pos >= curItem.position && itemIndex >= 0; pos--) {
                    ii = mItems.get(itemIndex);
                    while (pos < ii.position && itemIndex > 0) {
                        itemIndex--;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos > ii.position) {
                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
                        pos--;
                    }
                    offset -= ii.widthFactor + marginOffset;
                    ii.offset = offset;
                }
            }
        }

       
        final int itemCount = mItems.size();
        float offset = curItem.offset;
        int pos = curItem.position - 1;
        mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
        mLastOffset = curItem.position == N - 1 ?
                curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
         // 计算缓存列表中当前页面前面页面的偏移量(根据当前页面计算)
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = mItems.get(i);
            while (pos > ii.position) {
                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
            }
            offset -= ii.widthFactor + marginOffset;
            ii.offset = offset;
            if (ii.position == 0) mFirstOffset = offset;
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;
        // 计算缓存列表中当前页面后面页面的偏移量(根据当前页面计算)
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = mItems.get(i);
            while (pos < ii.position) {
                offset += mAdapter.getPageWidth(pos++) + marginOffset;
            }
            if (ii.position == N - 1) {
                mLastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }

        mNeedCalculatePageOffsets = false;
    }

对于calculatePageOffsets()方法,其中的逻辑处理比较难用文字描述,大家自行体会吧。此其实就做了两件事:

  1. 根据oldItem.position与curItem.position的大小关系,来确定curItem的offset值是等于oldItem.offset加上还是减去它们之间间隔的页面(页面宽度+ marginOffset)之和。其中:

mPageMargin是页面之间的间隔, marginOffset = mPageMargin / childWidth 每个页面的offset = mAdapter.getPageWidth(pos) + marginOffset

  1. 得到curItem的offset后,计算出curItem左边页面和右边页面的offset值。
  • 一些收尾工作
void populate(int newCurrentItem) {
        ....

        //回调PagerAdapter的setPrimaryItem,通知当前显示的页面  
        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
        //回调PagerAdapter的finishUpdate,通知页面更新结束  
        mAdapter.finishUpdate(this);

        // 将ItemInfo的内容更新到childView的LayoutParams中
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.childIndex = i;
            if (!lp.isDecor && lp.widthFactor == 0.f) {
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        //重新对页面排序  
        sortChildDrawingOrder();
        //如果ViewPager被设定为可获焦的,则将当前显示的页面设定为获焦 
        if (hasFocus()) {
            View currentFocused = findFocus();
            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if (ii == null || ii.position != mCurItem) {
                for (int i=0; i<getChildCount(); i++) {
                    View child = getChildAt(i);
                    ii = infoForChild(child);
                    if (ii != null && ii.position == mCurItem) {
                        if (child.requestFocus(focusDirection)) {
                            break;
                        }
                    }
                }
            }
        }
    }

到这里populate(int newCurrentItem)就分析完毕了,此方法逻辑有些复杂,也不知道有没有讲述清楚。大家可以反复阅读这段源码,慢慢理解。

onLayout()
protected void onLayout(boolean changed, int l, int t, int r, int b) {
        
        final int count = getChildCount();
        int width = r - l;
        int height = b - t;
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();
        final int scrollX = getScrollX();

        int decorCount = 0;

        //先对DecorView进行layout,再对普通页面进行layout
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //左边和顶部的边距初始化为0  
                int childLeft = 0;
                int childTop = 0;
                if (lp.isDecor) {
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                     //根据水平方向上的Gravity,确定childLeft的值
                    switch (hgrav) {
                        default:
                            childLeft = paddingLeft;
                            break;
                        case Gravity.LEFT:
                            childLeft = paddingLeft;
                            paddingLeft += child.getMeasuredWidth();
                            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;
                    }
                    //与上面水平方向的同理,据水平方向上的Gravity,确定childTop的值
                    switch (vgrav) {
                        default:
                            childTop = paddingTop;
                            break;
                        case Gravity.TOP:
                            childTop = paddingTop;
                            paddingTop += child.getMeasuredHeight();
                            break;
                        case Gravity.CENTER_VERTICAL:
                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,
                                    paddingTop);
                            break;
                        case Gravity.BOTTOM:
                            childTop = height - paddingBottom - child.getMeasuredHeight();
                            paddingBottom += child.getMeasuredHeight();
                            break;
                    }
                    //上面计算的childLeft是相对ViewPager的左边计算的,  
                    //还需要加上x方向已经滑动的距离scrollX  
                    childLeft += scrollX;
                    //对DecorView布局  
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                    decorCount++;
                }
            }
        }

        //普通页面可用宽度
        final int childWidth = width - paddingLeft - paddingRight;
        //下面针对普通页面布局,在此onLayout之前已经得到正确的偏移量了  
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ItemInfo ii;
                 //调用infoForChild(child)通过 view 获取 ItemInfo,得到关于这个子view的position,offset等信息
                if (!lp.isDecor && (ii = infoForChild(child)) != null) {
                   //计算左边偏移量
                    int loff = (int) (childWidth * ii.offset);
                    //将左边距+左边偏移量得到左边最终的位置
                    int childLeft = paddingLeft + loff;
                    int childTop = paddingTop;
                    //如果需要重新测量,则重新测量
                    if (lp.needsMeasure) {
                       //标记已经测量过了 
                        lp.needsMeasure = false;
                        final int widthSpec = MeasureSpec.makeMeasureSpec(
                                (int) (childWidth * lp.widthFactor),
                                MeasureSpec.EXACTLY);
                        final int heightSpec = MeasureSpec.makeMeasureSpec(
                                (int) (height - paddingTop - paddingBottom),
                                MeasureSpec.EXACTLY);
                        child.measure(widthSpec, heightSpec);
                    }
                    //child调用自己的layout方法来布局自己
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                }
            }
        }
        mTopPageBounds = paddingTop;
        mBottomPageBounds = height - paddingBottom;
        mDecorChildCount = decorCount;

        //因为是初始化,所以mFirstLayout为true,调用scrollToItem()滑动到当前的页面位置
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
         //标记已经布局过了,即不再是第一次布局了
        mFirstLayout = false;
    }

关于onLayout()方法中的代码相对来说还是挺简单的,除去对DecorView进行布局外,就是根据offset偏移量来计算出left值,然后直接调用View.layout方法进行布局,最后如果是第一次布局,那么就调用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);
        }
    }

到这里,onLayout()就分析完毕了,最后关于scrollToItem()smoothScrollTo(destX, 0, velocity)completeScroll(false)pageScrolled(destX)方法将在下一篇文章viewpager滑动处理陆续分析。

关于初始化顺序调用的几个主要方法最后就只剩下draw(Canvas canvas)onDraw(Canvas canvas)方法没有分析了。不过关于这两个方法并没有做什么特殊的处理,仅仅只是绘制各个页面之间间隔和viewpager的边缘效应效果,于本次功能实现没有太多的关联。所以,本文就不再贴出关于两个方法的源码了,大家有兴趣,可以自行去阅读。

最后

关于改造ViewPager变为无限循环的第一部分(viewpager部分方法源码解析)到此就分析完毕了,关于viewpager滑动处理以及页面切换的原理将在下篇文章中分析。也不知道关于ViewPager的初始化原理有没有分析清楚,如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linux驱动

24.QTableView函数使用,右击菜单实现

对于QStandardItem的setData()成员 函数的第二个参数role 是模型数据角色 

15040
来自专栏项勇

笔记22 | 学习整理开源APP(BaseAnimation)程序源码“中的通讯录效果(三)

16840
来自专栏HT

基于 HTML5 Canvas 的 3D 模型列表贴图

少量图片对于我们赋值是没有什么难度,但是如果图片的量大的话,我们肯定希望能很直接地显示在界面上供我们使用,再就是排放的位置等等,这些都需要比较直观的操作,在实际...

315100
来自专栏LeoXu的博客

Flex笔记_处理用户输入 原

        Label、RichText、RichEditableText、TextInput、TextArea、RichTextEditor(MX)

8520
来自专栏hightopo

基于HTML5和WebGL的碰撞测试

16320
来自专栏跟着阿笨一起玩NET

winform程序中将控件置于最顶层或最底层的方法

一种方法是在WinForm窗体中使用Controls控件集的SetChildIndex方法,该方法将子控件设定为指定的索引值,其方法原型如下:

54020
来自专栏xx_Cc的学习总结专栏

iOS-UITextField 全面解析iOS中UITextField 使用全面解析UITextField的代理方法通知UITextField 在storyboard 中设置属性

65960
来自专栏mathor

GUI编程

 AWT(Abstract Window Toolkit)包含了很多类和接口,用于Java Application的GUI(Graphics User Inte...

11820
来自专栏Android开发实战

Android自定义View系列 (从小白做起) 一: 初识

很多的Android入门程序猿来说对于自定义View,可能都是比较恐惧的,其实没那么难,写的多了也就熟练了。 高手之路也是从小白做起的。

9220
来自专栏懒人开发

CoordinatorLayout使用(二):Behavior流程 和 事件流

上一篇,我们大体理解了 Behavior简单理解 具体代码可以见 https://github.com/2954722256/use_little_demo ...

21460

扫码关注云+社区

领取腾讯云代金券