关于 循环 Banner 的自定义 View 实现

最近在肝工作室的最终考核,不过感觉可以趁机水好多篇博客??
这次是通过自定义 View 来实现循环的 Banner 。
写之前在网上看过很多教程,大都是一个很大的 ViewPager ,我总是感觉那样不好,所以我就自己写了一个。

此外,带有 实战 标签的文章大都不涉及复杂的原理过程,重在贴代码。可以自行研究。

最终效果

图片加载器

首先把图片加载抽象出来,因为 Banner 里图片可以是从文件拿,可以是从网上拿,也可以在 Drawable 拿。为了适配,我把图片加载抽象了出来。

1
2
3
4
5
6
7
8
9
10
11
/**
* Created by HeYanLe on 2020/5/7 0007.
* https://github.com/heyanLE
*/
public interface ImageInfo {

ImageInfo prepare();

void covert(ImageView imageView);

}

这就是一个图片的加载接口,调用 prepare 加载图片,调用 covert(ImageView) 把图片渲染到 ImageView 里。

因为我图片是拿 Drawable 里面的,我就直接写上 Drawable 加载器的实现了,其他网络图片啥的可以参考。

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
/**
* Created by HeYanLe on 2020/5/7 0007.
* https://github.com/heyanLE
*/
public class DrawableInfo implements ImageInfo {

private Drawable drawable;
private int resId;
private Context context;

@Override
public DrawableInfo prepare() {
drawable = context.getDrawable(resId);
return this;
}

@Override
public void covert(ImageView imageView) {
imageView.setImageDrawable(drawable);
}

public static DrawableInfo of(int resId , Context context){
DrawableInfo info = new DrawableInfo();
info.resId = resId;
info.context = context;
return info;
}
}

of 是构造方法,参数为 Drawableid ,其他的应该也很好理解。

图片加载器环

循环 Banner ,我们需要一个对象来装所有的图片加载器,并且这些图片加载器要形成一个环,其实也不难,主要是指针的处理,直接上示意图和代码把 (示意图以四个图片加载器为例)。

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
 /**
* Created by HeYanLe on 2020/5/7 0007.
* https://github.com/heyanLE
*/
public class ImageRing {

private List<ImageInfo> images = new ArrayList<>();
private int curImageIndex = 0;

public int getImagesCount(){
return images.size();
}

public void addImageInfo(ImageInfo info){
images.add(info);
}

public void scrollNext(){
if (images.isEmpty()){
return ;
}
if (curImageIndex == images.size()-1){
curImageIndex = 0;
}else{
curImageIndex++;
}

}

public void scrollPre(){
if (images.isEmpty()){
return ;
}
if (curImageIndex == 0){
curImageIndex = images.size()-1;
}else{
curImageIndex--;
}

}

public ImageInfo getNextImage(){
if (images.isEmpty()){
return null;
}
if (curImageIndex == images.size()-1){
return images.get(0);
}
return images.get(curImageIndex+1);
}

public ImageInfo getPreImage(){
if (images.isEmpty()){
return null;
}
if (curImageIndex == 0){
return images.get(images.size()-1);
}
return images.get(curImageIndex-1);
}

public ImageInfo getCurImage(){
return images.get(curImageIndex);
}

public int getCurImageIndex() {
return curImageIndex;
}

public void setCurImage(int curImage) {
this.curImageIndex = curImage;
}
}

这里开始是核心,其实主要思路就是一个自定义个的 ViewGroup ,里面只有三个 ImageView,没错,只有三个。

嘛,不太会做动图,这里借用一下 小灰灰 的图,

平常时候,我们将中间一张设置为当前显示的图片,第一张和第三张分别是上一张和下一张(这里可以直接使用我上面的图片加载环里获取上一个和获取下一个)。

滑动时候,图片跟着用户手指左右拖动,因为 Banner 一般宽度是整个 View 的宽度,所以不可能会拖动出 View

松手时候,根据现在的位置,先滑动到上一张或者是下一张,然后滑动到之后在一瞬间回到第二个 ImageView 然后渲染新的图片。

上代码

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
 /**
* banner 轮播 View
* 循环
* Created by HeYanLe on 2020/5/7 0007.
* https://github.com/heyanLE
*/
public class BannerView extends ViewGroup {

private boolean isAnimator = false;

private Handler handler;

private ImageRing imageRing;
private ImageView ivF;
private ImageView ivS;
private ImageView ivT;

private DotLinear dotLinear;

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

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

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

@Override
protected void onFinishInflate() {
super.onFinishInflate();

}

public void setImageRing(ImageRing imageRing){
this.imageRing = imageRing;
setVisibility(INVISIBLE);
drawImage();
scrollCur();
setVisibility(VISIBLE);
if (dotLinear != null)
dotLinear.setDotCount(imageRing.getImagesCount());
}

public void bindDotLinear(DotLinear dotLinear) {
this.dotLinear = dotLinear;
if (imageRing != null){
dotLinear.setDotCount(imageRing.getImagesCount());
dotLinear.setCurDot(imageRing.getCurImageIndex());
}
}

public void init(){

handler = new Handler();

ivF = new ImageView(getContext());
ivS = new ImageView(getContext());
ivT = new ImageView(getContext());

addView(ivF);
addView(ivS);
addView(ivT);

drawImage();
}

public void drawImage(){
if (imageRing == null){
return;
}

if (getChildCount() < 3){
init();
}

imageRing.getPreImage().covert(ivF);
imageRing.getCurImage().covert(ivS);
imageRing.getNextImage().covert(ivT);
scrollTo(ivF.getMeasuredWidth() ,0);
if (dotLinear != null)
dotLinear.setCurDot(imageRing.getCurImageIndex());
//scrollCur();
}

public void scrollNext(){

if (isAnimator){
return;
}

ValueAnimator animator = ValueAnimator.ofInt(getScrollX() ,ivF.getMeasuredWidth() + ivS.getMeasuredWidth());
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Integer value = (Integer) animation.getAnimatedValue();
scrollTo(value ,0);
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isAnimator = true;
}

@Override
public void onAnimationEnd(Animator animation) {
imageRing.scrollNext();
drawImage();
isAnimator = false;
}

@Override
public void onAnimationCancel(Animator animation) {

}

@Override
public void onAnimationRepeat(Animator animation) {

}
});
animator.start();

}

public void scrollPre(){
if (isAnimator){
return;
}

ValueAnimator animator = ValueAnimator.ofInt(getScrollX() ,0);
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Integer value = (Integer) animation.getAnimatedValue();
scrollTo(value ,0);
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isAnimator = true;
}

@Override
public void onAnimationEnd(Animator animation) {
imageRing.scrollPre();
drawImage();
isAnimator = false;
}

@Override
public void onAnimationCancel(Animator animation) {

}

@Override
public void onAnimationRepeat(Animator animation) {

}
});
animator.start();
}

public void scrollCur(){
if (isAnimator){
return;
}

ValueAnimator animator = ValueAnimator.ofInt(getScrollX() ,ivF.getMeasuredWidth());
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Integer value = (Integer) animation.getAnimatedValue();
scrollTo(value ,0);
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isAnimator = true;
}

@Override
public void onAnimationEnd(Animator animation) {
drawImage();
isAnimator = false;
}

@Override
public void onAnimationCancel(Animator animation) {

}

@Override
public void onAnimationRepeat(Animator animation) {

}
});
animator.start();
}

final Runnable runnable = new Runnable() {
@Override
public void run() {
scrollNext();
handler.postDelayed(runnable ,4000);
}
};

public void startPoll(){
handler.post(runnable);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int cWidth = 0;
int cHeight = 0;

cWidth = getMeasuredWidth();
cHeight = getMeasuredHeight();

int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).layout(-cWidth, 0, 0, 0);
}

if (imageRing != null) {
if (ivF != null) {
int cl1 = 0, ct1 = 0, cr1 = 0, cb1 = 0;
cr1 = cWidth;
cb1 = cHeight;
ivF.layout(cl1, ct1, cr1, cb1);
}
if(ivS != null){
ivS.layout(cWidth, 0, 2*cWidth, cHeight);
}
if(ivT != null){
ivT.layout(2*cWidth, 0, 3*cWidth, cHeight);
}
}
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
* 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
*/

int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

// 计算出所有的childView的宽和高
measureChildren(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(sizeHeight, MeasureSpec.AT_MOST));

/*
* 直接设置为父容器计算的值
*/
setMeasuredDimension(sizeWidth, sizeHeight);
}


private float downX;
private float offset;
private boolean isMove = false;

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {

int x = (int) event.getX();

if (isAnimator) {
return true;
}

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
isMove = true;
offset = downX - event.getX();
scrollTo((int) (ivF.getMeasuredWidth() + offset),0);
break;

case MotionEvent.ACTION_UP:
if (isMove){
if (offset >= 400){
scrollNext();
}else if(offset <= -400){
scrollPre();
}else{
scrollCur();
}
}
isMove = false;

break;

default:
break;
}

return true;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 将事件拦截掉
return true;
}

}

主要就是在 onLayout() 方法中计算出三个子 ImageView 的位置,然后上一页下一页的动画效果,还有滑动事件监听。

原点指示器

原点指示器就更加简单了,直接自定义一个,上面 BannerViewbindDotLinear 方法就是用于绑定原点指示器的。

原点指示器直接继承一个 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
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
public class DotLinear extends View {

private Paint backPaint;
private Paint dotPaint;
private Paint curDotPaint;

private int dotCount = 3;
private int dotSize = 40;
private int padding = 20;

private int curDot = 0;

public void initPoint(){
backPaint = new Paint();
backPaint.setColor(Color.BLACK);
backPaint.setAlpha((int)(255*0.3));
backPaint.setStyle(Paint.Style.FILL);

dotPaint = new Paint();
dotPaint.setColor(Color.WHITE);
dotPaint.setAlpha((int)(255*0.3));
dotPaint.setStyle(Paint.Style.FILL);

curDotPaint = new Paint();
curDotPaint.setColor(Color.WHITE);
curDotPaint.setStyle(Paint.Style.FILL);
}

public void setCurDot(int curDot) {
this.curDot = curDot;
invalidate();
}

public void setDotCount(int dotCount) {
this.dotCount = dotCount;
invalidate();
}

public DotLinear(Context context) {
super(context);
initPoint();
}

public DotLinear(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPoint();
}

public DotLinear(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPoint();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(dotSize*dotCount+(dotCount+1)*padding ,dotSize+2*padding);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

canvas.drawRoundRect(0,0,getMeasuredWidth() ,getMeasuredHeight() ,getMeasuredHeight()/3f,getMeasuredHeight()/3f ,backPaint);

float x = padding+dotSize/2f;
for(int i = 0 ; i < dotCount ; i ++){

if (i == curDot){
canvas.drawCircle(x ,getMeasuredHeight()/2f ,dotSize/2f ,curDotPaint);
}else{
canvas.drawCircle(x ,getMeasuredHeight()/2f ,dotSize/2f ,dotPaint);
}
x += dotSize + padding;

}

}
}

滑动冲突

我的父布局用的是 NestedScrollView 监听上下滑动,而我们的 Banner 监听左右滑动,直接写会出现滑动冲突。
这里直接自定义个一个 NestedScrollView 来解决冲突问题。

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
/**
* Created by HeYanLe on 2020/5/8 0008.
* https://github.com/heyanLE
*/
public class BannerScrollView extends NestedScrollView {

private boolean isScroll = true;
private float xDistance, yDistance, xLast, yLast;

public BannerScrollView(@NonNull Context context) {
super(context);
}

public BannerScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

public BannerScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isScroll = false;
xDistance = yDistance = 0f;
xLast = ev.getX();
yLast = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (isScroll){
return false;
}
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - xLast);
yDistance += Math.abs(curY - yLast);
xLast = curX;
yLast = curY;
if (xDistance > yDistance) {
isScroll = true;
return false;
}
return true;
case MotionEvent.ACTION_UP:
isScroll = false;

}
return super.onInterceptTouchEvent(ev);
}
}

使用

一切准备就绪后,开箱即用。

1
2
3
4
5
6
7
8
9
ImageRing ring = new ImageRing();
ring.addImageInfo(DrawableInfo.of(R.drawable.f ,this).prepare());
ring.addImageInfo(DrawableInfo.of(R.drawable.s ,this).prepare());
ring.addImageInfo(DrawableInfo.of(R.drawable.t ,this).prepare());
bannerView.setImageRing(ring);


bannerView.bindDotLinear(dotLinear);
bannerView.startPoll();

省去了部分代码。