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 无限循环 + 页码显示
(出处:http://www.cnblogs.com/linguanh) 前序: 网上的这类 ViewPager 很多,但是很多都不够好,体现在 bug多.对少页面不支持,例如1~2张图片.功能整合不 ...
- ViewPager结合view无限滑动
使用viewPager进无限滑动,这里的实现是在适配器里面进行,当然在外头使用滑动监听也行. import android.support.v4.view.PagerAdapter; import a ...
- 踩石行动:ViewPager无限轮播的坑
2016-6-19 前言 View轮播效果在app中很常见,一想到左右滑动的效果就很容易想到使用ViewPager来实现.对于像我们常说的banner这样的效果,具备无限滑动的功能是可以用ViewPa ...
- 详细分析Android viewpager 无限循环滚动图片
由于最近在忙于项目,就没时间更新博客了,于是趁着周日在房间把最近的在项目中遇到的技术总结下.最近在项目中要做一个在viewpager无限滚动图片的需求,其实百度一下有好多的例子,但是大部分虽然实现了, ...
- ViewPager无限轮播与自定义切换动画
一直在寻求一个能用得长久的ViewPager,寻寻觅觅终于发现,ViewPager有这一个就够了. 注:并非完全原创 先看一下效果: 淡入淡出: 旋转: 无限轮播的ViewPager 主要设计思路(以 ...
- ViewPager实现滑动翻页效果
实现ViewPager的滑动翻页效果可以使用ViewPager的setPageTransformer方法,如下: import android.content.Context; import andr ...
- Android实现ViewPager无限循环滚动回绕
Android实现ViewPager无限循环滚动回绕 Android系统提供的ViewPager标准方式是左右可以自由滑动,但是滑动到最左边的极限位置是第一个page,滑动到最右边的位置是最后一 ...
- 去掉SrollView、GrdiView、ListView、ViewPager等滑动到边缘的光晕效果
当我们使用SrollView.GrdiView.ListView.ViewPager带有滑动功能的组件时,滑动到边缘时总会出现类光晕效果.这是用于提示用户已经滑动到了组件的边缘,不能再滑动了,但有时候 ...
- ViewPager相互嵌套,导致子ViewPager无法滑动,且子ViewPager中的view无法被点击
场景:当使用ViewPager进行嵌套的时候,子viewPager是无法进行嵌套的,因此我们要重写ViewPager类,并重写里层viewPager类中的onTouchEvent方法,调用其父 ...
随机推荐
- 【Web】网页字体图标的使用
字体图标介绍 网页中图片有很多优点,但也有很多缺点,会增加文件的大小以及增加http请求.这时候就需要用的字体图标(iconfont).字体图标的优点,可以跟图片一样改变透明度.旋转等,本质上是文字, ...
- 使用SpringMVC的@CrossOrigin注解解决跨域请求问题
跨域问题,通俗说就是用ajax请求其他站点的接口,浏览器默认是不允许的.同源策略(Same-orgin policy)限制了一个源(orgin)中加载脚本或脚本与来自其他源(orgin)中资源的交互方 ...
- 使用EventLog Analyzer进行VMware日志管理
- P3834 【模板】可持久化线段树 1(主席树)
#include <bits/stdc++.h> #define read read() #define up(i,l,r) for(int i = (l);i <= (r);i++ ...
- Cisco interview
A. 1. Self-introduction I am Yanlin He . I am a master degree candidate of school of infomation sci ...
- js保存,获取,删除cookie的操作
[转]来自:http://blog.csdn.net/itmyhome1990/article/details/7363816 JSP表单页面 <form action="login& ...
- (5)How to let go of being a "good" person — and become a better person
https://www.ted.com/talks/dolly_chugh_how_to_let_go_of_being_a_good_person_and_become_a_better_perso ...
- ubuntu上安装win7系统(64位的)
http://www.linuxidc.com/Linux/2012-11/74195.htm deb文件在ubuntu上直接用dpkg -i xxx.deb 如果虚拟机上只显示32位,则可能是cpu ...
- 第30章:MongoDB-索引--地理信息索引
①地理信息索引 地理信息索引分为两类:2D平面索引,另外就是2DSphere球面索引.在2D索引里面基本上能够保存的信息都是坐标,而且坐标保存的就是经纬度坐标. 范例:定义一个商铺的集合 db.sho ...
- 使用函数的列 group by 分组需要别名
问题描述 使用如下截图的SQL统计数据,报1105错误,提示all columns in group by clause should be in the selected column. 给格式化 ...