用法概述:
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千字,本打算一篇博客写完,后来想想,这对读者是个折磨。所以我尽可能按照联系拆分成了四篇。然后在具体完成每一篇博客的时候,又根据自己的理解增加了很多文字解释,尽可能的让读者能够明白其中的思想和道理。后期有时间还会增加一些图片信息来辅助讲解,让内容显得不枯燥。感谢各位博友~