一、介绍
在项目中使用的自动轮播控件一直是网上别人做的,在出现问题的时候去看代码细节扫雷就非常浪费时间。于是痛定思痛自己造个轮子。
这个控件在app中使用非常频繁,并且原理也不复杂,就是在前后各加一页。相信每一个android开发者都会做这个东西。
功能介绍:
1.无限自动轮播。
2.指示器(下方的小点点)
3.滚动动画时间可调
4.拖拽的时候停止轮播
全部代码和示例代码已经上传到GitHub上了:
https://github.com/CuteWen/BannerView
有兴趣的可以下过来看看。
二、实现
首先要自定义一个View去继承ViewPager
然后我们自动轮播实现的关键其实都在PagerAdapter里面,我们可以自己封装一个PagerAdapter,但是自己封装的Adapter就会让使用者在写逻辑的时候要了解你的adapter封装到什么程度了,放出哪些方法,个人不太喜欢那样子,所以我这里使用了装饰者模式来扩展使用者写好的Adapter,这样使用的时候只要写一个最普通的PagerAdapter 就可以附加上自动轮播的功能了。
注:不太懂装饰者模式的同学可以去这里看一下,里面讲解的挺好的。
https://www.cnblogs.com/chenxing818/p/4705919.html
1.包装类
思考一下我们需要包装的功能,其实也就是要将页数+2,主要就是getCount这个方法了,另外在里面也要写好两个适配器之间的position转化的方法,统一调用这些方法可以避免逻辑的混乱。
下面就是我们的包装类了。
/**
* 适配器的包装类---------------------------------------------------------
*/
private class BannerAdapterWrapper extends PagerAdapter {
private PagerAdapter pagerAdapter;
public BannerAdapterWrapper(PagerAdapter pagerAdapter) {
this.pagerAdapter = pagerAdapter;
}
@Override
public int getCount() {
return pagerAdapter.getCount() > 1 ? pagerAdapter.getCount() + 2 : pagerAdapter.getCount();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view.equals(object);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
return pagerAdapter.instantiateItem(container, bannerToAdapterPosition(position));
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
pagerAdapter.destroyItem(container, position, object);
}
/**
* 展示出的position和实际的position 转换
*/
public int bannerToAdapterPosition(int position) {
int adapterCount = pagerAdapter.getCount();
if (adapterCount <= 1) return 0;
int adapterPosition = (position - 1) % adapterCount;
if (adapterPosition < 0) adapterPosition += adapterCount;
return adapterPosition;
}
public int toWrapperPosition(int position) {
return position + 1;
}
}
主要做了:
1.getCount的上限加了2 也就是前后各多一页的作用。
2.写了两个适配器之间的position之间的转换方法方便调用。
2.暗度陈仓(AdapterWrapper)之后的善后工作
看一下setAdapter方法:
/**
* 设置适配器的时候做初始化工作
*/
@Override
public void setAdapter(PagerAdapter adapter) {
this.adapter = adapter;
//注册原适配器刷新时的监听
this.adapter.registerDataSetObserver(new BannerPagerObserver());
//初始化包装适配器
bannerAdapterWrapper = new BannerAdapterWrapper(adapter);
//实际配置的adapter是包装后的适配器
super.setAdapter(bannerAdapterWrapper);
//注册适配器的监听 (这个在后文介绍)
addOnPageChangeListener(new BannerPageChangeListener());
//初始化handler处理定时事件 (这个在后文介绍)
looperHandler = new LooperHandler(this);
}
这里注册了一个DataSetObserver,这个平时用到的还比较少,它是用来监听Adapter.notifyDataSetChanged()的。
因为我们实际上绑定BannerView的是Wrapper之后的适配器adapter,而使用者手里调用的是原adapter的notifyDataSetChanged(),所以需要进行一个传递过程!
/**
* 数据刷新 传递刷新信号-----------------------------------------------------
*/
private class BannerPagerObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
dataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
dataSetChanged();
}
}
/**
* 刷新数据方法
*/
private void dataSetChanged() {
if (bannerAdapterWrapper != null && pagerAdapter.getCount() > 0) {
bannerAdapterWrapper.notifyDataSetChanged();
bannerIndicatorView.setCount(pagerAdapter.getCount());
setCurrentItem(0);
}
}
同理,我们在调用setCurrentItem()方法的时候position也是不一样的。
@Override
public void setCurrentItem(int item, boolean smoothScroll) {
super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item), smoothScroll);
}
@Override
public void setCurrentItem(int item) {
super.setCurrentItem(bannerAdapterWrapper.toWrapperPosition(item));
}
@Override
public int getCurrentItem() {
return bannerAdapterWrapper.bannerToAdapterPosition(super.getCurrentItem());
}
3.翻页监听
/**
* 监听翻页----------------------------------------------------------------
*/
private class BannerPageChangeListener implements OnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
// 在这里同步指示器
if (bannerIndicatorView != null) {
bannerIndicatorView.setSelect(bannerAdapterWrapper.bannerToAdapterPosition(position));
}
}
@Override
public void onPageScrollStateChanged(int state) {
int position = BannerView.super.getCurrentItem();
// 无限轮播的跳转
if (state == ViewPager.SCROLL_STATE_IDLE &&
(position == 0 || position == bannerAdapterWrapper.getCount() - 1)) {
setCurrentItem(bannerAdapterWrapper.bannerToAdapterPosition(position), false);
}
// 手指拖动翻页的时候暂停自动轮播
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (timer == null) {
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
looperHandler.sendEmptyMessage(0);
}
}, intervalTime + scrollTime, intervalTime + scrollTime);
}
} else if (state == ViewPager.SCROLL_STATE_DRAGGING) {
if (timer != null) {
timer.cancel();
timer = null;
}
}
}
}
里面的同步指示器和暂停自动轮播代码暂且不表。
主要就是无限轮播的跳转那一段代码 完成“无限”的实现。
4.自动轮播
这里我们使用了Timer+Handler的组合来完成定时滑动的操作:
/**
* 设置间隔时间 并开始Timer任务
*/
public void setIntervalTime(int intervalTime) {
this.intervalTime = intervalTime;
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
looperHandler.sendEmptyMessage(0);
}
}, intervalTime + scrollTime, intervalTime + scrollTime);
}
/**
* 处理定时任务-------------------------------------------------------------------
*/
private static class LooperHandler extends Handler {
private WeakReference weakReference;
public LooperHandler(BannerView bannerView) {
this.weakReference = new WeakReference<>(bannerView);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
weakReference.get().setCurrentItem(weakReference.get().getCurrentItem() + 1);
}
}
另外还有设置滚动的时间,这里需要使用一下反射去修改mScroller这个对象。
/**
* 设置滚动时间 利用反射
*/
public void setScrollTime(int scrollTime) {
try {
Field field = ViewPager.class.getDeclaredField("mScroller");
field.setAccessible(true);
FixedSpeedScroller scroller = new FixedSpeedScroller(getContext(),
new AccelerateInterpolator());
field.set(this, scroller);
scroller.setScrollDuration(scrollTime);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 修改ViewPager的滑动动画时间-----------------------------------------------------------
*/
private class FixedSpeedScroller extends Scroller {
private int duration = 300;
public FixedSpeedScroller(Context context, Interpolator interpolator) {
super(context, interpolator);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, this.duration);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy) {
super.startScroll(startX, startY, dx, dy, this.duration);
}
public void setScrollDuration(int duration) {
this.duration = duration;
}
}
5. 指示器
先上代码
public class BannerIndicatorView extends View {
private int count;
private int select;
private Paint pointPaint;
private Paint selectPaint;
private String selectColor = "#FFFFFF";
private String normalColor = "#80FFFFFF";
private int radius = 10;
private int interval = 10;
public BannerIndicatorView(Context context) {
this(context, null);
}
public BannerIndicatorView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
pointPaint = new Paint();
pointPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
pointPaint.setColor(Color.parseColor(normalColor));
pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);
selectPaint = new Paint();
selectPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
selectPaint.setColor(Color.parseColor(selectColor));
selectPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画出各个点的位置
for (int i = 0; i < count; i++) {
if (i == select) {
canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, selectPaint);
} else {
canvas.drawCircle(radius + i * (radius * 2 + interval), getHeight() / 2, radius, pointPaint);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = count * radius * 2 + (count - 1) * interval;
int height = radius * 2;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 设置第几个点选中,然后刷新
*/
public void setSelect(int select) {
this.select = select;
invalidate();
}
/**
* 设置个数
*/
public void setCount(int c) {
count = c;
}
public void setSelectColor(String selectColor) {
this.selectColor = selectColor;
}
public void setNormalColor(String normalColor) {
this.normalColor = normalColor;
}
}
这部分还是比较简单的,就是绘制了几个白色小圆点,然后提供setSelect的方法来变化选中点。
然后在BannerView里面写上setIndicator()的方法
/**
* 设置指示器,需要在setAdapter之后
*/
public void setIndicator(BannerIndicatorView bannerIndicatorView) {
this.bannerIndicatorView = bannerIndicatorView;
if (pagerAdapter != null) {
bannerIndicatorView.setCount(pagerAdapter.getCount());
}
}
三:示例与全部代码
在XML中的示例写法:
注意: android:layout_centerHorizonta = "true" 是为了让点居中。
class BannerActivity : AppCompatActivity() {
var bannerView: BannerView? = null
var indicatorView: BannerIndicatorView? = null
var adapter: BannerAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_banner)
bannerView = findViewById(R.id.bv_activity_banner) as BannerView
indicatorView = findViewById(R.id.biv_activity_banner) as BannerIndicatorView
adapter = BannerAdapter(this)
// 设置adapter
bannerView?.adapter = adapter
// 绑定指示器
bannerView?.setIndicator(indicatorView)
// 滚动动画的时间
bannerView?.setScrollTime(500)
// 设置轮播间隔
bannerView?.setIntervalTime(3000)
val data:ArrayList = ArrayList()
data.add("1111")
data.add("2222")
data.add("1111")
data.add("2222")
adapter?.addData(data)
}
}
这部分使用kotlin写的,不过调用就这几个方法,应该没什么看不懂的地方了。