Android自定义View之滑杆内部带数字的SeekBar

一、需求确认 首先我们要明确需求,要做一个什么样的Seekbar,分析清楚业务需求,再开始做。如图,产品大佬给的图是这样子的:
Android自定义View之滑杆内部带数字的SeekBar
文章图片

当然,作为一个工程师,第一步当然是去问问度娘,看有没有好的轮子,然后去github上淘淘金。我始终认为这是一个优秀工程师该有的解决问题的方法,哈哈~ 找过一圈之后,发现并没有适合的轮子可以用,这时心里开始咒骂产品了,“提的什么鬼需求,那么非主流”。但是骂完还得撸起袖子干啊。
二、该怎么做 首先我们分析,这个控件该怎么去实现。首先,我们知道,要画一条直线来当作进度条;然后呢,我们要画一个圆角矩形,矩形中要有数字。查看了SeekBar的源码,发现他有一个监听器回调滑杆事件,那么咱们也要有。好的,现在确认了需要的4样东西:直线、圆角矩形、数字、监听器。就可开工了。
三、开整 我们首先顶一个半径pointRadius,因为滑杆一般为圆形或者圆角正方形。还有线的高度lineHeight,progress进度值。这里还是贴代码了。

private int pointRadius = 45; //圆脚默认半径 private int pointColor = R.color.dream_2; //圆脚默认颜色private int lineHeight = 10; //线默认高度 private int lineClor = R.color.dream_1; //线默认颜色private int progress = 0; private final int PROGRESS_MIN = 0; private final int PROGRESS_MAX = 100;

当然,要画三个东西,就要准备三支笔。
private Paint linePaint; private Paint pointPaint; private Paint textPaint;

最后还有一个监听器,三个回调方法分别是开始的时候回调,滑动中回调,滑动完回调。当然可以根据自己的逻辑增加回调方法。
private OnProgressChangedListener progressChangedListener; public interface OnProgressChangedListener { void onStartChange(View view); void onProgressChange(View view, int progress); void onProgressChanged(View view, int progress); }

首先,我们画一条线
canvas.drawLine(pointRadius / 2, getHeight() / 2, getWidth() - pointRadius / 2, getHeight() / 2, linePaint);

然后画个圆角矩形(正方形)
RectF r2 = new RectF(); //RectF对象 r2.left = getCx() - pointRadius; //左边 r2.top = getHeight() / 2 - pointRadius; //上边 r2.right = getCx() + pointRadius; //右边 r2.bottom = getHeight() / 2 + pointRadius; //下边 canvas.drawRoundRect(r2, 20, 20, pointPaint);

然后就是画字了(文本)
if (progress > 9 && progress < 100) { canvas.drawText("" + progress, getCx() - 32, getHeight() / 2 + pointRadius / 3, textPaint); } else if (progress > 99) { canvas.drawText("" + progress, getCx() - pointRadius + 2, getHeight() / 2 + pointRadius / 3, textPaint); } else { canvas.drawText("" + progress, getCx() - pointRadius / 3, getHeight() / 2 + pointRadius / 3, textPaint); }

这里说下为啥要分三种情况,因为0-100分为一位数,二位数,三位数。每种情况文本的宽度都不同,所以对应的在x轴上的偏移量也不相同,所以这里分了三种情况去绘制。
【Android自定义View之滑杆内部带数字的SeekBar】读到这里,也许有人要问了,getCx()这家伙是干嘛的。这个是滑杆的X坐标。那么这个坐标怎么获取,因为滑杆滑动,这个值肯定是一个动态的。
private float getCx() { float cx = 0.0f; cx = (getWidth() - pointRadius * 2); if (cx < 0) { throw new IllegalArgumentException("TouchProgressView 宽度不可以小于 2 倍 pointRadius"); } return cx / 100 * progress + pointRadius; }

这个公式是怎么来的呢,看下图:
Android自定义View之滑杆内部带数字的SeekBar
文章图片

pro是当前的进度progress,r为滑杆的一半,我们需要的坐标值,就是图中的那个红点。怎么样,一目了然吧。
图画完了,接下来就是让我们的进度条动起来了。怎么动呢,我们通过改变progress的值,然后调用invalidate()方法,就可以实现进度条的滑动了。
public void setProgress(int progress) { if (progress < 0 || progress > 100) { throw new IllegalArgumentException("progress 不可以小于0 或大于100"); } this.progress = progress; invalidate(); if (progressChangedListener != null) { progressChangedListener.onProgressChange(this, progress); } }

同时,我们还有回调进度监听。什么时候让它动呢,当然使我们手触摸的时候,想都不用想,要重写onTouchEvent方法。
/** * 回调进度完成接口 */ private void callBackListener(int progress) { if (progressChangedListener != null) { progressChangedListener.onProgressChanged(this, progress); } }private void callBackIngListener() { if (progressChangedListener != null) { progressChangedListener.onStartChange(this); } }

我们这里先对进度回调做了个简单的封装。下面我们开始处理onTouchEvent。
@Override public boolean onTouchEvent(MotionEvent event) { Log.i("cqc", "event.getAction=" + event.getAction()); lastX = event.getX(); if (event.getX() < pointRadius) { setProgress(PROGRESS_MIN); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: callBackIngListener(); return true; case MotionEvent.ACTION_MOVE: callBackIngListener(); return true; case MotionEvent.ACTION_UP: callBackListener(PROGRESS_MIN); return true; default: return true; } } else if (event.getX() > getWidth() - pointRadius) { setProgress(PROGRESS_MAX); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: callBackIngListener(); return true; case MotionEvent.ACTION_MOVE: callBackIngListener(); return true; case MotionEvent.ACTION_UP: callBackListener(PROGRESS_MAX); return true; default: return true; } } else { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setProgress(calculProgress(event.getX())); callBackIngListener(); return true; case MotionEvent.ACTION_MOVE: setProgress(calculProgress(event.getX())); callBackIngListener(); return true; case MotionEvent.ACTION_UP: callBackListener(calculProgress(event.getX())); return true; } }return super.onTouchEvent(event); }

至此,我们的自定义View重要的点讲完了。可能有些大神会觉得很简单,但是笔者写这篇博客的初衷是帮助初级工程师们对自定义控件的掌握,所以写的有点多了。下面是效果图和源码。
Android自定义View之滑杆内部带数字的SeekBar
文章图片

import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Typeface; import android.support.annotation.ColorRes; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; /** * Created by chenqc on 2018/11/6. */public class TouchProgressView extends View { private static final String TAG = "TouchProgressView"; private Paint linePaint; private Paint pointPaint; private Paint textPaint; private float lastX = 0; private int pointRadius = 45; //圆角默认半径 private int pointColor = R.color.dream_2; //圆角默认颜色private int lineHeight = 10; //线默认高度 private int lineClor = R.color.dream_1; //线默认颜色private int progress = 0; private final int PROGRESS_MIN = 0; private final int PROGRESS_MAX = 100; private OnProgressChangedListener progressChangedListener; public interface OnProgressChangedListener { void onStartChange(View view); void onProgressChange(View view, int progress); void onProgressChanged(View view, int progress); }public TouchProgressView(Context context) { super(context, null); }public TouchProgressView(Context context, AttributeSet attrs) { super(context, attrs); }public TouchProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }/** * 设置圆角半径 * * @param radius */ public void setPointRadius(final int radius) { if (radius <= 0) { throw new IllegalArgumentException("radius 不可以小于等于0"); }if (getWidth() == 0) { post(new Runnable() { @Override public void run() { if (radius * 2 > getWidth()) { throw new IllegalArgumentException("radius*2 必须小于 view.getWidth() == " + getWidth()); } pointRadius = radius; } }); } else { if (radius * 2 > getWidth()) { throw new IllegalArgumentException("radius*2 必须小于 view.getWidth() == " + getWidth()); } this.pointRadius = radius; } }/** * 设置圆角颜色 * * @param color */ public void setPointColor(@ColorRes int color) { this.pointColor = color; }/** * 设置直线高度 * * @param height */ public void setLineHeight(int height) { if (height <= 0) { throw new IllegalArgumentException("height 不可以小于等于0"); }this.lineHeight = height; }/** * 设置直线颜色 * * @param color */ public void setLineColor(@ColorRes int color) { this.lineClor = color; }/** * 设置百分比 * * @param progress */ public void setProgress(int progress) { if (progress < 0 || progress > 100) { throw new IllegalArgumentException("progress 不可以小于0 或大于100"); } this.progress = progress; invalidate(); if (progressChangedListener != null) { progressChangedListener.onProgressChange(this, progress); } }/** * 滑动propress,不回调监听 */ public void slideProgress(int progress) { if (progress < 0 || progress > 100) { throw new IllegalArgumentException("progress 不可以小于0 或大于100"); } this.progress = progress; invalidate(); }public int getProgress() { return progress; }/** * 回调进度完成接口 */ private void callBackListener(int progress) { if (progressChangedListener != null) { progressChangedListener.onProgressChanged(this, progress); } }private void callBackIngListener() { if (progressChangedListener != null) { progressChangedListener.onStartChange(this); } }/** * 设置进度变化监听器 * * @param onProgressChangedListener */ public void setOnProgressChangedListener(OnProgressChangedListener onProgressChangedListener) { this.progressChangedListener = onProgressChangedListener; }@Override public boolean onTouchEvent(MotionEvent event) { Log.i("cqc", "event.getAction=" + event.getAction()); lastX = event.getX(); if (event.getX() < pointRadius) { setProgress(PROGRESS_MIN); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: callBackIngListener(); return true; case MotionEvent.ACTION_MOVE: callBackIngListener(); return true; case MotionEvent.ACTION_UP: callBackListener(PROGRESS_MIN); return true; default: return true; } } else if (event.getX() > getWidth() - pointRadius) { setProgress(PROGRESS_MAX); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: callBackIngListener(); return true; case MotionEvent.ACTION_MOVE: callBackIngListener(); return true; case MotionEvent.ACTION_UP: callBackListener(PROGRESS_MAX); return true; default: return true; } } else { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setProgress(calculProgress(event.getX())); callBackIngListener(); return true; case MotionEvent.ACTION_MOVE: setProgress(calculProgress(event.getX())); callBackIngListener(); return true; case MotionEvent.ACTION_UP: callBackListener(calculProgress(event.getX())); return true; } }return super.onTouchEvent(event); }@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }@Override public void draw(Canvas canvas) { super.draw(canvas); linePaint = new Paint(); linePaint.setAntiAlias(true); linePaint.setStyle(Paint.Style.FILL); linePaint.setStrokeCap(Paint.Cap.ROUND); linePaint.setStrokeWidth(lineHeight); linePaint.setColor(getResources().getColor(lineClor)); canvas.drawLine(pointRadius / 2, getHeight() / 2, getWidth() - pointRadius / 2, getHeight() / 2, linePaint); pointPaint = new Paint(); pointPaint.setAntiAlias(true); pointPaint.setStyle(Paint.Style.FILL); pointPaint.setColor(getResources().getColor(pointColor)); RectF r2 = new RectF(); //RectF对象 r2.left = getCx() - pointRadius; //左边 r2.top = getHeight() / 2 - pointRadius; //上边 r2.right = getCx() + pointRadius; //右边 r2.bottom = getHeight() / 2 + pointRadius; //下边 canvas.drawRoundRect(r2, 20, 20, pointPaint); //canvas.drawCircle(getCx(), getHeight() / 2, pointRadius, pointPaint); textPaint = new Paint(); textPaint.setAntiAlias(true); textPaint.setStyle(Paint.Style.FILL); textPaint.setTypeface(Typeface.DEFAULT_BOLD); textPaint.setTextSize(50); textPaint.setColor(getResources().getColor(R.color.dream_text)); Log.i("cqc", "progress=" + progress); if (progress > 9 && progress < 100) { canvas.drawText("" + progress, getCx() - 32, getHeight() / 2 + pointRadius / 3, textPaint); } else if (progress > 99) { Log.i("cqc", "11111"); canvas.drawText("" + progress, getCx() - pointRadius + 2, getHeight() / 2 + pointRadius / 3, textPaint); } else { canvas.drawText("" + progress, getCx() - pointRadius / 3, getHeight() / 2 + pointRadius / 3, textPaint); } }/** * 获取圆角的x轴坐标 * * @return */ private float getCx() { float cx = 0.0f; cx = (getWidth() - pointRadius * 2); if (cx < 0) { throw new IllegalArgumentException("TouchProgressView 宽度不可以小于 2 倍 pointRadius"); } return cx / 100 * progress + pointRadius; }/** * 计算触摸点的百分比 * * @param eventX * @return */ private int calculProgress(float eventX) { float proResult = (eventX - pointRadius) / (getWidth() - pointRadius * 2); return (int) (proResult * 100); }}


    推荐阅读