ViewPager源码解惑

带着疑惑去源码中寻找答案。

1.ViewPager实现思想。
2.Fragment事务是如何管理的。
3.FragmentStatePagerAdapter与FragmentPagerAdapter对比

学习完ViewPager源码,我可以做什么?
1.定义一个展示Image的PagerAdapter
2.如何使ViewPager做到循环效果

下面开始分析:
首先需要了解

    static class ItemInfo {
        Object object;
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;
    }
private final ArrayList mItems = new ArrayList();
int mCurItem

mCurItem:表示当前页面的位置

mItems:表示已经缓存的页面信息(通常会缓存当前显示页面以前当前页面前后页面,不过缓存页面的数量由mOffscreenPageLimit决定)

ItemInfo:这是用来保存页面信息的
{
Object表示页面展示的内容
position表示该页面的页码
scrolling表示是否正在滚动
widthFactor表示加载的页面占ViewPager所占的比例[0~1](默认返回1)
offset表示页面偏移量
}

最先从setAdapter开始。

 /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.setViewPagerObserver(null);
            mAdapter.startUpdate(this);
            //如果原有adapter就将其完全销毁,调用了destoryItem(),这里mark一下等会剖析destoryItem()源码。
            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.clear();
            removeNonDecorViews();
            mCurItem = 0;
            scrollTo(0, 0);
        }

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

        if (mAdapter != null) {
            if (mObserver == null) {
                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) {
              //这里需要mark一下 核心方法
                populate();
            } else {
                requestLayout();
            }
        }

        if (mAdapterChangeListener != null && oldAdapter != adapter) {
            mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
        }
    }

上一步就是简单的对ViewPager设置Adapter,如果原有Adapter则必须将其内容销毁(具体语法后面对比FragmentStatePagerAdapter与FragmentPagerAdapter时分析)。
现在看一下populate()方法

 void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        if (mCurItem != newCurrentItem) {
          //获取oldItem的信息(猜测以后会销毁的)
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        // 如果填充时我们界面正在滑动就停止填充
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // 与窗口关联之前不填充数据
        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);

        // 表示当前页面在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;
            }
        }
        //如果mItems中没有对应item,则从mAdapter.instantiateItem()中获取对应item数据。
        if (curItem == null && N > 0) {
            //mark一下等会看这个方法
            curItem = addNewItem(mCurItem, curIndex);
        }

        // 这里需要注意了,这里会缓冲当前item的前一个和后一个item。
        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;
           //销毁左边不需要展示的页面(就是除了前一个item),如果左边一个item不在内存中则需要加载到内存
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
              //如果超出了预加载范围并且超过了设定的预加载页面数量并且  
              //存在items(预加载页面缓存)中就destory该页面,这里的设计十分巧妙,  
             // 需要细细的去研读一下。
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
              //因为预加载的内存是连续的,如果当前为null表示也没有多余信息缓存了
                    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;
              //销毁右边不需要展示的页面(就是除了后一个item),如果右边一个item不在内存中则需要加载到内存
                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缓存的只有预加载的几个页面
            //计算当前页面的offset(偏移)
            //mark一下
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

        if (DEBUG) {
            Log.i(TAG, "Current page list:");
            for (int i=0; i

其实这个方法读完大概也能猜到Viewpager是如何管理以及展示各个页面了吧。
下面看一下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);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }

这里就很明显了,调用了PagerAdapter的instantiateItem方法,这个方法是我们用来设置当前页码所需要展示内容的。
下面看一看calculatePageOffsets方法

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;
        // 计算当前item的偏移量
        //根据oldItem计算(这个就自己体会了)
        if (oldCurInfo != null) {
            final int oldCurPosition = oldCurInfo.position;
            // Base offsets off of oldCurInfo.
            if (oldCurPosition < curItem.position) {
                int itemIndex = 0;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                for (int pos = oldCurPosition + 1;
                        pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                    ii = mItems.get(itemIndex);
                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
                        itemIndex++;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos < ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset += mAdapter.getPageWidth(pos) + marginOffset;
                        pos++;
                    }
                    ii.offset = offset;
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
                int itemIndex = mItems.size() - 1;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset;
                for (int pos = oldCurPosition - 1;
                        pos >= curItem.position && itemIndex >= 0; pos--) {
                    ii = mItems.get(itemIndex);
                    while (pos < ii.position && itemIndex > 0) {
                        itemIndex--;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos > ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
                        pos--;
                    }
                    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;
        // 计算前面页面的偏移量(根据当前页面计算)
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = mItems.get(i);
            while (pos > ii.position) {
                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
            }
            offset -= ii.widthFactor + marginOffset;
            ii.offset = offset;
            if (ii.position == 0) mFirstOffset = offset;
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;
        //  计算后面页面的偏移量(根据当前页面计算)
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = mItems.get(i);
            while (pos < ii.position) {
                offset += mAdapter.getPageWidth(pos++) + marginOffset;
            }
            if (ii.position == N - 1) {
                mLastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }

        mNeedCalculatePageOffsets = false;
    }

那么偏移量计算出来有什么用呢?等会分析..
下面看另外一个方法sortChildDrawingOrder

    private void sortChildDrawingOrder() {
        if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
            if (mDrawingOrderedChildren == null) {
                mDrawingOrderedChildren = new ArrayList();
            } else {
                mDrawingOrderedChildren.clear();
            }
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                mDrawingOrderedChildren.add(child);
            }
            //这里是根据View的position对View进行的一个排序,这里为什么要排序呢?先保留疑问
            Collections.sort(mDrawingOrderedChildren, sPositionComparator);
        }
    }

到这里基本把最核心的方法populate分析完。
我们在梳理一下流程:
1.更新items,将items中的内容换成当前展示页面以及预缓存页面(有序)。这里会直接或间接调用PagerAdapter的startUpdate()、instantiateItem()、destroyItem()、setPrimaryItem()、finishUpdate()等等方法,说明ViewPager的扩展性的强大,在这几个方法中我们必须管理好我们的子View。具体可以参考一下FragmentPagerAdapter和FragmentStatePagerAdapter。
2.计算每个items的off(偏移量),这个计算出来有什么作用呢?其实猜都能猜出来肯定绘制onLayout()方法中起作用的,等会分析。
其实该方法主要就是做了这两件事情。
下面我们分析一下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 childWidth = width - paddingLeft - paddingRight;
        // 从这里看到我们的offset派上用场了。
        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) {
                  //这里看出来了我们实际上是利用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());
                    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;
    }

下面就可以顺利的将子View绘制出来了,这就是大概的流程。仔细去研究内部滑动源码或者setCurrentPage源码都可以发现实际上是调用了populate()方法。当我们需要有View更新的时候比如addView()、removeView()都会进行requestLayout()、以及invalidate()。

下面直观的绘制一下流程:
当一个页面显示 偏移量为0


ViewPager源码解惑_第1张图片
第一个页面显示

第二个页面显示 偏移量为offset


ViewPager源码解惑_第2张图片
第二个页面显示.png

第三个页面显示 偏移量为2*offset
ViewPager源码解惑_第3张图片
第三个页面显示第一个页面destory.png

后面的就不做分析了,上面的图直观的表示出了Viewpager对其item的管理。

下面对比一下FragmentStatePagerAdapter和FragmentPagerAdapter
主要对比一下instantiateItem()与destoryItem()以及saveState()。

FragmentStatePagerAdapter

 public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }


     public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

        mCurTransaction.remove(fragment);
    }

FragmentPagerAdapter

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


    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        mCurTransaction.detach((Fragment)object);
    }

通过对比我们可以看出最大的区别就是,在FragmentPagerAdapter中只有将Fragment实例交由FragmentManager管理,那么Fragment将一直由FragmentManager维护,而FragmentStatePagerAdapter中Fragment只会是在预加载中由FragmentManager维护,而destory时会被销毁。

FragmentStatePagerAdapter

@Override
    public Parcelable saveState() {
        Bundle state = null;
        if (mSavedState.size() > 0) {
            state = new Bundle();
            Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
            mSavedState.toArray(fss);
            state.putParcelableArray("states", fss);
        }
        for (int i=0; i keys = bundle.keySet();
            for (String key: keys) {
                if (key.startsWith("f")) {
                    int index = Integer.parseInt(key.substring(1));
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        while (mFragments.size() <= index) {
                            mFragments.add(null);
                        }
                        f.setMenuVisibility(false);
                        mFragments.set(index, f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }

这里可以看出FragmentStatePagerAdapter对fragment的操作日志进行了保存。因为FragmentStatePagerAdapter中Fragment的是否在内存中的信息是由fragments集合维持。所以当我们Activity被销毁和重建时必须对该集合进行状态信息的维持。

通过源码的对比我们可以发现当我们的Fragment数量有限时我们应该使用FragmentPagerAdapter,当我们有很多页面Fragment需要展示的时候则用FragmentStatePagerAdapter。

1.定义一个展示Image的PagerAdapter

下面我们可以封装一个用来显示Image的PagerAdapter。
这是我封装的,仅供参考。

import android.graphics.Bitmap;
import android.support.v4.view.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

/**
 * Created by xiefei on 2016/4/10.
 */
public abstract class ImagePagerAdapter extends PagerAdapter{
    private final String Tag = "ImagePagerAdapter";
    //重新利用destory的ImageView
    private ImageView preImageView;
    public ImagePagerAdapter(){
    }
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        ImageView imageView;
        if(preImageView == null){
            imageView = new ImageView(container.getContext());
        }else
            imageView = preImageView;
        preImageView = null;
        Log.d(Tag,imageView.toString());
        imageView.setImageBitmap(getItem(position));
        container.addView(imageView);
        return imageView;
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        preImageView = (ImageView) object;
        container.removeView(preImageView);
    }

    public abstract Bitmap getItem(int position);
    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }
}

向上面这样封装达到一个优点,就是重用了布局,对需要展示很多的图片信息还是有一定优势的。

2.如何使ViewPager做到循环效果PagerAdapter

下面开始第二点,如何定义一个循环ViewPager
方法一:简单粗暴,直接修改getCount()

方法二:修改源代码,让ViewPager支持循环
因为涉及到会改变populate()方法以及其他方法,我只好重新定义一个ViewPager去更改,这部分我总结好了会更新。现在已完成在github上。
主要更改了populate()、scrollToItem()、determineTargetPage()。希望大家提出修改建议。

你可能感兴趣的:(ViewPager源码解惑)