ViewPager 全面剖析及使用详解

  • Post author:
  • Post category:其他


ViewPager在开发中的使用频率非常的高,所以在此做个总结。主要包括以下几方面:

  • ViewPager的简介和作用
  • ViewPager的适配器
  • ViewPager的翻页动画
  • 简化ViewPager的使用
  • ViewPager结合第三方库实现小圆点指示器效果
  • ViewPager结合design库实现tab切换
  • 基于ViewPager实现广告轮播控件

按照惯例,先上个效果图



demo.gif

基础篇



1.ViewPager的简介和作用



ViewPager是android扩展包v4包中的类,这个类可以让用户左右切换当前的view


1)ViewPager类直接继承了ViewGroup类,所有它是一个容器类,可以在其中添加其他的view类。


2)ViewPager类需要一个PagerAdapter适配器类给它提供数据。


3)ViewPager经常和Fragment一起使用,并且提供了专门的FragmentPagerAdapter和FragmentStatePagerAdapter类供Fragment中的ViewPager使用。


2.ViewPager的适配器



简介中提到了PagerAdapter,和ListView等控件使用一样,需要ViewPager设置PagerAdapter来完成页面和数据的绑定,这个PagerAdapter是一个基类适配器,我们经常用它来实现app引导图,它的子类有FragmentPagerAdapter和FragmentStatePagerAdapter,这两个子类适配器用于和Fragment一起使用,在安卓应用中它们就像listview一样出现的频繁。

实现一个最基本的PagerAdapter,

《必须实现四个方法》

,在代码里有注释

public class AdapterViewpager extends PagerAdapter {
    private List<View> mViewList;

    public AdapterViewpager(List<View> 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));
    }
}

实现一个最基本的FragmentPagerAdapter

public class AdapterFragment extends FragmentPagerAdapter {
    private List<Fragment> mFragments;

    public AdapterFragment(FragmentManager fm, List<Fragment> mFragments) {
        super(fm);
        this.mFragments = mFragments;
    }

    @Override
    public Fragment getItem(int position) {//必须实现
        return mFragments.get(position);
    }

    @Override
    public int getCount() {//必须实现
        return mFragments.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {//选择性实现
        return mFragments.get(position).getClass().getSimpleName();
    }
}

FragmentStatePagerAdapter的实现和FragmentPagerAdapter的实现一样就不在写了

3个适配器的基本实现讲完了是不是很简单,那他们的区别是什么呢?


PagerAdapter是基类适配器是一个通用的ViewPager适配器,相比PagerAdapter,FragmentPagerAdapter和FragmentStatePagerAdapter更专注于每一页是Fragment的情况,而这两个子类适配器使用情况也是有区别的。FragmentPagerAdapter适用于页面比较少的情况,FragmentStatePagerAdapter适用于页面比较多的情况。为什么?简单分析下两个适配器的源码就可以知道了。

  • FragmentStatePagerAdapter

       @Override
      public Object instantiateItem(ViewGroup container, int position) {
          // If we already have this item instantiated, there is nothing
          // to do.  This can happen when we are restoring the entire pager
          // from its saved state, where the fragment manager has already
          // taken care of restoring the fragments we previously had instantiated.
          if (mFragments.size() > position) {
              Fragment f = mFragments.get(position);//fragment被释放后这里得到的null值
              if (f != null) {
                  return f;
              }
          }
    
          if (mCurTransaction == null) {
              mCurTransaction = mFragmentManager.beginTransaction();
          }
    
          Fragment fragment = getItem(position);//fragment被释放后或者是初次进入页面拿到新的Fragment实例
          if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
          if (mSavedState.size() > position) {
              Fragment.SavedState fss = mSavedState.get(position);
              if (fss != null) {
                  fragment.setInitialSavedState(fss);
              }
          }
          while (mFragments.size() <= position) {
              mFragments.add(null);
          }
          fragment.setMenuVisibility(false);
          fragment.setUserVisibleHint(false);
          mFragments.set(position, fragment);
          mCurTransaction.add(container.getId(), fragment);//新的Fragment实例 是add上去的
    
          return fragment;
      }
    
     @Override
      public void destroyItem(ViewGroup container, int position, Object object) {
          Fragment fragment = (Fragment) object;
    
          if (mCurTransaction == null) {
              mCurTransaction = mFragmentManager.beginTransaction();
          }
          if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                  + " v=" + ((Fragment)object).getView());
          while (mSavedState.size() <= position) {
              mSavedState.add(null);
          }
          mSavedState.set(position, fragment.isAdded()
                  ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
          mFragments.set(position, null);//真正释放了fragment实例
    
          mCurTransaction.remove(fragment);
      }
  • FragmentPagerAdapter

      @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);//因为fragment实例没有被真正释放,所以可以直接attach效率高
          } else {
              fragment = getItem(position);//初始化页面的时候拿到fragment的实例
              if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
              mCurTransaction.add(container.getId(), fragment,
                      makeFragmentName(container.getId(), itemId));//add上去
          }
          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);//并没有真正释放fragment对象只是detach
      }

    从源码中我们可以看出FragmentStatePagerAdapter中fragment实例在destroyItem的时候被真正释放,所以FragmentStatePagerAdapter省内存。FragmentPagerAdapter中的fragment实例在destroyItem的时候并没有真正释放fragment对象只是detach,所以FragmentPagerAdapter消耗更多的内存,带来的好处就是效率更高一些。所以得出这样的结论:

    FragmentPagerAdapter适用于页面比较少的情况,FragmentStatePagerAdapter适用于页面比较多的情况,因此不同的场合选择合适的适配器才是正确的做法


3.ViewPager的翻页动画



为ViewPager设置适配器后,就可以正常使用了,接下来我们为ViewPager增加翻页动画,毕竟人的审美会疲劳,加上一些动画交互会提高不少逼格~~,ViewPager提供了

PageTransformer

接口用于实现翻页动画。


官方提供了PageTransformer的实现例子。

  public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        Log.d("DepthPageTransformer", view.getTag() + " , " + position + "");
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1);
            view.setTranslationX(0);
            view.setScaleX(1);
            view.setScaleY(1);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}
  public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;

    @SuppressLint("NewApi")
    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        Log.e("TAG", view + " , " + position + "");

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 1) 
        { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE)
                    / (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

实现翻页动画的关键就是重写transformPage方法,方法里有两个参数view和position,理解这两个参数非常重要。假设有三个页面view1,view2,view3从左至右在viewPager中显示

  • 往左滑动时:view1,view2,view3的position都是不断变小的。

                   view1的position: 0 → -1 → 负无穷大
                   view2的position: 1  0 → -1 
                   view3的position: 1 → 0
  • 往右滑动时:view1,view2,view3的position都是不断变大的。

                   view1的position: -1 → 0 
                   view2的position: -1 → 0  1 
                   view3的position: 0 → 1→ 正无穷大

    当position是正负无穷大时view就离开屏幕视野了。因此最核心的控制逻辑是在[-1,0]和(0,1]这两个区间,通过设置透明度,平移,旋转,缩放等动画组合可以实现各式各样的页面变化效果。


4.简化ViewPager的使用

不会偷懒的程序猿不是好程序员

这里只是做了最简单的封装,可以根据需要调整

  • PagerAdapter简化

    public class QuickPageAdapter<T extends View> extends PagerAdapter {
      private List<T> mList;
    
      public QuickPageAdapter(List<T> mList) {
          this.mList = mList;
      }
    
      @Override
      public int getCount() {
          return mList.size();
      }
    
      @Override
      public boolean isViewFromObject(View view, Object object) {
          return object == view;
      }
    
      @Override
      public Object instantiateItem(ViewGroup container, int position) {
          container.addView(mList.get(position));
          return mList.get(position);
      }
    
      @Override
      public void destroyItem(ViewGroup container, int position, Object object) {
          container.removeView(mList.get(position));
      }
    }

    使用它,这样不用每次都写个适配器


    List<View> views = new ArrayList<>();





    mViewPager.setAdapter(new QuickPageAdapter<View>(views));

  • FragmentPagerAdapter简化

    public class QuickFragmentPageAdapter<T extends Fragment> extends FragmentPagerAdapter {
      private List<T> mList;
      private String[] mStrings;
    
      /**
       * @param fm
       * @param list
       * @param titles PageTitles
       */
      public QuickFragmentPageAdapter(FragmentManager fm, List<T> list, String[] titles) {
          super(fm);
          mList = list;
          mStrings = titles;
      }
    
      @Override
      public Fragment getItem(int position) {
          return mList.get(position);
      }
    
      @Override
      public int getCount() {
          return mList.size();
      }
    
      @Override
      public CharSequence getPageTitle(int position) {
          return mStrings == null ? super.getPageTitle(position) : mStrings[position];
      }
    }

    FragmentStatePagerAdapter封装类似FragmentPagerAdapter就不写了,基本使用讲完了。


5.补充一个知识点



mViewPager.setOffscreenPageLimit()//这个方法是用来控制fragment不重新走生命周期的个数的,打个比方一共4个fragment页面,如果mViewPager.setOffscreenPageLimit(3),那么所有的fragment都只走一次生命周期,如果是mViewPager.setOffscreenPageLimit(2),那么其中有一个fragment会在切换的时候重新走一遍生命周期,FragmentStatePagerAdapter和FragmentPagerAdapter都是这样,但是FragmentPagerAdapter设置setOffscreenPageLimit不影响fragment缓存的个数,而FragmentStatePagerAdapter缓存的fragment实例个数就是setOffscreenPageLimit设置的值+1。另外setOffscreenPageLimit的缺省值是1,设置0是无效的会被强制赋值成1。

   private static final int DEFAULT_OFFSCREEN_PAGES = 1;
   public void setOffscreenPageLimit(int limit) {
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
                    DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;//强制赋值为1
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

提高篇



1.ViewPager结合第三方库实现小圆点指示器效果




https://github.com/ongakuer/CircleIndicator



screenshot.gif



使用看官方文档很简单。

看一下实现思路

    public void setViewPager(ViewPager viewPager) {
        mViewpager = viewPager;
        if (mViewpager != null && mViewpager.getAdapter() != null) {
            mLastPosition = -1;
            createIndicators();
            mViewpager.removeOnPageChangeListener(mInternalPageChangeListener);
            mViewpager.addOnPageChangeListener(mInternalPageChangeListener);//绑定上内部实现的PageChangeListener
            mInternalPageChangeListener.onPageSelected(mViewpager.getCurrentItem());
        }
    }

    private final OnPageChangeListener mInternalPageChangeListener = new OnPageChangeListener() {

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }

        @Override public void onPageSelected(int position) {//这里是动画的核心

            if (mViewpager.getAdapter() == null || mViewpager.getAdapter().getCount() <= 0) {
                return;
            }

            if (mAnimatorIn.isRunning()) {
                mAnimatorIn.end();
                mAnimatorIn.cancel();
            }

            if (mAnimatorOut.isRunning()) {
                mAnimatorOut.end();
                mAnimatorOut.cancel();
            }

            View currentIndicator;
            if (mLastPosition >= 0 && (currentIndicator = getChildAt(mLastPosition)) != null) {//页面离开屏幕时指示器动画
                currentIndicator.setBackgroundResource(mIndicatorUnselectedBackgroundResId);
                mAnimatorIn.setTarget(currentIndicator);
                mAnimatorIn.start();
            }

            View selectedIndicator = getChildAt(position);
            if (selectedIndicator != null) {//页面进入屏幕时指示器动画
                selectedIndicator.setBackgroundResource(mIndicatorBackgroundResId);
                mAnimatorOut.setTarget(selectedIndicator);
                mAnimatorOut.start();
            }
            mLastPosition = position;
        }

        @Override public void onPageScrollStateChanged(int state) {
        }
    };


2.ViewPager结合design库实现tab切换



在design库中有个TabLayout可以为viewPager加上Tab标题头

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.administrator.viewpager.MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/mTabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></android.support.design.widget.TabLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/mViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></android.support.v4.view.ViewPager>

</LinearLayout>

mTabLayout.setupWithViewPager(mViewPager);//一行代码完成绑定

更多高级的用法包括tab中添加icon等请转至这里

传送门


3.基于ViewPager实现广告轮播控件




https://github.com/daimajia/AndroidImageSlider



imageSlider.gif

源码分析,省略了部分代码:

public class SliderLayout extends RelativeLayout{

    private InfiniteViewPager mViewPager;//这个ViewPager只是修改了setPageTransformer方法去掉了if (Build.VERSION.SDK_INT >= 11) 的限制,结合NineOldDroid库让动画兼容低版本

    /**
     * InfiniteViewPager adapter.
     */
    private SliderAdapter mSliderAdapter;//这个是PagerAdapter

    /**
     * {@link com.daimajia.slider.library.Tricks.ViewPagerEx} indicator.
     */
    private PagerIndicator mIndicator;//页面指示器


    /**
     * A timer and a TimerTask using to cycle the {@link com.daimajia.slider.library.Tricks.ViewPagerEx}.
     */
    private Timer mCycleTimer;//用于轮播的定时器
    private TimerTask mCycleTask;

    /**
     * For resuming the cycle, after user touch or click the {@link com.daimajia.slider.library.Tricks.ViewPagerEx}.
     */
    private Timer mResumingTimer;
    private TimerTask mResumingTask;

    /**
     * {@link com.daimajia.slider.library.Tricks.ViewPagerEx} 's transformer
     */
    private BaseTransformer mViewPagerTransformer;//PageTransformer的封装用于控制页面翻页效果

    public SliderLayout(Context context, AttributeSet attrs, int defStyle) {//核心代码,用于初始化ViewPager
        super(context, attrs, defStyle);
        mContext = context;
        LayoutInflater.from(context).inflate(R.layout.slider_layout, this, true);

        final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs,R.styleable.SliderLayout,
                defStyle,0);

        mTransformerSpan = attributes.getInteger(R.styleable.SliderLayout_pager_animation_span, 1100);
        mTransformerId = attributes.getInt(R.styleable.SliderLayout_pager_animation, Transformer.Default.ordinal());
        mAutoCycle = attributes.getBoolean(R.styleable.SliderLayout_auto_cycle,true);
        int visibility = attributes.getInt(R.styleable.SliderLayout_indicator_visibility,0);
        for(PagerIndicator.IndicatorVisibility v: PagerIndicator.IndicatorVisibility.values()){
            if(v.ordinal() == visibility){
                mIndicatorVisibility = v;
                break;
            }
        }
        mSliderAdapter = new SliderAdapter(mContext);
        PagerAdapter wrappedAdapter = new InfinitePagerAdapter(mSliderAdapter);

        mViewPager = (InfiniteViewPager)findViewById(R.id.daimajia_slider_viewpager);
        mViewPager.setAdapter(wrappedAdapter);

        mViewPager.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                     case MotionEvent.ACTION_UP:
                        recoverCycle();
                        break;
                }
                return false;
            }
        });

        attributes.recycle();
        setPresetIndicator(PresetIndicators.Center_Bottom);
        setPresetTransformer(mTransformerId);
        setSliderTransformDuration(mTransformerSpan,null);
        setIndicatorVisibility(mIndicatorVisibility);
        if(mAutoCycle){
            startAutoCycle();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN://手指按下时候暂停轮播
                pauseAutoCycle();
                break;
        }
        return false;
    }

    /**
     * preset transformers and their names
     */
    public enum Transformer{//PageTransformer枚举
        Default("Default"),
        Accordion("Accordion"),
        Background2Foreground("Background2Foreground"),
        CubeIn("CubeIn"),
        DepthPage("DepthPage"),
        Fade("Fade"),
        FlipHorizontal("FlipHorizontal"),
        FlipPage("FlipPage"),
        Foreground2Background("Foreground2Background"),
        RotateDown("RotateDown"),
        RotateUp("RotateUp"),
        Stack("Stack"),
        Tablet("Tablet"),
        ZoomIn("ZoomIn"),
        ZoomOutSlide("ZoomOutSlide"),
        ZoomOut("ZoomOut");

        private final String name;

        private Transformer(String s){
            name = s;
        }
        public String toString(){
            return name;
        }

        public boolean equals(String other){
            return (other == null)? false:name.equals(other);
        }
    };
}

通过分析我们可以对SliderLayout实现思路小结一下:


1.内部持有一个修改过的ViewPager控件,可以兼容低版本的页面转换动画


2.内部有一个实现了PagerAdapter的SliderAdapter适配器


3.内部持有一个PagerIndicator 页面指示器可供选择


4.维护一个定时任务用于控制轮播


5.对手势事件进行处理暂停轮播,继续轮播


6.提供了很多缺省的PageTransformer方便调用

最后在构造函数中初始化ViewPager。

http://www.jianshu.com/p/e5abbda4a71c