ViewPager(八)一篇彻底读懂ViewPager源码(完结)

ViewPager系列文章,从入门,到习得,再到精通应用技巧,本篇迎来了大结局——ViewPager源码剖析。

传送门:
ViewPager系列一:初相识
ViewPager系列二: Adapter的爱恨情仇
ViewPager系列三:两个熊孩子天生不一样
ViewPager系列四:让ViewPager用起来更顺滑——换页监听及换页方法
ViewPager系列五:让ViewPager用起来更顺滑——懒加载及预加载定制
ViewPager系列六:让ViewPager用起来更顺滑——设置间距与添加转场动画
ViewPager系列七:让ViewPager用起来更顺滑——轮播、禁止滑动与指示器的配合
ViewPager系列八:一篇彻底读懂ViewPager源码(完结)


温馨提示:本篇博客内容较多,包括源码在内四万多字,文章分三个部分,读者可以每次看一个部分,或者在工作中遇到源码不懂的地方,想要在博客中找到答案,可以选择全局搜索关键字,给你最快速的解答

像其他源码类文章一样,分析源码是为了让我们更深入了解工作机制,以及解决我们开发过程中可能遇到的各种奇葩问题。此外,理解源码才能实现更高级的定制,甚至学习一些安卓UI应用技巧(限控件源码)。

本篇源码探究有别于其他源码类文章,主要分以下三个部分来分析:

1、ViewPager初始化
2、ViewPager显示item
3、ViewPager滑动控制

为什么说有别于其他文章呢?因为通常源码都是从我们应用的代码入口进行分析的(以ViewPager为例:是从ViewPager.setAdapter()这个方法开始的),而对其他部分抱着“不求甚解”的态度。这实际上马克思主义理论中矛盾论之抓住主要矛盾理论的运用,所以这也是无可厚非的。我这里硬是要重新布局文章结构,加了其他内容,这难免有“画蛇添足”之嫌,但是真的是这样的吗?


下面我们正式进入源码分析,开车了!

我们都知道ViewPager是一个出色的容器,也就是ViewGroup的孩子。ViewGroup虽然是View的子类,但是他拓展了View做不到的功能。View不能包含其他View,只用负责自己的一亩三分地就行,位置定下来的话,也不会在绘制的时候受到其他控件的影响。而ViewGroup则可以包含View,也可以包含ViewGroup,所以他需要给他的孩子划出一亩三分地(在ViewGroup给出准确的宽高值时),虽然责任多了,但是真正绘制他只需要绘制自己的一些装饰,而子View的绘制,他将委托给View自身绘制。(关于View的绘制可以看相关博客,或者下文也还有提及)。

1、 ViewPager初始化

当我们在Activity的onCreate的setContentView方法将Xml布局设置给视图的时候,此时,ViewPager会被初始化,会按照如下方法进行初始化:

ViewPager构造方法 -> initViewPager -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> draw -> onDraw

*注:以上方法有的不止调用一次,调用整体顺序按照上述步骤。想知道为什么的可以移步“最详细的安卓绘制分析”。 *

ViewPager在构造方法里调用了initViewPager这个初始化方法

void initViewPager() {
        setWillNotDraw(false);//重写onDraw需要调用,ViewGroup默认true
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);//孩子没有的时候才有
        setFocusable(true);
        final Context context = getContext();
        mScroller = new Scroller(context, sInterpolator);
        final ViewConfiguration configuration = ViewConfiguration.get(context);
        final float density = context.getResources().getDisplayMetrics().density;

        mTouchSlop = configuration.getScaledPagingTouchSlop();//滑动阈值
        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
        //左右边界效果
        mLeftEdge = new EdgeEffect(context);
        mRightEdge = new EdgeEffect(context);
        
		//其他省略。。。。。。
    }

在初始化方法中,首先调用了 setWillNotDraw()这个方法,并且将其设置false,这个默认是true,意味着,ViewPager这个ViewGroup会执行draw()的绘制方法。
然后是setDescendantFocusability(FOCUS_AFTER_DESCENDANTS),这个方法对获取焦点规则作了明确,就是在孩子都没有获取焦点的时候才获取(after descendants);
还配置了一些初始化的变量,涉及滑动的阈值,速度的范围,边界效果(EdgeEffect),以及边界的处理(ViewCompat.setOnApplyWindowInsetsListener),以及辅助功能分发配置(ViewCompat.setAccessibilityDelegate),这些在View详解系列文章会提到,这里不是重点,暂时不提 。

初始化完成后还没有粘贴到真正的窗口Window上,所以之后还会调用回调 onAttachedToWindow方法。这个方法很简单,只是重置了一个变量mFirstLayout

@Override
    protected void onAttachedToWindow() {
        Log.e(TAG,"onAttachedToWindow start");
        super.onAttachedToWindow();
        mFirstLayout = true;
        Log.e(TAG,"onAttachedToWindow end");
    }

这个变量是一个标记位,对是否第一次执行布局操作layout进行标记,这个时候没执行,所以重置为true。这个变量,读者先记着,在布局方法,滚动判断和设置适配器后会用到。

然后老生常谈的View绘制三部曲onMesure,onLayout,onDraw,当然由于上边打开了允许draw调用,所以在onDraw前还会调用draw方法。

我们依次来看(记住,这个时候,我们还没有给ViewPager添加子View,所以只是在测量绘制自身),首先是onMeasure()方法

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //存储宽高测量值
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));

    	//获取子View的宽高运算省略
        /*
         * Make sure all children have been properly measured. Decor views first.
         * Right now we cheat and make this less complicated by assuming decor
         * views won't intersect. We will pin to edges based on gravity.
         */
        int size = getChildCount();
        for (int i = 0; i < size; ++i) {
          //由于此时没有子View,所以这个对子View的测量放在下面分析,这里省略
        }

       //组装子View的宽高省略,下边分析
       
        // Make sure we have created all fragments that we need to have shown.
        //这里标记的mInLayout是为了在计算排列子View的时候避免在添加和删除子View而产生冲突
        mInLayout = true;
        //这里透露一下,populate是ViewPager原理的核心关键方法,下边会重点说
        //这里是没有子View。所以对子View的计算很快就会结束。
        populate();
        mInLayout = false;

        // Page views next.下边是委托子View去测量自己,这个时候子View所以先省略
        size = getChildCount();
        for (int i = 0; i < size; ++i) {
          //子View不存在
        }
    }

这样看来,在一开始的onMeasure方法里并没有做什么,测量了下自身,这也符合我们的理解,并且还计算了子View的位置,这里也是没什么必要的。由于开始的size是0,0,这个时候有了新的值,所以之后执行onSizeChanged()方法

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // Make sure scroll position is set correctly.
        if (w != oldw) {
        	//如果两值不同,就重新计算滚动位置,这里刚初始化时就是原始位置
            recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
        }
    }

在完成size改变的方法之后,会触发onLayout执行操作,这是三部曲里边的布局方法,在这个时候没有子View的话,就是在为自身做布局,在之后提供了子View之后,才会引发复杂的布局过程,这个时候不需要,我这里省略:

@Override
    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;
        for (int i = 0; i < count; i++) {
           //这个时候没有子View,内部布局暂时省略。。。
        }

        final int childWidth = width - paddingLeft - paddingRight;
        // Page views. Do this once we have the right padding offsets from above.
        for (int i = 0; i < count; i++) {
            //这个时候也不需要分析子View的布局
        }
        mTopPageBounds = paddingTop;
        mBottomPageBounds = height - paddingBottom;
        mDecorChildCount = decorCount;

        if (mFirstLayout) {
        //在第一次布局的时候将位置滚动到当前全局变量的位置
            scrollToItem(mCurItem, false, 0, false);
        }
        //这个时候,将第一次布局设置为false,每次都重置,实际上没有必要,不过不耗时
        mFirstLayout = false;
        Log.e(TAG,"onLayout end");
    }

onLayout执行完成之后,就完成了布局,测量布局都完成之后,就可以绘制了。下面看onDraw方法

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Draw the margin drawable between pages if needed.
        if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
        	//这个时候mItems没有添加任何item进去,所以size一定为0
        	//(从上边初始化的过程并没有对mItems进行设置,添加,所以为0)
        }
    }

好的,在初始化的全过程我们已经分析完成了。所以我们第一阶段分析就到这里,在这个部分因为没有设置子View,所以省略了全部关于子View的部分,但是唯一一点的是mFirstLayout这个时候的boolean值为false,已经完成了第一次布局。

2、 ViewPager显示item

上边的分析我们走了一遍,因为没有子View,所以整个绘制的过程,并没有做出实质性的绘制。把容器给配置好了,现在需要添加孩子了。而且,这也是最重要的一部分内容,涉及ViewPager的缓存机制,预加载机制,以及加载与销毁的时机的分析。

在代码中如果我们没有调用ViewPager的setAdapter()方法,ViewPager并不会显示出子View,即使你已经创建了PagerAdapter适配器。所以我们断言:ViewPager生效,触发子View加载与绘制。

下面我们来看,源码

public void setAdapter(@Nullable PagerAdapter adapter){
		//mAdapter全局变量一开始是null,所以这个时候不会走下面这个重置方法
		//在重新给ViewPager设置新的Adapter的时候就会触发下面的重置
		//这里我们虽然是第一次设置,但是由于重置也是通过这个方法,所以这里也一并分析
        if (mAdapter != null) {
        	//下面这个方法是给PagerAdapter设置观察者,这个是双向监听的重要方法
        	//这里由于之前设置的置为空
            mAdapter.setViewPagerObserver(null);
            //标记PagerAdapter方法的状态,这个是标记开始
            //可以我们在页面实现的PagerAdapter里覆写这个父类方法,可以在ViewPager正式显示子View之前干点什么事,在第一次加载的时候特别耗时的情况下,显示进度条什么的。
            mAdapter.startUpdate(this);
            for (int i = 0; i < mItems.size(); i++) {
            	//这里我们假设是更新子View,所以这个时候mItems长度不为0
            	//也就是有子View,所以这个时候需要销毁
            	//而具体怎么销毁,我们已经非常熟练了,是我们实现PagerAdapter必须实现的方法,销毁的具体逻辑交给用户来处理
            	//
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
            //这里结束更新
            mAdapter.finishUpdate(this);
            //清空子View容器
            mItems.clear();
            //这个时候是移除所有子View,当然要刨除掉DecorView
            //这个DecorView是装饰ViewPager的,默认是一旦设置了就会一直存在
            //在我们开发过程中较少使用到,后边我们会介绍下用法
            removeNonDecorViews();
            //重置当前位置为0,滚动到0,0
            mCurItem = 0;
            scrollTo(0, 0);
        }
		//以上的逻辑是重置Adapter过程,到这里就结束了
		//下面的过程就是在设置新的了
		//将全局变量保存到局部变量
        final PagerAdapter oldAdapter = mAdapter;
        //把我们设置给的赋值给全局变量
        mAdapter = adapter;
        
        mExpectedAdapterCount = 0;
		//如果我们设置了空的,就会忽视,并不会报错
        if (mAdapter != null) {
        	//初始化观察者
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            //上边重置设为null了,这个时候又重新设置进去了
            mAdapter.setViewPagerObserver(mObserver);
            //这个标志是为了避免下面populate这个方法状态冲突
            //populate这个方法我们下面着重介绍
            mPopulatePending = false;
            //全局变量设为局部变量
            final boolean wasFirstLayout = mFirstLayout;
            //这个时候将首次布局置为true
            //我们在onLayout的时候把这个设为false,这个时候我们又重置为false了
            mFirstLayout = true;
            上边这个变量置为0了,这个时候就把PagerAdapter我们实现的getCount()方法,这个之所以没起名叫mChildCount是因为,可能用户设置了很多子View,但是由于我们实现的getCount()方法并没有返回全部的数量,不能显示全部数量,所以起名叫mExpectedAdapterCount,也就是开发者期望显示的子View的数量。
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
            	//当保存的当前位置 >0
            	//重新保存状态,这个是关于状态值得保存,用于意外恢复数据
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                //设置当前的位置为保存的位置
                setCurrentItemInternal(mRestoredCurItem, false, true);
                //这个时候就可以把这个状态值设置为-1
                mRestoredCurItem = -1;
                //然后把状态值,和类加载器置为null
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
            	//这个变量是之前的mFirstLayout的Boolean值,我们知道首次初始化之后是false。再取反,就是true,所以第一次设置之后就会进入这个方法
            	//当然,这个时候是没有保存的当前位置需要恢复
            	//populate在上边说到那个mPopulatePending变量的时候说了
            	//这个方法是整个ViewPager的灵魂方法,也是ViewPager在处理子View逻辑的关键方法
                populate();
            } else {
            	//如果上边的条件全不满足,就会调用下边的引起重新测量重新布局的方法
                requestLayout();
            }
        }

        //下面这个方法就是在我们设置AdapterChangeListener监听Adapter变化的时候,希望我们收到的监听过程
        //如果有设置监听,mAdapterChangeListeners一定不为空
        //所以下边就会从这个列表中遍历通知所有的listener
        if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
            for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
                mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
            }
        }
    }

setAdapter()这个方法,既可以第一次设置,也可以重置,所以针对这两种状态,还要区别对待,如果是更新的话,就先走更新的逻辑,如果不是更新的话,就跳过更新的步骤,直接进入配置新的Adapter。而Adapter的变换我们有个关于Adapter改变的监听,这个时候需要给调用者返回旧的Adapter和新的Adapter(通过如下方法回调)。

mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);

接着Adapter的设置:
将adapter赋值给mAdapter全局变量,并且在不为空的情况下进行处理(如果为空就忽视)

1、实现双向绑定:初始化PagerObserver,并且将其设置给适配器,用于客户端在数据改变的情况通过调用notifyDataSetChanaged方法进行数据变更。这个方法就是调用我们初始化的PagerObserver的onChanged方法进行,onChanged方法内部则是调用ViewPager的包方法dataSetChanged,从而实现ViewPager数据变更。ViewPager里边设置Adapter,Adapter里设置了Observer,从而实现双向绑定。

2、重置变量:重置mPopulatePending变量为false,这个变量是用于在执行核心方法populate()的时候避免与滑动事件产生冲突;重置mFirstLayout为true,这个是在执行到下边条件判断的第一个条件时用到的。而当前的重置前的mFirstLayout方法会保存到局部变量wasFirstLayout里。条件判断的第二个条件正是和这个变量相关;还有重置mExpectedAdapterCount这个变量为我们实现的Adapter里getCount()方法返回的值。

3、条件判断,分情况执行:这里分了三种情况,第一种是处理意外,保存了上次的状态,这个是我们在ViewPager中覆写View的onSaveInstanceState和onRestoreInstanceState方法中收到的影响,在save方法中我们保存意外关闭的重要状态值,而在重新恢复的restore方法中我们会将mRestoredCurItem设置为上次保存的位置,所以一旦有上次保存,所以mRestoredCurItem一定不小于0,这个时候就会进入恢复流程,进入第一个条件语句。里边主要是跳转到上次保存的item位置,具体这里不分析了;第二种是wasFirstLayout为false的情况,也就是在onLayout方法执行过了的时候(因为在onLayout方法中会将赋值给wasFirstLayout的全局变量mFirstLayout设置为false,标志着第一次layout的结束),这个时候会调用到核心方法populate()进行缓存计算,populate是ViewPager的灵魂方法,下边会着重分析;最后一种情况就是当setAdapter执行方法在onLayout方法中变量置为false之前执行了,这个时候就会requestLayout引起重新的测量和布局。所以会执行onMeasure(),onLayout()方法。而在onMeasure()方法中会调用populate()方法。

4、Adapter改变监听分发:最后一部分就是在对Adapter改变的监听,这里需要注意一点,当Adapter是第一次设置,这个时候oldAdapter为null,在监听回调那里我们可以通过null值来判断是否是第一次。


整体的setAdapter方法流程我们捋了一遍,下面我们就进入上边一直强调的populate()方法:

populate()方法--------上代码

void populate() {
     populate(mCurItem);
}

调用带参数的方法,传入的是全局变量mCurItem,第一次设置Adapter的时候mCurItem为0;
提示:下边为完整源码,可以快速浏览,源码结束会有详细分析

void populate(int newCurrentItem) {
        //创建一个信息类,包含了子item构建所需要的全部信息
        ItemInfo oldCurInfo = null;
        //因为传进来的正是mCurItem,所以下边条件不符合
        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }
        //如果用户设置空适配器,排序子View然后拦截返回
        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        //因为这个方法是在多种情况下调用的,所以为了避免和滑动引起的冲突,需要延迟这部分绘制
        // Bail now if we are waiting to populate.  This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }
        //在粘贴到窗口上之前也不能去构建,否则就会使得我们保存的视图层次状态不生效
        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);//调用开始更新的方法,是一个标记方法

        final int pageLimit = mOffscreenPageLimit;//获取预加载数量,这里开始出现了缓存策略相关的变量了
        final int startPos = Math.max(0, mCurItem - pageLimit); //确定绘制子View的起始位置,起始位置是0和当前位置-缓存数量的较小值
        final int N = mAdapter.getCount();//获得用户需要显示的子View的数量
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);//确定绘制子View的结束位置,结束位置是最后一页和当前位置+缓存数量的较大值

        if (N != mExpectedAdapterCount) {//数量不一致时,数据发生变化,但是没有提示ViewPager更新
            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());
        }

        // Locate the currently focused item or add it if needed. 定位当前位置,并且及时添加新的子View
        int curIndex = -1;
        ItemInfo curItem = null;
        //第一次加载mItems的size为0
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }
        //如果当前为空,且总数不为空,执行添加
        if (curItem == null && N > 0) {
            //关键代码,增加子View,只添加当前View
            curItem = addNewItem(mCurItem, curIndex);
        }

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        //判断执行一侧循环次数有两个关键,必须同时满足才会销毁子View,否则将会不断创建
        //第一个是:必须满足至少两倍的ViewPager宽度占有率
        //第二个是:循环的子View索引是否超出缓存的边界
        if (curItem != null) {
            //左边判断
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;//前一个
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();//ViewPager宽度-左右padding的值
            final float leftWidthNeeded = clientWidth <= 0 ? 0 ://判断啊左边还需要的宽度,是个倍数关系
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {//先进这里
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
            //右边判断
            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
                        (float) getPaddingRight() / (float) clientWidth + 2.f;
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                        + " view: " + ((View) 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;
                    }
                }
            }

            //为后续布局和绘制做偏移校准,计算起始偏移量
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
            //告诉适配器当前的item位置及信息
            mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
        }

        if (DEBUG) {
            Log.i(TAG, "Current page list:");
            for (int i = 0; i < mItems.size(); i++) {
                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
            }
        }
        //标记配置基本参数结束,可以通过适配器回调
        mAdapter.finishUpdate(this);

        // Check width measurement of current pages and drawing sort order.
        // Update LayoutParams as needed.
        //将保存在ItemInfo里的信息赋值给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) {
                // 0 means requery the adapter for this, it doesn't have a valid width.
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        sortChildDrawingOrder();

        //遍历获取焦点
        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(View.FOCUS_FORWARD)) {
                            break;
                        }
                    }
                }
 }

在上边的源码中,解释了主要代码的功能作用,但是这样过一遍,很容易会忘记,所以我们总结一下:
分为以下几个部分:

A、特殊情况的拦截及处理
B、计算缓存边界
C、获取需要显示的Item信息
D、分别循环计算显示Item左边和右边需要缓存的Item信息,创建没有的,销毁多余的
E、计算偏移值,为布局做准备
F、将保存在ItemInfo里的信息赋值给LayoutParams
G、为转场动画准备,遍历获取焦点


在整个populate中,在计算缓存前,有四次对特殊情况的处理,有三次是在Adapter的startUpdate()标记方法之前,一次在其之后。这四种情况分别是:

1、mAdapter == null,用户还未设置适配器,这个时候停止处理并返回,这种情况是在ViewPager初始化的时候,在调用onMeasure()方法中,会调用populate(),方法,而此时populate()方法的执行没有意义,避免浪费性能及其他错误,这里直接返回。

2、mPopulatePending == true 的情况,这个值默认是false,而且在setAdapter中也会重置为false,只有在以下两种情况会为true:一种是在onTouchEvent当手指离开屏幕的时候,另外一种是结束虚拟滑动endFakeDrag的时候,看这个两个方法,可知都是和滑动相关的,而且,从调用位置我们发现,正好是手指离开屏幕,或者模拟手指离开屏幕,而产生的fling到新的位置,这个时候避免出错,延迟绘制。(执行sortChildDrawingOrder(),这个方法和我们转场动画有关)

3、getWindowToken() == null,这个方法获得的token正是当前View粘贴到(attach)到的window的token,如果为空,则说明此时View树还未与window产生联系,主页面还未调用onResume方法。这个时候也不能计算缓存。

4、N != mExpectedAdapterCount ,我们上边刚刚在setAdapter中提到这个变量的赋值,是调用Adapter的getCount()方法,而N也是用相同的方法获得的,如果同一个方法获得的数据个数不同,那说明用户修改了数据源,而并未调用notifyDataSetChanged()方法来告知ViewPager同步,所以谷歌这个给开发者抛出了异常,从而让开发者意识到错误的原因并及时修改。


对特殊情况处理完就正式进入缓存计算了,其实上边总结的B、C、D这三部分是一部分,但是代码过多为了便于理解,我拆开了。

缓存计算是在两个方法中进行的:
mAdapter.startUpdate(this);
mAdapter.finishUpdate(this);
这两个方法,是标记方法,我们可以在实现的Adapter中覆写这两个方法,来掌握时机,做一些事情。。。

缓存计算正式开始了————
一个重要的缓存配置变量mOffscreenPageLimit,在ViewPager(五)中我们介绍过这个变量的配置方法,可以定制我们的缓存数量
根据这个配置变量,我们计算了startPos,endPos,顾名思义!就是缓存的起始边界咯

边界有了就要加载Item了,但是在此之前还是要介绍几个重要的功能,重要哦~

ItemInfo 缓存条目的详细信息类,类中有几个属性分变量,分别是object(这个信息类中的View对象),position(item的位置),scrolling(boolean值,是否滑动状态),widthFactor(宽度因子,覆写Adapter的getPageWidth方法的返回值,默认为1f),offset(item偏移量)

mItems 一个属性变量,ArrayList类型,里边存放的为ItemInfo

在找当前要显示的item信息的时候,首先是从容器里找。因为populate是多个地方调用,不一定是setAdapter才调用的,所以可能mItems这个变量开始有元素,那么久尝试去里边找,找到就命中

 for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

如果找不到就去创建,即curItem为null

if (curItem == null && N > 0) {
            //关键代码,增加子View,只添加当前View
            curItem = addNewItem(mCurItem, curIndex);
 }

好了一个关键方法出来了,就是addNewItem(),我们来看源码

ItemInfo addNewItem(int position, int index) {
       ItemInfo ii = new ItemInfo();//创建信息类
       ii.position = position;//赋值位置信息
       ii.object = mAdapter.instantiateItem(this, position);//赋值我们包含的对象,这个方法很熟悉
       ii.widthFactor = mAdapter.getPageWidth(position);//赋值子View的宽度因子,方法也是我们覆写的,默认是1.0f
       if (index < 0 || index >= mItems.size()) {
           //依次添加
           mItems.add(ii);
       } else {
           //添加到指定位置
           mItems.add(index, ii);
       }
       return ii;
   }

里边有我们熟悉的方法instantiateItem,从而证明我们创建的子View确实就是从这个方法获得的,所以,我们在实现这个方法的时候,返回什么,他这就缓存什么。

另外还调用了getPageWidth,这个方法是实现ViewPager高级功能时实现的,就是设置当前item的显示宽度,是个比例值。(这里先剧透下,这个宽度因子,你不能设置2或者跟大,否则就会显示异常,而如果你设置太小的话,所有item能在一屏内显示的时候,就会发生鬼畜的现象,哈哈,对,是鬼畜。为什么呢,下边的源码能找到答案)

这个时候如果我们正确设置实现了instantiateItem这个方法并返回,那么此时的
curItem将不再为空,需要显示的实现结束了就在这个判空里边我们实现他的左右缓存,由于左和右实现原理相同,只是方向相反,这里只分析左实现,感兴趣的读者,可以自行分析右实现(源码是先左,后右)。

for循环是实现的关键,但是在这个之前是一些准备工作
储存一个循环处理变量,用于和比较:

float extraWidthLeft = 0f;

获得了当前位置的前一个位置

int itemIndex = curIndex - 1

在mItem缓存列表中,找一找,有没有符合条件的,没有返回null

ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;

计算一下这个item的左边还需要剩余空间(leftWidthNeeded )多少,是个比例关系,我们按照默认来看(即curItem.widthFactor == 1.0f),leftWidthNeeded就是1+左padding占的宽度比例值,如果我们假设padding也没有,那么就是1了。

final int clientWidth = getClientWidth();

final float leftWidthNeeded = clientWidth <= 0 ? 0 :2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;

准备工作就绪,就进入循环,循环是从当前位置减1开始的,依次递减,到0结束,循环分三种情况(三个条件对应)
1、extraWidthLeft >= leftWidthNeeded && pos < startPos:extraWidthLeft 是一个起始为0,不断增加的量,所以&&符号左边开始一定不符合,而&&符号右边就是控制缓存边界,而startPos正是我们计算的缓存左边界;
2、ii != null && pos == ii.position :&&符号左边是我们循环获得的ItemInfo不为空,右边是保存的位置和当前的循环位置相等
3、第三个条件时else,即除1、2外的其他情况

针对以上三个条件,我们动态来分析(以下示意图展示的都是mItems变化)
分析1
第一种分析:我们是第一次设置Adapter,这个时候mItem除了有当前位置一个元素外,没有其他。并且,循环一次都不会执行
我们按照缓存策略,默认缓存为1,右边虽然我们不分析,但是可以知道这次populate之后会缓存第二个Item。

分析2
第二种分析:这个时候我们在设置到第二个页面时(或滑动到下一页),当前的position为1,也就是第二个位置,我们假设分析一直到达不了右边界,这个时候的左侧有一个item,可以进入循环,我们发现符合第二个条件,因为ii已经缓存到Items里边了,所以不为null,并且position和pos均为0,这时我们看下符合条件我们做了什么

else if (ii != null && pos == ii.position) {
     extraWidthLeft += ii.widthFactor;
     itemIndex--;
     ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} 

将widthFactor,给extraWidthLeft变量加上去,此时itemIndex为0,再自减1的话,就是-1,所以下边的ii为null。然后进入下次循环,发现循环变量为-1,不符合循环条件,退出循环。

分析3
第三种分析:我们假设来到了第三页,这个时候的curIndex = 2,并且itemIndex = 1,循环可以执行,循环变量pos=1,这个时候命中第二个条件,把宽度因子叠加到extraWidthLeft 上,将itemIndex自减,这个时候itemIndex为0了,并且也是可以从mItems里边获得到的;然后下一次循环,pos=0;这个时候,我们发现命中了第一个条件(extraWidthLeft >= leftWidthNeeded按照leftWidthNeeded = 1满足,并且pos < startPos也满足,startPos此时为1),所以看第一个条件里边做了些什么:

 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
        if (ii == null) {
            break;
       	}
        if (pos == ii.position && !ii.scrolling) {
            mItems.remove(itemIndex);
            mAdapter.destroyItem(this, pos, ii.object);
            if (DEBUG) {
                Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
             }
            itemIndex--;
            curIndex--;
            ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        }
 } 

这是三个条件里最多代码的,首先判断ii是否空,为空就退出当前循环,不为空继续向下,呵,又一个判断,&&左边的是相等的,假设这个时候已经结束滑动了,所以&&右边的也符合,执行判断内部逻辑,首先我们移除了mItems中itemIndex的位置的缓存,这个时候ItemIndex为0。然后看到了我们另一个熟悉的方法destroyItem(),这个方法也很重要,正是我们实现适配器的重要方法,我们可以在元素销毁的时候干点什么,定义销毁策略。然后是两个变量的自减,所以这个时候curIndex为1(因为mItems进行了remove操作,导致index和position错位,为了下边右缓存计算不出现问题,需要矫正这个变化,差距是1,所以这里curIndex进行了自减),itemIndex为-1。获取到的ii为null。然后循环不符合条件了,所以循环结束。

分析4
第四种分析:有的读者一定会问,为什么没有执行第三个条件,这个时候在第三种分析的基础上,我们换方向,这个时候我们假设从第三页回到了第二页,这个时候由于我们之前删除了一个元素,所以我们回到之前确定curIndex的代码的地方,由于这段代码很容易让人不理解,所以这里我们不厌其烦的再copy以下咯:

int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
    final ItemInfo ii = mItems.get(curIndex);
    if (ii.position >= mCurItem) {
        if (ii.position == mCurItem) curItem = ii;
        break;
    }
}

对的,就是这个for循环,因为我们在第三种分析的时候将本来mItems的第0个元素remove掉了,导致原来实际位置为1的元素,在mItems集合中,放到了第0个位置,所以我们此时要显示实际位置为1的item,循环的第一个curIndex为0的情况获取到的ii就会命中(他的位置position恰好和mCurItem一致,这个时候mCurItem为1)。所以局部变量在进入下边的左右缓存计算处理时的curIndex =0;
接着进入左右缓存,此时curIndex = 0,itemIndex = -1,所以准备就绪后的ii,为null,所以左边缓存的条件判断,前两个条件都将不符合,那么就会命中第三个条件,下面我们来看,最后一个条件的源码:

else {
   ii = addNewItem(pos, itemIndex + 1);
   extraWidthLeft += ii.widthFactor;
   curIndex++;
   ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}

由于此时itemIndex为-1,而实际上应该0,所以为了纠正,这里加了个1,刚好重新获得了在第三种分析中被销毁的实际位置是0的Item。另外还需要注意一点是,由于此时curIndex是0,而实际上应该是1.所以这里又做了一次自增调整,而下边的ii是null,此时循环变量pos是0,下次循环是-1,不满足循环条件,退出循环。这里需要提醒的是,从右往左返回时,由于mItems数组会删除前边的额元素,导致错位,所以,这里在第三个条件里边做了矫正。也正因为上边的矫正,才会使得下边的右边缓存的计算才不会出现错误。

分析5
第五种分析:其实第五种分析是针对开发者在代码中调用setOffscreenPageLimit,改变缓存配置变量mOffscreenPageLimit,我们知道这个值,默认是1,正如我们前面四种分析的采用的缓存策略一样,但是我们想象这样一种情况,如果我们当前显示的是第四页,也就是mCurIndex = 3,这个时候我们设置了缓存配置变量为2,这个时候mItems中间应该保存有序号为1,2,3,4,5(假设右边没有到头),而由于默认的策略是1,而mItems中保存的仅是序号为2,3,4三个Item元素,这个时会先触发第二个条件,从缓存中获取,然后将itemIndex自减,针对左边接下来就会就会触发第三个条件,导致创建元素并插入到正确的位置;而如果又重新设置了缓存配置变量为1,这个时候针对左边缓存又需要销毁一个元素,还是会先进入第二个条件,itemIndex,然后进入第一个条件,进行remove。这样我们就知道了这个setOffscreenPageLimit是如何影响到ViewPager的缓存策略的。(这里如果不明白的读者,可以按照上边几种分析,结合代码再走一遍)

ps:左右缓存分析一样,右缓存读者可以自行分析~


对于缓存的分析是很重要的一块,也是一个理解的难点。我们已经突破了。接下来就是计算偏移值,调用calculatePageOffsets这个方法。方法主要分两大块,oldCurInfo!=null和oldCurInfo == null。就是初始化和滑动的两个过程,实际的逻辑取决于mCurItem这个属性变量与newCurrentItem这个参数是否相等。下面我们来分析这部分源码(源码较长,我把删减细节用文字代替):

private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
        final int N = mAdapter.getCount();
        final int width = getClientWidth();
        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
        // Fix up offsets for later layout.
        //为之后的布局修复调整偏移
        --------第一部分--------
        if (oldCurInfo != null) {
            final int oldCurPosition = oldCurInfo.position;
            // Base offsets off of oldCurInfo.
            if (oldCurPosition < curItem.position) {
 					//省略源码
                    ii.offset = offset;
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
               //省略源码
                    offset -= ii.widthFactor + marginOffset;
                    ii.offset = offset;
                }
            }
        }

        //如果是初始化操作或者刷新当前页从这里开始执行
        // Base all offsets off of curItem.
        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;
        // Previous pages前边的页,计算偏移
        --------第二部分--------
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
          //省略源码
            ii.offset = offset;
            if (ii.position == 0) mFirstOffset = offset;
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;
        // Next pages后边的页计算偏移
        --------第三部分--------
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
        //省略源码
            if (ii.position == N - 1) {
                mLastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }

        mNeedCalculatePageOffsets = false;
    }

以上代码我分了三部分,第一部分是对当前的item的offset偏移量进行计算,第二部分是对当前位置的左边所有的item的offset偏移量依次进行计算赋值,第三部分是对当前位置的右边所有的item的offset偏移量依次进行计算赋值。

有个ViewPager属性关注下:

mPageMargin 这个变量是我们通过设置setPageMargin方法设置的,默认0;

如果第一次设置,或者刷新当前页,即oldCurInfo == null。这个时候当前位置不变,这个当前位置不用改变,但是由于有一种情况是针对缓存策略的改变,所以即使当前位置不变,但是左右两边的偏移会有变化,所以需要重新计算。

这里我们需要注意的是,代码中出现的curIndex,和pos两个变量,这里解释下:

curIndex:是针对mItems的序号,是当前要显示的item在List里对应的位置
pos:这个是针对ViewPager的所有Item,是真实的位置。

只有我们清楚的了解以上两点,才不会被不断的while循环搞混,而while循环只是在不断矫正list的curIndex和pos的匹配。他两虽然没有关系,但是通过mItems集合获取curIndex位置的ItemInfo,这个对象的position属性记录的正是真实位置。所以这样就建立起了联系,所以curIndex位置获取到的ItemInfo的postion属性比pos变量小,那就增大curIndex或者减小pos;反之,curIndex位置获取到的ItemInfo的postion属性比pos变量大,那就减小curIndex或者增大pos,之所以可以连续处理,是因为mItems存的item虽然对不上号,但是他们是连续的。

有了以上这个算法的基础,我们再来看calculatePageOffsets的源码,计算当前位置的偏移offset的时候先是判断了下是向左滑,还是向右滑,这个是通过对比
oldCurInfo的position和curItme的position的大小关系,分成了两种情况,里边都是根据上边的算法矫正之后,给当前的itme赋上正确的offset的值。

然后第二部分和第三部分是计算左边右边的偏移量。其实都是直接算mItems里保存的item的偏移量,但是都是相对真实的第0个item开始计算的,虽然有可能缓存策略会把前边的回收,但相对位置还是按照有他计算的。举个栗子:如果缓存是1的话,当前位置是3(右边没到头),那么mItems里边这时,会只有实际位置2,3,4。这个时候0,1会回收,但是计算的实际偏移值offset,这三个分别是:2,3,4,而不是0,1,2。(这个offset是宽度倍数关系)

随后在calculatePageOffsets还需要关注两个属性变量

private float mFirstOffset = -Float.MAX_VALUE;//用来标记是否到达了最左边
private float mLastOffset = Float.MAX_VALUE;//用来标记是否到达了最右边

这两个变量会在这个时候做矫正,而在滑动的时候做判断
mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;

只要当前显示的不是第一个或者最后一个,他们值会和curItem的offset设置了相应的计算关系。这个时候在滑动的时候就能很快判断是否到达了边缘,只要这两个变量不是默认值,就没有到边

分析完calculatePageOffsets,我们返回调用他的地方(populate方法),接着向下:
连续调用

mAdapter.setPrimaryItem() 这个方法表征ViewPager马上要显示的Item信息

同样在适配器有个创建对象的方法instantiateItem,但是由于ViewPager的缓存策略导致创建的并不是要显示的,那么适配器为了解决用户回调当前显示的信息,而且是最及时的获取,就有了setPrimaryItem这个回调方法

接着调用

mAdapter.finishUpdate(this); 结束更新缓存,标记方法,与startUpdate成对出现

然后就是通过一个循环,将所有子View中的ItemInfo对象的widthFactor和position属性赋值给LayoutParams。如下:

lp.widthFactor = ii.widthFactor;
lp.position = ii.position;

然后是一个关于转场或者叫翻页动画的方法sortChildDrawingOrder();
代码不长:

 private void sortChildDrawingOrder() {
        //只有在设置transform的时候才会涉及这部分内容
        if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
            if (mDrawingOrderedChildren == null) {
                mDrawingOrderedChildren = new ArrayList<View>();
            } else {
                mDrawingOrderedChildren.clear();
            }
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                mDrawingOrderedChildren.add(child);
            }
            //排序,位置关系及是否是decor
            Collections.sort(mDrawingOrderedChildren, sPositionComparator);
        }
    }

整个方法执行的先决条件是mDrawingOrder != DRAW_ORDER_DEFAULT,只有满足这个条件才执行方法体的逻辑,看过之前博客的童鞋,也就是在转场动画的应用里,提到过,mDrawingOrder默认就是DRAW_ORDER_DEFAULT,而只有当调用了setPageTransform函数设置transform的时候,才会被赋值成DRAW_ORDER_REVERSE或者DRAW_ORDER_FORWARD,不管是哪个,都会导致条件满足。

代码很简单,依次添加子View到mDrawingOrderedChildren集合中,然后排序,所以我们只要知道他排序的规则是什么就行,看sPositionComparator(LayoutParams的内部类ViewPositionComparator):

static class ViewPositionComparator implements Comparator<View> {
        @Override
        public int compare(View lhs, View rhs) {
            final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
            final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
            if (llp.isDecor != rlp.isDecor) {
                return llp.isDecor ? 1 : -1;
            }
            return llp.position - rlp.position;
        }
    }

简单说下:如果比较的两个View一个是DecorView,一个不是,就按照参数前边的View的isDecor布尔值来排序。其他直接按照参数前边的位置减后边的位置,来决定排序。按照位置升序排序。

populate最后一步,遍历子View让其获得焦点,很简单设置获取焦点的规则为FOCUS_FORWARD:

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(View.FOCUS_FORWARD)) {
                   break;
               }
           }
       }
   }
}

好了,populate到这里就分析完了,但是读者你认为执行完populate就完事大吉,就能显示子View了吗?

答案是:还远远没完,还不能显示,至少我们分析还没有到位。那么还缺什么?这里提醒下,我们在执行完这个标记方法mAdapter.finishUpdate(this)后,我们多次调用了getChildCount(),这个方法是干嘛的,对,获取子View的个数,ViewPager是什么时候加载进来的子View的呢?你可能会觉得,我们不是调用addNewItem这个方法加进来了吗,说对了一半!为什么这样说?因为addNewItem只是将View加到了List集合里,而且还只是对象集合的一个属性,所以ViewGroup并没有收到他的子View。这个时候你一定有想法了,ViewGroup添加子View的方法:addView。你有想到这个方法什么时候调用的吗?也许你并不留意,把这句代码习惯性的写了。

揭晓:你在实现Adapter的时候,或者FragmentAdapter的默认的instantiateItem方法中,我们往往是从创建了View并返回,在这之中还有一句很关键的代码就是container.addView(),而container就是ViewGroup,他将自身传递过来,就是让开发者往里边添加子View的。而如果你一不小心漏了这句话,你会发现ViewPager什么都没有显示出来!

addView是ViewGroup的方法,ViewPager虽然有实现,但是重载的三个参数的,我们一般调用的是一个参数的,熟悉addView源码都童鞋,都知道,addView一个参数,两个参数,最终都会调用到三个参数的,所以还是会调用ViewPager实现的三个参数的addView方法,下面来看:

@Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }
        final LayoutParams lp = (LayoutParams) params;
        // Any views added via inflation should be classed as part of the decor
        lp.isDecor |= isDecorView(child);
        if (mInLayout) {
            if (lp != null && lp.isDecor) {
                throw new IllegalStateException("Cannot add pager decor view during layout");
            }
            lp.needsMeasure = true;
            addViewInLayout(child, index, params);
        } else {
            super.addView(child, index, params);
        }

        if (USE_CACHE) {
            if (child.getVisibility() != GONE) {
                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
            } else {
                child.setDrawingCacheEnabled(false);
            }
        }
    }

其实这个实现是为了处理ViewPager搞出来的这个DecorView的,然后会根据是否在执行从Measure中调用的populate的方法(mInLayout在onMeasure方法中做了赋值操作),分成了两个分支addViewInLayout和super.addView。

addViewInLayout只是将View加到ViewGroup的View[]数组中而不会调用requestLayout引起重新测量布局,但是这部分也不能不管,刚刚通过这种方法加进去的View还没有测量,所以没办法layout,这个时候在onLayout里边会有相应的处理。这里先提一嘴,等会到onLayout方法里看源码就知道了。

我们看ViewGroup的addView方法

public void addView(View child, int index, LayoutParams params) {
        //省略部分源码
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

我留了最关键的几行代码,没错requestLayout,invalidate,这个有过自定义View的经验的童鞋一定不陌生,这个时候就会引起重新测量,布局,绘制。

在博客第一部分的初始化里边说过onMeasure方法,onLayout方法和onDraw方法,这里就不说说过了,我们只看其中对子View处理的部分:

首先是onMeasure:

//之前省略了DecorView的处理源码

//这个就是我们在addView里边用于判读添加子View执行方式时用到的变量
//是通过这里赋值的,为True说明正在执行下边的或者即将执行populate方法
//默认为false
 	mInLayout = true;
	populate();
	mInLayout = false;
	
	// Page views next.
	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();
	       if (lp == null || !lp.isDecor) {
	           final int widthSpec = MeasureSpec.makeMeasureSpec(
	                   (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
	           child.measure(widthSpec, mChildHeightMeasureSpec);
	       }
	   }
	}

到这里开始我们才真正开始绘制View的流程,遍历所有不是decorView的子View,委托他们自己去测量

这里onSizeChanged就不分析了,就是重新滑动到正确的位置

然后是onLayout方法:

//省略对decorView的处理源码

final int childWidth = width - paddingLeft - paddingRight;
      // Page views. Do this once we have the right padding offsets from above.
      for (int i = 0; i < count; i++) {
          final View child = getChildAt(i);
          if (child.getVisibility() != GONE) {
              final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              ItemInfo ii;
              if (!lp.isDecor && (ii = infoForChild(child)) != null) {
                  int loff = (int) (childWidth * ii.offset);
                  int childLeft = paddingLeft + loff;
                  int childTop = paddingTop;
                  if (lp.needsMeasure) {
                      // This was added during layout and needs measurement.
                      // Do it now that we know what we're working with.
                      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);
                  }
                  if (DEBUG) {
                      Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
                              + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
                              + "x" + child.getMeasuredHeight());
                  }
                  child.layout(childLeft, childTop,
                          childLeft + child.getMeasuredWidth(),
                          childTop + child.getMeasuredHeight());
              }
          }
      }
      mTopPageBounds = paddingTop;
      mBottomPageBounds = height - paddingBottom;

我们看到了计算的offset派上用场了,由于offset是倍数值,所以它在layout计算距离的时候是先乘上一个childWidth,而这个宽度值时将padding减去的值,所以也就保证了padding能生效。

其他部分就是常规的计算相对位置的,并委托给View的layout完成最终测量,上边提到过addView的两种添加方法,addViewInLayout并不能引起测量和布局,但是也不能不量啊,这里就给了答案,我们在addViewInLayout前将子View的params的needsMeasure变量设置为true。在onLayout里我们在判断needsMeasure的值为true,在这里我们让其测量,然后再布局。

draw和onDraw方法我们之前都有分析,这里就不做分析,invalidate方法会让ViewGroup向下引起子View的绘制,位置大小有改变的才绘制。

最终见到我们ViewPager显示的子View


3、ViewPager的滑动控制

缓存处理时ViewPager很重要的一点,然而ViewPager不是一个静态控件,而是一个动态展示控件,那么它是如何在滑动中保证依然高效流畅的体验呢?这个是我们在本篇博客最后需要分析的问题,涉及很多处理滑动方面的知识,如果还不具备可以先看些基础的东西,等有了储备再看更容易,不过我尽量通俗的说,希望新手也能看的明白大概流程。

------------------我们简单回顾下基础知识--------------

我们都知道手指触动屏幕会产生触摸事件,这个是由硬件层捕获,回传给软件层的,在我们熟悉的界面中,事件是从Activity开始的(这里以Activity举例),然后依次ViewGroup -> ViewGroup -> … ViewGroup -> View 按照这条线不断向下传,这个方向的重点是View(如果中间有事件消费,就不会再往下传了),如果到了View仍没有事件消费,会反方向向上。
第一个方向传递涉及dispatchTouchEvent和onInterceptTouchEvent(这个方法ViewGroup有,View没有)(这个方向上的onInterceptTouchEvent方法返回true会导致onTouchEvent方法执行)
第二个回传的方法涉及到除上边一个方向的方法,还有onTouchEvent方法,如果返回时super的话,也就是默认,那就原路返回。

整个事件分发就ViewPager源码分析来讲,他是个ViewGroup,所以会先传递到dispatchTouchEvent,ViewPager没有覆写,在父类ViewGroup中有:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    		//删除了大部分的源码
		
           // Check for interception.
           final boolean intercepted;
           if (actionMasked == MotionEvent.ACTION_DOWN
                   || mFirstTouchTarget != null) {
               final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
               if (!disallowIntercept) {
                   intercepted = onInterceptTouchEvent(ev);
                   ev.setAction(action); // restore action in case it was changed
               } else {
                   intercepted = false;
               }
           } else {
               // There are no touch targets and this action is not an initial down
               // so this view group continues to intercept touches.
               intercepted = true;
           }
       return handled;
   }

事件的处理是从down事件开始的,所以只截取了down事件的源码,如果child没有主动调用requestDisallowInterceptTouchEvent,就会执行下一个重要的时间分发方法

intercepted = onInterceptTouchEvent(ev);

好了我们接着看onInterceptTouchEvent方法,这个方法ViewPager有实现,代码较多这里我就不贴了,直接分析
首先看down事件

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
       		//删除部分源码
            case MotionEvent.ACTION_DOWN: {
               //删除部分源码
                if (mScrollState == SCROLL_STATE_SETTLING
                        && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                  //删除部分源码
                    mIsBeingDragged = true;
                   
                } else {
                    mIsBeingDragged = false;
                }

        return mIsBeingDragged;
    }

这里ViewPager只是对如果之前是SCROLL_STATE_SETTLING个状态的事件进行拦截,其余的一概不拦截(在down事件)

下面我们再看move事件

 case MotionEvent.ACTION_MOVE: {
       //删除部分源码
        if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
            mIsBeingDragged = true;
         	//删除部分源码
        } else if (yDiff > mTouchSlop) {
        
            mIsUnableToDrag = true;
        }
        break;
    }

这部分ViewPager会在移动距离大于mTouchSlop,并前横向移动距离大小是竖向的两倍的情况也会拦截,这就意味着横向滑动将全部交给ViewPager处理。

然而这个move事件基本上不会传到onInterceptTouchEvent,这个方法,我们知道,如果子View不处理的话,或者直接拦截返回true,都会重新调用当前ViewGroup的onTouchEvent方法,这是个重点方法,我们去看下,ViewPager对于onTouchEvent也实现了:

这里有几个特殊情况的判断
1、官方注释的很清楚,就是我们在执行fakeDrag的时候,也要消耗这个事件,并且不往下执行

if (mFakeDragging) {
   // A fake drag is in progress already, ignore this real one
    // but still eat the touch events.
    // (It is likely that the user is multi-touching the screen.)
    return true;
}

2、到屏幕的边界,我们不处理

if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
   // Don't handle edge touches immediately -- they may actually belong to one of our
   // descendants.
   return false;
}

3、适配器还未设置,或者需要显示数量为0

if (mAdapter == null || mAdapter.getCount() == 0) {
    // Nothing to present or scroll; nothing to touch.
    return false;
}

除以上三种情况外,事件会顺利进入对时间类型的判断

首先是onTouchEvent的down事件处理

case MotionEvent.ACTION_DOWN: {
                mScroller.abortAnimation();
                mPopulatePending = false;
                populate();

                // Remember where the motion event started
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = ev.getPointerId(0);
                break;
            }

很简单,停止滚动,恢复变量,执行一次populate,初始化手指初始的位置,id

接下来:onTouchEvent的move事件,如果在Intercept方法里边拦截了,mIsBeingDragged == true,那么在move里将不判断事件方向,而Intercept一般不拦截(除了down事件那种情况),所以都会执行:

if (!mIsBeingDragged) {
    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
    if (pointerIndex == -1) {
        // A child has consumed some touch events and put us into an inconsistent
        // state.
        needsInvalidate = resetTouch();
        break;
    }
    final float x = ev.getX(pointerIndex);
    final float xDiff = Math.abs(x - mLastMotionX);
    final float y = ev.getY(pointerIndex);
    final float yDiff = Math.abs(y - mLastMotionY);
    }
    if (xDiff > mTouchSlop && xDiff > yDiff) {
        if (DEBUG) Log.v(TAG, "Starting drag!");
        Log.e(TAG,"onTouchEvent drag start");
        mIsBeingDragged = true;
        requestParentDisallowInterceptTouchEvent(true);
        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                mInitialMotionX - mTouchSlop;
        mLastMotionY = y;
        setScrollState(SCROLL_STATE_DRAGGING);
        setScrollingCacheEnabled(true);

        // Disallow Parent Intercept, just in case
        ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
        }
    }
}

计算了下移动量,是否满足,x>mTouchSlop且x>y,设置mIsBeingDragged = true;

然而这个时候都还没有对ViewPager进行滑动,页面还是禁止不动,接着:

 if (mIsBeingDragged) {
    // Scroll to follow the motion event
    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
    final float x = ev.getX(activePointerIndex);
    needsInvalidate |= performDrag(x);
}

我们看到了一个方法performDrag,从名字看出应该和拖动有关系,下面我们看这个方法:

private boolean performDrag(float x) {
        boolean needsInvalidate = false;
        
        //省略部分源码
        scrollTo((int) scrollX, getScrollY());
        pageScrolled((int) scrollX);

        return needsInvalidate;
    }

主要是计算scrollX,水平滚动距离,然后分别调用scrollTo方法和pageScrolled方法,ViewPager并没有实现scrollTo方法,所以这个调用的是View的scrollTo方法,熟悉自定义View的童鞋,知道scrollTo就是滑动控件。所以这个时候控件动起来了。pageScrolled方法在ViewPager的部分实现:

private boolean pageScrolled(int xpos) {
   	//省略部分源码
     final ItemInfo ii = infoForCurrentScrollPosition();
     final int width = getClientWidth();
     final int widthWithMargin = width + mPageMargin;
     final float marginOffset = (float) mPageMargin / width;
     final int currentPage = ii.position;
     final float pageOffset = (((float) xpos / width) - ii.offset)
             / (ii.widthFactor + marginOffset);
     final int offsetPixels = (int) (pageOffset * widthWithMargin);

     onPageScrolled(currentPage, pageOffset, offsetPixels);
     return true;
 }

计算了滑动偏移量,然后调用这个方法:onPageScrolled,这个方法看起来像是一个回调,我们看看里边干了什么(以下方法名省略,都是onPageScrolled,方法体适当删减):

final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) continue;
if (childOffset != 0) {
    child.offsetLeftAndRight(childOffset);
}

看上边粘贴的代码,先对子View是DecorView进行滑动同步

dispatchOnPageScrolled(position, offset, offsetPixels);

这是对滑动监听的回调分发,也就是我们设置的OnPageChangeListener。

 if (mPageTransformer != null) {
    final int scrollX = getScrollX();
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        if (lp.isDecor) continue;
        final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
        mPageTransformer.transformPage(child, transformPos);
    }
}

看上边的代码,这是onPageScrolled的最后一部分代码,是关于我们的转场动画的,如果我们设置了页面转换器,就会通过 mPageTransformer.transformPage(child, transformPos)这个方法调用我们实现的transformPage,我们通过传给我们的View执行属性动画,这样我们的动画才生效了。

final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();

我们曾经在ViewPager技巧的转场动画博客中有说过结论,就是下面这张图片
翻页相对位置示意图
当时只说了结论,没有解释为什么,现在结合上边那句代码,我简单说下:
分三点

child.getLeft() 滑动的View相对他的父布局的左边的距离
scrollX ViewPager横向滑动距离
getClientWidth() ViewPager的显示宽度

当不滑动的时候前两个相等,所以为0,这个时候居正中显示
当手指向左滑动时,View向左,这个时候View与相对ViewPager的getLeft不变,scrollX不断增大,当完全消失,即增加一个ViewPager的显示宽度,所以再除以一个getClientWidth()就会得出position的值时【0,-1】
当手指向右滑动时,View向右,这个时候View与相对ViewPager的getLeft依旧不变,scrollX不断减小,当完全消失,即减少一个ViewPager的显示宽度,所以再除以一个getClientWidth()就会得出position的值时【0,1】

分析完这个我们接着回到onTouchEvent事件处理方法,在onTouchEvent的move事件处理中我们说了很多。还差最后一个up事件的处理,我们接着看源码:

 case MotionEvent.ACTION_UP:
    if (mIsBeingDragged) {
        final VelocityTracker velocityTracker = mVelocityTracker;
        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
        mPopulatePending = true;
        final int width = getClientWidth();
        final int scrollX = getScrollX();
        final ItemInfo ii = infoForCurrentScrollPosition();
        final float marginOffset = (float) mPageMargin / width;
        final int currentPage = ii.position;
        final float pageOffset = (((float) scrollX / width) - ii.offset)
                / (ii.widthFactor + marginOffset);
        final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
        final float x = ev.getX(activePointerIndex);
        final int totalDelta = (int) (x - mInitialMotionX);
        int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                totalDelta);
        setCurrentItemInternal(nextPage, true, true, initialVelocity);

        needsInvalidate = resetTouch();
    }
    break;

这个条件处理都是在mIsBeingDragged为true的情况下进行的。里边涉及一些自定义View的知识,开始是获取加速度计算当前速度,然后通过两个重要的方法进行下一页预测和惯性滑动到准确位置。预测的方法是determineTargetPage,而实现惯性滑动的方法是setCurrentItemInternal。
首先看determineTargetPage这个方法:

if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
    targetPage = velocity > 0 ? currentPage : currentPage + 1;
} else {
    final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
    targetPage = currentPage + (int) (pageOffset + truncator);
}

如果fling距离超过mFlingDistance,并且速度也超过mMinimumVelocity,那么如果速度为正,那就直接到下一页(这里就是当前页+1)。如果上边两个条件不能同时满足,那么谷歌工程师有提供了一个4:6的方法因子,然后与参数pageOffset相加足不足1来判断是否到下一页。当然代码的鲁棒性也考虑了,也做了边界判断

targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));

有了这个nextPage(返回的targetPage)就能调用均匀滚动的方法了,接下来看setCurrentItemInternal,其实这个方法,我们在代码翻页的时候最终也是调用的这个方法,这里实现了复用和统一:

void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//省略部分源码
   		//再一次对边界矫正,鲁棒性
        if (item < 0) {
            item = 0;
        } else if (item >= mAdapter.getCount()) {
            item = mAdapter.getCount() - 1;
        }
        final int pageLimit = mOffscreenPageLimit;
        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
            //为了不出现故障,遇到跳转超出一屏的情况,要将他们锁死,保存当前的item,直到滑动结束
            for (int i = 0; i < mItems.size(); i++) {
                mItems.get(i).scrolling = true;
            }
        }
        final boolean dispatchSelected = mCurItem != item;

        if (mFirstLayout) {
            mCurItem = item;
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            requestLayout();
        } else {
            populate(item);
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }

这个方法主要是又对边界进行了校验,然后也考虑到了一次跳转距离超过一屏的情况,最后如果是第一次布局,就会请求重新测量,布局,并且分发当前选中项给OnPageChangeListener。最终在moMeasure也会执行populate方法和相应的滚动方法;而如果不是第一次布局,那就直接执行populate,不用再测量布局了,然后再调用均匀滚动方法。但是选中分发的回调没有执行,会不会在scrollToItem中调用呢?下面我们来看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();
        destX = (int) (width * Math.max(mFirstOffset,
                Math.min(curInfo.offset, mLastOffset)));
    }
    if (smoothScroll) {
        smoothScrollTo(destX, 0, velocity);
        if (dispatchSelected) {
            dispatchOnPageSelected(item);
        }
    } else {
        if (dispatchSelected) {
            dispatchOnPageSelected(item);
        }
        completeScroll(false);
        scrollTo(destX, 0);
        pageScrolled(destX);
    }
}

看到了吗,这个方法里边有选中的分发,这样就不会遗漏掉分发。在这个方法也见到了在populate的calculatePageOffsets中初始化的mFirstOffset,mLastOffset两个属性,用他们来和当前item的offset取中间值,传给对应的smoothScrollTo和scrollTo方法,还有我们在上边move事件中分析的pageScrolled方法。smoothScrollTo和scrollTo是两个具体滚动的方法,一个是一步到位,一个是采用Scroller这个帮助类,进行均匀滑动,这个是基本知识了,就不展开讲了。等整个滚动结束,我们就看到了ViewPager正确的归位了。


补充部分 ------ViewPager之DecorView

在ViewPager源码中几乎每个地方都会出现关于decorView的处理,我们甚至连用都没用过,其实不是我们没用过,是谷歌也有实现的,在上一篇ViewPager系列文章中,在指示器部分提到过两个原生谷歌控件,PagerTitleStrip和PagerTabStrip,这两个其实就是DecorView,也正是因为他们是DecorView并且作为ViewPager的子View才能与ViewPager的具体页面显示同步,为什么这么说,我们看下PagerTitleStrip的类源码:

@ViewPager.DecorView
public class PagerTitleStrip extends ViewGroup {
    ViewPager mPager;
    TextView mPrevText;
    TextView mCurrText;
    TextView mNextText;

我截取了一部分,关键的出来了,在类的上边我们看到了一个注解,而且这个注解实在ViewPager中定义的:

 @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited
    public @interface DecorView {
    }

那么ViewPager又是如何辨别那个View是DecorView呢?通过下面的方法:

private static boolean isDecorView(@NonNull View view) {
        Class<?> clazz = view.getClass();
        return clazz.getAnnotation(DecorView.class) != null;
    }

对,反射获取注解。DecorView这里在最后给大家简单的说这么几句。如果我们想让我们的自定义View实现DecorView的功能,就需要在类上加上上面的注解。


后记:ViewPager的源码到此就分析完了,这篇博客前前后后花费了笔者接近两周的时间,全都是碎片化的时间。本篇博客文字数量包括源码在内达到40000多字,自己也没有想到会花费这么久,写了这么多。就是按照自己脑子里的思路以及程序运行的流程走了一遍源码。其中包含了大量的细节,其中在setAdapter这部分尤其详细,笔者尽可能多的考虑了实际运行中出现的各种情况来分析代码。
其实代码是谷歌工程师写的,我们读源码,不仅要学里边的代码逻辑,来解决实际工作中的问题,更需要学习代码规范,以及一个健壮的代码都需要考虑哪些地方,代码是怎么很好复用的。对滑动冲突他们是怎么解决的,ViewPager对于缓存策略的运用非常的优秀,运用大量的算法,来保证计算正确,另外为了保证滑动的流畅,ViewPager是在onTouchEvent的move事件的时候专注滑动,而由于至少一侧一屏的缓存策略,保证了用户一次滑动都可以正常进行,然后在up事件中,根据他定义的算法规则判断将惯性滑向哪一侧,然后及时populate更新缓存,然后滑动到指定的item。这样设计既避免了重复调用populate浪费性能,也能让整个滑动和缓存处理很好的配合起来。

ViewPager系列博客结尾:这不仅是本篇博客的结尾,也是笔者设计的ViewPager系列博客的结尾。
第一篇,我们从市面上的产品来引出了VIewPager这个神奇,然后介绍了简单的用法;
第二篇我们由易入难地介绍了实现适配器的方法,以及每个方法的作用;
第三篇我们对比了谷歌提供的针对子VIew是Fragment的两个PagerAdapter的实现原理和区别;
第四篇,我们开始了高阶用法教程,首先是换页监听和方法,是多于我们常规使用的api的;
第五篇,还是高阶用法,ViewPager的懒加载机制,实现,和缓存的原理和实现;
第六篇,是高阶进程动画部分,以及配合动画使用的padding和margin设置,及原理;
第七篇是从比较大的层面的设计进阶,包括了轮播实现,禁止滑动以及各类指示器的配合;
第八篇也就是本篇,从初始化,显示子View和滑动三个方面剖析了ViewPager的源码,让ViewPager的源码暴露在读者面前。

系列博客耗时三个月,也都是些零零散散的时间。到今天终于完成了,由于字数过多或者打字过快,有可能会有一些明显的问题,也希望读者能够帮助指出并改正,最后祝大家:bug少,工资高!

下一系列会是什么主题呢?这里先透露下,笔者接下来会研究-------安卓Framework层部分重要代码剖析,以及安卓源码中蕴藏的设计思想和算法思想

敬请期待,谢谢

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,我会为您讲解关于Android中ViewPager和Fragment的使用。 ViewPager和Fragment是Android中非常常用的组件,他们可以一起使用来实现滑动页面效果。ViewPager是一个可以左右滑动切换页面的布局容器,而Fragment作为ViewPager的子页面,可以在ViewPager中进行动态添加和移除。 下面我们将分别介绍ViewPager和Fragment的使用。 ## ViewPager的使用 ### 1.布局文件 在布局文件中,我们需要使用ViewPager作为容器,将需要滑动切换的页面放入其中。如下所示: ``` <androidx.viewpager.widget.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"/> ``` ### 2.创建Adapter 我们需要创建一个Adapter继承自PagerAdapter,并重写以下方法: ``` public class MyPagerAdapter extends PagerAdapter { private List<Fragment> mFragments; public MyPagerAdapter(List<Fragment> fragments) { mFragments = fragments; } @Override public int getCount() { return mFragments.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view == object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { Fragment fragment = mFragments.get(position); container.addView(fragment.getView()); return fragment.getView(); } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); } } ``` ### 3.设置Adapter 在Activity或Fragment中,我们需要创建ViewPager的实例,并设置Adapter。如下所示: ``` ViewPager viewPager = findViewById(R.id.viewPager); List<Fragment> fragments = new ArrayList<>(); fragments.add(new Fragment1()); fragments.add(new Fragment2()); fragments.add(new Fragment3()); MyPagerAdapter adapter = new MyPagerAdapter(fragments); viewPager.setAdapter(adapter); ``` 这样,我们就完成了ViewPager的使用。 ## Fragment的使用 ### 1.创建Fragment 我们需要创建一个继承自Fragment的类,并重写以下方法: ``` public class Fragment1 extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment1, container, false); return view; } } ``` ### 2.布局文件 我们需要在Fragment中添加布局文件,如下所示: ``` <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:text="Fragment1" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> ``` 这样,我们就完成了Fragment的使用。 ## ViewPager和Fragment的结合使用 通过以上介绍,我们已经知道了如何使用ViewPager和Fragment了。现在我们需要将它们结合起来使用。 ### 1.创建Fragment 我们需要创建多个Fragment作为ViewPager的子页面。 ### 2.创建Adapter 我们需要创建一个PagerAdapter,将Fragment添加到ViewPager中。如上所示,我们已经创建了一个MyPagerAdapter。 ### 3.设置Adapter 在Activity或Fragment中,我们需要创建ViewPager的实例,并设置Adapter。如上所示,我们已经使用ViewPager的setAdapter方法设置了MyPagerAdapter。 这样,我们就完成了ViewPager和Fragment的结合使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值