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

Flutter中多行TextField的自定义背景实现需求

Flutter 实现带独立行背景与行间距的多行 TextField

核心思路

原生TextField无法直接给每行设置独立背景,采用Stack堆叠方案实现需求:

  • 上层是透明样式的TextField,负责处理输入交互
  • 下层根据输入内容的每行位置,绘制带圆角的背景块,同时控制行间距

完整实现代码

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

class LineBackgroundTextField extends StatefulWidget {
  final String hintText;
  final TextStyle? textStyle;
  final Color lineBackgroundColor;
  final double lineSpacing;
  final double borderRadius;

  const LineBackgroundTextField({
    super.key,
    required this.hintText,
    this.textStyle,
    this.lineBackgroundColor = const Color(0xFFE8F3FF),
    this.lineSpacing = 8.0,
    this.borderRadius = 8.0,
  });

  @override
  State<LineBackgroundTextField> createState() => _LineBackgroundTextFieldState();
}

class _LineBackgroundTextFieldState extends State<LineBackgroundTextField> {
  final TextEditingController _controller = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  List<Rect> _lineRects = [];

  @override
  void initState() {
    super.initState();
    _controller.addListener(_updateLineRects);
    _focusNode.addListener(_updateLineRects);
  }

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

  void _updateLineRects() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final renderObject = context.findRenderObject() as RenderBox?;
      if (renderObject == null) return;

      final textPainter = TextPainter(
        text: TextSpan(
          text: _controller.text.isEmpty ? widget.hintText : _controller.text,
          style: widget.textStyle ?? const TextStyle(fontSize: 16, color: Colors.black),
        ),
        textDirection: TextDirection.ltr,
        maxLines: null,
      )..layout(maxWidth: renderObject.size.width);

      final lineMetrics = textPainter.computeLineMetrics();
      setState(() {
        _lineRects = lineMetrics.map((line) {
          return Rect.fromLTWH(
            0,
            line.baseline - line.ascent + (line == lineMetrics.first ? 0 : widget.lineSpacing),
            line.width,
            line.height,
          );
        }).toList();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 绘制每行背景
        if (_lineRects.isNotEmpty)
          Positioned.fill(
            child: CustomPaint(
              painter: LineBackgroundPainter(
                lineRects: _lineRects,
                color: widget.lineBackgroundColor,
                borderRadius: widget.borderRadius,
              ),
            ),
          ),
        // 透明输入框
        TextField(
          controller: _controller,
          focusNode: _focusNode,
          maxLines: null,
          expands: false,
          style: widget.textStyle ?? const TextStyle(fontSize: 16, color: Colors.black),
          decoration: InputDecoration(
            hintText: widget.hintText,
            hintStyle: widget.textStyle?.copyWith(color: Colors.grey) ?? const TextStyle(fontSize: 16, color: Colors.grey),
            border: InputBorder.none,
            contentPadding: EdgeInsets.symmetric(vertical: widget.lineSpacing / 2, horizontal: 8),
          ),
          inputFormatters: [
            FilteringTextInputFormatter.deny(RegExp(r'^\n')), // 禁止首行输入换行
          ],
        ),
      ],
    );
  }
}

// 自定义背景绘制器
class LineBackgroundPainter extends CustomPainter {
  final List<Rect> lineRects;
  final Color color;
  final double borderRadius;

  LineBackgroundPainter({
    required this.lineRects,
    required this.color,
    required this.borderRadius,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = color;
    for (final rect in lineRects) {
      canvas.drawRRect(
        RRect.fromRectAndRadius(rect, Radius.circular(borderRadius)),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant LineBackgroundPainter oldDelegate) {
    return oldDelegate.lineRects != lineRects ||
        oldDelegate.color != color ||
        oldDelegate.borderRadius != borderRadius;
  }
}

使用示例

在页面中直接调用自定义组件:

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("行背景输入框")),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: LineBackgroundTextField(
          hintText: "输入内容...",
          textStyle: const TextStyle(fontSize: 18, color: Colors.black87),
          lineBackgroundColor: const Color(0xFFE8F3FF),
          lineSpacing: 10.0,
          borderRadius: 12.0,
        ),
      ),
    );
  }
}

关键说明

  • 通过TextPainter计算文本的每行位置和尺寸,实现背景块与文本行精准对齐
  • 利用Stack分离背景绘制层与输入层,既保证输入交互正常,又实现自定义背景效果
  • 支持自定义背景色、行间距、圆角半径和文本样式,适配不同场景需求

内容的提问来源于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.07 11:43:10