打造一个丝滑般自动轮播无限循环Android库(一)

本文同步发布在掘金,如需转载请注明出处。

BannerViewPager系列文章共三篇,此文为第一篇,另外两篇参看下面链接:

BannerViewPager源码剖析

剖析BannerViewPager中Indicator的设计思想

最近公司项目在升级AndroidX,由于项目中用到的一些比较老的库都已停止更新维护,因此需要将这些库替换掉,其中就包括自动轮播的Banner库。恰逢笔者在之前写过一个轮播图,因此就在此基础上重构,打造出了一个全新的支持多种样式的轮播库—BannerViewPager。个人觉得BannerViewPager要优于其它开源的Banner库,不仅仅是因为它拥有简洁高效的代码,更是因为它高度的可定制性。BannerViewPager不仅支持任意的页面布局,而且可以支持任意的Indicator样式。甚至连Indicator的位置都可以做到任意摆放。是的,就是这么随心所欲。无图言叼,还是先通过图片和代码一览BannerViewPager的功能吧(多图预警)。

一、BannerViewPager效果预览及API介绍

由于GIF图片质量问题,下面的预览图并不清晰,大家可以点击下面链接或者扫描二维码下载Apk体验。Apk存放在github上,下载速度可能会比较慢。

点击或扫描二维码下载apk

1.setIndicatorStyle(开局就放王炸?)

BannerViewPager目前内置了CIRCLE和DASH两种样式的指示器,通过setIndicatorStyle(int)一行代码就可以切换指示器的样式。当然,如果内置样式不满足你的需求。BannerViewPager还提供了自定义指示器的功能。只要继承BaseIndicatorView或者实现IIndicator接口,并重写相应方法,就可以通过自定义View为所欲为的打造任意的Indicator了。如下图【自定义】就是自己实现的指示器样式。

CIRCLE DASH 自定义

下面通过代码演示如何切换指示器:

1
2
3
4
5
mViewPager.setIndicatorStyle(IndicatorStyle.DASH)
.setIndicatorHeight(BannerUtils.dp2px(3f))
.setIndicatorWidth(BannerUtils.dp2px(3), BannerUtils.dp2px(10))
.setHolderCreator(() -> new ImageResourceViewHolder(0))
.create(mDrawableList)

通过5行代码就轻松的实现了上图【Dash】仿支付宝的Indicator样式(大家可以留意一下支付宝的轮播Indicator,挺有意思)。

关于自定义IndicatorView将会放在后边章节详细讲解。

2.setPageStyle

通过setPageStyle(int)一行代码开启一屏三页模式,一屏三页模式下目前有三种样式,分别如下图所示:

MULTI_PAGE MULTI_PAGE_SCALE MULTI_PAGE_OVERLAP
MULTI_PAGE MULTI_PAGE_SCALE MULTI_PAGE_OVERLAP
代码演示:
1
2
3
4
5
mViewPager.setPageStyle(PageStyle.MULTI_PAGE)
.setPageMargin(BannerUtils.dp2px(10))
.setRevealWidth(BannerUtils.dp2px(10))
.setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5)))
.create(mDrawableList);

同样通过短短5行代码就实现了上图【MULTI_PAGE】的效果,简单好用!

3.如何实现指示器位置任意摆放?

我们看到上面图表中MULTI_PAGE_OVERLAP模式下指示器显示到了Banner的下边。这种效果该怎么实现呢?其实BannerViewPager是支持把Indicator摆放在任意位置的。之所以能如此强大是因为我们通过自定义指示器替换了内置的IndicatorView,也就是说此时的IndicatorView已经脱离了BannerViewPager,也就理所当然的可以放在任意位置了。接下来通过代码来看下如何实现:

(1)Xml布局文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.zhpan.bannerview.BannerViewPager
android:id="@+id/banner_view"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_marginTop="20dp"
app:bvp_page_style="multi_page" />

<com.zhpan.bannerview.indicator.CircleIndicatorView
android:id="@+id/indicator_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/banner_view"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp" />
</RelativeLayout>

(2)通过setIndicatorView(IIndicator)替换内部指示器

1
2
3
4
5
6
      CircleIndicatorView indicatorView = findViewById(R.id.indicator_view);
mViewPager.setIndicatorView(indicatorView)
.setIndicatorColor(Color.parseColor("#888888"),
Color.parseColor("#118EEA"))
.setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5)))
.create(mDrawableList);

CircleIndicatorView是什么?其实他就是内置在BannerViewPager中的指示器,现在你只需要把它同BannerViewPager放在同一个布局文件中就可以了。又是仅仅通过一行代码就完成了对内部指示器的替换,不知道你看完之后是否会拍案叫绝,竟然如此简单!

4.setIndicatorSlideMode

我们应该见过很多App轮播图的指示器都会跟随页面一起滑动。BannerViewPager自然也不会少了这个功能。通过setIndicatorSlideMode(int)一行代码就可以轻松切换到下图(SMOOTH)的效果。

NORMAL SMOOTH

代码实现仍然非常简单,使用BannerViewPager你只需要记住一个核心–Only One Line!所以演示代码不再贴出你应该不会揍我吧?

5.setPageTransformerStyle

关于Transform更好的方式应该是留给开发者自己去实现,因此BannerViewPager中目前仅内置了四种常用Transform样式,如果不能满足需求,可以通过BannerViewPager的setPageTransformer(ViewPager.PageTransformer transformer)设置自定义的Transform。四种内置Transform样式如下:

STACK ACCORDION DEPTH ROTATE

当然,BannerViewPager的功能并不仅仅局限于此,更多功能就不再演示,可以看下面所有开放的API接口。

6.BannerViewPager开放的API

BannerViewPager开放了众多API,以供满足不同的需求,具体如下表:

方法名 方法描述 说明
BannerViewPager<T, VH> setCanLoop(boolean canLoop) 是否开启循环 默认值true
BannerViewPager<T, VH> setAutoPlay(boolean autoPlay) 是否开启自动轮播 默认值true
BannerViewPager<T, VH> setInterval(int interval) 自动轮播时间间隔 单位毫秒,默认值3000
BannerViewPager<T, VH> setScrollDuration(int scrollDuration) 设置页面滚动时间 设置页面滚动时间
BannerViewPager<T, VH> setRoundCorner(int radius) 设置圆角 默认无圆角 需要SDK_INT>=LOLLIPOP(API 21)
BannerViewPager<T, VH> setOnPageClickListener(OnPageClickListener onPageClickListener) 设置页面点击事件
BannerViewPager<T, VH> setHolderCreator(HolderCreator<VH> holderCreator) 设置HolderCreator 必须设置HolderCreator,否则会抛出NullPointerException
BannerViewPager<T, VH> setIndicatorVisibility(@Visibility int visibility) indicator vibility 默认值VISIBLE 2.4.2 新增
BannerViewPager<T, VH> setIndicatorStyle(int indicatorStyle) 设置指示器样式 可选枚举(CIRCLE, DASH) 默认CIRCLE
BannerViewPager<T, VH> setIndicatorGravity(int gravity) 指示器位置 可选值(CENTER、START、END)默认值CENTER
BannerViewPager<T, VH> setIndicatorColor(int normalColor,int checkedColor) 指示器圆点颜色 normalColor:未选中时颜色默认”#8C6C6D72”, checkedColor:选中时颜色 默认”#8C18171C”
BannerViewPager<T, VH> setIndicatorSlideMode(int slideMode) 设置Indicator滑动模式 可选(NORMAL、SMOOTH),默认值SMOOTH
BannerViewPager<T, VH> setIndicatorRadius(int radius) 设置指示器圆点半径 默认值4dp
BannerViewPager<T, VH> setIndicatorRadius(int normalRadius,int checkRadius) 设置指示器圆点半径 normalRadius:未选中时半径 checkedRadius:选中时的半径,默认值4dp
BannerViewPager<T, VH> setIndicatorWidth(int indicatorWidth) 设置指示器宽度,如果是圆形指示器,则为直径 默认值8dp
BannerViewPager<T, VH> setIndicatorWidth(int normalWidth, int checkWidth) 设置指示器宽度,如果是圆形指示器,则为直径 默认值8dp
BannerViewPager<T, VH> setIndicatorHeight(int indicatorHeight) 设置指示器高度,仅在Indicator样式为DASH时有效 默认值normalIndicatorWidth/2
BannerViewPager<T, VH> setIndicatorGap(int indicatorMargin) 指示器圆点间距 默认值为指示器宽度(或者是圆的直径)
BannerViewPager<T, VH> setIndicatorView(IIndicator indicatorView) 设置自定义指示器
BannerViewPager<T, VH> setPageTransformerStyle(int style) 设置页面Transformer内置样式
BannerViewPager<T, VH> setCurrentItem(int item) Set the currently selected page. 2.3.5新增
void getCurrentItem() 获取当前position 2.3.5新增
BannerViewPager<T, VH> setPageStyle(PageStyle pageStyle) 设置页面样式 2.4.0新增 可选(MULTI_PAGE、NORMAL)MULTI_PAGE:一屏多页样式
BannerViewPager<T, VH> setPageMargin(int pageMargin) 设置页面间隔 2.4.0新增
BannerViewPager<T, VH> setIndicatorMargin(int left, int top, int right, int bottom) 设置Indicator边距 2.4.1新增
BannerViewPager<T, VH> setOnPageChangeListener(OnPageChangeListener l) 页面改变的监听事件 2.4.3新增
void startLoop() 开启自动轮播 初始化BannerViewPager时不必调用该方法,设置setAutoPlay后会调用startLoop()
void stopLoop() 停止自动轮播 如果开启自动轮播,为避免内存泄漏需要在onStop()或onDestroy中调用此方法
List<T> getList() 获取Banner中的集合数据
void create(List list) 初始化并构造BannerViewPager 必须调用,否则前面设置的参数无效

7.BannerViewPager支持的attrs

你也可以通过xml来设置BannerViewPager,xml支持的attrs如下:
| Attributes | format | description |
|–|–|–|
| bvp_interval | integer | 自动轮播时间间隔 |
| bvp_scroll_duration | integer | 页面切换时滑动时间|
| bvp_can_loop | boolean| 是否循环 |
| bvp_auto_play | boolean | 是否自动播放 |
| bvp_indicator_checked_color | color | indicator选中时颜色 |
| bvp_indicator_normal_color | color | indicator未选中时颜色 |
| bvp_indicator_radius | dimension | indicator圆点半径或者Dash模式的1/2宽度 |
| bvp_round_corner| dimension | Banner圆角大小 |
| bvp_page_margin | dimension | 页面item间距 |
| bvp_reveal_width | dimension | 一屏多页模式下两边item漏出的宽度 |
| bvp_indicator_style | enum | indicator样式(circle/dash) |
| bvp_indicator_slide_mode | enum | indicator滑动模式(normal/smooth) |
| bvp_indicator_gravity | enum | indicator位置(center/start/end) |
| bvp_page_style | enum | page样式(normal/multi_page/multi_page_overlap/multi_page_scale) |
| bvp_transformer_style | enum | transform样式(normal/depth/stack/accordion) |
| bvp_indicator_visibility| enum | indicator visibility(visible/gone/invisible) |

二、BannerViewPager详细使用说明

1.gradle中添加依赖

如果您已迁移到AndroidX请使用latestVersion(>=2.4.3.1)

1
2
implementation 'com.zhpan.library:bannerview:latestVersion'

如果未迁移到AndroidX请使用(非Androidx的包托管在JCenter上):

1
implementation 'com.zhpan.library:bannerview:2.4.3.1'

2. 在xml文件中添加如下代码:

1
2
3
4
5
<com.zhpan.bannerview.BannerViewPager
android:id="@+id/banner_view"
android:layout_width="match_parent"
android:layout_margin="10dp"
android:layout_height="160dp" />

3.Banner的Item页面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/banner_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#66000000"
android:gravity="center_vertical">

<TextView
android:id="@+id/tv_describe"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="15dp"
android:gravity="center_vertical"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</LinearLayout>

</RelativeLayout>

4.自定义ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NetViewHolder implements ViewHolder<BannerData> {
private ImageView mImageView;
private TextView mTextView;

@Override
public View createView(ViewGroup viewGroup, Context context, int position) {
View view = LayoutInflater.from(context).inflate(R.layout.item_net, viewGroup, false);
mImageView = view.findViewById(R.id.banner_image);
mTextView = view.findViewById(R.id.tv_describe);
return view;
}

@Override
public void onBind(Context context, BannerData data, int position, int size) {
ImageLoaderOptions options = new ImageLoaderOptions.Builder().into(mImageView).load(data.getImagePath()).placeHolder(R.drawable.placeholder).build();
ImageLoaderManager.getInstance().loadImage(options);
mTextView.setText(data.getTitle());
}
}

5.BannerViewPager参数配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   private BannerViewPager<BannerData, NetViewHolder> mBannerViewPager;
private void initViewPager() {
mBannerViewPager = findViewById(R.id.banner_view);
mBannerViewPager.showIndicator(true)
.setInterval(3000)
.setCanLoop(false)
.setAutoPlay(true)
.setRoundCorner(DpUtils.dp2px(7))
.setIndicatorColor(Color.parseColor("#935656"), Color.parseColor("#FF4C39"))
.setIndicatorGravity(BannerViewPager.END)
.setScrollDuration(1000).setHolderCreator(NetViewHolder::new)
.setOnPageClickListener(position -> {
BannerData bannerData = mBannerViewPager.getList().get(position);
Toast.makeText(NetworkBannerActivity.this,
"点击了图片" + position + " " + bannerData.getDesc(), Toast.LENGTH_SHORT).show();

}).create(mList);
}

6.开启与停止轮播

2.5.0之后版本无需自行在Activity或Fragment中管理stopLoop和startLoop方法,但这两个方法依旧保留对外开发

如果开启了自动轮播功能,请务必在onDestroy中停止轮播,以免出现内存泄漏。

1
2
3
4
5
6
@Override
protected void onDestroy() {
super.onDestroy();
if (mBannerViewPager != null)
mViewpager.stopLoop();
}

为了节省性能也可以在onStop中停止轮播,在onResume中开启轮播:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onStop() {
super.onStop();
if (mBannerViewPager != null)
mBannerViewPager.stopLoop();
}

@Override
protected void onResume() {
super.onResume();
if (mBannerViewPager != null)
mBannerViewPager.startLoop();
}

三、高级功能—自定义IndicatorView

因为指示器的样式千变万化,BannerViewPager中不可能内置所有的样式,因此我将定义权限交给了开发者自己来实现,这样就可以满足所有开发者的需求了。但是自定义IndicatorView需要有一定的自定义View基础,尽管我已经在BaseIndicatorView中处理了许多逻辑,但是还是要开发者根据自身需求进行Indicator的绘制。好了,下面就让我们来看看如何实现自定义IndicatorView吧。

关于自定义IndicatorView其实我们在第一节中讲解Indicator摆放位置时已经提到了,就是通过setIndicator(IIndicator)来替换内部的指示器。当然,这个方法接收的参数不仅仅是内置的两个IndicatorView,它还可以是我们自己实现的Indicator。前提只需要继承BaseIndicatorView或者继承View并实现IIndicator,然后根据需求绘制即可。

(1)认识BaseIndicatorView

BaseIndicatorView是BannerViewPager库中的一个类,它继承自View并实现了IIndicator接口。在这个类中存储了BannerViewPager的许多参数信息,比如页面个数(pageSize)、页面滑动进度(slideProgress)以及当前页面位置(currentPosition)等,这些都是在绘制IndicatorView时会用到的信息。有了这些参数之后我们就可以比较轻松的去绘制指示器了。如果你觉得我这些数据计算的不够精确或者计算存在错误,那么你大可以自己实现IIndicator接口自行计算。本文我会通过继承BaseIndicatorView的方式来实现一个自定义指示器的例子。
你可以点击链接查看BaseIndicatorView的完整代码。

(2)开启自定义IndicatorView之路

好了,接下来我们就来完成一个如下图所示的自定义IndicatorView吧!

新建一个FigureIndicatorView类并继承BaseIndicatorView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class FigureIndicatorView extends BaseIndicatorView {

private int radius = DpUtils.dp2px(20);

private int backgroundColor = Color.parseColor("#88FF5252");

private int textColor = Color.WHITE;

private int textSize=DpUtils.dp2px(13);

// ...省略无关代码

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(2 * radius, 2 * radius);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(backgroundColor);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);
mPaint.setColor(textColor);
mPaint.setTextSize(textSize);
String text = currentPosition + 1 + "/" + pageSize;
int textWidth = (int) mPaint.measureText(text);
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int baseline = (getMeasuredHeight() - fontMetricsInt.bottom + fontMetricsInt.top) / 2 - fontMetricsInt.top;
canvas.drawText(text, (getWidth() - textWidth) / 2, baseline, mPaint);
}

public void setRadius(int radius) {
this.radius = radius;
}

@Override
public void setBackgroundColor(@ColorInt int backgroundColor) {
this.backgroundColor = backgroundColor;
}

public void setTextSize(int textSize) {
this.textSize = textSize;
}
// ...省略无关代码
}

有自定义View基础的同学应该能很轻松的看懂上边的代码。首先通过onMeasure()方法测量了View的大小,接下来就是在onDraw方法中绘制圆和文字了。很容易就实现了一个自定义的IndicatorView。当然,这个例子本身就比较简单。如果你需要绘制比较复杂且带有动画的Indicator,可以参考源码中的CircleIndicatorView和DashIndicatorView,或许它能给你一些灵感。

(3)设置自定义指示器

接下来就将我们自己绘制的指示器设置到BannerViewPager中吧!

1
2
3
4
5
6
7
8
9
10
FigureIndicatorView indicatorView = new FigureIndicatorView(mContext);
indicatorView.setRadius(BannerUtils.dp2px(18));
indicatorView.setTextSize(BannerUtils.dp2px(13));
indicatorView.setBackgroundColor(Color.parseColor("#aa118EEA"));

mViewPager.setIndicatorGravity(IndicatorGravity.END)
.setIndicatorView(indicatorView)
.setHolderCreator(() -> new ImageResourceViewHolder(0))
.create(mDrawableList);

依然如此潇洒自然!好了,关于BannerViewPager的介绍今天就讲解到这里了。接下来的一篇文章将会对BannerViewPager的源码进行剖析,了解下它是如何通过简单的Api实现来实现复杂的功能的。

都看到这里了,确定不到GitHub点个星再走?源码已放到文章末尾。如果有好的Idea也欢迎Pull Request。

《BannerViewPager源码解析》

源码下载

文章上次更新2019.11.16


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!