2. RichText使用
文字后展开、隐藏功能实现
完整代码在文本末尾
2.1. 实现目标
-
当文本超过3行时,显示省略号并显示 ‘更多’ 按钮,点击更多展示完整文本;
-
完整文本末尾显示 ‘隐藏’ 按钮,点击隐藏,回到状态 1
-
多条数据互不影响
2.2. 基本实现思路:
-
判断文本是否超过最大行
maxLines
,可以通过TextPainter
获取小控件属性,再根据didExceedMaxLines
属性返回的布尔值进行判断。 -
判断完成后,因为要在三行文本内加入按钮,只能切割字符串,再使用
RichText
行内拼接的控件。
2.3. 完整流程
-
列表组件渲染
因为是多条数据互不影响,则是列表渲染
ListView
Expanded( child: ListView.builder( itemCount: data.length, itemBuilder: this._listViewModal, ), )
-
补充代码,完整基础样式
-
获取文本容器属性
/** * @name: getTextPainter * @description: 获取TextPainter小控件属性 * @param {*} * @return {*} */ getTextPainter(BuildContext context, String text, TextStyle style, double maxWidth, int maxLines) { double textScaleFactor = MediaQuery.of(context).textScaleFactor; final TextPainter textPainter = TextPainter( locale: WidgetsBinding.instance!.window.locale, text: TextSpan(text: text, style: style), textScaleFactor: textScaleFactor, maxLines: maxLines, textDirection: TextDirection.ltr) ..layout(minWidth: 0, maxWidth: maxWidth); return textPainter; }
-
根据文本属性切割字符串
final _textPainter = getTextPainter( context, item["synopsis_tc"], descStyle, textMaxWidth, maxRowNum); final moreText = _textPainter.didExceedMaxLines; if (moreText) { // 三行文本容器的宽高信息 final textSize = _textPainter.size; // 在容器范围内,显示的文本数量 final position = _textPainter .getPositionForOffset(Offset(textMaxWidth, textSize.height)); // 为按钮预留空间-3 final endOffset = position.offset - 3; // 截取默认展示的文本 showStr = item["synopsis_tc"].substring(0, endOffset); }
-
拼接字符串,添加点击事件
/** * @name: text * @description: 当前3行的文本内容 * @param {*} * @return {*} */ RichText txt = RichText( text: TextSpan( text: (moreText && !showMoreIds.contains(textId)) ? "$showStr..." : '${item["synopsis_tc"]}', style: descStyle, children: [ TextSpan( text: moreText ? (showMoreIds.contains(textId) ? '隐藏' : '更多') : '', style: txtBtnSty, recognizer: TapGestureRecognizer() ..onTap = () { _setShowType(textId); }, ) ]));
2.4 完整代码
/*
* @Description:分类页面
* @version:
* @Date: 2022-03-14 14:29:37
*/
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:myapp_one/static_data/category_data.dart';
import 'package:myapp_one/utils/singleton.dart';
double titleSize = 18.0;
double descSize = 16.0;
double paddingOth = 10.0;
double paddingBot = 15.0;
// 获取屏幕宽高-网上一搜就有
double maxWidth = Singleton().screenWidth;
double maxHeight = Singleton().screenHeight;
int maxRowNum = 3;
double boxMaxWidth = maxWidth - (paddingOth * 2);
double textMaxWidth = boxMaxWidth - (paddingOth * 2);
int descRowNum = textMaxWidth ~/ descSize;
TextStyle descStyle = TextStyle(
fontSize: descSize,
color: Colors.black87,
);
TextStyle txtBtnSty = TextStyle(
fontSize: descSize - 1,
color: Colors.blue,
);
/**
* @name: getTextPainter
* @description: 获取TextPainter小控件属性
* @param {*}
* @return {*}
*/
getTextPainter(BuildContext context, String text, TextStyle style,
double maxWidth, int maxLines) {
double textScaleFactor = MediaQuery.of(context).textScaleFactor;
final TextPainter textPainter = TextPainter(
locale: WidgetsBinding.instance!.window.locale,
text: TextSpan(text: text, style: style),
textScaleFactor: textScaleFactor,
maxLines: maxLines,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: maxWidth);
return textPainter;
}
/**
* @name: CategoryPage
* @description: 主体class
* @param {*}
* @return {*}
*/
class CategoryPage extends StatefulWidget {
// 数据源头,可以根据字段编写数据
final content = categoryData["content"];
@override
_CategoryPageState createState() => _CategoryPageState();
}
/**
* @name: _CategoryPageState
* @description: 次级主体-主要实现的部分,包括state状态改变
* @param {*}
* @return {*}
*/
class _CategoryPageState extends State<CategoryPage> {
List data = [];
String nowDate = '';
List showMoreIds = [];
_setShowType(textId) {
setState(() {
bool boolRes = this.showMoreIds.contains(textId);
if (boolRes) {
this.showMoreIds.remove(textId);
} else {
this.showMoreIds.add(textId);
}
});
}
/**
* @name: _listViewModal
* @description: 列表渲染
* @param {*}
* @return {*}
*/
Widget _listViewModal(context, index) {
Map item = data[index];
String showStr = '';
String textId = "$index-${item['episode_no']}";
final _textPainter = getTextPainter(
context, item["synopsis_tc"], descStyle, textMaxWidth, maxRowNum);
final moreText = _textPainter.didExceedMaxLines;
if (moreText) {
// 三行文本容器的宽高信息
final textSize = _textPainter.size;
// 在容器范围内,显示的文本数量
final position = _textPainter
.getPositionForOffset(Offset(textMaxWidth, textSize.height));
// 为按钮预留空间-3
final endOffset = position.offset - 3;
// 截取默认展示的文本
showStr = item["synopsis_tc"].substring(0, endOffset);
}
/**
* @name: text
* @description: 当前3行的文本内容
* @param {*}
* @return {*}
*/
RichText txt = RichText(
text: TextSpan(
text: (moreText && !showMoreIds.contains(textId))
? "$showStr..."
: '${item["synopsis_tc"]}',
style: descStyle,
children: [
TextSpan(
text: moreText ? (showMoreIds.contains(textId) ? '隐藏' : '更多') : '',
style: txtBtnSty,
recognizer: TapGestureRecognizer()
..onTap = () {
_setShowType(textId);
},
)
]));
return Container(
margin:
EdgeInsets.fromLTRB(paddingOth, paddingOth, paddingOth, paddingBot),
padding: EdgeInsets.all(paddingOth),
decoration: BoxDecoration(
border: Border.all(color: Color.fromRGBO(0, 0, 0, 0.1), width: 1),
borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${item["start_datetime"]}',
style: TextStyle(
fontSize: titleSize,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 5),
Text(
item["title_tc"],
style: TextStyle(
fontSize: titleSize,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 5),
Stack(
children: [
txt,
],
),
SizedBox(height: 5),
],
),
);
}
@override
Widget build(BuildContext context) {
// 获取数据,可以替换成自身数据
nowDate = "2022-03-18";
data = this.widget.content[nowDate] ?? [];
return Scaffold(
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: data.length,
itemBuilder: this._listViewModal,
),
)
],
),
);
}
}