ViewPager 全面剖析及使用詳解

ViewPager在開發(fā)中的使用頻率非常的高,所以在此做個總結(jié)。主要包括以下幾方面:

  • ViewPager的簡介和作用
  • ViewPager的適配器
  • ViewPager的翻頁動畫
  • 簡化ViewPager的使用
  • ViewPager結(jié)合第三方庫實現(xiàn)小圓點指示器效果
  • ViewPager結(jié)合design庫實現(xiàn)tab切換
  • 基于ViewPager實現(xiàn)廣告輪播控件

按照慣例,先上個效果圖

demo.gif

基礎(chǔ)篇


1.ViewPager的簡介和作用
ViewPager是android擴展包v4包中的類,這個類可以讓用戶左右切換當(dāng)前的view
1)ViewPager類直接繼承了ViewGroup類,所有它是一個容器類,可以在其中添加其他的view類。
2)ViewPager類需要一個PagerAdapter適配器類給它提供數(shù)據(jù)。
3)ViewPager經(jīng)常和Fragment一起使用,并且提供了專門的FragmentPagerAdapter和FragmentStatePagerAdapter類供Fragment中的ViewPager使用。

2.ViewPager的適配器
簡介中提到了PagerAdapter,和ListView等控件使用一樣,需要ViewPager設(shè)置PagerAdapter來完成頁面和數(shù)據(jù)的綁定,這個PagerAdapter是一個基類適配器,我們經(jīng)常用它來實現(xiàn)app引導(dǎo)圖,它的子類有FragmentPagerAdapter和FragmentStatePagerAdapter,這兩個子類適配器用于和Fragment一起使用,在安卓應(yīng)用中它們就像listview一樣出現(xiàn)的頻繁。

實現(xiàn)一個最基本的PagerAdapter,《必須實現(xiàn)四個方法》,在代碼里有注釋

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

    public AdapterViewpager(List<View> mViewList) {
        this.mViewList = mViewList;
    }

    @Override
    public int getCount() {//必須實現(xiàn)
        return mViewList.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {//必須實現(xiàn)
        return view == object;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {//必須實現(xiàn),實例化
        container.addView(mViewList.get(position));
        return mViewList.get(position);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {//必須實現(xiàn),銷毀
        container.removeView(mViewList.get(position));
    }
}

實現(xiàn)一個最基本的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) {//必須實現(xiàn)
        return mFragments.get(position);
    }

    @Override
    public int getCount() {//必須實現(xiàn)
        return mFragments.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {//選擇性實現(xiàn)
        return mFragments.get(position).getClass().getSimpleName();
    }
}

FragmentStatePagerAdapter的實現(xiàn)和FragmentPagerAdapter的實現(xiàn)一樣就不在寫了

3個適配器的基本實現(xiàn)講完了是不是很簡單,那他們的區(qū)別是什么呢?
PagerAdapter是基類適配器是一個通用的ViewPager適配器,相比PagerAdapter,F(xiàn)ragmentPagerAdapter和FragmentStatePagerAdapter更專注于每一頁是Fragment的情況,而這兩個子類適配器使用情況也是有區(qū)別的。FragmentPagerAdapter適用于頁面比較少的情況,F(xiàn)ragmentStatePagerAdapter適用于頁面比較多的情況。為什么?簡單分析下兩個適配器的源碼就可以知道了。

  • 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省內(nèi)存。FragmentPagerAdapter中的fragment實例在destroyItem的時候并沒有真正釋放fragment對象只是detach,所以FragmentPagerAdapter消耗更多的內(nèi)存,帶來的好處就是效率更高一些。所以得出這樣的結(jié)論:FragmentPagerAdapter適用于頁面比較少的情況,F(xiàn)ragmentStatePagerAdapter適用于頁面比較多的情況,因此不同的場合選擇合適的適配器才是正確的做法

3.ViewPager的翻頁動畫
為ViewPager設(shè)置適配器后,就可以正常使用了,接下來我們?yōu)閂iewPager增加翻頁動畫,畢竟人的審美會疲勞,加上一些動畫交互會提高不少逼格~~,ViewPager提供了PageTransformer接口用于實現(xiàn)翻頁動畫。
官方提供了PageTransformer的實現(xiàn)例子。

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

實現(xiàn)翻頁動畫的關(guān)鍵就是重寫transformPage方法,方法里有兩個參數(shù)view和position,理解這兩個參數(shù)非常重要。假設(shè)有三個頁面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→ 正無窮大
    當(dāng)position是正負無窮大時view就離開屏幕視野了。因此最核心的控制邏輯是在[-1,0]和(0,1]這兩個區(qū)間,通過設(shè)置透明度,平移,旋轉(zhuǎn),縮放等動畫組合可以實現(xiàn)各式各樣的頁面變化效果。

4.簡化ViewPager的使用

不會偷懶的程序猿不是好程序員

這里只是做了最簡單的封裝,可以根據(jù)需要調(diào)整

  • 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簡化 
```java
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不重新走生命周期的個數(shù)的,打個比方一共4個fragment頁面,如果mViewPager.setOffscreenPageLimit(3),那么所有的fragment都只走一次生命周期,如果是mViewPager.setOffscreenPageLimit(2),那么其中有一個fragment會在切換的時候重新走一遍生命周期,F(xiàn)ragmentStatePagerAdapter和FragmentPagerAdapter都是這樣,但是FragmentPagerAdapter設(shè)置setOffscreenPageLimit不影響fragment緩存的個數(shù),而FragmentStatePagerAdapter緩存的fragment實例個數(shù)就是setOffscreenPageLimit設(shè)置的值+1。另外setOffscreenPageLimit的缺省值是1,設(shè)置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結(jié)合第三方庫實現(xiàn)小圓點指示器效果
https://github.com/ongakuer/CircleIndicator

screenshot.gif

使用看官方文檔很簡單。

看一下實現(xiàn)思路

  public void setViewPager(ViewPager viewPager) {
      mViewpager = viewPager;
      if (mViewpager != null && mViewpager.getAdapter() != null) {
          mLastPosition = -1;
          createIndicators();
          mViewpager.removeOnPageChangeListener(mInternalPageChangeListener);
          mViewpager.addOnPageChangeListener(mInternalPageChangeListener);//綁定上內(nèi)部實現(xiàn)的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結(jié)合design庫實現(xiàn)tab切換
在design庫中有個TabLayout可以為viewPager加上Tab標(biāo)題頭

<?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等請轉(zhuǎn)至這里 傳送門

3.基于ViewPager實現(xiàn)廣告輪播控件
https://github.com/daimajia/AndroidImageSlider

imageSlider.gif

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

public class SliderLayout extends RelativeLayout{
 
    private InfiniteViewPager mViewPager;//這個ViewPager只是修改了setPageTransformer方法去掉了if (Build.VERSION.SDK_INT >= 11) 的限制,結(jié)合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實現(xiàn)思路小結(jié)一下:
1.內(nèi)部持有一個修改過的ViewPager控件,可以兼容低版本的頁面轉(zhuǎn)換動畫
2.內(nèi)部有一個實現(xiàn)了PagerAdapter的SliderAdapter適配器
3.內(nèi)部持有一個PagerIndicator 頁面指示器可供選擇
4.維護一個定時任務(wù)用于控制輪播
5.對手勢事件進行處理暫停輪播,繼續(xù)輪播
6.提供了很多缺省的PageTransformer方便調(diào)用
最后在構(gòu)造函數(shù)中初始化ViewPager。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容