记录 flutter 文本内容展示过长优化

对于过长的文本, 大部分都是超长展示直接显示省略号。

但是! 如果ui既要省略号、又要根据展示内容去变化字号、还好换行,弹窗还要提示框、提示框箭头还要指向超出的"..." 当时听上去,一口就答应了, 自此坠入search的深渊无法自拔。

整理一下要求:

  1. 换行,文本超出省略号
  2. 点击之后弹出弹框展示全部内容
  3. 根据内容缩小放大字体, 比如: 两行的时候, 字体大一点, 一行的时候字体更大一点

需求分析:

  • 超出展示省略号, 这个好整,给Text组件设置 overflow: TextOverflow.ellipsis,再加一下maxLine: 3 (我这边最多展示三行)
flutter 复制代码
   Text(
      name,
      maxline: 3,
      overflow: TextOverflow.ellipsis
   )
  • 点击展示弹框这个也好整
  1. 直接把Widget弹框写好, 到时候在需要的地方直接弹出就行了
flutter 复制代码
        Stack(
          children: [
            Positioned(
              // bottom: -80.rpx,
              left: widget.left,
              top: alertTop,
              child: Container(
                padding: EdgeInsets.only(left: 6.rpx, right: 6.rpx),
                alignment: Alignment.center,
                constraints: widget.isRenji
                    ? widget.name.length > 30
                        ? BoxConstraints(
                            minWidth: 120.rpx,
                            maxWidth: 644.rpx,
                            minHeight: 40.rpx)
                        : BoxConstraints(
                            minWidth: 120.rpx,
                            maxWidth: 496.rpx,
                            minHeight: 40.rpx)
                    : BoxConstraints(
                        minWidth: 120.rpx,
                        maxWidth: 644.rpx,
                        minHeight: 40.rpx),
                decoration: BoxDecoration(
                  color: const Color.fromRGBO(0, 0, 0, 0.6),
                  borderRadius: BorderRadius.all(Radius.circular(10.rpx)),
                ),
                child: Stack(
                  alignment: Alignment.center,
                  fit: StackFit.passthrough,
                  clipBehavior: Clip.none,
                  children: [
                    Text(
                      widget.name,
                      style: Commons.positionText(),
                    ),
                  ],
                ),
              ),
            ),
            Positioned(
              // top: widget.name.length > 8 ? -12.rpx : -12.rpx,
              // top: widget.name.length > 8 ? -12.rpx : -12.rpx,
              top: alertTop - 12.rpx,
              left: alertLeft,
              child: Container(
                width: 0.rpx,
                height: 0.rpx,
                clipBehavior: Clip.antiAlias,
                decoration: BoxDecoration(
                  border: Border(
                    left: BorderSide(
                      color: Colors.transparent,
                      width: 13.rpx,
                    ),
                    right: BorderSide(
                      color: Colors.transparent,
                      width: 13.rpx,
                    ),
                    top: BorderSide(
                      color: const Color.fromRGBO(0, 0, 0, 0.6),
                      width: 12.rpx,
                    ),
                    bottom: BorderSide(
                      color: Colors.transparent,
                      width: 12.rpx,
                    ),
                  ),
                ),
              ),
            )
          ],
        )
  1. 弹窗方式,直接使用 OverlayEntry 这个组件
flutter 复制代码
    _overlayEntry = OverlayEntry(
      builder: (context) {
        return "上一步写好的Widget";
      },
    );
   弹出
   Overlay.of(context)!.insert(_overlayEntry!);
   关闭
   _overlayEntry.remove()

为了防止弹框弹出之后,由于....原因, 要做一下弹窗删除的处理, 我是在弹出的时候添加了 _overlayEntry.remove(),之前的弹窗没有关闭,又弹出新的弹窗。两个弹框都在页面上。 老板看了都会"笑嘻嘻"

  1. 下面就是要处理这个弹窗展示的箭头指向问题了 直接使用容器右下角偏移量
flutter 复制代码
     RenderObject? renderObject = boxKey.currentContext?.findRenderObject();
    if (renderObject != null) {
      RenderBox renderBox = renderObject as RenderBox;
      Size size = renderBox.size;
      Offset position = renderBox.localToGlobal(Offset.zero);
      alertTop = position.dy + size.height + 10.rpx;
      alertLeft = position.dx + size.width - 25.rpx;
    }
    注意: 我这个箭头的Widget并没有和弹框写一块, 是基于全局定位的
   
  • 根据内容长度去判断展示什么字号,嗯.... 这个... 。 纠结了半天, 最终还是问deepseek。 给我推荐了一个AutoSizeText 这个还挺好用和Text文本用法极其相似。
flutter 复制代码
 AutoSizeText(
    key: boxKey,   
    textKey: textKey, // 如果要读取文本的行数之类属性, 要用这个key
    // onResized: (sizeInfo) { 
    //   // 正确拼写
    //   print('调整后的字体大小: ${sizeInfo?.fontSize}');
    // },
    names,
    overflow: TextOverflow.ellipsis,
    stepGranularity: 1.rpx,
    minFontSize: 33.rpx,
    maxLines: names.length > 15 ? 3 : 1,
    maxFontSize: widget.style.fontSize ?? 118.rpx,
    style: TextStyle(
        fontFamily: widget.style.fontFamily,
        fontWeight: widget.style.fontWeight,
        fontStyle: widget.style.fontStyle,
        color: widget.style.color,
        // height: widget.style.height,
        fontSize: widget.style.fontSize),
  )

问题来了! 如何能获取到它自动调整字体大小呢! 问deepseek, 他说有这个属性, 我用的是最新版本没有发现。 文档中也没有查到。 那么怎么办呢。 上边代码中有一个 textKey 可以通过这个来获取widget 的style属性

flutter 复制代码
      final RenderParagraph renderParagraph =
        textKey.currentContext?.findRenderObject() as RenderParagraph;
    final textStyle = renderParagraph.text.style;

拿到属性之后就可以随意的textStyle.fontSize了

还有个问题! 这样做不管文本有没有超出, 点击都会弹出提示框

上边获取到了文本fontSize的大小, 可以使用TextPainter,样式直接取用渲染出来Text Wiget的样式。

flutter 复制代码
    final textSpan = TextSpan(text: widget.name, style: textStyle);
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
      maxLines: 3,
      ellipsis: '...',
    );
    // 约束宽度与实际渲染一致
    textPainter.layout(maxWidth: renderBox.size.width);
    bool isOver = textPainter.didExceedMaxLines;

这样通过didExceedMaxlines判断文本是否超过自己设置的maxline行数了。

分析完毕! 贴一下全部的代码

flutter 复制代码
import 'dart:async';

import 'package:auto_size_text/auto_size_text.dart';
import 'package:bed_side/style/commons.dart';
import 'package:flutter/material.dart';
import "package:bed_side/utils/size_extension.dart";
import 'package:flutter/rendering.dart';

class NameTextFlow extends StatefulWidget {
  final String name;
  final TextStyle style;
  final double left;
  const NameTextFlow({
    Key? key,
    required this.name,
    required this.style,
    this.left = 60,
  }) : super(key: key);
  @override
  State<NameTextFlow> createState() => NameTextFlowState();
}

class NameTextFlowState extends State<NameTextFlow> {
  bool showName = true;
  Timer? cutTimer;
  OverlayEntry? _overlayEntry;
  GlobalKey boxKey = GlobalKey();
  GlobalKey textKey = GlobalKey();
  double alertTop = 0.0;
  double alertLeft = 0.0;
  TextSpan textSpan = TextSpan();
  cutDownName() {
    cutTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
      timer.cancel();
      setState(() {
        showName = false;
      });
    });
  }

  toggleName() {
    setState(() {
      showName = !showName;
      if (showName) {
        // cutDownName();
        alertTip();
      } else {
        deletetTip();
      }
    });
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    cutTimer?.cancel();
    deletetTip();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    String names = widget.name;
    return Stack(
      fit: StackFit.passthrough,
      clipBehavior: Clip.none,
      children: [
        GestureDetector(
          onTap: () {
            if (!checkTextOver()) return;
            RenderObject? renderObject =
                boxKey.currentContext?.findRenderObject();
            if (renderObject != null) {
              RenderBox renderBox = renderObject as RenderBox;
              Size size = renderBox.size;
              Offset position = renderBox.localToGlobal(Offset.zero);
              alertTop = position.dy + size.height + 10.rpx;
              alertLeft = position.dx + size.width - 25.rpx;
            }
            toggleName();
          },
          child: Container(
              constraints: BoxConstraints(minWidth: 300.rpx, maxWidth: 454.rpx),
              child: AutoSizeText(
                key: boxKey,
                textKey: textKey,
                softWrap: true,
                textAlign: TextAlign.left,
                names,
                overflow: TextOverflow.ellipsis,
                stepGranularity: 1.rpx,
                minFontSize: 33.rpx,
                maxLines: names.length > 15 ? 3 : 1,
                maxFontSize: widget.style.fontSize ?? 118.rpx,
                style: TextStyle(
                    fontFamily: widget.style.fontFamily,
                    fontWeight: widget.style.fontWeight,
                    fontStyle: widget.style.fontStyle,
                    color: widget.style.color,
                    fontSize: widget.style.fontSize),
              )
              ),
        ),
      ],
    );
  }

  /// 弹出弹框
  void alertTip() {
    if (_overlayEntry != null) {
      _overlayEntry!.remove();
    }
    _overlayEntry = OverlayEntry(
      builder: (context) {
        return Stack(
          children: [
            Positioned(
              // bottom: -80.rpx,
              left: widget.left,
              top: alertTop,
              child: Container(
                padding: EdgeInsets.only(left: 6.rpx, right: 6.rpx),
                alignment: Alignment.center,
                constraints: widget.isRenji
                    ? widget.name.length > 30
                        ? BoxConstraints(
                            minWidth: 120.rpx,
                            maxWidth: 644.rpx,
                            minHeight: 40.rpx)
                        : BoxConstraints(
                            minWidth: 120.rpx,
                            maxWidth: 496.rpx,
                            minHeight: 40.rpx)
                    : BoxConstraints(
                        minWidth: 120.rpx,
                        maxWidth: 644.rpx,
                        minHeight: 40.rpx),
                decoration: BoxDecoration(
                  color: const Color.fromRGBO(0, 0, 0, 0.6),
                  borderRadius: BorderRadius.all(Radius.circular(10.rpx)),
                ),
                child: Stack(
                  alignment: Alignment.center,
                  fit: StackFit.passthrough,
                  clipBehavior: Clip.none,
                  children: [
                    Text(
                      widget.name,
                      style: Commons.positionText(),
                    ),
                  ],
                ),
              ),
            ),
            Positioned(
              // top: widget.name.length > 8 ? -12.rpx : -12.rpx,
              // top: widget.name.length > 8 ? -12.rpx : -12.rpx,
              top: alertTop - 12.rpx,
              left: alertLeft,
              child: Container(
                width: 0.rpx,
                height: 0.rpx,
                clipBehavior: Clip.antiAlias,
                decoration: BoxDecoration(
                  border: Border(
                    left: BorderSide(
                      color: Colors.transparent,
                      width: 13.rpx,
                    ),
                    right: BorderSide(
                      color: Colors.transparent,
                      width: 13.rpx,
                    ),
                    top: BorderSide(
                      color: const Color.fromRGBO(0, 0, 0, 0.6),
                      width: 12.rpx,
                    ),
                    bottom: BorderSide(
                      color: Colors.transparent,
                      width: 12.rpx,
                    ),
                  ),
                ),
              ),
            )
          ],
        );
      },
    );
    Overlay.of(context)!.insert(_overlayEntry!);
  }

  /// 删除弹框
  void deletetTip() {
    if (_overlayEntry == null) return;
    _overlayEntry!.remove();
    _overlayEntry = null;
  }

  // 计算省略号的位置
  bool checkTextOver() {
    final RenderParagraph renderParagraph =
        textKey.currentContext?.findRenderObject() as RenderParagraph;
    final textStyle = renderParagraph.text.style;
    final renderBox = boxKey.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) return false;
    final textSpan = TextSpan(text: widget.name, style: textStyle);
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
      maxLines: 3,
      ellipsis: '...',
    );
    // 约束宽度与实际渲染一致
    textPainter.layout(maxWidth: renderBox.size.width);
    final lineMetrics = textPainter.computeLineMetrics();
    switch (lineMetrics.length) {
      case 1:
        break;
      default:
    }
    // 检查是否真的被截断
    return textPainter.didExceedMaxLines;
  }
}
相关推荐
前端之虎陈随易11 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
一路向北he11 小时前
字节钢铁军团--“提供情境,而非控制”
java·开发语言·前端
kyriewen11 小时前
豆包和千问同时关了智能体,我用它们搭的 3 个自动化全废了——迁移方案整理
前端·javascript·ai编程
前端一小卒12 小时前
我用 TypeScript 从零手写了一个 Claude Code,然后发现它的核心只有 30 行
前端·agent
GitLqr12 小时前
Flutter 3.44 插件内置 Kotlin (KGP) 双向兼容适配指南
android·flutter·dart
大圣编程13 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang13 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆14 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜14 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
负责的蛋挞16 小时前
异步HttpModule的实现方式
java·服务器·前端