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

Flutter中如何动态更新SelectableText组件的文本选中状态?

解决Flutter SelectableText动态更新选中状态不生效的问题

问题根源

你的代码中虽然更新了_currentSelection,但存在几个核心问题导致UI未同步:

  1. 未绑定选中状态到组件:SelectableText默认使用内部维护的选中状态,没有将你更新的_currentSelection传递给组件的selection参数,因此组件感知不到状态变化。
  2. 焦点丢失导致高亮消失:SelectableText的选中高亮需要组件处于焦点状态,滚动过程中可能失去焦点,导致视觉上看不到选中效果。
  3. 坐标转换与文本配置不匹配:文本位置计算时的坐标转换逻辑、TextPainter配置和SelectableText不一致,可能导致选中位置计算错误。

修复方案

以下是修改后的完整代码,关键修复点已标注:

class _TextDescriptionWithOffset extends State<TextDescriptionWithOffset> {
  GlobalKey _selectionWidgetKey = GlobalKey();
  TextSelection _currentSelection = TextSelection.collapsed(offset: 0);
  TextSpanGenerator? textSpanGenerator;
  String? temp;
  final FocusNode _focusNode = FocusNode(); // 新增:用于保持组件焦点

  @override
  void initState() {
    super.initState();
    _focusNode.requestFocus(); // 初始请求焦点,确保高亮可见
    textSpanGenerator = TextSpanGenerator(
      widget.content,
      widget.textObjects,
      searchString: widget.searchString ?? '',
      widget.textMarginWidth,
      TextSpanGeneratorMode.originalText,
    );
    temp = textSpanGenerator!.plainText;
    // 断言确保文本源一致,避免offset匹配错误
    assert(temp == widget.content, "文本源不一致会导致选中位置计算错误");
  }

  void _onPointerScroll(PointerScrollEvent event) {
    final RenderBox renderBox = _selectionWidgetKey.currentContext!.findRenderObject() as RenderBox;
    // 修正:将全局坐标转换为SelectableText的本地坐标
    Offset localPosition = renderBox.globalToLocal(event.position);

    TextPosition? textPosition = _getTextPosition(localPosition);
    if (textPosition != null) {
      setState(() {
        _currentSelection = TextSelection(
          baseOffset: _currentSelection.baseOffset,
          extentOffset: textPosition.offset,
        );
      });
      // 确保焦点始终保持,防止高亮消失
      if (!_focusNode.hasFocus) {
        _focusNode.requestFocus();
      }
    }
  }

  TextPosition? _getTextPosition(Offset localPosition) {
    final RenderBox renderBox = _selectionWidgetKey.currentContext!.findRenderObject() as RenderBox;
    final TextPainter textPainter = TextPainter(
      text: TextSpan(text: temp, style: TextStyle(color: Colors.black)),
      textDirection: TextDirection.ltr,
      textScaler: TextScaler.linear(1.0), // 与SelectableText配置一致
      textWidthBasis: TextWidthBasis.longestLine, // 与SelectableText配置一致
    )..layout(maxWidth: renderBox.size.width);

    // 边界检查:避免坐标越界导致的位置错误
    if (localPosition.dx < 0 || localPosition.dy < 0 || localPosition.dx > textPainter.width || localPosition.dy > textPainter.height) {
      return null;
    }
    return textPainter.getPositionForOffset(localPosition);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Focus(
          focusNode: _focusNode, // 绑定自定义焦点节点
          child: MouseRegion(
            onEnter: (_) => widget.onTextSelectionMode?.call(true),
            onExit: (_) => widget.onTextSelectionMode?.call(false),
            child: Listener(
              onPointerSignal: (event) {
                if (event is PointerScrollEvent) {
                  _onPointerScroll(event);
                }
              },
              child: SelectableText.rich(
                TextSpan(
                  text: widget.content,
                  style: TextStyle(color: Colors.black),
                ),
                key: _selectionWidgetKey,
                strutStyle: StrutStyle.disabled,
                style: TextStyle(color: Colors.black),
                enableInteractiveSelection: widget.interactive,
                cursorWidth: 0,
                textScaler: TextScaler.linear(1.0),
                textWidthBasis: TextWidthBasis.longestLine,
                selection: _currentSelection, // 关键:绑定自定义选中状态
                focusNode: _focusNode, // 绑定焦点节点
                onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? cause) {
                  setState(() {
                    _currentSelection = newSelection;
                  });
                  widget.onSelectionChanged?.call(newSelection.textInside(temp));
                },
              ),
            ),
          ),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _focusNode.dispose(); // 销毁焦点节点,避免内存泄漏
    super.dispose();
  }
}

关键修复点说明

  • 绑定selection参数:将_currentSelection传递给SelectableText的selection属性,让组件使用你维护的选中状态。
  • 使用FocusNode保持焦点:滚动过程中确保组件始终持有焦点,避免高亮因焦点丢失而消失。
  • 统一文本配置:TextPainter的textScaler、textWidthBasis与SelectableText保持一致,保证位置计算准确。
  • 修正坐标转换:使用renderBox.globalToLocal(event.position)将全局坐标转换为组件本地坐标,避免位置偏移。
  • 边界检查:防止越界坐标导致的无效TextPosition。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 09:25:18