当前位置: 首页 > 工具软件 > ViewPager > 使用案例 >

ViewPager源码解析之ViewPager如何呈现

堵睿范
2023-12-01

概述

经过上一篇Fragment源码的分析,我发现想要完全弄懂源码中的每一个点是不可能的,而且会让自己陷入细枝末节无法自拔,所以我认为在整体上先有一个感性认识,细节问题遇到了再去有针对性的解决是一个比较好的阅读源码的姿势。

经过思考,我觉得还是像Fragment一样从API的调用来分析ViewPager的运行过程 会比较清晰。

平常我们使用到ViewPager,一般会调用下面几个方法,如果是配合Fragment的话,界面就能显示在屏幕上了。

mViewPager.setAdapter(new HomeAdapter(getXXXManager(), mFragments));
mViewPager.setCurrentItem(2);

ViewPager就为一个ViewGroup,和其他所有View一样,会在Activity(假设是在Activity的布局中)的onCreate() 回调的 setContentView() 中通过反射被实例化作为DOM树的一个节点,这里会调用其构造器。接着,我们会在Activity的onCreate() 中调用ViewPager#setAdapter() 和 ViewPager#setCurrentItem() , 在onResume() 回调之后由ViewRootImpl发起绘制流程,依次会回调到ViewPager的onMeasure() 、onLayout() 、onDraw() ,ViewPager就可以显示在屏幕上了。之后ViewPager的滑动就和onInterceptTouchEvent() 、onTouchEvent() 回调相关。我们按顺序一个一个来看。

关注一下成员

有一个静态内部类代表了页面的状态,我们需要关注一下。

   //ViewPager.java
    static class ItemInfo {
        Object object;
        int position;
        boolean scrolling;
      //占屏比,0~1
        float widthFactor;
        //页面偏移量
        float offset;
    }

构造器

都调用了initViewPager() 方法

   //ViewPager.java
    public ViewPager(Context context) {
        super(context);
        initViewPager();
    }

初始化了一些变量,看到新建了一个Scroller对象,这个对象一般是用来做弹性滑动的,所以可能和后面的滑动相关。

//ViewPager.java
void initViewPager() {
        ...
        mScroller = new Scroller(context, sInterpolator);

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

        ...
    }

setAdapter()

在进入到ViewPager的绘制流程之前,我们会在Activity的例如onCreate()回调中调用ViewPager的这个方法。这个方法做了如下几件事,重要细节备注在源码中:

1.如果原来设置了Adapter,清除相关信息。

2.设置新的Adapter,请求开始绘制。

3.回调相关函数。

    //ViewPager.java
    public void setAdapter(PagerAdapter adapter) {
        if (mAdapter != null) {
          //清除监听者,是这个ViewPager的一个内部类
            mAdapter.setViewPagerObserver(null);
          //回调
            mAdapter.startUpdate(this);
          //清除页面信息
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
          //回调
            mAdapter.finishUpdate(this);
          //清除mItems
            mItems.clear();
            removeNonDecorViews();
          //设置当前位置
            mCurItem = 0;
            //滑动到
            scrollTo(0, 0);
        }

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

      //配置新的信息
        if (mAdapter != null) {
            if (mObserver == null) {
              //是ViewPager的一个内部类
                mObserver = new PagerObserver();
            }
            mAdapter.setViewPagerObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            //如果有需要恢复的页面
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
              //重要函数,但这里调用不到,因为我们这里是第一次布局。
                populate();
            } else {
              //绘制
                requestLayout();
            }
        }

        // Dispatch the change to any listeners
      //英文已经解释了~
        if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
            for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
                mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
            }
        }
    }

setCurrentItem()

紧接着我们可能调用了这个方法,这个时候ViewPager并没有进入到绘制流程中。

   //ViewPager.java
    public void setCurrentItem(int item) {
        mPopulatePending = false;
      //传入的参数为 1(假设), false, false。
        setCurrentItemInternal(item, !mFirstLayout, false);
    }

最终调用到四参的这个方法,目前只会执行下面这一部分。从英文注释可以看出,只是设置了一下mCurItem,具体让layout()去处理。
最终调用到四参的这个方法,目前只会执行下面这一部分。从英文注释可以看出,只是设置了一下mCurItem,具体让layout()去处理。

//参数为 1, false, false, 0
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        ...
        final boolean dispatchSelected = mCurItem != item;

        if (mFirstLayout) {
            // We don't have any idea how big we are yet and shouldn't have any pages either.
            // Just set things up and let the pending layout handle things.
            mCurItem = item;
            if (dispatchSelected) {//true
                //回调所有接口的onPageSelected方法
                dispatchOnPageSelected(item);
            }
          //请求绘制
            requestLayout();
        } 
        ...
    }

onMeasure()

随着Activity进入到绘制View的流程中,ViewPager也会进入绘制流程,首先被回调的方法就是这个(之前还会回调一个onAttachedToWindow方法,只是将mFirstLayout设为了true)。我们先要知道一点,ViewPager管理的View有两种,一种是Decor View,它实现了一个空接口作为区分,一种是Page View,就是我们通过适配器给予ViewPager管理的视图。纵观onMeasure()方法,我觉得只要清楚大致的流程就行(关键点在于其中的populate() ) ,onMeasure()中主要做了下面几件事:

1.测量Decor View。

2.populate()。

3.测量Page View

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));

        final int measuredWidth = getMeasuredWidth();
        final int maxGutterSize = measuredWidth / 10;
        mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

        // Children are just made to fill our space.
      //适配padding
        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();


      //Decor View先测量
        int size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                ...
            }
        }

        ...

        // Make sure we have created all fragments that we need to have shown.
        //保证所有我们需要的Fragments显示,这个参数保证在Fragment的INITIALING状态就进行View的创建。
        mInLayout = true;
        //重要,更新缓存页面信息
        populate();
        mInLayout = false;

        // Page views next.
      //测量Page View
        size = getChildCount();
        for (int i = 0; i < size; ++i) {
            ...
        }
    }

populate()

onMeasure()中的细节大家可以自己去查看源码,我这里就不做过多的纠缠,重点放在这个方法上,这个方法在许多地方被多次调用,而且它与Page View的更新、创建、销毁都密切相关。需要重点理解的地方我已经注释在代码里了。

    void populate() {
        populate(mCurItem);
    }

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;

        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }
        ...

        mAdapter.startUpdate(this);
        //mOffscreenPagLimit的默认值为1
        //除了当前界面,缓存的界面为前后mOffscreenPagLimit个
        final int pageLimit = mOffscreenPageLimit;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);

        ...

        // Locate the currently focused item or add it if needed.
        //mItems代表已经加载在内存中的Page View的信息,这里查看mCurItem所指的页面是否已经被加载。
        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;
            }
        }
        //若mCurItem所指的页面没有被加载,那么创建它,代码放在后面
        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }

        //下面这段就开始更新mItems也就是缓存页面的内容
        //记住我们检查的pos的范围是 mCurItem-1 -> startPos以及 mCurItem+1 -> endPos
        //对应有一个itemIndex,检查mItems中的内容,可能涉及添加和删除,
        //范围是curIndex-1 -> 0 以及 curIndex+1 -> mItems.size()
        //最终我们更新完的mItems保存的是从startPos~endPos的页面信息。


        if (curItem != null) {
            //左边界权重
            float extraWidthLeft = 0.f;
            //从左一开始的下标(相对于当前目标页面而言,下同)
            int itemIndex = curIndex - 1;
            //从左一开始的已经缓存的页面信息
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            //ViewPager(显示)宽度
            final int clientWidth = getClientWidth();
            //左一页面边界权重
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;

            //下面的这个for,意思是更新左边缓存的Page

            //从左一开始向左遍历
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
              //如果缓存超过1个页面,而且当前检查的pos比左边界startPos小(这个if可以最后看,开始时不会进入)
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    //如果此时缓存页面信息没有了,处理结束
                    if (ii == null) {
                        break;
                    }
                    //如果当前检查的pos和页面的position相同且页面不在滑动
                    if (pos == ii.position && !ii.scrolling) {
                        //移除这个页面信息
                        mItems.remove(itemIndex);
                       //Adapter要销毁Item(不同的Adapter都有销毁视图的处理)
                        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;
                    }
                } 
              //如果检查的pos存在页面信息
                else if (ii != null && pos == ii.position) {
                    //增加extraWidthLeft
                    extraWidthLeft += ii.widthFactor;
                    //继续向左取缓存的页面信息
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } 
              //如果检查的pos不存在页面信息,但需要缓存这个页面信息
                else {
                    //创建页面信息,
                  //注意到这里第一个参数指的是ItemInfo.position,
                  //第二个参数指的是mItems中插入的位置
                    ii = addNewItem(pos, itemIndex + 1);
                  //增加extraWidthLeft
                    extraWidthLeft += ii.widthFactor;
                    //既然添加了一个页面信息,当前页面信息下标加1
                    curIndex++;
                  //继续取这个未命中的页面信息
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }

          /*
            右边类似,不再赘述
          */
            ...
            //更新所有ItemInfo中的偏移量
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

        ...

        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
        mAdapter.finishUpdate(this);

        // Check width measurement of current pages and drawing sort order.
        // Update LayoutParams as needed.

        //为缓存的页面更新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();

        ....
    }

利用Adapter的instantiateItem() 并添加到mItems中。

    ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        ii.object = mAdapter.instantiateItem(this, position);
        ii.widthFactor = mAdapter.getPageWidth(position);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }

onLayout()

也就是说onMeasure()中不仅测量了Decor View 和Page View,还对需要缓存的Page View的信息做了保存。进入到onLayout()方法,就是对Decor View 和 Page View进行布局了,这里和所有的ViewGroup一样,ViewPager需要对它的子View进行位置的确定,布局的细节就不说了(我也没看),但是我们得关注一下Page View的布局,注意到if判断中有一句(ii = infoForChild(child)) != null,也就是说只有缓存的Page View才会被布局! ,ViewGroup并没有将所有子View绘制出来,只绘制了展示的和缓存的Page。

@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;

        // First pass - decor views. We need to do this in two passes so that
        // we have the proper offsets for non-decor views later.
      //对Decor View布局
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                int childLeft = 0;
                int childTop = 0;
                if (lp.isDecor) {
                    ......
                }
            }
        }

        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;

                    ...
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                }
            }
        }
        ...
    }

子View何时被添加?

之后的onDraw()执行完之后,ViewPager就可以显示在屏幕上了。但是阅读完源码,我们始终没有看到ViewPager的子View是何时添加的(addView()),populate()中也只是对需要缓存的View的信息做了保存。

这里要分两种情况,第一种情况是子View是非Fragment的情况,一般我们会在Adapter的instantiateItem() 中主动调用addView() 把子View添加到container中,而这个方法的调用又是在ViewPager的onMeasure()的populate()中,populate()添加需要缓存的子View,然后又会请求重绘(addView()中),这也是为什么ViewPager的onMeasure()会被调用多次的原因。

第二种情况是子View是Fragment的情况,严谨点说应该是子View是Fragment的View的情况,我们经常用的关于Fragment的Adapter有两种,FragmentPagerAdapter和FragmentStatePagerAdapter。这一块内容太多,可以单独写一篇了,这里只看一下Fragment的View到底是如何添加到ViewPager中的。如果是在Activity中的ViewPager,我们一般会这样写:

mViewPager.setAdapter(new MyAdapter(getSupportFragmentManager(), mFragments));

getSupportFragmentManager() 通过FragmentController获得FragmentManagerImpl对象(不熟悉的同学可以看一下Fragment源码解析),也就是说FragmentPagerAdapter中维护着Activity(或者Fragment)的FragmentManagerImpl,用它来管理Fragment。

public FragmentPagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
}

因为ViewPager#populate()中会调用到instantiateItem() ,接着我们看到instantiateItem() ,它返回的是一个Fragment对象,也就是说ViewPager的ItemInfo.Object持有的是Fragment实例,ItemInfo持有的是页面的信息。

我们接着看,会在mFragmentManager寻找是否存在对应名字的Fragment。我们假设第一次初始化,那么当然是没有,接着到了很关键的地方,mCurTransaction(事务)调用add方法,第一个参数传入的是container.getId(),这个container是什么?在ViewPager的addNewItem() 中我们清楚的看到是ViewPager本身。

熟悉Fragment事务的同学都知道,这里的add相当于把Fragment的交给了FragmentManagerImpl,之后会对Fragment的状态进行升级同步,在状态同步的过程中,Fragment的View会被创建并且调用container的addView()添加到container中,那么现在我们清楚了,ViewPager利用Activity(或Fragment)的FragmentManagerImpl管理Fragment的状态,把自己作为添加Fragment视图的container(相当于我们平常使用Fragment时的布局中的FrameLayout)。

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // Do we already have this fragment?
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position);
            if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
        }
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

我们总结一下,缘起ViewPager#onMeasure(),测量完Decor View之后就会进入populate(),这个方法里添加(更新)需要缓存的页面信息(ItemInfo)时会调用Adapter的instantiateItem(),在这个方法中会给ViewPager分配子View(同步或者异步),接着就是测量Page View。

到这里ViewPager的显示过程结束了,我脑海里已经有了一个ViewPager大致的框架,下一篇想看一看Viewpager滑动相关的内容,并且对FragmentPagerAdapter和FragmentStatePagerAdapter两个Adapter做相关知识的了解。

 类似资料: