踩石行动:ViewPager无限轮播的坑
2016-6-19
前言
View轮播效果在app中很常见,一想到左右滑动的效果就很容易想到使用ViewPager来实现。对于像我们常说的banner这样的效果,具备无限滑动的功能是可以用ViewPager实现的,不过使用ViewFlow更简单些。
最近项目里的一个页面的banner功能出了问题,使用的是viewPager + handler实现的,之前的代码实在是设计的过于复杂,就自己重新实现了一遍。整体来说,ViewPager可以实现无限滚动,但方式比较绕。
ViewPager的使用
首先来简单概括下ViewPager的使用。
1.编写PagerAdapter。
需要实现PagerAdapter中以下方法:
Object instantiateItem(ViewGroup container, int position)
ViewPager每次最多需要保持1-3个View,此方法就是我们提供page view的地方。生成的View对象一定要添加到container中才可以正常显示。返回的Object对象是和此View关联的一个自定义对象(类似View.setTag),比如可以把一个对应View的数据对象返回。一般的,没有特殊需要时,我们返回View对象本身。boolean isViewFromObject(View view, Object object)
就是指示ViewPager中的View对象和instantiateItem返回的Object对象的关系。如果在instantiateItem我们返回的是View本身,那么此处return view == object就可以。void destroyItem(ViewGroup container, int position, Object object)
要知道PagerView是每次最多显示3个page view的,为了像ListView对应的BaseAdapter那样复用View对象,此方法为我们提供了回收添加到ViewPager中的不再显示的对象的方式。
object就是instantiateItem返回的对象,container、position正是instantiateItem的position,container。
根据前面的分析,在destroyItem中,我们把position处的page view从container移除即可,此处的object对象正是instantiateItem中add到container的page view对象。执行完container.removeView((View) object)
后,可以使用一个List来维护回收的View,这样可以避免创建大量的View对象——就像ListView的BaseAdapter那样——转而使用List中的可服用View对象,确切的说,如果展示的是同一“类型”的视图(布局orView),那么最多需要4个View对象,我们就可以满足ViewPager的显示需要了。public int getCount()
返回ViewPager要展示的page view的总数量。ViewPager的左右滑动正是根据getCount()以及当前展示的page的位置来控制的。
2. ViewPager和PagerAdapter关联同步
ViewPager和PagerAdapter的关系就如同ListView和BaseAdapter的关系,是视图和视图数据适配器的关系——满满都是模式。
ViewPager.setAdapter(PagerAdapter adapter)
首先把创建好的PagerAdapter对象设置给ViewPager对象,这样,它们就关联了。ViewPager就展示了此PagerAdapter的数据。ViewPager.setCurrentItem(int item)
设置viewPager当前展示的page位置,默认是0。PagerAdapter.notifyDataSetChanged()
当PagerAdapter的数据发生改变时,必须执行此方法和关联的ViewPager进行同步,否则运行中会产生异常。
不过:PagerAdapter不像BaseAdapter那样,notifyDataSetChanged方法在UI表现上是有问题的,建议每次数据发生变化后,直接使用setAdapter重新关联。原因下面会有说明。
实现无限滑动的思路
典型的,为了让ViewPager可以无限滑动,我们让getCount返回一个很大的值,例如Integer.MAX_VALUE,然后setCurrentItem把ViewPager显示的当前Page设置在总页数的中间位置。
思路如上,下面给出完整的代码:
...
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
public class BannerPagerAdapter extends PagerAdapter {
private ArrayList<ImageView> reusableImgViews = new ArrayList<>();
private ArrayList<String> bannerPicList = new ArrayList<>();
private Activity activity;
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true).cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.resetViewBeforeLoading(true)
.considerExifParams(true)
.build();
public BannerPagerAdapter(Activity activity) {
bannerPicList.add("http://img1.gtimg.com/auto/pics/hv1/63/227/1381/89857473.jpg");
bannerPicList.add("https://images0.cnblogs.com/i/316630/201408/092010425847554.png");
bannerPicList.add("http://img5.imgtn.bdimg.com/it/u=854234410,2851953187&fm=15&gp=0.jpg");
bannerPicList.add("http://img0.imgtn.bdimg.com/it/u=1615470112,4224934998&fm=15&gp=0.jpg");
this.activity = activity;
}
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
public int getStartPageIndex() {
int index = getCount() / 2;
int remainder = index % bannerPicList.size();
index = index - remainder;
return index;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
ImageView imgView;
if (reusableImgViews.size() == 0) {
imgView = new ImageView(activity);
imgView.setScaleType(ImageView.ScaleType.FIT_XY);
} else {
imgView = reusableImgViews.remove(reusableImgViews.size() - 1);
}
String url = bannerPicList.get(getBannerIndexOfPosition(position));
ImageLoader.getInstance().displayImage(url, imgView, displayImageOptions);
container.addView(imgView);
return imgView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
reusableImgViews.add((ImageView) object);
}
private int getBannerIndexOfPosition(int position) {
return position % bannerPicList.size();
}
}
在Activity的onCreate中:
void onCreate(Bundle savedInstanceState) {
...
viewPager = (ViewPager) findViewById(R.id.banner_viewpager);
BannerPagerAdapter adapter = new BannerPagerAdapter(this);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(adapter.getStartPageIndex());
...
}
以上代码实现简单的无限滑动足够了,但是,ViewPager有几个局限性,甚至是坑值得注意。
ViewPager的局限性
1. setCurrentItem卡顿
当getCount返回的页数非常大的时候,比如10亿,调用setCurrentItem会引起ANR。这个和getCount以及当前的page位置有关。通过查看源码可以发现,ViewPager中的populate(int newCurrentItem)
和calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)
这两个方法中,有for循环的执行次数和getCount成正比,具体细节有兴趣的朋友可以观察源码。
经过我的实验,在pageCount非常大的时候,setCurrentItem方法如果引起ViewPager的页码切换跨度大于1时,就会引起明显的卡顿。正巧的是,我们使用ViewPager实现滑动效果(handler自动++或--页码)的时候,每次页码仅仅是增加或者减小1,所以不会卡顿。但是,如果代码中有逻辑setCurrentItem引起页码变化大于1,比如当前在第3页,直接切换到getCount() / 2页时,直接就ANR了。
有意思的是,在onCreate中setAdapter之后,第一次viewPager.setCurrentItem(adapter.getStartPageIndex())
并不会引起ANR,应该是onCreate时ViewPager还没有执行一些内部计算的原因。
setCurrentItem引起的ANR和是否指定第二个参数smoothScroll没有关系。
2. notifyDataSetChanged后滑动效果不对
这个情况是UI表现上,ViewPager的左右滑动效果的小bug。
在正常使用ViewPager,没有任何无限滑动的逻辑的情况下:
假设第一次setAdapter的时候,getCount返回1,此时ViewPager只有一个page,不可以左右滑动。
然后改变Adapter对象的内部数据集合大小,getCount返回3,notifyDataSetChanged后,此时可以滑动3个页面。
接下来再修改数据集合,让getCount返回1,notifyDataSetChanged后,此时按期望,ViewPager是不可以滑动的,但是,实际效果是:ViewPager可以滑动——看得见之前3页时的额外View——看到1个还是2个和——notifyDataSetChanged时ViewPager的正在显示的page有关,但是无法滑动到除position为1的其它页码。
大家有兴趣可以自己试下,解决方法很奇葩:
就是每次adapter的数据发生变化后,根据需要先setCurrentItem到默认起始位置,之后执行setAdapter就行。PagerAdapter的notifyDataSetChanged并不像它应该承诺的那样,而为了实现在Adapter数据发生变化后通知更新ViewPager的目的:需要再次执行viewPager.setAdapter(adapter)
。
3. 关于viewPager设计的吐槽
ViewPager显然是按照了ListView那样的方式来计算总页数的,但是对于一个每次只显示3页的View来说,每次左滑和右滑的时候调用一个让子类重写的判断是否还有左边page view和右边page view的方法岂不更好?
setCurrentItem里面的逻辑简直了,竟然和getCount成正比耗费时间,那就只能当设计者根本没有考虑使用此View在非常大量数据的情况了!真不知道ViewPager是性能卓越了,还是功能丰富了,比起ViewFlow,不知道它多出那么多代码的情况下,还有notifyDataSetChanged和setAdapter的UI表现不同这样的狗血。
更好的无限滑动的解决方案
由于ViewPager的总页数很大时对setCurrentItem造成的限制。需要避免getCount返回很大值来实现可以“无限”左右滑动的假象。
1. getCount、getPageIndexOfPosition
getCount返回一个很小的值,例如360来让viewPager保证可以左右滑动就行。这里假设实际有n个View,那么getCount返回n + 2就可以了,但是,为了避免频繁的setCurrentItem来重置当前页,这个值用不着太小。
举个例子,对于有n = 5个View需要通过ViewPager来实现无限滑动的情况,getCount返回300,那么在instantiateItem等地方,需要根据ViewPager显示的page的position来得到实际的数据集合里显示的数据的索引:
getPageIndexOfPosition方法的逻辑很直接:
public int getItemIndexForPosition(int position) {
return position % data.size();
}
position就是ViewPager对应展示的page view的位置,position和要展示的数据集合的大小的余数就是对应数据集合的数据的索引。
2. setCurrentItem重置viewPager的当前页
当getCount返回一个不是很大的值的时候,ViewPager很快就会到达左右边界,就无法继续滑动了。
解决方式是在ViewPager快要切换到边界时,使用setCurrentItem把它重置回中间位置。
为ViewPager提供继承自SimpleOnPageChangeListener的类的对象:
viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
if (position <= 2 || position >= adapter.getCount() - 3) {
// 重置页面
int page = adapter.getItemIndexForPosition(position);
int newPosition = adapter.getStartPageIndex() + page;
viewPager.setCurrentItem(newPosition);
}
}
});
注意2点:
重置前后显示的实际数据的位置需要保持不变。
如果考虑到用户体验,为了保证滑动过程中切换page不是非常生硬,可以先setCurrentItem到newPosition +/- 1位置,之后再setCurrentItem(newPosition, true)动画滑动到正确位置。
上面就通过减少getCount的值,结合setCurrentItem完成了ViewPager的无限滑动。
自动轮播
使用handler的sendEmptyMessageDelayed很容易让ViewPager以固定频率自带切换页面。这里强调下,使用线程当然也可以,就是性能上看,避免线程来完成这种“定时”效果——大材小用,Thread是为了不卡顿主线程执行耗时的操作,简单的定时操作handler消息轮询就可以了,app中不要让thread泛滥。
这里handler的所有操作都应该在UI线程中被调用,没有同步的必要:
class AutoScrollHandler extends Handler {
boolean pause = false;
@Override
public void handleMessage(Message msg) {
if (!pause) {
viewPager.setCurrentItem(viewPager.getCurrentItem() + 1);
}
sendEmptyMessageDelayed(msg.what, 3000);
}
void startLoop() {
pause = false;
removeCallbacksAndMessages(null);
sendEmptyMessageDelayed(1, 3000);
}
void stopLoop() {
removeCallbacksAndMessages(null);
}
}
上面pause是为了实现在手指拖拽ViewPager的时候暂停自动轮播,在SimpleOnPageChangeListener中:
@Override
public void onPageScrollStateChanged(int state) {
switch (state) {
case ViewPager.SCROLL_STATE_DRAGGING:
autoSkipHandler.pause = true;
break;
case ViewPager.SCROLL_STATE_IDLE:
autoSkipHandler.pause = false;
break;
}
}
总结
在要展示的View为1个时,没有必要滑动的。
当界面不可见时,可以暂停自动轮播。这样,在onPause和onResume中stopLoop和startLoop,一些情况下onStart和onStop是不执行的。
ViewPager本身的局限性是不适合超大量数据,当然这个假设在实际中又几乎不成立,即便是百万级别的view要展示,viewPager还是不会卡顿。
这里强调的是:既然ViewPager每次只展示最多3个page,而且左右滑动的逻辑可以在每次滑动时进行检查,那么对于任意大的数据集合,它都应该不会卡顿。而且,没有必要在非常大的页码跨度的情况下执行那些根本看不出差别的滑动效果!
实现一个自己的可切换显示View的ViewGroup不是什么难事。最好的,ViewFlow就有这种内置的无限循环滑动的效果,而且自带了简单的pageIndicator那样的小圆点效果。
项目地址是:https://github.com/pakerfeldt/android-viewflow。
非常建议使用。
踩石行动:ViewPager无限轮播的坑的更多相关文章
- ViewPager无限轮播与自定义切换动画
一直在寻求一个能用得长久的ViewPager,寻寻觅觅终于发现,ViewPager有这一个就够了. 注:并非完全原创 先看一下效果: 淡入淡出: 旋转: 无限轮播的ViewPager 主要设计思路(以 ...
- ViewPager +无限轮播+滑动速度修改+指示小点
养成习惯,做过代码记录总结. ViewPager 使用记录 1. ViewPage 位于V4包. 2.主要用来做banner轮播. 3.原理:适配器重用提高效率,与listview等一个原理. 下面记 ...
- ViewPager实现无限轮播踩坑记
最近笔者想通过ViewPager来实现一个广告Banner,并实现无限轮播的效果,但是在这个过程中踩了不少的坑,听我慢慢道来.如果大家有遇到和我一样的情况,可以参考我的解决方法,没有那就更好,如果针对 ...
- Android使用ViewPager做轮播
ViewPager.html div.oembedall-githubrepos { border: 1px solid #DDD; list-style-type: none; margin: 0 ...
- Android真正意义上的无限轮播Banner
在android开发的时候,经常会使用到轮播图,对于这种效果,一般情况下,我们都会使用一种叫做ViewPager的来实现. 传统的实现逻辑是自定义一个View继承ViewPager,在适配器中 将co ...
- 利用RecyclerView实现无限轮播广告条
代码地址如下:http://www.demodashi.com/demo/14771.html 前言: 公司产品需要新增悬浮广告条的功能,要求是可以循环滚动,并且点击相应的浮条会跳转到相应的界面,在实 ...
- ViewPage实现无限轮播画廊效果
1. 效果图 2. 布局文件 主要使用的 android:clipChildren的意思:是否限制子View在其范围内.再父布局和viewpager中设置该属性 ,要显示三个界面 ,还要设置marg ...
- Android实现广告页图片无限轮播
一.概述 对于一个联网的Android应用, 首页广告无限轮播基本已经成为标配了. 那么它是怎么实现的呢? 有几种实现方式呢? 二.无限轮播的实现 1.最常规的手段是用 ViewPager来实现 2. ...
- iOS开发之三个Button实现图片无限轮播(参考手机淘宝,Swift版)
这两天使用Reveal工具查看"手机淘宝"App的UI层次时,发现其图片轮播使用了三个UIButton的复用来实现的图片循环无缝滚动.于是乎就有了今天这篇博客,看到“手机淘宝”这个 ...
随机推荐
- 在 C# 里使用 F# 的 option 变量
在使用 C# 与 F# 混合编程的时候(通常是使用 C# 实现 GUI,F#负责数据处理),经常会遇到要判断一个 option 是 None 还是 Some.虽然 Option module 里有 i ...
- H5坦克大战之【玩家控制坦克移动2】
周一没有看圣诞大战,这几天比较忙也没有看赛后的报道,今天就先不扯NBA,随便扯扯自己.昨天在电脑里找东西的时候翻到以前兼职健身教练时的照片,思绪一下子回到学生时代,脑子久久换不过来.现在深深觉得健身和 ...
- 修改session垃圾回收几率
<?php //修改session垃圾回收几率 ini_set('session.gc_probability','1'); ini_set('session.gc_divisor','2'); ...
- Java中用得比较顺手的事件监听
第一次听说监听是三年前,做一个webGIS的项目,当时对Listener的印象就是个"监视器",监视着界面的一举一动,一有动静就触发对应的响应. 一.概述 通过对界面的某一或某些操 ...
- 说一说python的牛比与不爽
本人写了10年php了.今年开始改写python了.不是说php有什么不好,php在自己的势力范围内还是很牛比的.只是我已经不能满足于php那两亩地了. 习惯了脚本,所以很自然就过度到python了. ...
- linux拷贝命令,移动命令
http://blog.sina.com.cn/s/blog_7479f7990101089d.html
- javaScript生成二维码(支持中文,生成logo)
资料搜索 选择star最多的两个 第一个就是用的比较多的jquery.qrcode.js(但不支持中文,不能带logo)啦,第二个支持ie6+,支持中文,根据第二个源代码,使得,jquery.qrco ...
- mysql-5.6.34 Installation from Source code
Took me a while to suffer from the first successful souce code installation of mysql-5.6.34. Just pu ...
- hbase协处理器编码实例
Observer协处理器通常在一个特定的事件(诸如Get或Put)之前或之后发生,相当于RDBMS中的触发器.Endpoint协处理器则类似于RDBMS中的存储过程,因为它可以让你在RegionSer ...
- U盘安装Kali 出现cd-rom无法挂载 已解决
用U盘安装Kali Linux的过程中,出现cd-rom无法挂载的现象,百度坑比啊,醉了.下面亲测成功 出现无法挂载后,选择执行shell 第一步:df -m此时会看到挂载信息,最下面的是/dev/* ...