Introduction: There is a requirement of scale in business. I have tried to use virtual List scheme to draw DOM before, but there is a big drawback that when sliding speed is too fast, it will lead to blank rendering. I have been worried about it all the time, so I have achieved today’s implementation.
1. Knowledge points on Canvas
1. It is recommended to refer to the Canvas API used in the code from the Chinese document of the Canvas API.
2. DPR is used to set the width and height of canvas in the code, mainly to solve the problem of drawing on the mobile terminal. For details, please refer to the reasons and solutions of canvas drawing blur on the mobile terminal in this article.
3. As for why two canvas are created for drawing in the code, it is mainly to avoid the flash screen problem. See this article for more information about using dual caches to solve canvas clearRect splash screen problems
2. Initialize the component
We first defined the props Interface that was passed in
Secondly, write the corresponding initialization function. The function of initialization function is to ensure the correctness of parameter transmission, obtain the relevant information of the current DOM for saving, and initialize the relevant value ~
3. Write the drawing function
Before drawing, we need to think about what parts of a scale we need to draw. In the current component, I will draw the background color and the baseline, the scale pointer, the scale scale, and the gradient area respectively. In fact, the code is very simple, refer to the Canvas API to know what it means.
A little organization, combined with drawing a scale line, is what this code looks like.
4. Add custom scroll events to the component
With custom sliding, it’s really a process of stepping on holes. Originally wanted to use the UI component of the sliding source code, the result found that others rely on CSS to write the corresponding Bezier curve function is done. Fortunately, after some consulting, I saw zhang Xinxu’s article how to use Tween. Js various original animation motion slow algorithm to realize the essence.
Start by adding the corresponding mobile listening event to our component
Write the corresponding listener function on
Pick your favorite Easeout function
Write down the corresponding scroll handling function ~ with easeout
That’s basically it
5. About requestAnimationFrame
This is really lazy and does not want to deal with compatibility issues (because it is a mobile component). Browsers that do not support this API need to be replaced with setTimeout. Here I want to post compatibility writing for vant UI.
6. Attach all codes
import React, { useCallback, useEffect, useRef, useState } from "react"
import './index.less'
const MOMENTUM_LIMIT_TIME = 300
const MOMENTUM_LIMIT_DISTANCE = 40
const defaultSetting = {
/** 刻度线高度 */
scaleHeight: 48,
/** 开始值/最小阈值 */
start:0,
/** 结束值/最大阈值 */
end:0,
/** 间距 */
lineMargin: 10,
/** 精度 */
precision: 0.1
}
/*
* t: current time(当前时间);
* b: beginning value(初始值);
* c: change in value(变化量);
* d: duration(持续时间)。
*/
function easeOut(t: number, b: number, c: number, d: number) {
return c * ((t = t/d - 1) * t * t + 1) + b
}
function range(num: number, min: number, max: number): number {
return Math.min(Math.max(num, min), max);
}
interface ScaleComponentProps{
/** 刻度尺当前值 */
current:number;
/** 刻度尺开始值/最小值 */
start?:number;
/** 刻度尺结束值值/最大值 */
end?:number;
/** 刻度尺精度 */
precision?:number;
/** 回调函数 */
onChange?: (value:number) => void;
}
/** 初始化渲染的canvas信息 */
interface OriginCanvasInfo{
canvas?:HTMLCanvasElement,
context?:CanvasRenderingContext2D,
originCanvasWidth:number,
originCanvasHeight:number,
dprOrginCanvasWidth:number,
dprOriginCanvasHeight:number,
dpr:number
}
const ScaleComponent:React.FC<ScaleComponentProps> = (props) => {
const { current,start,end,precision,onChange } = props;
const canvasRef = useRef<HTMLCanvasElement>(null)
const originCanvasInfo = useRef<OriginCanvasInfo>({
originCanvasWidth:0,
originCanvasHeight:0,
dprOrginCanvasWidth:0,
dprOriginCanvasHeight:0,
dpr:0
})
const startVal = useRef(0)
/** 滑动相关信息记录 */
const touchInfo = useRef({
startX:0,
currentMoveX:0
})
/** 滑动的开始时间 */
const touchStartTime = useRef(0)
const limitThreshole = (value: number) => range(value,defaultSetting.start,defaultSetting.end)
const scrollAction = (distance: number, _duration: number) => {
let targetDistance = startVal.current - distance
const duration = 13
let currentTime = 1
const originVal = startVal.current
const step = () =>{
let value = easeOut(currentTime,originVal,targetDistance-originVal,duration)
if(currentTime < duration){
startVal.current = Math.round(limitThreshole(value) / defaultSetting.precision) * defaultSetting.precision
drawScale()
currentTime+=1
window.requestAnimationFrame(step)
}else{
}
}
window.requestAnimationFrame(step)
}
/** 根据当前精度保留对应的整数或小数位置,再转成数字类型 */
const handlePrecisionNum = (num:number) =>{
const settingPrecision = defaultSetting.precision
if(settingPrecision < 1){
return Number(num.toFixed(settingPrecision * 10))
}
return Number(num.toFixed(settingPrecision - 1))
}
/** 绘制中间线 */
const drawMiddleLine = (context:CanvasRenderingContext2D) =>{
/** ————绘制中间线———— */
const midLineXVal = Math.floor(originCanvasInfo.current.originCanvasWidth / 2)
context.beginPath()
context.lineWidth = 4
context.lineCap = 'round'
context.moveTo(midLineXVal,0)
context.lineTo(midLineXVal ,48)
context.strokeStyle = '#44CD8D'
context.stroke()
context.closePath()
}
/** 绘制背景色和底线 */
const drawBackGroundUnderLine = (tempCanvas: HTMLCanvasElement,tempContext: CanvasRenderingContext2D) =>{
tempContext.fillStyle = '#fff'
tempContext.fillRect(0, 0, tempCanvas.width, 200)
tempContext.beginPath()
tempContext.moveTo(0,0)
tempContext.lineTo(tempCanvas.width,0)
tempContext.strokeStyle = '#9E9E9E'
tempContext.stroke()
tempContext.closePath()
}
/** 绘制两侧渐变区域 */
const drawLinearGradient = (context: CanvasRenderingContext2D) =>{
const originCanvasWidth = originCanvasInfo.current.originCanvasWidth
context.beginPath()
let lineargradient = context.createLinearGradient(65, 31, 0, 31)
lineargradient.addColorStop(0,'rgba(255, 255, 255, 0)')
lineargradient.addColorStop(1,'#FFFFFF')
context.fillStyle = lineargradient
context.fillRect(0, 0, 65, 91)
context.closePath()
context.beginPath()
let lineargradient1 = context.createLinearGradient(originCanvasWidth, 31, originCanvasWidth - 65, 31)
lineargradient1.addColorStop(0,'#FFFFFF')
lineargradient1.addColorStop(1,'rgba(255, 255, 255, 0)')
context.fillStyle = lineargradient1
context.fillRect(originCanvasWidth - 65, 0, 65, 91)
context.closePath()
}
/** 绘制刻度线 */
const drawScale = (useCallback(() =>{
const {context,originCanvasWidth,originCanvasHeight,dprOrginCanvasWidth,dprOriginCanvasHeight,dpr} = originCanvasInfo.current
if(!context) return
let tempCanvas:HTMLCanvasElement = document.createElement('canvas')
let tempContext = tempCanvas.getContext('2d')!
tempCanvas.style.width = `${originCanvasWidth}px`
tempCanvas.style.height = `${originCanvasHeight}px`
tempCanvas.width = dprOrginCanvasWidth
tempCanvas.height = dprOriginCanvasHeight
tempContext.scale(dpr,dpr)
drawBackGroundUnderLine(tempCanvas,tempContext)
/** 当前刻度尺最左侧的刻度值 */
let beginNum = startVal.current - (originCanvasWidth / 2) / defaultSetting.lineMargin * defaultSetting.precision
/** 当前能绘制刻度尺的总数 */
let scaleTotal = originCanvasWidth / defaultSetting.lineMargin | 0
/** 当前刻度值与向上取整的刻度值之间的差值 */
let beginNumDiffVal = Math.ceil(beginNum / defaultSetting.precision) * defaultSetting.precision - beginNum
/** 计算出间距与精度之间的比例值 */
const marginPrecisionRatio = defaultSetting.lineMargin / defaultSetting.precision
/** 需要空出来的位移值 */
const blankMoveVal = beginNumDiffVal * marginPrecisionRatio
for(let i = 0; i < scaleTotal; i++){
let currentNum = Math.ceil(beginNum / defaultSetting.precision + i) * defaultSetting.precision
if (currentNum < defaultSetting.start) {
continue
} else if (currentNum > defaultSetting.end) {
break
}
tempContext.beginPath()
tempContext.strokeStyle = "#9E9E9E"
tempContext.font = '16px SimSun, Songti SC'
tempContext.fillStyle = '#333333'
tempContext.textAlign = 'center'
tempContext.lineWidth = 1
let drawXval = blankMoveVal + i * defaultSetting.lineMargin
if (currentNum % (defaultSetting.precision * 10) === 0) {
tempContext.moveTo(drawXval, 0)
tempContext.strokeStyle = "#666"
tempContext.shadowColor = '#9e9e9e'
tempContext.fillText(String(currentNum),drawXval,defaultSetting.scaleHeight + 18)
tempContext.lineTo(drawXval, defaultSetting.scaleHeight)
} else if (currentNum % (defaultSetting.precision * 5) === 0) {
tempContext.strokeStyle = "#888"
tempContext.moveTo(drawXval, 0)
tempContext.lineTo(drawXval, defaultSetting.scaleHeight - 8)
} else {
tempContext.moveTo(drawXval, 0)
tempContext.lineTo(drawXval, defaultSetting.scaleHeight - 18)
}
tempContext.stroke()
tempContext.closePath()
}
context.clearRect(0, 0, tempCanvas.width, tempCanvas.height)
context.drawImage(tempCanvas, 0, 0, dprOrginCanvasWidth,dprOriginCanvasHeight, 0, 0, originCanvasWidth, originCanvasHeight)
drawMiddleLine(context)
drawLinearGradient(context)
onChange?.(handlePrecisionNum(startVal.current))
},[onChange]))
useEffect(()=>{
/** 初始化 */
const drawScaleInit = () =>{
if(current < (start || 0)){
throw Error('当前值小于开始值?你真是个大聪明')
}else if(current > (end || 0)){
throw Error('当前值大于结束值?你真是个大聪明')
}
if(!canvasRef.current) return
let canvas:HTMLCanvasElement = canvasRef.current
let context = canvas.getContext('2d')!
const { width: originCanvasWidth, height: originCanvasHeight } = canvas.getBoundingClientRect()
canvas.style.width = `${originCanvasWidth}px`
canvas.style.height = `${originCanvasHeight}px`
const dpr = window.devicePixelRatio
canvas.width = dpr * originCanvasWidth
canvas.height = dpr * originCanvasHeight
context.scale(dpr,dpr)
/** 设置当前值 */
startVal.current = current
originCanvasInfo.current = {
canvas,
context,
originCanvasWidth,
originCanvasHeight,
dprOrginCanvasWidth:dpr * originCanvasWidth,
dprOriginCanvasHeight:dpr * originCanvasHeight,
dpr
}
if(start) defaultSetting.start = start
if(end){
defaultSetting.end = end
}else{
defaultSetting.end = current + 100
}
if(precision){
defaultSetting.precision = precision
}
}
drawScaleInit()
drawScale()
},[current,start,end,precision,drawScale])
const onTouchStart = (event: TouchEvent | React.TouchEvent) =>{
touchInfo.current.startX = event.touches[0].pageX
touchInfo.current.currentMoveX = event.touches[0].pageX
touchStartTime.current = Date.now()
}
const onTouchMove = (event: TouchEvent | React.TouchEvent) =>{
const current_x = event.touches[0].pageX
const move_x = current_x - touchInfo.current.currentMoveX
startVal.current = range(startVal.current - move_x / defaultSetting.lineMargin * defaultSetting.precision,defaultSetting.start,defaultSetting.end)
window.requestAnimationFrame(()=>drawScale())
touchInfo.current.currentMoveX = current_x
const now = Date.now()
if (now - touchStartTime.current > MOMENTUM_LIMIT_TIME) {
touchStartTime.current = now
touchInfo.current.startX = current_x
}
}
const onTouchEnd = (event:TouchEvent | React.TouchEvent) =>{
const duration = Date.now() - touchStartTime.current
const distance = (event.changedTouches[0].pageX - touchInfo.current.startX) * defaultSetting.precision
const allowEaseAction = duration < MOMENTUM_LIMIT_TIME && Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE * defaultSetting.precision
if(allowEaseAction){
scrollAction(distance, duration)
}else{
startVal.current = Math.round(limitThreshole(startVal.current) / defaultSetting.precision) * defaultSetting.precision
window.requestAnimationFrame(()=>drawScale())
}
}
return (
<canvas
className="component-scale"
ref={canvasRef}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
</canvas>
)
}
export default ScaleComponent
Copy the code
Call the following in the page:
<ScaleComponent start={0} end={200} precision={1} current={this.state.numVal} onChange={this.onChange}></ScaleComponent>
Copy the code
I really don’t want to find out how to record a GIF on MAC. It’s late at night after watching a movie. If you are interested, you can CV a wave to the page to see the effect.
7. A small summary
When you encounter a problem, you should dare to think about a solution. Even if you can’t solve it now, you can divide the problem into multiple dimensions to solve it. The most important thing is the desire to solve, followed by the ability. The worst thing you can do is choose to suck when you know what the problem is.