ViewPager2离屏加载

解决ViewPager无法禁用预加载难题

Posted by Tristan on April 12, 2021

背景

在开发首页、城市选择页和我的优惠券页面时,都使用了ViewPager组件处理横向页面切换进行分屏展示。ViewPager满足基本的功能可能是OK的,但要说完胜却有不足。比如,1)无法将离屏缓存页参数设置为小于1的数,即不能禁用预加载;2)无法通过Adapter动态更新数据;3)禁止手势滑动翻页后,依然会有页面滚动的迹象。

预加载

ViewPager默认情况下切换到当前页面时,会默认预先加载左右两侧的布局到ViewPager中(尽管两侧的View并不可见),我们称这种情况叫预加载;由于ViewPager对offscreenPageLimit设置了限制,页面的预加载是不可避免。

private static final int DEFAULT_OFFSCREEN_PAGES = 1;

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {//不允许小于1
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
} 

相信ViewPager这样设计是有原因的,但是实际需求中,这样是不满足产品需要的。比如:产品不期望在下一屏View尚不可见时出现曝光埋点。

懒加载

懒加载只会在页面可见时加载数据。既然ViewPager不能禁用预加载,那么大家自然就想到了懒加载,这已然是没有办法的办法。

ViewPager如何实现懒加载

PagerAdapter的入口方法及调用流程代码:

// 输入数据
void populate(int newCurrentItem) {
    // Locate the currently focused item or add it if needed.
    int curIndex = -1;//==========================================================1_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);//===============================1_2
    }

    // 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;//===========================================2_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);//============================2_2
                extraWidthLeft += ii.widthFactor;
                curIndex++;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }

        float extraWidthRight = curItem.widthFactor;
        itemIndex = curIndex + 1;//===============================================3_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);//=============================3_2
                    itemIndex++;
                    extraWidthRight += ii.widthFactor;
                    ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                }
            }
        }

        mAdapter.setPrimaryItem(this, mCurItem, curItem.object);//=================4
    }
    
}

首先会计算当前位置和左右位置预加载的布局,然后调用addNewItem方法加载相应位置的布局。

ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    ii.object = mAdapter.instantiateItem(this, position);//========================5
    ii.widthFactor = mAdapter.getPageWidth(position);
    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    return ii;
}

然后是真正初始化ItemView,ViewPager此时已经创建好了左中右3个Fragment。

  • instantiateItem(ViewGroup container, int position) //初始化ItemView,返回需要添加ItemView
  • destroyItem(iewGroup container, int position, Object object) //销毁ItemView,移除指定的ItemView
@Override
public Object instantiateItem(@NonNull 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);//================6_1
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);//========================================6_2
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));//====================6_3
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

最后才是指定一个Item为当前的主页面。

  • setPrimaryItem(ViewGroup container, int position, Object object) //设置当前页面的主Item
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);//========================隐藏
        }
        fragment.setMenuVisibility(true);
        fragment.setUserVisibleHint(true);//========================================可见
        mCurrentPrimaryItem = fragment;
    }
}

setPrimaryItem(ViewGroup container, int position, Object object),该方法表示当前页面正在显示主要Item,何为主要Item?如果预加载的ItemView已经划入屏幕,当前的PrimaryItem依然不会改变,除非新的ItemView完全划入屏幕,且滑动已经停止才会判断。

ViewPager为了进行布局预加载,首先要通过PagerAdapter调用instantiateItem(ViewGroup container, int position)方法创建ItemView。PagerAdapter搭载Fragment的两个实现类FragmentPagerAdapter和FragmentStatePagerAdapter都是通过此方法方法创建Fragment对象的。很不幸,FragmentPagerAdapter和FragmentStatePagerAdapter一股脑的在instantiateItem()中进行创建且进行add或attach操作,并没有在setPrimaryItem()方法中对Fragment进行操作。

因此,预加载会导致不可见的Fragment一股脑的调用onCreate、onCreateView、onResume等方法,用户唯有通过Fragment.setUserVisibleHint()方法识别当前可见的Fragment,这样基于Fragment的懒加载方案就实现了。懒加载可以控制数据延迟加载,但无法阻止布局提前创建。

ViewPager实现懒加载要借助Fragment的setUserVisibleHint方法,缺此方法不可行。

Fragment中的setUserVisibleHint方法在v1.1.0中已经标记为已过时(@Deprecated)。

ViewPager2

ViewPager2介绍

ViewPager2内部通过挂载一个RecyclerView实现分屏显示,ViewPager2的预加载和缓存基本交由RecyclerView来处理,它设置Adapter其实是给RecyclerView设置Adapter。

public final class ViewPager2 extends ViewGroup {

    public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

    private LinearLayoutManager mLayoutManager;
    RecyclerView mRecyclerView;
    
    private boolean mUserInputEnabled = true;
    
    private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
    
    private void initialize(Context context, AttributeSet attrs) {
        mRecyclerView = new RecyclerViewImpl(context);
        mRecyclerView.setId(ViewCompat.generateViewId());
        mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);

        mLayoutManager = new LinearLayoutManagerImpl(context);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
        setOrientation(context, attrs);

        mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());

        ...
        
        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
    }
    
    public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
        final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
        mAccessibilityProvider.onDetachAdapter(currentAdapter);
        unregisterCurrentItemDataSetTracker(currentAdapter);
        mRecyclerView.setAdapter(adapter);
        mCurrentItem = 0;
        restorePendingState();
        mAccessibilityProvider.onAttachAdapter(adapter);
        registerCurrentItemDataSetTracker(adapter);
    }
}

ViewPager2作为独立的库被发布,引入时只需要在build.gradle添加如下依赖即可:

implementation ‘androidx.viewpager2:viewpager2:1.1.0’

ViewPager2和ViewPager在使用方式上差不多,主要有以下API:

  • setOffscreenPageLimit() 设置屏幕外加载页面数量
  • setAdapter() 设置适配器
  • setUserInputEnabled() 设置是否允许用户输入/触摸
  • registerOnPageChangeCallback() 注册页面改变回调
  • setOrientation() 设置布局方向
  • setPageTransformer() 设置页面滑动时的变换效果

ViewPager2离屏加载

同样,先看下ViewPager2对于离屏加载参数的定义。

/**
 * Value to indicate that the default caching mechanism of RecyclerView should be used instead
 * of explicitly prefetch and retain pages to either side of the current page.
 * @see #setOffscreenPageLimit(int)
 */
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
    if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        throw new IllegalArgumentException(
                "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
    }
    mOffscreenPageLimit = limit;
    // Trigger layout so prefetch happens through getExtraLayoutSize()
    mRecyclerView.requestLayout();
}

注释直译:该值指示应使用RecyclerView的默认缓存机制,而不是显式预取并将页面保留在当前页面的任何一侧。
白话文说:我这个值是给RecyclerView的默认缓存用的,而不是直接用来预加载左右侧的页面的(“显示”代表这里面有隐含)。

private class LinearLayoutManagerImpl extends LinearLayoutManager {
    @Override
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
            @NonNull int[] extraLayoutSpace) {
        int pageLimit = getOffscreenPageLimit();
        if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            // Only do custom prefetching of offscreen pages if requested
            super.calculateExtraLayoutSpace(state, extraLayoutSpace);
            return;
        }
        final int offscreenSpace = getPageSize() * pageLimit;
        extraLayoutSpace[0] = offscreenSpace;
        extraLayoutSpace[1] = offscreenSpace;
    }
}

可以看见,ViewPager2和ViewPager在预加载的实现上有所不同:ViewPager默认预加载值就是1,如果外部设置小于1的话会直接抛出异常;ViewPager2提供了一个负数作为默认值(早期版本这个值是0,当前的版本是-1),然后将该值提供给RecyclerView的LinearLayoutManager处理默认的预加载方案。

OffscreenPageLimit本质上是重写LinearLayoutManager的calculateExtraLayoutSpace方法,该方法是最新的recyclerView包加入的功能。

calculateExtraLayoutSpace方法定义了布局额外的空间,何为布局额外的空间?默认空间等于RecyclerView的宽高空间,定义这个意在可以放大可布局的空间,该方法参数extraLayoutSpace是一个长度为2的int数组,第一条数据接受左边/上边的额外空间,第二条数据接受右边/下边的额外空间,故上诉代码是表明左右/上下各扩大offscreenSpace。

综上代码,OffscreenPageLimit其实就是放大了LinearLayoutManager的布局空间。

布局对比
为了对比两者加载布局的效果,这里准备了LinearLayout同时展示ViewPager和ViewPager2,设置相同的Item布局和数据源,然后用Android布局分析工具抓取两者的布局结构,效果如下: image 从分析结果来看,ViewPager会默认会预布局两侧各一个布局,ViewPager2默认不进行预布局,主要由各自的默认offscreenPageLimit参数决定,ViewPager默认为1且不允许小于1,ViewPager2默认为0。 image 分析运行结果,在设置相同的offscreenPageLimit时,两者都会预布局左右(上下)两者的offscreenPageLimit个ItemView。 从对比结果上来看,ViewPager2的offscreenPageLimit和ViewPager运行结果一样,但是ViewPager2最小offscreenPageLimit可以设置为0。

预加载
上面分析我们已经知道,pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT时,ViewPager2的预加载其实是交给了RecyclerView的LinearLayoutManager来管理的,ViewPager2的预加载即RecyclerView的预加载。 RecyclerView是默认开启预加载的,如果要关闭预加载,可以使用下面的代码:

private boolean mItemPrefetchEnabled = true;

((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);

预加载的开关在LayoutManager上,只需要获取LayoutManager并调用setItemPrefetchEnabled()即可控制开关。

缓存
ViewPager2默认会缓存2条ItemView,而且在最新的RecyclerView中可以自定义缓存Item的个数,方法如下:

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {

    final Recycler mRecycler = new Recycler();

    public void setItemViewCacheSize(int size) {
        mRecycler.setViewCacheSize(size);
    }

    public final class Recycler {

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        static final int DEFAULT_CACHE_SIZE = 2;

        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

        void updateViewCacheSize() {
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                recycleCachedViewAt(i);
            }
        }
    }

}
((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemViewCacheSize(0);

预加载和缓存在View层面没有本质的区别,都是已经准备了布局,但是没有加载到parent视图上;预加载和离屏加载在View层面有本质的区别,离屏加载的View已经添加到parent上。

ViewPager2对Fragment的支持

Fragment生命周期测试

目前,ViewPager2对Fragment的支持只能使用FragmentStateAdapte。默认情况下,ViewPager2开启了预加载,离屏加载是关闭的。

  1. 离屏加载关闭时,经过验证,是否开启预加载,对Fragment的生命周期没有影响,结果如下图: 1
  2. 打开l离屏加载,设置offscreenPageLimit=1时: 2

打印结果解读:

  • 默认情况下,ViewPager2会缓存两条数据,所以滑动到第4页,第1页的Fragment才开始移除;
  • 设置offscreenPageLimit=1时,ViewPager2在第1页会加载两条数据,会把下一页View提前加载进来;以后每滑一页,会加载下一页的数据,直到第5页,会移除第1页的Fragment;第6页会移除第2页的Fragment。

怎样理解offscreenPageLimit对Fragment的影响呢?如果offscreenPageLimit=1,这样ViewPager2最多可以承托3个ItemView,再加上2个缓存的ItemView,就是5个,由于offscreenPageLimit会在ViewPager2两边各放置一个,所以向前最多承载4个,这样很自然就是第5个时候,回收第1个。

小结:使用ViewPager2+Fragment实现懒加载,其实ViewPager2默认的离屏加载就支持了,不需要将offscreenPageLimit设置大于0。新版本Fragment中再也不会监听得到setUserVisibleHint方法的调用,该方法也已被标记为过时。

FragmentStateAdapter源码简单解读

onCreateViewHolder()方法

public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
}
static FragmentViewHolder create(ViewGroup parent) {
    FrameLayout container = new FrameLayout(parent.getContext());
    container.setLayoutParams(
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
    container.setId(ViewCompat.generateViewId());
    container.setSaveEnabled(false);
    return new FragmentViewHolder(container);
}

onCreateViewHolder()创建一个宽高都MATCH_PARENT的FrameLayout,注意这里并不像PagerAdapter是Fragment的rootView。

onBindViewHolder()


public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    final long itemId = holder.getItemId();
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null && boundItemId != itemId) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
    mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
    //保证目标Fragment不为空,意思是可以提前创建
    ensureFragment(position);
    /** Special case when {@link RecyclerView} decides to keep the {@link container}
     * attached to the window, but not to the view hierarchy (i.e. parent is null) */
    final FrameLayout container = holder.getContainer();
    //如果ItemView已经在添加到Window中,且parent不等于null,会触发绑定viewHoder操作;
    if (ViewCompat.isAttachedToWindow(container)) {
        if (container.getParent() != null) {
            throw new IllegalStateException("Design assumption violated.");
        }
        container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if (container.getParent() != null) {
                    container.removeOnLayoutChangeListener(this);
                    //将Fragment和ViewHolder绑定
                    placeFragmentInViewHolder(holder);
                }
            }
        });
    }
    //回收垃圾Fragments
    gcFragments();
} 
  • onBindViewHolder()首先会获取当前position对应的Fragment,这意味着预加载的Fragment对象会提前创建;
  • 如果当前的holder.itemView已经添加到屏幕且已经布局且parent不等于空,就会将Fragment绑定到ViewHodler;
  • 每次调用都会gc一次,主要的避免用户修改数据源造成垃圾对象;

onViewAttachedToWindow()

public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
    placeFragmentInViewHolder(holder);
    gcFragments();
}

onViewAttachedToWindow()方法调用onViewAttachedToWindow将Fragment和hodler绑定。

onViewRecycled()

public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
}

当onViewRecycled()时才会触发Fragment移除。

核心添加操作:

//将Fragment.rootView添加到FrameLayout;
scheduleViewAttach(fragment, container);//将rootI
mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();

//主要是监听onFragmentViewCreated方法,获取rootView然后添加到container
private void scheduleViewAttach(final Fragment fragment, final FrameLayout container) {
    // After a config change, Fragments that were in FragmentManager will be recreated. Since
    // ViewHolder container ids are dynamically generated, we opted to manually handle
    // attaching Fragment views to containers. For consistency, we use the same mechanism for
    // all Fragment views.
    mFragmentManager.registerFragmentLifecycleCallbacks(
            new FragmentManager.FragmentLifecycleCallbacks() {
                @Override
                public void onFragmentViewCreated(@NonNull FragmentManager fm,
                        @NonNull Fragment f, @NonNull View v,
                        @Nullable Bundle savedInstanceState) {
                    if (f == fragment) {
                        fm.unregisterFragmentLifecycleCallbacks(this);
                        addViewToContainer(v, container);
                    }
                }
            }, false);
}

Adapter小结:

  • 目前ViewPager2对Fragment支持只能用FragmentStateAdapter,FragmentStateAdapter在遇到预加载时,只会创建Fragment对象,不会把Fragment真正的加入到布局中,所以自带懒加载效果;
  • FragmentStateAdapter不会一直保留Fragment实例,回收的ItemView也会移除Fragment,所以得做好Fragment重建后恢复数据的准备;
  • FragmentStateAdapter在遇到offscreenPageLimit>0时,处理离屏Fragment和可见Fragment没有什么区别,所以无法通过setUserVisibleHint判断显示与否,这一点得注意。

ViewPager2总结

  1. 高效的复用,利用RecyclerView预加载和缓存机制轻松实现了复用性上的借用。
  2. 自带懒加载,采用更贴合Fragment生命周期的页面管理方式便捷实现了懒加载。
  3. 更新有效的Adapter,解决了notifyDataSetChanged方法失效的诸多问题。

参考文档

RecyclerView缓存机制详解
ViewPager2重大更新,支持offscreenPageLimit
ViewPager2和Fragment可见性及懒加载解决方案
ViewPager2 详细使用
RecyclerView扩展(五) - ViewPager2的源码分析
探索取代ViewPager的ViewPager2
ViewPager2:官方Viewpager升级版来临
ViewPager刷新数据,动态更改adapter的数量
ViewPager数据修改使用notifyDataSetChanged无刷新的问题
ViewPagerHelper