实现这个功能的目前已经有很多种方式了,譬如说继承ViewGroup仿ViewPager通过adapter循环add、dstory指定的item,使用fragment添加移除view的方式,设置viewpager的索引条目达到上限==。以上几种方式属于Banner的常用选择,其中动效做的非常出色的如代码家的ImageSlider,曾经在项目中也引用过,不过在使用的过程中出现了内存开销过大的问题,1张图片也可以左右轮回切换的问题,不得不说是个硬伤。后来遇到了一种新的方式,于是分享出来给大家。
Banner和viewpgaer的最大区别是Banner可以在itemPosition=0和itemPosition=size-1之间来回无缝切换,因此我们理想状态是作如下处理的:当滑动到itemPosition=0的时候,左侧有对应itemPosition=0到itemPosition=size-1的view;当滑动到itemPosition=size-1的时候,右侧有对应itemPosition=0到itemPosition=size-1的view。这是我们一般直接的做法:设置viewpager的索引条目达到上限。然而开销的问题让我们会想到复用,结合listview的adapter的viewholder形式,可能会采取第i屏的第itemPosition=position的view复用第1屏的itemPosition=position的view。这样的做法看似不错,但是对于拥有着程序员思维的我们觉得这仅仅是一个逻辑不够严密,过程不够完美,存在潜藏的隐患的idea。于是两条道路,优化or另辟蹊径。
其实再做上述过程中的考虑时候,我们想到了复用,想到了设置size=Integer.MAX_VALUE实现itemPosition=0和itemPosition=size-1之间的切换,但是仔细考虑一下,我们也仅仅需要实现itemPosition=0和itemPosition=size-1之间的切换,就可以交给viewpager本身的机制来完成剩下页卡之间的轮播。假设一共需要展示4个view,于是画图如下:
... item=2 -> item=3 -> item=0 -> item=1 -> item=2 -> item=3 -> item=0 -> item=1 ...
也就是说我们发现可以考虑当itemPosition=3的时候下一屏itemPosition=0的view就是当前屏被滑动过去itemPosition=0的View,用来代替无限size=Integer.MAX_VALUE的实现方式,然而这样难点又回到了如和实现两者之间的无动画切换。于是我们想到实现size=Integer.MAX_VALUE的本质其实就是对viewpager不停的addView和destoryView,当滑动到itemPosition=0的时候在左侧添加一个itemPosition=3的view,当滑动到itemPosition=3的时候在右侧添加一个itemPosition=0的view,但是后续的itemPosition=1和itemPosition=2的view就可以不用我们手动add进去,因为viewpager的本质会自动默认itemPosition=3左侧、itemPosition=0右侧存在itemPosition=1、2的view,因此上述图我们可以简化成:
|<—————————————|
itemPosition 3 -> 0 -> 1 -> 2 -> 3 -> 0
|—————————————>|
于是我们把addView简化成了addView(0,item3View)和addView(5,item0View),而当加载到这两个view的时候我们认为它加载的其实就是itemPosition=3和itemPosition=0的view。认为这个词语在代码中的体现就是set,因此当我们在首尾各添加了一个view之后可以写出如下代码:
if(getCurrentPosition() == 0){
setCurrentPosition(4);
}
if(getCurrentPosition() == 5){
setCurrentPosition(1);
}
抽取该思路方法:
@Override
public int getCount(){
if(mDatas != null && mDatas.size() >= 0){
if(mDatas.size() > 0){
return mDatas.size() + 1;
}else{
return 0;
}
}
}
addOnPageChangeListener(new OnPageChangeListener(){
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if(getCurrentPosition() == 0){
setCurrentPosition(4);
}
if(getCurrentPosition() == 5){
setCurrentPosition(1);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
看起来这样的逻辑是不错的,但是因为setCurrentPosition这个方法默认是带动画效果切换view的,所以我们得通过
setCurrentPosition(position,false);
来设置我们的认为。其次我们在实际切换过程中发现了切换即将结束的时候被强行提前结束切换效果。通过分析发现OnPageChangeListener.onPageSelected总是在画面未切换结束、界面上含有一个view部分内容的时候便已经执行了。因此我们需要判断切换的状态。而OnPageChangeListener.onPageScrollStateChanged的参数state提供了我们切换的状态,按照Google的API所说,state提供了三种状态:
SCROLL_STATE_IDLE=0:什么都没做
SCROLL_STATE_DRAGGING=1:开始滑动
SCROLL_STATE_SETTLING=2:滑动结束
在SCROLL_STATE_IDLE和SCROLL_STATE_SETTLING之间判断后发现真正的界面停留是SCROLL_STATE_IDLE状态,所以通过判断SCROLL_STATE_IDLE状态我们进行无缝界面切换,避免提前技术切换和僵直切换。
换代码如下:
@Override public void onPageScrollStateChanged(int state) {
if(state == SCROLL_STATE_IDLE){
if(getCurrentPosition() == 0){
setCurrentPosition(getChildCount() - 2);
}
if(getCurrentPosition() == getChildCount() - 1){
setCurrentPosition(1);
}
}
}
好了这个时候我们发现循环动画切换已经彻底实现了,接下来实现页卡指示器。我们可以通过在onPageScrollStateChanged()里面设置回调监听。然而这个时候我们通过onPageScrollStateChanged变换了页卡,同时通过计算也能获取当前正确的页卡坐标,但是当手指快速滑动的时候我们的页卡指示器竟然没有自动切换,当且仅当手指离开屏幕的时候跳转到当前的页卡指示器。通过逻辑梳理我们发现,SCROLL_STATE_IDLE的状态仅在手指离开屏幕的时候被出发,即快速连续滑动屏幕过程中不会触发该状态,通过该状态SCROLL_STATE_IDLE得到的itemPosition也是不及时的,而非当前状态的itemPosition则很难得到判断。于是用最原始的方式onPageSelected方法设置页卡指示器。
通过分析,onPageSelected的参数position是当前view的position,因此这个position与数据的positon坐标器非同一个坐标器。但因为我们在计算数据总数的时候头尾分别加了一个数据展示,因此view的position比数据的position永远超前一位,即
dataPosition = viewPosition - 1;
而我们的viewPosition在onPageSelected方法里面就是参数position,故而,我们应该在onPageSelected方法里面设置回调监听,设置页卡器。
不过通过刚才的问题我们会发现,因为onPageScrollStateChanged的state判断,当连续滑动不触发SCROLL_STATE_IDLE状态,即当处于重置页面坐标的时候如果不触发该状态,最终结果就是不能够在view的itemPosition=0和itemPosition=size-1的时候不能够向左向右滑动,除非松开手指触发SCROLL_STATE_IDLE。针对这一点也未想到更好的解决方法。
接下来提供已经封装好的BannerPager,涵盖:
1. 页卡指示器功能;
2. 自动轮播功能(当手指滑动时重置当前页卡的切换间隔时间);
3. 新增获取正确数据的currentItemPosition的方法和设置currentItemPosition:getRealCurrentItem()和setRealCurrentItem(int),setRealCurrentItem(int,boolean)方法。(用以代替Android的API提供的setCurrentItem(int),setCurrentItem(int,boolean)方法和getCurrentItem()方法);
4. 新增addPageIndicator(OnIndicatorChangedListener)方法,用来代替addOnPageChangeListener(OnPageChangeListener)方法;
5. 配置自定义属性来设置指示器的位置和背景图片。
点击我跳转到GitHub项目地址
推荐:
代码家的AndroidImageSlider