先来看看效果图
两种效果图其实都是一个逻辑, 控制弧度的起始角度与结束角度,再控制粗细即可实现
先在我们页面上创建一个convas元素
<canvas style="{{'width:'+(canvasSize+'px')+';'+('height:'+(canvasSize+'px')+';')}}" canvas-id="slider" type="2d" id="myCanvas" bindtouchstart="onMouseDown" bindtouchmove="onMouseMove" bindtouchend="onChend"></canvas>
canvasSize:控制convas大小的变量,我这里是正方形渲染,所以控制的宽高是一样的大小
onMouseDown:手指按下事件
onMouseMove:手指移动事件
onChend:手指松开事件
现在我们已经有了convas元素,接下来我们先画一个弧形
//先定义一些全局的变量,写在data里面也可以
let startAngle, allAngle, lineWidth, ctx, cvs, r, dotR, A, O;
//.......
data: {
//定义一些参数
ratio: 0,
integerTemperature:0,
decimalsTemperature:0,
canvasSize: 0,
minTemperature:20, //最小值 这里是我自己的变量,根据你们需求自己定义
maxTemperature:62, //最大值
unitTemperature:'°C', //单位
step:1,// {step:滑块的步进单位}
value:0, //存储的温度值
config: {
size: 450, //渲染的convas大小
dotRatio: 5,//渲染的滑块点大小
nowRatio: 50,
lineRatio: 4, //线条的粗细
allAngle: 300, //结束的弧度
canMove: true, //是否可以拖动滑块
startAngle: 120, //起始弧度
dotColor: '#FFF', //点的颜色
actColor: '#e0282f' //激活态颜色
}
}
//.......
//写一个初始化的函数
init() {
const query = wx.createSelectorQuery()
// 查询canvas实例
query.select("#myCanvas").fields({
node: true,
size: true
}).exec((res) => {
// 创建实例
cvs = res[0].node
// 创建上下文
ctx = cvs.getContext("2d")
const dpr = wx.getSystemInfoSync().pixelRatio
this.data.canvasSize = this.data.config.size / 750 * wx.getSystemInfoSync().windowWidth
cvs.width = this.data.canvasSize * dpr
cvs.height = this.data.canvasSize * dpr
startAngle = this.data.config.startAngle > 360 ? 360 : this.data.config.startAngle
allAngle = this.data.config.allAngle > 360 ? 360 : this.data.config.allAngle
this.data.ratio = this.data.config.nowRatio
r = this.data.canvasSize * .4
dotR = (r * this.data.config.dotRatio / 100) * dpr
lineWidth = (r * this.data.config.lineRatio / 100) * dpr
A = {
x: this.data.canvasSize / 2,
y: this.data.canvasSize / 2
}
O = {
x: this.data.canvasSize / 2 + r,
y: this.data.canvasSize / 2
}
const config = this.data.config
config.nowRatio = Math.floor(this.data.ratio)
this.setData({
config,
canvasSize: this.data.canvasSize,
ratio: Math.floor(this.data.ratio),
})
//开始准备画滑块
this.drawSlider()
})
},
calcDotPosition() {
let cosA = Math.cos((startAngle + allAngle * this.data.ratio / 100) * 2 * Math.PI / 360),
sinA = Math.sin((startAngle + allAngle * this.data.ratio / 100) * 2 * Math.PI / 360)
return {
x: (O.x - A.x) * cosA - (O.y - A.y) * sinA + A.x,
y: (O.y - A.y) * cosA + (O.x - A.x) * sinA + A.y
}
},
//------------------------最主要的代码-------------
drawSlider() {
// 背景
const dpr = wx.getSystemInfoSync().pixelRatio
ctx.beginPath()
ctx.clearRect(0, 0, this.data.canvasSize * dpr, this.data.canvasSize * dpr)
const style = ctx.createLinearGradient(0, 0, this.data.canvasSize, 0);
//-----这里是定义滑块非激活态的颜色,我这里是用的渐变色-----
style.addColorStop(0, '#e3e6ea');
style.addColorStop(.5, '#e3e6ea');
style.addColorStop(1, '#e3e6ea');
ctx.shadowBlur = 0
ctx.shadowColor = "transparent"
ctx.lineCap = 'butt'
ctx.strokeStyle = style
ctx.lineWidth = (lineWidth)
ctx.arc(A.x * dpr, A.y * dpr, r * dpr, (startAngle - 3) / 180 * Math.PI, (allAngle + startAngle + 3) / 180 * Math.PI)
ctx.stroke()
// 进度条 --画激活态
ctx.beginPath()
ctx.shadowBlur = 0
ctx.shadowColor = "transparent" //阴影
ctx.strokeStyle = (this.data.config.actColor)
ctx.arc(A.x * dpr, A.y * dpr, r * dpr, (startAngle - 3) / 180 * Math.PI, ((this.data.ratio / 100) * (allAngle / 180) + (startAngle / 180)) * Math.PI)
ctx.stroke()
let{
minTemperature, //最小温度值 这里是我自己的变量,根据你们需求自己定义
maxTemperature, //最大温度值
ratio,
unitTemperature, //温度单位
value //存储的温度值
step,
} = this.data
//把当前的弧度转换为数值 *我这里做了小数数据与整数分隔
const num = String((Math.round(((((maxTemperature - minTemperature) * ratio / 100) + minTemperature).toFixed(1)) / step) * step).toFixed(1)).split('.')
function drawCenteredText(ctx, text, centerX, centerY, size, falg) {
ctx.fillStyle = "#58595b"; // 文本颜色
// 设置字体样式
ctx.font = ` ${size*dpr}px AlibabaPuHuiTi_2_65_Medium`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (falg) {
// 测量文本宽度
const textWidth = ctx.measureText(text).width;
centerX = centerX+textWidth+50
centerY=centerY-40
}
// 渲染文本
ctx.fillText(text, centerX, centerY);
}
// 调用函数
drawCenteredText(ctx, `${num[0]}`, A.x * dpr, A.y * dpr, 42);
drawCenteredText(ctx, `${unitTemperature}`, A.x * dpr, A.y * dpr, 14, true);
// 滑块
ctx.beginPath()
ctx.shadowBlur = 0
ctx.shadowColor = "gray"
ctx.strokeStyle = "#fff"
ctx.lineWidth = 5
ctx.fillStyle = (this.data.config.dotColor)
ctx.arc(this.calcDotPosition().x * dpr, this.calcDotPosition().y * dpr, dotR, 0, 2 * Math.PI, false)
ctx.stroke()
function degreesToRadians(degrees) {
return degrees * Math.PI / 180;
}
// 画刻度
const numTicks = 7; //要画几个刻度
const angleSpan = this.config.allAngle;
const angleIncrement = angleSpan / numTicks;
// 在数字下方画小圆球
const ballDiameter = 3 * dpr; // 圆球直径,考虑dpr
for (let i = 0; i <= numTicks; i++) {
const cx = A.x * dpr;
const cy = A.y * dpr;
const angle = startAngle + i * angleIncrement;
const x = cx + ((r * dpr) + 15) * Math.cos(degreesToRadians(angle));
const y = cy + ((r * dpr) + 15) * Math.sin(degreesToRadians(angle));
// 文本距离圆周的距离,单位为像素
const textOffset = 10 * dpr; // 考虑dpr
const textX = x + textOffset * Math.cos(degreesToRadians(angle));
const textY = y + textOffset * Math.sin(degreesToRadians(angle));
// 设置文本样式,根据dpr调整字体大小
ctx.font = `${10 * dpr}px Arial`; // 字体大小根据dpr调整
ctx.textAlign = "center"; // 文本居中
ctx.textBaseline = "middle"; // 文本基线居中
ctx.fillStyle = "#e1373c"; // 文本颜色
const num = (maxTemperature - minTemperature) / numTicks
// 文本的旋转角度,使其垂直于圆周
const rotationAngle = angle + 90; // Canvas坐标系下,+90度使文本垂直于半径
ctx.save(); // 保存当前绘图状态
ctx.translate(textX, textY); // 移动原点至文本位置
ctx.rotate(degreesToRadians(rotationAngle)); // 旋转
ctx.fillText(parseInt((num * i) + minTemperature), 0, 0); // 绘制文本
ctx.restore(); // 恢复之前的绘图状态
ctx.beginPath();
const x2 = cx + ((r * dpr - 5)) * Math.cos(degreesToRadians(angle));
const y2 = cy + ((r * dpr - 5)) * Math.sin(degreesToRadians(angle));
let textX2 = (x2) + textOffset * Math.cos(degreesToRadians(angle));
let textY2 = (y2) + textOffset * Math.sin(degreesToRadians(angle));
ctx.arc(textX2, textY2, ballDiameter / 2, 0, 2 * Math.PI);
ctx.fillStyle = "#e1373c"; // 圆球颜色
ctx.fill();
ctx.closePath();
}
},
然后在生命周期中调用init() 即可获得一个弧形的滑块了,那么接下来我们完成三个事件中的处理
onMouseDown:手指按下事件
onMouseDown(e) {
this.data.downValue = this.data.value
if (!this.data.config.canMove) return
// 背景
const {
x,
y
} = e.touches[0]
this.calcAngle(x, y)
},
calcAngle(x, y) {
const dpr = wx.getSystemInfoSync().pixelRatio
const AB = Math.sqrt(Math.pow(A.x - O.x, 2) + Math.pow(A.y - O.y, 2)),
AC = Math.sqrt(Math.pow(A.x - x, 2) + Math.pow(A.y - y, 2)),
BC = Math.sqrt(Math.pow(O.x - x, 2) + Math.pow(O.y - y, 2)),
cosA = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AB * AC)
// 触摸的点到圆心的距离超过范围
if (AC > r * 1.2 || AC < r * .7) return
let angleA = Math.round(Math.acos(cosA) * 180 / (Math.PI))
// 判断夹角在顺时针还是逆时针
const dx = x - A.x;
const dy = y - A.y;
if ((O.x - A.x) * dy - (O.y - A.y) * dx < 0) {
angleA = 360 - angleA
}
const tempAngle = angleA - startAngle < 0 ? Math.round((360 - startAngle + angleA) / allAngle * 100) : Math.round((angleA - startAngle) / allAngle * 100)
if (tempAngle > 100) return
this.data.ratio = tempAngle
const config = this.data.config
config.nowRatio = tempAngle
this.drawSlider()
},
onMouseMove:手指移动事件
onMouseMove(e) {
if (!this.data.config.canMove) return
const {
x,
y
} = e.touches[0]
this.calcAngle(x, y)
},
onChend:手指松开事件
/** 计算温度 */
calculateTemperature() {
const {
minTemperature,
maxTemperature,
ratio,
temperatureSlider,
modeSelect
} = this.data
const step = temperatureSlider[modeSelect][0].step
const num = String((Math.round(((((maxTemperature - minTemperature) * ratio / 100) + minTemperature).toFixed(1)) / step) * step).toFixed(1)).split('.')
// 取出整数
this.setData({
integerTemperature: num[0],
decimalsTemperature: num[1] || 0
})
},
onChend(e) {
this.data.timeTable = setTimeout(() => {
this.calculateTemperature()
let{
integerTemperature,
decimalsTemperature,
modeSelect
} = this.data
const num = (Number(integerTemperature) + Number(decimalsTemperature / 10))
if (this.data.downValue == num) {
return
}
}, 1500)
},