想必原生的progressbar大家都很熟悉,但是最近做项目要实现一个带有分割原点的和小球回弹的progressbar,实现效果如下:
circledemo.gif可以看到是一个很基础的自定义控件,但是确很常用,一般用在用户选择应用的自动关闭时间,字体大小等
那下面就来具体分析下:
首先说下思路:
1.绘制viewgroup也就是后面的灰色的背景点和线。
2.新建view也就是拖动的小球,通过重写view的ontouchevent从而实现小球的移动以及回弹效果。
3.通过重写viewgroup的ontouchevent从而实现点击后面的小圆点,让小球到此位置
4.通过重写viewgroup的ondraw方法,从而画出跟随小球的进度条
1:画线和点
由于继承了framelayout不用考虑layoutparams和onlayout方法
故只要在viewgroup的onmeasure方法里
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureWidth = MeasureSpec.getSize(widthMeasureSpec);
measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST) {
measureHeight = dpToPx(20);
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY);
mOvalRadius = resetRadius == -1 ? measureHeight / 2 : resetRadius;
int eachWidth = (measureWidth - 2 * mOvalRadius) / internalNumber;
if (eachWidth > 0) {
int totalPaintWidth = 0;
mAllintervalPoints.clear();
while (totalPaintWidth <= measureWidth) {
mAllintervalPoints.add(new Point(totalPaintWidth, measureHeight / 2));
totalPaintWidth = totalPaintWidth + eachWidth;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
这里通过在viewgroup的onmeausre前用了for循环算出了间隔点的每个位置,从而加到了AllintervalPoints里,也就是他的子view能获取到这个point的list
然后很明显对wrap_content的情况做了支持。
然后看下ondraw方法:
@Override
protected void onDraw(Canvas canvas) {
float halfHeight = measureHeight / 2;
int eachWidth = (measureWidth - 2 * mOvalRadius) / internalNumber;
canvas.save();
canvas.translate(mOvalRadius, 0);
int totalPaintWidth = 0;
while (totalPaintWidth <= measureWidth) {
//canvas.drawLine(totalPaintWidth, -halfHeight / 2, totalPaintWidth, halfHeight / 2, mBackGroundPaint);
canvas.drawCircle(totalPaintWidth, halfHeight, halfHeight / 2, mBackGroundPaint);
totalPaintWidth = totalPaintWidth + eachWidth;
}
canvas.restore();
canvas.drawLine(mOvalRadius, measureHeight / 2, measureWidth - mOvalRadius, measureHeight / 2, mBackGroundPaint);
在ondraw里画出了线以及各个小圆点。
2:新建并拖动小球
首先在小球的onmeasure中
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getParent() instanceof SeekBarView) {
seekBarView = (SeekBarView) getParent();
} else {
throw new RuntimeException("this view parent must be seekBarView");
}
measureHeight = getMeasuredHeight();
try {
mOvalRadius = seekBarView.mOvalRadius;
mAllintervalPoints = seekBarView.mAllintervalPoints;
centerX = (mAllintervalPoints.get(1).x - mAllintervalPoints.get(0).x) / 2;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.setMargins(mAllintervalPoints.get(1).x, 0, 0, 0);
setLayoutParams(params);
Log.i("CircleView", measureHeight + "");
} catch (Exception e) {
e.printStackTrace();
}
setMeasuredDimension(measureHeight, measureHeight);
}
这里对小球的宽度做了重新的measure,因为父view传过来的宽度是父view的,所以重新设置了下,并做了容错。
然后重写了onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
int lastX = 0;
int x = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("CircleView", getRight() + "");
lastX = x;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
if (getLeft() + offsetX >= 0 && getRight() + offsetX <= ((ViewGroup) getParent()).getMeasuredWidth()) {
ViewCompat.offsetLeftAndRight(this, offsetX);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
int finalX = getLeft();
for (int i = 0; i < mAllintervalPoints.size(); i++) {
if (mAllintervalPoints.get(i).x >= finalX) {
try {
if (mAllintervalPoints.get(i - 1).x + centerX >= finalX) {
layout(mAllintervalPoints.get(i - 1).x, getTop(), mAllintervalPoints.get(i - 1).x + 2 * mOvalRadius, getBottom());
dispatchListener(i - 1);
break;
} else {
layout(mAllintervalPoints.get(i).x, getTop(), mAllintervalPoints.get(i).x + 2 * mOvalRadius, getBottom());
dispatchListener(i);
break;
}
} catch (Exception e) {
layout(mAllintervalPoints.get(i).x, getTop(), mAllintervalPoints.get(i).x + 2 * mOvalRadius, getBottom());
dispatchListener(i);
break;
}
}
}
break;
}
return true;
}
在小球的ontouchevent里利用 ViewCompat.offsetLeftAndRight这个滑动是改变view的layout的不同于scroller以及属性动画从而能改变getleft()的值。
3.点击小圆点,移动小球
@Override
public boolean onTouchEvent(MotionEvent event) {
int lastX = 0;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if ((lastX - x) < Math.abs(defaultMinInternal)) {
Point finalPoint = isControlPoint(x, y);
if (finalPoint != null) {
if (getChildAt(0) instanceof CircleView) {
CircleView circleView = (CircleView) getChildAt(0);
circleView.simulateScroll(finalPoint);
} else {
throw new RuntimeException("the seekBarView must be one child");
}
}
}
break;
}
return true;
}
这里首先判定是click,然后判断点击区域是否在几个小点的范围内
/**
* 判断点击区域
*/
private Point isControlPoint(float x, float y) {
for (Point controlPoint : mAllintervalPoints) {
RectF pointRange = new RectF(controlPoint.x - defaultRectNumber,
controlPoint.y - defaultRectNumber,
controlPoint.x + defaultRectNumber,
controlPoint.y + defaultRectNumber);
// 如果包含了就,返回true
if (pointRange.contains(x, y)) {
return controlPoint;
}
}
return null;
}
新建一个rect判定范围,然后传给小球具体的位置从而让小球layout发证改变。
4.画进度条
同样是通过viewgroup的ondraw方法
@Override
protected void onDraw(Canvas canvas) {
float halfHeight = measureHeight / 2;
int eachWidth = (measureWidth - 2 * mOvalRadius) / internalNumber;
canvas.save();
canvas.translate(mOvalRadius, 0);
int progressPaintWidth = 0;
for (int i = 0; i < mAllintervalPoints.size(); i++) {
if (progressPaintWidth != -1 && mAllintervalPoints.get(i).x >= progressMeasureLine) {
try {
progressMeasureWidth = mAllintervalPoints.get(i - 1).x;
break;
} catch (Exception e) {
progressMeasureWidth = mAllintervalPoints.get(i).x;
}
}
}
while (progressPaintWidth <= progressMeasureWidth) {
//canvas.drawLine(totalPaintWidth, -halfHeight / 2, totalPaintWidth, halfHeight / 2, mBackGroundPaint);
canvas.drawCircle(progressPaintWidth, halfHeight, halfHeight / 2, mProgressPaint);
progressPaintWidth = progressPaintWidth + eachWidth;
}
canvas.restore();
canvas.drawLine(mOvalRadius, measureHeight / 2, progressMeasureLine + mOvalRadius, measureHeight / 2, mProgressPaint);
}
这里算出了小球getleft()的距离从而更新了进度条。
最后还提供了setIntervalNumber和setOvalRadius方法,当然以后可能还会做更大的扩展。
github地址:https://github.com/jhonsonkilly/scrollcircledemo
欢迎star!!!
网友评论