轮播页面实践与封装

ANR 与 无限轮播

以前跟着课程做过一个无限轮播的 Demo ,原理就是 Adapter 的 getCount() 返回 Integer.MaxValue 加上子线程控制页面切换。但是这几天在用这种模式的时候发现一个隐藏的坑,有很高几率触发 ANR,这可是不得了的问题(我就不说为了找到这个 ANR 原因花了多大功夫)。在这种模式下,当更新 ViewPager 的数据源的时候,基本上百分百会导致 ANR ,原因就是 PagerAdapter 的 getCount()返回一个很大的值,再调用 setCurrentItem() 更新页面,如果引起的页面跨度超过 1,例如从第 3 页跳转到第 1 页,就会导致 ANR。

通过阅读源码,在 populate(int newCurrentItem)calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)这两个方法中,有 for 循环的执行次数和 getCount() 成正比[1],但是在第一次执行 setCurrentItem() 却不会引起 ANR,同时该方法的第二个参数与是否会引起 ANR 并没有关系。

因为这个原因,综合考虑我就 getCount() 中返回 1000 * data.size() 即可。设置 currentItem 为 getCount() / 2 - (getCount() % data.size() ,这样左右都能滑动 500 次,又可以避免 ANR,已经能够满足基本的使用了。

自动轮播

ViewPager 的自动轮播可以用多种方式控制实现,开个子线程,开个定时器,用个 Handler,都是可以的。但是这其中,无疑以 Handler 方式为最佳。子线程虽然也不错,但是子线程的开启是为了保证主线程不卡顿,一般开子线程是为了完成某个独立的耗时任务,这种任务通常都是由封装好的线程池去执行,要避免在 App 中充斥着闲散的子线程。至于定时器,与 Handler 最大的区别就是 timer 的任务不能单独取消,若要取消就会取消全部任务。

具体的逻辑见 AutoCarouselHandler 源码:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class AutoCarouselHandler extends Handler {
/**
* 请求更新页面
*/
public static final int MSG_UPDATE_IMAGE = 1;
/**
* 请求暂停轮播
*/
public static final int MSG_KEEP_SILENT = 2;
/**
* 请求停止轮播
*/
public static final int MSG_STOP_UPDATE = 3;
/**
* 默认轮播间隔时间,用户触摸等待时间
*/
public static final long MSG_DELAY = 5000;
/**
* 使用弱引用避免 Handler 泄露
*/
private WeakReference<ViewPager> pagerRef;
/**
* 轮播间隔时间
*/
private long carouselMillis;

public AutoCarouselHandler(WeakReference<ViewPager> pagerRef) {
carouselMillis = MSG_DELAY;
this.pagerRef = pagerRef;
}

public AutoCarouselHandler(WeakReference<ViewPager> pagerRef, long carouselMillis) {
this.carouselMillis = carouselMillis;
this.pagerRef = pagerRef;
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
ViewPager viewPager = pagerRef.get();
if (viewPager == null) {
//viewPager 已经回收,无需再处理 UI 了
return;
}
//避免多次刷新导致积累连续刷新
if (hasMessages(AutoCarouselHandler.MSG_UPDATE_IMAGE)) {
removeMessages(AutoCarouselHandler.MSG_UPDATE_IMAGE);
}
switch (msg.what) {
case MSG_UPDATE_IMAGE:
int pos = viewPager.getCurrentItem();
viewPager.setCurrentItem(pos + 1);
//准备下次播放
sendEmptyMessageDelayed(MSG_UPDATE_IMAGE, carouselMillis);
break;
case MSG_KEEP_SILENT:
//触摸,取消更新AD的消息
if (hasMessages(MSG_UPDATE_IMAGE)) {
removeMessages(MSG_UPDATE_IMAGE);
}
//重新开始计时,如果继续暂停,那么继续取消
sendEmptyMessageDelayed(MSG_UPDATE_IMAGE, (Long) msg.obj);
break;
case MSG_STOP_UPDATE:
//停止轮播,清空所有计划
if (hasMessages(MSG_UPDATE_IMAGE)) {
removeMessages(MSG_UPDATE_IMAGE);
}
if (hasMessages(MSG_KEEP_SILENT)) {
removeMessages(MSG_KEEP_SILENT);
}
break;
default:
break;
}
}
}

AutoCarouselHandler 是对所有通用轮播 Handler 的一个封装,可以适用于大部分的自动轮播的需求。根据这个 Handler ,来看看 PageAdapter 要如何构建:
为了结构清晰,我定义了一个 Carouselable 接口,里面包含三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   public interface Carouselable {
/**
* 开始轮播
*/
void startCarousel();

/**
* 停止轮播
*/
void stopCarousel();

/**
* 暂停轮播,自动恢复轮播,默认暂停时间为轮播的切换时间
*/
void pauseCarousel();

/**
* 暂停轮播,自主设置暂停轮播时间
* @param millis
*/
void pauseCarousel(long millis);
}

PagerAdapter 源码:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class AdPagerAdapter extends PagerAdapter implements Carouselable {
public static final long NORMAL_CAROUSEL_MILLIS = 5000;
private List<ImageView> adViewList;
private int count;
private Handler handler;
private long carouselMillis;
private WeakReference<ViewPager> pager;

public AdPagerAdapter(List<ImageView> adViewList, ViewPager viewPager) {
this(adViewList, viewPager, NORMAL_CAROUSEL_MILLIS);
}

public AdPagerAdapter(List<ImageView> adViewList, ViewPager viewPager, long carouselMillis) {
this.adViewList = adViewList;
this.carouselMillis = carouselMillis;
pager = new WeakReference<>(viewPager);
handler = new AutoCarouselHandler(pager, carouselMillis);
}

@Override
public int getCount() {
return 1000 * adViewList.size();
}

@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
int pos = position % adViewList.size();
pos = pos < 0 ? position + adViewList.size() : pos;
ViewGroup parent = (ViewGroup) adViewList.get(pos).getParent();
if (parent != null) {
parent.removeView(adViewList.get(pos));
}
container.addView(adViewList.get(pos));
return adViewList.get(pos);
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (adViewList.size() > 3) {
container.removeView((View) object);
}
}

@Override
public int getItemPosition(Object object) {
if (count > 0) {
count--;
return POSITION_NONE;
}
return super.getItemPosition(object);
}

@Override
public void notifyDataSetChanged() {
count = getCount();
super.notifyDataSetChanged();
}


@Override
public void startCarousel() {
int pos = getCount() / 2 - (getCount() % adViewList.size());
pager.get().setCurrentItem(pos);
handler.sendEmptyMessageDelayed(AutoCarouselHandler.MSG_UPDATE_IMAGE, carouselMillis);
}

@Override
public void stopCarousel() {
handler.sendEmptyMessage(AutoCarouselHandler.MSG_STOP_UPDATE);
}

@Override
public void pauseCarousel() {
pauseCarousel(carouselMillis);
}

@Override
public void pauseCarousel(long millis) {
Message msg = Message.obtain();
msg.what = AutoCarouselHandler.MSG_KEEP_SILENT;
msg.obj = millis;
handler.sendMessage(msg);
}
}

如果要在用户触摸 ViewPager 的时候暂停轮播,只需要在 onTouchLIstener 的 onTouch() 中发送暂停轮播的消息即可:

1
2
3
4
5
6
@Override
public boolean onTouch(View v, MotionEvent event) {
//暂停轮播
adPagerAdapter.pauseCarousel();
return false;
}

总结

其实要实现轮播并不复杂,关键是要实现一个健壮性较高的组件就比较苦难,就拿文章开头说的那种实现,如果没有去更新数据源,没有大跨度跳转页面,我可能永远都不知道原来一个 ANR 距离我们那么近还没有发现。而所做的一系列的看似复杂多余的工作都是为了更高的健壮性,并且虽然开头看似做了好多无用的工作,但是到了后面才会发现这是为以后做基础,在之后的开发里就会节省大量的时间。