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的关系,是视图和视图数据适配器的关系——满满都是模式。

  1. ViewPager.setAdapter(PagerAdapter adapter)

    首先把创建好的PagerAdapter对象设置给ViewPager对象,这样,它们就关联了。ViewPager就展示了此PagerAdapter的数据。

  2. ViewPager.setCurrentItem(int item)

    设置viewPager当前展示的page位置,默认是0。

  3. 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无限滑动的更多相关文章

  1. 一行代码引入 ViewPager 无限循环 + 页码显示

    (出处:http://www.cnblogs.com/linguanh) 前序: 网上的这类 ViewPager 很多,但是很多都不够好,体现在 bug多.对少页面不支持,例如1~2张图片.功能整合不 ...

  2. ViewPager结合view无限滑动

    使用viewPager进无限滑动,这里的实现是在适配器里面进行,当然在外头使用滑动监听也行. import android.support.v4.view.PagerAdapter; import a ...

  3. 踩石行动:ViewPager无限轮播的坑

    2016-6-19 前言 View轮播效果在app中很常见,一想到左右滑动的效果就很容易想到使用ViewPager来实现.对于像我们常说的banner这样的效果,具备无限滑动的功能是可以用ViewPa ...

  4. 详细分析Android viewpager 无限循环滚动图片

    由于最近在忙于项目,就没时间更新博客了,于是趁着周日在房间把最近的在项目中遇到的技术总结下.最近在项目中要做一个在viewpager无限滚动图片的需求,其实百度一下有好多的例子,但是大部分虽然实现了, ...

  5. ViewPager无限轮播与自定义切换动画

    一直在寻求一个能用得长久的ViewPager,寻寻觅觅终于发现,ViewPager有这一个就够了. 注:并非完全原创 先看一下效果: 淡入淡出: 旋转: 无限轮播的ViewPager 主要设计思路(以 ...

  6. ViewPager实现滑动翻页效果

    实现ViewPager的滑动翻页效果可以使用ViewPager的setPageTransformer方法,如下: import android.content.Context; import andr ...

  7. Android实现ViewPager无限循环滚动回绕

     Android实现ViewPager无限循环滚动回绕 Android系统提供的ViewPager标准方式是左右可以自由滑动,但是滑动到最左边的极限位置是第一个page,滑动到最右边的位置是最后一 ...

  8. 去掉SrollView、GrdiView、ListView、ViewPager等滑动到边缘的光晕效果

    当我们使用SrollView.GrdiView.ListView.ViewPager带有滑动功能的组件时,滑动到边缘时总会出现类光晕效果.这是用于提示用户已经滑动到了组件的边缘,不能再滑动了,但有时候 ...

  9. ViewPager相互嵌套,导致子ViewPager无法滑动,且子ViewPager中的view无法被点击

        场景:当使用ViewPager进行嵌套的时候,子viewPager是无法进行嵌套的,因此我们要重写ViewPager类,并重写里层viewPager类中的onTouchEvent方法,调用其父 ...

随机推荐

  1. jQuery加载完成事件 $(function(){ })的全局异常拦截

    通常我们在页面加载完成的时候要写入一些功能脚本,如: $(function(){/*脚本 - 1*/ console.log('start'); }) $(function(){/*脚本 - 2*/ ...

  2. Eclipse编辑XML自动提示(zz)

    Eclipse编辑XML自动提示 博客分类: j2se XMLEclipseiBATISSpringSQL  IED Eclipse Java EE IDE for Web Developers: D ...

  3. hdu6069 多校Counting Divisors

    思路:对于n^k其实就是每个因子的个数乘了一个K.然后现在就变成了求每个数的每个质因子有多少个,但是比赛的时候只想到sqrt(n)的分解方法,总复杂度爆炸,就一直没过去,然后赛后看官方题解感觉好妙啊! ...

  4. ServiceDesk Plus更有序地组织IT项目

  5. SQL SERVER 如果判断text类型数据不为空

    一个字段Remark的数据类型设置先设置为varcharr(255),后来考虑到扩展性需要将其定义为TEXT类型,但是SQL 语句报错.      SQL 语句:      SELECT * FROM ...

  6. 2018.12.15 poj3415 Common Substrings(后缀自动机)

    传送门 后缀自动机基础题. 给两个字符串,让你求长度不小于kkk的公共子串的数量. 这题可以用后缀自动机解决废话 考虑对其中一个字串建出后缀自动机,然后用另一个在上面跑,注意到如果一个状态有贡献的话, ...

  7. 使用docker 安装 GITLIB

    在安装 gitlib 社区版时,配置老不成功,改成使用docker安装 比较顺利,省事. 1外部卷配置 docker 需要配置一些卷在外部,创建一下git的目录 我们创建一个在home下 创建一个gi ...

  8. vue 开发系列(八) 动态表单开发

    概要 动态表单指的是我们的表单不是通过vue 组件一个个编写的,我们的表单是根据后端生成的vue模板,在前端通过vue构建出来的.主要的思路是,在后端生成vue的模板,前端通过ajax的方式加载后端的 ...

  9. boost--signal

    1.signals2库 signals2库实现了线程安全的观察者模式,在signals2中观察者模式被称为信号/插槽(signals/slots),它是一种函数回调机制.一个信号可以关联一个或多个插槽 ...

  10. 研究生flag

    是时候定个计划了,感觉日子一天天水,不加油学点东西,迟早要掉队…… 刷刷算法题库吧,貌似选几个管用的刷刷——https://hihocoder.com/problemset 争取明年三月份的PAT顶级 ...