ViewPager源码解析(一):onMeasure、onLayout、populate

最近想撸一个垂直方向的VerticalViewPager,如果想要把它做到屌,那自然是要参考下现有我们的ViewPager实现。
该篇从ViewPager的measure与layout着手,解读ViewPager如何来实现自身已经childView的测量与布局。

onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //直接设置ViewPager自身的大小,这里可以看出ViewPager设置wrap_content时不起作用
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));

        //mGutterSize是在事件分发时用来判断是否拖拽的一个阈值,影响是否拦截事件。
        final int measuredWidth = getMeasuredWidth();
        final int maxGutterSize = measuredWidth / 10;
        mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

        // 设置完宽高后计算出ViewPager实际可用显示的宽高
        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        /**
         * 以下为测量DecorView的过程
         * 这里的 DecorView是ViewPager内部定义的注解
         * ViewPager还可以通过xml布局添加item
         *     <ViewPager>
         *         <DecorView/>
         *     </ViewPager>
         * 所以DecorView是用来表示在xml布局中添加的子view,在xml布局中添加子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();
                if (lp != null && lp.isDecor) {
                    //这里省略DecorView的具体测量过程
                    //…………
                    
                    //ViewPager实际可用显示的宽高减去DecorView已占用的宽高
                    if (consumeVertical) {
                        childHeightSize -= child.getMeasuredHeight();
                    } else if (consumeHorizontal) {
                        childWidthSize -= child.getMeasuredWidth();
                    }
                }
            }
        }

        //计算去除decorView占位后的宽高测量模式(用于adapter中添加的ChildView)
        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

        /**
         * populate()方法是ViewPager的一个核心方法了,它里面有矫正当前ViewPager上需要加载的ItemInfo,
         * 下面会详细分析【ItemInfo】^①和【populate()】^③
         */
        mInLayout = true;
        populate();
        mInLayout = false;

        /* *
         * 测量非DecorView的子View(通过Adapter添加的ChildView)
         * 上面通过populate()已经矫正过需要加载的Item,
         * 所以下面的循环的getChildCount并不会等于DecorView的数量+Adapter.getCount()
         * (实际情况是: getChildCount() = DecorView数量+mItems.size(),
         *   mItem中存放当前ViewPager需要加载的ItemInfo,populate()后会得到最新的mItems)
         */
        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);
                }

                //这里的LayoutParams为【ViewPager.LayoutParams】^②,下面会详细分析
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                //这个lp == null严重怀疑是个笔误,应该是lp != null
                //同时插一点lp.widthFactor,可以看到该值会直接决定ViewPager中ChildView的显示宽度。
                //可以通过复写Adapter.getPageWidth()方法返回值改变该值。
                if (lp == null || !lp.isDecor) {
                    final int widthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                    child.measure(widthSpec, mChildHeightMeasureSpec);
                }
            }
        }
    }

总结一下onMeasure()的整体流程:
1、设置ViewPager自身的大小;
2、算出可用大小后为每个子DecorView测量;
3、通过populate()方法调整ViewPager当前需要加载的itemInfo
4、为当前ViewPager加载的所有非DecorView测量。

接下来分析onMeasure预留的3个重要对象或方法:
①:ItemInfo
ItemInfo用来保存当前ViewPager需要加载的子View的相关信息。

static class ItemInfo {
        Object object;//childView,该值为Adapter.instantiateItem()的返回值
        int position;//childView在viewPager中的位置
        boolean scrolling;//是否在滚动
        float widthFactor;//宽度的占比,可以通过复写Adapter.getPageWidth()方法返回值改变该值,默认返回1。
        float offset;//页面的偏移,用来决定layout时childView的位置
    }

②:ViewPager.LayoutParams
ViewPager.LayoutParams中主要关心以下几个重要字段。

         //是否为DecorView
        public boolean isDecor;

        // childView宽度与ViewPager宽度的比值(实际是可以超过1的)
        float widthFactor = 0.f;

        // 与Adapter中position相对应,DecorView不用关心该字段
        int position;

        // childView在mItems中的位置
        int childIndex;

③:populate()
populate()方法较长,逻辑代码较多,不过耐心点阅读难度也不高。

void populate(int newCurrentItem) {
       /**
        *省略部分代码
        *……
        */


        /**
         * 这里mOffscreenPageLimit决定缓存数量,
         * 可以通过setOffscreenPageLimit()方法来该表该值
         * mItems最多只会缓存1(mCurItem)+2*mOffscreenPageLimit个item,
         * mCurItem左右不足mOffscreenPageLimit个item时则达不到最大数量。
         * 以下starPos与endPos分别表示mItems中缓存的起始与结束position
         */
        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);

        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                /**
                 * 知识点,系统提供了根据id获取id名字的方法(之前傻傻反射去获取)
                 */
                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());
        }

        /**
         * 在mItems中找到当前的ItemInfo
         * 这里需要区分mCurItem和curIndex
         * mCurItem表示当前item对应所有item的position
         * curIndex表示当前itemInfo对应items中的位置
         */
        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;
            }
        }

        /**
         * 如果当前未找到对应的ItemInfo,
         * addNewItem()方法会调用Adapter.instantiateItem()方法往ViewPager中添加对应mCurItem的childView,
         * 同时根据Adapter.instantiateItem()的返回值创建对应的ItemInfo并添加到mItems
         */
        if (curItem == null && N > 0) {
            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.
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            //获取当前ItemInfo的左边的ItemInfo li,下面循环从这里开始
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            //获取到viewPager真实可用宽度
            final int clientWidth = getClientWidth();

            /**
             * leftWidthNeeded表示左边打到回收条件的宽度比例的阈值,
             * 以下处理三种场景
             * 场景1:当左边ItemInfo的extraWidthLeft超过leftWidthNeeded,
             *      且对应的位置不在上面计算的startPos-endPos范围内时将从mItems中移除(如果存在)
             *      同时调用Adapter.destroyItem()通知Adapter回收itemInfo对应的view
             * 场景2:在显示范围内且存在,继续下一次循环
             * 场景3:在显示范围内且不存在,则通过addNewItem()方法调用Adapter.instantiateItem()方法
             *      往ViewPager中添加对应mCurItem的childView,
             *      同时根据Adapter.instantiateItem()的返回值创建对应的ItemInfo并添加到mItems
             */
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;

            /**
             * 遍历mCurItem左边的ItemInfo
             */
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                //场景1
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        //移除缓存时回调 mAdapter.destroyItem
                        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;
                    }
                    //场景2
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    //场景3
                } 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;
                    }
                }
            }

            /**
             * 这里在整理好缓存items后,计算每个items里的{@link ItemInfo#offset}偏移量
             */
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

        /**
         * 这里是知识点,adapter会讲当前的{@link ItemInfo#object}通过setPrimaryItem回调给adapter,
         * 这个curItem.object就是Adapter中instantiateItem(ViewGroup container, int position)方法的返回值,一般都会返回我们添加的chilView
         * 所以在我们需要获取ViewPager当前的childView时,我们可以在Adapter中可以复写setPrimaryItem方法将curItem.object保存起来,
         */
        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);

        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) {
                // 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;
                        }
                    }
                }
            }
        }
    }

populate()代码稍微有些长,但是核心逻辑也不难理解,它实现了ViewPager的添加childView和删除childView的功能,大致流程为:
1、根据mOffscreenPageLimit计算mItems将要储存ViewPager中startPos~endPos对应的ItemInfo;
2、从mItems中获取当前mCurItem对应的ItemInfo,若没有则通过addNewItem()方法调用Adapter.instantiateItem()为ViewPager添加mCurItem位置的childView。同时创建该位置对应的ItemInfo并添加到mItems中;
3、循环遍历当前item左边的所有item,若不在leftWidthNeeded与startPos-endPos决定范围内,则从mItems中删除,同时调用Adapter.destroyItem()方法来通知Adapter回收itemInfo对应的view。若在该范围内且不存在时则通过addNewItem()方法调用Adapter.instantiateItem()方法 往ViewPager中添加对应mCurItem的childView,同时根据Adapter.instantiateItem()的返回值创建对应的ItemInfo并添加到mItems;
4、循环遍历当前item右边的所有item,逻辑与左循环基本一致;
5、通过以上流程整理好需要加载的mItems后,计算mItems里的每个ItemInfo的偏移量,用来决定layout时的位置
6、最后将以上更新的ItemInfo中的内容更新到对应childView的LayoutParams中

populate()方法决定了ViewPager添加childView以及删除childView,在整个ViewPager源码中多个方法中调用,有:onMeasure()、setAdapter()、onInterceptTouchEvent()、setAdapter()、setPageTransformer()、setOffscreenPageLimit()、smoothScrollTo()等多个涉及到页面发生变化的方法中调用,由此可见它在ViewPager中时非常重要的一个方法,所以好好理解它还是非常有必要的。

onLayout()

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //……略略略……

        // 首先对子DecorView进行layout
        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) {
                   //……略略略……

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

        //对Adapter中添加的childView进行layout
        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) {
                       //……略略略……

                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                }
            }
        }
        mTopPageBounds = paddingTop;
        mBottomPageBounds = height - paddingBottom;
        mDecorChildCount = decorCount;

        //首次layout需要将viewPager滚动到对应的mCurItem位置
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
        mFirstLayout = false;
    }

可以看到onLayout()方法还是比较简单的,通过循环DecorView与非DecorView(Adapter中添加的childView)结合他们对应的LayoutParams,对所有childView进行layout。

到这里ViewPager的onMeasure与onLayout就分析完了,重要的事情最后在提醒一次整个流程的重点就在populate()方法的流程,populate()也是整个ViewPager的一个核心方法,还是要耐心将它读完的。
到这里ViewPager的onMeasure、onLayout、populate就已经分析完了,下一篇《ViewPager源码解析(二):setAdapter,notifyDataSetChanged》
会进一步分析ViewPager的数据绑定与刷新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351