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方法,调用其父 ...
随机推荐
- Zxing2.1扫描取景框变形问题解决
修改竖屏扫描的贴子,2.0之前的都很适用.可是到了2.1,有些贴子的做法可以将扫描框改为竖屏,但是取景框里扫描到的东西是变形的(扁的),本人仔细研究一番,终于解决了这个问题,下面贴出解决办法: 1.修 ...
- 【轻松前端之旅】<!DOCTYPE>标签
前端学习,先学习HTML,CSS,Javascript HTML - HyperText Markup Language HTML-超文本标记语言,提供了一种标记网页内容的方法. 浏览器怎么知道如何显 ...
- docker 批量操作容器
docker stop $(sudo docker ps -q)
- 如何将字符串转化为Jsoup的Document 对象
有些时候在java操作解析html元素的时候比较繁琐,今天螃蟹就介绍一种可将html转换为document对象的方法——jsoup jsoup为我们解析html提供了比较全的API接口,我们通过将ht ...
- C# 编码标准(一)
一直想写一个自己用的代码标准,经过一段时间的优秀开源源码的观察和看其他人写的标准,感觉好的代码给人感觉就是舒服,也非常重要.所以把它们记录归纳总结,以备以后忘记,另外平时写代码的时候可以拿来参考下.下 ...
- 实现两个sym转一个sym
CVO输出如果是一个像素并行输出,选择内嵌人插入同步码.如果两个像素并行输出是不能选择内嵌的,只能选择分离的方式.如果把输出的并行数据给VIP并且要求是内嵌,那只能在内部转或者外部转. 这里是实现外部 ...
- cyclone IV中DDR的一个报错{Too many output and bidirectional pins per VCCIO and ground pair in I/O bank 8 }
Error (169224): Too many output and bidirectional pins per VCCIO and ground pair in I/O bank 8 when ...
- Effective C++ 随笔(5)
条款27:尽量稍作转型动作 const_cast:常量性移除 dynamic_cast:安全向下转型 reinterpret_cast: static_cast: 如在子类当中享调用父类当中的某个方法 ...
- C#的委托与Java的自定义接口的异曲同工的同步操作
C#的委托(以WinForm为例) 在子窗体(ChildFrm)中定义一个委托 this.CaptureListener(callback);//子窗体触发委托事件,以告诉调用的窗体 /// < ...
- css3种引入方式,样式与长度颜色,常用样式,css选择器
# CSS三种引入方式 ## 一.三种方式的书写规范 #### 1.行间式 ```html<div style="width: 100px; height: 100px; backgr ...