记录 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;
  }
}
相关推荐
jakeswang43 分钟前
查询条件与查询数据的ajax拼装
前端·ajax
samuel91844 分钟前
axios取消重复请求
前端·javascript·vue.js
三天不学习1 小时前
JiebaAnalyzer 分词模式详解【搜索引擎系列教程】
前端·搜索引擎·jiebaanalyzer
滿1 小时前
Vue 3 中按照某个字段将数组分成多个数组
前端·javascript·vue.js
安分小尧1 小时前
[特殊字符] 使用 Handsontable 构建一个支持 Excel 公式计算的动态表格
前端·javascript·react.js·typescript·excel
好_快1 小时前
Lodash源码阅读-baseClone
前端·javascript·源码阅读
Double Point1 小时前
(三十一) Dart 中的网络请求教程:从知乎日报 API 获取数据
前端
excel1 小时前
webpack 核心编译器 十二 节
前端
好_快1 小时前
Lodash源码阅读-baseToString
前端·javascript·源码阅读