📱
【Flutter】ListTile風の挙動をする高さ設定可能なWidgetの作成
動作確認環境
Flutter 2.2.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f4abaa0735 (5 weeks ago) • 2021-07-01 12:46:11 -0700
Engine • revision 241c87ad80
Tools • Dart 2.13.4
ListTileの特性
ListTile
自体はそのまま使えればListViewやUITableViewのような表示が簡単にでき、便利ではあるものの、
-
title
のみなら高さ56.0
-
title
とsubtitle
があるなら高さ72.0
と固定の高さで描画され、title
やsubtitle
の行数に応じて固定で高さが増えるつくりになっている。
ListTile
自体にheight
を設定する項目もなく、Container
やExpanded
でListTile
を囲んで高さを広がるように調整しようとすれば、Ripple Effect(タップ時の波紋)が半端だったり、意図しない隙間ができたりと、見栄え悪い形になる。
FlutterのListTile APIにも以下のように記載があり、このWidget自体が固定の高さで作られるものとして記載されている。
A single fixed-height row that typically contains some text as well as a leading or trailing icon.
(高さが固定された1つの行で、通常、いくつかのテキストと先頭または末尾のアイコンが含まれています。)
なので、ListTile
を使って高さを任意の値にしようとすることは諦め、自前で独自のWidgetを作ることになる。
ListTileの代替Widget
title
やsubtitle
のTextStyle
を除いて、ListTile
と近いものとして以下のような作りで実現(複雑になりすぎたので、もう少しシンプルに改良できるところはありそう)。
TextStyle
は、VariableHeightListTile
に渡すText
内で記載してあげれば反映されるため作るのが面倒臭いので省いた
VariableHeightListTile
class VariableHeightListTile extends StatelessWidget {
const VariableHeightListTile({
Key? key,
this.leading,
required this.title,
this.subtitle,
this.trailing,
this.onTap,
this.height,
}) : super(key: key);
final Widget? leading;
final Widget title;
final Widget? subtitle;
final Widget? trailing;
final VoidCallback? onTap;
final double? height;
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final _leading = leading ?? SizedBox.shrink();
final _trailing = trailing ?? SizedBox.shrink();
final double _padding = 16.0;
final _height = (height ?? 56.0) - _padding * 2;
final MainAxisAlignment _mainAxisAlignment =
(_leading is SizedBox || _trailing is SizedBox)
? MainAxisAlignment.spaceEvenly
: MainAxisAlignment.start;
SizedBox _margin(Widget widget) {
return (widget is SizedBox) ? SizedBox.shrink() : SizedBox(width: 32.0);
}
Widget _texts({
required Widget title,
Widget? subtitle,
}) {
final _title = title;
final _subtitle = subtitle ?? SizedBox.shrink();
final _defaultTitleStyle = theme.textTheme.bodyText1!;
final _defaultSubtitleStyle = theme.textTheme.subtitle1!;
return LayoutBuilder(builder: (context, size) {
bool _isMultiLine = false;
if (_title is Text) {
final style = _title.style ?? _defaultTitleStyle;
final span = TextSpan(text: _title.data, style: style);
final tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr,
);
tp.layout(maxWidth: size.maxWidth);
_isMultiLine |= tp.didExceedMaxLines;
}
if (_subtitle is Text) {
final style = _subtitle.style ?? _defaultSubtitleStyle;
final span = TextSpan(text: _subtitle.data, style: style);
final tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr,
);
tp.layout(maxWidth: size.maxWidth);
_isMultiLine |= tp.didExceedMaxLines;
}
double _padding = (_isMultiLine) ? 5.0 : 16.0;
CrossAxisAlignment _crossAxisAlignment = (_title is Text)
? CrossAxisAlignment.start
: CrossAxisAlignment.center;
return Padding(
padding: EdgeInsets.symmetric(vertical: _padding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: _crossAxisAlignment,
children: [
_title,
_subtitle,
],
),
);
});
}
return InkWell(
child: Container(
padding: EdgeInsets.symmetric(horizontal: _padding),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: _height,
),
child: Row(
mainAxisAlignment: _mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_leading,
_margin(_leading),
Expanded(
child: _texts(
title: title,
subtitle: subtitle,
),
),
_margin(_trailing),
_trailing,
],
),
),
),
onTap: onTap,
);
}
}
ListTileのシンプルな代替Widget
VariableHeightListTile
は柔軟な作りにしようとした結果、複雑な内部構造になってしまった(よりListTileを真似るならListTileのコードをコピーして、高さを指定できるように改変した方が楽)。
ただ実際に使う時は、1つのListView
内で`数パターンの形しか使わないため、もっとシンプルな形で代替Widgetを用意できる。以下に例として記載する。
CodePen貼ろうとしたら、CodePenはnull-safetyに非対応で使えなかったため、DartPadで実行サンプルを貼っておく。
title または titleとsubtitle
class ImitationListTile extends StatelessWidget {
const ImitationListTile({
Key? key,
required this.title,
this.subtitle,
this.height,
this.onTap,
}) : super(key: key);
final Text title;
final Text? subtitle;
final double? height;
final VoidCallback? onTap;
Widget build(BuildContext context) {
final _subtitle = subtitle ?? const SizedBox.shrink();
final _height = height ?? 54.0;
return InkWell(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: _height),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
_subtitle,
],
),
),
),
onTap: onTap,
);
}
}
leadingとtitle
class ImitationListTile extends StatelessWidget {
const ImitationListTile({
Key? key,
required this.leading,
required this.title,
this.height,
this.onTap,
}) : super(key: key);
final Icon leading;
final Text title;
final double? height;
final VoidCallback? onTap;
Widget build(BuildContext context) {
final _height = height ?? 54.0;
return InkWell(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: _height),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
leading,
const SizedBox(width: 32.0),
title,
],
),
),
),
onTap: onTap,
);
}
}
titleとtrailing または titleとsubtitleとtrailing
class ImitationListTile extends StatelessWidget {
const ImitationListTile(
{required this.title, required this.trailing, this.subtitle, this.height, this.onTap});
final Text title;
final Text? subtitle;
final Widget trailing;
final double? height;
final VoidCallback? onTap;
Widget build(BuildContext context) {
final _subtitle = subtitle ?? const SizedBox.shrink();
final _height = height ?? 54.0;
return InkWell(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: _height),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
_subtitle,
],
),
),
const SizedBox(width: 16.0),
trailing,
],
),
),
),
onTap: onTap,
);
}
}
Discussion