You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中双击图片缩放后无法滚动到点击位置的修复问询

React图片查看器双击缩放后滚动位置修正

问题描述

实现React图片查看器的双击缩放功能时,双击放大后无法将点击位置正确滚动到容器可视区域内,滚动位置计算偏差,导致点击部分偏移。

现有代码

组件代码

import clsx from 'clsx'
import React, { useRef, useState } from 'react'
import classes from './styles.module.scss'
import { IImageViewerProps } from '.'

const DOUBLE_CLICK_ZOOM_RATE = 0.5
const ZOOM_RATE = 0.1
const DOUBLE_CLICK_MAX = 2

export function ImageViewer({ src, title }: IImageViewerProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const ref = useRef<HTMLImageElement>(null)
  const [imgScale, setImgScale] = useState(1)
  const [doubleClickCount, setDoubleClickCount] = useState(0)
  const [isDragging, setIsDragging] = useState(false)
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 })

  const handleDoubleClick = (
    e: React.MouseEvent<HTMLImageElement, MouseEvent>
  ) => {
    e.stopPropagation()
    e.preventDefault()

    if (doubleClickCount < DOUBLE_CLICK_MAX) {
      const newScale = imgScale + DOUBLE_CLICK_ZOOM_RATE

      // After zooming, calculate the new scroll position to keep the clicked part visible
      if (containerRef.current && ref.current) {
        const containerRect = containerRef.current.getBoundingClientRect()
        const imageRect = ref.current.getBoundingClientRect()
        const x = (e.clientX - containerRect.left) / containerRect.width
        const y = (e.clientY - containerRect.top) / containerRect.height

        const newScrollLeft =
          x * (imageRect.width * newScale - containerRect.width)
        const newScrollTop =
          y * (imageRect.height * newScale - containerRect.height)

        console.log(newScrollLeft)
        containerRef.current.scrollLeft = 10
        containerRef.current.scrollTop = newScrollTop

        setImgScale(newScale)
        setDoubleClickCount((prevState) => prevState + 1)
      }
    } else {
      setImgScale(1)
      setDoubleClickCount(0)
    }
  }

  const handleMouseDown = (
    e: React.MouseEvent<HTMLImageElement, MouseEvent>
  ) => {
    e.stopPropagation()
    e.preventDefault()

    setIsDragging(true)
    setDragStart({ x: e.clientX, y: e.clientY })
  }

  const handleMouseMove = (
    e: React.MouseEvent<HTMLImageElement, MouseEvent>
  ) => {
    e.stopPropagation()
    e.preventDefault()

    if (isDragging && containerRef.current) {
      const deltaX = e.clientX - dragStart.x
      const deltaY = e.clientY - dragStart.y

      containerRef.current.scrollLeft -= deltaX
      containerRef.current.scrollTop -= deltaY

      setDragStart({ x: e.clientX, y: e.clientY })
    }
  }

  const handleMouseUp = (e: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
    e.stopPropagation()
    e.preventDefault()

    setIsDragging(false)
  }

  return (
    <div
      className={clsx(classes.imgContainer)}
      ref={containerRef}
       onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    >
      <img
        ref={ref}
        style={{
          width: `${imgScale * 100}%`
        }}
        src={src}
        alt={title}
        className={clsx(classes.imgContainer__image)}
        onDoubleClick={handleDoubleClick}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
      />
    </div>
  )
}

SCSS样式

.imgContainer{
  width: 100%;
  max-height: 30vh;
  overflow: scroll;

  &__image{
    width: 100%;
    height: auto;
    max-height: 30vh;
    margin: 0 auto;
  }
}

修正方案

关键问题分析

  1. 滚动位置计算逻辑错误:原代码使用容器宽度的比例计算滚动偏移,未考虑图片当前的滚动位置和缩放前后的坐标映射关系。
  2. DOM更新时机问题:原代码在图片缩放前设置滚动位置,此时图片尺寸尚未更新,导致滚动设置无效。

修改后的核心代码

新增状态保存双击位置,配合useEffect在图片缩放完成后调整滚动位置:

import clsx from 'clsx'
import React, { useRef, useState, useEffect } from 'react'
import classes from './styles.module.scss'
import { IImageViewerProps } from '.'

const DOUBLE_CLICK_ZOOM_RATE = 0.5
const ZOOM_RATE = 0.1
const DOUBLE_CLICK_MAX = 2

export function ImageViewer({ src, title }: IImageViewerProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const ref = useRef<HTMLImageElement>(null)
  const [imgScale, setImgScale] = useState(1)
  const [doubleClickCount, setDoubleClickCount] = useState(0)
  const [isDragging, setIsDragging] = useState(false)
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
  // 新增:保存双击时的鼠标位置
  const [lastDoubleClickPos, setLastDoubleClickPos] = useState<{x: number, y: number} | null>(null)

  const handleDoubleClick = (
    e: React.MouseEvent<HTMLImageElement, MouseEvent>
  ) => {
    e.stopPropagation()
    e.preventDefault()

    if (doubleClickCount < DOUBLE_CLICK_MAX) {
      const newScale = imgScale + DOUBLE_CLICK_ZOOM_RATE
      // 保存当前点击位置
      setLastDoubleClickPos({x: e.clientX, y: e.clientY})
      // 更新缩放比例
      setImgScale(newScale)
      setDoubleClickCount(prev => prev + 1)
    } else {
      setImgScale(1)
      setDoubleClickCount(0)
      setLastDoubleClickPos(null)
    }
  }

  // 新增:监听缩放比例变化,调整滚动位置
  useEffect(() => {
    if (!lastDoubleClickPos || !containerRef.current || !ref.current) return

    const container = containerRef.current
    const img = ref.current
    const containerRect = container.getBoundingClientRect()
    const {x: clientX, y: clientY} = lastDoubleClickPos

    // 计算点击位置相对于容器的偏移
    const clickOffsetX = clientX - containerRect.left
    const clickOffsetY = clientY - containerRect.top

    // 计算点击位置在当前缩放图片上的绝对坐标(包含滚动偏移)
    const clickAbsX = clickOffsetX + container.scrollLeft
    const clickAbsY = clickOffsetY + container.scrollTop

    // 计算缩放比例的变化倍数
    const scaleRatio = imgScale / (imgScale - DOUBLE_CLICK_ZOOM_RATE)

    // 缩放后点击位置的新绝对坐标
    const newClickAbsX = clickAbsX * scaleRatio
    const newClickAbsY = clickAbsY * scaleRatio

    // 计算新的滚动位置,让点击点保持在容器原位置
    const newScrollLeft = newClickAbsX - clickOffsetX
    const newScrollTop = newClickAbsY - clickOffsetY

    // 应用滚动位置
    container.scrollLeft = newScrollLeft
    container.scrollTop = newScrollTop

  }, [imgScale, lastDoubleClickPos])

  // 其余拖拽逻辑保持不变
  const handleMouseDown = (
    e: React.MouseEvent<HTMLImageElement, MouseEvent>
  ) => {
    e.stopPropagation()
    e.preventDefault()

    setIsDragging(true)
    setDragStart({ x: e.clientX, y: e.clientY })
  }

  const handleMouseMove = (
    e: React.MouseEvent<HTMLImageElement, MouseEvent>
  ) => {
    e.stopPropagation()
    e.preventDefault()

    if (isDragging && containerRef.current) {
      const deltaX = e.clientX - dragStart.x
      const deltaY = e.clientY - dragStart.y

      containerRef.current.scrollLeft -= deltaX
      containerRef.current.scrollTop -= deltaY

      setDragStart({ x: e.clientX, y: e.clientY })
    }
  }

  const handleMouseUp = (e: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
    e.stopPropagation()
    e.preventDefault()

    setIsDragging(false)
  }

  return (
    <div
      className={clsx(classes.imgContainer)}
      ref={containerRef}
      onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    >
      <img
        ref={ref}
        style={{
          width: `${imgScale * 100}%`
        }}
        src={src}
        alt={title}
        className={clsx(classes.imgContainer__image)}
        onDoubleClick={handleDoubleClick}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
      />
    </div>
  )
}

修改说明

  1. 保存双击位置:新增lastDoubleClickPos状态存储双击时的鼠标坐标,确保在图片缩放后能获取到正确的点击位置。
  2. 延迟滚动设置:使用useEffect监听imgScale变化,确保在图片缩放完成、DOM更新后再计算并设置滚动位置。
  3. 正确的坐标映射:
    • 计算点击位置在当前缩放图片上的绝对坐标(包含滚动偏移)
    • 根据缩放比例计算该点在新缩放级别下的绝对位置
    • 调整滚动位置,让点击点保持在容器中的原始位置,确保点击区域始终可见

内容的提问来源于stack exchange,提问作者Pouya Afraz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 09:52:34