踩石行动: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的复用来实现的图片循环无缝滚动.于是乎就有了今天这篇博客,看到“手机淘宝”这个 ...
随机推荐
- 客户端的验证插件validator
简单,智能,令人愉悦的表单验证~~~ 官方文档:http://www.niceue.com/validator/ <!DOCTYPE html> <html> <head ...
- redis 学习笔记(1)
redis持久化 snapshot数据快照(rdb) 这是一种定时将redis内存中的数据写入磁盘文件的一种方案,这样保留这一时刻redis中的数据镜像,用于意外回滚.redis的snapshot的格 ...
- 玩转spring boot——结合jQuery和AngularJs
在上篇的基础上 准备工作: 修改pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q ...
- CSS——关于z-index及层叠上下文(stacking context)
以下内容根据CSS规范翻译. z-index 'z-index'Value: auto | <integer> | inheritInitial: autoApplies to: posi ...
- css3圆形百分比进度条的实现原理
原文地址:css3圆形百分比进度条的实现原理 今天早上起来在查看jquery插件机制的时候,一不小心点进了css3圆形百分比进度条的相关文章,于是一发不可收拾,开始折腾了... 关于圆形圈的实现,想必 ...
- LINQ to SQL Where条件
1. 适用场景 实现条件的过滤和查询等功能. 2. 说明 跟SQL语句中的where作用相似,都起到了范围的限定即过滤的作用,而判断条件是紧跟后面的条件子句.where主要分为三种形式:简单形式.条件 ...
- UML图中经常用到几种的关系图例
学习这个东西挺奇怪的,时间一长就容易忘记,或者记不清楚.今天看到一些UML图的关系,发现有些出入了,索性就写下来,以后再忘记的时候过来看看. 在UML的类图中,常见的有以下几种关系: 继承(Gener ...
- (一)Spark简介-Java&Python版Spark
Spark简介 视频教程: 1.优酷 2.YouTube 简介: Spark是加州大学伯克利分校AMP实验室,开发的通用内存并行计算框架.Spark在2013年6月进入Apache成为孵化项目,8个月 ...
- Jquery 获得当前标签的名称和标签属性
得到标签的名称 $("#name").prop("tagName"); 或者 $("#name")[0].tagName; 注意:1.得到的 ...
- vue2.0构建淘票票webapp
项目描述 之前一直用vue1.x写项目,最近为了过渡到vue2.0,特易用vue2.0栈仿写了淘票票页面,而且加入了express作为后台服务. 前端技术栈:vue2.0 + vue-router + ...