欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android实现带指示器的自动轮播式ViewPager

程序员文章站 2023-12-20 14:10:46
前言 最近在做项目的时候,有个需求就是实现自动轮播式的viewpager,最直观的例子就是知乎日报顶部的viewpager,它内部有着好几个子view,每个一段时间便自动...

前言

最近在做项目的时候,有个需求就是实现自动轮播式的viewpager,最直观的例子就是知乎日报顶部的viewpager,它内部有着好几个子view,每个一段时间便自动滑动到下一个item view,而底部的指示器也随之跟着改变。使用这种viewpager的好处是在有限的空间内可以展示出多样化的信息。轮播式viewpager广泛应用于各种应用内部,用于展示广告等。抱着学习和分享的目的,笔者把轮播式viewpager写成了一个独立的控件,以方便以后的使用。

效果展示

话不多说,我们先来看看实现的效果是怎样的:

Android实现带指示器的自动轮播式ViewPager

从上面的动态图可以看到,当我们手指拖动viewpager的时候,下方的指示器随着页面的滑动而滑动,当点击添加数据的按钮的时候,viewpager的数据项变多,同时下方的指示器也随之改变,适应了数据项的数目。

Android实现带指示器的自动轮播式ViewPager

从上面的动态图可以看到,当我们不用手指进行拖动的时候,该viewpager会每隔4s左右的时间自动进行滚动,滚动到最后一个item view的时候,下一次会滚到第一个位置。

github地址及使用介绍

读者可以直接到我的github中获取源码。
github:bannerviewpager,控件及其相关文件都放在了该目录下的library模块内,而app模块则是上面效果展示的一个简单应用。

通过以下几个步骤,就能方便地使用该控件了:
1、像普通的viewpager一样,在布局文件中放入该控件如下:

<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical">

 <com.chenyu.library.bannerviewpager.bannerviewpager
 android:id="@+id/banner"
 android:layout_width="match_parent"
 android:layout_height="200dp">

 </com.chenyu.library.bannerviewpager.bannerviewpager>

 <!-- others -->
</linearlayout>

2、获取bannerviewpager的实例,进行相应的配置,比如我们使用viewpager的时候,也需要设置它的适配器等。这里笔者实现了一个viewpageradapter,用作bannerviewpager的适配器:

//获取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);

和一般的viewpager没什么两样,都是:获取实例——创建适配器——设置适配器。而适配器的数据集一般都是一个view集合,用作viewpager的item view,所以需要事先准备好相应的view集合。此外,一般轮播式viewpager点击某一项后会打开相应的页面,所以这里提供了一个onpageclicklistener的监听器,在创建适配器的时候同时创建该监听器即可。

原理简析

接下来,笔者将简要分析bannerviewpager的实现思路,具体的请读者参考源码~

实现自动滚动

首先,我们先思考一下,系统自带的viewpager是一个独立控件,没有指示器,也没有自动滚动的功能,但是它是一个现成的,可左右滑动的控件,我们肯定是需要viewpager的,因此,我们可以利用一个布局,把viewpager包裹起来,同时在这个布局里面再放入indicator(指示器)。

那么,第一步,先新建bannerviewpager.java继承自framelayout,而这个framelayout有两个子元素:viewpager和indicator。至于indicator,下面会说到。在构造函数内对这两个控件进行初始化先:

public class bannerviewpager extends framelayout implements viewpager.onpagechangelistener {

 private viewpager mviewpager;
 private viewpagerindicator mindicator;
 private viewpageradapter madapter;
 //...
 public bannerviewpager(context context, attributeset attrs, int defstyleattr) {
 super(context, attrs, defstyleattr);
 this.mcontext = context;
 initviews();
 }

 private 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);

 //initialize the indicator
 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);
 }
 //省略...
}

这里没什么好说的,主要是对viewpager和viewpagerindicator进行初始化,设置它们的布局参数以便在framelayout中得到正确的显示。

接着,对viewpager实现自动滚动,这个的实现原理也不难,我们只要知道每时每刻的viewpager的滑动状态、当前的page position值即可,而viewpager有这样一个监听器:viewpager.onpagechangelistener,只要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方法),并且重写这几个方法以实现我们的需求:

 //保存当前的position值
 private int mcurrentposition;
 //viewpager's rolling state
 private int mviewpagerscrollstate;

 @override
 public void onpagescrolled(int position, float positionoffset, int positionoffsetpixels) {
 setindicator(position,positionoffset); //下面会讲到
 }

 @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;
 }
 }

每当当前页面被选中的时候,就会调用onpageselected方法,此时保存当前position值。那么,什么叫做当前页面被选中呢?经过实验验证,当一个item被完全展示在viewpager中的时候,就是选中状态,但如果当前正在被手指拖动,即使下一个item滑动到了中间位置,也不是选中状态。接着,我们看onpagescrollstatechanged方法,当viewpager的状态发生改变的时候,就会触发。那么,**viewpager的状态改变是什么意思呢?**viewpager有如下三种状态:idle,停止状态,无手指触摸;dragging,正在被手指拖动;settling,松开手指的时候,viewpager由于惯性向能滑到的最后一个位置滑去的状态。我们重写的方法中,mviewpagesrollstate记录了viewpager的实时状态,同时停止状态的时候,也记录了一个mreleasingtime值,这个值的作用下面会介绍。通过这个监听器,我们获取到了mcurrentposition和mviewpagescrollstate这两个值。

接下来,我们要考虑自动任务的问题了。在android中,自动任务可以使用handler和runnable来实现,通过postdelay方法来不断实现循环,代码如下:

 private handler mhandler = new handler(){
 @override
 public void handlemessage(message msg) {
  switch (msg.what){
  case message_auto_rolling:
   if(mcurrentposition == madapter.getcount() - 1){
   mviewpager.setcurrentitem(0,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还是等待,因为如果用户当前正在触摸viewpage,那么肯定是不能自动滚动到下一页的,此外,还有一种情况,就是当用户手指离开屏幕的时候,需要等待一段时间才能开始自动滚动任务,否则会造成不好的用户体验,这也就是mreleasingtime的作用之处了。在handler中,根据runnable发送过来的不同信息来进行不同的操作,如果需要滚动到下一个页面,则调用viewpager#setcurrentitem方法来进行滑动,该方法有两个参数,第一个参数是要滑动的位置,第二个参数表示是否开启动画。

实现指示器

接下来,我们来考虑,指示器怎么实现。指示器有如下需求:指示器由一系列圆点构成,未被选中的page所对应的圆点为灰色,而选中的page所对应的圆点为橙色,橙色的圆点能随着page的滑动而滑动。当viewpage的数据变动的时候,比如新增了页面,那么指示器所包含的圆点也会随着变多。

那么,我们可以这样来实现需求:灰色的圆点作为indicator的背景,通过ondraw()方法来绘制,而橙色圆点则通过一个子view来显示,利用onlayout()方法来控制它的位置,这样就能实现橙色圆点在灰色圆点上运动的效果了。而它们具体的位置控制,可以利用上面viewpager.onpagechangelistener#onpagescrolled方法来获取具体的位置以及位置偏移百分比。

我们先来实现绘制部分,新建viewpagerindicator.java继承自linearlayout,先对属性初始化:

public class viewpagerindicator extends linearlayout {

 private context mcontext;
 private paint mpaint;
 private view mmoveview; 
 //省略...

 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.gray);

 mmoveview = new moveview(mcontext);
 addview(mmoveview);
 }

 @override
 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 super.onmeasure(widthmeasurespec, heightmeasurespec);
 setmeasureddimension(mpadding + (mradius*2 + mpadding) * mitemcount,2*mradius + 2*mpadding);
 }

 @override
 protected void ondraw(canvas canvas) {
 super.ondraw(canvas);
 for(int i = 0;i < mitemcount;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.argb(255,255,176,93));
 }

 @override
 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
  super.onmeasure(widthmeasurespec, heightmeasurespec);
  setmeasureddimension(mradius*2,mradius*2);
 }

 @override
 protected void ondraw(canvas canvas) {
  super.ondraw(canvas);
  canvas.drawcircle(mradius,mradius,mradius,mpaint);
 }
 }
}

从上面的代码可以看到,在init()方法内,我们调用了setwillnotdraw(false)方法,这个方法有什么用呢?如果有写过自定义view的读者应该知道,viewgroup默认是不会调用它自身的ondraw()方法的,只有调用了该方法设置为false或者给viewgroup设置一种背景颜色的情况下才会调用ondraw()方法。

解决了这个问题后,我们来看onmeasure()方法,在这个方法内,我们要对该indicator的宽高做出测量,以便接下来的布局和绘制流程,而对于我们的需求而言,只要该布局能够包裹住我们的指示器,并且四边留有一定的空间即可,那么布局的宽度就与page的数量有关了。为了方便起见,这里先给一个默认值,比如5个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);
 }
}

在setpositionandoffset方法内调用了requestlayout()方法,这个方法会导致view树的测量、布局、重绘流程的发生,因此在onlayout方法内,通过mcurrentposition、mpositionoffset这两个值来控制moveview的位置就可以了。

好了,到现在为止,viewpagerindicator基本已经完成了,但是还有一个问题,如果适配器里面的数据刷新了,page的数量变多了,而指示器的数目却依然没变,上面我们使用的mitemcount是默认值,为5个。因此,我们必须在数据刷新的时候,及时通知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(观察者),在viewpageadapter内实现datasetsubject(被观察者),通过registersubscriber方法进行注册,当viewpageadapter的数据列表发生变动的时候,回调datasetsubscriber的update()方法,并把当前的数据长度作为参数传递进来,而bannerviewpager再进一步调用viewpagerindicator的方法来重新布局即可。

先来看viewpagerindicator.java:

public class viewpageradapter extends pageradapter implements datasetsubject {

 private list<datasetsubscriber> msubscribers = new arraylist<>();
 private list<? extends view> mdataviews;
 private onpageclicklistener monpageclicklistener;

 /**
 * 构造函数
 * @param mdataviews view列表
 */
 public viewpageradapter(list<? extends view> mdataviews,onpageclicklistener listener) {
 this.mdataviews = mdataviews;
 this.monpageclicklistener = listener;
 }

 //省略...

 @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());
 }
 }
}```

由于数据列表的变动一般都会调用notifydatasetchanged()方法,所以我们在这个方法内再调用notifysubscriber()方法即可。而在bannerviewpager,则实现datasetsubscriber的update()方法即可,如下所示:

```java
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);

 //start the auto-rolling task if needed
 if(isautorolling){
 postdelayed(mautorollingtask,mautorollingtime);
 }

}

在update()方法内,调用了viewpagerindicator#setitemcount方法,从而重新布局。
那么,指示器也实现完毕了。

实现page的点击事件处理

还有最后一个需求,就是对page的点击进行处理,因为往往viewpager的内容只是一个概括性的内容,为了得到更加详细的信息,用户通常会点击它的item从而打开一个新的页面,这样就需要我们对点击事件进行处理了。其实实现方式不难,思路类似于笔者之前在recyclerview的相关文章的处理点击事件中的方式,通过定义一个新的接口:onpageclicklistener,定义一个onpageclick方法。如下:

public interface onpageclicklistener { 
 void onpageclick(view view,int position);
}

只要在item view初始化的时候,给每个item view都设置一个view.onclicklistener,并且在onclick方法里面调用我们的onpageclick方法即可。

具体如下所示,viewpageradapter:

@override
public view 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;
}

在构建适配器的时候,同时实现onpageclicklistener即可。

以上便是本文的全部内容,非常感谢你的阅读~
欢迎到github中获取本文的源码,欢迎star or fork。再次感谢!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: