需求背景:需要在Taro小程序中给图片根据坐标增加标注信息,并实现图片的放大缩小,切换下一页功能。
实现技术:Taro、Canvas
实现代码
const [swiperCurrent, setSwiperCurrent] = useState(0) // 图片页码
const [imgList, setImgList] = useState([]) // 图片数据
const [isMoving, setIsMoving] = useState(false) // 图片是否移动
const [lastTouch, setLastTouch] = useState({ x: 0, y: 0 }) // 最后触摸的坐标
const [lastDistance, setLastDistance] = useState(0) // 最后缩放
const scale = useRef(1) // 放大倍数
const position = useRef({ x: 0, y: 0 }) // 图片移动的位置
const canvasRef = useRef(null)
const ctxRef = useRef(null)
const currentImageRef = useRef(null)
const imageCacheRef = useRef(new Map()) // 缓存图片,不然放大缩小时每次渲染图片会导致图片闪烁
const currentImgObjRef = useRef(null) // 当前图片对象
const lastClickTimeRef = useRef(0)
useEffect(() => {
geImgtList()
}, [])
const geImgtList = async () => {
const img = [{ url: 'http://e.hiphotos.baidu.com/image/pic/item/a1ec08fa513d2697e542494057fbb2fb4316d81e.jpg',coord:[ [10, 10], [ 20, 20 ] ], suspectCode: '标注的文字' }]
setImgList(img)
if(!!img.length) {
const currentImage = img[0];
currentImageRef.current = currentImage
try {
const localImagePath = await loadBgImg(currentImage.url)
await initCanvas(localImagePath, currentImage.coord, currentImage .suspectCode)
} catch (error) {
console.error('图片加载失败:', error)
}
}
}
const loadBgImg = async (imgUrl) => {
if (imageCacheRef.current.has(imgUrl)) {
return imageCacheRef.current.get(imgUrl)
}
try {
const res = await Taro.downloadFile({
url: imgUrl
})
if (res.statusCode === 200) {
imageCacheRef.current.set(imgUrl, res.tempFilePath)
return res.tempFilePath
} else {
throw new Error(`下载失败: ${res.statusCode}`)
}
} catch (error) {
console.error('图片加载失败:', error)
throw error
}
}
const initCanvas = (imagePath, coordinates = null, suspectCode) => {
return new Promise((resolve, reject) => {
Taro.createSelectorQuery()
.select('#myCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (res[0] && res[0].node) {
const canvas = res[0].node
canvasRef.current = canvas
const ctx = canvas.getContext('2d')
ctxRef.current = ctx
const canvasWidth = res[0].width
const canvasHeight = res[0].height
const dpr = Taro.getSystemInfoSync().pixelRatio
canvas.width = canvasWidth * dpr
canvas.height = canvasHeight * dpr
ctx.scale(dpr, dpr)
// 创建图片对象并保存引用
const img = canvas.createImage()
currentImgObjRef.current = img
img.onload = () => {
try {
drawImage(img, coordinates, suspectCode)
resolve()
} catch (error) {
console.error('绘制图片时出错:', error)
reject(error)
}
}
img.onerror = (error) => {
console.error('图片加载失败:', error)
reject(error)
}
img.src = imagePath
} else {
console.error('无法获取 canvas 节点')
reject(new Error('无法获取 canvas 节点'))
}
})
})
}
const drawImage = (img, coordinates = null, suspectCode) => {
if (!canvasRef.current || !ctxRef.current) return
const canvas = canvasRef.current
const ctx = ctxRef.current
const canvasWidth = canvas.width / (Taro.getSystemInfoSync().pixelRatio || 1)
const canvasHeight = canvas.height / (Taro.getSystemInfoSync().pixelRatio || 1)
// 计算自适应尺寸
const imgWidth = img.width
const imgHeight = img.height
const scaleX = canvasWidth / imgWidth
const scaleY = canvasHeight / imgHeight
const baseScale = Math.min(scaleX, scaleY, 1)
// 应用当前缩放
const currentScale = baseScale * scale.current
// 计算绘制尺寸
const drawWidth = imgWidth * currentScale
const drawHeight = imgHeight * currentScale
// 计算位置 - 垂直居中并应用偏移
const baseX = (canvasWidth - drawWidth) / 2
const baseY = (canvasHeight - drawHeight) / 2
const x = baseX + position.current.x
const y = baseY + position.current.y
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
// 绘制图片
ctx.drawImage(img, x, y, drawWidth, drawHeight)
// 如果有坐标信息,绘制边框
if (coordinates && coordinates.length >= 2) {
const [x1, y1] = coordinates[0];
const [x2, y2] = coordinates[1];
const scaledX1 = x + (x1 / imgWidth) * drawWidth;
const scaledY1 = y + (y1 / imgHeight) * drawHeight;
const scaledX2 = x + (x2 / imgWidth) * drawWidth;
const scaledY2 = y + (y2 / imgHeight) * drawHeight;
// 绘制矩形边框
ctx.strokeStyle = '#2e9cef';
ctx.lineWidth = 1;
ctx.strokeRect(
scaledX1,
scaledY1,
scaledX2 - scaledX1,
scaledY2 - scaledY1
);
// 绘制 suspectCode 文本
let text = suspectCode
if (text) {
ctx.font = '14px Microsoft YaHei';
ctx.fillStyle = '#2e9cef';
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
const textX = scaledX1;
const textY = scaledY1 - 5;
ctx.fillText(text, textX, textY);
}
}
}
// 单指触摸 - 移动
const handleTouchStart = (e) => {
if (e.touches.length === 1) {
const touch = e.touches[0]
setLastTouch({ x: touch.clientX, y: touch.clientY })
setIsMoving(true)
} else if (e.touches.length === 2) {
// 双指触摸 - 缩放
const touch1 = e.touches[0]
const touch2 = e.touches[1]
const distance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
)
setLastDistance(distance)
}
}
// 移动
const handleTouchMove = (e) => {
if (e.touches.length === 1 && isMoving) {
// 单指移动
const touch = e.touches[0]
const deltaX = touch.clientX - lastTouch.x
const deltaY = touch.clientY - lastTouch.y
position.current = { x: position.current.x + deltaX, y: position.current.y + deltaY }
setLastTouch({ x: touch.clientX, y: touch.clientY })
// 立即重绘
if (currentImgObjRef.current) {
const currentImage = currentImageRef.current
drawImage(currentImgObjRef.current, currentImage?.coord, currentImage.suspectCode)
}
} else if (e.touches.length === 2) {
// 双指缩放
const touch1 = e.touches[0]
const touch2 = e.touches[1]
const distance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
)
if (lastDistance > 0) {
const scaleFactor = distance / lastDistance
const newScale = Math.max(0.5, Math.min(3, scale.current * scaleFactor))
if (newScale !== scale.current) {
scale.current = newScale
// 立即重绘
if (currentImgObjRef.current) {
const currentImage = currentImageRef.current
drawImage(currentImgObjRef.current, currentImage?.coord, currentImage.suspectCode)
}
}
}
setLastDistance(distance)
}
}
const handleTouchEnd = () => {
setIsMoving(false)
setLastDistance(0)
}
// 双击图片回到图片初始大小
const handleClick = () => {
const currentTime = new Date().getTime();
const timeDiff = currentTime - lastClickTimeRef.current;
// 点击时长
if(timeDiff < 200) {
resetTransform()
}
lastClickTimeRef.current = currentTime;
}
// 重置缩放和位置
const resetTransform = () => {
if (!currentImgObjRef.current) return
scale.current = 1
position.current = { x: 0, y: 0 }
// 立即重绘
const currentImage = currentImageRef.current
drawImage(currentImgObjRef.current, currentImage?.coord, currentImage .suspectCode)
}
// 处理图片切换
const handleSwiperChange = async (idx) => {
// 切换图片时重置缩放和位置
scale.current = 1
position.current = { x: 0, y: 0 }
const currentImage = imgData.imgList[idx];
currentImageRef.current = currentImage
try {
const localImagePath = await loadBgImg(currentImage.url)
const img = canvasRef.current.createImage()
currentImgObjRef.current = img
img.onload = () => {
drawImage(img, currentImage.coord, currentImage.suspectCode)
}
img.onerror = () => {
console.error('切换图片加载失败')
}
img.src = localImagePath
} catch (error) {
console.error('切换图片失败:', error)
}
}
// 左滑
const canvasLeftClick = () => {
let idx = 0
if(!swiperCurrent) {
idx = imgList.length - 1
} else {
idx = swiperCurrent - 1
}
setSwiperCurrent(idx)
handleSwiperChange(idx)
}
// 右滑
const canvasRightClick = () => {
let idx = 0
if(swiperCurrent === imgList.length - 1) {
idx = 0
} else {
idx = swiperCurrent + 1
}
setSwiperCurrent(idx)
handleSwiperChange(idx)
}
return (
<View>
<Canvas
id="myCanvas"
type="2d"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchEnd}
onClick={handleClick}
/>
<View className='canvas-left' onClick={canvasLeftClick}>
<Image src='/img/left.png' className='canvas-arrow-icon'></Image>
</View>
<View className='canvas-right' onClick={canvasRightClick}>
<Image src='/img/right.png' className='canvas-arrow-icon'></Image>
</View>
</View>
)
#myCanvas {
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
}
.canvas-left {
width: 150px;
height: 150px;
position: fixed;
top: 50%;
left: 10px;
transform: translateY(-50%);
z-index: 10;
line-height: 150px;
text-align: left;
.canvas-arrow-icon {
width: 80px;
height: 80px;
}
}
.canvas-right {
width: 150px;
height: 150px;
position: fixed;
top: 50%;
right: 10px;
transform: translateY(-50%);
z-index: 10;
line-height: 150px;
text-align: right;
.canvas-arrow-icon {
width: 80px;
height: 80px;
}
}
以上就是全部代码
项目上线后还遇到一个问题:在微信小程序上看不到图片,经过排查是因为后端使用的图片的域名是新加的,需要在小程序后台设置downloadFile 的合法域名。