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

如何让React-Konva的Transformer自动匹配文本总行高?

React-Konva文本组件Transformer宽度缩小时高度自动适配方案

问题核心:通过Transformer缩小文本组件宽度时,文本自动换行,但TextObject和Transformer的高度未同步更新,导致换行后的文本被遮挡。

问题根源

你手动维护了textHeight状态,在Transformer变换时仅基于缩放比例计算高度,没有同步Konva.Text根据文本内容和当前宽度自动计算的真实高度。Konva.Text在宽度变化后会重新计算高度,但这个值没有传递到TextObject和Transformer的属性中。

解决方案

  1. 依赖Konva.Text自动计算高度:移除手动设置的height属性,让Konva根据文本内容和宽度自动计算高度。
  2. 实时同步真实高度:在文本内容、宽度变化时,获取Konva.Text的height()值,同步到状态并更新Transformer高度。
  3. 修正Transformer变换逻辑:在onTransform中调整宽度后,立即触发真实高度的同步,不再手动计算高度。

修改后的完整组件代码

import React, { useState, useRef, useEffect } from 'react';
import { Text as TextObject, Transformer } from 'react-konva';
import { Html } from 'react-konva-utils';
import { BsFillCheckSquareFill } from 'react-icons/bs';
import { usePropertiesContext } from '@/hooks/PropertiesContent';

interface TextProps {
  x: number;
  y: number;
  fontSize: number;
  fill: string;
  fontVariant: string;
  align: undefined;
  fontFamily: string;
  opacity: number;
  rotation: number;
  isSelected: boolean;
  boundingBoxWidth: number;
  boundingBoxHeight: number;
  onSelect: () => void;
  onChange: ({ x, y }: { x: number; y: number }) => void;
}

const Text: React.FC<TextProps> = ({
  x,
  y,
  fontSize,
  fill,
  fontVariant,
  align,
  fontFamily,
  opacity,
  rotation,
  isSelected,
  boundingBoxWidth,
  boundingBoxHeight,
  onSelect,
  onChange,
}) => {
  const { settextRotation } = usePropertiesContext();
  const [text, setText] = useState('Your Text');
  const [edit, setEdit] = useState(false);
  const [textWidth, setTextWidth] = useState(120);
  const [realHeight, setRealHeight] = useState(20);
  const [rotationAngle, setRotationAngle] = useState(rotation);
  const shapeRef = useRef<any>();
  const trRef = useRef<Transformer>();

  useEffect(() => {
    if (!edit && isSelected) {
      trRef.current?.nodes([shapeRef.current]);
      trRef.current.getLayer().batchDraw();
    }
  }, [edit, isSelected]);

  useEffect(() => {
    const textNode = shapeRef.current;
    if (textNode) {
      const newHeight = textNode.height();
      setRealHeight(newHeight);
      // 同步更新Transformer高度
      if (trRef.current && isSelected && !edit) {
        trRef.current.setAttrs({ height: newHeight });
        trRef.current.getLayer().batchDraw();
      }
    }
    setRotationAngle(rotation);
  }, [rotation, textWidth, text, isSelected, edit]);

  const updateSizeAndPosition = (newTextWidth: number) => {
    setTextWidth(newTextWidth);
    shapeRef.current?.width(newTextWidth);
    
    if (trRef.current && isSelected && !edit) {
      trRef.current.setAttrs({ width: newTextWidth });
      trRef.current.getLayer().batchDraw();
    }
  };

  return (
    <React.Fragment>
      <Html
        divProps={{
          style: {
            position: 'absolute',
            top: `${y}px`,
            left: `${x}px`,
            transform: `rotate(${rotationAngle}deg)`,
          },
        }}
      >
        {edit ? (
          <div className="flex items-start gap-x-2">
            <textarea
              value={text}
              onChange={(e) => {
                setText(e.target.value);
                const newWidth = e.target.scrollWidth;
                updateSizeAndPosition(newWidth);
              }}
              style={{
                width: `${textWidth}px`,
                minHeight: `${realHeight}px`,
                backgroundColor: 'transparent',
                color: fill,
                textAlign: align,
                fontSize: fontSize,
                fontFamily: fontFamily,
                fontVariant: fontVariant,
                opacity: opacity,
                lineHeight: 1,
                appearance: 'none',
                overflow: 'hidden',
                resize: 'none',
                border: 'none',
                outline: 'none',
              }}
            />
            <button onClick={() => setEdit(false)}>
              <BsFillCheckSquareFill className="text-blue-500 hover:text-blue-600 text-lg shadow-sm" />
            </button>
          </div>
        ) : null}
      </Html>
      {!edit ? (
        <TextObject
          onClick={onSelect}
          onTap={onSelect}
          onDblClick={() => setEdit(true)}
          onDblTap={() => setEdit(true)}
          text={text}
          ref={shapeRef}
          fontSize={fontSize}
          fill={fill}
          x={x}
          y={y}
          opacity={opacity}
          width={textWidth}
          fontFamily={fontFamily}
          fontVariant={fontVariant}
          align={align}
          rotation={rotation}
          draggable
          onDragEnd={(e) => {
            onChange({
              x: e.target.x(),
              y: e.target.y(),
            });
          }}
          onDragMove={(e) => {
            if (e.target.x() - 10 < 0) e.target.x(10);
            if (e.target.y() - 10 < 0) e.target.y(10);
            if (e.target.x() + e.target.width() + 10 > boundingBoxWidth)
              e.target.x(boundingBoxWidth - e.target.width() - 10);
            if (e.target.y() + e.target.height() + 10 > boundingBoxHeight)
              e.target.y(boundingBoxHeight - e.target.height() - 10);
          }}
        />
      ) : null}

      {isSelected && !edit && (
        <Transformer
          x={x}
          y={y}
          width={textWidth}
          height={realHeight}
          rotateAnchorOffset={30}
          anchorCornerRadius={90}
          padding={5}
          ref={trRef}
          boundBoxFunc={(oldBox, newBox) => {
            if (newBox.width < 5 || newBox.height < 5) {
              return oldBox;
            }
            return newBox;
          }}
          onTransform={(e) => {
            const node = e.target;
            const newRotation = Math.round(node.rotation());
            settextRotation(newRotation);
            
            const newWidth = Math.max(node.width() * node.scaleX(), 20);
            node.setAttrs({
              width: newWidth,
              scaleX: 1,
              scaleY: 1,
            });
            
            setTextWidth(newWidth);
            onChange({
              x: e.target.x(),
              y: e.target.y(),
            });
          }}
        />
      )}
    </React.Fragment>
  );
};

export default Text;

关键修改说明

  • 移除了手动维护的textHeight和transformerHeight状态,完全依赖Konva.Text自动计算的真实高度。
  • 在useEffect中监听文本内容、宽度、选中状态等变化,实时同步真实高度到realHeight,并更新Transformer的高度属性。
  • Transformer的onTransform逻辑不再手动计算高度,仅调整宽度,高度由Konva自动处理。
  • 编辑模式下的textarea使用minHeight绑定真实高度,确保编辑区域与渲染区域高度一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 16:24:51