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

Flutter实现文本框选中内容变色及多段样式保留方案

解决方案

你当前代码使用SelectableText.rich,它仅支持文本选择,不支持编辑。要实现可编辑+富文本样式修改,需要改用TextField配合自定义的TextEditingController来维护富文本内容和样式。

核心思路

  1. 用TextEditingController管理文本内容,通过自定义子类维护富文本样式列表
  2. 监听文本选择变化,实时记录选中范围
  3. 修改样式时,重构富文本的TextSpan列表,保留未选中区域的原有样式,更新选中区域的样式
  4. 确保文本编辑时,样式能跟随内容同步更新

修改后的完整代码

import 'dart:math';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late _RichTextEditingController _controller;
  (int startIndex, int endIndex) _selection = (0, 0);

  @override
  void initState() {
    super.initState();
    const initialText = 'Lorem ipsum dolor sit amet, '
        'consectetur adipiscing elit, '
        'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '
        'Ut enim ad minim veniam, '
        'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. '
        'Duis aute irure dolor in reprehenderit '
        'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
        'Excepteur sint occaecat cupidatat non proident, '
        'sunt in culpa qui officia deserunt mollit anim id est laborum.';
    // 初始化富文本控制器,默认所有文本使用空样式
    _controller = _RichTextEditingController(
      text: initialText,
      spans: [TextSpan(text: initialText, style: const TextStyle())],
    );
    _controller.addListener(_updateSelection);
  }

  void _updateSelection() {
    final selection = _controller.selection;
    if (!selection.isValid) return;
    setState(() {
      _selection = (min(selection.start, selection.end), max(selection.start, selection.end));
    });
  }

  void highlight(Color color) {
    final (start, end) = _selection;
    if (start == end) return;

    final currentText = _controller.text;
    final List<TextSpan> newSpans = [];
    int currentPos = 0;

    // 拆分现有span,找到选中区域对应的位置,重构样式
    for (final span in _controller.spans) {
      final spanLength = span.text?.length ?? 0;
      final spanEnd = currentPos + spanLength;

      // 情况1:当前span在选中区域之前
      if (spanEnd <= start) {
        newSpans.add(span);
      }
      // 情况2:当前span与选中区域重叠
      else if (currentPos < end && spanEnd > start) {
        // 重叠前的部分
        if (currentPos < start) {
          newSpans.add(TextSpan(
            text: span.text?.substring(0, start - currentPos),
            style: span.style,
          ));
        }
        // 重叠部分(应用新样式)
        final overlapStart = max(currentPos, start);
        final overlapEnd = min(spanEnd, end);
        newSpans.add(TextSpan(
          text: span.text?.substring(overlapStart - currentPos, overlapEnd - currentPos),
          style: TextStyle(
            color: color.computeLuminance() > 0.25 ? Colors.black : Colors.white,
            backgroundColor: color,
          ),
        ));
        // 重叠后的部分
        if (spanEnd > end) {
          newSpans.add(TextSpan(
            text: span.text?.substring(end - currentPos),
            style: span.style,
          ));
        }
      }
      // 情况3:当前span在选中区域之后
      else if (currentPos >= end) {
        newSpans.add(span);
      }

      currentPos = spanEnd;
    }

    setState(() {
      // 更新控制器的span列表
      _controller.spans = newSpans;
      // 通知UI刷新
      _controller.notifyListeners();
      // 恢复选中状态
      _controller.selection = TextSelection(baseOffset: start, extentOffset: end);
    });
  }

  late final Widget highlightButtons = Padding(
    padding: const EdgeInsets.only(top: 50, bottom: 25),
    child: Row(children: [
      for (final color in [
        Colors.red,
        Colors.orange,
        Colors.amber,
        Colors.yellow,
        Colors.lime,
        Colors.green,
        Colors.cyan,
        Colors.blue,
        Colors.indigo,
        Colors.purple,
      ])
        _HighlightButton(color: color, onPressed: () => highlight(color))
    ]),
  );

  late final Widget resetButton = OutlinedButton(
    style: OutlinedButton.styleFrom(foregroundColor: Colors.grey),
    onPressed: () {
      const initialText = 'Lorem ipsum dolor sit amet, '
          'consectetur adipiscing elit, '
          'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '
          'Ut enim ad minim veniam, '
          'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. '
          'Duis aute irure dolor in reprehenderit '
          'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
          'Excepteur sint occaecat cupidatat non proident, '
          'sunt in culpa qui officia deserunt mollit anim id est laborum.';
      setState(() {
        _controller = _RichTextEditingController(
          text: initialText,
          spans: [TextSpan(text: initialText, style: const TextStyle())],
        );
        _controller.addListener(_updateSelection);
        _selection = (0, 0);
      });
    },
    child: const Text('reset'),
  );

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        margin: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width / 16),
        alignment: Alignment.center,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: _controller,
              maxLines: null,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                hintText: '输入或编辑文本...',
              ),
            ),
            highlightButtons,
            resetButton,
          ],
        ),
      ),
    );
  }
}

// 自定义富文本编辑控制器,维护TextSpan样式列表
class _RichTextEditingController extends TextEditingController {
  _RichTextEditingController({required String text, required this.spans})
      : super(text: text);

  List<TextSpan> spans;

  @override
  TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) {
    // 合并全局样式和span样式
    return TextSpan(
      style: style,
      children: spans.map((span) => TextSpan(
        text: span.text,
        style: style?.merge(span.style) ?? span.style,
      )).toList(),
    );
  }
}

class _HighlightButton extends StatelessWidget {
  const _HighlightButton({required this.color, required this.onPressed});
  final Color color;
  final void Function() onPressed;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.all(4),
        child: ElevatedButton(
          style: OutlinedButton.styleFrom(backgroundColor: color),
          onPressed: onPressed,
          child: const SizedBox.shrink(),
        ),
      ),
    );
  }
}

关键修改说明

  1. 替换SelectableText为TextField:TextField支持文本编辑,配合自定义控制器实现富文本渲染
  2. 自定义_RichTextEditingController:重写buildTextSpan方法,通过维护spans列表来管理不同区域的文本样式
  3. 精细化样式拆分:高亮时拆分现有样式区间,确保未选中区域的样式被完整保留,仅修改选中部分的样式
  4. 实时选择监听:通过控制器的addListener方法实时获取当前文本选中范围

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 02:54:53