Android实现真正的ViewPager【平滑过渡】+【循环滚动】!!!顺带还有【末页跳转】。
实现真正的ViewPager【平滑过渡】+【循环滚动】!!!顺带还有【末页跳转】。
首先呢, 我要对网上常见的3种ViewPager的循环滚动方法做个概述。急需看真正实现方法的同志请选择性忽略下面这一长段。不过有时间精力的话还是看看,尤其后两者,我的方法是基于这两者的:
-
不讲道理式。特点:不管3721,先在PagerAdapter的getCount();里返回一个很大的值。保守的返回个100,极端的直接Integer.MAX_VALUE。看到这里估计就已经有很多孩子笑了,这尼玛跟大学C语言课本上,数组长度未知就定义个1000单位的思想简直如出一辙啊有木有!!!好吧,那么这种方法显然并没有真的解决问题,虽然不会有几个用户真有耐心去滑动Integer.MAX_VALUE / 2次。。。不过鉴于目前网上其它方法都有这样那样的缺陷,还是有不少人宁愿选择这个的。那么我就说详细点吧。这个方法后续操作中,比较关键的步骤有两点: 一、一定要设置初始位置为Integer.MAX_VALUE / 2,即
setCurrentItem(Integer.MAX_VALUE / 2);
,这样才能保证一开始就能双向滑动。 二、要在自定义的PagerAdapter的instantiateItem(ViewGroup arg0, int arg1);【较早的Api里为View arg0,已废弃】方法里返回当前位置对总页面数的模。即views.get(arg1 % views.size());
-
逻辑大神式。特点:试图通过OnPageChangeListener接口的onPageScrolled();、onPageSelected();、onPageScrollStateChanged();三个方法,来判别出当前是否滑动到了首/末位置,且用户是否仍在向后/前滑动,然后setCurrentItem();。这种方法其实是比较负责任的,但十分辛苦,且到现在也没能发现一套比较合适的判别标准。不过不代表没有,有志于此的同志请继续努力!这里列出我个人在大量尝试后总结出的需要注意的几点供有志者参考: 一、
onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
此方法的几个参数十分难以把握!!!具体规则我这么描述:position为当前屏幕上所露出的所有View的Item取下限。比如,当前Item为3,轻轻向右滑动一下,2露出了一点点,那么position就是2,这点很重要,是我的末页跳转方法的基础!;而如果向左滑动,露出的4比3大,那么只要3没完全隐匿,那么position就一直按照3算。positionOffset是当前Item较大的那个View占视图的百分比,0-1,没有负数!当滑动结束时,onPageScrolled();最后一次调用,positionOffset为0。是的,这个方法的调用是连续的,也就是说,用户一次滑动,这个方法会持续调用N多次,直至某个View充满视图并且稳定住!(但具体调用次数也不确定,尤其在首末位置向边界滑动,如果Log一下,会看到出现调用不确定次数的打印,且positionOffset都为0. 二、onPageSelected(int position);
更恶心。。。这个方法是在onPageScrolled();的一系列调用中间插入进去的,而不是常识的在其结束并稳定在某个视图后!大致规律是positionOffset向左向右越过某个值左右时触发,这个值不总是0.5,它与用户的滑动速度、拖甩手势等相关,应该是有一套计算公式的!而一旦这个方法触发,意味着马上就要进入position位置的view了(而不是已经),且这一动作不可被撤销,所以看起来好像是在进入后才调用的。 三、onPageScrollStateChanged(int state);
这个方法是唯一比较正常的。在一次滑动中,其状态值及改变顺序为:1-开始滑动、2-越过触发onPageSelected();的值、0-滑动结束(呃,其实我也记不清了,反正这个方法对我没啥用。。。) -
接近成功式。是的,这个方法行得通!只是牺牲了一些细节,过渡不太平滑,虽然它看起来可以。这个方法的思想是设置标志位置。设原本的View切换顺序:0->1->2->3,那么现在,在第一个位置之前加插入3,最后一个位置之后插入0,得到3->0->1->2->3->0,然后如果滑动到了第一个3,则跳转到第二个3,滑动到第二个0,则转到第一个0。
看代码:
(OnPageChangeListener) @Override public void onPageSelected(int position) { // TODO Auto-generated method stub if (position == 0) viewPager.setCurrentItem(views.size() - 2); else if (position == views.size() - 1) viewPager.setCurrentItem(0); }
是的,我仿佛听到了恍然大悟的声音。这个方法的确巧妙,已经无限接近成功。但可惜的是,这里面有些注意事项,网上那个发明这方法的人并未列出(网上太乱了,真心不知道原作者是谁,不过也得感谢一下),使得一些情况下莫名其妙的失败了。更主要的是,这个方法并不能平滑过渡,虽然它看起来好像可以,但你会发现首位连接处丢了半个周期的滑动。但这并不是不可逆转的,我的方法就是对该方法的一种修正。
好,现在是正文(前戏有点长了啊。。。)
=========================================================
=======================华丽的分割线==========================
=========================================================
-
先上盘小菜:【末页跳转】。
其实许多的程序引导页做到这点,就是不知道方法是否和我的一样。先看代码:
(OnPageChangeListener) boolean isEnd; @Override onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // TODO Auto-generated method stub if (isEnd&&position== views.size() - 1) finish(); else isEnd = (position== views.size() - 1); }
就是这短短几行代码。具体什么意思呢?首先是一个布尔变量,记录当前是否滑动到了末位值。滑动到末位值的特征是什么呢?就是position为views.size() - 1。那么再看if里面的条件:如果上次滑动到了末位值,并且position== views.size() - 1,这个并且后面的是怎么回事呢?如果滑动到了末位值再往回滑动岂不是也会跳转?如果你看了之前对网上方法的分析第二条,你可能就明白了,是的,position并不是当前view的Item,也不是当前占据比例较大的Item,它只是当前视图中说显示的view里Item较小的那个的Item。当往回滑动时,由于前一个View的Item较小,position为views.size() - 2,自然触发不了跳转。而这时就会进入else,isEnd被置否了。但不用担心,只要用户没有真的回到前一页,最终onPageScrolled会被最后一次调用且参数position回到views.size() - 1,使isEnd 停留在true。当且仅当用户再向后滑动时,onPageScrolled才会以(views.size() - 1, 0, 0)的参数,触发if,进而跳转。
-
重头戏,【平滑过渡】+【循环滚动】。
这个方法是根据网上第三种方法优化而来的:
//首先得到View的集合: ArrayList<View> views = new ArrayList<>(); for (Drawable image : images) { ImageView imageView = new ImageView(this); imageView .setImageDrawable(image ); views.addView(imageView ); } //错误的做法,View不能复用! //(或者你去PagerAdapter里面在把View添加进ViewGroup那里做手脚) /* //首位插入末位的View views .add(0, views .get(views .size() - 1)); //末尾增加首位(现在是次位)的View views .add(0, views .get(1)); */ //然而图片是可以复用的 //首位插入末位的图片 ImageView tempImageView = new ImageView(context); tempImageView .setImageDrawable( ((ImageView) views .get(views .size() - 1)).getDrawable()); views.add(0, tempImageView); //末尾增加首位(现在是次位)的图片 tempImageView = new ImageView(context); tempImageView .setImageDrawable( ((ImageView) views .get(1)).getDrawable()); views.add(tempImageView); //设置ViewPager ViewPager viewPager = new ViewPager(this); viewPager .setAdapter(new MyPagerAdapter(views)); viewPager .setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { //第二个参数false。默认true是平滑过渡,但现在其实已经过渡完成,自然应当是false if (position == 0 && positionOffset == 0) viewPager .setCurrentItem(views.size() - 2, false); else if (position == views.size() - 1 && positionOffset == 0) viewPager .setCurrentItem(1, false); } @Override public void onPageSelected(int position) {} @Override public void onPageScrollStateChanged(int state) { } }); //从随机位置开始,重要的是一定不能是首末位置(0、views.size() - 1) viewPager .setCurrentItem(1 + new Random().nextInt(views.size() - 2));
关键的地方注释都写好了。我再简单解释一下,为什么我不在onPageSelected();方法里做跳转的判断呢?如果认真看了之前对网上第二种方法的分析就会明白。onPageSelected方法是在跳转过程中调用的,所以会导致首位连接处的跳转少半个周期。这就是网上第三种方法出错的地方。然后就是几个注意事项,setCurrentItem()要带上第二个参数,设为false、View不能复用、要设置初始位置不为首末位置。
ok,就这样了。这片博客有点长啊,要耐心看完哦~(我是不是该把这句话放开头?看到这句话的举爪~~~)
2016-08-20:感谢@suwueng指出,这个方法在从末尾滑动到页首的时候会出现一下闪烁,大概是那一页还没完全加载出来而直接set引起的。所以这个方法还是有缺陷的,如果谁能想出好的解决方法还请务必分享一下。
附上我的小游戏链接,数码宝贝2048,在里面选择数码宝贝的地方用到了这个。GitHub