最近在複習自定義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;
initData();
addChildView();
}
public void initData() {
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);
}
private void addChildView() {
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來畫訓示器的,為什麼?這樣要什麼形狀的訓示器我們都可以自己畫。
重寫dispatchDraw并在此畫訓示器,為什麼重寫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;
}
最後,其實代碼基本都貼上了,還有不清楚的可以看這裡。