この記事は Flutter #2 Advent Calendar 2020 の9日目の記事です。
はじめに
(執筆が遅くなりすみません🙇♂️💦)
株式会社アトラエで普段は yenta のAndroidアプリを書いています。
また、最近は他事業部でFlutterでAndroidアプリの開発も兼任しております。
やったこと
以下のGIFのようなFacebookなどでみる「もっと見る」「閉じる」を実装しました。
ポイント
zennで @hayabusabusa さんが似たような記事を書いてらっしゃったので、こちら から勉強させていただきました🙇♂️
基本的には、上記の記事の内容のように、 textPainter.didExceedMaxLines
によって trimLines
よりも文章が多いか少ないかを判別し、それに応じてウィジェットを切り替えたりします。
TextSpan textSpan;
if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
text: _readMore
? widget.text.substring(0, endIndex)
: widget.text,
style: DefaultTextStyle.of(context).style,
children: _readMore
? <TextSpan>[
ellipsizeTextSp,
readMoreOrLessTextSp,
]
: <TextSpan>[
readMoreOrLessTextSp,
],
);
} else {
textSpan = textSpan = TextSpan(
text: widget.text,
style: DefaultTextStyle.of(context).style,
);
}
ボタンではなく、文章中に文字を表示するところのポイントですが、 「もっと見る」のtextPainterのwidth
と textのtextPainterのwidth
の差を比較し、「もっと見る」のテキストを追加する場合の元のテキストのendIndexを取得し、文章をカットすることです。
final pos = textPainter.getPositionForOffset(Offset(
textSize.width - readMoreOrLessTextSize.width,
textSize.height,
));
final endIndex = textPainter.getOffsetBefore(pos.offset);
全文
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Read More Text App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Read More Text'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: ExpandableText(
'aaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbb\nccccccccccccccc\ndddddddddddddddd\neeeeeeeeeeee'),
),
);
}
}
class ExpandableText extends StatefulWidget {
const ExpandableText(
this.text, {
Key key,
this.trimLines = 2,
}) : assert(text != null),
super(key: key);
final String text;
final String ellipsizeText = '...';
final String readMoreText = ' もっと見る';
final String readLessText = ' 閉じる';
final int trimLines;
@override
ExpandableTextState createState() => ExpandableTextState();
}
class ExpandableTextState extends State<ExpandableText> {
bool _readMore = true;
void _onTapReadMoreOrLess() {
setState(() => _readMore = !_readMore);
}
final colorClickableText = Color(0xFF00ADFE);
final textColor = Colors.black;
@override
Widget build(BuildContext context) {
final readMoreOrLessTextSp = TextSpan(
text: _readMore ? widget.readMoreText : widget.readLessText,
style: TextStyle(color: colorClickableText),
recognizer: TapGestureRecognizer()..onTap = _onTapReadMoreOrLess,
);
final ellipsizeTextSp = TextSpan(
text: widget.ellipsizeText,
style: DefaultTextStyle.of(context).style,
);
final textSp = TextSpan(
text: widget.text,
style: DefaultTextStyle.of(context).style,
);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
assert(constraints.hasBoundedWidth);
final textPainter = TextPainter(
text: readMoreOrLessTextSp,
textDirection: TextDirection.ltr,
maxLines: widget.trimLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final readMoreOrLessTextSize = textPainter.size;
textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;
final pos = textPainter.getPositionForOffset(Offset(
textSize.width - readMoreOrLessTextSize.width,
textSize.height,
));
final endIndex = textPainter.getOffsetBefore(pos.offset);
TextSpan textSpan;
if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
text: _readMore
? widget.text.substring(0, endIndex)
: widget.text,
style: DefaultTextStyle.of(context).style,
children: _readMore
? <TextSpan>[
ellipsizeTextSp,
readMoreOrLessTextSp,
]
: <TextSpan>[
readMoreOrLessTextSp,
],
);
} else {
textSpan = textSpan = TextSpan(
text: widget.text,
style: DefaultTextStyle.of(context).style,
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: _readMore ? widget.trimLines : null,
);
},
);
}
}
おわりに
いつもよくみるレイアウトを実装してみようとすると、意外と知らない知識を得ることができますね。
TextSpanやTextPainterは今回初めて触ったので、勉強になりました。
変なところがあったらコメントお願いします🙇♂️
TwitterでFlutterのこと呟いてたりするのでもしよかったらフォローお願いします!
@muttsu_623
明日は @granoeste さんです!
よろしくお願いします👏