写在前面:
最近做的一个小项目有图片轮播的需求,各种查资料发现大部分都是通过设置adapter的getCount方法返回Integer.MAX_VALUE实现的。很显然,这种方法有很多弊端,比如很容易ANR。或者采用其他方法,但在首页和尾页的跳转不够自然。那么有没有比较好的方法呢?在现有方法的基础上,加上自己的思考和改进,我终于很好的实现了ViewPager图片轮播。趁着周末分享给大家,欢迎批评和指正。
首先,很容易想到的是,要想让尾页左滑到首页、首页右滑到尾页有一个平稳的过渡过程,那么在首页尾页的两边必须存在着对应要跳转的页面,然后跳转完成后我们再“偷偷”替换为要显示的真实页面(两者页面显示内容一样,只是位置不一样,“偷偷”指的是用户不可见,在内容不变的情况下替换了页面的位置)。
如上图所示,假定现在想显示4张图片,分别为view0到view3。根据上面的思想:
(1)我们可以在adapter中额外多加两个view,即如图中的红色的view3和view0。这样position1的view0很容易过渡到position0的view3,同理尾页到首页的过渡也很自然。
(2)第1步之后,我们很轻易的发现一个问题:滑到position0时,右滑之后并没有对应的view可以显示,同理position5。这就需要把position0的view3偷偷换成position4的view3,这样position0的右滑也就是position4的右滑,即滑到view2,很好的满足了设想的需求。
(1)要实现第一步,只要重写adapter的一些方法即可。
private class ImagePagerAdapter extends PagerAdapter{
private String[] images;
private LayoutInflater inflater;
public ImagePagerAdapter(String[] images) {
this.images = images;
this.inflater = getLayoutInflater();
}
@Override
public int getCount() {
//返回实际要显示的图片数+2
return images.length + 2;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
//注意不要remove 否则容易闪屏
// ((ViewPager)container).removeView((View) object);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View mView = View.inflate(getApplicationContext(),R.layout.activity_uil_viewpager_item,null);
//这是重点
int realPosition = (position - 1 + images.length)%images.length;
ImageView imageView = (ImageView) mView.findViewById(R.id.myimage);
final ProgressBar bar = (ProgressBar) mView.findViewById(R.id.loading);
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG,"hahaha click");
}
});
//通过UIL加载图片
imageLoader.displayImage(images[realPosition], imageView, ImageLoaderHelper.getInstance(getApplicationContext()).getSimpleDisplayImageOptions(),
new SimpleImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {//开始加载的时候执行
bar.setVisibility(View.VISIBLE);
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {//加载成功的时候执行
bar.setVisibility(View.GONE);
}
@Override
public void onLoadingCancelled(String imageUri, View view) {//加载取消的时候执行
super.onLoadingCancelled(imageUri, view);
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {//加载失败的时候执行
String message = null;
switch (failReason.getType()){//加载失败类型
case IO_ERROR:// 文件I/O错误
message = "IO_ERROR";
break;
case DECODING_ERROR:// 解码错误
message = "DECODING_ERROR";
break;
case NETWORK_DENIED:// 网络延迟
message = "NETWORK_DENIED";
break;
case OUT_OF_MEMORY:// 内存不足
message = "OUT_OF_MEMORY";
break;
case UNKNOWN:// 原因不明
default:
message = "UNKNOWN";
break;
}
bar.setVisibility(View.GONE);
}
}, new ImageLoadingProgressListener() {
@Override
public void onProgressUpdate(String s, View view, int i, int i1) {//在这里更新 ProgressBar的进度信息
int progress = 100*i/i1;
bar.setProgress(progress);
}
});
((ViewPager)container).addView(mView,0);
return mView;
}
}
以上代码,关于图片资源的UIL获取可忽略。比较重要的是getCount()返回实际要显示的图片数+2,这样才能在首尾附加两个view。另外一个是在instantiateItem()方法中int realPosition = (position - 1 + images.length)%images.length;这里的realPosition对应要显示View的index。由上图,这个关系也是显而易见的。此外,还要注意的是在destroyItem()中不要removeView否则快速滑动的时候容易出现闪屏。
PS:由上图可以发现,应该初始化ViewPager.setCurrentItem(1);才能从预设的第一页开始播放。
(2)至于第二步,“偷偷”替换的过程,主要和PageChangeListener的三个要实现的方法有关,这里先把主要代码附上。
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
/**
* 当页面在滑动了调用
* @param position 当前页面,即点击滑动的页面
* @param positionOffset 当前页面偏移的百分比
* @param positionOffsetPixels 当前页面偏移的像素位置
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (position == Constants.images.length && positionOffset > 0.99) {
//在position4左滑且左滑positionOffset百分比接近1时,偷偷替换为position1(原本会滑到position5)
mViewPager.setCurrentItem(1, false);
} else if (position == 0 && positionOffset < 0.01) {
//在position1右滑且右滑百分比接近0时,偷偷替换为position4(原本会滑到position0)
mViewPager.setCurrentItem(4, false);
}
}
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete. 一般在滑动30%的时候就会调用
*
* @param position Position index of the new selected page.
*/
@Override
public void onPageSelected(int position) {
//当有手动操作时,remove掉之前auto的runnable。延迟将由手动的这次决定。
//总之,一个页面selected之后 最多只有一个runnable,要把多的remove掉
handler.removeCallbacks(runnable);
Log.d(TAG, "onPageSelected,page:" + position);
if (position != Constants.images.length+1 && position != 0){
handler.postDelayed(runnable,3*1000);
}
}
@Override
public void onPageScrollStateChanged(int state) {
switch (state) {
case 0://什么都没做 空闲状态
break;
case 1://正在滑动
break;
case 2://滑动完毕
break;
}
}
});
onPageScrolled:
页面滚动的时候就会调用,三个参数如注释所示。值得注意的是:
关于左滑:在一个页面中,比如position4,左滑过程中onPageScrolled的position参数一直为4,positionOffset由0递增无限接近1。
关于右滑:比如当前页面在position1,右滑过程中,position参数立刻变为0,且positionOffset由1递减至0.
(这里为了便于帮助理解首尾页跳转逻辑,特地选了position1和position4,其实对所有位置都是这样。大家可以自己左右滑动观察打印信息,这一点对页面跳转的条件的理解很重要),这里positionOffset > 0.99和positionOffset < 0.01的限制条件是为了在整个页面几乎全部显示出来之后默默替换,否则页面转换不够自然。此外,不能够限定为1和0,所以我取了比较接近的数值。
此外,mViewPager.setCurrentItem(1, false);中第二个参数为false表示页面切换无动画,这里由于当前显示内容和待切换内容一致,因而用户察觉不到位置替换过程。参数为true,则为平滑过渡。
PS:这里易犯的错误是,在onPageSelected方法中实现页面切换。其实这个方法在页面还在滑动的过程中就会调用(可见官方文档说明,我这里多次观察在偏移量为30%左右的时候就会调用),这样会有明显不自然的切换效果。
关于自动轮播,一般都是采用handler+timer的方法。还应当注意的是,手动滑动后应该重新计时。
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "have received a msg");
int curindex = (mViewPager.getCurrentItem()+1)%(Constants.images.length+2);
mViewPager.setCurrentItem(curindex,true);
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
Message message = new Message();
handler.sendMessage(message);
}
};
页面加载的时候先调用一次 handler.postDelayed(runnable, 3 * 1000);实现一次页面自动切换,然后重点在onPageSelected()方法中的处理,实现自动轮播。
@Override
public void onPageSelected(int position) {
//当有手动操作时,remove掉之前auto的runnable。延迟将由手动的这次决定。
//总之,一个页面selected之后 最多只有一个runnable,要把多的remove掉
handler.removeCallbacks(runnable);
Log.d(TAG, "onPageSelected,page:" + position);
if (position != Constants.images.length+1 && position != 0){
handler.postDelayed(runnable,3*1000);
}
}
注意前面的handler.removeCallbacks(runnable);是很必要的,防止手动+自动会产生多余的post从而加快页面切换间隔(之前忽略了这点纠结了很久)。
源码见:https://github.com/YangLili1994/MyUniversalImageLoader