ViewPager源码学习——布局篇

本文详细探讨了ViewPager的源码,从onMeasure、onLayout和onDraw三个方面进行解析。在onMeasure阶段,ViewPager设置自身大小并测量所有Item;在populate方法中,更新ItemInfo数组,管理缓存;onLayout则负责布置Decor View和普通View,确保页面定位到第一个Item。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看一点记录一点,虽然网上很多大佬都有记录,但还是想自己学完记录下
ViewPager继承自ViewGroup,先从onMeasure、onLayout、onDraw开始看起

onMeasure

主要做了这几件事:

  1. 对整个ViewPager的大小进行设置。设置的大小为父类传递过来的大小,也就是剩余的空间,对这个不理解的可以看博主另一篇文章Android自定义时间轴
  2. 测量DecorView,并对其设置大小。使用ViewPager.DecorView注释的视图被视为ViewPager“装饰”的一部分。具体可以看博主另一篇文章PagerTitleStrip使用
  3. 测量Adapter的所有View,也就是每个Item,并设置其大小。
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //设置ViewPager的大小,设置的大小为父类传递的剩余空间
        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.
        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        /*
         * 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.
         */
        //开始设置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) {
                    //判断是不是设置了Gravity.LEFT || hgrav == Gravity.RIGHT
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    //判断是不是设置了Gravity.TOP || vgrav == Gravity.BOTTOM
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    int widthMode = MeasureSpec.AT_MOST;
                    int heightMode = MeasureSpec.AT_MOST;
                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                    boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;

                    if (consumeVertical) {
                        widthMode = MeasureSpec.EXACTLY;
                    } else if (consumeHorizontal) {
                        heightMode = MeasureSpec.EXACTLY;
                    }

                    int widthSize = childWidthSize;
                    int heightSize = childHeightSize;
                    if (lp.width != LayoutParams.WRAP_CONTENT) {
                        widthMode = MeasureSpec.EXACTLY;
                        if (lp.width != LayoutParams.MATCH_PARENT) {
                            widthSize = lp.width;
                        }
                    }
                    if (lp.height != LayoutParams.WRAP_CONTENT) {
                        heightMode = MeasureSpec.EXACTLY;
                        if (lp.height != LayoutParams.MATCH_PARENT) {
                            heightSize = lp.height;
                        }
                    }
                    //对DecorView进行设置长和宽
                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                    child.measure(widthSpec, heightSpec);

                    if (consumeVertical) {
                        //ViewPager剩余的高(也就是每个Item的高)是减掉DecorView的高
                        childHeightSize -= child.getMeasuredHeight();
                    } else if (consumeHorizontal) {
                        //ViewPager剩余的宽(也就是每个Item的宽)是减掉DecorView的宽
                        childWidthSize -= child.getMeasuredWidth();
                    }
                }
            }
        }
        //ViewPager宽度剩余的MeasureSpec,没用上
        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
        //ViewPager高度剩余的MeasureSpec
        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

        // Make sure we have created all fragments that we need to have shown.
        mInLayout = true;
        populate();
        mInLayout = false;

        // Page views next.
        //开始设置每个Item的宽高
        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) {
                    //生成每个Item的宽的MeasureSpec
                    final int widthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                    //对每个Item的宽高进行设置
                    child.measure(widthSpec, mChildHeightMeasureSpec);
                }
            }
        }
    }

populate

主要做了这几件事:

  1. 根据传入的当前界面更新mItems数组,该数组存放的是对应的ItemInfo对象
  2. 如果左面超出了缓存限制,则删除左边,如果左边需要缓存却没有缓存,则在左边创建对应的ItemInfo
  3. 如果右面超出了缓存限制,则删除右边,如果右边需要缓存却没有缓存,则在右边创建对应的ItemInfo
  4. 如果当前ViewPager可获得焦点,把焦点传递给子View
//newCurrentItem表示是当前显示的界面(也就是传入的Position)
void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        //如果传入进来的Item不等于当前Item
        if (mCurItem != newCurrentItem) {
            //得到之前的ItemInfo
            oldCurInfo = infoForPosition(mCurItem);
            //更新Position
            mCurItem = newCurrentItem;
        }
        //当mAdapter为null时,如果调用了setPageTransformer,传递了true选项,就会对子View进行排序,优先绘制Decor View
        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        /**
        private void sortChildDrawingOrder() {
                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);
                    }
                    Collections.sort(mDrawingOrderedChildren, sPositionComparator);
                }

            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;
                    }
                }
            }**/

        // 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.
        //TODO 待研究
        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.
        //在ViewPager没有attached之前,不要进行populate.
        if (getWindowToken() == null) {
            return;
        }
        //调用Adapter的startUpdate方法
        mAdapter.startUpdate(this);
        //mOffscreenPageLimit就是我们通过setOffscreenPageLimit设置的参数,默认为1
        final int pageLimit = mOffscreenPageLimit;
        //这个startPos是缓存页面的起始页,如果mCurItem是1,mCurItem - pageLimit为0。如果mCurItem是2,mCurItem - pageLimit为1
        final int startPos = Math.max(0, mCurItem - pageLimit);
        //得到ViewPager的所有Item
        final int N = mAdapter.getCount();
        //这个endPos是缓存页面的终止页,如果mCurItem是1,mCurItem + pageLimit为2,如果mCurItem是2,mCurItem + pageLimit为3
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);
        //从抛出异常的情况来看,应该是用户改变了PagerAdapter的内容,但是没用调用notifyDataSetChanged方法,导致数量不一致了
        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's"
                    + " contents without calling PagerAdapter#notifyDataSetChanged!"
                    + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N
                    + " Pager id: " + resName
                    + " Pager class: " + getClass()
                    + " Problematic adapter: " + mAdapter.getClass());
        }

        // Locate the currently focused item or add it if needed.
        //存放mItems数组中的位置
        int curIndex = -1;
        //mCurItem在mItems里面,就返回对应的ItemInfo,否则,就创建一个ItemInfo放到mItems里面,并赋值给curItem
        ItemInfo curItem = null;
        //遍历mItems存放的ItemInfo对象,找到当前获得焦点的页面,更新ItemInfo,然后跳出
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

        /**
            static class ItemInfo {
                    //调用instantiateItem生成的View
                    Object object;
                    //当前的position
                    int position;
                    boolean scrolling;
                    //在Adapter的getPageWidth方法返回,用于显示当前页面
                    //的比例,最后将会在onLayout方法中通过
                    //childWidth * lp.widthFactor测量当前View的宽度
                    float widthFactor;
                    //这个参数暂时也没咋懂,反正是通过widthFactor + marginOffset = width > 0 ? (float) mPageMargin / width : 0;
                    float offset;
                }
        **/ 
        //如果curItem为空,说明,mItems没有任何东西,或者说当前界面ItemInfo没有保存到mItems里面去
        if (curItem == null && N > 0) {
            //mCurItem是position的位置
            //curIndex是mCurItem所对应的ItemInfo在mItems中的位置
            curItem = addNewItem(mCurItem, curIndex);
            /**
                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;
                    }
            **/
        }

        // 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;
            //mCurItem所对应的ItemInfo在mItems中的上一个位置
            int itemIndex = curIndex - 1;
            //获得焦点View的上一个ItemInfo,没有就为null
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            //得到ViewPager的宽度可用大小
            final int clientWidth = getClientWidth();
            //从下文看,缓存的原理就是缓存当前页面和左右两边的页面(左右两边根据pageLimit计算出来的)
            //假如当前是2,那么pos是1,1是应该缓存的界面,extraWidthLeft += ii.widthFactor extraWidthLeft为1
            //pos--
            //这时如果mItems左面还有view,那么应该是需要移除的view
            //extraWidthLeft >= leftWidthNeeded && pos < startPos成立
            //进行移除
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem .widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            //从当前界面左边开始,不包括自身
            //此方法主要是删除左边不在缓存范围内和如果左边没有缓存,在左边创建缓存
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                //pos < startPos找到需要移出去的ItemInfo
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    //如果左边没有界面,跳出循环。  
                    if (ii == null) {
                        break;
                    }
                    //
                    if (pos == ii.position && !ii.scrolling) {
                        //将需要移除的itemInfo从mItems中移除
                        mItems.remove(itemIndex);
                        //调用Adapter的destroyItem方法,
                        //pos是position
                        //ii.object是移除的View
                        //如果Adapter不重写会报异常throw new UnsupportedOperationException("Required method destroyItem was not overridden");
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
                        }

                        itemIndex--;
                        //mItems里面位置变化
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                }
                //当前位置是需要缓存的界面,将左边宽度加上上一个页面的宽度
                 else if (ii != null && pos == ii.position) {
                     //extraWidthLeft += ii.widthFactor 
                    extraWidthLeft += ii.widthFactor;
                    //itemIndex--
                    itemIndex--;
                    //获取mItems上一个的上一个itemInfo
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    //这个位置需要缓存,但是却没有对应ItemInfo,
                    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;
                //此方法主要是删除右边不在缓存范围内和如果右边没有缓存,在右边创建缓存
                //假如当前是1,那么pos是2,2是应该缓存的界面,extraWidthLeft += ii.widthFactor extraWidthLeft为2
                //这时如果mItems右面还有view,那么应该是需要移除的view
                //extraWidthRight >= rightWidthNeeded && pos > endPos成立
                //进行移除
                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);
        }

        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);
            }
        }
        //回调当前setPrimaryItem方法,参数为当前显示的界面
        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.
        //将mItems里面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();
        //如果当前ViewPager是具有焦点的,将焦点分发给子View
        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;
                        }
                    }
                }
            }
        }
    }

onLayout

主要做了这几件事:

  1. 设置Decor View的位置
  2. 设置普通View的位置
  3. 将页面移动到第一个Item位置
@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) {
                    //判断DecorView是不是设置了Gravity.LEFT || hgrav == Gravity.RIGHT
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    //判断DecorView是不是设置了Gravity.TOP || vgrav == Gravity.BOTTOM
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    //因为作者的代码是设置了Gravity.TOP
                    switch (hgrav) {
                        default:
                            //进入default将childLeft设置为paddingLeft
                            childLeft = paddingLeft;
                            break;
                        case Gravity.LEFT:
                            childLeft = paddingLeft;
                            paddingLeft += child.getMeasuredWidth();
                            break;
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
                                    paddingLeft);
                            break;
                        case Gravity.RIGHT:
                            childLeft = width - paddingRight - child.getMeasuredWidth();
                            paddingRight += child.getMeasuredWidth();
                            break;
                    }
                    switch (vgrav) {
                        default:
                            childTop = paddingTop;
                            break;
                        case Gravity.TOP:
                            //childTop设置为paddingTop
                            //paddingTop为DecorView的高
                            childTop = paddingTop;
                            paddingTop += child.getMeasuredHeight();
                            break;
                        case Gravity.CENTER_VERTICAL:
                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,
                                    paddingTop);
                            break;
                        case Gravity.BOTTOM:
                            childTop = height - paddingBottom - child.getMeasuredHeight();
                            paddingBottom += child.getMeasuredHeight();
                            break;
                    }
                    childLeft += scrollX;
                    //确定DecorView的位置
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                    //将改DecorView的计数器加1
                    decorCount++;
                }
            }
        }

        final int childWidth = width - paddingLeft - paddingRight;
        // Page views. Do this once we have the right padding offsets from above.
        //设置非Decor View的位置
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ItemInfo ii;
                //如果子 View不是Decor View 并且子 View保存在mItems中
                if (!lp.isDecor && (ii = infoForChild(child)) != null) {
                    //这个loff是计算的页面的左面位置
                    //第一个View的ii.offset为0,loff为0
                    //第二个View的ii.offset为1.0,loff为childWidth * ii.offset
                    //效果图如下所示
                    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());
                    }
                    //设置普通View的布局
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                }
            }
        }
        //mTopPageBounds全局变量,是普通View的Top(刨除掉Decor View)
        mTopPageBounds = paddingTop;
        //mBottomPageBounds全局变量,是普通View的Bottom(刨除掉Decor View)
        mBottomPageBounds = height - paddingBottom;
        //Decor View的个数
        mDecorChildCount = decorCount;
        //第一次布局,滑动到第一个Item
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
        //将第一次布局标志置为false
        mFirstLayout = false;
    }

onLayout图示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值