Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:math' as math;
8 : import 'dart:ui';
9 :
10 : import 'package:flutter/material.dart'
11 : show
12 : Colors,
13 : Divider,
14 : Ink,
15 : InkWell,
16 : ListTileStyle,
17 : MaterialState,
18 : MaterialStateMouseCursor,
19 : MaterialStateProperty,
20 : Theme,
21 : ThemeData,
22 : VisualDensity,
23 : debugCheckHasMaterial,
24 : kThemeChangeDuration;
25 : import 'package:flutter/widgets.dart';
26 : import 'package:flutter/rendering.dart';
27 :
28 : /// An inherited widget that defines color and style parameters for [MongolListTile]s
29 : /// in this widget's subtree.
30 : ///
31 : /// Values specified here are used for [MongolListTile] properties that are not given
32 : /// an explicit non-null value.
33 : ///
34 : /// The [MongolDrawer] widget specifies a tile theme for its children which sets
35 : /// [style] to [ListTileStyle.drawer].
36 : class MongolListTileTheme extends InheritedTheme {
37 : /// Creates a list tile theme that controls the color and style parameters for
38 : /// [MongolListTile]s.
39 11 : const MongolListTileTheme({
40 : Key? key,
41 : this.dense = false,
42 : this.shape,
43 : this.style = ListTileStyle.list,
44 : this.selectedColor,
45 : this.iconColor,
46 : this.textColor,
47 : this.contentPadding,
48 : this.tileColor,
49 : this.selectedTileColor,
50 : this.enableFeedback,
51 : this.verticalTitleGap,
52 : this.minHorizontalPadding,
53 : this.minLeadingHeight,
54 : required Widget child,
55 1 : }) : super(key: key, child: child);
56 :
57 : /// Creates a list tile theme that controls the color and style parameters for
58 : /// [MongolListTile]s, and merges in the current list tile theme, if any.
59 : ///
60 : /// The [child] argument must not be null.
61 0 : static Widget merge({
62 : Key? key,
63 : bool? dense,
64 : ShapeBorder? shape,
65 : ListTileStyle? style,
66 : Color? selectedColor,
67 : Color? iconColor,
68 : Color? textColor,
69 : EdgeInsetsGeometry? contentPadding,
70 : Color? tileColor,
71 : Color? selectedTileColor,
72 : bool? enableFeedback,
73 : double? verticalTitleGap,
74 : double? minHorizontalPadding,
75 : double? minLeadingHeight,
76 : required Widget child,
77 : }) {
78 0 : return Builder(
79 0 : builder: (BuildContext context) {
80 0 : final MongolListTileTheme parent = MongolListTileTheme.of(context);
81 0 : return MongolListTileTheme(
82 : key: key,
83 0 : dense: dense ?? parent.dense,
84 0 : shape: shape ?? parent.shape,
85 0 : style: style ?? parent.style,
86 0 : selectedColor: selectedColor ?? parent.selectedColor,
87 0 : iconColor: iconColor ?? parent.iconColor,
88 0 : textColor: textColor ?? parent.textColor,
89 0 : contentPadding: contentPadding ?? parent.contentPadding,
90 0 : tileColor: tileColor ?? parent.tileColor,
91 0 : selectedTileColor: selectedTileColor ?? parent.selectedTileColor,
92 0 : enableFeedback: enableFeedback ?? parent.enableFeedback,
93 0 : verticalTitleGap: verticalTitleGap ?? parent.verticalTitleGap,
94 : minHorizontalPadding:
95 0 : minHorizontalPadding ?? parent.minHorizontalPadding,
96 0 : minLeadingHeight: minLeadingHeight ?? parent.minLeadingHeight,
97 : child: child,
98 : );
99 : },
100 : );
101 : }
102 :
103 : /// If true then [ListTile]s will have the vertically dense layout.
104 : final bool dense;
105 :
106 : /// If specified, [shape] defines the [MongolListTile]'s shape.
107 : final ShapeBorder? shape;
108 :
109 : /// If specified, [style] defines the font used for [MongolListTile] titles.
110 : final ListTileStyle style;
111 :
112 : /// If specified, the color used for icons and text when a [MongolListTile] is selected.
113 : final Color? selectedColor;
114 :
115 : /// If specified, the icon color used for enabled [MongolListTile]s that are not selected.
116 : final Color? iconColor;
117 :
118 : /// If specified, the text color used for enabled [MongolListTile]s that are not selected.
119 : final Color? textColor;
120 :
121 : /// The tile's internal padding.
122 : ///
123 : /// Insets a [MongolListTile]'s contents: its [MongolListTile.leading], [MongolListTile.title],
124 : /// [MongolListTile.subtitle], and [MongolListTile.trailing] widgets.
125 : final EdgeInsetsGeometry? contentPadding;
126 :
127 : /// If specified, defines the background color for `MongolListTile` when
128 : /// [MongolListTile.selected] is false.
129 : ///
130 : /// If [MongolListTile.tileColor] is provided, [tileColor] is ignored.
131 : final Color? tileColor;
132 :
133 : /// If specified, defines the background color for `MongolListTile` when
134 : /// [MongolListTile.selected] is true.
135 : ///
136 : /// If [MongolListTile.selectedTileColor] is provided, [selectedTileColor] is ignored.
137 : final Color? selectedTileColor;
138 :
139 : /// The vertical gap between the titles and the leading/trailing widgets.
140 : ///
141 : /// If specified, overrides the default value of [MongolListTile.verticalTitleGap].
142 : final double? verticalTitleGap;
143 :
144 : /// The minimum padding on the left and right of the title and subtitle widgets.
145 : ///
146 : /// If specified, overrides the default value of [MongolListTile.minHorizontalPadding].
147 : final double? minHorizontalPadding;
148 :
149 : /// The minimum height allocated for the [MongolListTile.leading] widget.
150 : ///
151 : /// If specified, overrides the default value of [MongolListTile.minLeadingHeight].
152 : final double? minLeadingHeight;
153 :
154 : /// If specified, defines the feedback property for `MongolListTile`.
155 : ///
156 : /// If [MongolListTile.enableFeedback] is provided, [enableFeedback] is ignored.
157 : final bool? enableFeedback;
158 :
159 : /// The closest instance of this class that encloses the given context.
160 : ///
161 : /// Typical usage is as follows:
162 : ///
163 : /// ```dart
164 : /// MongolListTileTheme theme = MongolListTileTheme.of(context);
165 : /// ```
166 1 : static MongolListTileTheme of(BuildContext context) {
167 : final MongolListTileTheme? result =
168 1 : context.dependOnInheritedWidgetOfExactType<MongolListTileTheme>();
169 : return result ?? const MongolListTileTheme(child: SizedBox());
170 : }
171 :
172 0 : @override
173 : Widget wrap(BuildContext context, Widget child) {
174 0 : return MongolListTileTheme(
175 0 : dense: dense,
176 0 : shape: shape,
177 0 : style: style,
178 0 : selectedColor: selectedColor,
179 0 : iconColor: iconColor,
180 0 : textColor: textColor,
181 0 : contentPadding: contentPadding,
182 0 : tileColor: tileColor,
183 0 : selectedTileColor: selectedTileColor,
184 0 : enableFeedback: enableFeedback,
185 0 : verticalTitleGap: verticalTitleGap,
186 0 : minHorizontalPadding: minHorizontalPadding,
187 0 : minLeadingHeight: minLeadingHeight,
188 : child: child,
189 : );
190 : }
191 :
192 1 : @override
193 : bool updateShouldNotify(MongolListTileTheme oldWidget) {
194 3 : return dense != oldWidget.dense ||
195 3 : shape != oldWidget.shape ||
196 3 : style != oldWidget.style ||
197 3 : selectedColor != oldWidget.selectedColor ||
198 3 : iconColor != oldWidget.iconColor ||
199 3 : textColor != oldWidget.textColor ||
200 3 : contentPadding != oldWidget.contentPadding ||
201 3 : tileColor != oldWidget.tileColor ||
202 3 : selectedTileColor != oldWidget.selectedTileColor ||
203 3 : enableFeedback != oldWidget.enableFeedback ||
204 3 : verticalTitleGap != oldWidget.verticalTitleGap ||
205 3 : minHorizontalPadding != oldWidget.minHorizontalPadding ||
206 3 : minLeadingHeight != oldWidget.minLeadingHeight;
207 : }
208 : }
209 :
210 : /// A single fixed-width column that typically contains some text as well as
211 : /// a leading or trailing icon.
212 : ///
213 : /// This widget is the vertical text version of [ListTile].
214 : ///
215 : /// {@youtube 560 315 https://www.youtube.com/watch?v=l8dj0yPBvgQ}
216 : ///
217 : /// A list tile contains one to three lines of text optionally flanked by icons or
218 : /// other widgets, such as check boxes. The icons (or other widgets) for the
219 : /// tile are defined with the [leading] and [trailing] parameters. The first
220 : /// line of text is not optional and is specified with [title]. The value of
221 : /// [subtitle], which _is_ optional, will occupy the space allocated for an
222 : /// additional line of text, or two lines if [isThreeLine] is true. If [dense]
223 : /// is true then the overall width of this tile and the size of the
224 : /// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced.
225 : ///
226 : /// It is the responsibility of the caller to ensure that [title] does not wrap,
227 : /// and to ensure that [subtitle] doesn't wrap (if [isThreeLine] is false) or
228 : /// wraps to two lines (if it is true).
229 : ///
230 : /// The widths of the [leading] and [trailing] widgets are constrained
231 : /// according to the
232 : /// [Material spec](https://material.io/design/components/lists.html).
233 : /// An exception is made for one-line MongolListTiles for accessibility. Please
234 : /// see the example below to see how to adhere to both Material spec and
235 : /// accessibility requirements.
236 : ///
237 : /// Note that [leading] and [trailing] widgets can expand as far as they wish
238 : /// vertically, so ensure that they are properly constrained.
239 : ///
240 : /// List tiles are typically used in horizontal [ListView]s, or arranged in [Rows]s in
241 : /// [MongolDrawer]s and [Card]s.
242 : ///
243 : /// Requires one of its ancestors to be a [Material] widget.
244 : ///
245 : /// {@tool snippet}
246 : ///
247 : /// This example uses a [ListView] to demonstrate different configurations of
248 : /// [MongolListTile]s in [Card]s.
249 : ///
250 : /// 
251 : ///
252 : /// ```dart
253 : /// ListView(
254 : /// scrollDirection: Axis.horizontal,
255 : /// children: const <Widget>[
256 : /// Card(child: MongolListTile(title: Text('One-line MongolListTile'))),
257 : /// Card(
258 : /// child: MongolListTile(
259 : /// leading: FlutterLogo(),
260 : /// title: MongolText('One-line with leading widget'),
261 : /// ),
262 : /// ),
263 : /// Card(
264 : /// child: MongolListTile(
265 : /// title: MongolText('One-line with trailing widget'),
266 : /// trailing: Icon(Icons.more_vert),
267 : /// ),
268 : /// ),
269 : /// Card(
270 : /// child: MongolListTile(
271 : /// leading: FlutterLogo(),
272 : /// title: MongolText('One-line with both widgets'),
273 : /// trailing: Icon(Icons.more_vert),
274 : /// ),
275 : /// ),
276 : /// Card(
277 : /// child: MongolListTile(
278 : /// title: MongolText('One-line dense MongolListTile'),
279 : /// dense: true,
280 : /// ),
281 : /// ),
282 : /// Card(
283 : /// child: MongolListTile(
284 : /// leading: FlutterLogo(size: 56.0),
285 : /// title: MongolText('Two-line MongolListTile'),
286 : /// subtitle: MongolText('Here is a second line'),
287 : /// trailing: Icon(Icons.more_vert),
288 : /// ),
289 : /// ),
290 : /// Card(
291 : /// child: MongolListTile(
292 : /// leading: FlutterLogo(size: 72.0),
293 : /// title: MongolText('Three-line MongolListTile'),
294 : /// subtitle: MongolText(
295 : /// 'A sufficiently long subtitle warrants three lines.'
296 : /// ),
297 : /// trailing: Icon(Icons.more_vert),
298 : /// isThreeLine: true,
299 : /// ),
300 : /// ),
301 : /// ],
302 : /// )
303 : /// ```
304 : /// {@end-tool}
305 : /// {@tool snippet}
306 : ///
307 : /// To use a [MongolListTile] within a [Column], it needs to be wrapped in an
308 : /// [Expanded] widget. [MongolListTile] requires fixed height constraints,
309 : /// whereas a [Column] does not constrain its children.
310 : ///
311 : /// ```dart
312 : /// Column(
313 : /// children: const <Widget>[
314 : /// Expanded(
315 : /// child: MongolListTile(
316 : /// leading: FlutterLogo(),
317 : /// title: MongolText('These MongolListTiles are expanded '),
318 : /// ),
319 : /// ),
320 : /// Expanded(
321 : /// child: MongolListTile(
322 : /// trailing: FlutterLogo(),
323 : /// title: MongolText('to fill the available space.'),
324 : /// ),
325 : /// ),
326 : /// ],
327 : /// )
328 : /// ```
329 : /// {@end-tool}
330 : /// {@tool snippet}
331 : ///
332 : /// Tiles can be much more elaborate. Here is a tile which can be tapped, but
333 : /// which is disabled when the `_act` variable is not 2. When the tile is
334 : /// tapped, the whole column has an ink splash effect (see [InkWell]).
335 : ///
336 : /// ```dart
337 : /// int _act = 1;
338 : /// // ...
339 : /// MongolListTile(
340 : /// leading: const Icon(Icons.flight_land),
341 : /// title: const MongolText("Trix's airplane"),
342 : /// subtitle: _act != 2 ? const MongolText('The airplane is only in Act II.') : null,
343 : /// enabled: _act == 2,
344 : /// onTap: () { /* react to the tile being tapped */ }
345 : /// )
346 : /// ```
347 : /// {@end-tool}
348 : ///
349 : /// To be accessible, tappable [leading] and [trailing] widgets have to
350 : /// be at least 48x48 in size. However, to adhere to the Material spec,
351 : /// [trailing] and [leading] widgets in one-line MongolListTiles should visually be
352 : /// at most 32 ([dense]: true) or 40 ([dense]: false) in width, which may
353 : /// conflict with the accessibility requirement.
354 : ///
355 : /// For this reason, a one-line MongolListTile allows the width of [leading]
356 : /// and [trailing] widgets to be constrained by the width of the MongolListTile.
357 : /// This allows for the creation of tappable [leading] and [trailing] widgets
358 : /// that are large enough, but it is up to the developer to ensure that
359 : /// their widgets follow the Material spec.
360 : ///
361 : /// {@tool snippet}
362 : ///
363 : /// Here is an example of a one-line, non-[dense] MongolListTile with a
364 : /// tappable leading widget that adheres to accessibility requirements and
365 : /// the Material spec. To adjust the use case below for a one-line, [dense]
366 : /// MongolListTile, adjust the horizontal padding to 8.0.
367 : ///
368 : /// ```dart
369 : /// MongolListTile(
370 : /// leading: GestureDetector(
371 : /// behavior: HitTestBehavior.translucent,
372 : /// onTap: () {},
373 : /// child: Container(
374 : /// width: 48,
375 : /// height: 48,
376 : /// padding: const EdgeInsets.symmetric(horizontal: 4.0),
377 : /// alignment: Alignment.center,
378 : /// child: const CircleAvatar(),
379 : /// ),
380 : /// ),
381 : /// title: const MongolText('title'),
382 : /// dense: false,
383 : /// ),
384 : /// ```
385 : /// {@end-tool}
386 : ///
387 : /// See also:
388 : ///
389 : /// * [MongolListTileTheme], which defines visual properties for [MongolListTile]s.
390 : /// * [ListView], which can display an arbitrary number of [MongolListTile]s
391 : /// in a scrolling list.
392 : /// * [CircleAvatar], which shows an icon representing a person and is often
393 : /// used as the [leading] element of a MongolListTile.
394 : /// * [Card], which can be used with [Row] to show a few [MongolListTile]s.
395 : /// * [VerticalDivider], which can be used to separate [MongolListTile]s.
396 : /// * [MongolListTile.divideTiles], a utility for inserting [VerticalDivider]s
397 : /// in between [MongolListTile]s.
398 : /// * <https://material.io/design/components/lists.html>
399 : /// * Cookbook: [Use lists](https://flutter.dev/docs/cookbook/lists/basic-list)
400 : /// * Cookbook: [Implement swipe to dismiss](https://flutter.dev/docs/cookbook/gestures/dismissible)
401 : class MongolListTile extends StatelessWidget {
402 : /// Creates a vertical list tile.
403 : ///
404 : /// If [isThreeLine] is true, then [subtitle] must not be null.
405 : ///
406 : /// Requires one of its ancestors to be a [Material] widget.
407 2 : const MongolListTile({
408 : Key? key,
409 : this.leading,
410 : this.title,
411 : this.subtitle,
412 : this.trailing,
413 : this.isThreeLine = false,
414 : this.dense,
415 : this.visualDensity,
416 : this.shape,
417 : this.contentPadding,
418 : this.enabled = true,
419 : this.onTap,
420 : this.onLongPress,
421 : this.mouseCursor,
422 : this.selected = false,
423 : this.focusColor,
424 : this.hoverColor,
425 : this.focusNode,
426 : this.autofocus = false,
427 : this.tileColor,
428 : this.selectedTileColor,
429 : this.enableFeedback,
430 : this.verticalTitleGap,
431 : this.minHorizontalPadding,
432 : this.minLeadingHeight,
433 0 : }) : assert(!isThreeLine || subtitle != null),
434 1 : super(key: key);
435 :
436 : /// A widget to display above the title.
437 : ///
438 : /// Typically an [Icon] or a [CircleAvatar] widget.
439 : final Widget? leading;
440 :
441 : /// The primary content of the list tile.
442 : ///
443 : /// Typically a [MongolText] widget.
444 : ///
445 : /// This should not wrap. To enforce the single line limit, use
446 : /// [MongolText.maxLines].
447 : final Widget? title;
448 :
449 : /// Additional content displayed below the title.
450 : ///
451 : /// Typically a [MongolText] widget.
452 : ///
453 : /// If [isThreeLine] is false, this should not wrap.
454 : ///
455 : /// If [isThreeLine] is true, this should be configured to take a maximum of
456 : /// two lines. For example, you can use [MongolText.maxLines] to enforce the number
457 : /// of lines.
458 : ///
459 : /// The subtitle's default [TextStyle] depends on [TextTheme.bodyText2] except
460 : /// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled]
461 : /// and [selected].
462 : ///
463 : /// When [enabled] is false, the text color is set to [ThemeData.disabledColor].
464 : ///
465 : /// When [selected] is false, the text color is set to [MongolListTileTheme.textColor]
466 : /// if it's not null and to [TextTheme.caption]'s color if [MongolListTileTheme.textColor]
467 : /// is null.
468 : final Widget? subtitle;
469 :
470 : /// A widget to display under the title.
471 : ///
472 : /// Typically an [Icon] widget.
473 : final Widget? trailing;
474 :
475 : /// Whether this list tile is intended to display three lines of text.
476 : ///
477 : /// If true, then [subtitle] must be non-null (since it is expected to give
478 : /// the second and third lines of text).
479 : ///
480 : /// If false, the list tile is treated as having one line if the subtitle is
481 : /// null and treated as having two lines if the subtitle is non-null.
482 : ///
483 : /// When using a [MongolText] widget for [title] and [subtitle], you can enforce
484 : /// line limits using [MongolText.maxLines].
485 : final bool isThreeLine;
486 :
487 : /// Whether this list tile is part of a horizontally dense list.
488 : ///
489 : /// If this property is null then its value is based on [MongolListTileTheme.dense].
490 : ///
491 : /// Dense list tiles default to a smaller width.
492 : final bool? dense;
493 :
494 : /// Defines how compact the list tile's layout will be.
495 : ///
496 : /// See also:
497 : ///
498 : /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
499 : /// widgets within a [Theme].
500 : final VisualDensity? visualDensity;
501 :
502 : /// The tile's shape.
503 : ///
504 : /// Defines the tile's [InkWell.customBorder] and [Ink.decoration] shape.
505 : ///
506 : /// If this property is null then [MongolListTileTheme.shape] is used.
507 : /// If that's null then a rectangular [Border] will be used.
508 : final ShapeBorder? shape;
509 :
510 : /// The tile's internal padding.
511 : ///
512 : /// Insets a [MongolListTile]'s contents: its [leading], [title], [subtitle],
513 : /// and [trailing] widgets.
514 : ///
515 : /// If null, `EdgeInsets.symmetric(vertical: 16.0)` is used.
516 : final EdgeInsetsGeometry? contentPadding;
517 :
518 : /// Whether this list tile is interactive.
519 : ///
520 : /// If false, this list tile is styled with the disabled color from the
521 : /// current [Theme] and the [onTap] and [onLongPress] callbacks are
522 : /// inoperative.
523 : final bool enabled;
524 :
525 : /// Called when the user taps this list tile.
526 : ///
527 : /// Inoperative if [enabled] is false.
528 : final GestureTapCallback? onTap;
529 :
530 : /// Called when the user long-presses on this list tile.
531 : ///
532 : /// Inoperative if [enabled] is false.
533 : final GestureLongPressCallback? onLongPress;
534 :
535 : /// The cursor for a mouse pointer when it enters or is hovering over the
536 : /// widget.
537 : ///
538 : /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
539 : /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
540 : ///
541 : /// * [MaterialState.selected].
542 : /// * [MaterialState.disabled].
543 : ///
544 : /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
545 : final MouseCursor? mouseCursor;
546 :
547 : /// If this tile is also [enabled] then icons and text are rendered with the same color.
548 : ///
549 : /// By default the selected color is the theme's primary color. The selected color
550 : /// can be overridden with a [MongolListTileTheme].
551 : final bool selected;
552 :
553 : /// The color for the tile's [Material] when it has the input focus.
554 : final Color? focusColor;
555 :
556 : /// The color for the tile's [Material] when a pointer is hovering over it.
557 : final Color? hoverColor;
558 :
559 : /// {@macro flutter.widgets.Focus.focusNode}
560 : final FocusNode? focusNode;
561 :
562 : /// {@macro flutter.widgets.Focus.autofocus}
563 : final bool autofocus;
564 :
565 : /// Defines the background color of `MongolListTile` when [selected] is false.
566 : ///
567 : /// When the value is null, the `tileColor` is set to [MongolListTileTheme.tileColor]
568 : /// if it's not null and to [Colors.transparent] if it's null.
569 : final Color? tileColor;
570 :
571 : /// Defines the background color of ` MongolListTile` when [selected] is true.
572 : ///
573 : /// When the value if null, the `selectedTileColor` is set to
574 : /// [MongolListTileTheme.selectedTileColor] if it's not null and to
575 : /// [Colors.transparent] if it's null.
576 : final Color? selectedTileColor;
577 :
578 : /// Whether detected gestures should provide acoustic and/or haptic feedback.
579 : ///
580 : /// For example, on Android a tap will produce a clicking sound and a
581 : /// long-press will produce a short vibration, when feedback is enabled.
582 : ///
583 : /// See also:
584 : ///
585 : /// * [Feedback] for providing platform-specific feedback to certain actions.
586 : final bool? enableFeedback;
587 :
588 : /// The vertical gap between the titles and the leading/trailing widgets.
589 : ///
590 : /// If null, then the value of [MongolListTileTheme.verticalTitleGap] is used. If
591 : /// that is also null, then a default value of 16 is used.
592 : final double? verticalTitleGap;
593 :
594 : /// The minimum padding on the left and right of the title and subtitle widgets.
595 : ///
596 : /// If null, then the value of [MongolListTileTheme.minHorizontalPadding] is used. If
597 : /// that is also null, then a default value of 4 is used.
598 : final double? minHorizontalPadding;
599 :
600 : /// The minimum height allocated for the [MongolListTile.leading] widget.
601 : ///
602 : /// If null, then the value of [MongolListTileTheme.minLeadingHeight] is used. If
603 : /// that is also null, then a default value of 40 is used.
604 : final double? minLeadingHeight;
605 :
606 : /// Add a one pixel border in between each tile. If color isn't specified the
607 : /// [ThemeData.dividerColor] of the context's [Theme] is used.
608 : ///
609 : /// See also:
610 : ///
611 : /// * [VerticalDivider], which you can use to obtain this effect manually.
612 : static Iterable<Widget> divideTiles(
613 : {BuildContext? context,
614 : required Iterable<Widget> tiles,
615 : Color? color}) sync* {
616 : assert(color != null || context != null);
617 :
618 : final Iterator<Widget> iterator = tiles.iterator;
619 : final bool hasNext = iterator.moveNext();
620 : if (!hasNext) return;
621 :
622 : final Decoration decoration = BoxDecoration(
623 : border: Border(
624 : right: Divider.createBorderSide(context, color: color),
625 : ),
626 : );
627 :
628 : Widget tile = iterator.current;
629 : while (iterator.moveNext()) {
630 : yield DecoratedBox(
631 : position: DecorationPosition.foreground,
632 : decoration: decoration,
633 : child: tile,
634 : );
635 : tile = iterator.current;
636 : }
637 : if (hasNext) yield tile;
638 : }
639 :
640 1 : Color? _iconColor(ThemeData theme, MongolListTileTheme? tileTheme) {
641 2 : if (!enabled) return theme.disabledColor;
642 :
643 2 : if (selected && tileTheme?.selectedColor != null) {
644 1 : return tileTheme!.selectedColor;
645 : }
646 :
647 3 : if (!selected && tileTheme?.iconColor != null) return tileTheme!.iconColor;
648 :
649 1 : switch (theme.brightness) {
650 1 : case Brightness.light:
651 : // For the sake of backwards compatibility, the default for unselected
652 : // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73).
653 3 : return selected ? theme.colorScheme.primary : Colors.black45;
654 1 : case Brightness.dark:
655 1 : return selected
656 2 : ? theme.colorScheme.primary
657 : : null; // null - use current icon theme color
658 : }
659 : }
660 :
661 1 : Color? _textColor(
662 : ThemeData theme, MongolListTileTheme? tileTheme, Color? defaultColor) {
663 2 : if (!enabled) return theme.disabledColor;
664 :
665 2 : if (selected && tileTheme?.selectedColor != null) {
666 1 : return tileTheme!.selectedColor;
667 : }
668 :
669 3 : if (!selected && tileTheme?.textColor != null) return tileTheme!.textColor;
670 :
671 3 : if (selected) return theme.colorScheme.primary;
672 :
673 : return defaultColor;
674 : }
675 :
676 1 : bool _isDenseLayout(MongolListTileTheme? tileTheme) {
677 2 : return dense ?? tileTheme?.dense ?? false;
678 : }
679 :
680 1 : TextStyle _titleTextStyle(ThemeData theme, MongolListTileTheme? tileTheme) {
681 : final TextStyle style;
682 : if (tileTheme != null) {
683 1 : switch (tileTheme.style) {
684 1 : case ListTileStyle.drawer:
685 0 : style = theme.textTheme.bodyText1!;
686 : break;
687 1 : case ListTileStyle.list:
688 2 : style = theme.textTheme.subtitle1!;
689 : break;
690 : }
691 : } else {
692 0 : style = theme.textTheme.subtitle1!;
693 : }
694 2 : final Color? color = _textColor(theme, tileTheme, style.color);
695 1 : return _isDenseLayout(tileTheme)
696 1 : ? style.copyWith(fontSize: 13.0, color: color)
697 1 : : style.copyWith(color: color);
698 : }
699 :
700 1 : TextStyle _subtitleTextStyle(
701 : ThemeData theme, MongolListTileTheme? tileTheme) {
702 2 : final TextStyle style = theme.textTheme.bodyText2!;
703 : final Color? color =
704 4 : _textColor(theme, tileTheme, theme.textTheme.caption!.color);
705 1 : return _isDenseLayout(tileTheme)
706 1 : ? style.copyWith(color: color, fontSize: 12.0)
707 1 : : style.copyWith(color: color);
708 : }
709 :
710 1 : TextStyle _trailingAndLeadingTextStyle(
711 : ThemeData theme, MongolListTileTheme? tileTheme) {
712 2 : final TextStyle style = theme.textTheme.bodyText2!;
713 2 : final Color? color = _textColor(theme, tileTheme, style.color);
714 1 : return style.copyWith(color: color);
715 : }
716 :
717 1 : Color _tileBackgroundColor(MongolListTileTheme? tileTheme) {
718 1 : if (!selected) {
719 2 : if (tileColor != null) return tileColor!;
720 2 : if (tileTheme?.tileColor != null) return tileTheme!.tileColor!;
721 : }
722 :
723 1 : if (selected) {
724 2 : if (selectedTileColor != null) return selectedTileColor!;
725 1 : if (tileTheme?.selectedTileColor != null) {
726 1 : return tileTheme!.selectedTileColor!;
727 : }
728 : }
729 :
730 : return Colors.transparent;
731 : }
732 :
733 1 : @override
734 : Widget build(BuildContext context) {
735 1 : assert(debugCheckHasMaterial(context));
736 1 : final ThemeData theme = Theme.of(context);
737 1 : final MongolListTileTheme tileTheme = MongolListTileTheme.of(context);
738 :
739 : IconThemeData? iconThemeData;
740 : TextStyle? leadingAndTrailingTextStyle;
741 2 : if (leading != null || trailing != null) {
742 2 : iconThemeData = IconThemeData(color: _iconColor(theme, tileTheme));
743 : leadingAndTrailingTextStyle =
744 1 : _trailingAndLeadingTextStyle(theme, tileTheme);
745 : }
746 :
747 : Widget? leadingIcon;
748 1 : if (leading != null) {
749 1 : leadingIcon = AnimatedDefaultTextStyle(
750 : style: leadingAndTrailingTextStyle!,
751 : duration: kThemeChangeDuration,
752 1 : child: IconTheme.merge(
753 : data: iconThemeData!,
754 1 : child: leading!,
755 : ),
756 : );
757 : }
758 :
759 1 : final TextStyle titleStyle = _titleTextStyle(theme, tileTheme);
760 1 : final Widget titleText = AnimatedDefaultTextStyle(
761 : style: titleStyle,
762 : duration: kThemeChangeDuration,
763 1 : child: title ?? const SizedBox(),
764 : );
765 :
766 : Widget? subtitleText;
767 : TextStyle? subtitleStyle;
768 1 : if (subtitle != null) {
769 1 : subtitleStyle = _subtitleTextStyle(theme, tileTheme);
770 1 : subtitleText = AnimatedDefaultTextStyle(
771 : style: subtitleStyle,
772 : duration: kThemeChangeDuration,
773 1 : child: subtitle!,
774 : );
775 : }
776 :
777 : Widget? trailingIcon;
778 1 : if (trailing != null) {
779 1 : trailingIcon = AnimatedDefaultTextStyle(
780 : style: leadingAndTrailingTextStyle!,
781 : duration: kThemeChangeDuration,
782 1 : child: IconTheme.merge(
783 : data: iconThemeData!,
784 1 : child: trailing!,
785 : ),
786 : );
787 : }
788 :
789 : const EdgeInsets _defaultContentPadding =
790 : EdgeInsets.symmetric(vertical: 16.0);
791 : const TextDirection textDirection = TextDirection.ltr;
792 : final EdgeInsets resolvedContentPadding =
793 2 : contentPadding?.resolve(textDirection) ??
794 1 : tileTheme.contentPadding?.resolve(textDirection) ??
795 : _defaultContentPadding;
796 :
797 : final MouseCursor resolvedMouseCursor =
798 1 : MaterialStateProperty.resolveAs<MouseCursor>(
799 1 : mouseCursor ?? MaterialStateMouseCursor.clickable,
800 : <MaterialState>{
801 3 : if (!enabled || (onTap == null && onLongPress == null))
802 1 : MaterialState.disabled,
803 2 : if (selected) MaterialState.selected,
804 : },
805 : );
806 :
807 1 : return InkWell(
808 2 : customBorder: shape ?? tileTheme.shape,
809 2 : onTap: enabled ? onTap : null,
810 2 : onLongPress: enabled ? onLongPress : null,
811 : mouseCursor: resolvedMouseCursor,
812 1 : canRequestFocus: enabled,
813 1 : focusNode: focusNode,
814 1 : focusColor: focusColor,
815 1 : hoverColor: hoverColor,
816 1 : autofocus: autofocus,
817 2 : enableFeedback: enableFeedback ?? tileTheme.enableFeedback ?? true,
818 1 : child: Semantics(
819 1 : selected: selected,
820 1 : enabled: enabled,
821 1 : child: Ink(
822 1 : decoration: ShapeDecoration(
823 2 : shape: shape ?? tileTheme.shape ?? const Border(),
824 1 : color: _tileBackgroundColor(tileTheme),
825 : ),
826 1 : child: SafeArea(
827 : left: false,
828 : right: false,
829 : minimum: resolvedContentPadding,
830 1 : child: _MongolListTile(
831 : leading: leadingIcon,
832 : title: titleText,
833 : subtitle: subtitleText,
834 : trailing: trailingIcon,
835 1 : isDense: _isDenseLayout(tileTheme),
836 2 : visualDensity: visualDensity ?? theme.visualDensity,
837 1 : isThreeLine: isThreeLine,
838 1 : titleBaselineType: titleStyle.textBaseline!,
839 1 : subtitleBaselineType: subtitleStyle?.textBaseline,
840 : verticalTitleGap:
841 2 : verticalTitleGap ?? tileTheme.verticalTitleGap ?? 16,
842 : minHorizontalPadding:
843 2 : minHorizontalPadding ?? tileTheme.minHorizontalPadding ?? 4,
844 : minLeadingHeight:
845 2 : minLeadingHeight ?? tileTheme.minLeadingHeight ?? 40,
846 : ),
847 : ),
848 : ),
849 : ),
850 : );
851 : }
852 : }
853 :
854 : // Identifies the children of a _MongolListTileElement.
855 10 : enum _ListTileSlot {
856 : leading,
857 : title,
858 : subtitle,
859 : trailing,
860 : }
861 :
862 : class _MongolListTile extends RenderObjectWidget {
863 1 : const _MongolListTile({
864 : Key? key,
865 : this.leading,
866 : required this.title,
867 : this.subtitle,
868 : this.trailing,
869 : required this.isThreeLine,
870 : required this.isDense,
871 : required this.visualDensity,
872 : required this.titleBaselineType,
873 : required this.verticalTitleGap,
874 : required this.minHorizontalPadding,
875 : required this.minLeadingHeight,
876 : this.subtitleBaselineType,
877 1 : }) : super(key: key);
878 :
879 : final Widget? leading;
880 : final Widget title;
881 : final Widget? subtitle;
882 : final Widget? trailing;
883 : final bool isThreeLine;
884 : final bool isDense;
885 : final VisualDensity visualDensity;
886 : final TextBaseline titleBaselineType;
887 : final TextBaseline? subtitleBaselineType;
888 : final double verticalTitleGap;
889 : final double minHorizontalPadding;
890 : final double minLeadingHeight;
891 :
892 1 : @override
893 1 : _MongolListTileElement createElement() => _MongolListTileElement(this);
894 :
895 1 : @override
896 : _MongolRenderListTile createRenderObject(BuildContext context) {
897 1 : return _MongolRenderListTile(
898 1 : isThreeLine: isThreeLine,
899 1 : isDense: isDense,
900 1 : visualDensity: visualDensity,
901 1 : titleBaselineType: titleBaselineType,
902 1 : subtitleBaselineType: subtitleBaselineType,
903 1 : verticalTitleGap: verticalTitleGap,
904 1 : minHorizontalPadding: minHorizontalPadding,
905 1 : minLeadingHeight: minLeadingHeight,
906 : );
907 : }
908 :
909 1 : @override
910 : void updateRenderObject(
911 : BuildContext context, _MongolRenderListTile renderObject) {
912 : renderObject
913 2 : ..isThreeLine = isThreeLine
914 2 : ..isDense = isDense
915 2 : ..visualDensity = visualDensity
916 2 : ..titleBaselineType = titleBaselineType
917 2 : ..subtitleBaselineType = subtitleBaselineType
918 2 : ..verticalTitleGap = verticalTitleGap
919 2 : ..minHorizontalPadding = minHorizontalPadding
920 2 : ..minLeadingHeight = minLeadingHeight;
921 : }
922 : }
923 :
924 : class _MongolListTileElement extends RenderObjectElement {
925 2 : _MongolListTileElement(_MongolListTile widget) : super(widget);
926 :
927 : final Map<_ListTileSlot, Element> slotToChild = <_ListTileSlot, Element>{};
928 :
929 1 : @override
930 1 : _MongolListTile get widget => super.widget as _MongolListTile;
931 :
932 1 : @override
933 : _MongolRenderListTile get renderObject =>
934 1 : super.renderObject as _MongolRenderListTile;
935 :
936 1 : @override
937 : void visitChildren(ElementVisitor visitor) {
938 3 : slotToChild.values.forEach(visitor);
939 : }
940 :
941 0 : @override
942 : void forgetChild(Element child) {
943 0 : assert(slotToChild.containsValue(child));
944 0 : assert(child.slot is _ListTileSlot);
945 0 : assert(slotToChild.containsKey(child.slot));
946 0 : slotToChild.remove(child.slot);
947 0 : super.forgetChild(child);
948 : }
949 :
950 1 : void _mountChild(Widget? widget, _ListTileSlot slot) {
951 2 : final Element? oldChild = slotToChild[slot];
952 1 : final Element? newChild = updateChild(oldChild, widget, slot);
953 : if (oldChild != null) {
954 0 : slotToChild.remove(slot);
955 : }
956 : if (newChild != null) {
957 2 : slotToChild[slot] = newChild;
958 : }
959 : }
960 :
961 1 : @override
962 : void mount(Element? parent, Object? newSlot) {
963 1 : super.mount(parent, newSlot);
964 3 : _mountChild(widget.leading, _ListTileSlot.leading);
965 3 : _mountChild(widget.title, _ListTileSlot.title);
966 3 : _mountChild(widget.subtitle, _ListTileSlot.subtitle);
967 3 : _mountChild(widget.trailing, _ListTileSlot.trailing);
968 : }
969 :
970 1 : void _updateChild(Widget? widget, _ListTileSlot slot) {
971 2 : final Element? oldChild = slotToChild[slot];
972 1 : final Element? newChild = updateChild(oldChild, widget, slot);
973 : if (oldChild != null) {
974 2 : slotToChild.remove(slot);
975 : }
976 : if (newChild != null) {
977 2 : slotToChild[slot] = newChild;
978 : }
979 : }
980 :
981 1 : @override
982 : void update(_MongolListTile newWidget) {
983 1 : super.update(newWidget);
984 2 : assert(widget == newWidget);
985 3 : _updateChild(widget.leading, _ListTileSlot.leading);
986 3 : _updateChild(widget.title, _ListTileSlot.title);
987 3 : _updateChild(widget.subtitle, _ListTileSlot.subtitle);
988 3 : _updateChild(widget.trailing, _ListTileSlot.trailing);
989 : }
990 :
991 1 : void _updateRenderObject(RenderBox? child, _ListTileSlot slot) {
992 : switch (slot) {
993 1 : case _ListTileSlot.leading:
994 2 : renderObject.leading = child;
995 : break;
996 1 : case _ListTileSlot.title:
997 2 : renderObject.title = child;
998 : break;
999 1 : case _ListTileSlot.subtitle:
1000 2 : renderObject.subtitle = child;
1001 : break;
1002 1 : case _ListTileSlot.trailing:
1003 2 : renderObject.trailing = child;
1004 : break;
1005 : }
1006 : }
1007 :
1008 1 : @override
1009 : void insertRenderObjectChild(RenderObject child, _ListTileSlot slot) {
1010 1 : assert(child is RenderBox);
1011 1 : _updateRenderObject(child as RenderBox, slot);
1012 4 : assert(renderObject.children.keys.contains(slot));
1013 : }
1014 :
1015 1 : @override
1016 : void removeRenderObjectChild(RenderObject child, _ListTileSlot slot) {
1017 1 : assert(child is RenderBox);
1018 4 : assert(renderObject.children[slot] == child);
1019 1 : _updateRenderObject(null, slot);
1020 4 : assert(!renderObject.children.keys.contains(slot));
1021 : }
1022 :
1023 0 : @override
1024 : void moveRenderObjectChild(
1025 : RenderObject child, Object? oldSlot, Object? newSlot) {
1026 0 : assert(false, 'not reachable');
1027 : }
1028 : }
1029 :
1030 : class _MongolRenderListTile extends RenderBox {
1031 1 : _MongolRenderListTile({
1032 : required bool isDense,
1033 : required VisualDensity visualDensity,
1034 : required bool isThreeLine,
1035 : required TextBaseline titleBaselineType,
1036 : TextBaseline? subtitleBaselineType,
1037 : required double verticalTitleGap,
1038 : required double minHorizontalPadding,
1039 : required double minLeadingHeight,
1040 : }) : _isDense = isDense,
1041 : _visualDensity = visualDensity,
1042 : _isThreeLine = isThreeLine,
1043 : _titleBaselineType = titleBaselineType,
1044 : _subtitleBaselineType = subtitleBaselineType,
1045 : _verticalTitleGap = verticalTitleGap,
1046 : _minHorizontalPadding = minHorizontalPadding,
1047 : _minLeadingHeight = minLeadingHeight;
1048 :
1049 : final Map<_ListTileSlot, RenderBox> children = <_ListTileSlot, RenderBox>{};
1050 :
1051 1 : RenderBox? _updateChild(
1052 : RenderBox? oldChild, RenderBox? newChild, _ListTileSlot slot) {
1053 : if (oldChild != null) {
1054 1 : dropChild(oldChild);
1055 2 : children.remove(slot);
1056 : }
1057 : if (newChild != null) {
1058 2 : children[slot] = newChild;
1059 1 : adoptChild(newChild);
1060 : }
1061 : return newChild;
1062 : }
1063 :
1064 : RenderBox? _leading;
1065 2 : RenderBox? get leading => _leading;
1066 1 : set leading(RenderBox? value) {
1067 3 : _leading = _updateChild(_leading, value, _ListTileSlot.leading);
1068 : }
1069 :
1070 : RenderBox? _title;
1071 2 : RenderBox? get title => _title;
1072 1 : set title(RenderBox? value) {
1073 3 : _title = _updateChild(_title, value, _ListTileSlot.title);
1074 : }
1075 :
1076 : RenderBox? _subtitle;
1077 2 : RenderBox? get subtitle => _subtitle;
1078 1 : set subtitle(RenderBox? value) {
1079 3 : _subtitle = _updateChild(_subtitle, value, _ListTileSlot.subtitle);
1080 : }
1081 :
1082 : RenderBox? _trailing;
1083 2 : RenderBox? get trailing => _trailing;
1084 1 : set trailing(RenderBox? value) {
1085 3 : _trailing = _updateChild(_trailing, value, _ListTileSlot.trailing);
1086 : }
1087 :
1088 : // The returned list is ordered for hit testing.
1089 : Iterable<RenderBox> get _children sync* {
1090 : if (leading != null) yield leading!;
1091 : if (title != null) yield title!;
1092 : if (subtitle != null) yield subtitle!;
1093 : if (trailing != null) yield trailing!;
1094 : }
1095 :
1096 2 : bool get isDense => _isDense;
1097 : bool _isDense;
1098 1 : set isDense(bool value) {
1099 2 : if (_isDense == value) return;
1100 1 : _isDense = value;
1101 1 : markNeedsLayout();
1102 : }
1103 :
1104 2 : VisualDensity get visualDensity => _visualDensity;
1105 : VisualDensity _visualDensity;
1106 1 : set visualDensity(VisualDensity value) {
1107 2 : if (_visualDensity == value) return;
1108 1 : _visualDensity = value;
1109 1 : markNeedsLayout();
1110 : }
1111 :
1112 2 : bool get isThreeLine => _isThreeLine;
1113 : bool _isThreeLine;
1114 1 : set isThreeLine(bool value) {
1115 2 : if (_isThreeLine == value) return;
1116 1 : _isThreeLine = value;
1117 1 : markNeedsLayout();
1118 : }
1119 :
1120 2 : TextBaseline get titleBaselineType => _titleBaselineType;
1121 : TextBaseline _titleBaselineType;
1122 1 : set titleBaselineType(TextBaseline value) {
1123 2 : if (_titleBaselineType == value) return;
1124 0 : _titleBaselineType = value;
1125 0 : markNeedsLayout();
1126 : }
1127 :
1128 2 : TextBaseline? get subtitleBaselineType => _subtitleBaselineType;
1129 : TextBaseline? _subtitleBaselineType;
1130 1 : set subtitleBaselineType(TextBaseline? value) {
1131 2 : if (_subtitleBaselineType == value) return;
1132 1 : _subtitleBaselineType = value;
1133 1 : markNeedsLayout();
1134 : }
1135 :
1136 0 : double get verticalTitleGap => _verticalTitleGap;
1137 : double _verticalTitleGap;
1138 1 : double get _effectiveVerticalTitleGap =>
1139 5 : _verticalTitleGap + visualDensity.vertical * 2.0;
1140 :
1141 1 : set verticalTitleGap(double value) {
1142 2 : if (_verticalTitleGap == value) return;
1143 0 : _verticalTitleGap = value;
1144 0 : markNeedsLayout();
1145 : }
1146 :
1147 0 : double get minHorizontalPadding => _minHorizontalPadding;
1148 : double _minHorizontalPadding;
1149 :
1150 1 : set minHorizontalPadding(double value) {
1151 2 : if (_minHorizontalPadding == value) return;
1152 0 : _minHorizontalPadding = value;
1153 0 : markNeedsLayout();
1154 : }
1155 :
1156 0 : double get minLeadingHeight => _minLeadingHeight;
1157 : double _minLeadingHeight;
1158 :
1159 1 : set minLeadingHeight(double value) {
1160 2 : if (_minLeadingHeight == value) return;
1161 0 : _minLeadingHeight = value;
1162 0 : markNeedsLayout();
1163 : }
1164 :
1165 1 : @override
1166 : void attach(PipelineOwner owner) {
1167 1 : super.attach(owner);
1168 1 : for (final RenderBox child in _children) {
1169 0 : child.attach(owner);
1170 : }
1171 : }
1172 :
1173 1 : @override
1174 : void detach() {
1175 1 : super.detach();
1176 2 : for (final RenderBox child in _children) {
1177 1 : child.detach();
1178 : }
1179 : }
1180 :
1181 1 : @override
1182 : void redepthChildren() {
1183 3 : _children.forEach(redepthChild);
1184 : }
1185 :
1186 1 : @override
1187 : void visitChildren(RenderObjectVisitor visitor) {
1188 2 : _children.forEach(visitor);
1189 : }
1190 :
1191 0 : @override
1192 : List<DiagnosticsNode> debugDescribeChildren() {
1193 0 : final List<DiagnosticsNode> value = <DiagnosticsNode>[];
1194 0 : void add(RenderBox? child, String name) {
1195 0 : if (child != null) value.add(child.toDiagnosticsNode(name: name));
1196 : }
1197 :
1198 0 : add(leading, 'leading');
1199 0 : add(title, 'title');
1200 0 : add(subtitle, 'subtitle');
1201 0 : add(trailing, 'trailing');
1202 : return value;
1203 : }
1204 :
1205 1 : @override
1206 : bool get sizedByParent => false;
1207 :
1208 1 : static double _minHeight(RenderBox? box, double width) {
1209 1 : return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
1210 : }
1211 :
1212 1 : static double _maxHeight(RenderBox? box, double width) {
1213 1 : return box == null ? 0.0 : box.getMaxIntrinsicHeight(width);
1214 : }
1215 :
1216 1 : @override
1217 : double computeMinIntrinsicHeight(double width) {
1218 1 : final double leadingHeight = leading != null
1219 5 : ? math.max(leading!.getMinIntrinsicHeight(width), _minLeadingHeight) +
1220 1 : _effectiveVerticalTitleGap
1221 : : 0.0;
1222 1 : return leadingHeight +
1223 6 : math.max(_minHeight(title, width), _minHeight(subtitle, width)) +
1224 2 : _maxHeight(trailing, width);
1225 : }
1226 :
1227 1 : @override
1228 : double computeMaxIntrinsicHeight(double width) {
1229 1 : final double leadingHeight = leading != null
1230 5 : ? math.max(leading!.getMaxIntrinsicHeight(width), _minLeadingHeight) +
1231 1 : _effectiveVerticalTitleGap
1232 : : 0.0;
1233 1 : return leadingHeight +
1234 6 : math.max(_maxHeight(title, width), _maxHeight(subtitle, width)) +
1235 2 : _maxHeight(trailing, width);
1236 : }
1237 :
1238 1 : double get _defaultTileWidth {
1239 1 : final bool hasSubtitle = subtitle != null;
1240 1 : final bool isTwoLine = !isThreeLine && hasSubtitle;
1241 1 : final bool isOneLine = !isThreeLine && !hasSubtitle;
1242 :
1243 2 : final Offset baseDensity = visualDensity.baseSizeAdjustment;
1244 3 : if (isOneLine) return (isDense ? 48.0 : 56.0) + baseDensity.dx;
1245 3 : if (isTwoLine) return (isDense ? 64.0 : 72.0) + baseDensity.dx;
1246 3 : return (isDense ? 76.0 : 88.0) + baseDensity.dx;
1247 : }
1248 :
1249 1 : @override
1250 : double computeMinIntrinsicWidth(double height) {
1251 1 : return math.max(
1252 1 : _defaultTileWidth,
1253 3 : title!.getMinIntrinsicWidth(height) +
1254 2 : (subtitle?.getMinIntrinsicWidth(height) ?? 0.0),
1255 : );
1256 : }
1257 :
1258 1 : @override
1259 : double computeMaxIntrinsicWidth(double height) {
1260 1 : return computeMinIntrinsicWidth(height);
1261 : }
1262 :
1263 0 : @override
1264 : double computeDistanceToActualBaseline(TextBaseline baseline) {
1265 : return 0.0;
1266 : }
1267 :
1268 1 : static double? _boxBaseline(RenderBox box, TextBaseline baseline) {
1269 1 : return box.getDistanceToBaseline(baseline);
1270 : }
1271 :
1272 1 : static Size _layoutBox(RenderBox? box, BoxConstraints constraints) {
1273 : if (box == null) return Size.zero;
1274 1 : box.layout(constraints, parentUsesSize: true);
1275 1 : return box.size;
1276 : }
1277 :
1278 1 : static void _positionBox(RenderBox box, Offset offset) {
1279 1 : final BoxParentData parentData = box.parentData! as BoxParentData;
1280 1 : parentData.offset = offset;
1281 : }
1282 :
1283 1 : @override
1284 : Size computeDryLayout(BoxConstraints constraints) {
1285 1 : assert(debugCannotComputeDryLayout(
1286 : reason:
1287 : 'Layout requires baseline metrics, which are only available after a full layout.',
1288 : ));
1289 : return Size.zero;
1290 : }
1291 :
1292 : // All of the dimensions below were taken from the Material Design spec:
1293 : // https://material.io/design/components/lists.html#specs
1294 1 : @override
1295 : void performLayout() {
1296 1 : final BoxConstraints constraints = this.constraints;
1297 1 : final bool hasLeading = leading != null;
1298 1 : final bool hasSubtitle = subtitle != null;
1299 1 : final bool hasTrailing = trailing != null;
1300 1 : final bool isTwoLine = !isThreeLine && hasSubtitle;
1301 1 : final bool isOneLine = !isThreeLine && !hasSubtitle;
1302 2 : final Offset densityAdjustment = visualDensity.baseSizeAdjustment;
1303 :
1304 1 : final BoxConstraints maxIconWidthConstraint = BoxConstraints(
1305 : // One-line trailing and leading widget widths do not follow
1306 : // Material specifications, but this sizing is required to adhere
1307 : // to accessibility requirements for smallest tappable widget.
1308 : // Two- and three-line trailing widget widths are constrained
1309 : // properly according to the Material spec.
1310 3 : maxWidth: (isDense ? 48.0 : 56.0) + densityAdjustment.dx,
1311 : );
1312 1 : final BoxConstraints looseConstraints = constraints.loosen();
1313 : final BoxConstraints iconConstraints =
1314 1 : looseConstraints.enforce(maxIconWidthConstraint);
1315 :
1316 1 : final double tileHeight = looseConstraints.maxHeight;
1317 2 : final Size leadingSize = _layoutBox(leading, iconConstraints);
1318 2 : final Size trailingSize = _layoutBox(trailing, iconConstraints);
1319 : assert(
1320 3 : tileHeight != leadingSize.height || tileHeight == 0.0,
1321 : 'Leading widget consumes entire tile height. Please use a sized widget, '
1322 : 'or consider replacing MongolListTile with a custom widget '
1323 : '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)',
1324 : );
1325 : assert(
1326 3 : tileHeight != trailingSize.height || tileHeight == 0.0,
1327 : 'Trailing widget consumes entire tile height. Please use a sized widget, '
1328 : 'or consider replacing MongolListTile with a custom widget '
1329 : '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)',
1330 : );
1331 :
1332 : final double titleStart = hasLeading
1333 4 : ? math.max(_minLeadingHeight, leadingSize.height) +
1334 1 : _effectiveVerticalTitleGap
1335 : : 0.0;
1336 : final double adjustedLeadingHeight = hasLeading
1337 4 : ? math.max(leadingSize.height + _effectiveVerticalTitleGap, 32.0)
1338 : : 0.0;
1339 : final double adjustedTrailingHeight =
1340 2 : hasTrailing ? math.max(trailingSize.height, 32.0) : 0.0;
1341 1 : final BoxConstraints textConstraints = looseConstraints.tighten(
1342 2 : height: tileHeight - titleStart - adjustedTrailingHeight,
1343 : );
1344 2 : final Size titleSize = _layoutBox(title, textConstraints);
1345 2 : final Size subtitleSize = _layoutBox(subtitle, textConstraints);
1346 :
1347 : double? titleBaseline;
1348 : double? subtitleBaseline;
1349 : if (isTwoLine) {
1350 1 : titleBaseline = isDense ? 28.0 : 32.0;
1351 1 : subtitleBaseline = isDense ? 48.0 : 52.0;
1352 1 : } else if (isThreeLine) {
1353 1 : titleBaseline = isDense ? 22.0 : 28.0;
1354 1 : subtitleBaseline = isDense ? 42.0 : 48.0;
1355 : } else {
1356 0 : assert(isOneLine);
1357 : }
1358 :
1359 1 : final double defaultTileWidth = _defaultTileWidth;
1360 :
1361 : double tileWidth;
1362 : double titleX;
1363 : double? subtitleX;
1364 : if (!hasSubtitle) {
1365 1 : tileWidth = math.max(
1366 4 : defaultTileWidth, titleSize.width + 2.0 * _minHorizontalPadding);
1367 3 : titleX = (tileWidth - titleSize.width) / 2.0;
1368 : } else {
1369 1 : assert(subtitleBaselineType != null);
1370 4 : titleX = titleBaseline! - _boxBaseline(title!, titleBaselineType)!;
1371 1 : subtitleX = subtitleBaseline! -
1372 4 : _boxBaseline(subtitle!, subtitleBaselineType!)! +
1373 3 : visualDensity.horizontal * 2.0;
1374 : tileWidth = defaultTileWidth;
1375 :
1376 : // If the title and subtitle overlap, move the title left by half
1377 : // the overlap and the subtitle right by the same amount, and adjust
1378 : // tileWidth so that both titles fit.
1379 3 : final double titleOverlap = titleX + titleSize.width - subtitleX;
1380 1 : if (titleOverlap > 0.0) {
1381 0 : titleX -= titleOverlap / 2.0;
1382 0 : subtitleX += titleOverlap / 2.0;
1383 : }
1384 :
1385 : // If the title or subtitle overflow tileWidth then punt: title
1386 : // and subtitle are arranged in a column, tileWidth = row width plus
1387 : // _minHorizontalPadding on the left and right.
1388 2 : if (titleX < _minHorizontalPadding ||
1389 5 : (subtitleX + subtitleSize.width + _minHorizontalPadding) >
1390 : tileWidth) {
1391 : tileWidth =
1392 6 : titleSize.width + subtitleSize.width + 2.0 * _minHorizontalPadding;
1393 1 : titleX = _minHorizontalPadding;
1394 3 : subtitleX = titleSize.width + _minHorizontalPadding;
1395 : }
1396 : }
1397 :
1398 : // This attempts to implement the redlines for the horizontal position of the
1399 : // leading and trailing icons on the spec page:
1400 : // https://material.io/design/components/lists.html#specs
1401 : // The interpretation for these redlines is as follows:
1402 : // - For large tiles (> 72dp), both leading and trailing controls should be
1403 : // a fixed distance from the left. As per guidelines this is set to 16dp.
1404 : // - For smaller tiles, trailing should always be centered. Leading can be
1405 : // centered or closer to the left. It should never be further than 16dp
1406 : // to the left.
1407 : final double leadingX;
1408 : final double trailingX;
1409 1 : if (tileWidth > 72.0) {
1410 : leadingX = 16.0;
1411 : trailingX = 16.0;
1412 : } else {
1413 4 : leadingX = math.min((tileWidth - leadingSize.width) / 2.0, 16.0);
1414 3 : trailingX = (tileWidth - trailingSize.width) / 2.0;
1415 : }
1416 :
1417 : if (hasLeading) {
1418 3 : _positionBox(leading!, Offset(leadingX, 0.0));
1419 : }
1420 3 : _positionBox(title!, Offset(titleX, adjustedLeadingHeight));
1421 : if (hasSubtitle) {
1422 3 : _positionBox(subtitle!, Offset(subtitleX!, adjustedLeadingHeight));
1423 : }
1424 : if (hasTrailing) {
1425 1 : _positionBox(
1426 4 : trailing!, Offset(trailingX, tileHeight - trailingSize.height));
1427 : }
1428 :
1429 3 : size = constraints.constrain(Size(tileWidth, tileHeight));
1430 4 : assert(size.width == constraints.constrainWidth(tileWidth));
1431 4 : assert(size.height == constraints.constrainHeight(tileHeight));
1432 : }
1433 :
1434 1 : @override
1435 : void paint(PaintingContext context, Offset offset) {
1436 1 : void doPaint(RenderBox? child) {
1437 : if (child != null) {
1438 1 : final BoxParentData parentData = child.parentData! as BoxParentData;
1439 3 : context.paintChild(child, parentData.offset + offset);
1440 : }
1441 : }
1442 :
1443 2 : doPaint(leading);
1444 2 : doPaint(title);
1445 2 : doPaint(subtitle);
1446 2 : doPaint(trailing);
1447 : }
1448 :
1449 1 : @override
1450 : bool hitTestSelf(Offset position) => true;
1451 :
1452 1 : @override
1453 : bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
1454 2 : for (final RenderBox child in _children) {
1455 1 : final BoxParentData parentData = child.parentData! as BoxParentData;
1456 1 : final bool isHit = result.addWithPaintOffset(
1457 1 : offset: parentData.offset,
1458 : position: position,
1459 1 : hitTest: (BoxHitTestResult result, Offset transformed) {
1460 3 : assert(transformed == position - parentData.offset);
1461 1 : return child.hitTest(result, position: transformed);
1462 : },
1463 : );
1464 : if (isHit) return true;
1465 : }
1466 : return false;
1467 : }
1468 : }
|