6
6
library ;
7
7
8
8
import 'dart:async' ;
9
- import 'dart:math' show max, min;
10
9
11
- import 'package:flutter/rendering.dart' ;
12
- import 'package:flutter/scheduler.dart' ;
13
10
import 'package:flutter/services.dart' ;
14
11
15
12
import 'actions.dart' ;
16
13
import 'basic.dart' ;
17
- import 'constants.dart' ;
18
14
import 'editable_text.dart' ;
19
15
import 'focus_manager.dart' ;
20
16
import 'framework.dart' ;
21
17
import 'inherited_notifier.dart' ;
22
- import 'layout_builder.dart' ;
23
18
import 'overlay.dart' ;
24
19
import 'shortcuts.dart' ;
25
20
import 'tap_region.dart' ;
26
- import 'value_listenable_builder.dart' ;
27
21
28
22
// Examples can assume:
29
23
// late BuildContext context;
@@ -219,10 +213,10 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
219
213
/// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
220
214
/// Builds the selectable options widgets from a list of options objects.
221
215
///
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] .
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] .
226
220
///
227
221
/// In order to track which item is highlighted by keyboard navigation, the
228
222
/// resulting options will be wrapped in an inherited
@@ -313,10 +307,6 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
313
307
class _RawAutocompleteState <T extends Object > extends State <RawAutocomplete <T >> {
314
308
final GlobalKey _fieldKey = GlobalKey ();
315
309
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
-
320
310
final OverlayPortalController _optionsViewController = OverlayPortalController (
321
311
debugLabel: '_RawAutocompleteState' ,
322
312
);
@@ -449,22 +439,30 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
449
439
}
450
440
451
441
Widget _buildOptionsView (BuildContext context) {
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),
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 (
461
459
highlightIndexNotifier: _highlightedOptionIndex,
462
- fieldConstraints : _fieldBoxConstraints.value ! ,
463
- builder : ( BuildContext context) {
464
- return widget.optionsViewBuilder (context, _select, _options);
465
- } ,
466
- );
467
- } ,
460
+ child : Builder (
461
+ builder :
462
+ ( BuildContext context) => widget.optionsViewBuilder (context, _select, _options),
463
+ ) ,
464
+ ),
465
+ ) ,
468
466
);
469
467
}
470
468
@@ -506,7 +504,6 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
506
504
widget.focusNode? .removeListener (_updateOptionsViewVisibility);
507
505
_internalFocusNode? .dispose ();
508
506
_highlightedOptionIndex.dispose ();
509
- _fieldBoxConstraints.dispose ();
510
507
super .dispose ();
511
508
}
512
509
@@ -520,224 +517,25 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
520
517
_onFieldSubmitted,
521
518
) ??
522
519
const SizedBox .shrink ();
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 (
520
+ return OverlayPortal .targetsRootOverlay (
521
+ controller: _optionsViewController,
522
+ overlayChildBuilder: _buildOptionsView,
523
+ child: TextFieldTapRegion (
524
+ child: SizedBox (
530
525
key: _fieldKey,
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
- ),
526
+ child: Shortcuts (
527
+ shortcuts: _shortcuts,
528
+ child: Actions (
529
+ actions: _actionMap,
530
+ child: CompositedTransformTarget (link: _optionsLayerLink, child: fieldView),
540
531
),
541
532
),
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
- ),
652
533
),
653
534
),
654
535
);
655
536
}
656
537
}
657
538
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
-
741
539
class _AutocompleteCallbackAction <T extends Intent > extends CallbackAction <T > {
742
540
_AutocompleteCallbackAction ({required super .onInvoke, required this .isEnabledCallback});
743
541
0 commit comments