ViewPager是v4支持库中的一个控件,相信几乎所有接触Android开发的人都对它不陌生。之所以还要在这里翻旧账,是因为我在最近的项目中有多个需求用到了它,觉得自己对它的认识不够深刻。我计划从最简单的使用场景出发,记录我到目前为止所对ViewPager的使用情况以及有关它的一些知识点。
这个系列的代码将存放在Github仓库中,每篇文章对应一个分支或几个分支。
这是第三篇文章,将讨论集中有关如何使用ViewPager展示无限循环视图的方法。
方法1:极大化PagerAdapter.getCount
的返回值
这是最简单的实现方法。关键在于重写PagerAdapter.getCount
方法,将其返回值设置为Integer.MAX_VALUE
,然后通通过取模position%count
的方式获取得对应的数据进行视图渲染。
... @Override public int getCount() { return Integer.MAX_VALUE; } @Override public Object instantiateItem(ViewGroup container, int position) { int index = position % 3; String text = texts.get(index); TextView textView = new TextView(container.getContext()); textView.setText(text); container.addView(textView); return textView; } ...
这种方法毕竟不是真实的无限循环,只是虚拟了一个极大的页数,让用户翻页的时候很触及到“世界的尽头”。所以在初始化的时候需要完成一个关键初始化:
viewPager.setCurrentItem(Integer.MAX_VALUE / 2, false);
把初始化页面定位到世界的中央。
相关代码在分支:03-fake-infinite-cycle可以获取。
方法2:在数据源首尾添加重复节点
这是实现ViewPager
无限循环的另一种方案:通过在数据源的首尾处添加重复的数据(在源数据前插入最后一个数据,其后插入原来的第一个数据),这两个重复数据的作用是在滚动过程中作为中间视图,当滚动停止时立刻切换到最终的视图,进入下一个滚动循环。
相关代码见分支:03-infinite-cycle-with-additional-views
首先在往PagerAdapter插入数据的时候对数据进行一下处理:
public void setTexts(List<String> texts) { this.texts.clear(); if (texts == null) { notifyDataSetChanged(); return; } // 只有一个数据时不循环 if (texts.size() == 1) { this.texts.addAll(texts); // 多个数据,插入重复数据 } else if (texts.size() > 1) { this.texts.add(texts.get(texts.size() - 1)); this.texts.addAll(texts); this.texts.add(texts.get(0)); } notifyDataSetChanged(); }
其次让ViewPager实现ViewPager.OnPageChangeListener
接口,监听滚动状态。代码如下:
@Override public void onPageSelected(int position) { int realCount = getCount() - 2; // 多于1,才会循环跳转 if ( getCount() > 1) { // 首位之前,跳转到末尾(N) if ( position < 1) { position = realCount; viewPager.setCurrentItem(position,false); } // 末位之后,跳转到首位(1) else if ( position > realCount) { position = 1; viewPager.setCurrentItem(position,false); } } }
最后组装一下ViewPager
和PagerAdapter
即可:
viewPager.setAdapter(adapter); viewPager.addOnPageChangeListener(adapter); if (adapter.getCount() > 1) { viewPager.setCurrentItem(1, false); }
注意最后的if
语句,它让ViewPager默认显示第一页。否则页面将展示最后一个源数据的内容且无法向右滑动。
实际上这种方法也是有缺陷的。当用户滑动ViewPager
到源数据的最后一个节点(下标:getCount()-2)并且先要继续滑动显示下一个节点时,这期间ViewPager
首先随用户手指一动正常展示我们插入的重复内容(下标:getCount()-1),当滚动停止且触发了onPageSelected
回调,ViewPager立即切换到源数据的第一页(下标:1)进入下一个循环。这会导致几个不协调的现象:
- 切换到下一个循环的时候会破坏
ViewPager
的滚动动画(如:滚动惯性动画)。 - 切换前展示的缓存视图在切换时被销毁,切换后的视图需要重新生成。如果这里有需要延迟加载的内容也会导致展示不协调。
方法3:改进方法2
针对上述方法2提出的两个缺点,在此将着重解决缺点1出现的动画不连贯的现象,作为第三种方案进行介绍。至于缺点2可以通过缓存视图的方式解决,就不在此赘述。
方法3的代码见分支:03-infinite-cycle-better-practise
该方案已经满足我目前的需求。它的关键点如下:
首先,如方法2一样在数据源头尾插入重复节点,用于过渡。这里我重新写了setTexts
方法,让只有一个数据的场景也可以循环:
public void setTexts(List<String> texts) { this.count = 0; this.texts.clear(); if (texts != null && texts.size() > 0) { this.count = texts.size(); for (int i = 0; i <= count + 1; i++) { if (i == 0) { this.texts.add(texts.get(count - 1)); } else if (i == count + 1) { this.texts.add(texts.get(0)); } else { this.texts.add(texts.get(i - 1)); } } } notifyDataSetChanged(); }
接下来解决方法2的动画不连贯的问题。注意到在方法2中在OnPageChangeListener
的onPageSelected
方法中处理了循环的跳转逻辑。然后onPageSelected
是ViewPager
处理ACTION_UP
事件时回调的。也就是说,当用户的手指时快速拖动后离开ViewPager
时,ViewPager
回调了该方法,然后还会继续后续的衰减动画。在这个时间点使用setCurrentItem
跳转到指定视图必然会造成动画停顿的问题。
把切换循环改在ViewPager
的滚动状态发生变化时进行。怎么做呢?见代码:
// count为源数据的条目 // currentItem为PagerAdapter当前选中项 @Override public void onPageSelected(int position) { currentItem = position; } @Override public void onPageScrollStateChanged(int state) { switch (state) { case ViewPager.SCROLL_STATE_IDLE://No operation if (currentItem == 0) { viewPager.setCurrentItem(count, false); } else if (currentItem == count + 1) { viewPager.setCurrentItem(1, false); } break; case ViewPager.SCROLL_STATE_DRAGGING: //start Sliding if (currentItem == 0) { viewPager.setCurrentItem(count, false); } else if (currentItem == count + 1) { viewPager.setCurrentItem(1, false); } break; case ViewPager.SCROLL_STATE_SETTLING://end Sliding break; } }
代码中在状态变为停止“SCROLL_STATE_IDLE
”或状态变为开始滚动“SCROLL_STATE_DRAGGING
”时处理了循环切换的逻辑。
这里描述一下整个流程。如果用户处于第一页且继续向右滑动手指,或者处于最后一页且继续向左滑动手指时,在状态由空闲变为开始滚动“SCROLL_STATE_DRAGGING
”进行切换。第一种情况,如果最终成功切换到目标页面,那么在状态变为空闲时由于currentItem
已经发生变化,所以不会重复切换。第二种情况,如果没有成功切换到目标页面,ViewPager
需要在状态变为“SCROLL_STATE_IDLE
”时再次切换回原来的视图。
注意在初始化ViewPager
时调用一下setCurrentItem(1)
,让它正确显示第一个视图。
小结
ViewPager
循环展示数据的方法目前就介绍到这里。我认为方法1和方法3根据不同场景考虑是否使用。出于某种情结,我更倾向于使用方法3,毕竟方法三是查看了github中的banner库之后总结出来的。
本文来自作者同步博客