前段时间群里兄弟项目中有类似这样的需求
我看到兄弟受苦受难,于心不忍。又因事不关己,打算高高挂起。正在爱恨纠结之时,日神对我说:没事多造点轮子,你的人生会有很多收获。这波鸡汤让我深受触动,于是决定拯救兄弟于水生火热之中。
重写onMeasure 决策自身大小
显而易见当可以拖拽的范围极限为零时,也就是RangeSeeBar正常显示能够接受的极限,粗略一看:Width > 2 * Height
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightSize * 2 > widthSize) {
setMeasuredDimension(widthSize, widthSize / 2);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
public class RangeSeekBar extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int lineTop, lineBottom, lineLeft, lineRight;
private int lineCorners;
private int lineWidth;
private RectF line = new RectF();
public RangeSeekBar(Context context) {
this(context, null);
}
public RangeSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightSize * 2 > widthSize) {
setMeasuredDimension(widthSize, (int) (widthSize / 2));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int seekBarRadius = h / 2;
/**
* 属性 left right top bottom 描述了SeekBar按钮的位置
* 蓝后根据它们预先设置确定出 RectF line 背景的三维
* lineCorners 圆滑的边缘似乎会比直角更好看
*/
lineLeft = seekBarRadius;
lineRight = w - seekBarRadius;
lineTop = seekBarRadius - seekBarRadius / 4;
lineBottom = seekBarRadius + seekBarRadius / 4;
lineWidth = lineRight - lineLeft;
line.set(lineLeft, lineTop, lineRight, lineBottom);
lineCorners = (int) ((lineBottom - lineTop) * 0.45f);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.FILL);
paint.setColor(0xFFD7D7D7);
canvas.drawRoundRect(line, lineCorners, lineCorners, paint);
}
}
拖动舞台已经备好,SeekBar按钮半径也已定好。顺水推舟,下一步就绘制SeekBar把。
SeekBar按钮 拥有对象是极好的
粗略一想:按钮有颜色、有大小、有变色、被绘制,碰撞检测、边界检测、被拖拽等,最关键的是有多个。因此SeekBar按钮可以说是一个复杂的集合体,是时候来发对象了。
private class SeekBar {
int widthSize;
int left, right, top, bottom;
Bitmap bmp;
/**
* 当RangeSeekBar尺寸发生变化时,SeekBar按钮尺寸随之变化
*
* @param centerX SeekBar按钮的X中心在RangeSeekBar中的相对位置
* @param centerY SeekBar按钮的Y中心在RangeSeekBar中的相对位置
* @param heightSize RangeSeekBar期望SeekBar所拥有的高度
*/
void onSizeChanged(int centerX, int centerY, int heightSize) {
/**
* 属性 left right top bottom 描述了SeekBar按钮的位置
* widthSize = heightSize * 0.8f 可见按钮实际区域是个矩形而非正方形
* 圆圈按钮为什么要占有矩形区域?因为按钮阴影效果。不要阴影不行吗?我就不
* 那么 onMeasure 那边说好的2倍宽度?我就不
*/
widthSize = (int) (heightSize * 0.8f);
left = centerX - widthSize / 2;
right = centerX + widthSize / 2;
top = centerY - heightSize / 2;
bottom = centerY + heightSize / 2;
bmp = Bitmap.createBitmap(widthSize, heightSize, Bitmap.Config.ARGB_8888);
int bmpCenterX = bmp.getWidth() / 2;
int bmpCenterY = bmp.getHeight() / 2;
int bmpRadius = (int) (widthSize * 0.5f);
Canvas defaultCanvas = new Canvas(bmp);
Paint defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 绘制Shadow
defaultPaint.setStyle(Paint.Style.FILL);
int barShadowRadius = (int) (bmpRadius * 0.95f);
defaultCanvas.save();
defaultCanvas.translate(0, bmpRadius * 0.25f);
RadialGradient shadowGradient = new RadialGradient(bmpCenterX, bmpCenterY, barShadowRadius, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);
defaultPaint.setShader(shadowGradient);
defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, barShadowRadius, defaultPaint);
defaultPaint.setShader(null);
defaultCanvas.restore();
// 绘制Body
defaultPaint.setStyle(Paint.Style.FILL);
defaultPaint.setColor(0xFFFFFFFF);
defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint);
// 绘制Border
defaultPaint.setStyle(Paint.Style.STROKE);
defaultPaint.setColor(0xFFD7D7D7);
defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint);
}
void draw(Canvas canvas) {
canvas.drawBitmap(bmp, left, top, null);
}
}
public class RangeSeekBar extends View {
private SeekBar seekBar = new SeekBar();
private class SeekBar {
...
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int seekBarRadius = h / 2;
...
// 在RangeSeekBar确定尺寸时确定SeekBar按钮尺寸
seekBar.onSizeChanged(seekBarRadius, seekBarRadius, h);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
// 在RangeSeekBar被绘制时绘制SeekBar按钮
seekBar.draw(canvas);
}
}
onTouchEvent 触摸监听 让SeekBar按钮动起来
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
boolean touchResult = false;
// 进行检测,手指手指是否落在当前SeekBar上。即声明SeekBar时使用left、top、right、bottom属性所描述区域的内部
if (seekbar.collide(event)) {
touchResult = true;
}
return touchResult;
case MotionEvent.ACTION_MOVE:
float percent;
float x = event.getX();
if (x <= lineLeft) {
percent = 0;
} else if (x >= lineRight){
percent = 1;
} else {
percent = (x - lineLeft) * 1f / (lineWidth);
}
// SeekBar按钮根据当前手指在拖动条上的滑动而滑动
seekbar.slide(percent);
invalidate();
break;
}
return super.onTouchEvent(event);
}
private class SeekBar {
int lineWidth; // 拖动条宽度 可在onSizeChanged时刻获得
float currPercent;
int left, right, top, bottom;
boolean collide(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int offset = (int) (lineWidth * currPercent);
return x > left + offset && x < right + offset && y > top && y < bottom;
}
void slide(float percent) {
if (percent < 0) percent = 0;
else if (percent > 1) percent = 1;
currPercent = percent;
}
void draw(Canvas canvas) {
int offset = (int) (lineWidth * currPercent);
canvas.save();
canvas.translate(offset, 0);
canvas.drawBitmap(bmp, left, top, null);
canvas.restore();
}
}
更好的视觉体验
到目前位置,SeekBar被按压时显得死气沉沉,接下来为其添加强烈的视觉反馈。
那么之前通过onSizeChanged预设按钮的偷懒手段就GG了,因为SeekBar的UI效果需要随触摸状态的变化而变化。
首先在onTouchEvent中拿到这个变化
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
seekBar.material = seekBar.material >= 1 ? 1 : seekBar.material + 0.1f;
...
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
seekBar.materialRestore();
break;
}
return super.onTouchEvent(event);
}
private class SeekBar {
float material = 0;
ValueAnimator anim;
final TypeEvaluator te = new TypeEvaluator() {
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int alpha = (int) (Color.alpha(startValue) + fraction * (Color.alpha(endValue) - Color.alpha(startValue)));
int red = (int) (Color.red(startValue) + fraction * (Color.red(endValue) - Color.red(startValue)));
int green = (int) (Color.green(startValue) + fraction * (Color.green(endValue) - Color.green(startValue)));
int blue = (int) (Color.blue(startValue) + fraction * (Color.blue(endValue) - Color.blue(startValue)));
return Color.argb(alpha, red, green, blue);
}
};
void draw(Canvas canvas) {
int offset = (int) (lineWidth * currPercent);
canvas.save();
canvas.translate(left, 0);
canvas.translate(offset, 0);
drawDefault(canvas);
canvas.restore();
}
private void drawDefault(Canvas canvas) {
int centerX = widthSize / 2;
int centerY = heightSize / 2;
int radius = (int) (widthSize * 0.5f);
// draw shadow
defaultPaint.setStyle(Paint.Style.FILL);
canvas.save();
canvas.translate(0, radius * 0.25f);
canvas.scale(1 + (0.1f * material), 1 + (0.1f * material), centerX, centerY);
defaultPaint.setShader(shadowGradient);
canvas.drawCircle(centerX, centerY, radius, defaultPaint);
defaultPaint.setShader(null);
canvas.restore();
// draw body
defaultPaint.setStyle(Paint.Style.FILL);
defaultPaint.setColor(te.evaluate(material, 0xFFFFFFFF, 0xFFE7E7E7));
canvas.drawCircle(centerX, centerY, radius, defaultPaint);
// draw border
defaultPaint.setStyle(Paint.Style.STROKE);
defaultPaint.setColor(0xFFD7D7D7);
canvas.drawCircle(centerX, centerY, radius, defaultPaint);
}
private void materialRestore() {
if (anim != null) anim.cancel();
anim = ValueAnimator.ofFloat(material, 0);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
material = (float) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
material = 0;
invalidate();
}
});
anim.start();
}
}
Range
Range的意思就是范围,但是就算知道这些似乎并没有什么卵用 _(:3 」∠)_
so为了了解其中规律,本宝宝使劲摸索。最终发现
如果分开来看它们都拥有自己的固定滑动区间,右边的SeekBar按钮就是左边SeekBar按钮向右平移了个SeekBar按钮宽度而已。
public class RangeSeekBar extends View {
private SeekBar leftSB = new SeekBar();
private SeekBar rightSB = new SeekBar();
/**
* 用来记录当前用户触摸的到底是哪个SB
*/
private SeekBar currTouch;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
...
// rightSB就如同分析的一样,紧紧贴在leftSB的右边而已
rightSB.left += leftSB.widthSize;
rightSB.right += leftSB.widthSize;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
leftSB.draw(canvas);
rightSB.draw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
boolean touchResult = false;
/**
* 为什么不先检测leftSB而先检测rightSB?为什么? (●'◡'●)
*/
if (rightSB.collide(event)) {
currTouch = rightSB;
touchResult = true;
} else if (leftSB.collide(event)) {
currTouch = leftSB;
touchResult = true;
}
return touchResult;
case MotionEvent.ACTION_MOVE:
float percent;
float x = event.getX();
if (currTouch == leftSB) {
if (x < lineLeft) {
percent = 0;
} else {
percent = (x - lineLeft) * 1f / (lineWidth - rightSB.widthSize);
}
if (percent > rightSB.currPercent) {
percent = rightSB.currPercent;
}
leftSB.slide(percent);
} else if (currTouch == rightSB) {
if (x > lineRight) {
percent = 1;
} else {
percent = (x - lineLeft - leftSB.widthSize) * 1f / (lineWidth - leftSB.widthSize);
}
if (percent < leftSB.currPercent) {
percent = leftSB.currPercent;
}
rightSB.slide(percent);
}
invalidate();
break;
}
return super.onTouchEvent(event);
}
}
比如现在2个按钮直接就保留了一个距离,当然也可以保留n个
支持刻度模式
当然支持刻度的同时也支持预留范围
支持自定义UI按钮样式背景颜色
似乎少了按压状态变化
如何使用?
[戳我转到RangeSeekBar使用教程]
如果您喜欢这篇文章,您也可以进行打赏, 金额任意 感谢您对作者的支持
版权声明:欢迎转载,但请尊重作者劳动成果,转载请注明出处-->http://blog.csdn.net/bfbx5173 QQ群:274306954