带指示器的自动轮播式ViewPager

带指示器的自动轮播式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集合。

以上便是本文的全部内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值