带着疑惑去源码中寻找答案。
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
第二个页面显示 偏移量为offset
第三个页面显示 偏移量为2*offset
后面的就不做分析了,上面的图直观的表示出了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()。希望大家提出修改建议。