这是Android源代码分析第二篇,第一篇写的太烂了,真的是烂,烂到我自己都不想去改。
所以以后看源码啊,还得看继承关系比较浅的……不然分分钟看死你。接下来是正文。
本次文中所提到源码出处均基于support-v4-22.2.1-source.jar。在这之后的支持库源码已经发生变动,以下所述可能和新版源码中有所不同。
我们在使用Fragment+ViewPager来实现滑动页卡的需求时,避不开一个类,就是FragmentPageAdapter
(出自android.support.v4.app
扩展包),这个类用来管理ViewPager
中的Fragment
,其中有一个我们可以在很多其他地方都能找到的方法notifyDataSetChanged
,不管你是习惯使用ListView
+BaseAdapter
,还是RecyclerView
+RecyclerView.Adapter
,都有这个熟悉的方法来帮助你在更新集合数据之后来刷新界面。接下来我们就来看看FragmentPageAdapter
是如何来做这件事情的。
FragmentPageAdapter
继承自PagerAdapter
,方法notifyDataSetChanged
也是在PagerAdapter
中定义的。在FragmentPagAdapter
类中没有关于这个方法的声明,说明这个方法在调用的时候完全出自PagerAdpater
。所以我们的标题也可以改成PageAdapter#notifyDataSetChanged
做了什么。。。接下来我们来看看这个类。
package android.support.v4.view;
public abstract class PagerAdapter {
...
private DataSetObservable mObservable = new DataSetObservable();
public void notifyDataSetChanged() {
mObservable.notifyChanged();
}
public void registerDataSetObserver(DataSetObserver observer) {
mObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mObservable.unregisterObserver(observer);
}
...
}
值得注意的是,PagerAdapter这个抽象类定义了一个成员变量mObservable
,类型为DataSetObervable。我们来看看这个类的一些相关情况。
package android.database;
public class DataSetObservable extends Observable<DataSetObserver> {
public void notifyChanged() {
synchronized(mObservers) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
...
}
package android.database;
import java.util.ArrayList;
public abstract class Observable<T> {
protected final ArrayList<T> mObservers = new ArrayList<T>();
public void registerObserver(T observer) {
//方法体省略
}
public void unregisterObserver(T observer) {
//方法体省略
}
public void unregisterAll() {
//方法体省略
}
}
package android.database;
public abstract class DataSetObserver {
public void onChanged() {
// Do nothing
}
public void onInvalidated() {
// Do nothing
}
}
DataSetObervable
继承自Observable
,同时将父类的泛型定义明确为一个抽象类DataSetObserver
,看名字很是接近,如果有同学熟悉RxJava肯定能明白其中的联系。DataSetObervable
内部维护了一个元素类型为DataSetObserver
的ArrayList
,DataSetObserver
则有两个方法onchange
和onInvalidated
可供子类去重写。ArrayList
的出现使得我们也认识到了一点,就是一个Adapter
可以被多个ViewPager
去绑定,不然这个list的意义在哪呢?
我们回到PagerAdapter
这个类,当调用notifyDataSetChanged时,调用了DataSetObserver
的notifyChanged
方法,而在这个方法的定义中又取出ArrayList
所有的DataSetObserver去执行onChange
方法。而PagerAdapter
中管理mObservable
有registerDataSetObserver
和unregisterDataSetObserver
两个方法,这个两个方法标记为公开,从名字就能看出来这两个方法分别是将observer
添加和移除到ArrayList
中,所以,我们现在去ViewPager
类中寻找,是否调用了这两个方法。
我们在ViewPager类里面又发现一个很像上面描述的成员变量
private PagerObserver mObserver;
private class PagerObserver extends DataSetObserver {
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
这个成员变量连带着定义出现在了ViewPager
类中,这个是一个私有的内部类,当ViewPager#PagerObserver
调用DataSetObserver#onChange()
方法时,内部只调用了一行,便是ViewPager#dataSetChanged()方法。这个方法十分复杂,我们稍后再看,我们先来找一找mObserver
是如何和我们的PagerAdapter建立关联的。
public void setAdapter(PagerAdapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mObserver);
mAdapter.startUpdate(this);
//省略
}
//省略
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.registerDataSetObserver(mObserver);
//省略
}
}
果不其然,在ViewPager
的setAdapter(PagerAdapter adapter)
方法中,我们发现了一些东西。首先如果ViewPager
已经绑定了Adapter
的话,首先移除之前的mObserver
,然后在重新绑定了新的mObserver
。
所以目前我们可以总结一下,调用FragmentPageAdapter#notifyDataSetChanged
实际上调用了ViewPager#dataSetChanged()
方法。同时在这个方法里面,我们也需要关注另外一个方法,那就是PagerAdapter#getItemPosition(Object object)
方法
/**
* Called when the host view is attempting to determine if an item's position
* has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
* item has not changed or {@link #POSITION_NONE} if the item is no longer present
* in the adapter.
*
* <p>The default implementation assumes that items will never
* change position and always returns {@link #POSITION_UNCHANGED}.
*
* @param object Object representing an item, previously returned by a call to
* {@link #instantiateItem(View, int)}.
* @return object's new position index from [0, {@link #getCount()}),
* {@link #POSITION_UNCHANGED} if the object's position has not changed,
* or {@link #POSITION_NONE} if the item is no longer present.
*/
public int getItemPosition(Object object) {
return POSITION_UNCHANGED;
}
根据API的解释,大体的意思就是当item的位置发生改变时,在主视图尝试更新的时候会调用这个方法,这里的传入参数Object就是在PagerAdapter#instantiateItem(ViewGroup container, int position)
方法中返回的单个fragment,如果返回POSITION_UNCHANGED
表示参数Object的位置没有发生变化,这也是默认的返回值,如果该Object(fragment)不再出现时,应当返回POSITION_NONE
。
所以我们的每一个fragment都应该维护一个int值来表示当前的位置,在完成业务逻辑之后准备调用notifyDataSetChanged
之前,将不再出现的fragment该int值更新为POSITION_NONE,同时对于不更新和位置变化的fragment,也要将其更新为其他的逻辑。
接下来我们来看看最关键的方法,更新的具体操作,dataSetChanged()
。
void dataSetChanged() {
final int adapterCount = mAdapter.getCount();//当前适配器数据数量
mExpectedAdapterCount = adapterCount;
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
mItems.size() < adapterCount;
//需要填充的标记(如果当前item数目小于ViewPager的缓存区大小,且新的数据大于之前集合)
int newCurrItem = mCurItem;//记录当前位置下标
boolean isUpdating = false;
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
//此处应当返回PagerAdapter的getItemPosition值,默认为PagerAdapter#POSITION_UNCHANGED
final int newPos = mAdapter.getItemPosition(ii.object);
//如果为默认的POSITION_UNCHANGED值,直接跳出到下一次for循环
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
//如果为POSITION_NONE,移除这个item的内容
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) {
//如果是当前显示的item,更新其下标为getItemPosition的返回值
if (ii.position == mCurItem) {
// Our current item changed position. Follow it.
newCurrItem = newPos;
}
ii.position = newPos;
needPopulate = true;
}
}
/**
* needPopulate为true的情况:
* 1:存在某个item的返回值为PagerAdapter.POSITION_NONE;
* 2:存在某个item的getItemPosition数值和之前初始化保存item下标不同;
*/
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();
}
}
在程序的第10行,for循环遍历了当前的item列表,逐一取出getItemPosition
的返回值,
1. 如果是POSITION_UNCHANGED
的话,直接continue。
2. 如果为POSITION_NONE
,则对item进行PagerAdapter#destroyItem
来销毁这个item。而且如果是当前显示的item,needPopulate
置为true。
3. 如果返回的值不是上述的两个,而且还和之前的值也不一样的话,将下标更新为新的值。needPopulate
置为true。
有的同学就要问了,如果我想捣乱呢?我在覆写FragmentAdapter的时候将getItemPosition的返回值全部设置为POSITION_NONE
,就像下面这样。
@Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
如果按照这个逻辑,那岂不是所有的item都被销毁了?整个ViewPager都没有内容了?
我想你还是太年轻,在dataSetChanged()
方法中,存在一个比较重要的boolean 类型的变量needPopulate
,当这个变量为true的时候,将刷新布局,同时也使用setCurrentItemInternal
方法来刷新当前位置的item内容。而在setCurrentItemInternal
方法中,又调用了一个超级无比复杂的方法populate
,这个方法将会根据你PagerAdapter的instantiateItem
和getCount
方法来重新实例化item,所以。除非你在instantiateItem
和getCount
一起做手脚,否则只是靠getItemPosition
方法来变更item是不现实的。
所以总结一下,needPopulate
将在以下情况为true。
- 存在某个Item的getItemPosition
方法返回了POSITION_NONE
。
- 存在某个Item的getItemPosition
方法返回了与之前下标不同的的值。
由于populate
方法实在太长,而且逻辑很复杂,难度也很大,本文不再分析这个方法,我们需要知道的时,如果你需要通过notifyDataSetChanged
方法来更新ViewPager,请在调用notifyDataSetChanged
方法前也更新PagerAdapter的instantiateItem
和getCount
方法(FragmentPageAdapter
与其对应的是getItem
和getCount
方法)。
@Override
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;
}
@Override
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);
}
上面的代码出自
android.support.v4.app.FragmentPagerAdapter
,可以看见在instantiateItem
方法中在当前FragmentManager中找不到指定Fragment时,调用了getItem
方法来获得一个新的Fragment,而在Fragment存在的时候只是attach了这个Fragment,同时在destroyItem
方法中也并未销毁指定的Fragment,只是detach
了这个Fragment。
如果你只是想更新其中某一个item或者其他item的内容,期望重新初始化一下相应的Fragment(刷新数据,重新载入等等),你可以将需要刷新的item的getItemPosition
方法返回POSITION_NONE
,这样的话系统会认为你不需要这个item而首先destroyItem
(源码中实际上只是通过事务栈来detach
了这个Fragment),然后又会因为getCount
方法和getItem
并没有发生改变而重新将这个Fragment加入指定的item中,达到了刷新的效果。