6
6
library ;
7
7
8
8
import 'dart:async' ;
9
+ import 'dart:math' show max, min;
9
10
11
+ import 'package:flutter/rendering.dart' ;
12
+ import 'package:flutter/scheduler.dart' ;
10
13
import 'package:flutter/services.dart' ;
11
14
12
15
import 'actions.dart' ;
13
16
import 'basic.dart' ;
17
+ import 'constants.dart' ;
14
18
import 'editable_text.dart' ;
15
19
import 'focus_manager.dart' ;
16
20
import 'framework.dart' ;
17
21
import 'inherited_notifier.dart' ;
22
+ import 'layout_builder.dart' ;
18
23
import 'overlay.dart' ;
19
24
import 'shortcuts.dart' ;
20
25
import 'tap_region.dart' ;
26
+ import 'value_listenable_builder.dart' ;
21
27
22
28
// Examples can assume:
23
29
// late BuildContext context;
@@ -213,10 +219,10 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
213
219
/// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
214
220
/// Builds the selectable options widgets from a list of options objects.
215
221
///
216
- /// The options are displayed floating below or above the field using a
217
- /// [CompositedTransformFollower] inside of an [ Overlay] , not at the same
218
- /// place in the widget tree as [RawAutocomplete] . To control whether it opens
219
- /// upward or downward, use [optionsViewOpenDirection] .
222
+ /// The options are displayed floating below or above the field inside of an
223
+ /// [Overlay] , not at the same place in the widget tree as [RawAutocomplete] .
224
+ /// To control whether it opens upward or downward, use
225
+ /// [optionsViewOpenDirection] .
220
226
///
221
227
/// In order to track which item is highlighted by keyboard navigation, the
222
228
/// resulting options will be wrapped in an inherited
@@ -307,6 +313,10 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
307
313
class _RawAutocompleteState <T extends Object > extends State <RawAutocomplete <T >> {
308
314
final GlobalKey _fieldKey = GlobalKey ();
309
315
final LayerLink _optionsLayerLink = LayerLink ();
316
+
317
+ /// The box constraints that the field was last built with.
318
+ final ValueNotifier <BoxConstraints ?> _fieldBoxConstraints = ValueNotifier <BoxConstraints ?>(null );
319
+
310
320
final OverlayPortalController _optionsViewController = OverlayPortalController (
311
321
debugLabel: '_RawAutocompleteState' ,
312
322
);
@@ -439,30 +449,22 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
439
449
}
440
450
441
451
Widget _buildOptionsView (BuildContext context) {
442
- final TextDirection textDirection = Directionality .of (context);
443
- final Alignment followerAlignment = switch (widget.optionsViewOpenDirection) {
444
- OptionsViewOpenDirection .up => AlignmentDirectional .bottomStart,
445
- OptionsViewOpenDirection .down => AlignmentDirectional .topStart,
446
- }.resolve (textDirection);
447
- final Alignment targetAnchor = switch (widget.optionsViewOpenDirection) {
448
- OptionsViewOpenDirection .up => AlignmentDirectional .topStart,
449
- OptionsViewOpenDirection .down => AlignmentDirectional .bottomStart,
450
- }.resolve (textDirection);
451
-
452
- return CompositedTransformFollower (
453
- link: _optionsLayerLink,
454
- showWhenUnlinked: false ,
455
- targetAnchor: targetAnchor,
456
- followerAnchor: followerAlignment,
457
- child: TextFieldTapRegion (
458
- child: AutocompleteHighlightedOption (
452
+ return ValueListenableBuilder <BoxConstraints ?>(
453
+ valueListenable: _fieldBoxConstraints,
454
+ builder: (BuildContext context, BoxConstraints ? constraints, Widget ? child) {
455
+ return _RawAutocompleteOptions (
456
+ fieldKey: _fieldKey,
457
+ optionsLayerLink: _optionsLayerLink,
458
+ optionsViewOpenDirection: widget.optionsViewOpenDirection,
459
+ overlayContext: context,
460
+ textDirection: Directionality .maybeOf (context),
459
461
highlightIndexNotifier: _highlightedOptionIndex,
460
- child : Builder (
461
- builder:
462
- ( BuildContext context) => widget.optionsViewBuilder (context, _select, _options),
463
- ) ,
464
- ),
465
- ) ,
462
+ fieldConstraints : _fieldBoxConstraints.value ! ,
463
+ builder: ( BuildContext context) {
464
+ return widget.optionsViewBuilder (context, _select, _options);
465
+ } ,
466
+ );
467
+ } ,
466
468
);
467
469
}
468
470
@@ -504,6 +506,7 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
504
506
widget.focusNode? .removeListener (_updateOptionsViewVisibility);
505
507
_internalFocusNode? .dispose ();
506
508
_highlightedOptionIndex.dispose ();
509
+ _fieldBoxConstraints.dispose ();
507
510
super .dispose ();
508
511
}
509
512
@@ -517,25 +520,224 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
517
520
_onFieldSubmitted,
518
521
) ??
519
522
const SizedBox .shrink ();
520
- return OverlayPortal .targetsRootOverlay (
521
- controller: _optionsViewController,
522
- overlayChildBuilder: _buildOptionsView,
523
- child: TextFieldTapRegion (
524
- child: SizedBox (
523
+ return LayoutBuilder (
524
+ builder: (BuildContext context, BoxConstraints constraints) {
525
+ // TODO(victorsanni): Also track the width of the field box so that the
526
+ // options view maintains the same width as the field if its width
527
+ // changes but its constraints remain unchanged.
528
+ _fieldBoxConstraints.value = constraints;
529
+ return OverlayPortal .targetsRootOverlay (
525
530
key: _fieldKey,
526
- child: Shortcuts (
527
- shortcuts: _shortcuts,
528
- child: Actions (
529
- actions: _actionMap,
530
- child: CompositedTransformTarget (link: _optionsLayerLink, child: fieldView),
531
+ controller: _optionsViewController,
532
+ overlayChildBuilder: _buildOptionsView,
533
+ child: TextFieldTapRegion (
534
+ child: Shortcuts (
535
+ shortcuts: _shortcuts,
536
+ child: Actions (
537
+ actions: _actionMap,
538
+ child: CompositedTransformTarget (link: _optionsLayerLink, child: fieldView),
539
+ ),
531
540
),
532
541
),
542
+ );
543
+ },
544
+ );
545
+ }
546
+ }
547
+
548
+ class _RawAutocompleteOptions extends StatefulWidget {
549
+ const _RawAutocompleteOptions ({
550
+ required this .fieldKey,
551
+ required this .optionsLayerLink,
552
+ required this .optionsViewOpenDirection,
553
+ required this .overlayContext,
554
+ required this .textDirection,
555
+ required this .highlightIndexNotifier,
556
+ required this .builder,
557
+ required this .fieldConstraints,
558
+ });
559
+
560
+ final WidgetBuilder builder;
561
+ final GlobalKey fieldKey;
562
+
563
+ final LayerLink optionsLayerLink;
564
+ final OptionsViewOpenDirection optionsViewOpenDirection;
565
+ final BuildContext overlayContext;
566
+ final TextDirection ? textDirection;
567
+ final ValueNotifier <int > highlightIndexNotifier;
568
+ final BoxConstraints fieldConstraints;
569
+
570
+ @override
571
+ State <_RawAutocompleteOptions > createState () => _RawAutocompleteOptionsState ();
572
+ }
573
+
574
+ class _RawAutocompleteOptionsState extends State <_RawAutocompleteOptions > {
575
+ VoidCallback ? removeCompositionCallback;
576
+ Offset fieldOffset = Offset .zero;
577
+
578
+ // Get the field offset if the field's position changes when its layer tree
579
+ // is composited, which occurs for example if the field is in a scroll view.
580
+ Offset _getFieldOffset () {
581
+ final RenderBox ? fieldRenderBox =
582
+ widget.fieldKey.currentContext? .findRenderObject () as RenderBox ? ;
583
+ final RenderBox ? overlay =
584
+ Overlay .of (widget.overlayContext).context.findRenderObject () as RenderBox ? ;
585
+ return fieldRenderBox? .localToGlobal (Offset .zero, ancestor: overlay) ?? Offset .zero;
586
+ }
587
+
588
+ void _onLeaderComposition (Layer leaderLayer) {
589
+ SchedulerBinding .instance.addPostFrameCallback ((Duration duration) {
590
+ if (! mounted) {
591
+ return ;
592
+ }
593
+ final Offset nextFieldOffset = _getFieldOffset ();
594
+ if (nextFieldOffset != fieldOffset) {
595
+ setState (() {
596
+ fieldOffset = nextFieldOffset;
597
+ });
598
+ }
599
+ });
600
+ }
601
+
602
+ @override
603
+ void initState () {
604
+ super .initState ();
605
+ removeCompositionCallback = widget.optionsLayerLink.leader? .addCompositionCallback (
606
+ _onLeaderComposition,
607
+ );
608
+ }
609
+
610
+ @override
611
+ void didUpdateWidget (_RawAutocompleteOptions oldWidget) {
612
+ super .didUpdateWidget (oldWidget);
613
+ if (widget.optionsLayerLink.leader != oldWidget.optionsLayerLink.leader) {
614
+ removeCompositionCallback? .call ();
615
+ removeCompositionCallback = widget.optionsLayerLink.leader? .addCompositionCallback (
616
+ _onLeaderComposition,
617
+ );
618
+ }
619
+ }
620
+
621
+ @override
622
+ void dispose () {
623
+ removeCompositionCallback? .call ();
624
+ super .dispose ();
625
+ }
626
+
627
+ @override
628
+ Widget build (BuildContext context) {
629
+ return CompositedTransformFollower (
630
+ link: widget.optionsLayerLink,
631
+ followerAnchor: switch (widget.optionsViewOpenDirection) {
632
+ OptionsViewOpenDirection .up => Alignment .bottomLeft,
633
+ OptionsViewOpenDirection .down => Alignment .topLeft,
634
+ },
635
+ // When the field goes offscreen, don't show the options.
636
+ showWhenUnlinked: false ,
637
+ child: CustomSingleChildLayout (
638
+ delegate: _RawAutocompleteOptionsLayoutDelegate (
639
+ layerLink: widget.optionsLayerLink,
640
+ fieldOffset: fieldOffset,
641
+ optionsViewOpenDirection: widget.optionsViewOpenDirection,
642
+ textDirection: Directionality .of (context),
643
+ fieldConstraints: widget.fieldConstraints,
644
+ ),
645
+ child: TextFieldTapRegion (
646
+ child: AutocompleteHighlightedOption (
647
+ highlightIndexNotifier: widget.highlightIndexNotifier,
648
+ // optionsViewBuilder must be able to look up
649
+ // AutocompleteHighlightedOption in its context.
650
+ child: Builder (builder: widget.builder),
651
+ ),
533
652
),
534
653
),
535
654
);
536
655
}
537
656
}
538
657
658
+ /// Positions the options view.
659
+ class _RawAutocompleteOptionsLayoutDelegate extends SingleChildLayoutDelegate {
660
+ _RawAutocompleteOptionsLayoutDelegate ({
661
+ required this .layerLink,
662
+ required this .fieldOffset,
663
+ required this .optionsViewOpenDirection,
664
+ required this .textDirection,
665
+ required this .fieldConstraints,
666
+ }) : assert (layerLink.leaderSize != null );
667
+
668
+ /// Links the options in [RawAutocomplete.optionsViewBuilder] to the field in
669
+ /// [RawAutocomplete.fieldViewBuilder] .
670
+ final LayerLink layerLink;
671
+
672
+ /// The position of the field in [RawAutocomplete.fieldViewBuilder] .
673
+ final Offset fieldOffset;
674
+
675
+ /// A direction in which to open the options view overlay.
676
+ final OptionsViewOpenDirection optionsViewOpenDirection;
677
+
678
+ /// The [TextDirection] of this part of the widget tree.
679
+ final TextDirection textDirection;
680
+
681
+ /// The [BoxConstraints] for the field in [RawAutocomplete.fieldViewBuilder] .
682
+ final BoxConstraints fieldConstraints;
683
+
684
+ // A big enough height for about one item in the default
685
+ // Autocomplete.optionsViewBuilder. The assumption is that the user likely
686
+ // wants the list of options to move to stay on the screen rather than get any
687
+ // smaller than this. Allows Autocomplete to work when it has very little
688
+ // screen height available (as in b/317115348) by positioning itself on top of
689
+ // the field, while in other cases to size itself based on the height under
690
+ // the field.
691
+ static const double _kMinUsableHeight = kMinInteractiveDimension;
692
+
693
+ // Limits the child to the space above/below the field, with a minimum, and
694
+ // with the same maxWidth constraint as the field has.
695
+ @override
696
+ BoxConstraints getConstraintsForChild (BoxConstraints constraints) {
697
+ final Size fieldSize = layerLink.leaderSize! ;
698
+ return BoxConstraints (
699
+ // The field width may be zero if this is a split RawAutocomplete with no
700
+ // field of its own. In that case, don't change the constraints width.
701
+ maxWidth: fieldSize.width == 0.0 ? constraints.maxWidth : fieldSize.width,
702
+ maxHeight: max (_kMinUsableHeight, switch (optionsViewOpenDirection) {
703
+ OptionsViewOpenDirection .down => constraints.maxHeight - fieldOffset.dy - fieldSize.height,
704
+ OptionsViewOpenDirection .up => fieldOffset.dy,
705
+ }),
706
+ );
707
+ }
708
+
709
+ // Positions the child above/below the field and aligned with the left/right
710
+ // side based on text direction.
711
+ @override
712
+ Offset getPositionForChild (Size size, Size childSize) {
713
+ final Size fieldSize = layerLink.leaderSize! ;
714
+ final double dx = switch (textDirection) {
715
+ TextDirection .ltr => 0.0 ,
716
+ TextDirection .rtl => fieldSize.width - childSize.width,
717
+ };
718
+ final double dy = switch (optionsViewOpenDirection) {
719
+ OptionsViewOpenDirection .down => min (
720
+ fieldSize.height,
721
+ size.height - childSize.height - fieldOffset.dy,
722
+ ),
723
+ OptionsViewOpenDirection .up => size.height - min (childSize.height, fieldOffset.dy),
724
+ };
725
+ return Offset (dx, dy);
726
+ }
727
+
728
+ @override
729
+ bool shouldRelayout (_RawAutocompleteOptionsLayoutDelegate oldDelegate) {
730
+ if (! fieldOffset.isFinite || ! layerLink.leaderSize! .isFinite) {
731
+ return false ;
732
+ }
733
+ return layerLink != oldDelegate.layerLink ||
734
+ fieldOffset != oldDelegate.fieldOffset ||
735
+ optionsViewOpenDirection != oldDelegate.optionsViewOpenDirection ||
736
+ textDirection != oldDelegate.textDirection ||
737
+ fieldConstraints != oldDelegate.fieldConstraints;
738
+ }
739
+ }
740
+
539
741
class _AutocompleteCallbackAction <T extends Intent > extends CallbackAction <T > {
540
742
_AutocompleteCallbackAction ({required super .onInvoke, required this .isEnabledCallback});
541
743
0 commit comments