对使用ViewPager的一点理解

大家都用过ViewPager吧😄,使用ViewPager时,需要给它配一个Adapter,通常我们要继承下面三个Adapter来,分别是:

———>最基本的PagerAdapter

——————>继承PagerAdapter的FragmentPagerAdapter

——————>继承PagerAdapter的FragmentStatePagerAdapter

那么这三个Adapter有什么区别,我们应该怎么选择呢?

1.关于PagerAdapter

首先,我们知道如果继承了PagerAdapter,需要我们实现下面两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* Return the number of views available.
*/
public abstract int getCount();


/**
* Create the page for the given position. The adapter is responsible
* for adding the view to the container given here, although it only
* must ensure this is done by the time it returns from
* {@link #finishUpdate(ViewGroup)}.
*
* @param container The containing View in which the page will be shown.
* @param position The page position to be instantiated.
* @return Returns an Object representing the new page. This does not
* need to be a View, but can be some other container of the page.
*/
public Object instantiateItem(ViewGroup container, int position) {
return instantiateItem((View) container, position);
}

第一个是要告诉ViewPager一共有几个页面,第二个则是ViewPager需要加载页面了,你需要根据position创建不同的页面对象,这里通常是View对象。

当然,如果你的ViewPager中内容比较复杂,需要用Fragment来自动管理其生命周期,那么可以使用FragmentPagerAdapter和FragmentStatePagerAdapter中的一种,那么他俩有什么区别呢?

首先我们看一下FragmentPagerAdapter的instantiateItemdestroyItem方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 	@Override
public Object instantiateItem(ViewGroup container, int position) {
...
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));
}
...
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);
}

public long getItemId(int position) {
return position;
}


private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}

可以看出instantiateItem方法在添加Fragment时,会带一个Fragment的名字,其实就是Tag:

1
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

这个Tag是根据一定规则从makeFragmentName中获得的,这里唯一变化的就是id,默认的id可从getItemId(position)中看到,就是当前的position,保证了每个fragment的tag是唯一的。

当ViewPager通过instantiateItem获取Fragment的时候,会先根据之前设置的Tag找一下这个Fragment存不存在:

1
mFragmentManager.findFragmentByTag(name)

根据获取到的Fragment有两种添加方式,如果根据Tag找到了之前存在的Fragment,就attach上去:

1
mCurTransaction.attach(fragment);

如果没找到,那就把新创建的Fragment add上去:

1
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

当不用这个Fragment的时,会调用destroyItem方法,可以看到这里是detach掉了Fragment,并没有销毁。

综上,一个页面只会创建一次,创建时根据当前的position给Fragment设置一个Tag,当不需要时只是把Fragmetn detach掉,并不会销毁,下次需要时通过Tag复用Fragment。

所以,如果你有大量的Fragment要展示,FragmentPagerAdapter会持有每一个Fragment不释放,最终走向OOM。

所以,无论使用PagerAdapter还是FragmentPagerAdapter,有多少页面,就会创建多少页面对象,页面很多的情况下,会非常占用内存,虽然这样,但是它们也有各自的应用场景,例如,App的首次安装的启动引导页面个数是固定的,而且如果比较复杂,通常做成Fragment,这时使用FragmentPagerAdapter较为合适;而如果页面中有需要自动轮播卡片的地方,则可以使用PagerAdapter实现,因为其页面一般都是View,结构简单,数量也是固定的。

那如果我需要做一个可以无限滑动的卡片,就需要用到FragmentStatePagerAdapter了,我们看一下FragmentStatePagerAdapter的instantiateItemdestroyItem方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);

@Override
public Object instantiateItem(ViewGroup container, int position) {
...

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

Fragment fragment = getItem(position);
while (mFragments.size() <= position) {
mFragments.add(null);
}

...
mCurTransaction.add(container.getId(), fragment);

return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
...
mCurTransaction.remove(fragment);
}

可以看到,FragmentStatePagerAdapter在初始化页面时,通过getItem这个抽象方法来获得Fragment,并将其add到容器里,而不用的时候直接remove掉,这样就不会一直占用Fragment,可以实现无限滑动,唯一关心的就是在getItem中创建一个Fragment就可以了。

以上就是我对三种PagerAdapter的简单理解。在使用ViewPager的过程中,除了adapter的选择,还需要到了下面的问题。

2.常见的两个问题小析

1.notifyDataSetChanged()后页面没有刷新的问题

这个问题主要是由于当数据更新时,ViewPager会调用dataSetChanged方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null.

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

...
}

...
}

这个方法会调用adapter的getItemPosition()来获得object的位置情况是否发生变化,如果不发生变化就不更新了,如果而这个方法默认是返回的就是POSITION_UNCHANGED:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 /**
* 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;
}

所以要想每次刷新都让页面更新,需要在adapter中重写getItemPosition方法,并返回POSITION_NONE就可以了:

1
2
3
4
5

@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
2.初始化数据时不会调用OnPageChangeListener

相信大家都遇到过这种情况,就是ViewPager初始化时并不会调用OnPageChangeListener,所以很多时候我们都是手动调用第一页的onPageSelected方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

@Override
public void onPageSelected(int position) {

}

@Override
public void onPageScrollStateChanged(int state) {

}
};

public void OnCreate(){
...
mViewPager.setOnPageChangeListener(onPageChangeListener);
onPageChangeListener.onPageSelected(0); // 手动调用第一页
...
}

但是如果如果你使用的是FragmentPagerAdapter时,会发现在onPageSelected(0)时,会发现Fragment还没有创建成功,这时候会出现NPE(NullPointerException)。所以这时候可以这样写:

1
2
3
4
5
6
mViewPager.post(new Runnable(){
@Override
public void run() {
onPageChangeListener.onPageSelected(0); // 手动调用第一页
}
});

这样就会在viewpager的UI事件队列完成后处理onPageSelected方法的内容,这个时候Fragment已经好了。

但是,虽然这样,讲要进行的工作交给post会打乱同步时序,让要做的事充满了不确定性,我不知道它什么时候能调用onPageSelected方法。而且这样做也存在另外一个问题,当使用notifyDataSetChanged()刷新数据时,还是不会调用当前页的onPageSelected来更新当前页面,所以我更倾向于下面这种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MyPagerAdapter extends FragmentStatePagerAdapter {
...

private Object lastPrimaryItem;

private OnPageSwitchListener onPageSwitchListener;
public MyPagerAdapter(FragmentManager fm, OnPageSwitchListener onPageSwitchListener) {
super(fm);
this.onPageSwitchListener = onPageSwitchListener;
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.finishUpdate(container);
super.setPrimaryItem(container, position, object);
if (object == lastPrimaryItem) {
return; // setPrimaryItem会多次调用,过滤掉多余的调用
}
lastPrimaryItem = object;
MyFragment fragment = (MyFragment)object;
if (onPageSwitchListener != null && fragment != null) {
onPageSwitchListener.onPageSwitch(position, fragment);
}
}

@Override
public void finishUpdate(ViewGroup container) {
// 这里将finishUpdate放到setPrimaryItem之前执行,见上面的方法👆
// 原因是finishUpdate后的Fragment才算正式可以用了
}

public interface OnPageSwitchListener{
void onPageSwitch(int position, MyFragment fragment);
}
}

class MyActivity extends Activity implements MyPagerAdapter.OnPageSwitchListener{

@Override
public void onPageSwitch(int position, MyFragment fragment) {
// Do whatever you like here. :)
}
}

简单说就是,PagerAdapter中有一个重要的方法叫setPrimaryItem,当一个页面显示的时候都会调用这个方法(多次),通过一些简单的处理,可以实现与mViewPager.setOnPageChangeListener()类似的功能,这样就不用每次调用onPageChangeListener.onPageSelected(0)或者使用Post操作了。

以上分析较为肤浅,有问题欢迎指正、补充,谢谢🙏

参考

[1] https://stackoverflow.com/questions/16074058/onpageselected-doesnt-work-for-first-page

[2] https://stackoverflow.com/questions/11794269/onpageselected-isnt-triggered-when-calling-setcurrentitem0

Author

calvinche

Posted on

2019-02-01

Licensed under

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×