美文网首页源码原理知识点
view系列源码分析之三大常用控件之viewpager源码分析

view系列源码分析之三大常用控件之viewpager源码分析

作者: 暴走的小青春 | 来源:发表于2020-03-01 23:36 被阅读0次

    viewpager作为android官方的控件,一般情况下都是viewpager+fragment的模式,用过viewpage的人都知道它里面坑比较多,下面带着问题来看下viewpager。
    1.viewpager是如何滑动的
    2.viewpager支持刷新么
    3.viewpager的缓存机制
    首先先分析下viewpager里的onMeasure方法

    @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.
            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.
             */
            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) {
                        final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                        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;
                            }
                        }
                        final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                        final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                        child.measure(widthSpec, heightSpec);
    
                        if (consumeVertical) {
                            childHeightSize -= child.getMeasuredHeight();
                        } else if (consumeHorizontal) {
                            childWidthSize -= child.getMeasuredWidth();
                        }
                    }
                }
            }
    
            mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
            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.
            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);
                    }
                }
            }
        }
    

    重点一:我们知道一般在viewgroup的onMeaure中的职责便是确定自己的宽高,然后测量子view的宽高,最后在根据子view的宽高做点自适应的操作,而viewpager一开始就确定了宽高,这也导致了无法做到自适应的效果(官方可能觉得没必要)
    重点二:viewpager的测量里,主要还是他的populate方法,也是和缓存相关的一个方法,可以说是viewpager的预布局,其layout方法的位置参照与这个预布局的值
    进入populate看下

     void populate(int newCurrentItem) {
            ItemInfo oldCurInfo = null;
            if (mCurItem != newCurrentItem) {
                oldCurInfo = infoForPosition(mCurItem);
                mCurItem = newCurrentItem;
            }
    
            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);
            final int N = mAdapter.getCount();
            final int endPos = Math.min(N - 1, mCurItem + pageLimit);
    
            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.
            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;
                }
            }
    
            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 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                final int clientWidth = getClientWidth();
                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);
            }
    
            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.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.
            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;
                            }
                        }
                    }
                }
            }
        }
    

    可以说代码是非常的多,首先看下其处理缓存的逻辑

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

    这也就是mOffscreenPageLimit的作用,可以说是从startPos到endPos各缓存mOffscreenPageLimit的页数,比如这个值是2,那viewpager最多缓存的页数就是2+2+1就是5页
    然后会给当前的item新建view

     if (curItem == null && N > 0) {
                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;
        }
    

    也就是构建一个ItemInfo对象,下面的两个if语句就是用来纠正viewpager的item的位置的,比如原来viewpager里的item是 0 1 2 然后往左滑动,viewpager中存在的item是 1 2 3 当然要添加一个位置是3的item,也要移除第0个item,所以会回调destroyItem方法,也就是把view从viewpager中移除

      mAdapter.destroyItem(this, pos, ii.object);
    

    如果你用的适配器是原生的pageradapter的话,一定要注意内存的释放,当然一般fragmentpageradapter和frgmentstatepageradater已经做出了相应的移除操作,具体的差别下篇文章分析
    然后回调setPrimaryItem方法

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

    我们所熟知的fragmentpageradapter中fragment是否显示和隐藏
    就在这方法中被设置的,这也是为何第一次fragment的setvisiblehint方法被调用的时候onCreate方法没被创建了,因为在

     mAdapter.finishUpdate(this);
    

    中,fragment才用handler提交事务也就是回调oncreate方法

    //FragmentPagerAdapter
      @SuppressWarnings("ReferenceEquality")
        @Override
        public void setPrimaryItem(ViewGroup container, int position, Object object) {
            Fragment fragment = (Fragment)object;
            if (fragment != mCurrentPrimaryItem) {
                if (mCurrentPrimaryItem != null) {
                    mCurrentPrimaryItem.setMenuVisibility(false);
                    mCurrentPrimaryItem.setUserVisibleHint(false);
                }
                if (fragment != null) {
                    fragment.setMenuVisibility(true);
                    fragment.setUserVisibleHint(true);
                }
                mCurrentPrimaryItem = fragment;
            }
        }
    
     @Override
        public void finishUpdate(ViewGroup container) {
            if (mCurTransaction != null) {
                mCurTransaction.commitNowAllowingStateLoss();
                mCurTransaction = null;
            }
        }
    

    可以简单看下,然后回到populate方法中,可以说这方法已经把view加到了viewpager中,并更新了其缓存,设置了每个view不同的ItemInfo对象,里面保存了位置的信息,所以在看onLayout时

    @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.
            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 hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                        final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                        switch (hgrav) {
                            default:
                                childLeft = paddingLeft;
                                break;
                            case Gravity.LEFT:
                                childLeft = paddingLeft;
                                paddingLeft += child.getMeasuredWidth();
                                break;
                            case Gravity.CENTER_HORIZONTAL:
                                childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
                                        paddingLeft);
                                break;
                            case Gravity.RIGHT:
                                childLeft = width - paddingRight - child.getMeasuredWidth();
                                paddingRight += child.getMeasuredWidth();
                                break;
                        }
                        switch (vgrav) {
                            default:
                                childTop = paddingTop;
                                break;
                            case Gravity.TOP:
                                childTop = paddingTop;
                                paddingTop += child.getMeasuredHeight();
                                break;
                            case Gravity.CENTER_VERTICAL:
                                childTop = Math.max((height - child.getMeasuredHeight()) / 2,
                                        paddingTop);
                                break;
                            case Gravity.BOTTOM:
                                childTop = height - paddingBottom - child.getMeasuredHeight();
                                paddingBottom += child.getMeasuredHeight();
                                break;
                        }
                        childLeft += scrollX;
                        child.layout(childLeft, childTop,
                                childLeft + child.getMeasuredWidth(),
                                childTop + child.getMeasuredHeight());
                        decorCount++;
                    }
                }
            }
    
            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;
            mDecorChildCount = decorCount;
    
            if (mFirstLayout) {
                scrollToItem(mCurItem, false, 0, false);
            }
            mFirstLayout = false;
        }
    

    我们很容易就看到child.layout方法依靠的是loff字段也就是ItemInfo中的offset偏移量
    在看完onMeasure和onLayout后,很自然的就想到了viewpager滑动的原理,当然就是scroller,其事件分发的机制和滑动的原理都和scrollview一样,所以这边就不在多说了,最后来看下其更新的数据的情况
    这里的viewpager用了观察者的模式,最终数据的更新会调用datasetchanged方法,来看下此方法

    void dataSetChanged() {
            // This method only gets called if our observer is attached, so mAdapter is non-null.
    
            final int adapterCount = mAdapter.getCount();
            mExpectedAdapterCount = adapterCount;
            boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
                    && mItems.size() < adapterCount;
            int newCurrItem = mCurItem;
    
            boolean isUpdating = false;
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                final int newPos = mAdapter.getItemPosition(ii.object);
    
                if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                    continue;
                }
    
                if (newPos == PagerAdapter.POSITION_NONE) {
                    mItems.remove(i);
                    i--;
    
                    if (!isUpdating) {
                        mAdapter.startUpdate(this);
                        isUpdating = true;
                    }
    
                    mAdapter.destroyItem(this, ii.position, ii.object);
                    needPopulate = true;
    
                    if (mCurItem == ii.position) {
                        // Keep the current item in the valid range
                        newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                        needPopulate = true;
                    }
                    continue;
                }
    
                if (ii.position != newPos) {
                    if (ii.position == mCurItem) {
                        // Our current item changed position. Follow it.
                        newCurrItem = newPos;
                    }
    
                    ii.position = newPos;
                    needPopulate = true;
                }
            }
    
            if (isUpdating) {
                mAdapter.finishUpdate(this);
            }
    
            Collections.sort(mItems, COMPARATOR);
    
            if (needPopulate) {
                // Reset our known page widths; populate will recompute them.
                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) {
                        lp.widthFactor = 0.f;
                    }
                }
    
                setCurrentItemInternal(newCurrItem, false, true);
                requestLayout();
            }
        }
    
    

    我们可以看到默认的getItemPosition方法返回的是POSITION_UNCHANGED这里就直接跳过去了,我们重写getItemPosition方法后,改成了POSITION_NONE就会发现所谓的更新数据就是把所有的view都在viewpager上移除,然后记录下mCurItem,然后调用setCurrentItemInternal,内部也就是populate方法进行了添加而已,可以说viewpager的机制相对于recycleview还是有点差别的,其更新其实也要频繁的添加和移除,效率上不是很高,所以谷歌退出了基于recycleview的viewpager2来优化此问题

    相关文章

      网友评论

        本文标题:view系列源码分析之三大常用控件之viewpager源码分析

        本文链接:https://www.haomeiwen.com/subject/qtuonctx.html