引言

最近在工作中由于需要客制化系统的关系,接触到了很多ViewPager相关的UI,发现很多底层原生的界面也还是依然采用ViewPager+Fragment的布局方式,事实上这依然还是主流,微信5.0到6.0等还是采用这种布局方法,所以还是有必要总结下ViewPager的相关知识,增强下记忆和理解,毕竟伴随着经验的提升即使再去阅读同一本书也会有不一样的体会。

一、ViewPager

1、概述

ViewPager继承自ViewGroup,是左右两个屏幕平滑地切换的一个容器,容器里呈现的视图由对应的Adapter决定,和其他标准的AdapterView类似。简而言之就是我们通过Adapter把View放到ViewPager里,动动手指我们就可以实现左右滑动互相切换View了。另外ViewPager的更新不是直接由ViewPager本身去完成的,而是通过观察者去调用PagerAdapter的notifyDataSetChanged等相关方法去完成界面更新工作。

2、重要的成员

成员说明
public ViewPager (Context context)
public ViewPager (Context context, AttributeSet attrs)
interface ViewPager.OnPageChangeListener内部接口当切换不同Page时触发对应回调方法
void addView(View child, int index, ViewGroup.LayoutParams params)添加View到ViewPager里
boolean dispatchKeyEvent(KeyEvent event)派发键盘事件到下一个视图
ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
PagerAdapter getAdapter()获取对应的适配器
int getCurrentItem()返回当前page的index
int getOffscreenPageLimit()Returns the number of pages that will be retained to either side of the current page in the view hierarchy in an idle state.
int getPageMargin()返回Page之间的外间距
boolean onTouchEvent(MotionEvent ev)重写这个方法可以处理Touch事件
void removeView(View view)注意不能在 draw(android.graphics.Canvas), onDraw(android.graphics.Canvas), dispatchDraw(android.graphics.Canvas)或其他相关方法调用
void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer)当the scroll position改变时触发,可以用于设置自定义的动画效果,即只要实现PageTransformer接口和其唯一的方法transformPage(View view, float position)
void setOnPageChangeListener(ViewPager.OnPageChangeListener listener)设置Page切换事件监听
void setPageMarginDrawable(int resId)
void setAdapter(PagerAdapter adapter)设置Adapter

二、重要的PagerAdapter

PagerAdapter其实是一个抽象类封装了一些接口、回调和功能方法,在实际开发过程中我们往往是使用它及其子类——FragmentPagerAdapterFragmentStatePagerAdapter的子类来填充到ViewPager中,通俗点来说,ViewPager只是个空皮囊,只有填充了PagerAdapter才能发挥其作用,正所谓巧妇也难为无米之炊,PagerAdapter就是这里的米,有了米才能各显神通做出各种各样的饭。
这里写图片描述

1、实现PagerAdapter时必须要重写的四个成员方法

ViewPager不是直接与View关联的,而是通过一个key对象,通过这个key对象我们可以追踪并且唯一识别指定的page在PagerAdapter的位置。

  • Object instantiateItem(ViewGroup container, int position)——实例化item,PagerAdapter将选择将这个对象填充到在当前ViewPager里。

  • void destroyItem(ViewGroup container, int position, Object object)——将item从指定的位置移出容器

  • int getCount()——获取item的总数,一般都是获取集合的size

  • isViewFromObject(View,Object)——判断容器里的View是否与一个key值相关联,比如说例如Fragment+ViewPager的处理,每个页面都由它是对应的Fragment来呈现,但ViewPager并不是直接与View关联,而是关联一个key。我们实现这个方法也很简单谷歌推荐我们只需一句——return view == object;

public class ViewPagerAdapter extends PagerAdapter {  
    private List<View> mList;  
    public ViewPagerAdapter(List<View> list) {  
        this.mList = list;  
    }  
    @Override  
    public int getCount() {  
        if (mList != null && mList.size() > 0) {  
            return mList.size();  
        } else {  
            return 0;  
        }  
    }  
    @Override  
    public boolean isViewFromObject(View view, Object object) {  
        return view == object;  
    }  
    @Override  
    public void destroyItem(ViewGroup container, int position, Object object) {  
        container.removeView((View) object);  
    }  
    @Override  
    public Object instantiateItem(ViewGroup container, int position) {  
        container.addView(mList.get(position));  
        return mList.get(position);  
    }  
    @Override  
    public int getItemPosition(Object object) {  
        return POSITION_NONE;  
    }  
}

2、PagerAdapter重要的成员方法

PagerAdapter的工作过程其实很简单,当ViewPager里的内容要改变的时候,就调用startUpdate方法来完成,随即也会多次调用instantiateItem或者destroyItem方法,finishUpdate最后被调用来完成更新工作。finishUpdate最终会返回需要添加到容器里的View和对应的 key对象(通过instantiateItem完成);而一些传递到destroyItem的则被移除。

成员方法说明
void finishUpdate(ViewGroup container)当完成page的更新工作时被调用
int getItemPosition(Object object)但需要确定item位置是否改变时被调用,默认实现永远返回-1
void notifyDataSetChanged()但需要去更新的时候我们可以去调用这个方法完成
void registerDataSetObserver(DataSetObserver observer)注册观察者
void unregisterDataSetObserver(DataSetObserver observer)取消注册
void restoreState(Parcelable state, ClassLoader loader)
Parcelable saveState()

3、FragmentPagerAdapter

FragmentPagerAdapter继承自PagerAdapter,主要的成员方法功能都是大同小异的,这个适配器主要就是用于快速实现Fragment在ViewPager里面进行滑动切换的,所以,如果我们想实现Fragment的左右滑动,优先选择ViewPager+FragmentPagerAdapter模式,因为FragmentPagerAdapter拥有自己的缓存策略,当配合ViewPager配合使用时,会缓存当前Fragment以及左边一个、右边一个(一共三个Fragment对象)假设有三个Fragment,那么在ViewPager初始化之后,3个fragment都会加载完成,中间的Fragment在整个生命周期里面只会加载一次,当最左边的Fragment处于显示状态,最右边的Fragment由于超出缓存范围,会被销毁,当再次滑到中间的Fragment的时候,最右边的Fragment会被再次初始化。所以比较适合用户制作较少页面切换的tab界面(最多3个),FragmentPagerAdapter会对我们浏览过Fragment进行缓存,保存这些界面的临时状态,这样当我们左右滑动的时候,界面切换更加的流畅。但是,这样也会增加程序占用的内存。那么3个以上的话,谷歌推荐移步到FragmentStatePagerAdapter

3.1、当我们使用FragmentPagerAdapter的时候,它的宿主ViewPager**必须设置对应的id,同时必须实现两个方法和构造方法**。

  • getCount()——返回的是ViewPager页面的数量,即Fragement的数量。

  • getItem(int position)——返回的是要显示的fragment对象。

//实现一个FragmentPagerAdapter最少只需要实现getCount和getItem方法即可
FragmentPagerAdapter fragmentadapter = new FragmentPagerAdapter(  
             getSupportFragmentManager()) {  
         @Override  
         public int getCount() {  
             return fragments.size();  //getCount()返回的是ViewPager页面的数量,多少个Fragement对应多少个页面
         }  
         @Override  
         public Fragment getItem(int position) {  
             return fragments.get(position);//返回的是要显示的fragment对象。
         }  
      }  
   };  

4、FragmentStatePagerAdapter

当然除了FragmentPagerAdapter之外,还有一个类也是专门用于快速实现多个(3个以上)Fragment在ViewPager里面进行适配并滑动切换的,即FragmentStatePagerAdapterFragmentStatePagerAdapter也直接继承自PagerAdapter的,其工作方式和ListView十分相似的。当Fragment对用户不可见的时候,整个Fragment会被销毁并且保存Fragment的保存状态。基于这样的特性,FragmentStatePagerAdapter比FragmentPagerAdapter更适合用于很多界面之间的转换,而且消耗更少的内存资源。

3.1、当我们使用FragmentStatePagerAdapter的时候,它的宿主ViewPager必须设置对应的id,同时必须实现两个方法

  • getCount()——返回的是ViewPager页面的数量,即Fragement的数量。

  • getItem(int position)——返回的是要显示的fragment对象。

5、关于使用中发现,在删除或者修改数据的时候,PagerAdapter无法像BaseAdapter那样仅通过notifyDataSetChanged方法通知刷新View。

通常情况下调用notifyDataSetChanged方法会让ViewPager通过Adapter的getItemPosition方法查询一遍所有child view,如果所有child view位置均为POSITION_NONE,表示所有的child view都不存在,ViewPager会调用destroyItem方法销毁,并且重新生成,从而加大系统开销,并在一些复杂情况下导致逻辑问题。特别是对于只是希望更新child view内容的时候,但造成了完全不必要的开销,

  • 列表内容如果是针对于child view比较简单的情况(例如仅有TextView、ImageView等,没有ListView等展示数据的情况),可以重写PagerAdapter的方法getItemPosition
@Override    
public int getItemPosition(Object object) {    
    return POSITION_NONE;    
}
  • 但复杂的情况则需要根据自己的需求来实现notifyDataSetChanged的功能,比如,在仅需要对某个View内容进行更新时,在instantiateItem()时,用View.setTag方法加入标志,在需要更新信息时,通过findViewWithTag的方法找到对应的View进行局部更新。

6、FragmentPagerAdapter和FragmentStatePagerAdapter

FragmentPagerAdapter适合在较少Fragment滑动切换的界面使用的,划过的fragment会保存在内存中,尽管已经划过。而FragmentStatePagerAdapter和ListView有点类似,只会保存当前界面,以及下一个界面和上一个界面(如果有),最多保存3个,其他会被销毁掉。最后要注意的是FragmentStatePagerAdapter可能不经意间会造成内存未正常回收,严重导致内存溢出,比如图片资源没有释放,资源引用问题。(之前在网上看到过EditText由于保存焦点导致Fragment未被释放而导致OOM,设置edtText.saveEanble(false)就可以解决此问题)。最后PagerAdapter都只是数据集,数据集的改变只能发生在主线程里并且以必须调用notifyDataSetChanged() 来通知完成更新。

小结

这篇主要是简单描述了下ViewPager的主要功能以及PagerAdapter的角色和一些简略的原理,由于篇幅问题,下一篇再结合实例来总结下。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐