循环广告栏现在基本成了网络应用app类的标配,不管是网易云音乐还是淘宝这类客户端都添加了循环广告栏用来显示最新的活动与新更新的内容。下面就通过继承一个ViewGroup来实现一个简单的这样的控件。主要的功能点如下:
- 可以手动拖动实现弹性滑动
- 可以自动切换到下一页
- 滑动到最后一屏的时候跳转到第一屏幕
这个控件是比较简单的控件,没有实现从最后一页平滑过渡到第一页的效果,这个效果我会在下一个循环控件中去实现。下面就来看看代码的实现。
public RecyclerBannerView(Context context) {
this(context, null);
}
public RecyclerBannerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
initPaint();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerBannerView);
//获取自动播放动画的时间
mRecyclerDuration = a.getInteger(R.styleable.RecyclerBannerView_recycler_duration, this
.getResources().getInteger(R.integer.default_recycler_duration));
//页面indicator圆圈的半径
mRadius = a.getDimensionPixelSize(R.styleable.RecyclerBannerView_recycler_radius, this
.getResources().getDimensionPixelSize(R.dimen.default_radius));
//两个indicator之间的距离
mCircleSpacing = a.getDimensionPixelSize(R.styleable.RecyclerBannerView_recycler_spacing,
this.getResources().getDimensionPixelSize(R.dimen.default_spacing));
a.recycle();
}
private void init(Context context) {
mContext = context;
mScroller = new Scroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//生成一个Looper线程
mRecyclerThread = new HandlerThread("recyclerbanner");
mRecyclerThread.start();
mRecyclerHander = new Handler(mRecyclerThread.getLooper()) {
@Override
public void dispatchMessage(Message msg) {
if (msg.what == MSG_NEXT) {
onScrollStart();
snapToPage(getNextPage(), true);
}
}
};
}
private void initPaint() {
mPaint1 = new Paint();
mPaint1.setColor(this.getResources().getColor(R.color.colorWhite));
mPaint1.setStyle(Paint.Style.STROKE);
mPaint1.setAntiAlias(true);
mPaint2 = new Paint();
mPaint2.setColor(this.getResources().getColor(R.color.colorGreen));
mPaint2.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint2.setAntiAlias(true);
}
在这段构造方法中,我们做了一些初始化的操作,在init()方法中初始化了一个Looper线程,通过这个Looper线程,我们就可以定时的发送消息让控件去滑动页面。在initPaint()中初始化了两个画笔:一个用来画页面的指示圆圈,一个用来画当前的页面位置。还有一部分就是通过TypedArray获取我们设置的三个属性:动画播放的时间,指示圆点的半径,指示圆点的距离。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = this.getChildCount();
for (int i = 0; i < count; i++) {
View currentView = this.getChildAt(i);
measureChild(currentView, widthMeasureSpec, heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
final int count = this.getChildCount();
mLeftBound = this.getPaddingLeft();
mRightBound = mLeftBound + this.getMeasuredWidth() * count;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = this.getChildCount();
int left = this.getPaddingLeft();
int top = this.getPaddingTop();
for (int i = 0; i < count; i++) {
View currentView = this.getChildAt(i);
currentView.layout(left, top, left + currentView.getMeasuredWidth(), top + currentView
.getMeasuredHeight());
left += currentView.getMeasuredWidth();
}
}
在onMeasure()方法中,对每个子view进行测量操作,我们希望每个子view的大小和控件的大小一样,所有可以直接传递父控件的measuredWidth和measuredHeight给每个子view,不需要做太多的处理。在onLayout()方法中,对每个view进行布局操作,这里使用的是线性的布局,让每个view都排成一列,类似于LinearLayout的水平排列。onSizeChanged()方法中确定了控件滑动的左边界和右边界,超过这个区域是不让控件可以滑动的。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getActionMasked();
int x = (int) ev.getRawX();
switch (action) {
case MotionEvent.ACTION_DOWN:
int diff = x - mLastTouchX;
mLastTouchX = x;
if (Math.abs(diff) > mTouchSlop) {
disableChildren();
clearMessage();
return true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
enableChildren();
break;
}
return super.dispatchTouchEvent(ev);
}
重写disptachTouchEvent()拦截事件,当我们手指在屏幕上滑动的距离超过最小的认为的滑动距离的时候,我们就拦截这个事件给控件不处理,不向下传递给子view处理了,clearMessage()方法是用来清除Looper线程中的消息的。
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
final int count = this.getChildCount();
if (mVeloctyTracker == null) {
mVeloctyTracker = VelocityTracker.obtain();
}
mVeloctyTracker.addMovement(event);
mVeloctyTracker.computeCurrentVelocity(1000);
int x = (int) event.getRawX();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastTouchX = x;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = mLastTouchX - x;
int xVelocity = (int) mVeloctyTracker.getXVelocity();
if (xVelocity > 0 && this.getScrollX() + deltaX < mLeftBound) {
this.scrollTo(mLeftBound, 0);
} else if (xVelocity < 0 && this.getScrollX() + deltaX + this.getChildAt(count - 1)
.getWidth() >= mRightBound) {
this.scrollTo(mRightBound - this.getMeasuredWidth(), 0);
} else {
this.scrollBy(deltaX, 0);
}
mLastTouchX = x;
return true;
case MotionEvent.ACTION_UP:
int index = (getScrollX() + getWidth() / 2) / getWidth();
int diffX = getWidth() * index - getScrollX();
mScroller.startScroll(getScrollX(), 0, diffX, 0);
invalidate();
if (mVeloctyTracker != null) {
mVeloctyTracker.clear();
mVeloctyTracker.recycle();
mVeloctyTracker = null;
}
break;
}
return true;
}
onTouchEvent()是我们重点处理的地方,当手指在屏幕上面滑动的时候对每个事件进行处理。在ACTION_DOWN的时候,我们记下当前的按下的坐标点;在ACTION_MOVE的时候,记下两次滑动的距离差,如果是向左滑动,滑动的距离加上当前的scrollerX,如果超出了mLeftBound,只滑动到mLeftBound的位置,向右滑动如果超出了mRightBound - getWidth()的长度,我们也只让滑动到这个位置,剩下的在滑动区域内的距离就直接调用scrollBy()方法设置滑动;当ACTION_UP的时候,如果当前的scrollX超出了半个控件宽度的时候,我们使控件滑动到下一页,否则还是返回原来的位置。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
} else {
clearMessage();
onScrollEnd();
mRecyclerHander.sendEmptyMessageDelayed(MSG_NEXT, mRecyclerDuration);
}
}
这个地方设置了两处动作:当手指抬起的时候,我们根据设置的位置弹性的滑动。滑动结束的时候,发送播放动画的延迟消息。
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
drawChildCircle(canvas);
drawCurrentCircle(canvas);
}
private void drawChildCircle(Canvas canvas) {
canvas.save();
final int count = this.getChildCount();
if (count > 0) {
int halfLength = count / 2;
if (count % 2 == 0) { //偶数个子view
mFirstLeftPosition = getWidth() / 2 - mCircleSpacing / 2 - (halfLength - 1) * (2 *
mRadius + mCircleSpacing) - mRadius;
} else {
mFirstLeftPosition = getWidth() / 2 - (2 * mRadius + mCircleSpacing) * halfLength;
}
for (int i = 0; i < count; i++) {
canvas.drawCircle(getScrollX() + mFirstLeftPosition + (2 * mRadius + mCircleSpacing)
* i, getHeight
() - mRadius - 10, mRadius, mPaint1);
}
}
canvas.restore();
}
private void drawCurrentCircle(Canvas canvas) {
canvas.save();
float ratio = (getScrollX() % getWidth()) * 1.0f / getWidth() + getCurrentPage();
int translateX = getScrollX() + mFirstLeftPosition + (int) (ratio * (mCircleSpacing + 2 *
mRadius));
canvas.translate(translateX, getHeight() - mRadius - 10);
canvas.drawCircle(0, 0, mRadius, mPaint2);
canvas.restore();
}
这部分是用来画出页面指示标签的地方,首先是在drawChildCircle()方法中画出所有的圆圈,这个部分需要算出地一个view的起始的圆心坐标,分为奇数个和偶数个子view两种情况,最后算出第一个子view的坐标后,挨一画出所有的圆圈。drawCurrentCircle()方法,则是根据当前的位置和滑动的比例算出当前的指示器的位置,然后画出这个圆圈。
最后实现的效果如下:
这个控件还不能算是一个真正的循环控件,还存在一个最大不足的地方:最后一页不能平滑过渡第一页,第一页向左滑动也不能过渡到第一页。后面循环控件二的时候将解决该问题。