TabLayout+ViewPager的使用

3,298 阅读9分钟

标签(空格分隔): 安卓UI


一、TabLayout的基本使用方式

TabLayout这个控件展示的效果很简单,就是一个水平的布局用来展示不同的Tab,每个Tab对应的View一般会结合其他控件一起来使用;

1、TabLayout常用方法

TabLayout常用的方法就是新增Tab和Tab切换监听: addTab(@NonNull Tab tab)同类的方法:Add a tab to this layout. newTab():Create and return a new {@link Tab} removeTab(Tab tab):Remove a tab from the layout. setTabMode(@Mode int mode) :Set the behavior mode for the Tabs in this layout. setSelectedTabIndicatorColor(@ColorInt int color):Sets the tab indicator's color for the currently selected tab. setSelectedTabIndicatorHeight(int height):Sets the tab indicator's height for the currently selected tab. setScrollPosition(int position, float positionOffset, boolean updateSelectedText):Set the scroll position of the tabs. addOnTabSelectedListener(@NonNull OnTabSelectedListener listener):Add a {@link TabLayout.OnTabSelectedListener} that will be invoked when tab selection changes. TabLayout还提供了其他比较多的UI定制方法,可以直接参考源码;

2、TabLayout+ViewPager使用

TabLayout控件只是提供了展示不同Tab的布局,一般场景下,需要根据切换Tab,展示其他效果,使用比较多的就是TabLayout+ViewPager;关于ViewPager的使用见下文ViewPager介绍,目前只介绍是TabLayout+ViewPager如何结合使用; TabLayout提供了如下方法: setupWithViewPager(@Nullable ViewPager viewPager)、setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh)、setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) ,这几个方法虽然参数不同,但是作用是差不多的,Google官方解释:

/**
 * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
 *
 * <p>This method will link the given ViewPager and this TabLayout together so that
 * changes in one are automatically reflected in the other. This includes scroll state changes
 * and clicks. The tabs displayed in this layout will be populated
 * from the ViewPager adapter's page titles.</p>
 *
 * <p>If {@code autoRefresh} is {@code true}, any changes in the {@link PagerAdapter} will
 * trigger this layout to re-populate itself from the adapter's titles.</p>
 *
 * <p>If the given ViewPager is non-null, it needs to already have a
 * {@link PagerAdapter} set.</p>

从上面解释可以看到,通过上述的方法,就可以将TabLayout和ViewPager关联起来,任何一个控件的改变都会影响另一个;

二、ViewPager的使用方法

ViewPager这个控件设置的初衷就是实现类似广告左右滑动的时候切换页面的效果,因此这个控件也类似于ListView一样,需要通过Adapater来指定Page的数据源;Google控件无处不在用到适配器模式,想想适配器模式的应用效果 官方文档介绍如下:

/**
* Layout manager that allows the user to flip left and right
* through pages of data.  You supply an implementation of a
* {@link PagerAdapter} to generate the pages that the view shows.
* * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment},
* which is a convenient way to supply and manage the lifecycle of each page.
* There are standard adapters implemented for using fragments with the ViewPager,
* which cover the most common use cases.  These are
* {@link android.support.v4.app.FragmentPagerAdapter} and
* {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these
* classes have simple code showing how to build a full user interface
* with them.
*/
public class ViewPager extends ViewGroup {
    ......
}

从上面的官方解释,总结起来有以下几点:

1.ViewPager类直接继承了ViewGroup类,所有它是一个容器类,可以在其中添加其他的view类。 2.ViewPager类需要一个PagerAdapter适配器类给它提供数据。 3.ViewPager经常和Fragment一起使用,并且提供了专门的FragmentPagerAdapter和FragmentStatePagerAdapter类供Fragment中的ViewPager使用。

从上面的表述可以看到,ViewPager的数据是通过PagerAdapter来提供;

2.1、PagerAdapter的使用

首先将官方文档中对PagerAdapter的主要介绍截取出来:

/**
* Base class providing the adapter to populate pages inside of
* a {@link ViewPager}.  You will most likely want to use a more
* specific implementation of this, such as
* {@link android.support.v4.app.FragmentPagerAdapter} or
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
* <p>When you implement a PagerAdapter, you must override the following methods
* at minimum:</p>
* <ul>
* <li>{@link #instantiateItem(ViewGroup, int)}</li>
* <li>{@link #destroyItem(ViewGroup, int, Object)}</li>
* <li>{@link #getCount()}</li>
* <li>{@link #isViewFromObject(View, Object)}</li>
* </ul>
* <p>PagerAdapter supports data set changes. Data set changes must occur on the
* main thread and must end with a call to {@link #notifyDataSetChanged()} similar
* to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
* set change may involve pages being added, removed, or changing position. The
* ViewPager will keep the current page active provided the adapter implements
* the method {@link #getItemPosition(Object)}.</p>
*/
public abstract class PagerAdapter {
    ......
}

以上就可以将PagerAdapter概述为:

1.大多数情况下,我们根据具体情况实现PagerAdapter并且更加具体的适配器; 2.当你实现一个PagerAdapter时,你至少需要重写下面的几个方法:instantiateItem、destroyItem、getCount、isViewFromObject; 3.ViewPager使用回调机制来显示一个更新步骤,而不是直接使用视图回收机制。 4.PagerAdapter支持数据集的改变。数据集的改变必须放在主线程中,并且在结束时调用notifyDataSetChanged()方法,这与通过BaseAdapter适配的AdapterView类似。

上面传达除了使用PagerAdapter最重要的讯息,根据具体的情况我们实现符合我们需求的PagerAdapter,需要重写四个方法;同时大部分场景下使用已有的FragmentPagerAdapter和FragmentStatePagerAdapter能够满足需求;PagerAdapter可以和ListView对应的Adapter对比,两者的实质是不一样,只不过用于不同的控件,展示的UI效果也就不一样了;

1、简单实现PagerAdapter

实现一个最简单的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));
    }
}

关注一下四个函数要实现的功能:

/**
* Create the page for the given position.  The adapter is responsible
* for adding the view to the container given here, although it only
* must ensure this is done by the time it returns from
* {@link #finishUpdate(ViewGroup)}.
* @param container The containing View in which the page will be shown.
* @param position The page position to be instantiated.
* @return Returns an Object representing the new page.  This does not
* need to be a View, but can be some other container of the page.
*/
public Object instantiateItem(ViewGroup container, int position) {
    ......
}

instantiateItem最重要的作用就是将某个位置对应的Page添加到container,类似RecyclerView.adpter的onCreateView;

/**
* Determines whether a page View is associated with a specific key object
* as returned by {@link #instantiateItem(ViewGroup, int)}. This method is
* required for a PagerAdapter to function properly.
* @param view Page View to check for association with <code>object</code>
* @param object Object to check for association with <code>view</code>
* @return true if <code>view</code> is associated with the key object <code>object</code>
*/
public abstract boolean isViewFromObject(View view, Object object);

isViewFromObject就是返回是否某个Page View和instantiateItem返回的对象对应; destroyItem、getCount也参考如上实现非常容易理解;

2、FragmentPagerAdapter

简单的实现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();
    }
}

要使用FragmentPagerAdapter很简单,作如上实现即可,FragmentPagerAdapter是PagerAdapter的子类,看一下官方文档:

/**
* <p>This version of the pager is best for use when there are a handful of
* typically more static fragments to be paged through, such as a set of tabs.
* The fragment of each page the user visits will be kept in memory, though its
* view hierarchy may be destroyed when not visible.  This can result in using
* a significant amount of memory since fragment instances can hold on to an
* arbitrary amount of state.  For larger sets of pages, consider
* {@link FragmentStatePagerAdapter}.
* <p>Subclasses only need to implement {@link #getItem(int)}
* and {@link #getCount()} to have a working adapter.
*/
public abstract class FragmentPagerAdapter extends PagerAdapter {
    ......
}

从上面主要的简介可以看到,FragmentPagerAdapter对应的所有pager fragments会一直被持有放在内存,及时对应的Fragment不可见的时候;因此,FragmentPagerAdapter的使用一定要注意,如果FragmentPagerAdapter对应的pages数量比较大,不要使用FragmentPagerAdapter,可能引发内存问题; FragmentPagerAdapter最重要就是实现:

/**
 * Return the Fragment associated with a specified position.
 */
public abstract Fragment getItem(int position);

3、FragmentStatePagerAdapter

要实现FragmentStatePagerAdapter和实现FragmentPagerAdapter实际上是一样的,但是正如前面说到的内存问题,FragmentStatePagerAdapter内部实现了对内存的处理,具体代码参考: FragmentStatePagerAdapter的destroyItem:

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

    mCurTransaction.remove(fragment);
}

FragmentPagerAdapter的destroyItem:

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

差别就在mCurTransaction.remove(fragment)mCurTransaction.detach((Fragment)object),参考FragmentTransaction对应的方法就可以理解;更多具体的实现可以参考源码实现;从上面的对比可以看到,FragmentStatePagerAdapter用于当pages比较多或者Fragment内存占用比较多的情况,而FragmentPagerAdapter用于占用内存不大,但是使用频率很高的情况,性能会更高;因此要根据具体情况使用; 那FragmentPagerAdapter占用的pages内存什么时候会释放呢?参考FragmentTransaction和源码需要进一步分析;

2.2、ViewPager支持更多的展示

Android Design的控件都支持更多的UI展示,ViewPager的效果是左右滑动实现页面翻转,当然也支持过场动画,具体实现参考ViewPager.PageTransformer接口;

/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
*position of the pager. 0 is front and center. 1 is one full
*page position to the right, and -1 is one page position to the left.
*/
void transformPage(View page, float position);

1、提升性能的setOffscreenPageLimit

ViewPager提供了一个setOffscreenPageLimit方法,官方解释如下:

/**
* Set the number of pages that should be retained to either side of the
* current page in the view hierarchy in an idle state. Pages beyond this
* limit will be recreated from the adapter when needed.
*
* <p>This is offered as an optimization. If you know in advance the number
* of pages you will need to support or have lazy-loading mechanisms in place
* on your pages, tweaking this setting can have benefits in perceived smoothness
* of paging animations and interaction. If you have a small number of pages (3-4)
* that you can keep active all at once, less time will be spent in layout for
* newly created view subtrees as the user pages back and forth.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.</p>
*
* @param limit How many pages will be kept offscreen in an idle state.
*/
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;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}

翻译过来几个重点:

1.当设置了限制数量m,当滑动的时候,超过m的page才会每次都要重新创建; 2.实际上这个函数的作用就是会保存相应数量的pages,因为viewpager可能存在频繁的左右滑动,因此如果总是重复绘制UI,性能和用户体验上可能会有影响; 3.还是综合考虑性能和内存的影响,谨慎使用这个方法;

关于这个方法更多的介绍参考**populate(int newCurrentItem)**方法,可以清楚这个方法最终如何影响ViewPager的使用。

2.3、基于Java范形实现ViewPager

基于Java的范性来实现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));
  }

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