实现安卓无限轮播组件Banner

前言

前些天需要使用到安卓的banner,也就是现在主流app主页的无限轮播的横幅,现在已经有很多好的开源项目可以直接使用,不过我还是想自己去实现一遍。因为是访问的网络数据,实际过程中还是有些坑的,所以还是记录一下。

具体实现

首先还是来看看最终的效果,gif是有些卡顿,跑起来还是很流畅的
BannViewDemo
了解到,现在实现这种横幅,基本上是2种方式,一种是使用RecyclerView的横向滚动去实现,因为横幅是从一个页面直接跳转到下一页,用RecyclerView需要监听滑动的过程,计算滑动的距离,然后进行跳转,后面官方考虑到这一点提供了PagerSnapHelper这个工具类来解决这个问题,这里就不多说了。
这篇博客主要就是写的就是第二种方式,使用ViewPager去实现。

首先,要用ViewPager实现无限轮播,可以使PagerAdapter的getCount方法返回Integer.MAX_VALUE。也就是让页面数量返回一个Integer的最大值,这样在滑动过程中产生一种无限循环的假象,首先写一个抽象基类,继承自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
public abstract class BannerViewBaseAdapter extends PagerAdapter {
private List<View> mList;
private View mView;
public BannerViewBaseAdapter() {
mList = new ArrayList<>();
}
/**
* 返回Integer的最大值
*/
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Log.d("I am postion cx cx xcx", String.valueOf(position));
if (getSize() != 0) {
if (mList.size() <= (position % getSize())) {
for (int i = mList.size();i <= position % getSize();++i) {
mList.add(getView(container,i));
}
}
mView = mList.get(position % getSize());
if (mView.getParent() != null) {
container.removeView(mView);
}
container.addView(mView);
}
return mView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
// 滑动下一张图时当前的图
if (getSize() != 0 && position != 0) {
container.removeView(mList.get(position % getSize()));
}
}
/**
* 获取要显示的View
* @param container
* @param position
* @return
*/
public abstract View getView(ViewGroup container,int position);
/**
* 获取实际ItemView的数量
* @return
*/
public abstract int getSize();

这里主要是重写instantiateItem方法,进行添加页面的逻辑,首先要判断getSize不为0,因为实际网络加载时可能是一个异步的耗时操作,如果执行到下面的计算时,除数为0肯定会报错的,其它类似地方也都是这样,然后注意到下面的position%getSize,这里是用当前位置对实际item的数量进行模运算取余,得到的值就是当前item的实际位置(前面说过无限轮播是不停的增加页面,造成轮播的假象,实际位置就是指在进行轮播的几个item中,当前处的位置),接下来就判断,如果item不在集合中,就把view添加到一个List集合里面,最后要防止同一个view的重复添加,所以每次添加前需要移除这个view。
接着重写destroyItem方法,每次循环跳转时都要销毁掉之前的view。下面两个抽象方法就是用来获取到具体的值了。

然后就是我们的具体视图的适配器

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
public class BannerViewAdapter extends BannerViewBaseAdapter {
private List<TestBean> mBeansList;
private Context mContext;
public BannerViewAdapter(List<TestBean> bannerBeans) {
this.mBeansList = bannerBeans;
}
@Override
public View getView(ViewGroup container, int position) {
AppCompatImageView imageView;
TextView title;
if (mContext == null) {
mContext = container.getContext();
}
View mView = LayoutInflater.from(mContext).inflate(R.layout.banner_item_layout,null);
final TestBean bean = mBeansList.get(position);
imageView = mView.findViewById(R.id.image);
title = mView.findViewById(R.id.banner_title);
title.setText(bean.getTitle());
Glide.with(mContext).load(bean.getImageId())
.error(R.drawable.ic_launcher_background)
.into(imageView);
notifyDataSetChanged();
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext,"你点击了"+bean.getTitle(),Toast.LENGTH_SHORT).show();
}
});
return mView;
}
@Override
public int getSize() {
return mBeansList.size();
}
}

这里基础抽象基类,逻辑比较简单,没什么好讲的。

视图绘制

然后重点就是自定义的BannerView类了

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {
private ViewPager mViewPager;
/**
* 圆点布局
*/
private LinearLayout mPointContainer;
private BannerViewBaseAdapter mAdapter;
/**
* 圆点数量
*/
private int mPointCount;
/**
* 圆点图片
*/
private ImageView[] mPoints;
/**
* 最后一个圆点
*/
private int mLastPos;
/**
* 当前是否触摸
*/
private boolean isTouch = false;
private ScheduledExecutorService executorService;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
postDelayed(new Runnable() {
@Override
public void run() {
mViewPager.setCurrentItem(mViewPager.getCurrentItem()+1);
}
},1000);
break;
default:
break;
}
}
};
public BannerView(@NonNull Context context, AttributeSet attributeSet) {
super(context,attributeSet);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initView();
}
private void initView() {
mViewPager = findViewById(R.id.views_container);
mPointContainer = findViewById(R.id.point_container);
mViewPager.addOnPageChangeListener(this);
}
public void setAdapter(BannerViewAdapter adapter) {
this.mAdapter = adapter;
mPointCount = mAdapter.getSize();
mViewPager.setAdapter(mAdapter);
Log.d("sddsccdsvdsvdv", String.valueOf(mPointCount*100));
initPoint();
/**
* 防止第二次刷新后 显示空白页面
*/
mViewPager.setCurrentItem(mPointCount*100+3);
startScroll();
}
/**
* 加载圆点
*/
private void initPoint() {
if (mPointCount == 0) {
return;
}
mPoints = new ImageView[mPointCount];
// 清chu所有圆点
mPointContainer.removeAllViews();
for (int i=0;i < mPointCount;i++) {
ImageView view = new ImageView(getContext());
view.setImageResource(R.drawable.point_normal);
mPointContainer.addView(view);
mPoints[i] = view;
}
if (mPoints[0] != null) {
mPoints[0].setImageResource(R.drawable.point_selected);
}
mLastPos = 0;
}
/**
* 改变圆点位置
*/
private void changePoint(int currentPoint) {
if (mLastPos == currentPoint) {
return;
}
mPoints[currentPoint].setImageResource(R.drawable.point_selected);
mPoints[mLastPos].setImageResource(R.drawable.point_normal);
mLastPos = currentPoint;
}
public void startScroll() {
executorService = new ScheduledThreadPoolExecutor(1);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (isTouch) {
return;
}
handler.sendEmptyMessage(0);
}
},1000,3000, TimeUnit.MILLISECONDS);
}
public void cancelScroll() {
if (executorService != null) {
executorService.shutdown();
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (mPointCount != 0) {
changePoint(position % mPointCount);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouch = true;
break;
case MotionEvent.ACTION_UP:
isTouch = false;
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
}

BannerView继承FrameLayout,实现滑动监听的接口,这里先看看initView方法,这里给viewpager设置了适配器,然后接着就是加载圆点指示器的方法,逻辑也比较简单,加载前注意先移除掉之前的圆点,防止刷新后点的数量重复添加。然后就是第一次加载时,时ViewPager跳转到mPoinCoun*100+x的位置,也是为了防止首次加载无法向左滑动,就不是无限循环的假象了。然后就让我们的ViewPager执行定时滑动任务,定时任务有很多的实现方式,可以使用Timer+handler的方式,可以使用CountDownTimer类来实现,这里的话,我使用的是线程池来进行的定时任务,后面分别传入预加载与跳转周期,然后里面需要进行判断当前是否触摸屏幕,所以要重写dispatchEvent方法,监听当前的动作,然后向handler发送消息,再进行页面滑动,这里可能会有疑惑,线程池已经设置了定时任务,为什么还要向handler去发送消息,进行延时处理,handler里面才是真正的滚动延时,除了是非Ui线程不进行Ui更新的操作 也是因为在我们刷新后,线程池会造成阻塞,无法正常执行。我使用了其它几种方式,还是出现了各种问题,这里就不多说了。

整个BannerView的流程就走完了
再看看布局文件 banner_item_layout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"/>
<TextView
android:id="@+id/banner_title"
android:text="我是圈子名字"
android:textSize="15sp"
android:textColor="@android:color/white"
android:layout_width="match_parent"
android:gravity="center_vertical"
android:paddingLeft="10dp"
android:layout_gravity="bottom"
android:background="@color/transparency"
android:layout_height="35dp" />
</android.support.design.widget.CoordinatorLayout>

banner_view_layout

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
<com.legend.bannerviewdemo.banner.BannerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/my_slide_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v4.view.ViewPager
android:id="@+id/views_container"
android:layout_width="match_parent"
android:layout_height="180dp"></android.support.v4.view.ViewPager>
<LinearLayout
android:id="@+id/indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="5dp"
android:orientation="vertical"
android:visibility="visible">
<LinearLayout
android:id="@+id/point_container"
android:layout_width="match_parent"
android:layout_height="11dp"
android:gravity="center_horizontal"
android:orientation="horizontal" />
</LinearLayout>
</com.legend.bannerviewdemo.banner.BannerView>

使用的话就直接给adapter添加数据,然后实例化给BannerView设置adapter就可以了。这里很要注意一点,就是每次刷新之前记得手动关闭线程池,也就是调用BannView中的cancelScroll()方法,不然会造成进行网络加载,第二次刷新加载数据时,banner直接出现空白页面,这肯定不是我们想要的。 另外,配合RecyclerView使用时,可以把BannerView动态添加到RecyclerView的头部,直接放到布局中,如果要实现RecyclerView上滑时,banner跟着一起滚动的话,可以使用NestedScrollView,但是会造成嵌套滑动冲突,这一点也没看到好的解决办法,设置了NestedScrollingEnable属性,但是会导致RecyclerView无法上拉加载,如果一次性加载完数据的话,RecyclerView的复用和回收机制就没起到作用了,很容易出现OOM,这也不是我们想看到的,所以最好还是配合RecyclerView使用。

总结

总的来说,实现一个并不困难,难在实际过程中会出现各种各样的问题,也正是常说的,debug时间远远多于写代码的是时间(说到底还是经验不足的,理解不够深的缘故)。废话不多说,最后该Demo地址github

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 前言
  2. 2. 具体实现
    1. 2.1. 定义适配器
    2. 2.2. 视图绘制
  3. 3. 总结
,