Android 图片轮播器的实现及源码解析

在很多产品,尤其是电商类社区内的网页或者app中,我们经常会看到一个图片轮播墙,一页一页的广告/活动/商品介绍每隔一段时间就切换到下一张。那在安卓中我们该如何实现图片轮播器呢?面对自定义样式、自定义图片加载框架等等复杂的自定义需求,如何设计接口使得使用者可以很方便的自定义属性呢?接下来我从wangyeming/ImageBanner源码出发,探讨下我对这个小小功能框架的设计和实现。

图片轮播,一页两页,一页两页

图片轮播

首先明确需求:

  1. 图片轮播器由若干张不定的页面构成,每个页面上的元素包括:图片(必选) + 指示器(可选,可能是点点点,可能是数字等)
  2. 可以手势滑动图片的切换
  3. 闭环展示,第一张的左边是最后一张,最后一张的右边是第一张,无限循环播放。

看一下我画的设计结构图(很丑,轻拍)

设计结构图

可以看到有这么几个对象:

ImageBanner:自定义控件,包含定时任务管理器TimerController、增强ViewPager、指示器BannerIndicator。内部包含了诸如开启,关闭轮播等逻辑。设计为抽象类,通过钩子方法实现UI样式的自定义。

TimerController: 定时任务管理器, 无论是Timer也好,手动设计的定时Handler也好,它的职责就是执行定时任务,具体到图片轮播器里,职责就通知CirclePageAdapter和BannerIndicator切换到下一张。

CustomSwipeViewPager: 增强ViewPager, 方便随时禁止和开启手势滑动。

CirclePageAdapter: ViewPager的adapter, 通过在左右两边各增加一个伪Pager,滑动到0,和最后一个时,无动画切换到最后一个和0,从而实现循环滑动。同样设计为抽象类,ImageView的样式,图片加载的方式等同样通过钩子方法留出来供使用者自定义。

BannerIndicator: 指示器,设计成接口形式,实现该接口的自定义View都可以作为轮播器当中的指示器。最大程度自定义UI。


如何使用?

  • 派生CirclePageAdapter,实现单个图片加载的样式和点击事件
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
public class CustomCirclePageAdapter extends CirclePageAdapter<BannerImage> {

public CustomCirclePageAdapter(Context context) {
super(context);
}

@Override
protected void showImage(ImageView vImage, BannerImage bannerImage) {
//自定义采用何种图片加载方式
Glide.with(mContext)
.load(bannerImage.getImagePath())
.placeholder(R.drawable.default_loading)
.error(R.drawable.topic_pic)
.dontAnimate()
.into(vImage);
}

@Override
protected void onClickImage(final BannerImage bannerImage) {
//自定义每张图片的点击事件
Uri uri = Uri.parse(bannerImage.getLink());
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try {
mContext.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
protected ImageView createImageView() {
//自定义图片的样式
ImageView vImage = new ImageView(mContext);
vImage.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, DemoUtil.dp2px(mContext, 100)));
vImage.setScaleType(ImageView.ScaleType.FIT_XY);
return vImage;
}
}
  • 自定义指示器(可选),实现BannerIndicator接口即可
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
public class CustomBannerIndicator extends LinearLayout implements BannerIndicator {

private List<ImageView> vimg = new ArrayList<>();

public CustomBannerIndicator(Context context) {
this(context, null);
}

public CustomBannerIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CustomBannerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
setOrientation(HORIZONTAL);
}

@Override
public void showInitState(int imageCount) {
for (int i = 0; i < imageCount; i++) {
ImageView vImage = new ImageView(getContext());
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
int margin = DemoUtil.dp2px(getContext(), 10);
layoutParams.setMargins(margin, 0, margin, 0);
vImage.setLayoutParams(layoutParams);
vimg.add(vImage);
vImage.setBackgroundResource(i == 0 ? R.drawable.dot_choosen_ic : R.drawable.dot_unchoosen_ic);
addView(vImage);
}
}

@Override
public void notifyIndexChanged(int indexOfImage) {
for (int i = 0; i < vimg.size(); i++) {
vimg.get(i).setBackgroundResource(i == indexOfImage ? R.drawable.dot_choosen_ic : R.drawable.dot_unchoosen_ic);
}

}
}
  • 配置ImageBanner
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
public class CustomImageBanner extends ImageBanner<BannerImage> {

public CustomImageBanner(Context context) {
super(context);
}

public CustomImageBanner(Context context, AttributeSet attrs) {
super(context, attrs);
}

public CustomImageBanner(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected int getLayoutRes() {
return R.layout.custom_banner;
}

@Override
protected int getImagePagerViewId() {
return R.id.image_parer;
}

@Override
protected CirclePageAdapter<BannerImage> initCirclePageAdapter() {
return new CustomCirclePageAdapter(getContext());
}

@Override
protected int getBannerIndicatorViewId() {
return R.id.image_indicator;
}
}
  • 在xml中使用:
1
2
3
4
5
<com.tianyeguang.imagebanner.banner.CustomImageBanner
android:id="@+id/custom_image_banner"
android:layout_width="match_parent"
android:layout_height="136dp"
android:layout_margin="16dp"/>
  • 传入数据,正确的开启和关闭轮播的定时器:
1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onResume() {
super.onResume();
vBanner.start();
}

@Override
protected void onPause() {
vBanner.finish();
super.onPause();
}

具体的代码大家可以查看demo, demo的样式就是博文上的示意图,谢谢大家的阅读~