天天看点

ViewPager(七)让ViewPager用起来更顺滑——轮播、禁止滑动与指示器的配合

用法概述:

1、换页监听与换页方法

2、懒加载及预加载定制

3、设置间距与添加转场动画

4、轮播、禁止滑动与指示器的配合

ViewPager的技巧部分迎来了收关,这次我们更强调ViewPager的综合运用能力。无论是ViewPager的改造变种,还是与指示器的配合使用,都拓展了ViewPager的应用场景。

轮播、禁止滑动与指示器的配合

轮播更准确来说,叫自动轮播。不用手滑,像放电影一样的循环播放。禁止滑动是指,可以通过按键控制跳转,但不能滑动切换。而指示器就是指示当前页面的位置信息,内容信息的一类控件,往往和ViewPager配合使用的。

轮播Banner

anner是一个在应用中普遍存在的需求,什么是banner呢,就是一个自动循环的广告。

这里就是两个关键词:自动和循环

这两个原生的ViewPager都没有默认实现,也没有给暴露接口。为了实现这个效果,并且ViewPager对于子View的布局以及预加载机制已经非常成熟,所以我们没必要再手写一套ViewPager。但是我们有想利用ViewPager的优秀的特性。这时候我们就要用组合设计模式,结合组合模式自定义View,我们按照我们的默认规则对ViewPager进行逻辑控制,然后这个View对开发者透明,只暴露几个方法,然后我们就能实现这个banner了。

第一个关健词,自动,这个可以用handler迭代发消息实现

private int delayTime = 2000;
 //执行滚动runnable任务,后一个参数是间隔时间
mHandler.postDelayed(scrollTask, delayTime);

private Runnable scrollTask = new Runnable() {
        @Override
        public void run() {
            if (!isAutoPlay || realCount <= 1) return;
            //获取当前页码,增加一后代码执行跳转
            mCurrentItem = mPager.getCurrentItem();
            Log.e("Runnable", "自动轮播位置:" + mCurrentItem);
            mPager.setCurrentItem(mCurrentItem + 1, true);
            //任务里重复执行,实现自动的效果
            mHandler.postDelayed(scrollTask, delayTime);
        }
    };
           

第二个关键词,循环,应该怎么实现呢?

ViewPager是一个有头有尾的ViewGroup,Banner要实现头尾相接的循环效果。这里的冲突就在于对子View数量和位置的控制,让其不断循环复用。

这里提供两种思路--------------------------------------------------

a、让ViewPager在一个无限大的列表里边做假循环:

这种方法是一种简单的易于实现的方式,这里说的无限大其实也是一个相对较大,这里我们采用int正向最大值,让ViewPager在相当长的时间内,很难到达任何一端,并且我们在给ViewPager返回具体的子View的时候,让实际子View在该行程(ViewPager滚动的路径)中不断循环迭代,下面看代码(不是全部的代码,只是关键功能实现的伪代码):

//首先找到这个最大值,也就是默认是ViewPager的子View的数量
	private int mCount = Integer.MAX_VALUE;
//我们再定义一个ViewPager初始化位置,选用一半的位置
	private int mCenterPosition = (mCount - 1) / 2;
	private int mCurrentItem = mCenterPosition;
//由于真实的数量和要显示的数量之间有差异,所以还需要一个变量来标定真实数量
	private int realCount = 0;
//方便Adapter等全局使用url列表
	private List<Integer> mImageUrls;

	private void initData(){
	//初始化列表,本地的四张图片
		mImageUrls.add(R.mipmap.cat);
		mImageUrls.add(R.mipmap.monkey);
		mImageUrls.add(R.mipmap.sun);
		mImageUrls.add(R.mipmap.thanks);
	}

//然后初始化ViewPager
 	private void initViewPager() {
        if (mAdapter == null) {
            mAdapter = new BannerPageAdapter();
            mPager.addOnPageChangeListener(this);
        }
        mPager.setPageTransformer(true, new MyPageTransformer());
        mPager.setOffscreenPageLimit(3);
        mPager.setAdapter(mAdapter);
        mPager.setFocusable(true);
        mPager.setCurrentItem(mCurrentItem, false);
		//如果是1个真实子View,那就不用滚动,大于1个再滚动
        if (realCount > 1) {
            mPager.setScrollable(true);
        } else {
            mPager.setScrollable(false);
        }
        if (isAutoPlay && realCount > 1) startAutoPlay();
    }

//自动滚动逻辑,上边说过,简单贴一下
	private void startAutoPlay() {
        mHandler.removeCallbacks(scrollTask);
        mHandler.postDelayed(scrollTask, delayTime);
    }
//下面到了关键实现逻辑,主要就是校正position,是在Adapter逻辑中
	class BannerPageAdapter extends PagerAdapter {
	
	        @Override
	        public int getCount() {
	        //总的子View拓展到Integer.MAX_VALUE最大值,这是为了保证在一定时间内不到头,所以是假的
	            return mCount;
	        }
	
	        @Override
	        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
	            return view == object;
	        }
	
	        @NonNull
	        @Override
	        public Object instantiateItem(@NonNull ViewGroup container, final int position) {
	        //以下getRealPosition(int position)是关键校正的方法
	            ImageView imageView = new ImageView(mContext);
	            imageView.setImageDrawable(mContext.getResources().getDrawable(mImageUrls.get(getRealPosition(position))));
	            container.addView(imageView );
	         
	            ViewGroup.LayoutParams params = view.getLayoutParams();
	            params.width = 1080;
	            params.height = ViewGroup.LayoutParams.MATCH_PARENT;
	            view.setLayoutParams(params);
	            view.setPadding(100, 80, 100, 80);
	            if (view instanceof ImageView)
	                ((ImageView) view).setScaleType(ImageView.ScaleType.FIT_XY);
	            return view;
	        }
	
	        @Override
	        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object
	                object) {
	            container.removeView((View) object);
	        }
	
	    }
	
	private int getRealPosition(int position) {
		
	    if (realCount == 1) {
	    	//如果是只有一个,就不要循环,也不要调整,直接返回0
	        return 0;
	    } else {
	    	//如果不是一个就需要用以下的方法调整
	    	//得到初始化的中间位置与真实数量之间的余数,然后把真实位置将这余数差值去掉
	    	//因为List列表的允许的序号范围:【0,realCount-1】,所以再取余刚好对上列表的位置
	        int rex = mCenterPosition % realCount;
	        return (position - rex) % realCount;
	    }
	 }

	//最后再处理一个冲突,就是用户在触摸的时候将跳转交给用户,手指离开再自动切换
	//重写滑动冲突方法dispatchTouchEvent
	 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (isAutoPlay) {
            int action = ev.getAction();
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
                    || action == MotionEvent.ACTION_OUTSIDE) {
                startAutoPlay();
            } else if (action == MotionEvent.ACTION_DOWN) {
                stopAutoPlay();
            }
        }
        return super.dispatchTouchEvent(ev);
    }
	//最后是暂停自动滚动的逻辑,就是移除handler的runnable
	private void stopAutoPlay() {
        mHandler.removeCallbacks(scrollTask);
    }
           

以上就是重要的实现代码,不是全部代码,代码已上传github。

由于我们是人为将ViewPager的子View扩大到一个似乎到不了头的位置,但是并不是到不了头,现在我们来证明下,究竟多久到头:

Integer.MAX_VALUE是2的31次方-1,一半就是近似2的30次方(那个1这里我们忽略,因为影响不大)我们按切换一次耗时1s,到任何一头都需要2的30方,换算到小时:2^30 / 60 = 298261H,再换算到天:298261 / 24 = 12427天,最后换算到年:12427 / 365 = 34年。

是的按照1s的正常切换,到头需要34年,而用户一旦重新进入你的这个页面就又会被重置,所以根本不用担心会到头,假的也就成真的了。

这种实现方案有个好处,他实现从后往前也容易,只需要将runnable里边的设置下一页,改成设置上一页。非常简单。

b、动态控制ViewPager从最后一页到第一页的衔接跳转:

这种方法是比较负责的方法,因为涉及动态判断和调整,其实就是我们上边分析的对Page改变的监听之一的onPageScrollStateChanged(int state)方法中根据状态动态改变,在最后一页的时候调到第二页(为什么是第二页见代码分析),使用这种方法实现轮播,要比ViewPager的真实子View多两个,并且,紧跟在真实子View后,所以最终的子View的最后一个其实是第二个子View的重复,这样我们就能衔接上了,下面看代码(部分代码,也为伪代码):

//绝大多数的逻辑代码都与上一个实现方式相同,所以这里只列出不同的地方
//主要有以下几点:
//第一点设置View的时候需要处理下,因为显示的数量要比实际的数量多2
//首先都有个ImageView列表,存储需要放的子View
private List<ImageView> mImageViews = new ArrayList<>();
private void initData(){
	//实际url列表还得有
	mImageUrls.add(R.mipmap.cat);
	mImageUrls.add(R.mipmap.monkey);
	mImageUrls.add(R.mipmap.sun);
	mImageUrls.add(R.mipmap.thanks);
	//然后就是处理ImageView的列表
	this.realCount = urls.size();
        View imageView = null;
        if (realCount == 1) {
            imageView = imageLoader.createImageView(mContext);
            mImageViews.add(imageView);
            //这个方法就是封装下glide,或者自己实现的ImageLoader,给ImageView加载图片
            imageLoader.displayImage(mContext, mImageUrls.get(0), imageView);
        } else {
            int size = realCount + 1;
            Object url = null;
            for (int i = 0; i <= size; i++) {
                imageView = imageLoader.createImageView(mContext);
                mImageViews.add(imageView);
                //下面是关键了,也就是先把真实的按顺序放进列表,然后再顺序增加序号是0,和1的两个ImageView
                if (i < realCount) {
                    url = urls.get(i);
                } else if (i == realCount) {
                    url = urls.get(0);
                } else if (i == realCount + 1) {
                    url = urls.get(1);
                }
                //这个方法就是封装下glide,或者自己实现的ImageLoader,给ImageView加载图片
                imageLoader.displayImage(mContext, url, imageView);
            }
}

//第二点就是适配器,这次适配器变得简单了
class BannerPageAdapter extends PagerAdapter {

        @Override
        public int getCount() {
        //返回ImageView容器中的数量,也就是比真实的多2(特殊:如果真实数量为1 ,那这里就是1)
            return mImageViews.size();
        }

        @Override
        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
            return view == object;
        }

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container, final int position) {
        	//从容器中拿出对应位置的ImageView添加进container,然后返回
            View view = mImageViews.get(position);
            container.addView(view);
            ViewGroup.LayoutParams params = view.getLayoutParams();
            params.width = 1080;
            params.height = ViewGroup.LayoutParams.MATCH_PARENT;
            view.setLayoutParams(params);
            view.setPadding(100, 80, 100, 80);
            if (view instanceof ImageView)
                ((ImageView) view).setScaleType(ImageView.ScaleType.FIT_XY);
            return view;
        }

        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object
                object) {
            container.removeView((View) object);
        }
    }

	//下面就是最重要的第三点了,也是我们ViewPager技巧系列第一篇,整个系列第四篇知识的运用
	//涉及ViewPager.OnPageChangeListener的监控和特殊处理,其实这里可以用缺省的SimpleOnPageChangeListener
	//因为我们只用处理onPageScrollStateChanged这个回调方法
	//首先添加一下监听,并且让当前的界面实现这个借口
	mPager.addOnPageChangeListener(this);
	 @Override
    public void onPageScrollStateChanged(int state) {
        mCurrentItem = mPager.getCurrentItem();
        Log.e("PageChange-State", "当前位置:" + mCurrentItem);
        if (realCount <= 1) return;
        switch (state) {
            case ViewPager.SCROLL_STATE_IDLE:
                handlePagerRecycler(mCurrentItem);
                break;
            case ViewPager.SCROLL_STATE_DRAGGING:
                handlePagerRecycler(mCurrentItem);
                break;
            case ViewPager.SCROLL_STATE_SETTLING:
                break;
            default:
                break;
        }
    }
    
	private void handlePagerRecycler(int mCurrentItem) {
		//这次是关键了,当我们即将要展示列表的最后一个ImageView或者第一个ImageView的时候
		//我们要让他调到对应前边或者后边的拥有相同图片的假象
		//这个时候,其实显示位置已经变了,调用这个跳转方法要做到瞬时,不让用户察觉
		//所以要关掉smoothScroll开关,false
		//而在执行handler的runnable里边的跳转要把smoothScroll打开,true(用默认就行)
        if (mCurrentItem == 0) {
            mPager.setCurrentItem(realCount, false);
        } else if (mCurrentItem == realCount + 1) {
            mPager.setCurrentItem(1, false);
        }
    }

	//这次初始化要设置到多增加的第一个上边
	//这里由于真实图片有四张,所以初始化的序号就是4,即真实的size
	//如果是只有1个的话,那就显示0序号的
	if(realCount > 1){
		mPager.setCurrentItem(realCount);
	}else{
		mPager.setCurrentItem(0);
	}
	
           

以上就是第二种实现无限自动循环的部分逻辑代码,源代码是个自定义View,已上传github

以上实现的关键,我们需要搞清楚一个预加载的原理就行。ViewPager默认是会加载当前页的前后两页的,我们的策略就是让ViewPager永远到不了边,最多到最外边的次一个。当它将要到的时候,调整他们的位置,让他们迅速跳转到中间,因为我们至少需要准备前后两张,不能再少了,多了可以,但是没必要就浪费了,所以2个刚刚好,另一个关键就是调整位置的时机,页面切换的监听方法中onPageScrollStateChanged是对滑动状态的监听,恰好能够最及时的拿到下一个即将到达边界的View,然后做出迅速调整。

禁止ViewPager左右滑动

有童鞋会问,ViewPager不就是让人滑的吗,干嘛禁滑呢?

在首页的架构,有的情况是四个功能按键,控制整个界面切换,像微信这种的,不过微信貌似可以允许滑,所以这也证明他恰好用的也是ViewPager。有些需求就是不让滑,按功能按键跳就行,那应该怎么实现呢?

很简单,这次我们需要实现一下原生的ViewPager并且覆写滑动冲突的方法,我们起名叫做NoScrollViewPager,代码如下

public class NoScrollViewPager extends ViewPager {
    
    public NoScrollViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent arg0) {
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent arg0) {
        return false;
    }
}
           
实现的关键就是让拦截方法onInterceptTouchEvent和触摸事件处理方法onTouchEvent,两个方法恒定返回false,也就是不拦截也不处理,交给子View。

各种指示器的配合

和指示器的配合,是开发者在平时使用频率比较大的,但是这属于纯应用,而且也有很多开发者贡献了很多优秀的博客,我这里就不详细介绍用法了,罗列几个Google提供的,其他开源的优秀的框架我给出链接,供大家阅读使用

1、PagerTabStrip:谷歌提供的ViewPager搭档

这个是V4包的和ViewPager一起推出的指示器,同时显示过去,现在,和将来三个Tab,使用很方便

在xml中,作为ViewPager的子View,并且设置属性android:layout_gravity=”top\bottom”,设置top位于ViewPager的上部,或者bottom位于ViewPager的下部。

然后在代码中,我们熟悉的PagerAdapter的实现里,这回我们要重写getPageTitle(int position)这个方法,如下:

class MyAdapter extends FragmentPagerAdapter {

   //其他需要继承实现的方法,这里省略

    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return mTags[position];
    }
}
           

对,就是这个方法,你需要给每个子View的对应指示器指定文字信息,这样点击的时候才会知道点了什么。如果不实现这个方法的话,那么你的指示器只有一根下划线。

同理还有PagerTabStrip的父类PagerTitleStrip,它两是一组。只是PagerTabStrip即是指示器,也是交互器,是可以点击的能够交互的;而PagerTitleStrip只是一个指示器,不能点击,不能交互。

你不需要设置page改变回调,PagerTabStrip和PagerTitleStrip的内部会获取父View,即ViewPager,然后里边作了同步,以及会回调我们覆写的getPageTitle(int position)方法获取并设置给Tab的text。

PagerTabStrip系列控件是要求作为ViewPager的子类使用的,下面在介绍一个类似的,但是可以自由设置位置。

2、TabLayout:Tab布局指示器

这是Android Design 包下的类, 该包是Android5.0 引入的UI包

所以,第一步添加依赖:compile 'com.android.support:design:25.2.0’

(版本号根据你的工程设置合适的)

在页面中通过findViewById初始化控件,如:mTabLayout

添加Tab标签的文字有两种方法,一种是直接,一种间接

直接:mTabLayout.addTab(mTabLayout.newTab().setText(“”))直接将TabLayout的子标签,按顺序依次添加。

间接:和PagerTabStrip相同,覆写PagerAdapter的getPageTitle(int position)方法,返回的String会设置给对应的标签。

好了,有了以上的步骤,指示器就完成了。当然还可以调用以下方法设置指示器的样式:

mTabLayout.setSelectedTabIndicatorColor(Color.BLUE);//设置指示下划线颜色

mTabLayout.setSelectedTabIndicatorHeight(10);//设置指示器高度

mTabLayout.setTabGravity(TabLayout.GRAVITY_CENTER);//设置布局方式

mTabLayout.setTabMode(TabLayout.MODE_FIXED);//设置滑动的模式

mTabLayout.setTabTextColors(Color.BLACK,Color.RED);//设置未选和选中的文字颜色

mTabLayout.setBackgroundColor(Color.GREEN);//设置背景颜色

可以灵活定制,更全面的使用大家可以看这篇博客,号称这个控件总结最全

https://blog.csdn.net/wu371894545/article/details/65936966

3、开源MagicIndicator

GitHub地址:https://github.com/hackware1993/MagicIndicator

4、开源ViewPagerIndicator

GitHub地址:https://github.com/JakeWharton/ViewPagerIndicator

地址:http://www.jianshu.com/p/a2263ee3e7c3

5、开源PagerSlidingTabStrip

https://github.com/astuetz/PagerSlidingTabStrip

当然你也可以利用自定义View实现更加炫酷,更加符合你需求的定制化指示器,

其实没有什么是自定义View 实现不了的,加油吧,攻城狮

从下一篇我们就要深入ViewPager的源码了,上面遗留的很多问题我们到源码中去找答案,Come on!

后记:准备整个技巧系列,初始整理的字数就达到1万5千字,本打算一篇博客写完,后来想想,这对读者是个折磨。所以我尽可能按照联系拆分成了四篇。然后在具体完成每一篇博客的时候,又根据自己的理解增加了很多文字解释,尽可能的让读者能够明白其中的思想和道理。后期有时间还会增加一些图片信息来辅助讲解,让内容显得不枯燥。感谢各位博友~

继续阅读