越来越发现在学习中随随便便的盲目搜文章学习,各种解释详细的文章博客一堆,看的真是应接不暇,顿时感觉学啥都是入了无底洞。学不完,于是决定看一手资料紧接着就打开了官方文档和这个控件的源码。结果发现官方文档和这个类的注释介绍(以后找到官方文档直接浏览器翻译就看类的简介啦,不用担心粘贴类的简介带注释符号影响翻译啦)
bb了一大堆,进入正文哈。ViewPager这个控件在App中也是常见的。微信底部tab对应的四个界面、电商app首页Banner都可以找到ViewPager的足迹。今天就来总结下这个重要的控件。
效果预览
知识图
一、基本使用
1、简介
官方文档对其简介:一个继承了ViewGroup的容器,用户向ViewPager提供若干个带数据的界面。ViewPager允许这些界面进行翻转。
理解:
1、翻转:随着用户手指在屏幕滑动,而切换用户提供的界面。
2、数据界面:用户提供View或者fragment,通过适配器设置给ViewPager
2、常用api
1、setAdapter(PagerAdapter adapter) 设置适配器,学过RecyclerView、ListView都知道。
2、setOffscreenPageLimit(int limit) 设置缓存的页面个数,默认是 1。
3、setCurrentItem(int item) 跳转到指定页面
4、addOnPageChangeListener(OnPageChangeListener listener)页面滑动监听
5、setPageTransformer(boolean reverseDrawingOrder,PageTransformer transformer, int pageLayerType) 设置页面滑动的动画效果。
6、setPageMargin(int marginPixels) 设置不同页面之间的间隔
7、setPageMarginDrawable(…) 设置不同页面间隔之间的装饰图也就是 divide ,要想显示设置的图片,需要同时设置 setPageMargin()
3、常见的Adapter介绍及PagerAdapter探讨
- PagerAdapter
- FragmentPagerAdapter
- FragmentStatePagerAdapter
PagerAdapter:ViewPager的Adapter基类,此类为抽象类。
这里主要讲解PagerAdapter。至于FragmentPagerAdapter、FragmentStatePagerAdapter下文中讲解。
(1)PagerAdapter使用栗子
PagerAdapterDemoActivity代码
public class PagerAdapterDemoActivity extends AppCompatActivity {
// 准备一组图片
private int[] imgs = new int[]{
R.drawable.img_yao, R.drawable.img_zhuge,
R.drawable.img_libai, R.drawable.img_mingren,
R.drawable.img_haizei, R.drawable.img_douluo};
private List<View> mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pager_adapter_demo);
offerData();
initViewPager();
}
private void offerData() {
// adapter 提供数据
mList = new ArrayList<>();
// 为每个view视图内的图片填充数据
for (int img : imgs) {
@SuppressLint("InflateParams") View mView = LayoutInflater.from(this).inflate(R.layout.page_pageradapter, null, false);
AppCompatImageView imageView = mView.findViewById(R.id.img);
imageView.setImageResource(img);
mList.add(mView);
}
}
private void initViewPager() {
ViewPager viewPager = findViewById(R.id.vp_pager);
// 设置adapter
viewPager.setAdapter(new MyPagerAdapter(mList));
}
}
MyPagerAdapter 代码:
/**
* Created by sunnyDay on 2019/12/14 10:39
*/
public class MyPagerAdapter extends PagerAdapter {
private List<View> mList;
public MyPagerAdapter(List<View> mList) {
this.mList = mList;
}
@Override
public int getCount() {
return mList == null ? 0 : mList.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(mList.get(position));
return mList.get(position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object);
}
}
实现思路非常简单,activity中通过for 创建了6个带数据的View,然后通过MyPagerAdapter的构造吧数据传递给ViewPager的adapter。ViewPager 为ViewGroup的子类,所以也是个容器,adapter获得数据后Viewpager就会知道,便从adapter中获得子view。填充展示。
(2)补充
1、ViewPager本身没有直接提供子视图(View或者fragment)回收机制,而是采用回调来展示页面更新过程
2、PagerAdapter可以实现视图的回收、管理,例如FragmentPagerAdapter。Fragment事务来管理每个视图。
3、ViewPager将每个页面与键对象关联,而不是直接与Views关联。此键用于跟踪和唯一标识给定页面,而与给定页面在适配器中的位置无关。
4、当ViewPager执行setAdapter方法时,这个方法内部会调用pagerAdapter的startUpdate(this)方法通知更新页面,这时pagerAdapter便会调destroyItem先销毁界面,销毁destroyItem执行完毕后便发出finishUpdate(this)(页面销毁)通知。这时instantiateItem将与返回的键对象相关联的视图添加到传递给这些方法的父级ViewGroup中,并且destroyItem 删除这些键对象 相关联的视图。
5、一个非常简单的PagerAdapter可以选择将页面Views本身用作关键对象,从instantiateItem(ViewGroup, int) 创建后返回它们并将它们添加到父ViewGroup。匹配的 destroyItem(ViewGroup, int, Object)实现会将View从父ViewGroup中删除,并且isViewFromObject(View, Object) 可以实现为return view == object。
ps:补充来自官方文档资料理解这些东西需要稍微看下Viewpager、PagerAdapter部分方法源码。
(3)四个要实现的方法
1、int getCount()
返回值:ViewPager页面的个数,一般我们给定view或者fragment的集合。集合的大小就是页面个数
2、public boolean isViewFromObject(@NonNull View view, @NonNull Object object)
view:view 视图
object:键对象
返回值:标识页面视图是否与键对象相关联
true:当前view界面与给定的键对应
false:当前的view界面与给定的键不对应
一般我们直接view==object,通过布尔返回值自动判断
3、public Object instantiateItem(@NonNull ViewGroup container, int position)
container:父容器,其实就是Viewpager 对象(ViewPager的setAdapter源码可以看出)
position:当前页面索引
返回值:页面对象所关联的键
ps:此方法内部需要开发者手动add下view到容器。这时view就被添加到Viewpager了
4、public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object)
container:viewpager对象
position:当前页面索引
object:页面对象所关联的键
4、解惑
(1)有关PagerAdapter清除View的写法
使用PagerAdapter时为啥这两种写法都行?
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object); // 方式1
// container.removeView(mList.get(position)); // 方式2
}
其实二者都是删除指定的界面因为:上述介绍过
(2)有关LayoutInflater的使用疑问
上文栗子代码中有这样一段:
View mView = LayoutInflater.from(this).inflate(R.layout.page_pageradapter, null, false);
//1、View mView = LayoutInflater.from(this).inflate(R.layout.page_pageradapter, viewPager, false);
//2、View mView = LayoutInflater.from(this).inflate(R.layout.page_pageradapter, viewPager, true);
都使用过ListView,RecyclerView,我们或许会疑问这里inflate的后两个参数怎么填呢?大家可能会写成注释2里面的写法结果发现啥bug也没有?记得之前注释2的写法在recyclerView中就直接炸了,什么此view已经有了个parent,你要removeView才能添加这个view之类的错误就出来了?这里怎么回事小老弟?接下来我们就分析下:
ps:对LayoutInflater原理不熟的可以移步这里
注释2写法分析:
注释2代表将view视图添加到ViewPager,而在adapter中视图初始化我们 container.addView(mList.get(position));又添加了一遍为啥不报:The specified child already has a parent. You must call removeView() on the child’s parent first.
分析setAdapter源码:
可见我们上文我们通经过inflate吧View添加到viewPager时,就算添加成功,在设置adapter时,也会先进行View界面清空,在添加相应的View界面。所以同一个View就不会添加到两个不同的容器而报错了!!!!
(3)作死系列就是想崩溃
这种经常发生在我们初学viewpager阶段,本来是准备提供不同的view对象,结果这样一写,添加了一堆相同的view到集合中。最终在adapter中 container.addView(mList.get(position));时崩溃就出来了,哈哈如愿以偿,快乐了吧
5、ViewPager添加View
我们或许会疑问,ViewPager作为容器可以添加哪些View?可以在xml中给ViewPager添加子View吗?代码直接可以addView吗?
查看ViewPager的addView源码
可见直接代码添加View时这个View必须是DecorView类型的类
(1)ViewPager添加子view的两种方式
- 通过PagerAdapter
- 自定义View实现DecorView接口
添加普通的View到ViewPager中普通的View是不会生效的。直接添加View时这个View必须是DecorView类型的类,所以xml写在ViewPager控件的普通View,通过代码手动添加view到ViewGroup都不生效。
二、进阶玩法
本章节主要探讨下ViewPager 结合TableLayout+fragment的使用,以及常见的轮播图功能实现。
1、结合Tablayout+fragment
(1)adapter的介绍
结合fragment时就要用到FragmentPagerAdapter或者FragmentStatePagerAdapter了。
- FragmentPagerAdapter
上述说了PagerAdapter为抽象类,这里的FragmentPagerAdapter就是PagerAdapter实现类。它将每一个页面表示为一个 Fragment,并且每一个Fragment都将会保存到fragment manager当中。
这种pager十分适用于有一些静态fragment,例如一组tabs,的时候使用。每个页面对应的Fragment当用户可以访问的时候会一直存在内存中,但是,当这个页面不可见的时候,view hierarchy将会被销毁。这样子会导致应用程序占有太多资源。当页面数量比较大的时候,建议使用 FragmentStatePagerAdapter。
public Fragment getItem(int position)
public int getCount()
- FragmentStatePagerAdapter
也是PagerAdapter的直接子类。
当使用FragmentStatePagerAdapter 时,实现将只保留当前页面,当页面离开视线后,就会被消除,释放其资源;而在页面需要显示时,生成新的页面。这么实现的好处就是当拥有大量的页面时,不必在内存中占用大量的内存。
(2)简单栗子(有坑)
FragmentStatePageAdapterActivity代码:
public class FragmentStatePageAdapterActivity extends AppCompatActivity {
private com.google.android.material.tabs.TabLayout tabLayout;
private ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_state_page_adapter);
getSupportActionBar().hide();
initView();
initData();
}
private void initData() {
populateTabsData();
populateViewPagerData();
}
private void populateViewPagerData() {
List<Fragment> mList = new ArrayList<>();
mList.add(new AnimeFragment());
mList.add(new TvFragment());
mList.add(new LoLFragment());
mList.add(new KingGloryFragment());
mList.add(new PeaceFragment());
viewPager.setAdapter(new MyFragmentStateAdapter(getSupportFragmentManager(), FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT, mList));
tabLayout.setupWithViewPager(viewPager, false);//自动绑定viewpager,实现联动效果
}
private void populateTabsData() {
String[] arr = new String[]{
"动漫", "电视剧", "LOL", "王者荣耀", "和平精英",
};
for (String s : arr) {
//注意这里的对象使用(不要直接new TableLayout,使用的对象要为上文findviewbyid那个)否则回报空指针
tabLayout.addTab(tabLayout.newTab().setText(s));
}
}
private void initView() {
tabLayout = findViewById(R.id.my_tab_layout);
viewPager = findViewById(R.id.vp_pager);
}
}
FragmentStatePagerAdapter代码:
/**
* Created by sunnyDay on 2019/12/16 19:46
*/
public class MyFragmentStateAdapter extends FragmentStatePagerAdapter {
private List<Fragment> mList;
public MyFragmentStateAdapter(@NonNull FragmentManager fm, int behavior, List<Fragment> mlist) {
super(fm, behavior);
this.mList = mlist;
}
@NonNull
@Override
public Fragment getItem(int position) {
return mList.get(position);
}
@Override
public int getCount() {
return mList == null ? 0 : mList.size();
}
/**
* view pager 和 tab layout 联动时这个方法必须重写 否则 tab 标题会被全部置空
* */
// @Nullable
// @Override
// public CharSequence getPageTitle(int position) {
// String[] arr = new String[]{
// "动漫", "电视剧", "LOL", "王者荣耀", "和平精英",
// };
// return arr[position];
// }
}
实现流程梳理:
1、xml中放置了两个控件tablayout和viewpager很简单(具体参考下文给出的全部源码,这里不再给出)
2、给tableLayout控件的tabs赋值。这里注意上文代码中的注释。
3、搞个adapter吧fragment的集合传递过来即可
4、FragmentStatePagerAdapter的参数FragmentManager fm:fragmentManger对象
int behaviorbehavior的值:
- BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:指示仅当前片段处于该Lifecycle.State.RESUMED 状态。
- BEHAVIOR_SET_USER_VISIBLE_HINT:已经不建议使用,建议使用BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
5、构造函数FragmentStatePagerAdapter。如果BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT传入,则仅当前Fragment处于Lifecycle.State.RESUMED状态,而所有其他Fragment的上限为Lifecycle.State.STARTED。如果BEHAVIOR_SET_USER_VISIBLE_HINT通过,则所有片段都处于Lifecycle.State.RESUMED状态,并且将有的回调Fragment.setUserVisibleHint(boolean)。
(3)Viewpager与tablayout联动的坑
如上: tabLayout.setupWithViewPager(viewPager, false);设置了这句代码后我们惊奇的发现tablayout的文字全部都是空了。这时我们分析下源码如下。
private void setupWithViewPager( @Nullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) {
...
...
... 忽略n行
if (viewPager != null) {
this.viewPager = viewPager;
if (pageChangeListener == null) {
pageChangeListener = new TabLayoutOnPageChangeListener(this);
}
pageChangeListener.reset();
viewPager.addOnPageChangeListener(pageChangeListener);
currentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
addOnTabSelectedListener(currentVpSelectedListener);
final PagerAdapter adapter = viewPager.getAdapter();
if (adapter != null) {
// 重要方法
setPagerAdapter(adapter, autoRefresh);
}
if (adapterChangeListener == null) {
adapterChangeListener = new AdapterChangeListener();
}
adapterChangeListener.setAutoRefresh(autoRefresh);
viewPager.addOnAdapterChangeListener(adapterChangeListener);
setScrollPosition(viewPager.getCurrentItem(), 0f, true);
} else {
this.viewPager = null;
// 重要方法
setPagerAdapter(null, false);
}
这个方法内部有个重要方法setPagerAdapter,这时我们进入setPagerAdapter方法看看。
void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
... 省略n行
populateFromPagerAdapter(); // 重要方法
}
省略的代码都是有关观察者注册adapter的,这里最后有个populateFromPagerAdapter方法。
这里就明白为啥联动时不重写adapter的getPageTitle时,tabs消失的坑。
2、常见轮播图的实现
未完 待续!!!
源码:
传送门
参考:
官方文档
ViewPager 超详解:玩出十八般花样
ViewPager源码分析