如何让React-Konva的Transformer自动匹配文本总行高?
React-Konva文本组件Transformer宽度缩小时高度自动适配方案
问题核心:通过Transformer缩小文本组件宽度时,文本自动换行,但TextObject和Transformer的高度未同步更新,导致换行后的文本被遮挡。
问题根源
你手动维护了textHeight状态,在Transformer变换时仅基于缩放比例计算高度,没有同步Konva.Text根据文本内容和当前宽度自动计算的真实高度。Konva.Text在宽度变化后会重新计算高度,但这个值没有传递到TextObject和Transformer的属性中。
解决方案
- 依赖Konva.Text自动计算高度:移除手动设置的
height属性,让Konva根据文本内容和宽度自动计算高度。 - 实时同步真实高度:在文本内容、宽度变化时,获取Konva.Text的
height()值,同步到状态并更新Transformer高度。 - 修正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
相关产品推荐
相关产品推荐

