ViewPager是一个非常常用的Android控件,除了列表页面并列的时候使用外,还有一个很常见的应用场景就是轮播图,也就是Carousel或者Banner,一般其展示内容都是图片或者广告之类的。往往在轮播图里,需要实现自动播放,即不需要用户用手指滑动ViewPager,也能自己不断的往下切换数据元素,当跳转到最后一条时,接下来又重新回到最开始的位置。
一般来说,实现自动播放与无限循环不难,前者使用RxJava或者Runnable+Handler实现定时器然后不断的给ViewPager调用setCurrentItem方法,后者则是在PagerAdapter的getCount方法设置一个极大的值(一般是整型的上限),并且利用好取余的方法来从实际的数据List里获取数据即可。
一切好像看起来好像都没啥问题,是吧?但作为一个要进阶的Android Developer,我们不能仅仅满足于功能的实现,而且还要考虑一下体验的问题。我们先用一个简单的Demo,实现一个非常原始的轮播图,多余的代码就不罗列了,主要给一下PagerAdapter的实现,以及用Runnable+Handler实现的定时轮播,其他代码可以参考ViewPager与PagerAdapter使用详解与源码解析一文。注意啦,内存泄漏、取消Runnable等操作请自行处理,我为了省事就不额外写代码了。
初始化了一些必要的数据:
// 这里初始化了一个包含4个图片url的List,就不列举图片地址了 private List<String> mImageList = Arrays.asList("", "", "", ""); // Glide需要的一个配置选项 private RequestOptions options = new RequestOptions() .priority(Priority.NORMAL) .placeholder(R.mipmap.ic_launcher) .error(R.mipmap.ic_launcher) .skipMemoryCache(false) .diskCacheStrategy(DiskCacheStrategy.ALL); private Handler handler = new Handler();
然后就是实现一个PagerAdapter:
private class CarouselAdapter extends PagerAdapter { @Override public int getCount() { // 为了无限轮播,所以取整型的极限值。 return Integer.MAX_VALUE; } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { return view == o; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { ImageView imageView = new ImageView(VPActivity.this); imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); imageView.setScaleType(ImageView.ScaleType.FIT_XY); // mImageList是一个包含了几个图片url的List String url = mImageList.get(position % mImageList.size()); // 使用Glide加载图片,options是提前定义好的一个RequestOptions对象(Glide4.0后新添加的东西)。 Glide.with(VPActivity.this) .load(url) .apply(options) .into(imageView); container.addView(imageView); return imageView; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((ImageView)object); } }
然后我们在给ViewPager绑定了PagerAdapter后,使用Runnable+Handler来实现轮播:
Runnable runnable = new Runnable() { @Override public void run() { vpTest.setCurrentItem(vpTest.getCurrentItem() + 1, true); handler.postDelayed(this, 1000); } }; runnable.run();
运行结果:
不过呢,很多人会注意到一个问题:切换的太快了!用户看到这么快的切换动画,体验不太好。如果这个切换过程能慢一点,就好多了。注意了,刚才我设置的postDelayed间隔是1秒钟,但切换间隔与此无关,即使你设置成10秒,在切换的时候依然速度快的飞起!想要改善用户体验,让切换动画变得慢一点是很有必要的。
然而,ViewPager并没有提供相应的接口,所以只能去看它的源码了。很幸运,我们可以在ViewPager的源码里,找到这样一个变量:
private Scroller mScroller; // 就不复制它的初始化代码了
而且可以在源码里很清楚的看出来,setCurrentItem方法最终会调用smoothScrollTo(int x, int y, int velocity)方法,而其中又是依靠
this.mScroller.startScroll(sx, sy, dx, dy, duration);
这行代码,来把滑动动画的各种参数进行了设置,当然包括动画的持续时间。然后,再通过
ViewCompat.postInvalidateOnAnimation(this);
来实现动画的执行。
显然,如果能把tartScroll(sx, sy, dx, dy, duration)中的duration值进行改动,就算完成了我们减慢ViewPager切换动画,动态设定动画时间的目标了。不过呢,这个duration只是smoothScrollTo方法内部的一个局部变量,我们是改动不了的,所以就需要另辟蹊径了。
思路并不麻烦,可以一步一步的推:我们需要修改mScroller.startScroll的duration参数,但是这个参数获取不到。那么如果我们自己去继承Scroller类,并且覆盖startScroll方法,调用super.startScroll(startX, startY, dx, dy, mDuration)的时候,直接把这个间隔时间的值改掉,不就可以了?不过,ViewPager源码中的mScroller是private私有的,显然,想要替换替换掉它,需要依赖反射了。下面给出实现的代码:
// 自定义的Scroller public class ViewPagerScroller extends Scroller { private int mDuration = 1000; public ViewPagerScroller(Context context) { super(context); } public ViewPagerScroller(Context context, Interpolator interpolator) { super(context, interpolator); } public ViewPagerScroller(Context context, Interpolator interpolator, boolean flywheel) { super(context, interpolator, flywheel); } @Override public void startScroll(int startX, int startY, int dx, int dy, int duration) { super.startScroll(startX, startY, dx, dy, mDuration); } @Override public void startScroll(int startX, int startY, int dx, int dy) { super.startScroll(startX, startY, dx, dy, mDuration); } // 方法支持代码动态设定动画时间,实现改变ViewPager切换位置的滑动速度 public void setDuration(int time) { mDuration = time; } }
然后是通过反射来改变ViewPager中的mScroller:
try { Field mScroller = ViewPager.class.getDeclaredField("mScroller"); mScroller.setAccessible(true); ViewPagerScroller scroller = new ViewPagerScroller(this, new AccelerateInterpolator()); scroller.setDuration(500); mScroller.set(vpTest, scroller); } catch (Exception e) { e.printStackTrace(); }
这里我把动画执行时间设置为500毫秒,然后看一下运行结果:
恩,这样的滑动速度就舒服多了。
评论