Original PR: https://github.com/flutter/flutter/pull/143249 Revert PR: https://github.com/flutter/flutter/pull/161666 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
f545df54ff
commit
3297454732
@ -134,7 +134,7 @@ class Autocomplete<T extends Object> extends StatelessWidget {
|
|||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
options: options,
|
options: options,
|
||||||
openDirection: optionsViewOpenDirection,
|
openDirection: optionsViewOpenDirection,
|
||||||
maxOptionsHeight: optionsMaxHeight,
|
optionsMaxHeight: optionsMaxHeight,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
@ -176,7 +176,7 @@ class _AutocompleteOptions<T extends Object> extends StatelessWidget {
|
|||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
required this.openDirection,
|
required this.openDirection,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.maxOptionsHeight,
|
required this.optionsMaxHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
final AutocompleteOptionToString<T> displayStringForOption;
|
final AutocompleteOptionToString<T> displayStringForOption;
|
||||||
@ -185,7 +185,7 @@ class _AutocompleteOptions<T extends Object> extends StatelessWidget {
|
|||||||
final OptionsViewOpenDirection openDirection;
|
final OptionsViewOpenDirection openDirection;
|
||||||
|
|
||||||
final Iterable<T> options;
|
final Iterable<T> options;
|
||||||
final double maxOptionsHeight;
|
final double optionsMaxHeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -198,7 +198,7 @@ class _AutocompleteOptions<T extends Object> extends StatelessWidget {
|
|||||||
child: Material(
|
child: Material(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
|
constraints: BoxConstraints(maxHeight: optionsMaxHeight),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
@ -6,18 +6,24 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' show max, min;
|
||||||
|
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
|
import 'constants.dart';
|
||||||
import 'editable_text.dart';
|
import 'editable_text.dart';
|
||||||
import 'focus_manager.dart';
|
import 'focus_manager.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'inherited_notifier.dart';
|
import 'inherited_notifier.dart';
|
||||||
|
import 'layout_builder.dart';
|
||||||
import 'overlay.dart';
|
import 'overlay.dart';
|
||||||
import 'shortcuts.dart';
|
import 'shortcuts.dart';
|
||||||
import 'tap_region.dart';
|
import 'tap_region.dart';
|
||||||
|
import 'value_listenable_builder.dart';
|
||||||
|
|
||||||
// Examples can assume:
|
// Examples can assume:
|
||||||
// late BuildContext context;
|
// late BuildContext context;
|
||||||
@ -213,10 +219,10 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
|
|||||||
/// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
|
/// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
|
||||||
/// Builds the selectable options widgets from a list of options objects.
|
/// Builds the selectable options widgets from a list of options objects.
|
||||||
///
|
///
|
||||||
/// The options are displayed floating below or above the field using a
|
/// The options are displayed floating below or above the field inside of an
|
||||||
/// [CompositedTransformFollower] inside of an [Overlay], not at the same
|
/// [Overlay], not at the same place in the widget tree as [RawAutocomplete].
|
||||||
/// place in the widget tree as [RawAutocomplete]. To control whether it opens
|
/// To control whether it opens upward or downward, use
|
||||||
/// upward or downward, use [optionsViewOpenDirection].
|
/// [optionsViewOpenDirection].
|
||||||
///
|
///
|
||||||
/// In order to track which item is highlighted by keyboard navigation, the
|
/// In order to track which item is highlighted by keyboard navigation, the
|
||||||
/// resulting options will be wrapped in an inherited
|
/// resulting options will be wrapped in an inherited
|
||||||
@ -307,6 +313,10 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
|
|||||||
class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
|
class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
|
||||||
final GlobalKey _fieldKey = GlobalKey();
|
final GlobalKey _fieldKey = GlobalKey();
|
||||||
final LayerLink _optionsLayerLink = LayerLink();
|
final LayerLink _optionsLayerLink = LayerLink();
|
||||||
|
|
||||||
|
/// The box constraints that the field was last built with.
|
||||||
|
final ValueNotifier<BoxConstraints?> _fieldBoxConstraints = ValueNotifier<BoxConstraints?>(null);
|
||||||
|
|
||||||
final OverlayPortalController _optionsViewController = OverlayPortalController(
|
final OverlayPortalController _optionsViewController = OverlayPortalController(
|
||||||
debugLabel: '_RawAutocompleteState',
|
debugLabel: '_RawAutocompleteState',
|
||||||
);
|
);
|
||||||
@ -439,30 +449,22 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOptionsView(BuildContext context) {
|
Widget _buildOptionsView(BuildContext context) {
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
return ValueListenableBuilder<BoxConstraints?>(
|
||||||
final Alignment followerAlignment = switch (widget.optionsViewOpenDirection) {
|
valueListenable: _fieldBoxConstraints,
|
||||||
OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart,
|
builder: (BuildContext context, BoxConstraints? constraints, Widget? child) {
|
||||||
OptionsViewOpenDirection.down => AlignmentDirectional.topStart,
|
return _RawAutocompleteOptions(
|
||||||
}.resolve(textDirection);
|
fieldKey: _fieldKey,
|
||||||
final Alignment targetAnchor = switch (widget.optionsViewOpenDirection) {
|
optionsLayerLink: _optionsLayerLink,
|
||||||
OptionsViewOpenDirection.up => AlignmentDirectional.topStart,
|
optionsViewOpenDirection: widget.optionsViewOpenDirection,
|
||||||
OptionsViewOpenDirection.down => AlignmentDirectional.bottomStart,
|
overlayContext: context,
|
||||||
}.resolve(textDirection);
|
textDirection: Directionality.maybeOf(context),
|
||||||
|
|
||||||
return CompositedTransformFollower(
|
|
||||||
link: _optionsLayerLink,
|
|
||||||
showWhenUnlinked: false,
|
|
||||||
targetAnchor: targetAnchor,
|
|
||||||
followerAnchor: followerAlignment,
|
|
||||||
child: TextFieldTapRegion(
|
|
||||||
child: AutocompleteHighlightedOption(
|
|
||||||
highlightIndexNotifier: _highlightedOptionIndex,
|
highlightIndexNotifier: _highlightedOptionIndex,
|
||||||
child: Builder(
|
fieldConstraints: _fieldBoxConstraints.value!,
|
||||||
builder:
|
builder: (BuildContext context) {
|
||||||
(BuildContext context) => widget.optionsViewBuilder(context, _select, _options),
|
return widget.optionsViewBuilder(context, _select, _options);
|
||||||
),
|
},
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,6 +506,7 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
|||||||
widget.focusNode?.removeListener(_updateOptionsViewVisibility);
|
widget.focusNode?.removeListener(_updateOptionsViewVisibility);
|
||||||
_internalFocusNode?.dispose();
|
_internalFocusNode?.dispose();
|
||||||
_highlightedOptionIndex.dispose();
|
_highlightedOptionIndex.dispose();
|
||||||
|
_fieldBoxConstraints.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -517,12 +520,17 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
|||||||
_onFieldSubmitted,
|
_onFieldSubmitted,
|
||||||
) ??
|
) ??
|
||||||
const SizedBox.shrink();
|
const SizedBox.shrink();
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
// TODO(victorsanni): Also track the width of the field box so that the
|
||||||
|
// options view maintains the same width as the field if its width
|
||||||
|
// changes but its constraints remain unchanged.
|
||||||
|
_fieldBoxConstraints.value = constraints;
|
||||||
return OverlayPortal.targetsRootOverlay(
|
return OverlayPortal.targetsRootOverlay(
|
||||||
|
key: _fieldKey,
|
||||||
controller: _optionsViewController,
|
controller: _optionsViewController,
|
||||||
overlayChildBuilder: _buildOptionsView,
|
overlayChildBuilder: _buildOptionsView,
|
||||||
child: TextFieldTapRegion(
|
child: TextFieldTapRegion(
|
||||||
child: SizedBox(
|
|
||||||
key: _fieldKey,
|
|
||||||
child: Shortcuts(
|
child: Shortcuts(
|
||||||
shortcuts: _shortcuts,
|
shortcuts: _shortcuts,
|
||||||
child: Actions(
|
child: Actions(
|
||||||
@ -531,11 +539,205 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RawAutocompleteOptions extends StatefulWidget {
|
||||||
|
const _RawAutocompleteOptions({
|
||||||
|
required this.fieldKey,
|
||||||
|
required this.optionsLayerLink,
|
||||||
|
required this.optionsViewOpenDirection,
|
||||||
|
required this.overlayContext,
|
||||||
|
required this.textDirection,
|
||||||
|
required this.highlightIndexNotifier,
|
||||||
|
required this.builder,
|
||||||
|
required this.fieldConstraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
final GlobalKey fieldKey;
|
||||||
|
|
||||||
|
final LayerLink optionsLayerLink;
|
||||||
|
final OptionsViewOpenDirection optionsViewOpenDirection;
|
||||||
|
final BuildContext overlayContext;
|
||||||
|
final TextDirection? textDirection;
|
||||||
|
final ValueNotifier<int> highlightIndexNotifier;
|
||||||
|
final BoxConstraints fieldConstraints;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RawAutocompleteOptions> createState() => _RawAutocompleteOptionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RawAutocompleteOptionsState extends State<_RawAutocompleteOptions> {
|
||||||
|
VoidCallback? removeCompositionCallback;
|
||||||
|
Offset fieldOffset = Offset.zero;
|
||||||
|
|
||||||
|
// Get the field offset if the field's position changes when its layer tree
|
||||||
|
// is composited, which occurs for example if the field is in a scroll view.
|
||||||
|
Offset _getFieldOffset() {
|
||||||
|
final RenderBox? fieldRenderBox =
|
||||||
|
widget.fieldKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
final RenderBox? overlay =
|
||||||
|
Overlay.of(widget.overlayContext).context.findRenderObject() as RenderBox?;
|
||||||
|
return fieldRenderBox?.localToGlobal(Offset.zero, ancestor: overlay) ?? Offset.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLeaderComposition(Layer leaderLayer) {
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Offset nextFieldOffset = _getFieldOffset();
|
||||||
|
if (nextFieldOffset != fieldOffset) {
|
||||||
|
setState(() {
|
||||||
|
fieldOffset = nextFieldOffset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
removeCompositionCallback = widget.optionsLayerLink.leader?.addCompositionCallback(
|
||||||
|
_onLeaderComposition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_RawAutocompleteOptions oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.optionsLayerLink.leader != oldWidget.optionsLayerLink.leader) {
|
||||||
|
removeCompositionCallback?.call();
|
||||||
|
removeCompositionCallback = widget.optionsLayerLink.leader?.addCompositionCallback(
|
||||||
|
_onLeaderComposition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
removeCompositionCallback?.call();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CompositedTransformFollower(
|
||||||
|
link: widget.optionsLayerLink,
|
||||||
|
followerAnchor: switch (widget.optionsViewOpenDirection) {
|
||||||
|
OptionsViewOpenDirection.up => Alignment.bottomLeft,
|
||||||
|
OptionsViewOpenDirection.down => Alignment.topLeft,
|
||||||
|
},
|
||||||
|
// When the field goes offscreen, don't show the options.
|
||||||
|
showWhenUnlinked: false,
|
||||||
|
child: CustomSingleChildLayout(
|
||||||
|
delegate: _RawAutocompleteOptionsLayoutDelegate(
|
||||||
|
layerLink: widget.optionsLayerLink,
|
||||||
|
fieldOffset: fieldOffset,
|
||||||
|
optionsViewOpenDirection: widget.optionsViewOpenDirection,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
fieldConstraints: widget.fieldConstraints,
|
||||||
|
),
|
||||||
|
child: TextFieldTapRegion(
|
||||||
|
child: AutocompleteHighlightedOption(
|
||||||
|
highlightIndexNotifier: widget.highlightIndexNotifier,
|
||||||
|
// optionsViewBuilder must be able to look up
|
||||||
|
// AutocompleteHighlightedOption in its context.
|
||||||
|
child: Builder(builder: widget.builder),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Positions the options view.
|
||||||
|
class _RawAutocompleteOptionsLayoutDelegate extends SingleChildLayoutDelegate {
|
||||||
|
_RawAutocompleteOptionsLayoutDelegate({
|
||||||
|
required this.layerLink,
|
||||||
|
required this.fieldOffset,
|
||||||
|
required this.optionsViewOpenDirection,
|
||||||
|
required this.textDirection,
|
||||||
|
required this.fieldConstraints,
|
||||||
|
}) : assert(layerLink.leaderSize != null);
|
||||||
|
|
||||||
|
/// Links the options in [RawAutocomplete.optionsViewBuilder] to the field in
|
||||||
|
/// [RawAutocomplete.fieldViewBuilder].
|
||||||
|
final LayerLink layerLink;
|
||||||
|
|
||||||
|
/// The position of the field in [RawAutocomplete.fieldViewBuilder].
|
||||||
|
final Offset fieldOffset;
|
||||||
|
|
||||||
|
/// A direction in which to open the options view overlay.
|
||||||
|
final OptionsViewOpenDirection optionsViewOpenDirection;
|
||||||
|
|
||||||
|
/// The [TextDirection] of this part of the widget tree.
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
/// The [BoxConstraints] for the field in [RawAutocomplete.fieldViewBuilder].
|
||||||
|
final BoxConstraints fieldConstraints;
|
||||||
|
|
||||||
|
// A big enough height for about one item in the default
|
||||||
|
// Autocomplete.optionsViewBuilder. The assumption is that the user likely
|
||||||
|
// wants the list of options to move to stay on the screen rather than get any
|
||||||
|
// smaller than this. Allows Autocomplete to work when it has very little
|
||||||
|
// screen height available (as in b/317115348) by positioning itself on top of
|
||||||
|
// the field, while in other cases to size itself based on the height under
|
||||||
|
// the field.
|
||||||
|
static const double _kMinUsableHeight = kMinInteractiveDimension;
|
||||||
|
|
||||||
|
// Limits the child to the space above/below the field, with a minimum, and
|
||||||
|
// with the same maxWidth constraint as the field has.
|
||||||
|
@override
|
||||||
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||||
|
final Size fieldSize = layerLink.leaderSize!;
|
||||||
|
return BoxConstraints(
|
||||||
|
// The field width may be zero if this is a split RawAutocomplete with no
|
||||||
|
// field of its own. In that case, don't change the constraints width.
|
||||||
|
maxWidth: fieldSize.width == 0.0 ? constraints.maxWidth : fieldSize.width,
|
||||||
|
maxHeight: max(_kMinUsableHeight, switch (optionsViewOpenDirection) {
|
||||||
|
OptionsViewOpenDirection.down => constraints.maxHeight - fieldOffset.dy - fieldSize.height,
|
||||||
|
OptionsViewOpenDirection.up => fieldOffset.dy,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions the child above/below the field and aligned with the left/right
|
||||||
|
// side based on text direction.
|
||||||
|
@override
|
||||||
|
Offset getPositionForChild(Size size, Size childSize) {
|
||||||
|
final Size fieldSize = layerLink.leaderSize!;
|
||||||
|
final double dx = switch (textDirection) {
|
||||||
|
TextDirection.ltr => 0.0,
|
||||||
|
TextDirection.rtl => fieldSize.width - childSize.width,
|
||||||
|
};
|
||||||
|
final double dy = switch (optionsViewOpenDirection) {
|
||||||
|
OptionsViewOpenDirection.down => min(
|
||||||
|
fieldSize.height,
|
||||||
|
size.height - childSize.height - fieldOffset.dy,
|
||||||
|
),
|
||||||
|
OptionsViewOpenDirection.up => size.height - min(childSize.height, fieldOffset.dy),
|
||||||
|
};
|
||||||
|
return Offset(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRelayout(_RawAutocompleteOptionsLayoutDelegate oldDelegate) {
|
||||||
|
if (!fieldOffset.isFinite || !layerLink.leaderSize!.isFinite) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return layerLink != oldDelegate.layerLink ||
|
||||||
|
fieldOffset != oldDelegate.fieldOffset ||
|
||||||
|
optionsViewOpenDirection != oldDelegate.optionsViewOpenDirection ||
|
||||||
|
textDirection != oldDelegate.textDirection ||
|
||||||
|
fieldConstraints != oldDelegate.fieldConstraints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
|
class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
|
||||||
_AutocompleteCallbackAction({required super.onInvoke, required this.isEnabledCallback});
|
_AutocompleteCallbackAction({required super.onInvoke, required this.isEnabledCallback});
|
||||||
|
|
||||||
|
@ -592,6 +592,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.byType(RawAutocomplete<String>));
|
await tester.tap(find.byType(RawAutocomplete<String>));
|
||||||
await tester.enterText(find.byType(RawAutocomplete<String>), 'a');
|
await tester.enterText(find.byType(RawAutocomplete<String>), 'a');
|
||||||
|
await tester.pump();
|
||||||
expect(find.text('aa').hitTestable(), findsOneWidget);
|
expect(find.text('aa').hitTestable(), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user