带指示器的自动轮播式ViewPager
前言
自动轮播式的ViewPager的应用非常广泛,可以应用于各种应用的内部,最主要的还是用于展示广告。内部含有好几个子view,每隔一段时间就自动滑到下一个view中,底部的指示器也随之跟着改变,这就是我们所要完成的事情。
本文主要参考文章:http://blog.csdn.net/a553181867/article/details/52734261#comments
原理简析
首先我们思考一下,系统自带的ViewPager是一个独立控件,既没有指示器,也没有自动滚动功能,但是它是一个现成的,可左右滑动的控件,所以我们肯定是需要以ViewPager为基础的,我们所要做的配置和一般的ViewPager没什么不同,都是获取实例→创建适配器→设置适配器。
因此,我们可以利用一个布局,把ViewPager包裹起来,同时在这个布局里面再放入指示器(indicator)。
所以,第一步,先新建名为 BannerViewPager 的java类,继承自FrameLayout,这里我使用了帧布局,当然也可以使用其他能实现相同效果的布局。
public class BannerViewPager extends FrameLayout {
private Contetxt mContext;
private ViewPager mViewPager;
public BannerViewPager(Context context) {
this(context, null);
}
public BannerViewPager(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
initViews();
}
public void initViews() {
//initialize the viewpager
mViewPager = new ViewPager(mContext);
ViewPager.LayoutParams lp = new ViewPager.LayoutParams();
lp.width = ViewPager.LayoutParams.MATCH_PARENT;
lp.height = ViewPager.LayoutParams.MATCH_PARENT;
mViewPager.setLayoutParams(lp);
}
}
这里主要是对ViewPager进行初始化,设置布局参数以便在FrameLayout中得到正确的显示。接着我们需要对ViewPager实现自动滚动,这个的实现原理也不难,我们只要知道每时每刻的ViewPager的滑动状态以及当前的page position即可。而ViewPager就有这样的一个监视器:ViewPager.OnPageChangeListene,只要ViewPager进行了滑动,就会回调这个监听器的如下几个方法:
public interface OnPageChangeListener {
//只要ViewPager进行了滑动,该方法就会回调
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
//当前页面被选定的时候,回调
public void onPageSelected(int position);
//ViewPager的状态发生改变的时候,回调
public void onPageScrollStateChanged(int state);
}
那么,我们只要为ViewPager设置监听器(调用addOnPageChangeListener方法),并且重写这几个方法以实现我们的需求,所以我们就需要让BannerViewPager实现ViewPager.OnPageChangeListener接口:
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
...
//保存当前的position值
private int mCurrentPosition;
//viewpager's rolling state
private int mViewPagerScrollState;
private int mReleasingTime = 0;
...
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
int max = mAdapter.getCount() - 1;
int pos = position;
mCurrentPosition = position;
if (position == 0) {
mCurrentPosition = max - 1;
} else if (position == max) {
mCurrentPosition = 1;
}
pos = mCurrentPosition - 1;
}
@Override
public void onPageSelected(int position) {
mCurrentPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
mViewPagerScrollState = ViewPager.SCROLL_STATE_DRAGGING;
} else if (state == ViewPager.SCROLL_STATE_IDLE) {
mReleasingTime = (int) System.currentTimeMillis();
mViewPagerScrollState = ViewPager.SCROLL_STATE_IDLE;
// when scroll stops, change the viewpager to make circle
mViewPager.setCurrentItem(mCurrentPosition, false);
}
}
}
每当当前页面被选中的时候,就会调用onPageSelected方法,此时保存当前position值。那么, 什么叫做当前页面被选中呢? 经过实验验证,当一个Item被完全展示在ViewPager中的时候,就是选中状态,但如果当前正在被手指拖动,即使下一个item滑动到了中间位置,也不是选中状态。
接着,我们看onPageScrollStateChanged方法,当ViewPager的状态发生改变的时候,就会触发。那么, ViewPager的状态改变是什么意思呢? ViewPager有如下三种状态:IDLE,停止状态,没有手指触摸;DRAGGING,正在被手指拖动;SETTLING,松开手指的时候,ViewPager由于惯性能向着手指滑动方向的下一个item滑动时的状态。我们重写的方法中,mViewPageScrollState记录了ViewPager的实时状态,同时停止状态的时候,也记录了一个mReleasingTime值,这个值的作用下面会介绍。
最后,我们来看onPageScrolled方法,当页面在滑动的时候会调用此方法,在滑动被停止之前,此方法会一直得到调用。这个方法我们用来作为循环的基础,先获得适配器中item的个数,然后通过对现在位置的判断来设置当前位置,现在这个方法看起来很怪,为什么现在位置是第0个的时候,我们要将其改为倒数第二个呢?这个是因为我们要做到循环,其实做了一点小手脚,例如我们有ABC三个item要循环展示,按照循环的逻辑,A在往前会回到C,C在往后会来到A,所以我们就可以设置为一个CABCA的循环形式。所以,在当前位置为第0个的时候,在这个例子中也就是在第一个C的时候,我们将其偷偷换到倒数第二个,也就是第二个C;同理,对于A的处理也是如此。而在onPageScrolled这个方法,我们已经知道是在页面滑动的时候调用,而我们的移形换位应该在页面停止下来的时候进行,所以在onPageScrollStateChanged这个方法中,如果我们判断到状态是停止状态的话,我们在最后增加一句来设置ViewPager的位置。
通过实现这个监听器,我们就获取到了mCurrentPosition和mViewPageScrollState两个值为自动滚动做基础。
接下来,我们就要考虑自动任务的问题了。在Android中,自动任务可以使用Handler和Runnable来实现,通过postDelay方法来不断实现循环,代码如下:
...
//the interval between rollings
private int mAutoRollingTime = 4000;
private static final int MESSAGE_AUTO_ROLLING = 0X1001;
private static final int MESSAGE_AUTO_ROLLING_CANCEL = 0X1002;
...
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_AUTO_ROLLING:
if(mCurrentPosition == mAdapter.getCount() - 2){
mViewPager.setCurrentItem(1,true);
}else {
mViewPager.setCurrentItem(mCurrentPosition + 1,true);
}
postDelayed(mAutoRollingTask,mAutoRollingTime);
break;
case MESSAGE_AUTO_ROLLING_CANCEL:
postDelayed(mAutoRollingTask,mAutoRollingTime);
break;
}
}
};
/**
* This runnable decides the viewpager should roll to next page or wait.
*/
private Runnable mAutoRollingTask = new Runnable() {
@Override
public void run() {
int now = (int) System.currentTimeMillis();
int timediff = mAutoRollingTime;
if(mReleasingTime != 0){
timediff = now - mReleasingTime;
}
if(mViewPagerScrollState == ViewPager.SCROLL_STATE_IDLE){
//if user's finger just left the screen,we should wait for a while.
if(timediff >= mAutoRollingTime * 0.8){
mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING);
}else {
mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING_CANCEL);
}
}else if(mViewPagerScrollState == ViewPager.SCROLL_STATE_DRAGGING){
mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING_CANCEL);
}
}
};
在mAutoRollingTask这个Runnable内,我们根据不同的mViewPagerScrollState来决定是让ViewPager滚动到下一个page还是等待,因为如果用户当前正在触摸ViewPager,那么肯定是不能自动滚动到下一页的,此外,还有一种情况,就是当用户手指离开屏幕的时候,需要等待一段时间才能开始自动滚动任务,否则会造成不好的用户体验,这就是mReleasingTime的作用之处了。在Hnadler中,根据Runnable发送过来的不同信息进行不同的操作,如果需要滚动到下一个页面,则调用setCurrentItem方法来进行滑动,这里要注意我们为了循环所做的特殊处理。
实现指示器
指示器有如下需求:指示器由一系列圆点构成,一般的指示器都是使用颜色不同来区别,这里我使用的形状不同,选中的Page所对应的圆点会显示为方形,而方形能随着Page的滑动而滑动。
那么,我们可以这样来实现需求:白色的圆点作为Indicator的背景,通过onDraw()方法来绘制,而方形则通过一个子View来显示,利用onLayout()方法来控制它的位置,这样就能实现方形在圆点上运动的效果了。而他们具体的位置控制,可以利用上面的 onPageScrolled 方法来获取具体的位置以及位置偏移百分比。
我们先来实现绘制部分,新建 ViewPagerIndivactor.java 继承自 LinearLayout,先对属性初始化:
public class ViewPagerIndicator extends LinearLayout {
private Context mContext;
private Paint mPaint;
private View mMoveView;
private int mCurrentPosition = 0;
private float mPositionOffset;
private int mPadding = 10;
private int mRadius = 10;
private int mMoveRadius = 15;
private int mDistanceBtwItem = mRadius * 2 + mPadding;
private int mItemCount = 5;
public ViewPagerIndicator(Context context) {
this(context, null);
}
public ViewPagerIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
private void init() {
setOrientation(LinearLayout.HORIZONTAL);
setWillNotDraw(false);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.WHITE);
mMoveView = new MoveView(mContext);
addView(mMoveView);
}
public void setItemCount(int count) {
this.mItemCount = count;
requestLayout();
}
public void setRadius(int radius) {
this.mRadius = radius;
this.mMoveRadius = radius * 3 / 2;
this.mDistanceBtwItem = mRadius * 2 + mPadding;
requestLayout();
}
public void setPadding(int padding) {
this.mPadding = padding;
this.mDistanceBtwItem = mRadius * 2 + mPadding;
requestLayout();
}
public void setPositionAndOffset(int position, float offset) {
this.mCurrentPosition = position;
this.mPositionOffset = offset;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mPadding + (mRadius * 2 + mPadding) * (mItemCount - 2),
2*mRadius + 2*mPadding);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mItemCount - 2; i++) {
canvas.drawCircle(mRadius + mPadding + mRadius * i * 2 + mPadding * i,
mRadius + mPadding, mRadius, mPaint);
}
}
private class MoveView extends View {
private Paint mPaint;
public MoveView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.WHITE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mMoveRadius * 2, mMoveRadius * 2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mRadius, mRadius, mMoveRadius, mPaint);
}
}
}
从上面的代码可以看到,在init()方法内,我们调用了setWillNotDraw(false)方法,这个方法有什么用呢?如果有写过自定义View的应该知道,ViewGroup默认是不会调用它自身的onDraw()方法的,只有调用了该方法设置为false或者给ViewGroup设置一种背景颜色的情况下才会调用onDraw()方法。
解决了这个问题后,我们来看onMeasure()方法,在这个方法内,我们要对该indicator的宽高做出测量,以便接下来的布局和绘制流程,而对于我们的需求而言,只要该布局能够包囊住我们的指示器,并且四边留有一定的空间即可,那么布局的宽度就与Page的数量有关了。为了方便起见,这里先给了一个默认值,但是注意虽然是5,但是其实有两个是不用显示出来的,所以之后也只是绘制了三个圆点。
我们接着看onDraw()方法,这个方法内部,根据mItemCount的数量来进行绘制圆形,这里没什么 好讲的,只要注意他们之间的距离就可以了。接着,我们来绘制方形,新建一个内部类,继承自View,同样通过onMeasure、onDraw方法来进行测量、绘制流程,只不过大小变了。
好了绘制部分就完成了,接下来就是让这个MoveView进行移动了,由于要使MoveView配合Page的滑动而滑动,我们需要Page的具体位置以及位置偏移量,而这两个数值实在BannerViewPager的内部中获得的,所以我们可以在BannerViewPager中,每一此调用onPageScrolled方法的时候,来调用我们的ViewPagerIndicator的一个方法,而在这个方法内部,来请求布局,这样就能实现MoveView随着Page的滑动而滑动的效果了,具体如下:
public class ViewPagerIndicator extends LinearLayout {
...
public void setPositionAndOffset(int position, float offset) {
this.mCurrentPosition = position;
this.mPositionOffset = offset;
requestLayout();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mMoveView.layout(
(int) (mPadding + mDistanceBtwItem * (mCurrentPosition + mPositionOffset)),
mPadding,
(int) (mDistanceBtwItem * ( 1 + mCurrentPosition + mPositionOffset)),
mPadding + mRadius * 2);
}
}
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
...
private ViewPagerIndicator mIndicator;
...
private void initViews() {
...
mIndicator = new ViewPagerIndicator(mContext);
FrameLayout.LayoutParams indicatorlp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
indicatorlp.gravity = Gravity.BOTTOM | Gravity.CENTER;
indicatorlp.bottomMargin = 20;
mIndicator.setLayoutParams(indicatorlp);
}
...
private void setIndicator(int position, float offset) {
mIndicator.setPositionAndOffset(position, offset);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
...
setIndicator(pos, positionOffset);
}
}
在setPositionAndOffset方法内调用了requestLayout()方法,这个方法会导致View树的测量,布局,重绘流程的发生,因此在onLayout方法内,通过mCurrentPosition、mPositionOffset这两个值来控制MoveView的位置就可以了。
然后在BannerViewPager加入相应的代码来添加指示器到其中。
好了,到现在为止,ViewPagerIndicator基本已经完成了,但是还有一个问题,如果适配器里面的数据刷新了,page的数量变多了,而指示器的数目却依然没变。因此,我们 必须在数据刷新的时候,及时通知Indicator来增加指示器的数目。但是,我们进一步想想,数据列表保存在Adapter中,如果ViewPagerIndicator想要获取数据,那就要得到Adapter的一个引用,或者说Adapter需要得到ViewPagerIndicator的引用以便能够通知它,如果这样做的话,相当于把两个相关性不大的类联系到了一起,耦合度过高,这样不利于以后的维护。因此,这里我们采用了 观察者模式 来实现Adapter数据刷新时通知ViewPagerIndicator的这样一个需求。先新建两个接口,一个是 DataSetSubscriber, 观察者;另一个是 DataSetSubject, 被观察者。
public interface DataSetSubscriber {
void update(int count);
}
public interface DataSetSubject {
void registerSubscriber(DataSetSubscriber subscriber);
void removeSubscriber(DataSetSubscriber subscriber);
void notifySubscriber();
}
这里实现思路是这样的:在BannerViewPager内实现了一个DataSetSubscriber(观察者),在ViewPagerAdaper内实现DataSetSubject(被观察者),通过registerSubscriber方法进行注册,当ViewPageAdapter的数据列表发生变动的时候,回调DataSetSubscriber的update()方法,并把当前的数据长度作为参数传递进来,而BannerViewPager再进一步调用ViewPagerIndicator的方法来重新布局。
public class ViewPagerAdapter extends PagerAdapter implements DataSetSubject {
private List<DataSetSubscriber> mSubscribers = new ArrayList<>();
private List<? extends View> mDataViews;
private OnPageClickListener mOnPageClickListener;
public ViewPagerAdapter(List<? extends View> mDataViews, OnPageClickListener listener) {
this.mDataViews = mDataViews;
this.mOnPageClickListener = listener;
}
public View getView(int location) {
return this.mDataViews.get(location);
}
@Override
public int getCount() {
return mDataViews.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = mDataViews.get(position);
final int i = position;
if (mOnPageClickListener != null) {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mOnPageClickListener.onPageClick(v, i);
}
});
}
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
notifySubscriber();
}
@Override
public void registerSubscriber(DataSetSubscriber subscriber) {
mSubscribers.add(subscriber);
}
@Override
public void removeSubscriber(DataSetSubscriber subscriber) {
mSubscribers.remove(subscriber);
}
@Override
public void notifySubscriber() {
for (DataSetSubscriber subscriber : mSubscribers) {
subscriber.update(getCount());
}
}
}
在update()方法内,调用了ViewPagerIndicator的setItemCount方法,从而重新布局。
那么,指示器就实现完毕了。
实现Page的点击事件处理
如果,大家都是直接按照上面的代码来进行编写,会发现其实会报错,有一个类我们并没有编写就用到了,而那个类其实就是为了实现点击事件处理的需求。
因为往往ViewPager的内容只是一个概括性的内容,为了得到更加详细的消息,用户通常会点击它的item从而打开一个新的页面,这样就需要我们对点击事件进行处理了。这里我们通过定义了一个新的接口: OnPageClickListener ,定义一个onPageClick方法。如下:
public interface OnPageClickListener {
void onPageClick(View view,int position);
}
只要在item初始化的时候,给每个item view都设置一个View.OnClickListener,并且在onClick方法里面调用我们的onPageClick方法即可。
最后,我们在BannerViewPager里面配置上这个适配器即可
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
...
private ViewPagerAdapter mAdapet;
...
//by default,auto-rolling is on.
private boolean isAutoRolling = true;
...
public void setAdapter(ViewPagerAdapter adapter){
mViewPager.setAdapter(adapter);
mViewPager.addOnPageChangeListener(this);
mAdapter = adapter;
mAdapter.registerSubscriber(new DataSetSubscriber() {
@Override
public void update(int count) {
mIndicator.setItemCount(count);
}
});
//add the viewpager and the indicator to the container.
addView(mViewPager);
addView(mIndicator);
// skip the first one.
mViewPager.setCurrentItem(1, false);
//start the auto-rolling task if needed
if(isAutoRolling){ postDelayed(mAutoRollingTask,mAutoRollingTime);
}
}
}
在构建适配器的时候,同时实现OnPageClickListener即可。
//获取BannerViewPager实例
bannerViewPager = (BannerViewPager) findViewById(R.id.banner);
//实例化ViewPagerAdapter,第一个参数是View集合,第二个参数是页面点击监听器
mAdapter = new ViewPagerAdapter(mViews, new OnPageClickListener() {
@Override
public void onPageClick(View view, int position) {
Log.d("cylog","position:"+position);
}
});
//设置适配器
bannerViewPager.setAdapter(mAdapter);
mViews是事前准备好的相应的View集合。
以上便是本文的全部内容。