标签:
前段时间群里兄弟项目中有类似这样的需求
我看到兄弟受苦受难,于心不忍。又因事不关己,打算高高挂起。正在爱恨纠结之时,日神对我说:没事多造点轮子,你的人生会有很多收获。这波鸡汤让我深受触动,于是决定拯救兄弟于水生火热之中。
重写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按钮的位置<br> * 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<Integer> te = new TypeEvaluator<Integer>() { @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
Android View 自定义RangeSeekBar范围选择器 走在View进阶之路
标签:
原文地址:http://blog.csdn.net/bfbx5173/article/details/51869776