android 自定义 seekbar,Android View 自定义 RangeSeekBar 范围选择器

前段时间群里兄弟项目中有类似这样的需求

AAffA0nNPuCLAAAAAElFTkSuQmCC

我看到兄弟受苦受难,于心不忍。又因事不关己,打算高高挂起。正在爱恨纠结之时,日神对我说:没事多造点轮子,你的人生会有很多收获。这波鸡汤让我深受触动,于是决定拯救兄弟于水生火热之中。

AAffA0nNPuCLAAAAAElFTkSuQmCC

重写onMeasure 决策自身大小

AAffA0nNPuCLAAAAAElFTkSuQmCC

显而易见当可以拖拽的范围极限为零时,也就是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);

}

}

很明显这里设计seekBarRadius作为SeekBar按钮的半径,值为RangeSeekBar自身高度一半。那么为了使默认状态的SeekBar按钮圆心能压在背景条的起点和终点

背景条的起点和终点当然就分别相对于自身宽度往内部偏移一个半径咯。

AAffA0nNPuCLAAAAAElFTkSuQmCC

拖动舞台已经备好,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);

}

}

距离成功又进了一步

AAffA0nNPuCLAAAAAElFTkSuQmCC

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  top && y 

}

void slide(float percent) {

if (percent 

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();

}

}

AAffA0nNPuCLAAAAAElFTkSuQmCC

更好的视觉体验

到目前位置,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);

}

之后在SeekBar按钮中响应这个变化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();

}

}

draw方法中的直接绘制bmp的逻辑被替换为drawDefault

那么drawDefault的内部逻辑基本和预制bmp一样,唯二的区别在于对阴影Shadow做了个scale处理,对按钮Body颜色做了个渐变处理

materialRestore即当用户手指抬起后开个线程将状态渐变为初始态

AAffA0nNPuCLAAAAAElFTkSuQmCC

Range

Range的意思就是范围,但是就算知道这些似乎并没有什么卵用 _(:3 」∠)_

so为了了解其中规律,本宝宝使劲摸索。最终发现

AAffA0nNPuCLAAAAAElFTkSuQmCC

如果分开来看它们都拥有自己的固定滑动区间,右边的SeekBar按钮就是左边SeekBar按钮向右平移了个SeekBar按钮宽度而已。

AAffA0nNPuCLAAAAAElFTkSuQmCCpublic 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 

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 

percent = leftSB.currPercent;

}

rightSB.slide(percent);

}

invalidate();

break;

}

return super.onTouchEvent(event);

}

}

通过触摸改变一些属性的值,通过这些属性的值绘制出对应的UI效果,套路一切都是套路

那么继SwitchButton后,又算是重新温习了一次该套路

那么本宝宝的RangeSeekBar还能做到什么?

支持负数

支持预留(保留)范围

什么是预留(保留)范围?比如那个,你懂得。只可意会,不可言传。(≖ ‿ ≖)

AAffA0nNPuCLAAAAAElFTkSuQmCC

比如现在2个按钮直接就保留了一个距离,当然也可以保留n个

支持刻度模式

AAffA0nNPuCLAAAAAElFTkSuQmCC

当然支持刻度的同时也支持预留范围

支持自定义UI按钮样式背景颜色

AAffA0nNPuCLAAAAAElFTkSuQmCC

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android中,可以通过自定义SeekBar实现数字滑动功能。首先,在布局文件中定义定义SeekBar的样式,可以使用ProgressBar来实现。如下所示: ``` <ProgressBar android:id="@+id/customSeekBar" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="50" android:progressDrawable="@drawable/custom_seekbar_progress" android:thumb="@drawable/custom_seekbar_thumb" /> ``` 在drawable文件夹下创建custom_seekbar_progress.xml和custom_seekbar_thumb.xml来定义SeekBar的背景和滑块样式。在custom_seekbar_progress.xml中,可以使用shape和gradient标签来定义进度条的背景样式。在custom_seekbar_thumb.xml中,可以使用shape标签来定义滑块的样式。 接下来,在Activity或Fragment中找到SeekBar的实例,并设置OnSeekBarChangeListener监听。在监听中,通过getProgress方法获取SeekBar的进度值,并根据需要进行相应的处理。例如,可以在TextView中显示SeekBar的进度值,如下所示: ``` SeekBar customSeekBar = findViewById(R.id.customSeekBar); final TextView progressTextView = findViewById(R.id.progressTextView); customSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { progressTextView.setText(String.valueOf(progress)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { // 当开始滑动SeekBar时执行的操作 } @Override public void onStopTrackingTouch(SeekBar seekBar) { // 当结束滑动SeekBar时执行的操作 } }); ``` 通过设置OnSeekBarChangeListener监听,可以在SeekBar滑动时实时更新进度值,并进行相应的处理操作。根据自己的需求,可以在onProgressChanged、onStartTrackingTouch和onStopTrackingTouch方法中添加自定义的逻辑。 以上就是使用自定义SeekBar实现数字滑动的简单方法。可以根据自己的需求进行进一步的定制和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值