一、前言
最近项目里面需要用到多个seekbar并且需要显示不同的精度,于是参考了部分博客模仿华为的天气刻度盘添加了精度控制和自己定义的seekbar实现了如下效果(画面模糊是因为录制Gif图的工具导致的)。
本控件最低兼容到API16 更低的版本没有getThumb()函数
二、刻度盘部分
1.刻度:刻度的绘制类似于绘制时钟刻度绘制出一条刻度通过旋转canvas绘制出其他刻度
2.波浪:波浪的绘制是通过两个正弦函数y = Asin(wx+b)+h 实时变换sin的初相
3.精度控制:通过设定 精度模式,最大进度值和显示的最大值共同决定。最大进度值/当前进度值=实际显示的最大值/当前显示的值
-
4.WaveDialView代码:
package com.hubin.scaleseekbar; /* * @项目名: ScaleSeekBar * @包名: com.hubin.scaleseekbar * @文件名: DigitalThumbSeekbar * @创建者: 胡英姿 * @创建时间: 2018-03-10 14:38 * @描述: 一个带水波纹的刻度盘自定义view */ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import java.text.DecimalFormat; import java.util.Timer; import java.util.TimerTask; public class WaveDialView extends View { private static final String TAG = "ScaleView"; public static final int PRECISION_MODE_INTEGER =0; //整数模式 public static final int PRECISION_MODE_ONE_DECIMAL_PLACES =1; //精确到小数点后一位 public static final int PRECISION_MODE_TWO_DECIMAL_PLACES =2;//精确到小数点后两位 public int PRECISION_MODE_DEFUALE =PRECISION_MODE_INTEGER;//默认精度模式 整数模式 private float mMaxValue = 100; //显示的最大值 private String textUnit = ""; //右上角文字 单位 private String textName = "";//中间要显示的文字名字 // 画圆弧的画笔 private Paint paint; // 正方形的宽高 private int len; // 圆弧的半径 private float radius; // 矩形 private RectF oval; // 圆弧的起始角度 private float startAngle = 120; // 圆弧的经过总范围角度角度 private float sweepAngle = 300; // 刻度经过角度范围 private float targetAngle = 300; // 绘制文字 Paint textPaint; // 监听角度变化对应的颜色变化 private OnAngleColorListener onAngleColorListener; public WaveDialView(Context context) { this(context,null); } public WaveDialView(Context context, AttributeSet attrs) { super(context, attrs); paint = new Paint(); paint.setColor(Color.WHITE); paint.setAntiAlias(true); paint.setStyle(Paint.Style.STROKE); textPaint = new Paint(); textPaint.setARGB(255, 255, 255, 255); textPaint.setAntiAlias(true); waterPaint = new Paint(); waterPaint.setAntiAlias(true); moveWaterLine();//让水波纹开始运动 } /** * 设置动画效果,开启子线程定时绘制 * * @param trueAngle */ // 前进或者后退的状态,1代表前进,2代表后退。初始为后退状态。 int state = 2; // 每次后退时的值,实现越来越快的效果 private int[] back = {2, 2, 4, 4, 6, 6, 8, 8, 10}; // 每次前进时的值,实现越来越慢的效果 private int[] go = {10, 10, 8, 8, 6, 6, 4, 4, 2}; // 前进的下标 private int go_index = 0; // 后退的下标 private int back_index = 0; private float score; private int color; private boolean isRunning; /** * 使用定时器自动开始变幻刻度 * * @param trueAngle */ public void change(final float trueAngle) { if (isRunning) { return; } final Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { switch (state) { case 1: // 开始增加 targetAngle += go[go_index]; go_index++; if (go_index == go.length) {// 到最后的元素时,下标一直为最后的 go_index--; } if (targetAngle >= trueAngle) {// 如果画过刻度大于等于真实角度 // 画过刻度=真实角度 targetAngle = trueAngle; // 状态改为2 state = 2; isRunning = false; timer.cancel(); } break; case 2: isRunning = true; targetAngle -= back[back_index]; back_index++; if (back_index == back.length) { back_index--; } if (targetAngle <= 0) { targetAngle = 0; state = 1; } break; default: break; } computerScore();// 计算当前比例应该的多少分 // 计算出当前所占比例,应该增长多少 computerUp(); postInvalidate(); } }, 500, 30); } /** * 手动设置刻度盘的值 * @param trueAngle 0-300 */ public void setChange(final float trueAngle) { targetAngle = trueAngle; computerScore();//计算得分 if (clipRadius == 0) { final Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { if (clipRadius != 0) { // 计算出当前所占比例,应该增长多少 computerUp(); postInvalidate(); timer.cancel(); } } }, 0, 10); } else { computerUp(); invalidate(); } } //计算当前比例应该得多少分 private void computerScore() { score = targetAngle / 300 * mMaxValue; } //计算水位 private void computerUp() { up = (int) (targetAngle / 360 * clipRadius * 2); } /** * 调用此方法水波纹开始运动 */ public void moveWaterLine() { final Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { move += 1; if (move == 100) { timer.cancel(); } postInvalidate(); } }, 500, 200); } // 存放第一条水波Y值 private float[] firstWaterLine; // 第二条 private float[] secondWaterLine; // 画水球的画笔 private Paint waterPaint; // 影响三角函数的初相 private float move; // 剪切圆的半径 private int clipRadius; // 水球的增长值 int up = 0; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 通过测量规则获得宽和高 int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); // 取出最小值 len = Math.min(width, height); oval = new RectF(0, 0, len, len); radius = len / 2; clipRadius = (len / 2) - 45; firstWaterLine = new float[len]; secondWaterLine = new float[len]; setMeasuredDimension(len, len); } @Override protected void onDraw(Canvas canvas) { // 绘制一个圆弧,如果懂得坐标系的旋转,可以不写。 // canvas.drawArc(oval, startAngle, sweepAngle, false, paint); // 画布,圆心左边,半径,起始角度,经过角度, // 说白了就是canvas没有提供画特殊图形的方法,就需要我们自己去实现这种功能了 // 画刻度线 drawLine(canvas); // 画刻度线内的内容 drawText(canvas); } /** * 画水球的功能 * * @param canvas */ private void drawWaterView(Canvas canvas) { // y = Asin(wx+b)+h ,w影响周期,A影响振幅,h影响y位置,b为初相; // 将周期定为view总宽度 float mCycleFactorW = (float) (2 * Math.PI / len); // 得到第一条波的y值 for (int i = 0; i < len; i++) { firstWaterLine[i] = (float) (10 * Math .sin(mCycleFactorW * i + move) - up); } // 得到第一条波的y值 for (int i = 0; i < len; i++) { secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + move + 10) - up); } canvas.save(); // 裁剪成圆形区域 Path path = new Path(); waterPaint.setColor(color); path.reset(); canvas.clipPath(path); path.addCircle(len / 2, len / 2, clipRadius, Path.Direction.CCW); canvas.clipPath(path, android.graphics.Region.Op.REPLACE); // 将坐标系移到底部 canvas.translate(0, len / 2 + clipRadius); for (int i = 0; i < len; i++) { canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint); } for (int i = 0; i < len; i++) { canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint); } canvas.restore(); } /** * 实现画刻度线内的内容 * * @param canvas */ private void drawText(Canvas canvas) { Paint cPaint = new Paint(); // cPaint.setARGB(50, 236, 241, 243); cPaint.setAlpha(50); cPaint.setARGB(50, 236, 241, 243); // 画圆形背景 RectF smalloval = new RectF(40, 40, radius * 2 - 40, radius * 2 - 40); // 画水波 drawWaterView(canvas); canvas.drawArc(smalloval, 0, 360, true, cPaint); // 在小圆圈的外围画一个白色圈 canvas.drawArc(smalloval, 0, 360, false, paint); // 设置文本对齐方式,居中对齐 textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setTextSize(clipRadius / 2); // 画分数 switch (PRECISION_MODE_DEFUALE) { case PRECISION_MODE_ONE_DECIMAL_PLACES://小数点后一位 float f1Score = (float)(Math.round(score*10))/10; canvas.drawText("" + f1Score, radius, radius, textPaint); break; case PRECISION_MODE_TWO_DECIMAL_PLACES://小数点后两位 DecimalFormat fnum = new DecimalFormat("##0.00"); String f2Score=fnum.format(score); canvas.drawText( f2Score, radius, radius, textPaint); break; default://默认 整数模式 canvas.drawText("" + (int)score, radius, radius, textPaint); break; } textPaint.setTextSize(clipRadius / 6); // 画固定值分 canvas.drawText(textUnit, radius + clipRadius / 2, radius - clipRadius / 4, textPaint); textPaint.setTextSize(clipRadius / 6); // 画固定值立即优化 canvas.drawText(textName, radius, radius + clipRadius / 2, textPaint); } float a = sweepAngle / 100; private Paint linePaint; /** * 实现画刻度线的功能 * * @param canvas */ private void drawLine(final Canvas canvas) { // 保存之前的画布状态 canvas.save(); // 移动画布,实际上是改变坐标系的位置 canvas.translate(radius, radius); // 旋转坐标系,需要确定旋转角度 canvas.rotate(30); // 初始化画笔 linePaint = new Paint(); // 设置画笔的宽度(线的粗细) linePaint.setStrokeWidth(2); // 设置抗锯齿 linePaint.setAntiAlias(true); // 累计叠加的角度 float c = 0; for (int i = 0; i <= 100; i++) { if (c <= targetAngle && targetAngle != 0) {// 如果累计画过的角度,小于当前有效刻度 // 计算累计划过的刻度百分比(画过的刻度比上中共进过的刻度) double p = c / (double) sweepAngle; int red = 255 - (int) (p * 255); int green = (int) (p * 255); color = linePaint.getColor(); if (onAngleColorListener != null) { onAngleColorListener.onAngleColorListener(red, green); } linePaint.setARGB(255, red, green, 50); canvas.drawLine(0, radius, 0, radius - 20, linePaint); // 画过的角度进行叠加 c += a; } else { linePaint.setColor(Color.WHITE); canvas.drawLine(0, radius, 0, radius - 20, linePaint); } canvas.rotate(a); } // 恢复画布状态。 canvas.restore(); } public void setOnAngleColorListener( OnAngleColorListener onAngleColorListener) { this.onAngleColorListener = onAngleColorListener; } /** * 默认初始化配置 精度模式为整数 最大值为100 * @param textName 水球中间显示的名字 * @param textUnit 右上角的单位 */ public void setInitConfig(String textName,String textUnit) { this.textName = textName; this.textUnit = textUnit; } /** * 默认初始化配置 精度模式为整数 * @param maxValue 最大值 * @param textName 水球中间显示的名字 * @param textUnit 右上角的单位 */ public void setInitConfig(int maxValue,String textName,String textUnit) { mMaxValue= maxValue; this.textName = textName; this.textUnit = textUnit; } /** * 初始化配置 * @param precisionModeDefuale 精度模式 * @param maxValue 最大值 * @param textName 水球中间显示的名字 * @param textUnit 右上角的单位 */ public void setInitConfig(int precisionModeDefuale,float maxValue,String textName,String textUnit) { PRECISION_MODE_DEFUALE = precisionModeDefuale; mMaxValue= maxValue; this.textName = textName; this.textUnit = textUnit; } /** * 监听角度和颜色变化的接口 * * @author Administrator */ public interface OnAngleColorListener { void onAngleColorListener(int red, int green); } }
三、自定义seekbar
1.thumb(滑块):通过扩展已有的seekbar控件获取滑块位置并且通过canvas绘制图像和进度的数值
2.进度线:进度线条是一个圆角的矩形通过滑块的位置绘制矩形的长度,滑块左边一根右边一根拼起来
3.尺寸的确定都是基于滑块而绘制的所以在初始化时需要先有一个确定尺寸的滑块,我是在drawable中定义的一个只有尺寸的滑块,这样做的缺陷在于在XML布局文件中的android:layout_height="" 任何设置均无效设置成wrap_content即可,要调整大小就修改drawable中绘制的滑块的尺寸
4.精度控制:原理同刻度盘精度控制
-
5.DigitalThumbSeekbar代码:
package com.hubin.scaleseekbar; /* * @项目名: ScaleSeekBar * @包名: com.hubin.scaleseekbar * @文件名: DigitalThumbSeekbar * @创建者: 胡英姿 * @创建时间: 2018-03-10 14:38 * @描述: 自定义进度显示在thumb上的Seekbar */ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.v7.widget.AppCompatSeekBar; import android.util.AttributeSet; import java.text.DecimalFormat; public class DigitalThumbSeekbar extends AppCompatSeekBar { private Paint mTextPaint; private Paint mLinePaint; private RectF mRectF; private Paint mCirclePaint; private Paint mCirclePaint2; private Drawable mThumb; private float mMaxShowValue; //需要显示的最大值 private int mPrecisionMode;//精度模式 private int mViewWidth; private int mCenterX; private int mCenterY; private int mThumbHeight; public DigitalThumbSeekbar(Context context) { this(context, null); } public DigitalThumbSeekbar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DigitalThumbSeekbar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DigitalThumbSeekbar); mMaxShowValue = typedArray.getFloat(R.styleable.DigitalThumbSeekbar_maxShowValue, getMax());//获取最大显示值 mPrecisionMode = typedArray.getInt(R.styleable.DigitalThumbSeekbar_PrecisionMode, 0);//进度模式 typedArray.recycle();//释放资源 //设置滑块样式 mThumb = context.getResources().getDrawable(R.drawable.circle_thumb); setThumb(mThumb); setThumbOffset(0); initPaint();//初始化画笔 } private void initPaint() { //文字画笔 mTextPaint = new Paint(); mTextPaint.setARGB(255, 255, 255, 255); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(mThumb.getMinimumHeight() * 3 / 7);//文字大小为滑块高度的2/3 mTextPaint.setTextAlign(Paint.Align.CENTER); // 设置文本对齐方式,居中对齐 //进度画笔 mLinePaint = new Paint(); mLinePaint.setAntiAlias(true); //实心圆 mCirclePaint = new Paint(); // mCirclePaint.setColor(0xFFFFFFFF); mCirclePaint.setARGB(255, 255, 65, 130); mCirclePaint.setAntiAlias(true); mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); // mCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); //空心圆 mCirclePaint2 = new Paint(); mCirclePaint2.setARGB(255, 255, 255, 255); mCirclePaint2.setAntiAlias(true); mCirclePaint2.setStrokeWidth(mThumb.getMinimumHeight() / 20); mCirclePaint2.setStyle(Paint.Style.STROKE); } @Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //父容器传过来的宽度的值 mViewWidth = MeasureSpec.getSize(widthMeasureSpec) -getPaddingLeft() - getPaddingRight(); //根据滑块的尺寸确定大小 布局文件中的android:layout_height="" 任何设置不会改变绘制的大小 heightMeasureSpec = MeasureSpec.makeMeasureSpec(mThumb.getMinimumHeight(), MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //获取滑块坐标 Rect thumbRect = getThumb().getBounds(); mCenterX = thumbRect.centerX();//中心X坐标 mCenterY = thumbRect.centerY(); //中心Y坐标 mThumbHeight = thumbRect.height();//滑块高度 //绘制进度条 drawRect(canvas); //绘制滑块 canvas.drawCircle(mCenterX, mCenterY, mThumbHeight / 2, mCirclePaint); canvas.drawCircle(mCenterX, mCenterY, mThumbHeight / 2 - mThumbHeight / 20, mCirclePaint2);//描边 //绘制进度文字 drawProgress(canvas); } /** * 绘制进度条 * @param canvas */ private void drawRect(Canvas canvas) { //绘制左边的进度 mRectF = new RectF(); mRectF.left = 0; // mRectF.right = thumbRect.left; mRectF.right = mCenterX; mRectF.top = mCenterY - mThumbHeight / 4; mRectF.bottom = mCenterY + mThumbHeight / 4; mLinePaint.setColor(Color.GREEN); canvas.drawRoundRect(mRectF, mThumbHeight / 4, mThumbHeight / 4, mLinePaint); //绘制右边剩余的进度 mRectF.left= mCenterX; mRectF.right = mViewWidth; mRectF.top = mCenterY - mThumbHeight / 15; mRectF.bottom = mCenterY + mThumbHeight / 15; mLinePaint.setARGB(255,255,65,130); canvas.drawRoundRect(mRectF, mThumbHeight / 15, mThumbHeight /15, mLinePaint); } /** * 绘制显示的进度文本 * @param canvas */ private void drawProgress(Canvas canvas) { String progress; float score = mMaxShowValue*getProgress()/getMax(); switch (mPrecisionMode) { case 1://小数点后一位 float f1Score = (float)(Math.round(score*10))/10; progress="" + f1Score; break; case 2://小数点后两位 DecimalFormat fnum = new DecimalFormat("##0.00"); progress=fnum.format(score); break; default://默认 整数模式 progress="" +(int)score; break; } //测量文字高度 Rect bounds = new Rect(); mTextPaint.getTextBounds(progress, 0, progress.length(), bounds); int mTextHeight = bounds.height();//文字高度 // float mTextWidth = mTextPaint.measureText(progress); canvas.drawText(progress, mCenterX, mCenterY + mTextHeight / 2, mTextPaint); } }
四、包装
刻度盘显示在PopupWindow中,当触摸Seekbar显示PopupWindow并且使得sin函数开始运动,通过进度值去控制进度盘的指示位置和水的深度以及颜色ARGB值,所以整个对刻度盘的控制都包装在Seekbar 的监听器里面:
package com.hubin.scaleseekbar;
/*
* @项目名: ScaleSeekBar
* @包名: com.hubin.scaleseekbar
* @文件名: WaveDialSeekbarListener
* @创建者: 胡英姿
* @创建时间: 2018-03-09 16:26
* @描述: 一个将SoftDialView 包装在里面的seekbar监听器
*/
import android.graphics.Color;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import android.widget.SeekBar;
import java.lang.ref.WeakReference;
public class WaveDialSeekbarListener implements SeekBar.OnSeekBarChangeListener {
private static final String TAG = "WaveDialSeekbarListener";
private WeakReference<MainActivity> mActivityWeakReference; //activity的弱应用
private WaveDialView mWaveDialView;
private PopupWindow mPopupWindow;
private int mMaxProgress; //最大进度值
private float mMaxShowValue; //最大显示值
private String textUnit = ""; //右上角文字 单位
private String textName = "";//中间要显示的文字名字
private int precisionModeDefuale;//精度模式
/**
* 构造函数
* @param activity activity的引用
* @param maxProgress seekbar 的最大进度值
* @param maxShowValue 刻度盘上需要显示的最大值
* @param textName 中间要显示的文字名字
* @param textUnit 右上角文字 单位
* @param precisionModeDefuale 精度模式 整数,1位小数,2位小数
*/
public WaveDialSeekbarListener(MainActivity activity, int maxProgress, float maxShowValue, String textName, String textUnit, int precisionModeDefuale) {
mActivityWeakReference = new WeakReference(activity);
mMaxProgress= maxProgress;
mMaxShowValue = maxShowValue;
this.textName = textName;
this.textUnit = textUnit;
this.precisionModeDefuale = precisionModeDefuale;
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (mWaveDialView != null) {
mWaveDialView.setChange((float) progress*300/mMaxProgress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mWaveDialView = new WaveDialView(mActivityWeakReference.get());
mWaveDialView.setInitConfig(precisionModeDefuale,mMaxShowValue,textName,textUnit);
mWaveDialView.moveWaterLine();
mWaveDialView.setOnAngleColorListener(onAngleColorListener);//颜色监听器 可自行设置
//窗口宽度
int windowWidth = mActivityWeakReference.get().getWindowManager().getDefaultDisplay().getWidth();
mPopupWindow = new PopupWindow(mWaveDialView, windowWidth/2, ViewGroup
.LayoutParams.WRAP_CONTENT, true);
mPopupWindow.showAtLocation(View.inflate(mActivityWeakReference.get(),R.layout.activity_main, null),
Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0); //显示位置
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mWaveDialView=null;
mPopupWindow.dismiss();
}
private WaveDialView.OnAngleColorListener onAngleColorListener=new WaveDialView.OnAngleColorListener() {
@Override
public void onAngleColorListener(int red, int green) {
int c=Color.argb(150, red, green, 0);
mActivityWeakReference.get().setBgColor(c);
}
};
}