最近在复习自定义view的知识,结合做了一段时间的ios体会,总结出:
banner的开源库已经有很多,但在实现UI或产品的需求时总是找不到合适的,不是功能实现不了,而是能改造成产品需要的交互效果有时真是很蛋疼。所以就本着全是自己来实现的原则,参考一些资料来撸一个自定义banner,整下来也就300多行代码。
原理大概是这样:
1、自定义一个viewgroup,因为它可以用来添加其它view。
这里我只会用到3个itemview,左、中、右,根据滑动后对数据重新设置可以实现多个数据的循环切换。
这里图片的数据写死了,哎,需要动态设置的话可以自行处理一下,添加一个public方法,从itemViews可以拿到你需要设置的view。
还有,图片的显示这里直接显示在imageview上了,如果有大图的话建议还是用一些库来显示,例如glide。
public class BannerView extends ViewGroup {
private Context context;
private List<View> itemViews;//缓存左中右3个itemview
private List<Integer> data;//资源数据,这里只用一张图片id
private int itemWidth;//itemview的宽
private int itemHeight;//itemview的高
private boolean firstLayout = true;//防止layout多次处理
public BannerView(Context context) {
this(context, null);
}
public BannerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(final Context context) {
this.context = context;
addChildView();
}
private void addChildView() {
data = new ArrayList<>();
data.add(R.drawable.a);
data.add(R.drawable.b);
data.add(R.drawable.c);
data.add(R.drawable.d);
data.add(R.drawable.e);
itemViews = new ArrayList<>();
for (int i=0; i<3; i++) {
View view = LayoutInflater.from(this.context).inflate(R.layout.banner_item, null);
TextView tv = view.findViewById(R.id.tv);
tv.setText(""+(i+1));
ImageView iv = view.findViewById(R.id.iv);
iv.setImageResource(data.get(i));
addView(view);
itemViews.add(view);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
if (firstLayout) {
firstLayout = false;
itemWidth = i2-i;//好吧,这里没有处理margin和padding,如果有可以加在这里
itemHeight = i3-i1;//同上
//从左到右依次放置这3个itemview
for (int j=0; j<getChildCount(); j++) {
View view = getChildAt(j);
LayoutParams lp = view.getLayoutParams();
lp.width = itemWidth;
lp.height = itemHeight;
view.setLayoutParams(lp);
view.layout(i+(j*itemWidth), i1, i2+(j*itemWidth), i3);
}
//将第一个(position=0)资源设置到二个itemview,并滑动第二个itemview到屏幕
position = 0;
setCurrentItemRes(position);
scrollToBegin(false, 0);
}
}
private float downX, downY;
private boolean scrollEnd;
private int position;
private boolean moving;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float offsetX = event.getX() - downX;
setScrollX((int) (itemWidth-offsetX));
break;
case MotionEvent.ACTION_UP:
offsetX = event.getX() - downX;
if (Math.abs(offsetX) > itemWidth/3) {
needScrollToNext((int) offsetX);
}else {
scrollToBegin(true, (int) offsetX);
}
break;
}
return true;
}
/**
* 滚动到上一个或下一个
*/
private void needScrollToNext(int offsetX) {
final int end = offsetX>0? -0 : -2*itemWidth;
ValueAnimator animator = ValueAnimator.ofInt((int) offsetX-itemWidth, end);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (int) valueAnimator.getAnimatedValue();
setScrollX(-value);
if (value == end) {
if (end == 0) {
position--;
if (position < 0) {
position = data.size()-1;
}
}else {
position++;
if (position >= data.size()) {
position = 0;
}
}
if (!scrollEnd) {
scrollEnd = true;
scrollEndPro();
}
}
}
});
animator.start();
scrollEnd = false;
}
private void scrollToBegin(boolean hasAnim, int offsetX) {
if (hasAnim) {
ValueAnimator animator = ValueAnimator.ofInt((int) offsetX-itemWidth, -itemWidth);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (int) valueAnimator.getAnimatedValue();
setScrollX(-value);
if (value == -itemWidth) {
if (!scrollEnd) {
scrollEnd = true;
}
}
}
});
animator.start();
scrollEnd = false;
}else {
setScrollX(itemWidth);
}
}
/**
* 滑动结束的处理
*/
private void scrollEndPro() {
setCurrentItemRes(position);
scrollToBegin(false, 0);//设置左、中、右的资源完成后,始终显示中间的itemView
}
/**
* 设置左、中、右的资源
* @param currentPosition:当前显示的pos
*/
private void setCurrentItemRes(int currentPosition) {
if (currentPosition > 0 && currentPosition < data.size()-1) {
setLeftView(currentPosition-1);
setRightView(currentPosition+1);
}else if (currentPosition == 0) {
setLeftView(data.size()-1);
setRightView(currentPosition+1);
}else if (currentPosition == data.size()-1) {
setLeftView(currentPosition-1);
setRightView(0);
}
setMidView(currentPosition);
}
/**
* 设置左侧具体的资源
* @param pos
*/
private void setLeftView(int pos) {
ImageView leftIv = itemViews.get(0).findViewById(R.id.iv);
TextView leftTv = itemViews.get(0).findViewById(R.id.tv);
leftIv.setImageResource(data.get(pos));
leftTv.setText(""+(pos+1));
}
private void setMidView(int pos) {
ImageView midIv = itemViews.get(1).findViewById(R.id.iv);
TextView midTv = itemViews.get(1).findViewById(R.id.tv);
midIv.setImageResource(data.get(pos));
midTv.setText(""+(pos+1));
}
private void setRightView(int pos) {
ImageView rightIv = itemViews.get(2).findViewById(R.id.iv);
TextView rightTv = itemViews.get(2).findViewById(R.id.tv);
rightIv.setImageResource(data.get(pos));
rightTv.setText(""+(pos+1));
}
}
上面添加的itemview我使用了引入xml布局的方式,这样控件就可以方便添加和修改了,你要在一页添加什么控件尽管添加。
这是activity的xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.mybannerdemo.BannerView
android:id="@+id/bannerView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#fff"/>
</FrameLayout>
这是itemview的布局banner_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp">
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/d"
android:scaleType="centerCrop"
android:background="#ccc"/>
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|bottom"
android:text="1"
android:textSize="30dp"
android:textColor="#e3e"/>
</RelativeLayout>
这样就可以左右无限循环滑动切换了。
2、还没有结束,作为banner,还要实现以下的功能:
2.1、自动轮播,触摸停止轮播,松开继续自动轮播;
需要在onTouchEvent对触摸的处理:action_down、action_move时停止自动滚动,action_up时从新开始。
2.2、指示器跟着切换;
我是直接使用了canvas来画指示器的,为什么?还不是为了应对UI出的效果图,这样要什么形状我们都可以自己画。
为什么重写dispatchDraw?因为这样才能显示在viewgroup最上面,不信可以试试在onDraw画。
在画的时候对坐标x处理,因为canvas画的都是属于内容,而上面的滑动我用了setScrollX()实现,这个只是滑动view/viewgroup的内容。所以当切换图片时,这些指示器也会跟着滑动,所以对滑动的偏移进行计算,保证滑动时指示器看上去是静止的。
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
drawIndicator(canvas);
}
private void drawIndicator(Canvas canvas) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
for (int i=0; i<data.size(); i++) {
int x = mOffsetX + 100 + i*30;
int y = itemHeight-50;
if (i == position) {
paint.setColor(Color.RED);
}else {
paint.setColor(Color.GRAY);
}
canvas.drawCircle(x, y, 10, paint);
}
}
2.3、点击事件处理;
java不像kotlin可以传递函数作为参数,所以唯有用接口提供点击。
这里有点不好的就是,由于onTouchEvent返回true,子view的点击不会响应。所以这里使用action_up时(抬起手指)作为点击。如果实在是要知道点击了那个控件,可以根据点击的区域来遍历子view可以找得到。
private ClickListener clickListener;
public void addClickListener(ClickListener clickListener) {
this.clickListener = clickListener;
}
public interface ClickListener {
void onClick(View view, int position);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
。。。
case MotionEvent.ACTION_UP:
。。。
if (!moving) {
clickListener.onClick(null, position);
}
break;
}
return true;
}
最后,其实代码基本都贴上了,还有不清楚的可以看这里。
来源:oschina
链接:https://my.oschina.net/u/4365362/blog/4863392