一、概念
ViewPager是android扩展包v4包中的类,这个类可以让用户左右切换当前的View。ViewPager类直接继承了ViewGroup类,所以它是一个容器类,可以在其中添加其他的View类。
ViewPager类需要一个PagerAdapter适配器类给它提供数据。ViewPager经常和Fragment一起使用,并且提供了专门的FragmentPagerAdapter和FragmentStatePagerAdapter类供Fragment中的ViewPager使用。
二、适配器
PagerAdapter
public class AdapterViewpager extends PagerAdapter {
private List mViewList;
public AdapterViewpager(List mViewList) {
this.mViewList = mViewList;
}
@Override
public int getCount() {//必须实现
return mViewList.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {//必须实现
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {//必须实现,实例化
container.addView(mViewList.get(position));
return mViewList.get(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {//必须实现,销毁
container.removeView(mViewList.get(position));
}
}
instantiateItem():
instantiateItem()做了两件事,将当前视图添加到container中,并返回当前View。也就是说,instantiateItem()的功能是创建指定位置的页面视图,并且适配器有责任增加即将创建的View视图添加到这里给定的container中,它的返回值代表新增视图页面的Object(Key),这里没必要非要返回视图本身,也可以返回可以代表当前页面的任意值,只要可以与你增加的View一一对应即可,比如position变量也可以做为Key。
isViewFromObject():
该方法用来判断instantiateItem()所返回来的Key与一个页面视图是否是代表的同一个视图(即它俩是否是对应的,对应的表示同一个 View),如果对应的是同一个View返回true,否则返回false。
destroyItem():
该方法的功能是移除一个给定位置的页面,适配器有责任从容器中删除这个视图,这是为了确保在finishUpdate(viewGroup)返回时视图能够被移除。
FragmentPagerAdapter
public class ViewPagerAdapter extends FragmentPagerAdapter {
private ArrayList mCategoryList;
private Context mContext;
public ViewPagerAdapter(FragmentManager fm, Context context) {
super(fm);
mContext = context;
mCategoryList = DataCacheHelper.getInstance().getPagerChildCategories();
}
public void onCategorysChange(String key) {
if (key != null) {
mCategoryList = DataCache.getInstance().getChildCategorys(key);
notifyDataSetChanged();
} else {
DLog.e("onCategorysChange() Error!");
}
}
@Override
public Fragment getItem(int position) {// 必须实现
return ViewPagerFragment.newInstance(mContext, mCategoryList.get(position));
}
@Override
public int getCount() {// 必须实现
if (mCategoryList != null) {
return mCategoryList.size();
} else {
return 0;
}
}
@Override
public CharSequence getPageTitle(int position) {// 选择性实现
return mCategoryList.get(position).getName();
}
public String getFragmentTag(int viewPagerId, int fragmentPosition) {
return "android:switcher:" + viewPagerId + ":" + fragmentPosition;
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
FragmentStatePagerAdapter
实现同FragmentPagerAdapter。
三、生命周期
情况1:使用FragmentPagerAdapter、FragmentPagerStateAdapter不设置setOffscreenPageLimit
左右滑动页面,每次只缓存下一个页面和上一个页面。间隔的点击tab,如从位于tab1的时候直接选择tab3或tab4,tab1将会被销毁。
两者的区别在于:使用FragmentPagerAdapter的话Fragment不会detach,而使用FragmentPagerStateAdapter的话Fragment会detach。因此FragmentPagerAdapter适用于页面比较少的情况,而FragmentStatePagerAdapter适用于页面比较多的情况。
生命周期如下:
BottomTabFragment1 setUserVisibleHint false
BottomTabFragment2 setUserVisibleHint false
BottomTabFragment1 setUserVisibleHint true
BottomTabFragment1 onCreateView
BottomTabFragment1 onActivityCreated
BottomTabFragment1 onResume
BottomTabFragment2 onCreateView
BottomTabFragment2 onActivityCreated
BottomTabFragment2 onResume
//滑动到 Tab 2
BottomTabFragment3 setUserVisibleHint false
BottomTabFragment1 setUserVisibleHint false
BottomTabFragment2 setUserVisibleHint true
BottomTabFragment3 onCreateView
BottomTabFragment3 onActivityCreated
BottomTabFragment3 onResume
//跳过 Tab3 直接选择 Tab4
BottomTabFragment4 setUserVisibleHint false
BottomTabFragment2 setUserVisibleHint false
BottomTabFragment4 setUserVisibleHint true
BottomTabFragment4 onCreateView
BottomTabFragment4 onActivityCreated
BottomTabFragment4 onResume
BottomTabFragment2 onPause
BottomTabFragment2 onDestroyView
// FragmentPagerStateAdapter 会走一下两个生命周期方法
BottomTabFragment2 onDestroy
BottomTabFragment2 onDetach
BottomTabFragment1 onPause
BottomTabFragment1 onDestroyView
// FragmentPagerStateAdapter 会走一下两个生命周期方法
BottomTabFragment1 onDestroy
BottomTabFragment1 onDetach
// 用户回到桌面 再回到当前 APP 打开其他页面当前页面的生命周期也是这样的
BottomTabFragment3 onPause
BottomTabFragment4 onPause
BottomTabFragment3 onStop
BottomTabFragment4 onStop
BottomTabFragment3 onResume
BottomTabFragment4 onResume
情况2:使用FragmentPagerAdapter、FragmentPagerStateAdapter设置setOffscreenPageLimit为tab总数
创建ViewPager的时候所有页面都将创建完成,生命周期走到onResume。间隔的点击tab,如从位于tab1的时候直接选择tab3或tab4,tab1不会被销毁。
FragmentPagerAdapter和FragmentPagerStateAdapter没有区别。
生命周期如下:
BottomTabFragment1 setUserVisibleHint false
BottomTabFragment2 setUserVisibleHint false
BottomTabFragment3 setUserVisibleHint false
BottomTabFragment4 setUserVisibleHint false
BottomTabFragment1 setUserVisibleHint true
BottomTabFragment1 onCreateView
BottomTabFragment1 onActivityCreated
BottomTabFragment1 onResume
BottomTabFragment2 onCreateView
BottomTabFragment2 onActivityCreated
BottomTabFragment3 onCreateView
BottomTabFragment3 onActivityCreated
BottomTabFragment4 onCreateView
BottomTabFragment4 onActivityCreated
BottomTabFragment2 onResume
BottomTabFragment3 onResume
BottomTabFragment4 onResume
//选择 Tab2
BottomTabFragment1 setUserVisibleHint false
BottomTabFragment2 setUserVisibleHint true
//跳过 Tab3 直接选择 Tab4
BottomTabFragment2 setUserVisibleHint false
BottomTabFragment4 setUserVisibleHint true
// 用户回到桌面 再回到当前 APP 打开其他页面当前页面的生命周期也是这样的
BottomTabFragment1 onPause
BottomTabFragment2 onPause
BottomTabFragment3 onPause
BottomTabFragment4 onPause
BottomTabFragment1 onResume
BottomTabFragment2 onResume
BottomTabFragment3 onResume
BottomTabFragment4 onResume
可以看出第一次执行setUserVisibleHint(boolean isVisibleToUser)除了可见的Fragment外都为false,还可以看出除了这一点不同以外,所有的Fragment都走到了生命周期onResume阶段,而选择相邻tab的时候已经初始化完成的Fragment并不再重新走生命周期方法,只是setUserVisibleHint(boolean isVisibleToUser)为true,当用户进入其他页面的时候所有ViewPager缓存的Fragment都会调用onPause生命周期函数,当再次回到当前页面的时候都会调用onResume。该情况可以实现懒加载。
懒加载方案如下:
1.对于Fragment可见状态的判断需要设置两个标志位,Fragment View创建完成的标志位isViewCreated和Fragment第一次创建的标志位mIsFirstVisible。
2.为了获得Fragment不可见的状态,和再次回到可见状态的判断,我们还需要增加一个currentVisibleState标志位,该标志位在onResume中和onPause中结合getUserVisibleHint的返回值来决定是否应该回调可见和不可见状态函数。
代码如下:
public abstract class LazyLoadBaseFragment extends BaseLifeCircleFragment {
protected View rootView = null;
private boolean mIsFirstVisible = true;
private boolean isViewCreated = false;
private boolean currentVisibleState = false;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//走这里分发可见状态情况有两种,1. 已缓存的 Fragment 被展示的时候 2. 当前 Fragment 由可见变成不可见的状态时
// 对于默认 tab 和 间隔 checked tab 需要等到 isViewCreated = true 后才可以通过此通知用户可见,
// 这种情况下第一次可见不是在这里通知 因为 isViewCreated = false 成立,可见状态在 onActivityCreated 中分发
// 对于非默认 tab,View 创建完成 isViewCreated = true 成立,走这里分发可见状态,mIsFirstVisible 此时还为 false 所以第一次可见状态也将通过这里分发
if (isViewCreated){
if (isVisibleToUser && !currentVisibleState) {
dispatchUserVisibleHint(true);
}else if (!isVisibleToUser && currentVisibleState){
dispatchUserVisibleHint(false);
}
}
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 将 View 创建完成标志位设为 true
isViewCreated = true;
// 默认 Tab getUserVisibleHint() = true !isHidden() = true
// 对于非默认 tab 或者非默认显示的 Fragment 在该生命周期中只做了 isViewCreated 标志位设置 可见状态将不会在这里分发
if (!isHidden() && getUserVisibleHint()){
dispatchUserVisibleHint(true);
}
}
/**
* 统一处理 显示隐藏 做两件事
* 设置当前 Fragment 可见状态 负责在对应的状态调用第一次可见和可见状态,不可见状态函数
* @param visible
*/
private void dispatchUserVisibleHint(boolean visible) {
currentVisibleState = visible;
if (visible) {
if (mIsFirstVisible) {
mIsFirstVisible = false;
onFragmentFirstVisible();
}
onFragmentResume();
}else {
onFragmentPause();
}
}
/**
* 该方法与 setUserVisibleHint 对应,调用时机是 show,hide 控制 Fragment 隐藏的时候,
* 注意的是,只有当 Fragment 被创建后再次隐藏显示的时候才会调用,第一次 show 的时候是不会回调的。
*/
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden){
dispatchUserVisibleHint(false);
}else {
dispatchUserVisibleHint(true);
}
}
/**
* 需要再 onResume 中通知用户可见状态的情况是在当前页面再次可见的状态 !mIsFirstVisible 可以保证这一点,
* 而当前页面 Activity 可见时所有缓存的 Fragment 都会回调 onResume
* 所以我们需要区分那个Fragment 位于可见状态
* (!isHidden() && !currentVisibleState && getUserVisibleHint())可条件可以判定哪个 Fragment 位于可见状态
*/
@Override
public void onResume() {
super.onResume();
if (!mIsFirstVisible){
if (!isHidden() && !currentVisibleState && getUserVisibleHint()){
dispatchUserVisibleHint(true);
}
}
}
/**
* 当用户进入其他界面的时候所有的缓存的 Fragment 都会 onPause
* 但是我们想要知道只是当前可见的的 Fragment 不可见状态,
* currentVisibleState && getUserVisibleHint() 能够限定是当前可见的 Fragment
*/
@Override
public void onPause() {
super.onPause();
if (currentVisibleState && getUserVisibleHint()){
dispatchUserVisibleHint(false);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
//当 View 被销毁的时候我们需要重新设置 isViewCreated mIsFirstVisible 的状态
isViewCreated = false;
mIsFirstVisible = true;
}
/**
* 对用户第一次可见
*/
public void onFragmentFirstVisible(){
LogUtils.e(getClass().getSimpleName() + " ");
}
/**
* 对用户可见
*/
public void onFragmentResume(){
LogUtils.e(getClass().getSimpleName() + " 对用户可见");
}
/**
* 对用户不可见
*/
public void onFragmentPause(){
LogUtils.e(getClass().getSimpleName() + " 对用户不可见");
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater,container,savedInstanceState);
if (rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false);
}
initView(rootView);
return rootView;
}
/**
* 返回布局 resId
*
* @return layoutId
*/
protected abstract int getLayoutRes();
/**
* 初始化view
*
* @param rootView
*/
protected abstract void initView(View rootView);
}
log如下:
//默认选中第一 Tab
BottomTabFragment1 setUserVisibleHint false
BottomTabFragment2 setUserVisibleHint false
BottomTabFragment3 setUserVisibleHint false
BottomTabFragment4 setUserVisibleHint false
BottomTabFragment1 setUserVisibleHint true
BottomTabFragment1 onCreateView
BottomTabFragment1 onActivityCreated
BottomTabFragment1 对用户第一次可见
BottomTabFragment1 对用户可见
BottomTabFragment1 onResume
BottomTabFragment2 onCreateView
BottomTabFragment2 onActivityCreated
BottomTabFragment3 onCreateView
BottomTabFragment3 onActivityCreated
BottomTabFragment4 onCreateView
BottomTabFragment4 onActivityCreated
BottomTabFragment2 onResume
BottomTabFragment3 onResume
BottomTabFragment4 onResume
//滑动选中 Tab2
BottomTabFragment1 setUserVisibleHint false
BottomTabFragment1 对用户不可见
BottomTabFragment2 setUserVisibleHint true
BottomTabFragment2 对用户第一次可见
BottomTabFragment2 对用户可见
//间隔选中 Tab4
BottomTabFragment2 setUserVisibleHint false
BottomTabFragment2 对用户不可见
BottomTabFragment4 setUserVisibleHint true
BottomTabFragment4 对用户第一次可见
BottomTabFragment4 对用户可见
// 回退到桌面
BottomTabFragment1 onPause
BottomTabFragment2 onPause
BottomTabFragment3 onPause
BottomTabFragment4 onPause
BottomTabFragment4 对用户不可见
BottomTabFragment1 onStop
BottomTabFragment2 onStop
BottomTabFragment3 onStop
BottomTabFragment4 onStop
// 再次进入 APP
BottomTabFragment1 onResume
BottomTabFragment2 onResume
BottomTabFragment3 onResume
BottomTabFragment4 onResume
BottomTabFragment4 对用户可见
情况3:进入其他页面或者用户按home键回到桌面,当前ViewPager页面变成不见状态
四、刷新问题
现象:
如果想实现ViewPager的数据刷新,在调用PageAdapter的notifyDataSetChanged()会发现并没有效果,往后滑动两页到第三页会发现,除了ViewPager默认缓存的那三页数据没有刷新之外,后面的(第三页之后)数据都是刷新了的,这时再返回第一页会发现第一页的数据也发生变化了。
分析:
Viewpager的刷新过程:在每次调用PagerAdapter的notifyDataSetChanged()方法时,都会激活getItemPosition(Object object)方法,该方法会遍历ViewPager的所有Item(由缓存的Item数量决定,默认为当前页和其左右加起来共3页,这个可以自行设定,但是至少会缓存2页),为每个Item返回一个状态值(POSITION_NONE/POSITION_UNCHANGED),如果是POSITION_NONE,那么该Item会被destroyItem(ViewGroup container, int position, Object object)方法remove掉然后重新加载,如果是POSITION_UNCHANGED就不会重新加载。默认是 POSITION_UNCHANGED,所以如果不重写getItemPosition(Object object)并修改返回值,就无法看到notifyDataSetChanged()的刷新效果。
方案:
复写PagerAdapter的getItemPosition方法,返回POSITION_NONE。
public int getItemPosition(Object object) {
return POSITION_NONE;
}
//注意:要同时重写destroyItem方法
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
// 把 Object 强转为 View,然后将 view 从 ViewGroup 中清除
container.removeView((View) object);
}
但是该方案有个很明显的缺陷,就是会刷新所有的Item,这将导致系统资源的浪费,所以这种方式不适合数据量较大的场景。
可以通过以下方式优化:
在instantiateItem()方法中给每个View添加tag(使用setTag()方法),然后在getItemPosition()方法中通过View.getTag()来判断是否是需要刷新的页面,是就返回POSITION_NONE,否就返回 POSITION_UNCHANGED。
注意:当清空数据源的时候需要返回POSITION_NONE。
if (mDataList != null && mDataList.size()==0) {
return POSITION_NONE;
}
五、禁止过渡动画
当ViewPager绑定了TabLayout控件时,通常都有点击一个Tab实现切换ViewPager的功能,但是如果在切换时不想要那个切换动画,可以通过以下方式:
//第二个参数就是禁止滚动过渡的效果
mViewPager.setCurrentItem(0, false);
六、设置预加载页数
ViewPager默认会预加载左右两页的内容,如果想预加载更多,可以通过以下方式:
mViewPager.setOffscreenPageLimit(2); // 设置缓存view 的个数(实际有3个,缓存2个+正在显示的1个)
七、禁止手势左右切换
自定义View继承自ViewPager,代码如下:
//自定义ViewPager
public class ViewPagerEx extends ViewPager{
private boolean isPagingEnabled = true;
public ViewPagerEx(Context context) {
super(context);
}
public ViewPagerEx(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return this.isPagingEnabled && super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.isPagingEnabled && super.onInterceptTouchEvent(event);
}
public void setPagingEnabled(boolean canScroll) {
this.isPagingEnabled = canScroll;
}
@Override
public void setCurrentItem(int item, boolean smoothScroll) {
super.setCurrentItem(item, smoothScroll);
}
}
//Activity
mViewPager.setPagingEnabled(false); //禁止左右滑动
mViewPager.setPagingEnabled(true); //开启左右滑动