roll to 5a279edc239f4efd91223ddc609c2b5129325feb
diff --git a/engine/lib/src/platform_dispatcher.dart b/engine/lib/src/platform_dispatcher.dart
index 305c704..d7837b0 100644
--- a/engine/lib/src/platform_dispatcher.dart
+++ b/engine/lib/src/platform_dispatcher.dart
@@ -122,11 +122,14 @@
/// the application.
///
/// If any of their configurations change, [onMetricsChanged] will be called.
- Iterable<FlutterView> get views => _views.values;
- Map<Object, FlutterView> _views = <Object, FlutterView>{};
+ Iterable<FlutterView> get views => viewData.values;
+ Map<Object, FlutterView> viewData = <Object, FlutterView>{};
- // A map of opaque platform view identifiers to view configurations.
- Map<Object, ViewConfiguration> _viewConfigurations = <Object, ViewConfiguration>{};
+ Map<Object, ViewConfiguration> get windowConfigurations => _windowConfigurations;
+ final Map<Object, ViewConfiguration> _windowConfigurations =
+ <Object, ViewConfiguration>{};
+
+ FlutterView? get implicitView => viewData[kImplicitViewId];
/// A callback that is invoked whenever the [ViewConfiguration] of any of the
/// [views] changes.
@@ -181,33 +184,33 @@
List<int> displayFeaturesState,
) {
final ViewConfiguration previousConfiguration =
- _viewConfigurations[id] ?? const ViewConfiguration();
- if (!_views.containsKey(id)) {
- _views[id] = FlutterWindow._(id, this);
+ _windowConfigurations[id] ?? const ViewConfiguration();
+ if (!viewData.containsKey(id)) {
+ viewData[id] = FlutterView._(id, this);
}
- _viewConfigurations[id] = previousConfiguration.copyWith(
- window: _views[id],
+ _windowConfigurations[id] = previousConfiguration.copyWith(
+ window: viewData[id],
devicePixelRatio: devicePixelRatio,
geometry: Rect.fromLTWH(0.0, 0.0, width, height),
- viewPadding: WindowPadding._(
+ viewPadding: ViewPadding._(
top: viewPaddingTop,
right: viewPaddingRight,
bottom: viewPaddingBottom,
left: viewPaddingLeft,
),
- viewInsets: WindowPadding._(
+ viewInsets: ViewPadding._(
top: viewInsetTop,
right: viewInsetRight,
bottom: viewInsetBottom,
left: viewInsetLeft,
),
- padding: WindowPadding._(
+ padding: ViewPadding._(
top: math.max(0.0, viewPaddingTop - viewInsetTop),
right: math.max(0.0, viewPaddingRight - viewInsetRight),
bottom: math.max(0.0, viewPaddingBottom - viewInsetBottom),
left: math.max(0.0, viewPaddingLeft - viewInsetLeft),
),
- systemGestureInsets: WindowPadding._(
+ systemGestureInsets: ViewPadding._(
top: math.max(0.0, systemGestureInsetTop),
right: math.max(0.0, systemGestureInsetRight),
bottom: math.max(0.0, systemGestureInsetBottom),
@@ -1019,10 +1022,10 @@
this.devicePixelRatio = 1.0,
this.geometry = Rect.zero,
this.visible = false,
- this.viewInsets = WindowPadding.zero,
- this.viewPadding = WindowPadding.zero,
- this.systemGestureInsets = WindowPadding.zero,
- this.padding = WindowPadding.zero,
+ this.viewInsets = ViewPadding.zero,
+ this.viewPadding = ViewPadding.zero,
+ this.systemGestureInsets = ViewPadding.zero,
+ this.padding = ViewPadding.zero,
this.gestureSettings = const GestureSettings(),
this.displayFeatures = const <DisplayFeature>[],
});
@@ -1032,10 +1035,10 @@
double? devicePixelRatio,
Rect? geometry,
bool? visible,
- WindowPadding? viewInsets,
- WindowPadding? viewPadding,
- WindowPadding? systemGestureInsets,
- WindowPadding? padding,
+ ViewPadding? viewInsets,
+ ViewPadding? viewPadding,
+ ViewPadding? systemGestureInsets,
+ ViewPadding? padding,
GestureSettings? gestureSettings,
List<DisplayFeature>? displayFeatures,
}) {
@@ -1057,10 +1060,10 @@
final double devicePixelRatio;
final Rect geometry;
final bool visible;
- final WindowPadding viewInsets;
- final WindowPadding viewPadding;
- final WindowPadding systemGestureInsets;
- final WindowPadding padding;
+ final ViewPadding viewInsets;
+ final ViewPadding viewPadding;
+ final ViewPadding systemGestureInsets;
+ final ViewPadding padding;
final GestureSettings gestureSettings;
final List<DisplayFeature> displayFeatures;
@@ -1250,43 +1253,25 @@
detached,
}
-/// A representation of distances for each of the four edges of a rectangle,
-/// used to encode the view insets and padding that applications should place
-/// around their user interface, as exposed by [FlutterView.viewInsets] and
-/// [FlutterView.padding]. View insets and padding are preferably read via
-/// [MediaQuery.of].
-///
-/// For a generic class that represents distances around a rectangle, see the
-/// [EdgeInsets] class.
-///
-/// See also:
-///
-/// * [WidgetsBindingObserver], for a widgets layer mechanism to receive
-/// notifications when the padding changes.
-/// * [MediaQuery.of], for the preferred mechanism for accessing these values.
-/// * [Scaffold], which automatically applies the padding in material design
-/// applications.
-class WindowPadding {
- const WindowPadding._({ required this.left, required this.top, required this.right, required this.bottom });
+@Deprecated(
+ 'Use ViewPadding instead. '
+ 'This feature was deprecated after v3.8.0-14.0.pre.',
+)
+typedef WindowPadding = ViewPadding;
- /// The distance from the left edge to the first unpadded pixel, in physical pixels.
+class ViewPadding {
+ const ViewPadding._({ required this.left, required this.top, required this.right, required this.bottom });
+
final double left;
-
- /// The distance from the top edge to the first unpadded pixel, in physical pixels.
final double top;
-
- /// The distance from the right edge to the first unpadded pixel, in physical pixels.
final double right;
-
- /// The distance from the bottom edge to the first unpadded pixel, in physical pixels.
final double bottom;
- /// A window padding that has zeros for each edge.
- static const WindowPadding zero = WindowPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0);
+ static const ViewPadding zero = ViewPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0);
@override
String toString() {
- return 'WindowPadding(left: $left, top: $top, right: $right, bottom: $bottom)';
+ return 'ViewPadding(left: $left, top: $top, right: $right, bottom: $bottom)';
}
}
diff --git a/engine/lib/src/window.dart b/engine/lib/src/window.dart
index db46749..378d534 100644
--- a/engine/lib/src/window.dart
+++ b/engine/lib/src/window.dart
@@ -7,254 +7,35 @@
part of dart.ui;
-/// A view into which a Flutter [Scene] is drawn.
-///
-/// Each [FlutterView] has its own layer tree that is rendered into an area
-/// inside of a [FlutterWindow] whenever [render] is called with a [Scene].
-///
-/// ## Insets and Padding
-///
-/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/widgets/window_padding.mp4}
-///
-/// In this illustration, the black areas represent system UI that the app
-/// cannot draw over. The red area represents view padding that the view may not
-/// be able to detect gestures in and may not want to draw in. The grey area
-/// represents the system keyboard, which can cover over the bottom view padding
-/// when visible.
-///
-/// The [viewInsets] are the physical pixels which the operating
-/// system reserves for system UI, such as the keyboard, which would fully
-/// obscure any content drawn in that area.
-///
-/// The [viewPadding] are the physical pixels on each side of the
-/// display that may be partially obscured by system UI or by physical
-/// intrusions into the display, such as an overscan region on a television or a
-/// "notch" on a phone. Unlike the insets, these areas may have portions that
-/// show the user view-painted pixels without being obscured, such as a
-/// notch at the top of a phone that covers only a subset of the area. Insets,
-/// on the other hand, either partially or fully obscure the window, such as an
-/// opaque keyboard or a partially translucent status bar, which cover an area
-/// without gaps.
-///
-/// The [padding] property is computed from both
-/// [viewInsets] and [viewPadding]. It will allow a
-/// view inset to consume view padding where appropriate, such as when a phone's
-/// keyboard is covering the bottom view padding and so "absorbs" it.
-///
-/// Clients that want to position elements relative to the view padding
-/// regardless of the view insets should use the [viewPadding]
-/// property, e.g. if you wish to draw a widget at the center of the screen with
-/// respect to the iPhone "safe area" regardless of whether the keyboard is
-/// showing.
-///
-/// [padding] is useful for clients that want to know how much
-/// padding should be accounted for without concern for the current inset(s)
-/// state, e.g. determining whether a gesture should be considered for scrolling
-/// purposes. This value varies based on the current state of the insets. For
-/// example, a visible keyboard will consume all gestures in the bottom part of
-/// the [viewPadding] anyway, so there is no need to account for
-/// that in the [padding], which is always safe to use for such
-/// calculations.
-///
-/// See also:
-///
-/// * [FlutterWindow], a special case of a [FlutterView] that is represented on
-/// the platform as a separate window which can host other [FlutterView]s.
-abstract class FlutterView {
+const int kImplicitViewId = 0;
+
+class FlutterView {
+ FlutterView._(this.viewId, this.platformDispatcher);
+
+ /// The opaque ID for this view.
+ final Object viewId;
+
/// The platform dispatcher that this view is registered with, and gets its
/// information from.
- PlatformDispatcher get platformDispatcher;
+ final PlatformDispatcher platformDispatcher;
- /// The configuration of this view.
- ViewConfiguration get viewConfiguration;
+ ViewConfiguration get _viewConfiguration {
+ final PlatformDispatcher engineDispatcher = platformDispatcher;
+ assert(engineDispatcher.windowConfigurations.containsKey(viewId));
+ return engineDispatcher.windowConfigurations[viewId] ??
+ const ViewConfiguration();
+ }
- /// The number of device pixels for each logical pixel for the screen this
- /// view is displayed on.
- ///
- /// This number might not be a power of two. Indeed, it might not even be an
- /// integer. For example, the Nexus 6 has a device pixel ratio of 3.5.
- ///
- /// Device pixels are also referred to as physical pixels. Logical pixels are
- /// also referred to as device-independent or resolution-independent pixels.
- ///
- /// By definition, there are roughly 38 logical pixels per centimeter, or
- /// about 96 logical pixels per inch, of the physical display. The value
- /// returned by [devicePixelRatio] is ultimately obtained either from the
- /// hardware itself, the device drivers, or a hard-coded value stored in the
- /// operating system or firmware, and may be inaccurate, sometimes by a
- /// significant margin.
- ///
- /// The Flutter framework operates in logical pixels, so it is rarely
- /// necessary to directly deal with this property.
- ///
- /// When this changes, [onMetricsChanged] is called.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- double get devicePixelRatio => viewConfiguration.devicePixelRatio;
+ double get devicePixelRatio => _viewConfiguration.devicePixelRatio;
+ Rect get physicalGeometry => _viewConfiguration.geometry;
+ Size get physicalSize => _viewConfiguration.geometry.size;
+ ViewPadding get viewInsets => _viewConfiguration.viewInsets;
+ ViewPadding get viewPadding => _viewConfiguration.viewPadding;
+ ViewPadding get systemGestureInsets => _viewConfiguration.systemGestureInsets;
+ ViewPadding get padding => _viewConfiguration.padding;
+ GestureSettings get gestureSettings => _viewConfiguration.gestureSettings;
+ List<DisplayFeature> get displayFeatures => _viewConfiguration.displayFeatures;
- /// The dimensions and location of the rectangle into which the scene rendered
- /// in this view will be drawn on the screen, in physical pixels.
- ///
- /// When this changes, [onMetricsChanged] is called.
- ///
- /// At startup, the size and location of the view may not be known before Dart
- /// code runs. If this value is observed early in the application lifecycle,
- /// it may report [Rect.zero].
- ///
- /// This value does not take into account any on-screen keyboards or other
- /// system UI. The [padding] and [viewInsets] properties provide a view into
- /// how much of each side of the view may be obscured by system UI.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- Rect get physicalGeometry => viewConfiguration.geometry;
-
- /// The dimensions of the rectangle into which the scene rendered in this view
- /// will be drawn on the screen, in physical pixels.
- ///
- /// When this changes, [onMetricsChanged] is called.
- ///
- /// At startup, the size of the view may not be known before Dart code runs.
- /// If this value is observed early in the application lifecycle, it may
- /// report [Size.zero].
- ///
- /// This value does not take into account any on-screen keyboards or other
- /// system UI. The [padding] and [viewInsets] properties provide information
- /// about how much of each side of the view may be obscured by system UI.
- ///
- /// This value is the same as the `size` member of [physicalGeometry].
- ///
- /// See also:
- ///
- /// * [physicalGeometry], which reports the location of the view as well as
- /// its size.
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- Size get physicalSize => viewConfiguration.geometry.size;
-
- /// The number of physical pixels on each side of the display rectangle into
- /// which the view can render, but over which the operating system will likely
- /// place system UI, such as the keyboard, that fully obscures any content.
- ///
- /// When this property changes, [onMetricsChanged] is called.
- ///
- /// The relationship between this [viewInsets],
- /// [viewPadding], and [padding] are described in
- /// more detail in the documentation for [FlutterView].
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- /// * [MediaQuery.of], a simpler mechanism for the same.
- /// * [Scaffold], which automatically applies the view insets in material
- /// design applications.
- WindowPadding get viewInsets => viewConfiguration.viewInsets;
-
- /// The number of physical pixels on each side of the display rectangle into
- /// which the view can render, but which may be partially obscured by system
- /// UI (such as the system notification area), or or physical intrusions in
- /// the display (e.g. overscan regions on television screens or phone sensor
- /// housings).
- ///
- /// Unlike [padding], this value does not change relative to
- /// [viewInsets]. For example, on an iPhone X, it will not
- /// change in response to the soft keyboard being visible or hidden, whereas
- /// [padding] will.
- ///
- /// When this property changes, [onMetricsChanged] is called.
- ///
- /// The relationship between this [viewInsets],
- /// [viewPadding], and [padding] are described in
- /// more detail in the documentation for [FlutterView].
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- /// * [MediaQuery.of], a simpler mechanism for the same.
- /// * [Scaffold], which automatically applies the padding in material design
- /// applications.
- WindowPadding get viewPadding => viewConfiguration.viewPadding;
-
- /// The number of physical pixels on each side of the display rectangle into
- /// which the view can render, but where the operating system will consume
- /// input gestures for the sake of system navigation.
- ///
- /// For example, an operating system might use the vertical edges of the
- /// screen, where swiping inwards from the edges takes users backward
- /// through the history of screens they previously visited.
- ///
- /// When this property changes, [onMetricsChanged] is called.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- /// * [MediaQuery.of], a simpler mechanism for the same.
- WindowPadding get systemGestureInsets => viewConfiguration.systemGestureInsets;
-
- /// The number of physical pixels on each side of the display rectangle into
- /// which the view can render, but which may be partially obscured by system
- /// UI (such as the system notification area), or or physical intrusions in
- /// the display (e.g. overscan regions on television screens or phone sensor
- /// housings).
- ///
- /// This value is calculated by taking `max(0.0, FlutterView.viewPadding -
- /// FlutterView.viewInsets)`. This will treat a system IME that increases the
- /// bottom inset as consuming that much of the bottom padding. For example, on
- /// an iPhone X, [EdgeInsets.bottom] of [FlutterView.padding] is the same as
- /// [EdgeInsets.bottom] of [FlutterView.viewPadding] when the soft keyboard is
- /// not drawn (to account for the bottom soft button area), but will be `0.0`
- /// when the soft keyboard is visible.
- ///
- /// When this changes, [onMetricsChanged] is called.
- ///
- /// The relationship between this [viewInsets], [viewPadding], and [padding]
- /// are described in more detail in the documentation for [FlutterView].
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
- /// * [MediaQuery.of], a simpler mechanism for the same.
- /// * [Scaffold], which automatically applies the padding in material design
- /// applications.
- WindowPadding get padding => viewConfiguration.padding;
-
- /// Updates the view's rendering on the GPU with the newly provided [Scene].
- ///
- /// This function must be called within the scope of the
- /// [PlatformDispatcher.onBeginFrame] or [PlatformDispatcher.onDrawFrame]
- /// callbacks being invoked.
- ///
- /// If this function is called a second time during a single
- /// [PlatformDispatcher.onBeginFrame]/[PlatformDispatcher.onDrawFrame]
- /// callback sequence or called outside the scope of those callbacks, the call
- /// will be ignored.
- ///
- /// To record graphical operations, first create a [PictureRecorder], then
- /// construct a [Canvas], passing that [PictureRecorder] to its constructor.
- /// After issuing all the graphical operations, call the
- /// [PictureRecorder.endRecording] function on the [PictureRecorder] to obtain
- /// the final [Picture] that represents the issued graphical operations.
- ///
- /// Next, create a [SceneBuilder], and add the [Picture] to it using
- /// [SceneBuilder.addPicture]. With the [SceneBuilder.build] method you can
- /// then obtain a [Scene] object, which you can display to the user via this
- /// [render] function.
- ///
- /// See also:
- ///
- /// * [SchedulerBinding], the Flutter framework class which manages the
- /// scheduling of frames.
- /// * [RendererBinding], the Flutter framework class which manages layout and
- /// painting.
void render(Scene scene) {
// TODO(yjbanov): implement a basic preroll for better benchmark realism.
}
@@ -262,58 +43,9 @@
void updateSemantics(SemanticsUpdate update) {
platformDispatcher.updateSemantics(update);
}
-
- List<DisplayFeature> get displayFeatures => viewConfiguration.displayFeatures;
}
-/// A top-level platform window displaying a Flutter layer tree drawn from a
-/// [Scene].
-///
-/// The current list of all Flutter views for the application is available from
-/// `WidgetsBinding.instance.platformDispatcher.views`. Only views that are of type
-/// [FlutterWindow] are top level platform windows.
-///
-/// There is also a [PlatformDispatcher.instance] singleton object in `dart:ui`
-/// if `WidgetsBinding` is unavailable, but we strongly advise avoiding a static
-/// reference to it. See the documentation for [PlatformDispatcher.instance] for
-/// more details about why it should be avoided.
-///
-/// See also:
-///
-/// * [PlatformDispatcher], which manages the current list of [FlutterView] (and
-/// thus [FlutterWindow]) instances.
-class FlutterWindow extends FlutterView {
- FlutterWindow._(this._windowId, this.platformDispatcher);
-
- /// The opaque ID for this view.
- final Object _windowId;
-
- @override
- final PlatformDispatcher platformDispatcher;
-
- @override
- ViewConfiguration get viewConfiguration {
- assert(platformDispatcher._viewConfigurations.containsKey(_windowId));
- return platformDispatcher._viewConfigurations[_windowId]!;
- }
-}
-
-/// A [FlutterWindow] that includes access to setting callbacks and retrieving
-/// properties that reside on the [PlatformDispatcher].
-///
-/// It is the type of the global [window] singleton used by applications that
-/// only have a single main window.
-///
-/// In addition to the properties of [FlutterView], this class provides access
-/// to platform-specific properties. To modify or retrieve these properties,
-/// applications designed for more than one main window should prefer using
-/// `WidgetsBinding.instance.platformDispatcher` instead.
-///
-/// Prefer access through `WidgetsBinding.instance.window` or
-/// `WidgetsBinding.instance.platformDispatcher` over a static reference to
-/// [window], or [PlatformDispatcher.instance]. See the documentation for
-/// [PlatformDispatcher.instance] for more details about this recommendation.
-class SingletonFlutterWindow extends FlutterWindow {
+class SingletonFlutterWindow extends FlutterView {
SingletonFlutterWindow._(Object windowId, PlatformDispatcher platformDispatcher)
: super._(windowId, platformDispatcher) {
platformDispatcher._updateLifecycleState('resumed');
@@ -343,412 +75,99 @@
);
}
- /// A callback that is invoked whenever the [devicePixelRatio],
- /// [physicalSize], [padding], [viewInsets], [PlatformDispatcher.views], or
- /// [systemGestureInsets] values change.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// See [PlatformDispatcher.onMetricsChanged] for more information.
VoidCallback? get onMetricsChanged => platformDispatcher.onMetricsChanged;
set onMetricsChanged(VoidCallback? callback) {
platformDispatcher.onMetricsChanged = callback;
}
- /// The system-reported default locale of the device.
- ///
- /// {@template dart.ui.window.accessorForwardWarning}
- /// Accessing this value returns the value contained in the
- /// [PlatformDispatcher] singleton, so instead of getting it from here, you
- /// should consider getting it from `WidgetsBinding.instance.platformDispatcher` instead
- /// (or, when `WidgetsBinding` isn't available, from
- /// [PlatformDispatcher.instance]). The reason this value forwards to the
- /// [PlatformDispatcher] is to provide convenience for applications that only
- /// use a single main window.
- /// {@endtemplate}
- ///
- /// This establishes the language and formatting conventions that window
- /// should, if possible, use to render their user interface.
- ///
- /// This is the first locale selected by the user and is the user's primary
- /// locale (the locale the device UI is displayed in)
- ///
- /// This is equivalent to `locales.first` and will provide an empty non-null
- /// locale if the [locales] list has not been set or is empty.
Locale get locale => platformDispatcher.locale;
- /// The full system-reported supported locales of the device.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This establishes the language and formatting conventions that window
- /// should, if possible, use to render their user interface.
- ///
- /// The list is ordered in order of priority, with lower-indexed locales being
- /// preferred over higher-indexed ones. The first element is the primary [locale].
- ///
- /// The [onLocaleChanged] callback is called whenever this value changes.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
List<Locale> get locales => platformDispatcher.locales;
- /// Performs the platform-native locale resolution.
- ///
- /// Each platform may return different results.
- ///
- /// If the platform fails to resolve a locale, then this will return null.
- ///
- /// This method returns synchronously and is a direct call to
- /// platform specific APIs without invoking method channels.
Locale? computePlatformResolvedLocale(List<Locale> supportedLocales) {
return platformDispatcher.computePlatformResolvedLocale(supportedLocales);
}
- /// A callback that is invoked whenever [locale] changes value.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this callback is invoked.
VoidCallback? get onLocaleChanged => platformDispatcher.onLocaleChanged;
set onLocaleChanged(VoidCallback? callback) {
platformDispatcher.onLocaleChanged = callback;
}
- /// The lifecycle state immediately after dart isolate initialization.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This property will not be updated as the lifecycle changes.
- ///
- /// It is used to initialize [SchedulerBinding.lifecycleState] at startup
- /// with any buffered lifecycle state events.
String get initialLifecycleState => platformDispatcher.initialLifecycleState;
- /// The system-reported text scale.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This establishes the text scaling factor to use when rendering text,
- /// according to the user's platform preferences.
- ///
- /// The [onTextScaleFactorChanged] callback is called whenever this value
- /// changes.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this value changes.
double get textScaleFactor => platformDispatcher.textScaleFactor;
- /// The setting indicating whether time should always be shown in the 24-hour
- /// format.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This option is used by [showTimePicker].
bool get alwaysUse24HourFormat => platformDispatcher.alwaysUse24HourFormat;
- /// A callback that is invoked whenever [textScaleFactor] changes value.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this callback is invoked.
VoidCallback? get onTextScaleFactorChanged => platformDispatcher.onTextScaleFactorChanged;
set onTextScaleFactorChanged(VoidCallback? callback) {
platformDispatcher.onTextScaleFactorChanged = callback;
}
- /// The setting indicating the current brightness mode of the host platform.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// If the platform has no preference, [platformBrightness] defaults to
- /// [Brightness.light].
Brightness get platformBrightness => platformDispatcher.platformBrightness;
- /// A callback that is invoked whenever [platformBrightness] changes value.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
- /// observe when this callback is invoked.
VoidCallback? get onPlatformBrightnessChanged => platformDispatcher.onPlatformBrightnessChanged;
set onPlatformBrightnessChanged(VoidCallback? callback) {
platformDispatcher.onPlatformBrightnessChanged = callback;
}
- /// A callback that is invoked to notify the window that it is an appropriate
- /// time to provide a scene using the [SceneBuilder] API and the [render]
- /// method.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// When possible, this is driven by the hardware VSync signal. This is only
- /// called if [scheduleFrame] has been called since the last time this
- /// callback was invoked.
- ///
- /// The [onDrawFrame] callback is invoked immediately after [onBeginFrame],
- /// after draining any microtasks (e.g. completions of any [Future]s) queued
- /// by the [onBeginFrame] handler.
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- ///
- /// See also:
- ///
- /// * [SchedulerBinding], the Flutter framework class which manages the
- /// scheduling of frames.
- /// * [RendererBinding], the Flutter framework class which manages layout and
- /// painting.
FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;
set onBeginFrame(FrameCallback? callback) {
platformDispatcher.onBeginFrame = callback;
}
- /// A callback that is invoked for each frame after [onBeginFrame] has
- /// completed and after the microtask queue has been drained.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This can be used to implement a second phase of frame rendering that
- /// happens after any deferred work queued by the [onBeginFrame] phase.
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- ///
- /// See also:
- ///
- /// * [SchedulerBinding], the Flutter framework class which manages the
- /// scheduling of frames.
- /// * [RendererBinding], the Flutter framework class which manages layout and
- /// painting.
VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;
set onDrawFrame(VoidCallback? callback) {
platformDispatcher.onDrawFrame = callback;
}
- /// A callback that is invoked to report the [FrameTiming] of recently
- /// rasterized frames.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// It's prefered to use [SchedulerBinding.addTimingsCallback] than to use
- /// [SingletonFlutterWindow.onReportTimings] directly because
- /// [SchedulerBinding.addTimingsCallback] allows multiple callbacks.
- ///
- /// This can be used to see if the window has missed frames (through
- /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high
- /// latencies (through [FrameTiming.totalSpan]).
- ///
- /// Unlike [Timeline], the timing information here is available in the release
- /// mode (additional to the profile and the debug mode). Hence this can be
- /// used to monitor the application's performance in the wild.
- ///
- /// {@macro dart.ui.TimingsCallback.list}
- ///
- /// If this is null, no additional work will be done. If this is not null,
- /// Flutter spends less than 0.1ms every 1 second to report the timings
- /// (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for
- /// 60fps), or 0.01% CPU usage per second.
TimingsCallback? get onReportTimings => platformDispatcher.onReportTimings;
set onReportTimings(TimingsCallback? callback) {
platformDispatcher.onReportTimings = callback;
}
- /// A callback that is invoked when pointer data is available.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- ///
- /// See also:
- ///
- /// * [GestureBinding], the Flutter framework class which manages pointer
- /// events.
PointerDataPacketCallback? get onPointerDataPacket => platformDispatcher.onPointerDataPacket;
set onPointerDataPacket(PointerDataPacketCallback? callback) {
platformDispatcher.onPointerDataPacket = callback;
}
- /// The route or path that the embedder requested when the application was
- /// launched.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This will be the string "`/`" if no particular route was requested.
- ///
- /// ## Android
- ///
- /// On Android, the initial route can be set on the [initialRoute](/javadoc/io/flutter/embedding/android/FlutterActivity.NewEngineIntentBuilder.html#initialRoute-java.lang.String-)
- /// method of the [FlutterActivity](/javadoc/io/flutter/embedding/android/FlutterActivity.html)'s
- /// intent builder.
- ///
- /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#initial-route-with-a-cached-engine.
- ///
- /// ## iOS
- ///
- /// On iOS, the initial route can be set on the `initialRoute`
- /// parameter of the [FlutterViewController](/objcdoc/Classes/FlutterViewController.html)'s
- /// initializer.
- ///
- /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/ios/add-flutter-screen#route.
- ///
- /// See also:
- ///
- /// * [Navigator], a widget that handles routing.
- /// * [SystemChannels.navigation], which handles subsequent navigation
- /// requests from the embedder.
String get defaultRouteName => platformDispatcher.defaultRouteName;
- /// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
- /// [onDrawFrame] callbacks be invoked.
- ///
- /// {@template dart.ui.window.functionForwardWarning}
- /// Calling this function forwards the call to the same function on the
- /// [PlatformDispatcher] singleton, so instead of calling it here, you should
- /// consider calling it on `WidgetsBinding.instance.platformDispatcher` instead (or, when
- /// `WidgetsBinding` isn't available, on [PlatformDispatcher.instance]). The
- /// reason this function forwards to the [PlatformDispatcher] is to provide
- /// convenience for applications that only use a single main window.
- /// {@endtemplate}
- ///
- /// See also:
- ///
- /// * [SchedulerBinding], the Flutter framework class which manages the
- /// scheduling of frames.
void scheduleFrame() => platformDispatcher.scheduleFrame();
- /// Whether the user has requested that [updateSemantics] be called when
- /// the semantic contents of window changes.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The [onSemanticsEnabledChanged] callback is called whenever this value
- /// changes.
bool get semanticsEnabled => platformDispatcher.semanticsEnabled;
- /// A callback that is invoked when the value of [semanticsEnabled] changes.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
VoidCallback? get onSemanticsEnabledChanged => platformDispatcher.onSemanticsEnabledChanged;
set onSemanticsEnabledChanged(VoidCallback? callback) {
platformDispatcher.onSemanticsEnabledChanged = callback;
}
- /// A callback that is invoked whenever the user requests an action to be
- /// performed.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// This callback is used when the user expresses the action they wish to
- /// perform based on the semantics supplied by [updateSemantics].
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
SemanticsActionCallback? get onSemanticsAction => platformDispatcher.onSemanticsAction;
set onSemanticsAction(SemanticsActionCallback? callback) {
platformDispatcher.onSemanticsAction = callback;
}
- /// Additional accessibility features that may be enabled by the platform.
AccessibilityFeatures get accessibilityFeatures => platformDispatcher.accessibilityFeatures;
- /// A callback that is invoked when the value of [accessibilityFeatures] changes.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
VoidCallback? get onAccessibilityFeaturesChanged => platformDispatcher.onAccessibilityFeaturesChanged;
set onAccessibilityFeaturesChanged(VoidCallback? callback) {
platformDispatcher.onAccessibilityFeaturesChanged = callback;
}
- /// Change the retained semantics data about this window.
- ///
- /// {@macro dart.ui.window.functionForwardWarning}
- ///
- /// If [semanticsEnabled] is true, the user has requested that this function
- /// be called whenever the semantic content of this window changes.
- ///
- /// In either case, this function disposes the given update, which means the
- /// semantics update cannot be used further.
@override
void updateSemantics(SemanticsUpdate update) => platformDispatcher.updateSemantics(update);
- /// Sends a message to a platform-specific plugin.
- ///
- /// {@macro dart.ui.window.functionForwardWarning}
- ///
- /// The `name` parameter determines which plugin receives the message. The
- /// `data` parameter contains the message payload and is typically UTF-8
- /// encoded JSON but can be arbitrary data. If the plugin replies to the
- /// message, `callback` will be called with the response.
- ///
- /// The framework invokes [callback] in the same zone in which this method
- /// was called.
void sendPlatformMessage(String name,
ByteData? data,
PlatformMessageResponseCallback? callback) {
platformDispatcher.sendPlatformMessage(name, data, callback);
}
- /// Called whenever this window receives a message from a platform-specific
- /// plugin.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// The `name` parameter determines which plugin sent the message. The `data`
- /// parameter is the payload and is typically UTF-8 encoded JSON but can be
- /// arbitrary data.
- ///
- /// Message handlers must call the function given in the `callback` parameter.
- /// If the handler does not need to respond, the handler should pass null to
- /// the callback.
- ///
- /// The framework invokes this callback in the same zone in which the
- /// callback was set.
- // TODO(ianh): deprecate once framework uses [ChannelBuffers.setListener].
PlatformMessageCallback? get onPlatformMessage => platformDispatcher.onPlatformMessage;
set onPlatformMessage(PlatformMessageCallback? callback) {
platformDispatcher.onPlatformMessage = callback;
}
- /// Set the debug name associated with this platform dispatcher's root
- /// isolate.
- ///
- /// {@macro dart.ui.window.accessorForwardWarning}
- ///
- /// Normally debug names are automatically generated from the Dart port, entry
- /// point, and source file. For example: `main.dart$main-1234`.
- ///
- /// This can be combined with flutter tools `--isolate-filter` flag to debug
- /// specific root isolates. For example: `flutter attach --isolate-filter=[name]`.
- /// Note that this does not rename any child isolates of the root.
void setIsolateDebugName(String name) => PlatformDispatcher.instance.setIsolateDebugName(name);
}
diff --git a/framework/lib/material.dart b/framework/lib/material.dart
index fb07102..63901d8 100644
--- a/framework/lib/material.dart
+++ b/framework/lib/material.dart
@@ -21,7 +21,9 @@
library material;
export 'src/material/about.dart';
+export 'src/material/action_buttons.dart';
export 'src/material/action_chip.dart';
+export 'src/material/action_icons_theme.dart';
export 'src/material/adaptive_text_selection_toolbar.dart';
export 'src/material/animated_icons.dart';
export 'src/material/app.dart';
@@ -29,7 +31,6 @@
export 'src/material/app_bar_theme.dart';
export 'src/material/arc.dart';
export 'src/material/autocomplete.dart';
-export 'src/material/back_button.dart';
export 'src/material/badge.dart';
export 'src/material/badge_theme.dart';
export 'src/material/banner.dart';
diff --git a/framework/lib/src/cupertino/app.dart b/framework/lib/src/cupertino/app.dart
index dab2763..50b040d 100644
--- a/framework/lib/src/cupertino/app.dart
+++ b/framework/lib/src/cupertino/app.dart
@@ -558,6 +558,7 @@
restorationScopeId: widget.restorationScopeId,
);
}
+
return WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
@@ -595,7 +596,7 @@
@override
Widget build(BuildContext context) {
- final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
+ final CupertinoThemeData effectiveThemeData = (widget.theme ?? const CupertinoThemeData()).resolveFrom(context);
return ScrollConfiguration(
behavior: widget.scrollBehavior ?? const CupertinoScrollBehavior(),
diff --git a/framework/lib/src/cupertino/context_menu.dart b/framework/lib/src/cupertino/context_menu.dart
index 1fca184..acb7f74 100644
--- a/framework/lib/src/cupertino/context_menu.dart
+++ b/framework/lib/src/cupertino/context_menu.dart
@@ -277,7 +277,7 @@
/// opened in the default way to match a native iOS 16.0 app. The behavior
/// will match what will happen if the simple child image was passed as just
/// the [child] parameter, instead of [builder]. This can be manipulated to
- /// add more custamizability to the widget's animation.
+ /// add more customizability to the widget's animation.
///
/// ```dart
/// CupertinoContextMenu.builder(
diff --git a/framework/lib/src/cupertino/date_picker.dart b/framework/lib/src/cupertino/date_picker.dart
index 94522f1..7e20820 100644
--- a/framework/lib/src/cupertino/date_picker.dart
+++ b/framework/lib/src/cupertino/date_picker.dart
@@ -275,6 +275,7 @@
this.use24hFormat = false,
this.dateOrder,
this.backgroundColor,
+ this.showDayOfWeek = false
}) : initialDateTime = initialDateTime ?? DateTime.now(),
assert(
minuteInterval > 0 && 60 % minuteInterval == 0,
@@ -384,6 +385,9 @@
/// Defaults to null, which disables background painting entirely.
final Color? backgroundColor;
+ /// Whether to to show day of week alongside day. Defaults to false.
+ final bool showDayOfWeek;
+
@override
State<StatefulWidget> createState() { // ignore: no_logic_in_create_state, https://github.com/flutter/flutter/issues/70499
// The `time` mode and `dateAndTime` mode of the picker share the time
@@ -404,6 +408,7 @@
_PickerColumnType columnType,
CupertinoLocalizations localizations,
BuildContext context,
+ bool showDayOfWeek
) {
String longestText = '';
@@ -443,10 +448,20 @@
: localizations.postMeridiemAbbreviation;
break;
case _PickerColumnType.dayOfMonth:
+ int longestDayOfMonth = 1;
for (int i = 1; i <=31; i++) {
final String dayOfMonth = localizations.datePickerDayOfMonth(i);
if (longestText.length < dayOfMonth.length) {
longestText = dayOfMonth;
+ longestDayOfMonth = i;
+ }
+ }
+ if (showDayOfWeek) {
+ for (int wd = 1; wd < DateTime.daysPerWeek; wd++) {
+ final String dayOfMonth = localizations.datePickerDayOfMonth(longestDayOfMonth, wd);
+ if (longestText.length < dayOfMonth.length) {
+ longestText = dayOfMonth;
+ }
}
}
break;
@@ -649,7 +664,7 @@
double _getEstimatedColumnWidth(_PickerColumnType columnType) {
if (estimatedColumnWidths[columnType.index] == null) {
estimatedColumnWidths[columnType.index] =
- CupertinoDatePicker._getColumnWidth(columnType, localizations, context);
+ CupertinoDatePicker._getColumnWidth(columnType, localizations, context, widget.showDayOfWeek);
}
return estimatedColumnWidths[columnType.index]!;
@@ -1151,9 +1166,9 @@
}
void _refreshEstimatedColumnWidths() {
- estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context);
- estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context);
- estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context);
+ estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context, widget.showDayOfWeek);
+ estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context, widget.showDayOfWeek);
+ estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context, widget.showDayOfWeek);
}
// The DateTime of the last day of a given month in a given year.
@@ -1191,10 +1206,11 @@
selectionOverlay: selectionOverlay,
children: List<Widget>.generate(31, (int index) {
final int day = index + 1;
+ final int? dayOfWeek = widget.showDayOfWeek ? DateTime(selectedYear, selectedMonth, day).weekday : null;
return itemPositioningBuilder(
context,
Text(
- localizations.datePickerDayOfMonth(day),
+ localizations.datePickerDayOfMonth(day, dayOfWeek),
style: _themeTextStyle(context, isValid: day <= daysInCurrentMonth),
),
);
diff --git a/framework/lib/src/cupertino/localizations.dart b/framework/lib/src/cupertino/localizations.dart
index 77af039..ccb038a 100644
--- a/framework/lib/src/cupertino/localizations.dart
+++ b/framework/lib/src/cupertino/localizations.dart
@@ -82,12 +82,17 @@
/// Day of month that is shown in [CupertinoDatePicker] spinner corresponding
/// to the given day index.
///
+ /// If weekDay is provided then it will also show weekday name alongside the numerical day.
+ ///
/// Examples: datePickerDayOfMonth(1) in:
///
/// - US English: 1
/// - Korean: 1일
+ /// Examples: datePickerDayOfMonth(1, 1) in:
+ ///
+ /// - US English: Mon 1
// The global version uses date symbols data from the intl package.
- String datePickerDayOfMonth(int dayIndex);
+ String datePickerDayOfMonth(int dayIndex, [int? weekDay]);
/// The medium-width date format that is shown in [CupertinoDatePicker]
/// spinner. Abbreviates month and days of week.
@@ -292,7 +297,8 @@
/// function, rather than constructing this class directly.
const DefaultCupertinoLocalizations();
- static const List<String> _shortWeekdays = <String>[
+ /// Short version of days of week.
+ static const List<String> shortWeekdays = <String>[
'Mon',
'Tue',
'Wed',
@@ -341,7 +347,13 @@
String datePickerMonth(int monthIndex) => _months[monthIndex - 1];
@override
- String datePickerDayOfMonth(int dayIndex) => dayIndex.toString();
+ String datePickerDayOfMonth(int dayIndex, [int? weekDay]) {
+ if (weekDay != null) {
+ return ' ${shortWeekdays[weekDay - DateTime.monday]} $dayIndex ';
+ }
+
+ return dayIndex.toString();
+ }
@override
String datePickerHour(int hour) => hour.toString();
@@ -362,7 +374,7 @@
@override
String datePickerMediumDate(DateTime date) {
- return '${_shortWeekdays[date.weekday - DateTime.monday]} '
+ return '${shortWeekdays[date.weekday - DateTime.monday]} '
'${_shortMonths[date.month - DateTime.january]} '
'${date.day.toString().padRight(2)}';
}
diff --git a/framework/lib/src/cupertino/magnifier.dart b/framework/lib/src/cupertino/magnifier.dart
index 05a92b9..1541f5e 100644
--- a/framework/lib/src/cupertino/magnifier.dart
+++ b/framework/lib/src/cupertino/magnifier.dart
@@ -77,7 +77,7 @@
class _CupertinoTextMagnifierState extends State<CupertinoTextMagnifier>
with SingleTickerProviderStateMixin {
- // Initalize to dummy values for the event that the inital call to
+ // Initialize to dummy values for the event that the initial call to
// _determineMagnifierPositionAndFocalPoint calls hide, and thus does not
// set these values.
Offset _currentAdjustedMagnifierPosition = Offset.zero;
diff --git a/framework/lib/src/cupertino/nav_bar.dart b/framework/lib/src/cupertino/nav_bar.dart
index 6d73f55..648f4c8 100644
--- a/framework/lib/src/cupertino/nav_bar.dart
+++ b/framework/lib/src/cupertino/nav_bar.dart
@@ -897,7 +897,7 @@
titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
border: border,
- hasUserMiddle: userMiddle != null,
+ hasUserMiddle: userMiddle != null && (alwaysShowMiddle || !showLargeTitle),
largeExpanded: showLargeTitle,
child: navBar,
),
@@ -967,8 +967,8 @@
return;
}
- final BoxConstraints childConstriants = constraints.widthConstraints().loosen();
- child.layout(childConstriants, parentUsesSize: true);
+ final BoxConstraints childConstraints = constraints.widthConstraints().loosen();
+ child.layout(childConstraints, parentUsesSize: true);
final double maxScale = child.size.width != 0.0
? clampDouble(constraints.maxWidth / child.size.width, 1.0, 1.1)
diff --git a/framework/lib/src/cupertino/refresh.dart b/framework/lib/src/cupertino/refresh.dart
index 3ada10c..a774e4a 100644
--- a/framework/lib/src/cupertino/refresh.dart
+++ b/framework/lib/src/cupertino/refresh.dart
@@ -398,7 +398,7 @@
switch (refreshState) {
case RefreshIndicatorMode.drag:
// While we're dragging, we draw individual ticks of the spinner while simultaneously
- // easing the opacity in. Note that the opacity curve values here were derived using
+ // easing the opacity in. The opacity curve values here were derived using
// Xcode through inspecting a native app running on iOS 13.5.
const Curve opacityCurve = Interval(0.0, 0.35, curve: Curves.easeInOut);
return Opacity(
diff --git a/framework/lib/src/cupertino/route.dart b/framework/lib/src/cupertino/route.dart
index bb158ad..9576047 100644
--- a/framework/lib/src/cupertino/route.dart
+++ b/framework/lib/src/cupertino/route.dart
@@ -835,8 +835,8 @@
_CupertinoEdgeShadowDecoration? b,
double t,
) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!._colors == null ? b : _CupertinoEdgeShadowDecoration._(b._colors!.map<Color>((Color color) => Color.lerp(null, color, t)!).toList());
diff --git a/framework/lib/src/cupertino/switch.dart b/framework/lib/src/cupertino/switch.dart
index dedaf4e..ef48a4c 100644
--- a/framework/lib/src/cupertino/switch.dart
+++ b/framework/lib/src/cupertino/switch.dart
@@ -364,7 +364,7 @@
activeColor: activeColor,
trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor ?? CupertinoColors.white, context),
- // Opacity, lightness, and saturation values were aproximated with
+ // Opacity, lightness, and saturation values were approximated with
// color pickers on the switches in the macOS settings.
focusColor: CupertinoDynamicColor.resolve(
widget.focusColor ??
diff --git a/framework/lib/src/cupertino/text_field.dart b/framework/lib/src/cupertino/text_field.dart
index a8ec7e6..2a52d27 100644
--- a/framework/lib/src/cupertino/text_field.dart
+++ b/framework/lib/src/cupertino/text_field.dart
@@ -996,8 +996,7 @@
case TargetPlatform.windows:
case TargetPlatform.fuchsia:
case TargetPlatform.android:
- if (cause == SelectionChangedCause.longPress
- || cause == SelectionChangedCause.drag) {
+ if (cause == SelectionChangedCause.longPress) {
_editableText.bringIntoView(selection.extent);
}
break;
diff --git a/framework/lib/src/foundation/assertions.dart b/framework/lib/src/foundation/assertions.dart
index 59abde7..188d981 100644
--- a/framework/lib/src/foundation/assertions.dart
+++ b/framework/lib/src/foundation/assertions.dart
@@ -223,6 +223,13 @@
level: level,
);
+ @override
+ String toString({
+ TextTreeConfiguration? parentConfiguration,
+ DiagnosticLevel minLevel = DiagnosticLevel.info,
+ }) {
+ return valueToString(parentConfiguration: parentConfiguration);
+ }
@override
List<Object> get value => super.value!;
diff --git a/framework/lib/src/foundation/binding.dart b/framework/lib/src/foundation/binding.dart
index 97e30cb..359315b 100644
--- a/framework/lib/src/foundation/binding.dart
+++ b/framework/lib/src/foundation/binding.dart
@@ -146,7 +146,7 @@
return true;
}());
- assert(_debugInitializedType == null);
+ assert(_debugInitializedType == null, 'Binding is already initialized to $_debugInitializedType');
initInstances();
assert(_debugInitializedType != null);
diff --git a/framework/lib/src/foundation/memory_allocations.dart b/framework/lib/src/foundation/memory_allocations.dart
index 1323f2b..5084b57 100644
--- a/framework/lib/src/foundation/memory_allocations.dart
+++ b/framework/lib/src/foundation/memory_allocations.dart
@@ -39,7 +39,7 @@
/// long living place as it will prevent garbage collection.
final Object object;
- /// The representation of the event in a form, acceptible by a
+ /// The representation of the event in a form, acceptable by a
/// pure dart library, that cannot depend on Flutter.
///
/// The method enables code like:
diff --git a/framework/lib/src/gestures/gesture_settings.dart b/framework/lib/src/gestures/gesture_settings.dart
index afaa5ed..f701ef3 100644
--- a/framework/lib/src/gestures/gesture_settings.dart
+++ b/framework/lib/src/gestures/gesture_settings.dart
@@ -26,7 +26,7 @@
/// Create a new [DeviceGestureSettings] from the provided [view].
factory DeviceGestureSettings.fromView(ui.FlutterView view) {
- final double? physicalTouchSlop = view.viewConfiguration.gestureSettings.physicalTouchSlop;
+ final double? physicalTouchSlop = view.gestureSettings.physicalTouchSlop;
return DeviceGestureSettings(
touchSlop: physicalTouchSlop == null ? null : physicalTouchSlop / view.devicePixelRatio
);
diff --git a/framework/lib/src/gestures/monodrag.dart b/framework/lib/src/gestures/monodrag.dart
index 4930910..6617d9a 100644
--- a/framework/lib/src/gestures/monodrag.dart
+++ b/framework/lib/src/gestures/monodrag.dart
@@ -222,6 +222,24 @@
late OffsetPair _initialPosition;
late OffsetPair _pendingDragOffset;
Duration? _lastPendingEventTimestamp;
+
+ /// When asserts are enabled, returns the last tracked pending event timestamp
+ /// for this recognizer.
+ ///
+ /// Otherwise, returns null.
+ ///
+ /// This getter is intended for use in framework unit tests. Applications must
+ /// not depend on its value.
+ @visibleForTesting
+ Duration? get debugLastPendingEventTimestamp {
+ Duration? lastPendingEventTimestamp;
+ assert(() {
+ lastPendingEventTimestamp = _lastPendingEventTimestamp;
+ return true;
+ }());
+ return lastPendingEventTimestamp;
+ }
+
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int? _initialButtons;
@@ -363,7 +381,7 @@
if (_state != _DragState.accepted) {
_state = _DragState.accepted;
final OffsetPair delta = _pendingDragOffset;
- final Duration timestamp = _lastPendingEventTimestamp!;
+ final Duration? timestamp = _lastPendingEventTimestamp;
final Matrix4? transform = _lastTransform;
final Offset localUpdateDelta;
switch (dragStartBehavior) {
@@ -449,7 +467,7 @@
}
}
- void _checkStart(Duration timestamp, int pointer) {
+ void _checkStart(Duration? timestamp, int pointer) {
if (onStart != null) {
final DragStartDetails details = DragStartDetails(
sourceTimeStamp: timestamp,
diff --git a/framework/lib/src/gestures/resampler.dart b/framework/lib/src/gestures/resampler.dart
index aa36725..e2a67e0 100644
--- a/framework/lib/src/gestures/resampler.dart
+++ b/framework/lib/src/gestures/resampler.dart
@@ -240,7 +240,8 @@
// generated when the position has changed.
if (event is! PointerMoveEvent && event is! PointerHoverEvent) {
// Add synthetics `move` or `hover` event if position has changed.
- // Note: Devices without `hover` events are expected to always have
+ //
+ // Devices without `hover` events are expected to always have
// `add` and `down` events with the same position and this logic will
// therefore never produce `hover` events.
if (position != _position) {
diff --git a/framework/lib/src/material/about.dart b/framework/lib/src/material/about.dart
index 0b26bd2..ea3a336 100644
--- a/framework/lib/src/material/about.dart
+++ b/framework/lib/src/material/about.dart
@@ -463,11 +463,11 @@
Widget _packagesView(final BuildContext _, final bool isLateral) {
final Widget about = _AboutProgram(
- name: widget.applicationName ?? _defaultApplicationName(context),
- icon: widget.applicationIcon ?? _defaultApplicationIcon(context),
- version: widget.applicationVersion ?? _defaultApplicationVersion(context),
- legalese: widget.applicationLegalese,
- );
+ name: widget.applicationName ?? _defaultApplicationName(context),
+ icon: widget.applicationIcon ?? _defaultApplicationIcon(context),
+ version: widget.applicationVersion ?? _defaultApplicationVersion(context),
+ legalese: widget.applicationLegalese,
+ );
return _PackagesView(
about: about,
isLateral: isLateral,
@@ -870,10 +870,11 @@
page = Scaffold(
appBar: AppBar(
title: _PackageLicensePageTitle(
- title,
- subtitle,
- theme.primaryTextTheme,
- theme.appBarTheme.titleTextStyle,
+ title: title,
+ subtitle: subtitle,
+ theme: theme.useMaterial3 ? theme.textTheme : theme.primaryTextTheme,
+ titleTextStyle: theme.appBarTheme.titleTextStyle,
+ foregroundColor: theme.appBarTheme.foregroundColor,
),
),
body: Center(
@@ -909,7 +910,12 @@
automaticallyImplyLeading: false,
pinned: true,
backgroundColor: theme.cardColor,
- title: _PackageLicensePageTitle(title, subtitle, theme.textTheme, theme.textTheme.titleLarge),
+ title: _PackageLicensePageTitle(
+ title: title,
+ subtitle: subtitle,
+ theme: theme.textTheme,
+ titleTextStyle: theme.textTheme.titleLarge,
+ ),
),
SliverPadding(
padding: padding,
@@ -935,29 +941,29 @@
}
class _PackageLicensePageTitle extends StatelessWidget {
- const _PackageLicensePageTitle(
- this.title,
- this.subtitle,
- this.theme,
+ const _PackageLicensePageTitle({
+ required this.title,
+ required this.subtitle,
+ required this.theme,
this.titleTextStyle,
- );
+ this.foregroundColor,
+ });
final String title;
final String subtitle;
final TextTheme theme;
final TextStyle? titleTextStyle;
+ final Color? foregroundColor;
@override
Widget build(BuildContext context) {
- final Color? color = Theme.of(context).appBarTheme.foregroundColor;
final TextStyle? effectiveTitleTextStyle = titleTextStyle ?? theme.titleLarge;
-
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- Text(title, style: effectiveTitleTextStyle?.copyWith(color: color)),
- Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
+ Text(title, style: effectiveTitleTextStyle?.copyWith(color: foregroundColor)),
+ Text(subtitle, style: theme.titleSmall?.copyWith(color: foregroundColor)),
],
);
}
@@ -1227,16 +1233,18 @@
MaterialPageRoute<void> _masterPageRoute(BuildContext context) {
return MaterialPageRoute<dynamic>(
- builder: (BuildContext c) => BlockSemantics(
- child: _MasterPage(
- leading: widget.automaticallyImplyLeading && Navigator.of(context).canPop()
- ? BackButton(onPressed: () => Navigator.of(context).pop())
- : null,
- title: widget.title,
- automaticallyImplyLeading: widget.automaticallyImplyLeading,
- masterViewBuilder: widget.masterViewBuilder,
- ),
- ),
+ builder: (BuildContext c) {
+ return BlockSemantics(
+ child: _MasterPage(
+ leading: widget.automaticallyImplyLeading && Navigator.of(context).canPop()
+ ? BackButton(onPressed: () { Navigator.of(context).pop(); })
+ : null,
+ title: widget.title,
+ automaticallyImplyLeading: widget.automaticallyImplyLeading,
+ masterViewBuilder: widget.masterViewBuilder,
+ ),
+ );
+ },
);
}
@@ -1285,14 +1293,14 @@
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: title,
- leading: leading,
- actions: const <Widget>[],
- automaticallyImplyLeading: automaticallyImplyLeading,
- ),
- body: masterViewBuilder!(context, false),
- );
+ appBar: AppBar(
+ title: title,
+ leading: leading,
+ actions: const <Widget>[],
+ automaticallyImplyLeading: automaticallyImplyLeading,
+ ),
+ body: masterViewBuilder!(context, false),
+ );
}
}
@@ -1400,7 +1408,10 @@
),
),
),
- body: _masterPanel(context),
+ body: Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: _masterPanel(context),
+ ),
),
// Detail view stacked above main scaffold and master view.
SafeArea(
diff --git a/framework/lib/src/material/action_buttons.dart b/framework/lib/src/material/action_buttons.dart
new file mode 100644
index 0000000..6e0e72d
--- /dev/null
+++ b/framework/lib/src/material/action_buttons.dart
@@ -0,0 +1,418 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'action_icons_theme.dart';
+import 'button_style.dart';
+import 'debug.dart';
+import 'icon_button.dart';
+import 'icons.dart';
+import 'material_localizations.dart';
+import 'scaffold.dart';
+import 'theme.dart';
+
+abstract class _ActionButton extends StatelessWidget {
+ /// Creates a Material Design icon button.
+ const _ActionButton({
+ super.key,
+ this.color,
+ required this.icon,
+ required this.onPressed,
+ this.style,
+ });
+
+ /// The icon to display inside the button.
+ final Widget icon;
+
+ /// The callback that is called when the button is tapped
+ /// or otherwise activated.
+ ///
+ /// If this is set to null, the button will do a default action
+ /// when it is tapped or activated.
+ final VoidCallback? onPressed;
+
+ /// The color to use for the icon.
+ ///
+ /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
+ /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
+ final Color? color;
+
+ /// Customizes this icon button's appearance.
+ ///
+ /// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3]
+ /// is set to true, [style] is preferred for icon button customization, and any
+ /// parameters defined in [style] will override the same parameters in [IconButton].
+ ///
+ /// Null by default.
+ final ButtonStyle? style;
+
+ /// This returns the appropriate tooltip text for this action button.
+ String _getTooltip(BuildContext context);
+
+ /// This is the default function that is called when [onPressed] is set
+ /// to null.
+ void _onPressedCallback(BuildContext context);
+
+ @override
+ Widget build(BuildContext context) {
+ assert(debugCheckHasMaterialLocalizations(context));
+ return IconButton(
+ icon: icon,
+ style: style,
+ color: color,
+ tooltip: _getTooltip(context),
+ onPressed: () {
+ if (onPressed != null) {
+ onPressed!();
+ } else {
+ _onPressedCallback(context);
+ }
+ },
+ );
+ }
+}
+
+typedef _ActionIconBuilderCallback = WidgetBuilder? Function(ActionIconThemeData? actionIconTheme);
+typedef _ActionIconDataCallback = IconData Function(BuildContext context);
+typedef _AndroidSemanticsLabelCallback = String Function(MaterialLocalizations materialLocalization);
+
+class _ActionIcon extends StatelessWidget {
+ const _ActionIcon({
+ required this.iconBuilderCallback,
+ required this.getIcon,
+ required this.getAndroidSemanticsLabel,
+ });
+
+ final _ActionIconBuilderCallback iconBuilderCallback;
+ final _ActionIconDataCallback getIcon;
+ final _AndroidSemanticsLabelCallback getAndroidSemanticsLabel;
+
+ @override
+ Widget build(BuildContext context) {
+ final ActionIconThemeData? actionIconTheme = ActionIconTheme.of(context);
+ final WidgetBuilder? iconBuilder = iconBuilderCallback(actionIconTheme);
+ if (iconBuilder != null) {
+ return iconBuilder(context);
+ }
+
+ final IconData data = getIcon(context);
+ final String? semanticsLabel;
+ // This can't use the platform from Theme because it is the Android OS that
+ // expects the duplicated tooltip and label.
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ semanticsLabel = getAndroidSemanticsLabel(MaterialLocalizations.of(context));
+ break;
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ semanticsLabel = null;
+ break;
+ }
+
+ return Icon(data, semanticLabel: semanticsLabel);
+ }
+}
+
+/// A "back" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+/// * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
+/// [Navigator.maybePop] to return to the previous route.
+/// * [IconButton], which is a more general widget for creating buttons
+/// with icons.
+/// * [Icon], a Material Design icon.
+/// * [ThemeData.platform], which specifies the current platform.
+class BackButtonIcon extends StatelessWidget {
+ /// Creates an icon that shows the appropriate "back" image for
+ /// the current platform (as obtained from the [Theme]).
+ const BackButtonIcon({ super.key });
+
+ @override
+ Widget build(BuildContext context) {
+ return _ActionIcon(
+ iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+ return actionIconTheme?.backButtonIconBuilder;
+ },
+ getIcon: (BuildContext context) {
+ if (kIsWeb) {
+ // Always use 'Icons.arrow_back' as a back_button icon in web.
+ return Icons.arrow_back;
+ }
+ switch (Theme.of(context).platform) {
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ return Icons.arrow_back;
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ return Icons.arrow_back_ios;
+ }
+ },
+ getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+ return materialLocalization.backButtonTooltip;
+ },
+ );
+ }
+}
+
+/// A Material Design back icon button.
+///
+/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
+/// current [TargetPlatform]. When pressed, the back button calls
+/// [Navigator.maybePop] to return to the previous route unless a custom
+/// [onPressed] callback is provided.
+///
+/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
+/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
+/// situations.
+///
+/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
+/// used to override the default icon color of [BackButton]. If both exist, the [ButtonStyle.iconColor]
+/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
+///
+/// When deciding to display a [BackButton], consider using
+/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
+/// popped. If that value is false (e.g., because the current route is the
+/// initial route), the [BackButton] will not have any effect when pressed,
+/// which could frustrate the user.
+///
+/// Requires one of its ancestors to be a [Material] widget.
+///
+/// See also:
+///
+/// * [AppBar], which automatically uses a [BackButton] in its
+/// [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
+/// current [Route] is not the [Navigator]'s first route.
+/// * [BackButtonIcon], which is useful if you need to create a back button
+/// that responds differently to being pressed.
+/// * [IconButton], which is a more general widget for creating buttons with
+/// icons.
+/// * [CloseButton], an alternative which may be more appropriate for leaf
+/// node pages in the navigation tree.
+class BackButton extends _ActionButton {
+ /// Creates an [IconButton] with the appropriate "back" icon for the current
+ /// target platform.
+ const BackButton({
+ super.key,
+ super.color,
+ super.style,
+ super.onPressed,
+ }) : super(icon: const BackButtonIcon());
+
+ @override
+ void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
+
+ @override
+ String _getTooltip(BuildContext context) {
+ return MaterialLocalizations.of(context).backButtonTooltip;
+ }
+}
+
+/// A "close" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+/// * [CloseButton], an [IconButton] with a [CloseButtonIcon] that calls
+/// [Navigator.maybePop] to return to the previous route.
+/// * [IconButton], which is a more general widget for creating buttons
+/// with icons.
+/// * [Icon], a Material Design icon.
+/// * [ThemeData.platform], which specifies the current platform.
+class CloseButtonIcon extends StatelessWidget {
+ /// Creates an icon that shows the appropriate "close" image for
+ /// the current platform (as obtained from the [Theme]).
+ const CloseButtonIcon({ super.key });
+
+ @override
+ Widget build(BuildContext context) {
+ return _ActionIcon(
+ iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+ return actionIconTheme?.closeButtonIconBuilder;
+ },
+ getIcon: (BuildContext context) => Icons.close,
+ getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+ return materialLocalization.closeButtonTooltip;
+ },
+ );
+ }
+}
+
+/// A Material Design close icon button.
+///
+/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
+/// close button calls [Navigator.maybePop] to return to the previous route.
+///
+/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
+/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
+/// situations.
+///
+/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
+/// used to override the default icon color of [CloseButton]. If both exist, the [ButtonStyle.iconColor]
+/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
+///
+/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
+/// pages that may solicit additional actions to close.
+///
+/// See also:
+///
+/// * [AppBar], which automatically uses a [CloseButton] in its
+/// [AppBar.leading] slot when appropriate.
+/// * [BackButton], which is more appropriate for middle nodes in the
+/// navigation tree or where pages can be popped instantaneously with
+/// no user data consequence.
+/// * [IconButton], to create other Material Design icon buttons.
+class CloseButton extends _ActionButton {
+ /// Creates a Material Design close icon button.
+ const CloseButton({ super.key, super.color, super.onPressed, super.style })
+ : super(icon: const CloseButtonIcon());
+
+ @override
+ void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
+
+ @override
+ String _getTooltip(BuildContext context) {
+ return MaterialLocalizations.of(context).closeButtonTooltip;
+ }
+}
+
+/// A "drawer" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+/// * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
+/// [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
+/// * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
+/// calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
+/// * [IconButton], which is a more general widget for creating buttons
+/// with icons.
+/// * [Icon], a Material Design icon.
+/// * [ThemeData.platform], which specifies the current platform.
+class DrawerButtonIcon extends StatelessWidget {
+ /// Creates an icon that shows the appropriate "close" image for
+ /// the current platform (as obtained from the [Theme]).
+ const DrawerButtonIcon({ super.key });
+
+ @override
+ Widget build(BuildContext context) {
+ return _ActionIcon(
+ iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+ return actionIconTheme?.drawerButtonIconBuilder;
+ },
+ getIcon: (BuildContext context) => Icons.menu,
+ getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+ return materialLocalization.openAppDrawerTooltip;
+ },
+ );
+ }
+}
+
+/// A Material Design drawer icon button.
+///
+/// A [DrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
+/// close button calls [ScaffoldState.openDrawer] to the [Scaffold.drawer].
+///
+/// The default behaviour on press can be overriden with [onPressed].
+///
+/// See also:
+///
+/// * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
+/// calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
+/// * [IconButton], which is a more general widget for creating buttons
+/// with icons.
+/// * [Icon], a Material Design icon.
+/// * [ThemeData.platform], which specifies the current platform.
+class DrawerButton extends _ActionButton {
+ /// Creates a Material Design drawer icon button.
+ const DrawerButton({
+ super.key,
+ super.style,
+ super.onPressed,
+ }) : super(icon: const DrawerButtonIcon());
+
+ @override
+ void _onPressedCallback(BuildContext context) => Scaffold.of(context).openDrawer();
+
+ @override
+ String _getTooltip(BuildContext context) {
+ return MaterialLocalizations.of(context).openAppDrawerTooltip;
+ }
+}
+
+/// A "end drawer" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+/// * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
+/// [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
+/// * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
+/// calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer]
+/// * [IconButton], which is a more general widget for creating buttons
+/// with icons.
+/// * [Icon], a Material Design icon.
+/// * [ThemeData.platform], which specifies the current platform.
+class EndDrawerButtonIcon extends StatelessWidget {
+ /// Creates an icon that shows the appropriate "end drawer" image for
+ /// the current platform (as obtained from the [Theme]).
+ const EndDrawerButtonIcon({ super.key });
+
+ @override
+ Widget build(BuildContext context) {
+ return _ActionIcon(
+ iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+ return actionIconTheme?.endDrawerButtonIconBuilder;
+ },
+ getIcon: (BuildContext context) => Icons.menu,
+ getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+ return materialLocalization.openAppDrawerTooltip;
+ },
+ );
+ }
+}
+
+/// A Material Design end drawer icon button.
+///
+/// A [EndDrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
+/// end drawer button calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
+///
+/// The default behaviour on press can be overriden with [onPressed].
+///
+/// See also:
+///
+/// * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
+/// [ScaffoldState.openDrawer] to open a drawer.
+/// * [IconButton], which is a more general widget for creating buttons
+/// with icons.
+/// * [Icon], a Material Design icon.
+/// * [ThemeData.platform], which specifies the current platform.
+class EndDrawerButton extends _ActionButton {
+ /// Creates a Material Design end drawer icon button.
+ const EndDrawerButton({
+ super.key,
+ super.style,
+ super.onPressed,
+ }) : super(icon: const EndDrawerButtonIcon());
+
+ @override
+ void _onPressedCallback(BuildContext context) => Scaffold.of(context).openEndDrawer();
+
+ @override
+ String _getTooltip(BuildContext context) {
+ return MaterialLocalizations.of(context).openAppDrawerTooltip;
+ }
+}
diff --git a/framework/lib/src/material/action_icons_theme.dart b/framework/lib/src/material/action_icons_theme.dart
new file mode 100644
index 0000000..bfcbdd3
--- /dev/null
+++ b/framework/lib/src/material/action_icons_theme.dart
@@ -0,0 +1,153 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'action_buttons.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// late BuildContext context;
+
+/// A [ActionIconThemeData] that overrides the default icons of
+/// [BackButton], [CloseButton], [DrawerButton], and [EndDrawerButton] with
+/// [ActionIconTheme.of] or the overall [Theme]'s [ThemeData.actionIconTheme].
+@immutable
+class ActionIconThemeData with Diagnosticable {
+ /// Creates an [ActionIconThemeData].
+ ///
+ /// The builders [backButtonIconBuilder], [closeButtonIconBuilder],
+ /// [drawerButtonIconBuilder], [endDrawerButtonIconBuilder] may be null.
+ const ActionIconThemeData({ this.backButtonIconBuilder, this.closeButtonIconBuilder, this.drawerButtonIconBuilder, this.endDrawerButtonIconBuilder });
+
+ /// Overrides [BackButtonIcon]'s icon.
+ ///
+ /// If [backButtonIconBuilder] is null, then [BackButtonIcon]
+ /// fallbacks to the platform's default back button icon.
+ final WidgetBuilder? backButtonIconBuilder;
+
+ /// Overrides [CloseButtonIcon]'s icon.
+ ///
+ /// If [closeButtonIconBuilder] is null, then [CloseButtonIcon]
+ /// fallbacks to the platform's default close button icon.
+ final WidgetBuilder? closeButtonIconBuilder;
+
+ /// Overrides [DrawerButtonIcon]'s icon.
+ ///
+ /// If [drawerButtonIconBuilder] is null, then [DrawerButtonIcon]
+ /// fallbacks to the platform's default drawer button icon.
+ final WidgetBuilder? drawerButtonIconBuilder;
+
+ /// Overrides [EndDrawerButtonIcon]'s icon.
+ ///
+ /// If [endDrawerButtonIconBuilder] is null, then [EndDrawerButtonIcon]
+ /// fallbacks to the platform's default end drawer button icon.
+ final WidgetBuilder? endDrawerButtonIconBuilder;
+
+ /// Creates a copy of this object but with the given fields replaced with the
+ /// new values.
+ ActionIconThemeData copyWith({
+ WidgetBuilder? backButtonIconBuilder,
+ WidgetBuilder? closeButtonIconBuilder,
+ WidgetBuilder? drawerButtonIconBuilder,
+ WidgetBuilder? endDrawerButtonIconBuilder,
+ }) {
+ return ActionIconThemeData(
+ backButtonIconBuilder: backButtonIconBuilder ?? backButtonIconBuilder,
+ closeButtonIconBuilder: closeButtonIconBuilder ?? closeButtonIconBuilder,
+ drawerButtonIconBuilder: drawerButtonIconBuilder ?? drawerButtonIconBuilder,
+ endDrawerButtonIconBuilder: endDrawerButtonIconBuilder ?? endDrawerButtonIconBuilder,
+ );
+ }
+
+ /// Linearly interpolate between two action icon themes.
+ static ActionIconThemeData? lerp(ActionIconThemeData? a, ActionIconThemeData? b, double t) {
+ if (a == null && b == null) {
+ return null;
+ }
+ return ActionIconThemeData(
+ backButtonIconBuilder: t < 0.5 ? a?.backButtonIconBuilder : b?.backButtonIconBuilder,
+ closeButtonIconBuilder: t < 0.5 ? a?.closeButtonIconBuilder : b?.closeButtonIconBuilder,
+ drawerButtonIconBuilder: t < 0.5 ? a?.drawerButtonIconBuilder : b?.drawerButtonIconBuilder,
+ endDrawerButtonIconBuilder: t < 0.5 ? a?.endDrawerButtonIconBuilder : b?.endDrawerButtonIconBuilder,
+ );
+ }
+
+ @override
+ int get hashCode {
+ final List<Object?> values = <Object?>[
+ backButtonIconBuilder,
+ closeButtonIconBuilder,
+ drawerButtonIconBuilder,
+ endDrawerButtonIconBuilder,
+ ];
+ return Object.hashAll(values);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is ActionIconThemeData
+ && other.backButtonIconBuilder == backButtonIconBuilder
+ && other.closeButtonIconBuilder == closeButtonIconBuilder
+ && other.drawerButtonIconBuilder == drawerButtonIconBuilder
+ && other.endDrawerButtonIconBuilder == endDrawerButtonIconBuilder;
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<WidgetBuilder>('backButtonIconBuilder', backButtonIconBuilder, defaultValue: null));
+ properties.add(DiagnosticsProperty<WidgetBuilder>('closeButtonIconBuilder', closeButtonIconBuilder, defaultValue: null));
+ properties.add(DiagnosticsProperty<WidgetBuilder>('drawerButtonIconBuilder', drawerButtonIconBuilder, defaultValue: null));
+ properties.add(DiagnosticsProperty<WidgetBuilder>('endDrawerButtonIconBuilder', endDrawerButtonIconBuilder, defaultValue: null));
+ }
+}
+
+/// An inherited widget that overrides the default icon of [BackButtonIcon],
+/// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this
+/// widget's subtree.
+class ActionIconTheme extends InheritedTheme {
+ /// Creates a theme that overrides the default icon of [BackButtonIcon],
+ /// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this
+ /// widget's subtree.
+ const ActionIconTheme({
+ super.key,
+ required this.data,
+ required super.child,
+ });
+
+ /// Specifies the default icon overrides for descendant [BackButtonIcon],
+ /// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] widgets.
+ final ActionIconThemeData data;
+
+ /// The closest instance of this class that encloses the given context.
+ ///
+ /// If there is no enclosing [ActionIconTheme] widget, then
+ /// [ThemeData.actionIconTheme] is used.
+ ///
+ /// Typical usage is as follows:
+ ///
+ /// ```dart
+ /// ActionIconThemeData? theme = ActionIconTheme.of(context);
+ /// ```
+ static ActionIconThemeData? of(BuildContext context) {
+ final ActionIconTheme? actionIconTheme = context.dependOnInheritedWidgetOfExactType<ActionIconTheme>();
+ return actionIconTheme?.data ?? Theme.of(context).actionIconTheme;
+ }
+
+ @override
+ Widget wrap(BuildContext context, Widget child) {
+ return ActionIconTheme(data: data, child: child);
+ }
+
+ @override
+ bool updateShouldNotify(ActionIconTheme oldWidget) => data != oldWidget.data;
+}
diff --git a/framework/lib/src/material/app.dart b/framework/lib/src/material/app.dart
index fb82772..50e25f5 100644
--- a/framework/lib/src/material/app.dart
+++ b/framework/lib/src/material/app.dart
@@ -910,15 +910,14 @@
);
}
- Widget _materialBuilder(BuildContext context, Widget? child) {
+ ThemeData _themeBuilder(BuildContext context) {
+ ThemeData? theme;
// Resolve which theme to use based on brightness and high contrast.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
final bool useDarkTheme = mode == ThemeMode.dark
|| (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark);
final bool highContrast = MediaQuery.highContrastOf(context);
- ThemeData? theme;
-
if (useDarkTheme && highContrast && widget.highContrastDarkTheme != null) {
theme = widget.highContrastDarkTheme;
} else if (useDarkTheme && widget.darkTheme != null) {
@@ -927,6 +926,11 @@
theme = widget.highContrastTheme;
}
theme ??= widget.theme ?? ThemeData.light();
+ return theme;
+ }
+
+ Widget _materialBuilder(BuildContext context, Widget? child) {
+ final ThemeData theme = _themeBuilder(context);
final Color effectiveSelectionColor = theme.textSelectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
final Color effectiveCursorColor = theme.textSelectionTheme.cursorColor ?? theme.colorScheme.primary;
diff --git a/framework/lib/src/material/app_bar.dart b/framework/lib/src/material/app_bar.dart
index 638ed8a..8f8a9e4 100644
--- a/framework/lib/src/material/app_bar.dart
+++ b/framework/lib/src/material/app_bar.dart
@@ -9,8 +9,8 @@
import 'package:flute/services.dart';
import 'package:flute/widgets.dart';
+import 'action_buttons.dart';
import 'app_bar_theme.dart';
-import 'back_button.dart';
import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
@@ -21,7 +21,6 @@
import 'icon_button_theme.dart';
import 'icons.dart';
import 'material.dart';
-import 'material_localizations.dart';
import 'material_state.dart';
import 'scaffold.dart';
import 'tabs.dart';
@@ -755,14 +754,6 @@
super.dispose();
}
- void _handleDrawerButton() {
- Scaffold.of(context).openDrawer();
- }
-
- void _handleDrawerButtonEnd() {
- Scaffold.of(context).openEndDrawer();
- }
-
void _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification && widget.notificationPredicate(notification)) {
final bool oldScrolledUnder = _scrolledUnder;
@@ -777,8 +768,9 @@
break;
case AxisDirection.right:
case AxisDirection.left:
- // Scrolled under is only supported in the vertical axis.
- _scrolledUnder = false;
+ // Scrolled under is only supported in the vertical axis, and should
+ // not be altered based on horizontal notifications of the same
+ // predicate since it could be a 2D scroller.
break;
}
@@ -894,11 +886,8 @@
Widget? leading = widget.leading;
if (leading == null && widget.automaticallyImplyLeading) {
if (hasDrawer) {
- leading = IconButton(
- icon: const Icon(Icons.menu),
- iconSize: overallIconTheme.size ?? 24,
- onPressed: _handleDrawerButton,
- tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
+ leading = DrawerButton(
+ style: IconButton.styleFrom(iconSize: overallIconTheme.size ?? 24),
);
// TODO(chunhtai): remove (!hasEndDrawer && canPop) once internal tests
// are migrated.
@@ -1009,11 +998,8 @@
children: widget.actions!,
);
} else if (hasEndDrawer) {
- actions = IconButton(
- icon: const Icon(Icons.menu),
- iconSize: overallIconTheme.size ?? 24,
- onPressed: _handleDrawerButtonEnd,
- tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
+ actions = EndDrawerButton(
+ style: IconButton.styleFrom(iconSize: overallIconTheme.size ?? 24),
);
}
@@ -1543,13 +1529,16 @@
key: key,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
- actions: actions,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
+ hasLeading: leading != null,
title: title,
+ actions: actions,
foregroundColor: foregroundColor,
variant: _ScrollUnderFlexibleVariant.medium,
centerCollapsedTitle: centerTitle,
primary: primary,
+ leadingWidth: leadingWidth,
+ titleSpacing: titleSpacing,
),
bottom: bottom,
elevation: elevation,
@@ -1645,13 +1634,16 @@
key: key,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
- actions: actions,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
+ hasLeading: leading != null,
title: title,
+ actions: actions,
foregroundColor: foregroundColor,
variant: _ScrollUnderFlexibleVariant.large,
centerCollapsedTitle: centerTitle,
primary: primary,
+ leadingWidth: leadingWidth,
+ titleSpacing: titleSpacing,
),
bottom: bottom,
elevation: elevation,
@@ -2092,18 +2084,26 @@
class _ScrollUnderFlexibleSpace extends StatelessWidget {
const _ScrollUnderFlexibleSpace({
+ required this.hasLeading,
this.title,
+ this.actions,
this.foregroundColor,
required this.variant,
this.centerCollapsedTitle,
this.primary = true,
+ this.leadingWidth,
+ this.titleSpacing,
});
+ final bool hasLeading;
final Widget? title;
+ final List<Widget>? actions;
final Color? foregroundColor;
final _ScrollUnderFlexibleVariant variant;
final bool? centerCollapsedTitle;
final bool primary;
+ final double? leadingWidth;
+ final double? titleSpacing;
@override
Widget build(BuildContext context) {
@@ -2157,6 +2157,14 @@
centerTitle = centerCollapsedTitle ?? appBarTheme.centerTitle ?? platformCenter();
}
+ EdgeInsetsGeometry effectiveCollapsedTitlePadding = EdgeInsets.zero;
+ if (hasLeading && leadingWidth == null) {
+ effectiveCollapsedTitlePadding = centerTitle
+ ? config.collapsedCenteredTitlePadding!
+ : config.collapsedTitlePadding!;
+ } else if (hasLeading && leadingWidth != null) {
+ effectiveCollapsedTitlePadding = EdgeInsetsDirectional.only(start: leadingWidth!);
+ }
final bool isCollapsed = settings.isScrolledUnder ?? false;
return Column(
children: <Widget>[
@@ -2164,17 +2172,20 @@
padding: EdgeInsets.only(top: topPadding),
child: Container(
height: collapsedHeight,
- padding: centerTitle ? config.collapsedCenteredTitlePadding : config.collapsedTitlePadding,
- child: AnimatedOpacity(
- opacity: isCollapsed ? 1 : 0,
- duration: const Duration(milliseconds: 500),
- curve: const Cubic(0.2, 0.0, 0.0, 1.0),
- child: Align(
- alignment: centerTitle
- ? Alignment.center
- : AlignmentDirectional.centerStart,
+ padding: effectiveCollapsedTitlePadding,
+ child: NavigationToolbar(
+ centerMiddle: centerTitle,
+ middleSpacing: titleSpacing ?? appBarTheme.titleSpacing ?? NavigationToolbar.kMiddleSpacing,
+ middle: AnimatedOpacity(
+ opacity: isCollapsed ? 1 : 0,
+ duration: const Duration(milliseconds: 500),
+ curve: const Cubic(0.2, 0.0, 0.0, 1.0),
child: collapsedTitle,
),
+ trailing: actions != null ? Row(
+ mainAxisSize: MainAxisSize.min,
+ children: actions!,
+ ) : null,
),
),
),
@@ -2310,10 +2321,10 @@
_textTheme.headlineSmall?.apply(color: _colors.onSurface);
@override
- EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
+ EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
- EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
+ EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20);
@@ -2339,10 +2350,10 @@
_textTheme.headlineMedium?.apply(color: _colors.onSurface);
@override
- EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
+ EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
- EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
+ EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28);
diff --git a/framework/lib/src/material/app_bar_theme.dart b/framework/lib/src/material/app_bar_theme.dart
index dae83c0..7f4952f 100644
--- a/framework/lib/src/material/app_bar_theme.dart
+++ b/framework/lib/src/material/app_bar_theme.dart
@@ -204,6 +204,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static AppBarTheme lerp(AppBarTheme? a, AppBarTheme? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return AppBarTheme(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
diff --git a/framework/lib/src/material/back_button.dart b/framework/lib/src/material/back_button.dart
index 3e88494..95f4b97 100644
--- a/framework/lib/src/material/back_button.dart
+++ b/framework/lib/src/material/back_button.dart
@@ -2,197 +2,4 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flute/foundation.dart';
-import 'package:flute/widgets.dart';
-
-import 'debug.dart';
-import 'icon_button.dart';
-import 'icons.dart';
-import 'material_localizations.dart';
-import 'theme.dart';
-
-/// A "back" icon that's appropriate for the current [TargetPlatform].
-///
-/// The current platform is determined by querying for the ambient [Theme].
-///
-/// See also:
-///
-/// * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
-/// [Navigator.maybePop] to return to the previous route.
-/// * [IconButton], which is a more general widget for creating buttons
-/// with icons.
-/// * [Icon], a Material Design icon.
-/// * [ThemeData.platform], which specifies the current platform.
-class BackButtonIcon extends StatelessWidget {
- /// Creates an icon that shows the appropriate "back" image for
- /// the current platform (as obtained from the [Theme]).
- const BackButtonIcon({ super.key });
-
- @override
- Widget build(BuildContext context) {
- final String? semanticsLabel;
- final IconData data;
- switch (Theme.of(context).platform) {
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- data = Icons.arrow_back;
- break;
- case TargetPlatform.iOS:
- case TargetPlatform.macOS:
- data = Icons.arrow_back_ios;
- break;
- }
- // This can't use the platform from Theme because it is the Android OS that
- // expects the duplicated tooltip and label.
- switch (defaultTargetPlatform) {
- case TargetPlatform.android:
- semanticsLabel = MaterialLocalizations.of(context).backButtonTooltip;
- break;
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- case TargetPlatform.iOS:
- case TargetPlatform.macOS:
- semanticsLabel = null;
- break;
- }
-
- return Icon(data, semanticLabel: semanticsLabel);
- }
-}
-
-/// A Material Design back button.
-///
-/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
-/// current [TargetPlatform]. When pressed, the back button calls
-/// [Navigator.maybePop] to return to the previous route unless a custom
-/// [onPressed] callback is provided.
-///
-/// When deciding to display a [BackButton], consider using
-/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
-/// popped. If that value is false (e.g., because the current route is the
-/// initial route), the [BackButton] will not have any effect when pressed,
-/// which could frustrate the user.
-///
-/// Requires one of its ancestors to be a [Material] widget.
-///
-/// See also:
-///
-/// * [AppBar], which automatically uses a [BackButton] in its
-/// [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
-/// current [Route] is not the [Navigator]'s first route.
-/// * [BackButtonIcon], which is useful if you need to create a back button
-/// that responds differently to being pressed.
-/// * [IconButton], which is a more general widget for creating buttons with
-/// icons.
-/// * [CloseButton], an alternative which may be more appropriate for leaf
-/// node pages in the navigation tree.
-class BackButton extends StatelessWidget {
- /// Creates an [IconButton] with the appropriate "back" icon for the current
- /// target platform.
- const BackButton({ super.key, this.color, this.onPressed });
-
- /// The color to use for the icon.
- ///
- /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
- /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
- final Color? color;
-
- /// An override callback to perform instead of the default behavior which is
- /// to pop the [Navigator].
- ///
- /// It can, for instance, be used to pop the platform's navigation stack
- /// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
- /// situations.
- ///
- /// Defaults to null.
- final VoidCallback? onPressed;
-
- @override
- Widget build(BuildContext context) {
- assert(debugCheckHasMaterialLocalizations(context));
- return IconButton(
- icon: const BackButtonIcon(),
- color: color,
- tooltip: MaterialLocalizations.of(context).backButtonTooltip,
- onPressed: () {
- if (onPressed != null) {
- onPressed!();
- } else {
- Navigator.maybePop(context);
- }
- },
- );
- }
-}
-
-/// A Material Design close button.
-///
-/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
-/// close button calls [Navigator.maybePop] to return to the previous route.
-///
-/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
-/// pages that may solicit additional actions to close.
-///
-/// See also:
-///
-/// * [AppBar], which automatically uses a [CloseButton] in its
-/// [AppBar.leading] slot when appropriate.
-/// * [BackButton], which is more appropriate for middle nodes in the
-/// navigation tree or where pages can be popped instantaneously with
-/// no user data consequence.
-/// * [IconButton], to create other Material Design icon buttons.
-class CloseButton extends StatelessWidget {
- /// Creates a Material Design close button.
- const CloseButton({ super.key, this.color, this.onPressed });
-
- /// The color to use for the icon.
- ///
- /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
- /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
- final Color? color;
-
- /// An override callback to perform instead of the default behavior which is
- /// to pop the [Navigator].
- ///
- /// It can, for instance, be used to pop the platform's navigation stack
- /// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
- /// situations.
- ///
- /// Defaults to null.
- final VoidCallback? onPressed;
-
- @override
- Widget build(BuildContext context) {
- assert(debugCheckHasMaterialLocalizations(context));
- final String? semanticsLabel;
- // This can't use the platform from Theme because it is the Android OS that
- // expects the duplicated tooltip and label.
- switch (defaultTargetPlatform) {
- case TargetPlatform.android:
- semanticsLabel = MaterialLocalizations.of(context).closeButtonTooltip;
- break;
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- case TargetPlatform.iOS:
- case TargetPlatform.macOS:
- semanticsLabel = null;
- break;
- }
- return IconButton(
- icon: Icon(Icons.close, semanticLabel: semanticsLabel),
- color: color,
- tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
- onPressed: () {
- if (onPressed != null) {
- onPressed!();
- } else {
- Navigator.maybePop(context);
- }
- },
- );
- }
-}
+export 'action_buttons.dart' show BackButton, BackButtonIcon, CloseButton, CloseButtonIcon;
diff --git a/framework/lib/src/material/badge_theme.dart b/framework/lib/src/material/badge_theme.dart
index 1095100..7666bd4 100644
--- a/framework/lib/src/material/badge_theme.dart
+++ b/framework/lib/src/material/badge_theme.dart
@@ -94,6 +94,9 @@
/// Linearly interpolate between two [Badge] themes.
static BadgeThemeData lerp(BadgeThemeData? a, BadgeThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return BadgeThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
textColor: Color.lerp(a?.textColor, b?.textColor, t),
diff --git a/framework/lib/src/material/bottom_app_bar.dart b/framework/lib/src/material/bottom_app_bar.dart
index 41e83cb..ed91989 100644
--- a/framework/lib/src/material/bottom_app_bar.dart
+++ b/framework/lib/src/material/bottom_app_bar.dart
@@ -6,6 +6,7 @@
import 'package:flute/widgets.dart';
import 'bottom_app_bar_theme.dart';
+import 'colors.dart';
import 'elevation_overlay.dart';
import 'material.dart';
import 'scaffold.dart';
@@ -71,6 +72,7 @@
this.child,
this.padding,
this.surfaceTintColor,
+ this.shadowColor,
this.height,
}) : assert(elevation == null || elevation >= 0.0);
@@ -135,6 +137,18 @@
/// See [Material.surfaceTintColor] for more details on how this overlay is applied.
final Color? surfaceTintColor;
+ /// The color of the shadow below the app bar.
+ ///
+ /// If this property is null, then [BottomAppBarTheme.shadowColor] of
+ /// [ThemeData.bottomAppBarTheme] is used. If that is also null, the default value
+ /// is fully opaque black for Material 2, and transparent for Material 3.
+ ///
+ /// See also:
+ ///
+ /// * [elevation], which defines the size of the shadow below the app bar.
+ /// * [shape], which defines the shape of the app bar and its shadow.
+ final Color? shadowColor;
+
/// The double value used to indicate the height of the [BottomAppBar].
///
/// If this is null, the default value is the minimum in relation to the content,
@@ -177,29 +191,33 @@
final Color color = widget.color ?? babTheme.color ?? defaults.color!;
final Color surfaceTintColor = widget.surfaceTintColor ?? babTheme.surfaceTintColor ?? defaults.surfaceTintColor!;
final Color effectiveColor = isMaterial3 ? color : ElevationOverlay.applyOverlay(context, color, elevation);
+ final Color shadowColor = widget.shadowColor ?? babTheme.shadowColor ?? defaults.shadowColor!;
final Widget child = Padding(
padding: widget.padding ?? babTheme.padding ?? (isMaterial3 ? const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0) : EdgeInsets.zero),
child: widget.child,
);
- return SizedBox(
- height: height,
- child: PhysicalShape(
- clipper: clipper,
- elevation: elevation,
- color: effectiveColor,
- clipBehavior: widget.clipBehavior,
- child: Material(
- key: materialKey,
- type: isMaterial3 ? MaterialType.canvas : MaterialType.transparency,
- elevation: elevation,
- color: isMaterial3 ? effectiveColor : null,
- surfaceTintColor: surfaceTintColor,
- child: SafeArea(child: child),
- ),
- ),
+ final Material material = Material(
+ key: materialKey,
+ type: isMaterial3 ? MaterialType.canvas : MaterialType.transparency,
+ elevation: elevation,
+ color: isMaterial3 ? effectiveColor : null,
+ surfaceTintColor: surfaceTintColor,
+ shadowColor: shadowColor,
+ child: SafeArea(child: child),
);
+
+ final PhysicalShape physicalShape = PhysicalShape(
+ clipper: clipper,
+ elevation: elevation,
+ shadowColor: shadowColor,
+ color: effectiveColor,
+ clipBehavior: widget.clipBehavior,
+ child: material,
+ );
+
+ return SizedBox(height: height, child: physicalShape);
}
}
@@ -260,6 +278,9 @@
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
+
+ @override
+ Color get shadowColor => const Color(0xFF000000);
}
// BEGIN GENERATED TOKEN PROPERTIES - BottomAppBar
@@ -286,6 +307,9 @@
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
+
+ @override
+ Color get shadowColor => Colors.transparent;
}
// END GENERATED TOKEN PROPERTIES - BottomAppBar
diff --git a/framework/lib/src/material/bottom_app_bar_theme.dart b/framework/lib/src/material/bottom_app_bar_theme.dart
index e59c538..86f4a25 100644
--- a/framework/lib/src/material/bottom_app_bar_theme.dart
+++ b/framework/lib/src/material/bottom_app_bar_theme.dart
@@ -34,6 +34,7 @@
this.shape,
this.height,
this.surfaceTintColor,
+ this.shadowColor,
this.padding,
});
@@ -49,8 +50,6 @@
final NotchedShape? shape;
/// Overrides the default value for [BottomAppBar.height].
- ///
- /// If null, [BottomAppBar] height will be the minimum on the non material 3.
final double? height;
/// Overrides the default value for [BottomAppBar.surfaceTintColor].
@@ -60,6 +59,9 @@
/// See [Material.surfaceTintColor] for more details.
final Color? surfaceTintColor;
+ /// Overrides the default value for [BottomAppBar.shadowColor].
+ final Color? shadowColor;
+
/// Overrides the default value for [BottomAppBar.padding].
final EdgeInsetsGeometry? padding;
@@ -71,6 +73,7 @@
NotchedShape? shape,
double? height,
Color? surfaceTintColor,
+ Color? shadowColor,
EdgeInsetsGeometry? padding,
}) {
return BottomAppBarTheme(
@@ -79,6 +82,7 @@
shape: shape ?? this.shape,
height: height ?? this.height,
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
+ shadowColor: shadowColor ?? this.shadowColor,
padding: padding ?? this.padding,
);
}
@@ -94,12 +98,16 @@
///
/// {@macro dart.ui.shadow.lerp}
static BottomAppBarTheme lerp(BottomAppBarTheme? a, BottomAppBarTheme? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return BottomAppBarTheme(
color: Color.lerp(a?.color, b?.color, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
shape: t < 0.5 ? a?.shape : b?.shape,
height: lerpDouble(a?.height, b?.height, t),
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
+ shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
);
}
@@ -111,6 +119,7 @@
shape,
height,
surfaceTintColor,
+ shadowColor,
padding,
);
@@ -128,6 +137,7 @@
&& other.shape == shape
&& other.height == height
&& other.surfaceTintColor == surfaceTintColor
+ && other.shadowColor == shadowColor
&& other.padding == padding;
}
@@ -139,6 +149,7 @@
properties.add(DiagnosticsProperty<NotchedShape>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<double>('height', height, defaultValue: null));
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
+ properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
}
}
diff --git a/framework/lib/src/material/bottom_navigation_bar_theme.dart b/framework/lib/src/material/bottom_navigation_bar_theme.dart
index 7d150ef..ed515a5 100644
--- a/framework/lib/src/material/bottom_navigation_bar_theme.dart
+++ b/framework/lib/src/material/bottom_navigation_bar_theme.dart
@@ -174,6 +174,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static BottomNavigationBarThemeData lerp(BottomNavigationBarThemeData? a, BottomNavigationBarThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return BottomNavigationBarThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
diff --git a/framework/lib/src/material/bottom_sheet.dart b/framework/lib/src/material/bottom_sheet.dart
index 887d7e2..dfb37f8 100644
--- a/framework/lib/src/material/bottom_sheet.dart
+++ b/framework/lib/src/material/bottom_sheet.dart
@@ -78,6 +78,7 @@
this.onDragStart,
this.onDragEnd,
this.backgroundColor,
+ this.shadowColor,
this.elevation,
this.shape,
this.clipBehavior,
@@ -134,6 +135,18 @@
/// Defaults to null and falls back to [Material]'s default.
final Color? backgroundColor;
+ /// The color of the shadow below the sheet.
+ ///
+ /// If this property is null, then [BottomSheetThemeData.shadowColor] of
+ /// [ThemeData.bottomSheetTheme] is used. If that is also null, the default value
+ /// is transparent.
+ ///
+ /// See also:
+ ///
+ /// * [elevation], which defines the size of the shadow below the sheet.
+ /// * [shape], which defines the shape of the sheet and its shadow.
+ final Color? shadowColor;
+
/// The z-coordinate at which to place this material relative to its parent.
///
/// This controls the size of the shadow below the material.
@@ -275,6 +288,7 @@
final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints;
final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor ?? defaults.backgroundColor;
final Color? surfaceTintColor = bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor;
+ final Color? shadowColor = widget.shadowColor ?? bottomSheetTheme.shadowColor ?? defaults.shadowColor;
final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? defaults.elevation ?? 0;
final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape ?? defaults.shape;
final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
@@ -284,6 +298,7 @@
color: color,
elevation: elevation,
surfaceTintColor: surfaceTintColor,
+ shadowColor: shadowColor,
shape: shape,
clipBehavior: clipBehavior,
child: NotificationListener<DraggableScrollableNotification>(
@@ -559,10 +574,8 @@
onDragEnd: handleDragEnd,
),
builder: (BuildContext context, Widget? child) {
- // Disable the initial animation when accessible navigation is on so
- // that the semantics are added to the tree at the correct time.
final double animationValue = animationCurve.transform(
- MediaQuery.accessibleNavigationOf(context) ? 1.0 : widget.route.animation!.value,
+ widget.route.animation!.value,
);
return Semantics(
scopesRoute: true,
@@ -1144,6 +1157,9 @@
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
+
+ @override
+ Color? get shadowColor => Colors.transparent;
}
// END GENERATED TOKEN PROPERTIES - BottomSheet
diff --git a/framework/lib/src/material/bottom_sheet_theme.dart b/framework/lib/src/material/bottom_sheet_theme.dart
index c0ebc22..079786a 100644
--- a/framework/lib/src/material/bottom_sheet_theme.dart
+++ b/framework/lib/src/material/bottom_sheet_theme.dart
@@ -33,6 +33,7 @@
this.elevation,
this.modalBackgroundColor,
this.modalBarrierColor,
+ this.shadowColor,
this.modalElevation,
this.shape,
this.clipBehavior,
@@ -66,6 +67,9 @@
/// a modal bottom sheet.
final Color? modalBarrierColor;
+ /// Overrides the default value for [BottomSheet.shadowColor].
+ final Color? shadowColor;
+
/// Value for [BottomSheet.elevation] when the Bottom sheet is presented as a
/// modal bottom sheet.
final double? modalElevation;
@@ -94,6 +98,7 @@
double? elevation,
Color? modalBackgroundColor,
Color? modalBarrierColor,
+ Color? shadowColor,
double? modalElevation,
ShapeBorder? shape,
Clip? clipBehavior,
@@ -105,6 +110,7 @@
elevation: elevation ?? this.elevation,
modalBackgroundColor: modalBackgroundColor ?? this.modalBackgroundColor,
modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor,
+ shadowColor: shadowColor ?? this.shadowColor,
modalElevation: modalElevation ?? this.modalElevation,
shape: shape ?? this.shape,
clipBehavior: clipBehavior ?? this.clipBehavior,
@@ -118,8 +124,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BottomSheetThemeData? lerp(BottomSheetThemeData? a, BottomSheetThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return BottomSheetThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
@@ -127,6 +133,7 @@
elevation: lerpDouble(a?.elevation, b?.elevation, t),
modalBackgroundColor: Color.lerp(a?.modalBackgroundColor, b?.modalBackgroundColor, t),
modalBarrierColor: Color.lerp(a?.modalBarrierColor, b?.modalBarrierColor, t),
+ shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
modalElevation: lerpDouble(a?.modalElevation, b?.modalElevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
@@ -141,6 +148,7 @@
elevation,
modalBackgroundColor,
modalBarrierColor,
+ shadowColor,
modalElevation,
shape,
clipBehavior,
@@ -160,6 +168,7 @@
&& other.surfaceTintColor == surfaceTintColor
&& other.elevation == elevation
&& other.modalBackgroundColor == modalBackgroundColor
+ && other.shadowColor == shadowColor
&& other.modalBarrierColor == modalBarrierColor
&& other.modalElevation == modalElevation
&& other.shape == shape
@@ -174,6 +183,7 @@
properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
properties.add(ColorProperty('modalBackgroundColor', modalBackgroundColor, defaultValue: null));
+ properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
properties.add(ColorProperty('modalBarrierColor', modalBarrierColor, defaultValue: null));
properties.add(DoubleProperty('modalElevation', modalElevation, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
diff --git a/framework/lib/src/material/button_bar.dart b/framework/lib/src/material/button_bar.dart
index 069c285..d87ab4f 100644
--- a/framework/lib/src/material/button_bar.dart
+++ b/framework/lib/src/material/button_bar.dart
@@ -146,15 +146,14 @@
/// The spacing between buttons when the button bar overflows.
///
- /// If the [children] do not fit into a single row, they are
- /// arranged into a column. This parameter provides additional
- /// vertical space in between buttons when it does overflow.
+ /// If the [children] do not fit into a single row, they are arranged into a
+ /// column. This parameter provides additional vertical space in between
+ /// buttons when it does overflow.
///
- /// Note that the button spacing may appear to be more than
- /// the value provided. This is because most buttons adhere to the
- /// [MaterialTapTargetSize] of 48px. So, even though a button
- /// might visually be 36px in height, it might still take up to
- /// 48px vertically.
+ /// The button spacing may appear to be more than the value provided. This is
+ /// because most buttons adhere to the [MaterialTapTargetSize] of 48px. So,
+ /// even though a button might visually be 36px in height, it might still take
+ /// up to 48px vertically.
///
/// If null then no spacing will be added in between buttons in
/// an overflow state.
diff --git a/framework/lib/src/material/button_bar_theme.dart b/framework/lib/src/material/button_bar_theme.dart
index 20df2ae..d9c54bd 100644
--- a/framework/lib/src/material/button_bar_theme.dart
+++ b/framework/lib/src/material/button_bar_theme.dart
@@ -146,8 +146,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static ButtonBarThemeData? lerp(ButtonBarThemeData? a, ButtonBarThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ButtonBarThemeData(
alignment: t < 0.5 ? a?.alignment : b?.alignment,
diff --git a/framework/lib/src/material/button_style.dart b/framework/lib/src/material/button_style.dart
index 18cf679..440cac0 100644
--- a/framework/lib/src/material/button_style.dart
+++ b/framework/lib/src/material/button_style.dart
@@ -492,8 +492,8 @@
/// Linearly interpolate between two [ButtonStyle]s.
static ButtonStyle? lerp(ButtonStyle? a, ButtonStyle? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ButtonStyle(
textStyle: MaterialStateProperty.lerp<TextStyle?>(a?.textStyle, b?.textStyle, t, TextStyle.lerp),
diff --git a/framework/lib/src/material/button_theme.dart b/framework/lib/src/material/button_theme.dart
index 7e38bf1..62157d8 100644
--- a/framework/lib/src/material/button_theme.dart
+++ b/framework/lib/src/material/button_theme.dart
@@ -23,7 +23,7 @@
/// Button text is black or white depending on [ThemeData.brightness].
normal,
- /// Button text is [ThemeData.accentColor].
+ /// Button text is [ColorScheme.secondary].
accent,
/// Button text is based on [ThemeData.primaryColor].
diff --git a/framework/lib/src/material/card_theme.dart b/framework/lib/src/material/card_theme.dart
index db849ee..b4376b7 100644
--- a/framework/lib/src/material/card_theme.dart
+++ b/framework/lib/src/material/card_theme.dart
@@ -114,6 +114,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static CardTheme lerp(CardTheme? a, CardTheme? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return CardTheme(
clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
color: Color.lerp(a?.color, b?.color, t),
diff --git a/framework/lib/src/material/checkbox.dart b/framework/lib/src/material/checkbox.dart
index 39b8846..1b88890 100644
--- a/framework/lib/src/material/checkbox.dart
+++ b/framework/lib/src/material/checkbox.dart
@@ -305,7 +305,7 @@
///
/// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme]
/// is used. If that's null then the shape will be a [RoundedRectangleBorder]
- /// with a circular corner radius of 1.0.
+ /// with a circular corner radius of 1.0 in Material 2, and 2.0 in Material 3.
final OutlinedBorder? shape;
/// {@template flutter.material.checkbox.side}
@@ -522,9 +522,7 @@
..checkColor = effectiveCheckColor
..value = value
..previousValue = _previousValue
- ..shape = widget.shape ?? checkboxTheme.shape ?? const RoundedRectangleBorder(
- borderRadius: BorderRadius.all(Radius.circular(1.0)),
- )
+ ..shape = widget.shape ?? checkboxTheme.shape ?? defaults.shape!
..side = _resolveSide(widget.side) ?? _resolveSide(checkboxTheme.side),
),
);
@@ -759,6 +757,11 @@
@override
VisualDensity get visualDensity => _theme.visualDensity;
+
+ @override
+ OutlinedBorder get shape => const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(1.0)),
+ );
}
// BEGIN GENERATED TOKEN PROPERTIES - Checkbox
@@ -869,6 +872,11 @@
@override
VisualDensity get visualDensity => _theme.visualDensity;
+
+ @override
+ OutlinedBorder get shape => const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(2.0)),
+ );
}
// END GENERATED TOKEN PROPERTIES - Checkbox
diff --git a/framework/lib/src/material/checkbox_theme.dart b/framework/lib/src/material/checkbox_theme.dart
index 7d99ecd..ceb01bb 100644
--- a/framework/lib/src/material/checkbox_theme.dart
+++ b/framework/lib/src/material/checkbox_theme.dart
@@ -130,6 +130,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static CheckboxThemeData lerp(CheckboxThemeData? a, CheckboxThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return CheckboxThemeData(
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
fillColor: MaterialStateProperty.lerp<Color?>(a?.fillColor, b?.fillColor, t, Color.lerp),
@@ -195,6 +198,9 @@
if (a == null || b == null) {
return null;
}
+ if (identical(a, b)) {
+ return a;
+ }
return BorderSide.lerp(a, b, t);
}
}
diff --git a/framework/lib/src/material/chip_theme.dart b/framework/lib/src/material/chip_theme.dart
index 63e65bc..661411c 100644
--- a/framework/lib/src/material/chip_theme.dart
+++ b/framework/lib/src/material/chip_theme.dart
@@ -483,8 +483,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static ChipThemeData? lerp(ChipThemeData? a, ChipThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ChipThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/color_scheme.dart b/framework/lib/src/material/color_scheme.dart
index 2e55a26..0c209f5 100644
--- a/framework/lib/src/material/color_scheme.dart
+++ b/framework/lib/src/material/color_scheme.dart
@@ -827,6 +827,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static ColorScheme lerp(ColorScheme a, ColorScheme b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return ColorScheme(
brightness: t < 0.5 ? a.brightness : b.brightness,
primary: Color.lerp(a.primary, b.primary, t)!,
diff --git a/framework/lib/src/material/data_table.dart b/framework/lib/src/material/data_table.dart
index ba4f434..2f49028 100644
--- a/framework/lib/src/material/data_table.dart
+++ b/framework/lib/src/material/data_table.dart
@@ -392,7 +392,13 @@
this.onSelectAll,
this.decoration,
this.dataRowColor,
- this.dataRowHeight,
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
+ double? dataRowHeight,
+ double? dataRowMinHeight,
+ double? dataRowMaxHeight,
this.dataTextStyle,
this.headingRowColor,
this.headingRowHeight,
@@ -410,6 +416,11 @@
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
assert(!rows.any((DataRow row) => row.cells.length != columns.length)),
assert(dividerThickness == null || dividerThickness >= 0),
+ assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
+ assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
+ 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'),
+ dataRowMinHeight = dataRowHeight ?? dataRowMinHeight,
+ dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight,
_onlyTextColumn = _initOnlyTextColumn(columns);
/// The configuration and labels for the columns in the table.
@@ -504,7 +515,29 @@
/// If null, [DataTableThemeData.dataRowHeight] is used. This value defaults
/// to [kMinInteractiveDimension] to adhere to the Material Design
/// specifications.
- final double? dataRowHeight;
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
+ double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
+
+ /// {@template flutter.material.dataTable.dataRowMinHeight}
+ /// The minimum height of each row (excluding the row that contains column headings).
+ /// {@endtemplate}
+ ///
+ /// If null, [DataTableThemeData.dataRowMinHeight] is used. This value defaults
+ /// to [kMinInteractiveDimension] to adhere to the Material Design
+ /// specifications.
+ final double? dataRowMinHeight;
+
+ /// {@template flutter.material.dataTable.dataRowMaxHeight}
+ /// The maximum height of each row (excluding the row that contains column headings).
+ /// {@endtemplate}
+ ///
+ /// If null, [DataTableThemeData.dataRowMaxHeight] is used. This value defaults
+ /// to [kMinInteractiveDimension] to adhere to the Material Design
+ /// specifications.
+ final double? dataRowMaxHeight;
/// {@template flutter.material.dataTable.dataTextStyle}
/// The text style for data rows.
@@ -841,13 +874,17 @@
?? dataTableTheme.dataTextStyle
?? themeData.dataTableTheme.dataTextStyle
?? themeData.textTheme.bodyMedium!;
- final double effectiveDataRowHeight = dataRowHeight
- ?? dataTableTheme.dataRowHeight
- ?? themeData.dataTableTheme.dataRowHeight
+ final double effectiveDataRowMinHeight = dataRowMinHeight
+ ?? dataTableTheme.dataRowMinHeight
+ ?? themeData.dataTableTheme.dataRowMinHeight
+ ?? kMinInteractiveDimension;
+ final double effectiveDataRowMaxHeight = dataRowMaxHeight
+ ?? dataTableTheme.dataRowMaxHeight
+ ?? themeData.dataTableTheme.dataRowMaxHeight
?? kMinInteractiveDimension;
label = Container(
padding: padding,
- height: effectiveDataRowHeight,
+ constraints: BoxConstraints(minHeight: effectiveDataRowMinHeight, maxHeight: effectiveDataRowMaxHeight),
alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: effectiveDataTextStyle.copyWith(
@@ -1063,6 +1100,7 @@
clipBehavior: clipBehavior,
child: Table(
columnWidths: tableColumns.asMap(),
+ defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: tableRows,
border: border,
),
diff --git a/framework/lib/src/material/data_table_theme.dart b/framework/lib/src/material/data_table_theme.dart
index 1ff4b55..7dbc975 100644
--- a/framework/lib/src/material/data_table_theme.dart
+++ b/framework/lib/src/material/data_table_theme.dart
@@ -40,7 +40,13 @@
const DataTableThemeData({
this.decoration,
this.dataRowColor,
- this.dataRowHeight,
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
+ double? dataRowHeight,
+ double? dataRowMinHeight,
+ double? dataRowMaxHeight,
this.dataTextStyle,
this.headingRowColor,
this.headingRowHeight,
@@ -49,7 +55,11 @@
this.columnSpacing,
this.dividerThickness,
this.checkboxHorizontalMargin,
- });
+ }) : assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
+ assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
+ 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'),
+ dataRowMinHeight = dataRowHeight ?? dataRowMinHeight,
+ dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight;
/// {@macro flutter.material.dataTable.decoration}
final Decoration? decoration;
@@ -59,7 +69,17 @@
final MaterialStateProperty<Color?>? dataRowColor;
/// {@macro flutter.material.dataTable.dataRowHeight}
- final double? dataRowHeight;
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
+ double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
+
+ /// {@macro flutter.material.dataTable.dataRowMinHeight}
+ final double? dataRowMinHeight;
+
+ /// {@macro flutter.material.dataTable.dataRowMaxHeight}
+ final double? dataRowMaxHeight;
/// {@macro flutter.material.dataTable.dataTextStyle}
final TextStyle? dataTextStyle;
@@ -91,7 +111,13 @@
DataTableThemeData copyWith({
Decoration? decoration,
MaterialStateProperty<Color?>? dataRowColor,
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
double? dataRowHeight,
+ double? dataRowMinHeight,
+ double? dataRowMaxHeight,
TextStyle? dataTextStyle,
MaterialStateProperty<Color?>? headingRowColor,
double? headingRowHeight,
@@ -105,6 +131,8 @@
decoration: decoration ?? this.decoration,
dataRowColor: dataRowColor ?? this.dataRowColor,
dataRowHeight: dataRowHeight ?? this.dataRowHeight,
+ dataRowMinHeight: dataRowMinHeight ?? this.dataRowMinHeight,
+ dataRowMaxHeight: dataRowMaxHeight ?? this.dataRowMaxHeight,
dataTextStyle: dataTextStyle ?? this.dataTextStyle,
headingRowColor: headingRowColor ?? this.headingRowColor,
headingRowHeight: headingRowHeight ?? this.headingRowHeight,
@@ -122,10 +150,14 @@
///
/// {@macro dart.ui.shadow.lerp}
static DataTableThemeData lerp(DataTableThemeData a, DataTableThemeData b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return DataTableThemeData(
decoration: Decoration.lerp(a.decoration, b.decoration, t),
dataRowColor: MaterialStateProperty.lerp<Color?>(a.dataRowColor, b.dataRowColor, t, Color.lerp),
- dataRowHeight: lerpDouble(a.dataRowHeight, b.dataRowHeight, t),
+ dataRowMinHeight: lerpDouble(a.dataRowMinHeight, b.dataRowMinHeight, t),
+ dataRowMaxHeight: lerpDouble(a.dataRowMaxHeight, b.dataRowMaxHeight, t),
dataTextStyle: TextStyle.lerp(a.dataTextStyle, b.dataTextStyle, t),
headingRowColor: MaterialStateProperty.lerp<Color?>(a.headingRowColor, b.headingRowColor, t, Color.lerp),
headingRowHeight: lerpDouble(a.headingRowHeight, b.headingRowHeight, t),
@@ -141,7 +173,8 @@
int get hashCode => Object.hash(
decoration,
dataRowColor,
- dataRowHeight,
+ dataRowMinHeight,
+ dataRowMaxHeight,
dataTextStyle,
headingRowColor,
headingRowHeight,
@@ -163,7 +196,8 @@
return other is DataTableThemeData
&& other.decoration == decoration
&& other.dataRowColor == dataRowColor
- && other.dataRowHeight == dataRowHeight
+ && other.dataRowMinHeight == dataRowMinHeight
+ && other.dataRowMaxHeight == dataRowMaxHeight
&& other.dataTextStyle == dataTextStyle
&& other.headingRowColor == headingRowColor
&& other.headingRowHeight == headingRowHeight
@@ -179,7 +213,8 @@
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Decoration>('decoration', decoration, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dataRowColor', dataRowColor, defaultValue: null));
- properties.add(DoubleProperty('dataRowHeight', dataRowHeight, defaultValue: null));
+ properties.add(DoubleProperty('dataRowMinHeight', dataRowMinHeight, defaultValue: null));
+ properties.add(DoubleProperty('dataRowMaxHeight', dataRowMaxHeight, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('dataTextStyle', dataTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('headingRowColor', headingRowColor, defaultValue: null));
properties.add(DoubleProperty('headingRowHeight', headingRowHeight, defaultValue: null));
diff --git a/framework/lib/src/material/date_picker.dart b/framework/lib/src/material/date_picker.dart
index e9e0c90..ac032e7 100644
--- a/framework/lib/src/material/date_picker.dart
+++ b/framework/lib/src/material/date_picker.dart
@@ -164,6 +164,7 @@
String? fieldLabelText,
TextInputType? keyboardType,
Offset? anchorPoint,
+ final ValueChanged<DatePickerEntryMode>? onDatePickerModeChange
}) async {
initialDate = DateUtils.dateOnly(initialDate);
firstDate = DateUtils.dateOnly(firstDate);
@@ -202,6 +203,7 @@
fieldHintText: fieldHintText,
fieldLabelText: fieldLabelText,
keyboardType: keyboardType,
+ onDatePickerModeChange: onDatePickerModeChange,
);
if (textDirection != null) {
@@ -259,6 +261,7 @@
this.fieldLabelText,
this.keyboardType,
this.restorationId,
+ this.onDatePickerModeChange
}) : initialDate = DateUtils.dateOnly(initialDate),
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate),
@@ -356,6 +359,15 @@
/// Flutter.
final String? restorationId;
+
+ /// Called when the [DatePickerDialog] is toggled between
+ /// [DatePickerEntryMode.calendar],[DatePickerEntryMode.input].
+ ///
+ /// An example of how this callback might be used is an app that saves the
+ /// user's preferred entry mode and uses it to initialize the
+ /// `initialEntryMode` parameter the next time the date picker is shown.
+ final ValueChanged<DatePickerEntryMode>? onDatePickerModeChange;
+
@override
State<DatePickerDialog> createState() => _DatePickerDialogState();
}
@@ -394,16 +406,24 @@
Navigator.pop(context);
}
+ void _handleOnDatePickerModeChange() {
+ if (widget.onDatePickerModeChange != null) {
+ widget.onDatePickerModeChange!(_entryMode.value);
+ }
+ }
+
void _handleEntryModeToggle() {
setState(() {
switch (_entryMode.value) {
case DatePickerEntryMode.calendar:
_autovalidateMode.value = AutovalidateMode.disabled;
_entryMode.value = DatePickerEntryMode.input;
+ _handleOnDatePickerModeChange();
break;
case DatePickerEntryMode.input:
_formKey.currentState!.save();
_entryMode.value = DatePickerEntryMode.calendar;
+ _handleOnDatePickerModeChange();
break;
case DatePickerEntryMode.calendarOnly:
case DatePickerEntryMode.inputOnly:
@@ -467,7 +487,7 @@
color: headerForegroundColor,
)
// Material2 has support for landscape and the current M3 spec doesn't
- // address this layout, so handling it seperately here.
+ // address this layout, so handling it separately here.
: (orientation == Orientation.landscape
? textTheme.headlineSmall?.copyWith(color: headerForegroundColor)
: textTheme.headlineMedium?.copyWith(color: headerForegroundColor));
diff --git a/framework/lib/src/material/date_picker_theme.dart b/framework/lib/src/material/date_picker_theme.dart
index c9a394f..98a15a1 100644
--- a/framework/lib/src/material/date_picker_theme.dart
+++ b/framework/lib/src/material/date_picker_theme.dart
@@ -152,7 +152,7 @@
/// day labels in the grid of the date picker.
final MaterialStateProperty<Color?>? dayBackgroundColor;
- /// Overriddes the default highlight color that's typically used to
+ /// Overrides the default highlight color that's typically used to
/// indicate that a day in the grid is focused, hovered, or pressed.
final MaterialStateProperty<Color?>? dayOverlayColor;
@@ -356,6 +356,9 @@
/// Linearly interpolates between two [DatePickerThemeData].
static DatePickerThemeData lerp(DatePickerThemeData? a, DatePickerThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return DatePickerThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
@@ -393,8 +396,8 @@
}
static BorderSide? _lerpBorderSide(BorderSide? a, BorderSide? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return BorderSide.lerp(BorderSide(width: 0, color: b!.color.withAlpha(0)), b, t);
diff --git a/framework/lib/src/material/dialog.dart b/framework/lib/src/material/dialog.dart
index 414e8d7..99a1bee 100644
--- a/framework/lib/src/material/dialog.dart
+++ b/framework/lib/src/material/dialog.dart
@@ -482,9 +482,10 @@
/// Typically used to provide padding to the button bar between the button bar
/// and the edges of the dialog.
///
- /// If there are no [actions], then no padding will be included. It is also
- /// important to note that [buttonPadding] may contribute to the padding on
- /// the edges of [actions] as well.
+ /// The [buttonPadding] may contribute to the padding on the edges of
+ /// [actions] as well.
+ ///
+ /// If there are no [actions], then no padding will be included.
///
/// {@tool snippet}
/// This is an example of a set of actions aligned with the content widget.
@@ -546,21 +547,20 @@
/// * [OverflowBar], which [actions] configures to lay itself out.
final VerticalDirection? actionsOverflowDirection;
- /// The spacing between [actions] when the [OverflowBar] switches
- /// to a column layout because the actions don't fit horizontally.
+ /// The spacing between [actions] when the [OverflowBar] switches to a column
+ /// layout because the actions don't fit horizontally.
///
/// If the widgets in [actions] do not fit into a single row, they are
- /// arranged into a column. This parameter provides additional
- /// vertical space in between buttons when it does overflow.
+ /// arranged into a column. This parameter provides additional vertical space
+ /// between buttons when it does overflow.
///
- /// Note that the button spacing may appear to be more than
- /// the value provided. This is because most buttons adhere to the
- /// [MaterialTapTargetSize] of 48px. So, even though a button
- /// might visually be 36px in height, it might still take up to
- /// 48px vertically.
+ /// The button spacing may appear to be more than the value provided. This is
+ /// because most buttons adhere to the [MaterialTapTargetSize] of 48px. So,
+ /// even though a button might visually be 36px in height, it might still take
+ /// up to 48px vertically.
///
- /// If null then no spacing will be added in between buttons in
- /// an overflow state.
+ /// If null then no spacing will be added in between buttons in an overflow
+ /// state.
final double? actionsOverflowButtonSpacing;
/// The padding that surrounds each button in [actions].
diff --git a/framework/lib/src/material/dialog_theme.dart b/framework/lib/src/material/dialog_theme.dart
index c39da59..2a7c78a 100644
--- a/framework/lib/src/material/dialog_theme.dart
+++ b/framework/lib/src/material/dialog_theme.dart
@@ -111,6 +111,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static DialogTheme lerp(DialogTheme? a, DialogTheme? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return DialogTheme(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
diff --git a/framework/lib/src/material/divider_theme.dart b/framework/lib/src/material/divider_theme.dart
index 79d631d..58fd915 100644
--- a/framework/lib/src/material/divider_theme.dart
+++ b/framework/lib/src/material/divider_theme.dart
@@ -87,6 +87,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static DividerThemeData lerp(DividerThemeData? a, DividerThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return DividerThemeData(
color: Color.lerp(a?.color, b?.color, t),
space: lerpDouble(a?.space, b?.space, t),
diff --git a/framework/lib/src/material/drawer_theme.dart b/framework/lib/src/material/drawer_theme.dart
index 6986893..269af00 100644
--- a/framework/lib/src/material/drawer_theme.dart
+++ b/framework/lib/src/material/drawer_theme.dart
@@ -99,8 +99,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static DrawerThemeData? lerp(DrawerThemeData? a, DrawerThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return DrawerThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/dropdown.dart b/framework/lib/src/material/dropdown.dart
index 4286f51..29637b8 100644
--- a/framework/lib/src/material/dropdown.dart
+++ b/framework/lib/src/material/dropdown.dart
@@ -886,6 +886,7 @@
this.enableFeedback,
this.alignment = AlignmentDirectional.centerStart,
this.borderRadius,
+ this.padding,
// When adding new arguments, consider adding similar arguments to
// DropdownButtonFormField.
}) : assert(items == null || items.isEmpty || value == null ||
@@ -929,6 +930,7 @@
this.enableFeedback,
this.alignment = AlignmentDirectional.centerStart,
this.borderRadius,
+ this.padding,
required InputDecoration inputDecoration,
required bool isEmpty,
required bool isFocused,
@@ -1115,6 +1117,17 @@
/// instead.
final Color? dropdownColor;
+ /// Padding around the visible portion of the dropdown widget.
+ ///
+ /// As the padding increases, the size of the [DropdownButton] will also
+ /// increase. The padding is included in the clickable area of the dropdown
+ /// widget, so this can make the widget easier to click.
+ ///
+ /// Padding can be useful when used with a custom border. The clickable
+ /// area will stay flush with the border, as opposed to an external [Padding]
+ /// widget which will leave a non-clickable gap.
+ final EdgeInsetsGeometry? padding;
+
/// The maximum height of the menu.
///
/// The maximum height of the menu must be at least one row shorter than
@@ -1505,7 +1518,7 @@
autofocus: widget.autofocus,
focusColor: widget.focusColor ?? Theme.of(context).focusColor,
enableFeedback: false,
- child: result,
+ child: widget.padding == null ? result : Padding(padding: widget.padding!, child: result),
),
),
);
@@ -1566,6 +1579,7 @@
bool? enableFeedback,
AlignmentGeometry alignment = AlignmentDirectional.centerStart,
BorderRadius? borderRadius,
+ EdgeInsetsGeometry? padding,
// When adding new arguments, consider adding similar arguments to
// DropdownButton.
}) : assert(items == null || items.isEmpty || value == null ||
@@ -1635,6 +1649,7 @@
inputDecoration: effectiveDecoration.copyWith(errorText: field.errorText),
isEmpty: isEmpty,
isFocused: Focus.of(context).hasFocus,
+ padding: padding,
),
);
}),
diff --git a/framework/lib/src/material/dropdown_menu.dart b/framework/lib/src/material/dropdown_menu.dart
index a4d5ab9..d2e0632 100644
--- a/framework/lib/src/material/dropdown_menu.dart
+++ b/framework/lib/src/material/dropdown_menu.dart
@@ -498,6 +498,8 @@
?? theme.inputDecorationTheme
?? defaults.inputDecorationTheme!;
+ final MouseCursor effectiveMouseCursor = canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click;
+
return Shortcuts(
shortcuts: _kMenuTraversalShortcuts,
child: Actions(
@@ -539,6 +541,7 @@
width: widget.width,
children: <Widget>[
TextField(
+ mouseCursor: effectiveMouseCursor,
canRequestFocus: canRequestFocus(),
enableInteractiveSelection: canRequestFocus(),
textAlignVertical: TextAlignVertical.center,
diff --git a/framework/lib/src/material/dropdown_menu_theme.dart b/framework/lib/src/material/dropdown_menu_theme.dart
index 7d891c8..38a1d91 100644
--- a/framework/lib/src/material/dropdown_menu_theme.dart
+++ b/framework/lib/src/material/dropdown_menu_theme.dart
@@ -64,6 +64,9 @@
/// Linearly interpolates between two dropdown menu themes.
static DropdownMenuThemeData lerp(DropdownMenuThemeData? a, DropdownMenuThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return DropdownMenuThemeData(
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
inputDecorationTheme: t < 0.5 ? a?.inputDecorationTheme : b?.inputDecorationTheme,
diff --git a/framework/lib/src/material/elevated_button.dart b/framework/lib/src/material/elevated_button.dart
index 2ba8c92..97e8a45 100644
--- a/framework/lib/src/material/elevated_button.dart
+++ b/framework/lib/src/material/elevated_button.dart
@@ -333,10 +333,10 @@
/// * hovered - 3
/// * focused or pressed - 1
/// * `padding`
- /// * `textScaleFactor <= 1` - horizontal(16)
- /// * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
- /// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
- /// * `3 < textScaleFactor` - horizontal(4)
+ /// * `textScaleFactor <= 1` - horizontal(24)
+ /// * `1 < textScaleFactor <= 2` - lerp(horizontal(24), horizontal(12))
+ /// * `2 < textScaleFactor <= 3` - lerp(horizontal(12), horizontal(6))
+ /// * `3 < textScaleFactor` - horizontal(6)
/// * `minimumSize` - Size(64, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
@@ -351,6 +351,10 @@
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
+ ///
+ /// For the [ElevatedButton.icon] factory, the start (generally the left) value of
+ /// [padding] is reduced from 24 to 16.
+
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
@@ -390,10 +394,12 @@
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ final double padding1x = useMaterial3 ? 24.0 : 16.0;
return ButtonStyleButton.scaledPadding(
- const EdgeInsets.symmetric(horizontal: 16),
- const EdgeInsets.symmetric(horizontal: 8),
- const EdgeInsets.symmetric(horizontal: 4),
+ EdgeInsets.symmetric(horizontal: padding1x),
+ EdgeInsets.symmetric(horizontal: padding1x / 2),
+ EdgeInsets.symmetric(horizontal: padding1x / 2 / 2),
MediaQuery.textScaleFactorOf(context),
);
}
@@ -494,7 +500,13 @@
@override
ButtonStyle defaultStyleOf(BuildContext context) {
- final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ final EdgeInsetsGeometry scaledPadding = useMaterial3 ? ButtonStyleButton.scaledPadding(
+ const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0),
+ const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0),
+ const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0),
+ MediaQuery.textScaleFactorOf(context),
+ ) : ButtonStyleButton.scaledPadding(
const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
diff --git a/framework/lib/src/material/elevated_button_theme.dart b/framework/lib/src/material/elevated_button_theme.dart
index bc59beb..cff96c6 100644
--- a/framework/lib/src/material/elevated_button_theme.dart
+++ b/framework/lib/src/material/elevated_button_theme.dart
@@ -49,8 +49,8 @@
/// Linearly interpolate between two elevated button themes.
static ElevatedButtonThemeData? lerp(ElevatedButtonThemeData? a, ElevatedButtonThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ElevatedButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/expansion_panel.dart b/framework/lib/src/material/expansion_panel.dart
index f318c6b..f558fbc 100644
--- a/framework/lib/src/material/expansion_panel.dart
+++ b/framework/lib/src/material/expansion_panel.dart
@@ -118,7 +118,6 @@
///
/// See [ExpansionPanelList.radio] for a sample implementation.
class ExpansionPanelRadio extends ExpansionPanel {
-
/// An expansion panel that allows for radio functionality.
///
/// A unique [value] must be passed into the constructor. The
@@ -139,19 +138,24 @@
/// A material expansion panel list that lays out its children and animates
/// expansions.
///
-/// Note that [expansionCallback] behaves differently for [ExpansionPanelList]
-/// and [ExpansionPanelList.radio].
+/// The [expansionCallback] is called when the expansion state changes. For
+/// normal [ExpansionPanelList] widgets, it is the responsibility of the parent
+/// widget to rebuild the [ExpansionPanelList] with updated values for
+/// [ExpansionPanel.isExpanded]. For [ExpansionPanelList.radio] widgets, the
+/// open state is tracked internally and the callback is invoked both for the
+/// previously open panel, which is closing, and the previously closed panel,
+/// which is opening.
///
/// {@tool dartpad}
-/// Here is a simple example of how to implement ExpansionPanelList.
+/// Here is a simple example of how to use [ExpansionPanelList].
///
/// ** See code in examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart **
/// {@end-tool}
///
/// See also:
///
-/// * [ExpansionPanel]
-/// * [ExpansionPanelList.radio]
+/// * [ExpansionPanel], which is used in the [children] property.
+/// * [ExpansionPanelList.radio], a variant of this widget where only one panel is open at a time.
/// * <https://material.io/design/components/lists.html#types>
class ExpansionPanelList extends StatefulWidget {
/// Creates an expansion panel list widget. The [expansionCallback] is
@@ -208,10 +212,11 @@
/// passed to the second callback are the index of the panel that will close
/// and false, marking that it will be closed.
///
- /// For [ExpansionPanelList], the callback needs to setState when it's notified
- /// about the closing/opening panel. On the other hand, the callback for
- /// [ExpansionPanelList.radio] is intended to inform the parent widget of
- /// changes, as the radio panels' open/close states are managed internally.
+ /// For [ExpansionPanelList], the callback should call [State.setState] when
+ /// it is notified about the closing/opening panel. On the other hand, the
+ /// callback for [ExpansionPanelList.radio] is intended to inform the parent
+ /// widget of changes, as the radio panels' open/close states are managed
+ /// internally.
///
/// This callback is useful in order to keep track of the expanded/collapsed
/// panels in a parent widget that may need to react to these changes.
diff --git a/framework/lib/src/material/expansion_tile.dart b/framework/lib/src/material/expansion_tile.dart
index 0ec82f8..2829711 100644
--- a/framework/lib/src/material/expansion_tile.dart
+++ b/framework/lib/src/material/expansion_tile.dart
@@ -34,7 +34,8 @@
/// to the [leading] and [trailing] properties of [ExpansionTile].
///
/// {@tool dartpad}
-/// This example demonstrates different configurations of ExpansionTile.
+/// This example demonstrates how the [ExpansionTile] icon's location and appearance
+/// can be customized.
///
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
/// {@end-tool}
@@ -83,7 +84,7 @@
///
/// Typically a [CircleAvatar] widget.
///
- /// Note that depending on the value of [controlAffinity], the [leading] widget
+ /// Depending on the value of [controlAffinity], the [leading] widget
/// may replace the rotating expansion arrow icon.
final Widget? leading;
@@ -133,7 +134,7 @@
/// A widget to display after the title.
///
- /// Note that depending on the value of [controlAffinity], the [trailing] widget
+ /// Depending on the value of [controlAffinity], the [trailing] widget
/// may replace the rotating expansion arrow icon.
final Widget? trailing;
@@ -187,17 +188,19 @@
/// Specifies the alignment of each child within [children] when the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
- /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
+ /// [children], and the `crossAxisAlignment` parameter is passed directly into
+ /// the [Column].
///
/// Modifying this property controls the cross axis alignment of each child
- /// within its [Column]. Note that the width of the [Column] that houses
- /// [children] will be the same as the widest child widget in [children]. It is
- /// not necessarily the width of [Column] is equal to the width of expanded tile.
+ /// within its [Column]. The width of the [Column] that houses [children] will
+ /// be the same as the widest child widget in [children]. The width of the
+ /// [Column] might not be equal to the width of the expanded tile.
///
- /// To align the [Column] along the expanded tile, use the [expandedAlignment] property
- /// instead.
+ /// To align the [Column] along the expanded tile, use the [expandedAlignment]
+ /// property instead.
///
- /// When the value is null, the value of [expandedCrossAxisAlignment] is [CrossAxisAlignment.center].
+ /// When the value is null, the value of [expandedCrossAxisAlignment] is
+ /// [CrossAxisAlignment.center].
final CrossAxisAlignment? expandedCrossAxisAlignment;
/// Specifies padding for [children].
@@ -216,7 +219,7 @@
/// Used to override to the [ListTileThemeData.iconColor].
///
/// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
- /// is also null then the value of [ListTileThemeData.iconColor] is used.
+ /// is also null then the value of [ColorScheme.primary] is used.
///
/// See also:
///
@@ -227,6 +230,15 @@
/// The icon color of tile's expansion arrow icon when the sublist is collapsed.
///
/// Used to override to the [ListTileThemeData.iconColor].
+ ///
+ /// If this property is null then [ExpansionTileThemeData.collapsedIconColor] is used. If that
+ /// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface] is used. Otherwise,
+ /// defaults to [ThemeData.unselectedWidgetColor] color.
+ ///
+ /// See also:
+ ///
+ /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
+ /// [ExpansionTileThemeData].
final Color? collapsedIconColor;
@@ -235,7 +247,8 @@
/// Used to override to the [ListTileThemeData.textColor].
///
/// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
- /// is also null then the value of [ListTileThemeData.textColor] is used.
+ /// is also null then and [ThemeData.useMaterial3] is true, color of the [TextTheme.bodyLarge]
+ /// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color.
///
/// See also:
///
@@ -247,8 +260,10 @@
///
/// Used to override to the [ListTileThemeData.textColor].
///
- /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that
- /// is also null then the value of [ListTileThemeData.textColor] is used.
+ /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used.
+ /// If that is also null and [ThemeData.useMaterial3] is true, color of the
+ /// [TextTheme.bodyLarge] will be used for the [title] and [subtitle]. Otherwise,
+ /// defaults to color of the [TextTheme.titleMedium].
///
/// See also:
///
@@ -441,7 +456,9 @@
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
+ final ExpansionTileThemeData defaults = theme.useMaterial3
+ ? _ExpansionTileDefaultsM3(context)
+ : _ExpansionTileDefaultsM2(context);
_borderTween
..begin = widget.collapsedShape
?? expansionTileTheme.collapsedShape
@@ -458,13 +475,13 @@
_headerColorTween
..begin = widget.collapsedTextColor
?? expansionTileTheme.collapsedTextColor
- ?? theme.textTheme.titleMedium!.color
- ..end = widget.textColor ?? expansionTileTheme.textColor ?? colorScheme.primary;
+ ?? defaults.collapsedTextColor
+ ..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor;
_iconColorTween
..begin = widget.collapsedIconColor
?? expansionTileTheme.collapsedIconColor
- ?? theme.unselectedWidgetColor
- ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? colorScheme.primary;
+ ?? defaults.collapsedIconColor
+ ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor;
_backgroundColorTween
..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor
..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
@@ -498,3 +515,54 @@
);
}
}
+
+class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData {
+ _ExpansionTileDefaultsM2(this.context);
+
+ final BuildContext context;
+ late final ThemeData _theme = Theme.of(context);
+ late final ColorScheme _colorScheme = _theme.colorScheme;
+
+ @override
+ Color? get textColor => _colorScheme.primary;
+
+ @override
+ Color? get iconColor => _colorScheme.primary;
+
+ @override
+ Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color;
+
+ @override
+ Color? get collapsedIconColor => _theme.unselectedWidgetColor;
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES - ExpansionTile
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+// dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData {
+ _ExpansionTileDefaultsM3(this.context);
+
+ final BuildContext context;
+ late final ThemeData _theme = Theme.of(context);
+ late final ColorScheme _colors = _theme.colorScheme;
+
+ @override
+ Color? get textColor => _colors.onSurface;
+
+ @override
+ Color? get iconColor => _colors.primary;
+
+ @override
+ Color? get collapsedTextColor => _colors.onSurface;
+
+ @override
+ Color? get collapsedIconColor => _colors.onSurfaceVariant;
+}
+
+// END GENERATED TOKEN PROPERTIES - ExpansionTile
diff --git a/framework/lib/src/material/expansion_tile_theme.dart b/framework/lib/src/material/expansion_tile_theme.dart
index 8d62465..534ffe7 100644
--- a/framework/lib/src/material/expansion_tile_theme.dart
+++ b/framework/lib/src/material/expansion_tile_theme.dart
@@ -124,8 +124,8 @@
/// Linearly interpolate between ExpansionTileThemeData objects.
static ExpansionTileThemeData? lerp(ExpansionTileThemeData? a, ExpansionTileThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ExpansionTileThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/filled_button.dart b/framework/lib/src/material/filled_button.dart
index a7f52b6..2548eea 100644
--- a/framework/lib/src/material/filled_button.dart
+++ b/framework/lib/src/material/filled_button.dart
@@ -345,6 +345,50 @@
/// shape's [OutlinedBorder.side]. Typically the default value of an
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
///
+ /// ## Material 3 defaults
+ ///
+ /// If [ThemeData.useMaterial3] is set to true the following defaults will
+ /// be used:
+ ///
+ /// * `textStyle` - Theme.textTheme.labelLarge
+ /// * `backgroundColor`
+ /// * disabled - Theme.colorScheme.onSurface(0.12)
+ /// * others - Theme.colorScheme.secondaryContainer
+ /// * `foregroundColor`
+ /// * disabled - Theme.colorScheme.onSurface(0.38)
+ /// * others - Theme.colorScheme.onSecondaryContainer
+ /// * `overlayColor`
+ /// * hovered - Theme.colorScheme.onSecondaryContainer(0.08)
+ /// * focused or pressed - Theme.colorScheme.onSecondaryContainer(0.12)
+ /// * `shadowColor` - Theme.colorScheme.shadow
+ /// * `surfaceTintColor` - Colors.transparent
+ /// * `elevation`
+ /// * disabled - 0
+ /// * default - 1
+ /// * hovered - 3
+ /// * focused or pressed - 1
+ /// * `padding`
+ /// * `textScaleFactor <= 1` - horizontal(24)
+ /// * `1 < textScaleFactor <= 2` - lerp(horizontal(24), horizontal(12))
+ /// * `2 < textScaleFactor <= 3` - lerp(horizontal(12), horizontal(6))
+ /// * `3 < textScaleFactor` - horizontal(6)
+ /// * `minimumSize` - Size(64, 40)
+ /// * `fixedSize` - null
+ /// * `maximumSize` - Size.infinite
+ /// * `side` - null
+ /// * `shape` - StadiumBorder()
+ /// * `mouseCursor`
+ /// * disabled - SystemMouseCursors.basic
+ /// * others - SystemMouseCursors.click
+ /// * `visualDensity` - Theme.visualDensity
+ /// * `tapTargetSize` - Theme.materialTapTargetSize
+ /// * `animationDuration` - kThemeChangeDuration
+ /// * `enableFeedback` - true
+ /// * `alignment` - Alignment.center
+ /// * `splashFactory` - Theme.splashFactory
+ ///
+ /// For the [FilledButton.icon] factory, the start (generally the left) value of
+ /// [padding] is reduced from 24 to 16.
@override
ButtonStyle defaultStyleOf(BuildContext context) {
switch (_variant) {
@@ -364,10 +408,12 @@
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ final double padding1x = useMaterial3 ? 24.0 : 16.0;
return ButtonStyleButton.scaledPadding(
- const EdgeInsets.symmetric(horizontal: 16),
- const EdgeInsets.symmetric(horizontal: 8),
- const EdgeInsets.symmetric(horizontal: 4),
+ EdgeInsets.symmetric(horizontal: padding1x),
+ EdgeInsets.symmetric(horizontal: padding1x / 2),
+ EdgeInsets.symmetric(horizontal: padding1x / 2 / 2),
MediaQuery.textScaleFactorOf(context),
);
}
@@ -463,7 +509,13 @@
@override
ButtonStyle defaultStyleOf(BuildContext context) {
- final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ final EdgeInsetsGeometry scaledPadding = useMaterial3 ? ButtonStyleButton.scaledPadding(
+ const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0),
+ const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0),
+ const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0),
+ MediaQuery.textScaleFactorOf(context),
+ ) : ButtonStyleButton.scaledPadding(
const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
diff --git a/framework/lib/src/material/filled_button_theme.dart b/framework/lib/src/material/filled_button_theme.dart
index 8e3a4d3..4e45800 100644
--- a/framework/lib/src/material/filled_button_theme.dart
+++ b/framework/lib/src/material/filled_button_theme.dart
@@ -49,8 +49,8 @@
/// Linearly interpolate between two filled button themes.
static FilledButtonThemeData? lerp(FilledButtonThemeData? a, FilledButtonThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return FilledButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/floating_action_button_theme.dart b/framework/lib/src/material/floating_action_button_theme.dart
index 1c744d5..0b656e6 100644
--- a/framework/lib/src/material/floating_action_button_theme.dart
+++ b/framework/lib/src/material/floating_action_button_theme.dart
@@ -193,8 +193,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static FloatingActionButtonThemeData? lerp(FloatingActionButtonThemeData? a, FloatingActionButtonThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return FloatingActionButtonThemeData(
foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
diff --git a/framework/lib/src/material/icon_button.dart b/framework/lib/src/material/icon_button.dart
index 21f541b..e707e13 100644
--- a/framework/lib/src/material/icon_button.dart
+++ b/framework/lib/src/material/icon_button.dart
@@ -30,6 +30,8 @@
// See: <https://material.io/design/usability/accessibility.html#layout-typography>.
const double _kMinButtonSize = kMinInteractiveDimension;
+enum _IconButtonVariant { standard, filled, filledTonal, outlined }
+
/// A Material Design icon button.
///
/// An icon button is a picture printed on a [Material] widget that reacts to
@@ -115,8 +117,9 @@
/// {@end-tool}
///
/// Material Design 3 introduced new types (standard and contained) of [IconButton]s.
-/// The default [IconButton] is the standard type, and contained icon buttons can be produced
-/// by configuring the [IconButton] widget's properties.
+/// The default [IconButton] is the standard type. To create a filled icon button,
+/// use [IconButton.filled]; to create a filled tonal icon button, use [IconButton.filledTonal];
+/// to create a outlined icon button, use [IconButton.outlined].
///
/// Material Design 3 also treats [IconButton]s as toggle buttons. In order
/// to not break existing apps, the toggle feature can be optionally controlled
@@ -198,7 +201,104 @@
this.isSelected,
this.selectedIcon,
required this.icon,
- }) : assert(splashRadius == null || splashRadius > 0);
+ }) : assert(splashRadius == null || splashRadius > 0),
+ _variant = _IconButtonVariant.standard;
+
+ /// Create a filled variant of IconButton.
+ ///
+ /// Filled icon buttons have higher visual impact and should be used for
+ /// high emphasis actions, such as turning off a microphone or camera.
+ const IconButton.filled({
+ super.key,
+ this.iconSize,
+ this.visualDensity,
+ this.padding,
+ this.alignment,
+ this.splashRadius,
+ this.color,
+ this.focusColor,
+ this.hoverColor,
+ this.highlightColor,
+ this.splashColor,
+ this.disabledColor,
+ required this.onPressed,
+ this.mouseCursor,
+ this.focusNode,
+ this.autofocus = false,
+ this.tooltip,
+ this.enableFeedback,
+ this.constraints,
+ this.style,
+ this.isSelected,
+ this.selectedIcon,
+ required this.icon,
+ }) : assert(splashRadius == null || splashRadius > 0),
+ _variant = _IconButtonVariant.filled;
+
+ /// Create a filled tonal variant of IconButton.
+ ///
+ /// Filled tonal icon buttons are a middle ground between filled and outlined
+ /// icon buttons. They’re useful in contexts where the button requires slightly
+ /// more emphasis than an outline would give, such as a secondary action paired
+ /// with a high emphasis action.
+ const IconButton.filledTonal({
+ super.key,
+ this.iconSize,
+ this.visualDensity,
+ this.padding,
+ this.alignment,
+ this.splashRadius,
+ this.color,
+ this.focusColor,
+ this.hoverColor,
+ this.highlightColor,
+ this.splashColor,
+ this.disabledColor,
+ required this.onPressed,
+ this.mouseCursor,
+ this.focusNode,
+ this.autofocus = false,
+ this.tooltip,
+ this.enableFeedback,
+ this.constraints,
+ this.style,
+ this.isSelected,
+ this.selectedIcon,
+ required this.icon,
+ }) : assert(splashRadius == null || splashRadius > 0),
+ _variant = _IconButtonVariant.filledTonal;
+
+ /// Create a filled tonal variant of IconButton.
+ ///
+ /// Outlined icon buttons are medium-emphasis buttons. They’re useful when an
+ /// icon button needs more emphasis than a standard icon button but less than
+ /// a filled or filled tonal icon button.
+ const IconButton.outlined({
+ super.key,
+ this.iconSize,
+ this.visualDensity,
+ this.padding,
+ this.alignment,
+ this.splashRadius,
+ this.color,
+ this.focusColor,
+ this.hoverColor,
+ this.highlightColor,
+ this.splashColor,
+ this.disabledColor,
+ required this.onPressed,
+ this.mouseCursor,
+ this.focusNode,
+ this.autofocus = false,
+ this.tooltip,
+ this.enableFeedback,
+ this.constraints,
+ this.style,
+ this.isSelected,
+ this.selectedIcon,
+ required this.icon,
+ }) : assert(splashRadius == null || splashRadius > 0),
+ _variant = _IconButtonVariant.outlined;
/// The size of the icon inside the button.
///
@@ -465,6 +565,8 @@
/// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s.
final Widget? selectedIcon;
+ final _IconButtonVariant _variant;
+
/// A static convenience method that constructs an icon button
/// [ButtonStyle] given simple values. This method is only used for Material 3.
///
@@ -615,6 +717,7 @@
autofocus: autofocus,
focusNode: focusNode,
isSelected: isSelected,
+ variant: _variant,
child: iconButton,
);
}
@@ -714,6 +817,7 @@
this.isSelected,
this.style,
this.focusNode,
+ required this.variant,
required this.autofocus,
required this.onPressed,
required this.child,
@@ -722,6 +826,7 @@
final bool? isSelected;
final ButtonStyle? style;
final FocusNode? focusNode;
+ final _IconButtonVariant variant;
final bool autofocus;
final VoidCallback? onPressed;
final Widget child;
@@ -761,12 +866,16 @@
@override
Widget build(BuildContext context) {
+ final bool toggleable = widget.isSelected != null;
+
return _IconButtonM3(
statesController: statesController,
style: widget.style,
autofocus: widget.autofocus,
focusNode: widget.focusNode,
onPressed: widget.onPressed,
+ variant: widget.variant,
+ toggleable: toggleable,
child: widget.child,
);
}
@@ -779,6 +888,8 @@
super.focusNode,
super.autofocus = false,
super.statesController,
+ required this.variant,
+ required this.toggleable,
required Widget super.child,
}) : super(
onLongPress: null,
@@ -786,6 +897,9 @@
onFocusChange: null,
clipBehavior: Clip.none);
+ final _IconButtonVariant variant;
+ final bool toggleable;
+
/// ## Material 3 defaults
///
/// If [ThemeData.useMaterial3] is set to true the following defaults will
@@ -825,7 +939,16 @@
/// * `splashFactory` - Theme.splashFactory
@override
ButtonStyle defaultStyleOf(BuildContext context) {
- return _IconButtonDefaultsM3(context);
+ switch (variant) {
+ case _IconButtonVariant.filled:
+ return _FilledIconButtonDefaultsM3(context, toggleable);
+ case _IconButtonVariant.filledTonal:
+ return _FilledTonalIconButtonDefaultsM3(context, toggleable);
+ case _IconButtonVariant.outlined:
+ return _OutlinedIconButtonDefaultsM3(context, toggleable);
+ case _IconButtonVariant.standard:
+ return _IconButtonDefaultsM3(context, toggleable);
+ }
}
/// Returns the [IconButtonThemeData.style] of the closest [IconButtonTheme] ancestor.
@@ -963,15 +1086,16 @@
// Token database version: v0_158
class _IconButtonDefaultsM3 extends ButtonStyle {
- _IconButtonDefaultsM3(this.context)
+ _IconButtonDefaultsM3(this.context, this.toggleable)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
- final BuildContext context;
- late final ColorScheme _colors = Theme.of(context).colorScheme;
+ final BuildContext context;
+ final bool toggleable;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
// No default text style
@@ -1014,7 +1138,7 @@
if (states.contains(MaterialState.pressed)) {
return _colors.onSurfaceVariant.withOpacity(0.12);
}
- return null;
+ return Colors.transparent;
});
@override
@@ -1047,7 +1171,8 @@
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(24.0);
- // No default side
+ @override
+ MaterialStateProperty<BorderSide?>? get side => null;
@override
MaterialStateProperty<OutlinedBorder>? get shape =>
@@ -1073,3 +1198,442 @@
}
// END GENERATED TOKEN PROPERTIES - IconButton
+
+// BEGIN GENERATED TOKEN PROPERTIES - FilledIconButton
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+// dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _FilledIconButtonDefaultsM3 extends ButtonStyle {
+ _FilledIconButtonDefaultsM3(this.context, this.toggleable)
+ : super(
+ animationDuration: kThemeChangeDuration,
+ enableFeedback: true,
+ alignment: Alignment.center,
+ );
+
+ final BuildContext context;
+ final bool toggleable;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+ // No default text style
+
+ @override
+ MaterialStateProperty<Color?>? get backgroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.selected)) {
+ return _colors.primary;
+ }
+ if (toggleable) { // toggleable but unselected case
+ return _colors.surfaceVariant;
+ }
+ return _colors.primary;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get foregroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.38);
+ }
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ }
+ if (toggleable) { // toggleable but unselected case
+ return _colors.primary;
+ }
+ return _colors.onPrimary;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get overlayColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onPrimary.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onPrimary.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onPrimary.withOpacity(0.12);
+ }
+ }
+ if (toggleable) { // toggleable but unselected case
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.primary.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.primary.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.primary.withOpacity(0.12);
+ }
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onPrimary.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onPrimary.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onPrimary.withOpacity(0.12);
+ }
+ return Colors.transparent;
+ });
+
+ @override
+ MaterialStateProperty<double>? get elevation =>
+ const MaterialStatePropertyAll<double>(0.0);
+
+ @override
+ MaterialStateProperty<Color>? get shadowColor =>
+ const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+ @override
+ MaterialStateProperty<Color>? get surfaceTintColor =>
+ const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+ @override
+ MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
+ const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(8.0));
+
+ @override
+ MaterialStateProperty<Size>? get minimumSize =>
+ const MaterialStatePropertyAll<Size>(Size(40.0, 40.0));
+
+ // No default fixedSize
+
+ @override
+ MaterialStateProperty<Size>? get maximumSize =>
+ const MaterialStatePropertyAll<Size>(Size.infinite);
+
+ @override
+ MaterialStateProperty<double>? get iconSize =>
+ const MaterialStatePropertyAll<double>(24.0);
+
+ @override
+ MaterialStateProperty<BorderSide?>? get side => null;
+
+ @override
+ MaterialStateProperty<OutlinedBorder>? get shape =>
+ const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
+
+ @override
+ MaterialStateProperty<MouseCursor?>? get mouseCursor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return SystemMouseCursors.basic;
+ }
+ return SystemMouseCursors.click;
+ });
+
+ @override
+ VisualDensity? get visualDensity => VisualDensity.standard;
+
+ @override
+ MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+ @override
+ InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+}
+
+// END GENERATED TOKEN PROPERTIES - FilledIconButton
+
+// BEGIN GENERATED TOKEN PROPERTIES - FilledTonalIconButton
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+// dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _FilledTonalIconButtonDefaultsM3 extends ButtonStyle {
+ _FilledTonalIconButtonDefaultsM3(this.context, this.toggleable)
+ : super(
+ animationDuration: kThemeChangeDuration,
+ enableFeedback: true,
+ alignment: Alignment.center,
+ );
+
+ final BuildContext context;
+ final bool toggleable;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+ // No default text style
+
+ @override
+ MaterialStateProperty<Color?>? get backgroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.selected)) {
+ return _colors.secondaryContainer;
+ }
+ if (toggleable) { // toggleable but unselected case
+ return _colors.surfaceVariant;
+ }
+ return _colors.secondaryContainer;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get foregroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.38);
+ }
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onSecondaryContainer;
+ }
+ if (toggleable) { // toggleable but unselected case
+ return _colors.onSurfaceVariant;
+ }
+ return _colors.onSecondaryContainer;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get overlayColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onSecondaryContainer.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onSecondaryContainer.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onSecondaryContainer.withOpacity(0.12);
+ }
+ }
+ if (toggleable) { // toggleable but unselected case
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onSurfaceVariant.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onSurfaceVariant.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onSurfaceVariant.withOpacity(0.12);
+ }
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onSecondaryContainer.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onSecondaryContainer.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onSecondaryContainer.withOpacity(0.12);
+ }
+ return Colors.transparent;
+ });
+
+ @override
+ MaterialStateProperty<double>? get elevation =>
+ const MaterialStatePropertyAll<double>(0.0);
+
+ @override
+ MaterialStateProperty<Color>? get shadowColor =>
+ const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+ @override
+ MaterialStateProperty<Color>? get surfaceTintColor =>
+ const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+ @override
+ MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
+ const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(8.0));
+
+ @override
+ MaterialStateProperty<Size>? get minimumSize =>
+ const MaterialStatePropertyAll<Size>(Size(40.0, 40.0));
+
+ // No default fixedSize
+
+ @override
+ MaterialStateProperty<Size>? get maximumSize =>
+ const MaterialStatePropertyAll<Size>(Size.infinite);
+
+ @override
+ MaterialStateProperty<double>? get iconSize =>
+ const MaterialStatePropertyAll<double>(24.0);
+
+ @override
+ MaterialStateProperty<BorderSide?>? get side => null;
+
+ @override
+ MaterialStateProperty<OutlinedBorder>? get shape =>
+ const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
+
+ @override
+ MaterialStateProperty<MouseCursor?>? get mouseCursor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return SystemMouseCursors.basic;
+ }
+ return SystemMouseCursors.click;
+ });
+
+ @override
+ VisualDensity? get visualDensity => VisualDensity.standard;
+
+ @override
+ MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+ @override
+ InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+}
+
+// END GENERATED TOKEN PROPERTIES - FilledTonalIconButton
+
+// BEGIN GENERATED TOKEN PROPERTIES - OutlinedIconButton
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+// dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _OutlinedIconButtonDefaultsM3 extends ButtonStyle {
+ _OutlinedIconButtonDefaultsM3(this.context, this.toggleable)
+ : super(
+ animationDuration: kThemeChangeDuration,
+ enableFeedback: true,
+ alignment: Alignment.center,
+ );
+
+ final BuildContext context;
+ final bool toggleable;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+ // No default text style
+
+ @override
+ MaterialStateProperty<Color?>? get backgroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onSurface.withOpacity(0.12);
+ }
+ return Colors.transparent;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return _colors.inverseSurface;
+ }
+ return Colors.transparent;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get foregroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.38);
+ }
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onInverseSurface;
+ }
+ return _colors.onSurfaceVariant;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get overlayColor => MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onInverseSurface.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onInverseSurface.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onInverseSurface.withOpacity(0.12);
+ }
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onSurfaceVariant.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onSurfaceVariant.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onSurface.withOpacity(0.12);
+ }
+ return Colors.transparent;
+ });
+
+ @override
+ MaterialStateProperty<double>? get elevation =>
+ const MaterialStatePropertyAll<double>(0.0);
+
+ @override
+ MaterialStateProperty<Color>? get shadowColor =>
+ const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+ @override
+ MaterialStateProperty<Color>? get surfaceTintColor =>
+ const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+ @override
+ MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
+ const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(8.0));
+
+ @override
+ MaterialStateProperty<Size>? get minimumSize =>
+ const MaterialStatePropertyAll<Size>(Size(40.0, 40.0));
+
+ // No default fixedSize
+
+ @override
+ MaterialStateProperty<Size>? get maximumSize =>
+ const MaterialStatePropertyAll<Size>(Size.infinite);
+
+ @override
+ MaterialStateProperty<double>? get iconSize =>
+ const MaterialStatePropertyAll<double>(24.0);
+
+ @override
+ MaterialStateProperty<BorderSide?>? get side =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return null;
+ } else {
+ if (states.contains(MaterialState.disabled)) {
+ return BorderSide(color: _colors.onSurface.withOpacity(0.12));
+ }
+ return BorderSide(color: _colors.outline);
+ }
+ });
+
+ @override
+ MaterialStateProperty<OutlinedBorder>? get shape =>
+ const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
+
+ @override
+ MaterialStateProperty<MouseCursor?>? get mouseCursor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return SystemMouseCursors.basic;
+ }
+ return SystemMouseCursors.click;
+ });
+
+ @override
+ VisualDensity? get visualDensity => VisualDensity.standard;
+
+ @override
+ MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+ @override
+ InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+}
+
+// END GENERATED TOKEN PROPERTIES - OutlinedIconButton
diff --git a/framework/lib/src/material/icon_button_theme.dart b/framework/lib/src/material/icon_button_theme.dart
index 1f5261d..4c8cff4 100644
--- a/framework/lib/src/material/icon_button_theme.dart
+++ b/framework/lib/src/material/icon_button_theme.dart
@@ -49,8 +49,8 @@
/// Linearly interpolate between two icon button themes.
static IconButtonThemeData? lerp(IconButtonThemeData? a, IconButtonThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return IconButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/ink_decoration.dart b/framework/lib/src/material/ink_decoration.dart
index 3d5dd9d..159ae9d 100644
--- a/framework/lib/src/material/ink_decoration.dart
+++ b/framework/lib/src/material/ink_decoration.dart
@@ -288,7 +288,7 @@
_ink!.decoration = widget.decoration;
_ink!.configuration = createLocalImageConfiguration(context);
}
- return widget.child ?? const SizedBox();
+ return widget.child ?? ConstrainedBox(constraints: const BoxConstraints.expand());
}
@override
diff --git a/framework/lib/src/material/ink_sparkle.dart b/framework/lib/src/material/ink_sparkle.dart
index 6d3c7cc..3c1b1af 100644
--- a/framework/lib/src/material/ink_sparkle.dart
+++ b/framework/lib/src/material/ink_sparkle.dart
@@ -203,7 +203,7 @@
],
).animate(_animationController);
- // Creates an element of randomness so that ink eminating from the same
+ // Creates an element of randomness so that ink emanating from the same
// pixel have slightly different rings and sparkles.
_turbulenceSeed = turbulenceSeed ?? math.Random().nextDouble() * 1000.0;
}
diff --git a/framework/lib/src/material/input_decorator.dart b/framework/lib/src/material/input_decorator.dart
index 7b2b408..b6dd321 100644
--- a/framework/lib/src/material/input_decorator.dart
+++ b/framework/lib/src/material/input_decorator.dart
@@ -2741,7 +2741,7 @@
/// If null, defaults to a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
///
- /// Note that if you specify this style it will override the default behavior
+ /// Specifying this style will override the default behavior
/// of [InputDecoration] that changes the color of the label to the
/// [InputDecoration.errorStyle] color or [ColorScheme.error].
///
@@ -2771,7 +2771,7 @@
///
/// If null, defaults to [labelStyle].
///
- /// Note that if you specify this style it will override the default behavior
+ /// Specifying this style will override the default behavior
/// of [InputDecoration] that changes the color of the label to the
/// [InputDecoration.errorStyle] color or [ColorScheme.error].
///
@@ -2871,8 +2871,8 @@
/// By default the color of style will be used by the label of
/// [InputDecoration] if [InputDecoration.errorText] is not null. See
/// [InputDecoration.labelStyle] or [InputDecoration.floatingLabelStyle] for
- /// an example of how to replicate this behavior if you have specified either
- /// style.
+ /// an example of how to replicate this behavior when specifying those
+ /// styles.
/// {@endtemplate}
final TextStyle? errorStyle;
@@ -3022,7 +3022,7 @@
/// This example shows the differences between two `TextField` widgets when
/// [prefixIconConstraints] is set to the default value and when one is not.
///
- /// Note that [isDense] must be set to true to be able to
+ /// The [isDense] property must be set to true to be able to
/// set the constraints smaller than 48px.
///
/// If null, [BoxConstraints] with a minimum width and height of 48px is
@@ -3199,7 +3199,7 @@
/// This example shows the differences between two `TextField` widgets when
/// [suffixIconConstraints] is set to the default value and when one is not.
///
- /// Note that [isDense] must be set to true to be able to
+ /// The [isDense] property must be set to true to be able to
/// set the constraints smaller than 48px.
///
/// If null, [BoxConstraints] with a minimum width and height of 48px is
@@ -4645,23 +4645,23 @@
final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle();
if(states.contains(MaterialState.error)) {
if (states.contains(MaterialState.focused)) {
- return textStyle.copyWith(color:_colors.error);
+ return textStyle.copyWith(color: _colors.error);
}
if (states.contains(MaterialState.hovered)) {
- return textStyle.copyWith(color:_colors.onErrorContainer);
+ return textStyle.copyWith(color: _colors.onErrorContainer);
}
- return textStyle.copyWith(color:_colors.error);
+ return textStyle.copyWith(color: _colors.error);
}
if (states.contains(MaterialState.focused)) {
- return textStyle.copyWith(color:_colors.primary);
+ return textStyle.copyWith(color: _colors.primary);
}
if (states.contains(MaterialState.hovered)) {
- return textStyle.copyWith(color:_colors.onSurfaceVariant);
+ return textStyle.copyWith(color: _colors.onSurfaceVariant);
}
if (states.contains(MaterialState.disabled)) {
- return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
+ return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38));
}
- return textStyle.copyWith(color:_colors.onSurfaceVariant);
+ return textStyle.copyWith(color: _colors.onSurfaceVariant);
});
@override
@@ -4669,38 +4669,38 @@
final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle();
if(states.contains(MaterialState.error)) {
if (states.contains(MaterialState.focused)) {
- return textStyle.copyWith(color:_colors.error);
+ return textStyle.copyWith(color: _colors.error);
}
if (states.contains(MaterialState.hovered)) {
- return textStyle.copyWith(color:_colors.onErrorContainer);
+ return textStyle.copyWith(color: _colors.onErrorContainer);
}
- return textStyle.copyWith(color:_colors.error);
+ return textStyle.copyWith(color: _colors.error);
}
if (states.contains(MaterialState.focused)) {
- return textStyle.copyWith(color:_colors.primary);
+ return textStyle.copyWith(color: _colors.primary);
}
if (states.contains(MaterialState.hovered)) {
- return textStyle.copyWith(color:_colors.onSurfaceVariant);
+ return textStyle.copyWith(color: _colors.onSurfaceVariant);
}
if (states.contains(MaterialState.disabled)) {
- return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
+ return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38));
}
- return textStyle.copyWith(color:_colors.onSurfaceVariant);
+ return textStyle.copyWith(color: _colors.onSurfaceVariant);
});
@override
TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle();
if (states.contains(MaterialState.disabled)) {
- return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
+ return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38));
}
- return textStyle.copyWith(color:_colors.onSurfaceVariant);
+ return textStyle.copyWith(color: _colors.onSurfaceVariant);
});
@override
TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle();
- return textStyle.copyWith(color:_colors.error);
+ return textStyle.copyWith(color: _colors.error);
});
}
diff --git a/framework/lib/src/material/list_tile.dart b/framework/lib/src/material/list_tile.dart
index 7fa0ea3..642e6a1 100644
--- a/framework/lib/src/material/list_tile.dart
+++ b/framework/lib/src/material/list_tile.dart
@@ -131,7 +131,7 @@
/// see the example below to see how to adhere to both Material spec and
/// accessibility requirements.
///
-/// Note that [leading] and [trailing] widgets can expand as far as they wish
+/// The [leading] and [trailing] widgets can expand as far as they wish
/// horizontally, so ensure that they are properly constrained.
///
/// List tiles are typically used in [ListView]s, or arranged in [Column]s in
diff --git a/framework/lib/src/material/list_tile_theme.dart b/framework/lib/src/material/list_tile_theme.dart
index 9be598a..e57ac36 100644
--- a/framework/lib/src/material/list_tile_theme.dart
+++ b/framework/lib/src/material/list_tile_theme.dart
@@ -172,8 +172,8 @@
/// Linearly interpolate between ListTileThemeData objects.
static ListTileThemeData? lerp(ListTileThemeData? a, ListTileThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ListTileThemeData(
dense: t < 0.5 ? a?.dense : b?.dense,
@@ -480,6 +480,9 @@
Color? selectedColor,
Color? iconColor,
Color? textColor,
+ TextStyle? titleTextStyle,
+ TextStyle? subtitleTextStyle,
+ TextStyle? leadingAndTrailingTextStyle,
EdgeInsetsGeometry? contentPadding,
Color? tileColor,
Color? selectedTileColor,
@@ -488,6 +491,8 @@
double? minVerticalPadding,
double? minLeadingWidth,
ListTileTitleAlignment? titleAlignment,
+ MaterialStateProperty<MouseCursor?>? mouseCursor,
+ VisualDensity? visualDensity,
required Widget child,
}) {
return Builder(
@@ -502,6 +507,9 @@
selectedColor: selectedColor ?? parent.selectedColor,
iconColor: iconColor ?? parent.iconColor,
textColor: textColor ?? parent.textColor,
+ titleTextStyle: titleTextStyle ?? parent.titleTextStyle,
+ subtitleTextStyle: subtitleTextStyle ?? parent.subtitleTextStyle,
+ leadingAndTrailingTextStyle: leadingAndTrailingTextStyle ?? parent.leadingAndTrailingTextStyle,
contentPadding: contentPadding ?? parent.contentPadding,
tileColor: tileColor ?? parent.tileColor,
selectedTileColor: selectedTileColor ?? parent.selectedTileColor,
@@ -510,6 +518,8 @@
minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding,
minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth,
titleAlignment: titleAlignment ?? parent.titleAlignment,
+ mouseCursor: mouseCursor ?? parent.mouseCursor,
+ visualDensity: visualDensity ?? parent.visualDensity,
),
child: child,
);
diff --git a/framework/lib/src/material/magnifier.dart b/framework/lib/src/material/magnifier.dart
index 075d40e..8832510 100644
--- a/framework/lib/src/material/magnifier.dart
+++ b/framework/lib/src/material/magnifier.dart
@@ -86,7 +86,7 @@
// Should _only_ be null on construction. This is because of the animation logic.
//
// Animations are added when `last_build_y != current_build_y`. This condition
- // is true on the inital render, which would mean that the inital
+ // is true on the initial render, which would mean that the initial
// build would be animated - this is undesired. Thus, this is null for the
// first frame and the condition becomes `magnifierPosition != null && last_build_y != this_build_y`.
Offset? _magnifierPosition;
diff --git a/framework/lib/src/material/material.dart b/framework/lib/src/material/material.dart
index 74e7cdc..5a391ee 100644
--- a/framework/lib/src/material/material.dart
+++ b/framework/lib/src/material/material.dart
@@ -641,7 +641,7 @@
}
void _didChangeLayout() {
- if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
+ if (_inkFeatures?.isNotEmpty ?? false) {
markNeedsPaint();
}
}
@@ -651,16 +651,18 @@
@override
void paint(PaintingContext context, Offset offset) {
- if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
+ final List<InkFeature>? inkFeatures = _inkFeatures;
+ if (inkFeatures != null && inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Offset.zero & size);
- for (final InkFeature inkFeature in _inkFeatures!) {
+ for (final InkFeature inkFeature in inkFeatures) {
inkFeature._paint(canvas);
}
canvas.restore();
}
+ assert(inkFeatures == _inkFeatures);
super.paint(context, offset);
}
}
@@ -740,32 +742,71 @@
onRemoved?.call();
}
+ // Returns the paint transform that allows `fromRenderObject` to perform paint
+ // in `toRenderObject`'s coordinate space.
+ //
+ // Returns null if either `fromRenderObject` or `toRenderObject` is not in the
+ // same render tree, or either of them is in an offscreen subtree (see
+ // RenderObject.paintsChild).
+ static Matrix4? _getPaintTransform(
+ RenderObject fromRenderObject,
+ RenderObject toRenderObject,
+ ) {
+ // The paths to fromRenderObject and toRenderObject's common ancestor.
+ final List<RenderObject> fromPath = <RenderObject>[fromRenderObject];
+ final List<RenderObject> toPath = <RenderObject>[toRenderObject];
+
+ RenderObject from = fromRenderObject;
+ RenderObject to = toRenderObject;
+
+ while (!identical(from, to)) {
+ final int fromDepth = from.depth;
+ final int toDepth = to.depth;
+
+ if (fromDepth >= toDepth) {
+ final AbstractNode? fromParent = from.parent;
+ // Return early if the 2 render objects are not in the same render tree,
+ // or either of them is offscreen and thus won't get painted.
+ if (fromParent is! RenderObject || !fromParent.paintsChild(from)) {
+ return null;
+ }
+ fromPath.add(fromParent);
+ from = fromParent;
+ }
+
+ if (fromDepth <= toDepth) {
+ final AbstractNode? toParent = to.parent;
+ if (toParent is! RenderObject || !toParent.paintsChild(to)) {
+ return null;
+ }
+ toPath.add(toParent);
+ to = toParent;
+ }
+ }
+ assert(identical(from, to));
+
+ final Matrix4 transform = Matrix4.identity();
+ final Matrix4 inverseTransform = Matrix4.identity();
+
+ for (int index = toPath.length - 1; index > 0; index -= 1) {
+ toPath[index].applyPaintTransform(toPath[index - 1], transform);
+ }
+ for (int index = fromPath.length - 1; index > 0; index -= 1) {
+ fromPath[index].applyPaintTransform(fromPath[index - 1], inverseTransform);
+ }
+
+ final double det = inverseTransform.invert();
+ return det != 0 ? (inverseTransform..multiply(transform)) : null;
+ }
+
void _paint(Canvas canvas) {
assert(referenceBox.attached);
assert(!_debugDisposed);
- // find the chain of renderers from us to the feature's referenceBox
- final List<RenderObject> descendants = <RenderObject>[referenceBox];
- RenderObject node = referenceBox;
- while (node != _controller) {
- final RenderObject childNode = node;
- node = node.parent! as RenderObject;
- if (!node.paintsChild(childNode)) {
- // Some node between the reference box and this would skip painting on
- // the reference box, so bail out early and avoid unnecessary painting.
- // Some cases where this can happen are the reference box being
- // offstage, in a fully transparent opacity node, or in a keep alive
- // bucket.
- return;
- }
- descendants.add(node);
- }
// determine the transform that gets our coordinate system to be like theirs
- final Matrix4 transform = Matrix4.identity();
- assert(descendants.length >= 2);
- for (int index = descendants.length - 1; index > 0; index -= 1) {
- descendants[index].applyPaintTransform(descendants[index - 1], transform);
+ final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
+ if (transform != null) {
+ paintFeature(canvas, transform);
}
- paintFeature(canvas, transform);
}
/// Override this method to paint the ink feature.
diff --git a/framework/lib/src/material/menu_anchor.dart b/framework/lib/src/material/menu_anchor.dart
index d47d624..4ac2a51 100644
--- a/framework/lib/src/material/menu_anchor.dart
+++ b/framework/lib/src/material/menu_anchor.dart
@@ -3619,15 +3619,15 @@
@override
MaterialStateProperty<EdgeInsetsGeometry?>? get padding {
- return MaterialStatePropertyAll<EdgeInsetsGeometry>(
+ return const MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.symmetric(
- horizontal: math.max(
- _kTopLevelMenuHorizontalMinPadding,
- 2 + Theme.of(context).visualDensity.baseSizeAdjustment.dx,
- ),
+ horizontal: _kTopLevelMenuHorizontalMinPadding
),
);
}
+
+ @override
+ VisualDensity get visualDensity => Theme.of(context).visualDensity;
}
class _MenuButtonDefaultsM3 extends ButtonStyle {
@@ -3764,10 +3764,25 @@
// The horizontal padding number comes from the spec.
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+ VisualDensity visualDensity = Theme.of(context).visualDensity;
+ // When horizontal VisualDensity is greater than zero, set it to zero
+ // because the [ButtonStyleButton] has already handle the padding based on the density.
+ // However, the [ButtonStyleButton] doesn't allow the [VisualDensity] adjustment
+ // to reduce the width of the left/right padding, so we need to handle it here if
+ // the density is less than zero, such as on desktop platforms.
+ if (visualDensity.horizontal > 0) {
+ visualDensity = VisualDensity(vertical: visualDensity.vertical);
+ }
return ButtonStyleButton.scaledPadding(
- const EdgeInsets.symmetric(horizontal: 12),
- const EdgeInsets.symmetric(horizontal: 8),
- const EdgeInsets.symmetric(horizontal: 4),
+ EdgeInsets.symmetric(horizontal: math.max(
+ _kMenuViewPadding,
+ _kLabelItemDefaultSpacing + visualDensity.baseSizeAdjustment.dx,
+ )),
+ EdgeInsets.symmetric(horizontal: math.max(
+ _kMenuViewPadding,
+ 8 + visualDensity.baseSizeAdjustment.dx,
+ )),
+ const EdgeInsets.symmetric(horizontal: _kMenuViewPadding),
MediaQuery.maybeTextScaleFactorOf(context) ?? 1,
);
}
@@ -3805,15 +3820,13 @@
@override
MaterialStateProperty<EdgeInsetsGeometry?>? get padding {
- return MaterialStatePropertyAll<EdgeInsetsGeometry>(
- EdgeInsetsDirectional.symmetric(
- vertical: math.max(
- _kMenuVerticalMinPadding,
- 2 + Theme.of(context).visualDensity.baseSizeAdjustment.dy,
- ),
- ),
+ return const MaterialStatePropertyAll<EdgeInsetsGeometry>(
+ EdgeInsetsDirectional.symmetric(vertical: _kMenuVerticalMinPadding),
);
}
+
+ @override
+ VisualDensity get visualDensity => Theme.of(context).visualDensity;
}
// END GENERATED TOKEN PROPERTIES - Menu
diff --git a/framework/lib/src/material/menu_bar_theme.dart b/framework/lib/src/material/menu_bar_theme.dart
index ffda944..b895b0b 100644
--- a/framework/lib/src/material/menu_bar_theme.dart
+++ b/framework/lib/src/material/menu_bar_theme.dart
@@ -41,8 +41,11 @@
/// Creates a const set of properties used to configure [MenuTheme].
const MenuBarThemeData({super.style});
- /// Linearly interpolate between two text button themes.
+ /// Linearly interpolate between two [MenuBar] themes.
static MenuBarThemeData? lerp(MenuBarThemeData? a, MenuBarThemeData? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return MenuBarThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
}
}
diff --git a/framework/lib/src/material/menu_button_theme.dart b/framework/lib/src/material/menu_button_theme.dart
index 6eef7a4..0025a29 100644
--- a/framework/lib/src/material/menu_button_theme.dart
+++ b/framework/lib/src/material/menu_button_theme.dart
@@ -59,6 +59,9 @@
/// Linearly interpolate between two menu button themes.
static MenuButtonThemeData? lerp(MenuButtonThemeData? a, MenuButtonThemeData? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return MenuButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t));
}
diff --git a/framework/lib/src/material/menu_style.dart b/framework/lib/src/material/menu_style.dart
index fa0dc89..8632920 100644
--- a/framework/lib/src/material/menu_style.dart
+++ b/framework/lib/src/material/menu_style.dart
@@ -304,8 +304,8 @@
/// Linearly interpolate between two [MenuStyle]s.
static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return MenuStyle(
backgroundColor: MaterialStateProperty.lerp<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
diff --git a/framework/lib/src/material/menu_theme.dart b/framework/lib/src/material/menu_theme.dart
index 685ce48..7f541b1 100644
--- a/framework/lib/src/material/menu_theme.dart
+++ b/framework/lib/src/material/menu_theme.dart
@@ -43,6 +43,9 @@
/// Linearly interpolate between two menu button themes.
static MenuThemeData? lerp(MenuThemeData? a, MenuThemeData? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return MenuThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
}
diff --git a/framework/lib/src/material/navigation_bar.dart b/framework/lib/src/material/navigation_bar.dart
index c9bfa99..bdb20b7 100644
--- a/framework/lib/src/material/navigation_bar.dart
+++ b/framework/lib/src/material/navigation_bar.dart
@@ -169,7 +169,7 @@
/// is used. Otherwise, [ColorScheme.secondary] with an opacity of 0.24 is used.
final Color? indicatorColor;
- /// The shape of the selected inidicator.
+ /// The shape of the selected indicator.
///
/// If null, [NavigationBarThemeData.indicatorShape] is used. If that
/// is also null and [ThemeData.useMaterial3] is true, [StadiumBorder] is used.
diff --git a/framework/lib/src/material/navigation_bar_theme.dart b/framework/lib/src/material/navigation_bar_theme.dart
index 9fc4b8b..c3a79ab 100644
--- a/framework/lib/src/material/navigation_bar_theme.dart
+++ b/framework/lib/src/material/navigation_bar_theme.dart
@@ -125,8 +125,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static NavigationBarThemeData? lerp(NavigationBarThemeData? a, NavigationBarThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return NavigationBarThemeData(
height: lerpDouble(a?.height, b?.height, t),
diff --git a/framework/lib/src/material/navigation_drawer.dart b/framework/lib/src/material/navigation_drawer.dart
index 39c06fb..9583844 100644
--- a/framework/lib/src/material/navigation_drawer.dart
+++ b/framework/lib/src/material/navigation_drawer.dart
@@ -98,7 +98,7 @@
/// If that is also null, defaults to [ColorScheme.secondaryContainer].
final Color? indicatorColor;
- /// The shape of the selected inidicator.
+ /// The shape of the selected indicator.
///
/// If this is null, [NavigationDrawerThemeData.indicatorShape] is used.
/// If that is also null, defaults to [StadiumBorder].
@@ -114,7 +114,7 @@
/// [NavigationDrawerDestination] or null if no destination is selected.
///
/// A valid [selectedIndex] satisfies 0 <= [selectedIndex] < number of [NavigationDrawerDestination].
- /// For an invalid [selectedIndex] like `-1`, all desitinations will appear unselected.
+ /// For an invalid [selectedIndex] like `-1`, all destinations will appear unselected.
final int? selectedIndex;
/// Called when one of the [NavigationDrawerDestination] children is selected.
diff --git a/framework/lib/src/material/navigation_drawer_theme.dart b/framework/lib/src/material/navigation_drawer_theme.dart
index 1153a47..e611ce0 100644
--- a/framework/lib/src/material/navigation_drawer_theme.dart
+++ b/framework/lib/src/material/navigation_drawer_theme.dart
@@ -124,10 +124,9 @@
/// If both arguments are null then null is returned.
///
/// {@macro dart.ui.shadow.lerp}
- static NavigationDrawerThemeData? lerp(
- NavigationDrawerThemeData? a, NavigationDrawerThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ static NavigationDrawerThemeData? lerp(NavigationDrawerThemeData? a, NavigationDrawerThemeData? b, double t) {
+ if (identical(a, b)) {
+ return a;
}
return NavigationDrawerThemeData(
tileHeight: lerpDouble(a?.tileHeight, b?.tileHeight, t),
diff --git a/framework/lib/src/material/navigation_rail.dart b/framework/lib/src/material/navigation_rail.dart
index 27d244c..47b7a7a 100644
--- a/framework/lib/src/material/navigation_rail.dart
+++ b/framework/lib/src/material/navigation_rail.dart
@@ -578,7 +578,7 @@
);
final bool material3 = Theme.of(context).useMaterial3;
- final EdgeInsets destionationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context));
+ final EdgeInsets destinationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context));
Offset indicatorOffset;
final Widget themedIcon = IconTheme(
@@ -597,8 +597,8 @@
// Split the destination spacing across the top and bottom to keep the icon centered.
final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null;
indicatorOffset = Offset(
- minWidth / 2 + destionationPadding.left,
- _verticalDestinationSpacingM3 / 2 + destionationPadding.top,
+ minWidth / 2 + destinationPadding.left,
+ _verticalDestinationSpacingM3 / 2 + destinationPadding.top,
);
final Widget iconPart = Column(
children: <Widget>[
@@ -675,8 +675,8 @@
final Widget topSpacing = SizedBox(height: material3 ? 0 : verticalPadding);
final Widget labelSpacing = SizedBox(height: material3 ? lerpDouble(0, _verticalIconLabelSpacingM3, appearingAnimationValue)! : 0);
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding);
- final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
- final double indicatorVerticalPadding = destionationPadding.top;
+ final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2);
+ final double indicatorVerticalPadding = destinationPadding.top;
indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
@@ -723,8 +723,8 @@
final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel);
final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0);
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel);
- final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
- final double indicatorVerticalPadding = destionationPadding.top;
+ final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2);
+ final double indicatorVerticalPadding = destinationPadding.top;
indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
@@ -944,7 +944,7 @@
/// The color of the [indicatorShape] when this destination is selected.
final Color? indicatorColor;
- /// The shape of the selection inidicator.
+ /// The shape of the selection indicator.
final ShapeBorder? indicatorShape;
/// The label for the destination.
diff --git a/framework/lib/src/material/navigation_rail_theme.dart b/framework/lib/src/material/navigation_rail_theme.dart
index 2253054..cfa023b 100644
--- a/framework/lib/src/material/navigation_rail_theme.dart
+++ b/framework/lib/src/material/navigation_rail_theme.dart
@@ -143,8 +143,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static NavigationRailThemeData? lerp(NavigationRailThemeData? a, NavigationRailThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return NavigationRailThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/outlined_button.dart b/framework/lib/src/material/outlined_button.dart
index 0f26afe..3289c70 100644
--- a/framework/lib/src/material/outlined_button.dart
+++ b/framework/lib/src/material/outlined_button.dart
@@ -287,10 +287,10 @@
/// * `surfaceTintColor` - null
/// * `elevation` - 0
/// * `padding`
- /// * `textScaleFactor <= 1` - horizontal(16)
- /// * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
- /// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
- /// * `3 < textScaleFactor` - horizontal(4)
+ /// * `textScaleFactor <= 1` - horizontal(24)
+ /// * `1 < textScaleFactor <= 2` - lerp(horizontal(24), horizontal(12))
+ /// * `2 < textScaleFactor <= 3` - lerp(horizontal(12), horizontal(6))
+ /// * `3 < textScaleFactor` - horizontal(6)
/// * `minimumSize` - Size(64, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
@@ -307,6 +307,9 @@
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
+ ///
+ /// For the [OutlinedButton.icon] factory, the start (generally the left) value of
+ /// [padding] is reduced from 24 to 16.
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
@@ -347,10 +350,12 @@
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ final double padding1x = useMaterial3 ? 24.0 : 16.0;
return ButtonStyleButton.scaledPadding(
- const EdgeInsets.symmetric(horizontal: 16),
- const EdgeInsets.symmetric(horizontal: 8),
- const EdgeInsets.symmetric(horizontal: 4),
+ EdgeInsets.symmetric(horizontal: padding1x),
+ EdgeInsets.symmetric(horizontal: padding1x / 2),
+ EdgeInsets.symmetric(horizontal: padding1x / 2 / 2),
MediaQuery.textScaleFactorOf(context),
);
}
@@ -422,6 +427,23 @@
clipBehavior: clipBehavior ?? Clip.none,
child: _OutlinedButtonWithIconChild(icon: icon, label: label),
);
+
+ @override
+ ButtonStyle defaultStyleOf(BuildContext context) {
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ if (!useMaterial3) {
+ return super.defaultStyleOf(context);
+ }
+ final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
+ const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0),
+ const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0),
+ const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0),
+ MediaQuery.textScaleFactorOf(context),
+ );
+ return super.defaultStyleOf(context).copyWith(
+ padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
+ );
+ }
}
class _OutlinedButtonWithIconChild extends StatelessWidget {
diff --git a/framework/lib/src/material/outlined_button_theme.dart b/framework/lib/src/material/outlined_button_theme.dart
index bd12404..fd78642 100644
--- a/framework/lib/src/material/outlined_button_theme.dart
+++ b/framework/lib/src/material/outlined_button_theme.dart
@@ -49,8 +49,8 @@
/// Linearly interpolate between two outlined button themes.
static OutlinedButtonThemeData? lerp(OutlinedButtonThemeData? a, OutlinedButtonThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return OutlinedButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/page_transitions_theme.dart b/framework/lib/src/material/page_transitions_theme.dart
index f41fbc7..654cb1b 100644
--- a/framework/lib/src/material/page_transitions_theme.dart
+++ b/framework/lib/src/material/page_transitions_theme.dart
@@ -151,7 +151,7 @@
class _ZoomPageTransition extends StatelessWidget {
/// Creates a [_ZoomPageTransition].
///
- /// The [animation] and [secondaryAnimation] argument are required and must
+ /// The [animation] and [secondaryAnimation] arguments are required and must
/// not be null.
const _ZoomPageTransition({
required this.animation,
@@ -196,9 +196,15 @@
/// Whether the [SnapshotWidget] will be used.
///
- /// Notably, this improves performance by disabling animations on both the outgoing and
- /// incoming route. This also implies that ink-splashes or similar animations will
- /// not animate during the transition.
+ /// When this value is true, performance is improved by disabling animations
+ /// on both the outgoing and incoming route. This also implies that ink-splashes
+ /// or similar animations will not animate during the transition.
+ ///
+ /// See also:
+ ///
+ /// * [TransitionRoute.allowSnapshotting], which defines wether the route
+ /// transition will prefer to animate a snapshot of the entering and exiting
+ /// routes.
final bool allowSnapshotting;
/// The widget below this widget in the tree.
@@ -604,9 +610,36 @@
/// Constructs a page transition animation that matches the transition used on
/// Android Q.
const ZoomPageTransitionsBuilder({
+ this.allowSnapshotting = true,
this.allowEnterRouteSnapshotting = true,
});
+ /// Whether zoom page transitions will prefer to animate a snapshot of the entering
+ /// and exiting routes.
+ ///
+ /// If not specified, defaults to true.
+ ///
+ /// When this value is true, zoom page transitions will snapshot the entering and
+ /// exiting routes. These snapshots are then animated in place of the underlying
+ /// widgets to improve performance of the transition.
+ ///
+ /// Generally this means that animations that occur on the entering/exiting route
+ /// while the route animation plays may appear frozen - unless they are a hero
+ /// animation or something that is drawn in a separate overlay.
+ ///
+ /// {@tool dartpad}
+ /// This example shows a [MaterialApp] that disables snapshotting for the zoom
+ /// transitions on Android.
+ ///
+ /// ** See code in examples/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart **
+ /// {@end-tool}
+ ///
+ /// See also:
+ ///
+ /// * [PageRoute.allowSnapshotting], which enables or disables snapshotting
+ /// on a per route basis.
+ final bool allowSnapshotting;
+
/// Whether to enable snapshotting on the entering route during the
/// transition animation.
///
@@ -627,7 +660,7 @@
return _ZoomPageTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
- allowSnapshotting: route?.allowSnapshotting ?? true,
+ allowSnapshotting: allowSnapshotting && (route?.allowSnapshotting ?? true),
allowEnterRouteSnapshotting: allowEnterRouteSnapshotting,
child: child,
);
@@ -669,7 +702,13 @@
/// and delegates to [buildTransitions].
///
/// If a builder with a matching platform is not found, then the
-/// [FadeUpwardsPageTransitionsBuilder] is used.
+/// [ZoomPageTransitionsBuilder] is used.
+///
+/// {@tool dartpad}
+/// This example shows a [MaterialApp] that defines a custom [PageTransitionsTheme].
+///
+/// ** See code in examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart **
+/// {@end-tool}
///
/// See also:
///
@@ -702,8 +741,9 @@
Map<TargetPlatform, PageTransitionsBuilder> get builders => _builders;
final Map<TargetPlatform, PageTransitionsBuilder> _builders;
- /// Delegates to the builder for the current [ThemeData.platform]
- /// or [ZoomPageTransitionsBuilder].
+ /// Delegates to the builder for the current [ThemeData.platform].
+ /// If a builder for the current platform is not found, then the
+ /// [ZoomPageTransitionsBuilder] is used.
///
/// [MaterialPageRoute.buildTransitions] delegates to this method.
Widget buildTransitions<T>(
@@ -724,8 +764,8 @@
return matchingBuilder.buildTransitions<T>(route, context, animation, secondaryAnimation, child);
}
- // Just used to the builders Map to a list with one PageTransitionsBuilder per platform
- // for the operator == overload.
+ // Map the builders to a list with one PageTransitionsBuilder per platform for
+ // the operator == overload.
List<PageTransitionsBuilder?> _all(Map<TargetPlatform, PageTransitionsBuilder> builders) {
return TargetPlatform.values.map((TargetPlatform platform) => builders[platform]).toList();
}
@@ -968,7 +1008,9 @@
@override
bool shouldRepaint(covariant _ZoomExitTransitionPainter oldDelegate) {
- return oldDelegate.reverse != reverse || oldDelegate.fade.value != fade.value || oldDelegate.scale.value != scale.value;
+ return oldDelegate.reverse != reverse
+ || oldDelegate.fade.value != fade.value
+ || oldDelegate.scale.value != scale.value;
}
@override
diff --git a/framework/lib/src/material/paginated_data_table.dart b/framework/lib/src/material/paginated_data_table.dart
index 56ed3b4..8a10251 100644
--- a/framework/lib/src/material/paginated_data_table.dart
+++ b/framework/lib/src/material/paginated_data_table.dart
@@ -71,7 +71,13 @@
this.sortColumnIndex,
this.sortAscending = true,
this.onSelectAll,
- this.dataRowHeight = kMinInteractiveDimension,
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
+ double? dataRowHeight,
+ double? dataRowMinHeight,
+ double? dataRowMaxHeight,
this.headingRowHeight = 56.0,
this.horizontalMargin = 24.0,
this.columnSpacing = 56.0,
@@ -91,6 +97,11 @@
}) : assert(actions == null || (header != null)),
assert(columns.isNotEmpty),
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
+ assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
+ assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
+ 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'),
+ dataRowMinHeight = dataRowHeight ?? dataRowMinHeight ?? kMinInteractiveDimension,
+ dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight ?? kMinInteractiveDimension,
assert(rowsPerPage > 0),
assert(() {
if (onRowsPerPageChanged != null) {
@@ -147,7 +158,23 @@
///
/// This value is optional and defaults to kMinInteractiveDimension if not
/// specified.
- final double dataRowHeight;
+ @Deprecated(
+ 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+ 'This feature was deprecated after v3.7.0-5.0.pre.',
+ )
+ double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
+
+ /// The minimum height of each row (excluding the row that contains column headings).
+ ///
+ /// This value is optional and defaults to [kMinInteractiveDimension] if not
+ /// specified.
+ final double dataRowMinHeight;
+
+ /// The maximum height of each row (excluding the row that contains column headings).
+ ///
+ /// This value is optional and defaults to kMinInteractiveDimension if not
+ /// specified.
+ final double dataRowMaxHeight;
/// The height of the heading row.
///
@@ -518,7 +545,8 @@
// Make sure no decoration is set on the DataTable
// from the theme, as its already wrapped in a Card.
decoration: const BoxDecoration(),
- dataRowHeight: widget.dataRowHeight,
+ dataRowMinHeight: widget.dataRowMinHeight,
+ dataRowMaxHeight: widget.dataRowMaxHeight,
headingRowHeight: widget.headingRowHeight,
horizontalMargin: widget.horizontalMargin,
checkboxHorizontalMargin: widget.checkboxHorizontalMargin,
diff --git a/framework/lib/src/material/popup_menu.dart b/framework/lib/src/material/popup_menu.dart
index 44991de..65d89ab 100644
--- a/framework/lib/src/material/popup_menu.dart
+++ b/framework/lib/src/material/popup_menu.dart
@@ -251,7 +251,7 @@
/// The padding of the menu item.
///
- /// Note that [height] may interact with the applied padding. For example,
+ /// The [height] property may interact with the applied padding. For example,
/// If a [height] greater than the height of the sum of the padding and [child]
/// is provided, then the padding's effect will not be visible.
///
diff --git a/framework/lib/src/material/popup_menu_theme.dart b/framework/lib/src/material/popup_menu_theme.dart
index 9b3d4c8..00314ca 100644
--- a/framework/lib/src/material/popup_menu_theme.dart
+++ b/framework/lib/src/material/popup_menu_theme.dart
@@ -129,8 +129,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static PopupMenuThemeData? lerp(PopupMenuThemeData? a, PopupMenuThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return PopupMenuThemeData(
color: Color.lerp(a?.color, b?.color, t),
diff --git a/framework/lib/src/material/progress_indicator_theme.dart b/framework/lib/src/material/progress_indicator_theme.dart
index b2b5521..6690dee 100644
--- a/framework/lib/src/material/progress_indicator_theme.dart
+++ b/framework/lib/src/material/progress_indicator_theme.dart
@@ -84,8 +84,8 @@
///
/// If both arguments are null, then null is returned.
static ProgressIndicatorThemeData? lerp(ProgressIndicatorThemeData? a, ProgressIndicatorThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ProgressIndicatorThemeData(
color: Color.lerp(a?.color, b?.color, t),
diff --git a/framework/lib/src/material/radio_theme.dart b/framework/lib/src/material/radio_theme.dart
index a3775cc..e45553b 100644
--- a/framework/lib/src/material/radio_theme.dart
+++ b/framework/lib/src/material/radio_theme.dart
@@ -112,6 +112,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static RadioThemeData lerp(RadioThemeData? a, RadioThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return RadioThemeData(
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
fillColor: MaterialStateProperty.lerp<Color?>(a?.fillColor, b?.fillColor, t, Color.lerp),
diff --git a/framework/lib/src/material/refresh_indicator.dart b/framework/lib/src/material/refresh_indicator.dart
index c04a70f..5c217c3 100644
--- a/framework/lib/src/material/refresh_indicator.dart
+++ b/framework/lib/src/material/refresh_indicator.dart
@@ -5,8 +5,8 @@
import 'dart:async';
import 'dart:math' as math;
+import 'package:flute/cupertino.dart';
import 'package:flute/foundation.dart' show clampDouble;
-import 'package:flute/widgets.dart';
import 'debug.dart';
import 'material_localizations.dart';
@@ -59,6 +59,8 @@
onEdge,
}
+enum _IndicatorType { material, adaptive }
+
/// A widget that supports the Material "swipe to refresh" idiom.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
@@ -138,7 +140,38 @@
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
- });
+ }) : _indicatorType = _IndicatorType.material;
+
+ /// Creates an adaptive [RefreshIndicator] based on whether the target
+ /// platform is iOS or macOS, following Material design's
+ /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
+ ///
+ /// When the descendant overscrolls, a different spinning progress indicator
+ /// is shown depending on platform. On iOS and macOS,
+ /// [CupertinoActivityIndicator] is shown, but on all other platforms,
+ /// [CircularProgressIndicator] appears.
+ ///
+ /// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
+ /// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
+ ///
+ /// The target platform is based on the current [Theme]: [ThemeData.platform].
+ ///
+ /// Noteably the scrollable widget itself will have slightly different behavior
+ /// from [CupertinoSliverRefreshControl], due to a difference in structure.
+ const RefreshIndicator.adaptive({
+ super.key,
+ required this.child,
+ this.displacement = 40.0,
+ this.edgeOffset = 0.0,
+ required this.onRefresh,
+ this.color,
+ this.backgroundColor,
+ this.notificationPredicate = defaultScrollNotificationPredicate,
+ this.semanticsLabel,
+ this.semanticsValue,
+ this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
+ this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
+ }) : _indicatorType = _IndicatorType.adaptive;
/// The widget below this widget in the tree.
///
@@ -207,6 +240,8 @@
/// By default, the value of [strokeWidth] is 2.0 pixels.
final double strokeWidth;
+ final _IndicatorType _indicatorType;
+
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
///
/// The [RefreshIndicator] can be pulled out in two cases,
@@ -555,7 +590,7 @@
child: AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
- return RefreshProgressIndicator(
+ final Widget materialIndicator = RefreshProgressIndicator(
semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator ? null : _value.value,
@@ -563,6 +598,29 @@
backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth,
);
+
+ final Widget cupertinoIndicator = CupertinoActivityIndicator(
+ color: widget.color,
+ );
+
+ switch(widget._indicatorType) {
+ case _IndicatorType.material:
+ return materialIndicator;
+
+ case _IndicatorType.adaptive: {
+ final ThemeData theme = Theme.of(context);
+ switch (theme.platform) {
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ return materialIndicator;
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ return cupertinoIndicator;
+ }
+ }
+ }
},
),
),
diff --git a/framework/lib/src/material/scaffold.dart b/framework/lib/src/material/scaffold.dart
index a5a0885..06d794e 100644
--- a/framework/lib/src/material/scaffold.dart
+++ b/framework/lib/src/material/scaffold.dart
@@ -1158,9 +1158,9 @@
positionChild(_ScaffoldSlot.snackBar, Offset(xOffset, snackBarYOffsetBase - snackBarSize.height));
assert((){
- // Whether a floating SnackBar has been offsetted too high.
+ // Whether a floating SnackBar has been offset too high.
//
- // To improve the developper experience, this assert is done after the call to positionChild.
+ // To improve the developer experience, this assert is done after the call to positionChild.
// if we assert sooner the SnackBar is visible because its defaults position is (0,0) and
// it can cause confusion to the user as the error message states that the SnackBar is off screen.
if (isSnackBarFloating) {
diff --git a/framework/lib/src/material/scrollbar_theme.dart b/framework/lib/src/material/scrollbar_theme.dart
index 2257055..3575f5b 100644
--- a/framework/lib/src/material/scrollbar_theme.dart
+++ b/framework/lib/src/material/scrollbar_theme.dart
@@ -204,6 +204,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static ScrollbarThemeData lerp(ScrollbarThemeData? a, ScrollbarThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return ScrollbarThemeData(
thumbVisibility: MaterialStateProperty.lerp<bool?>(a?.thumbVisibility, b?.thumbVisibility, t, _lerpBool),
thickness: MaterialStateProperty.lerp<double?>(a?.thickness, b?.thickness, t, lerpDouble),
@@ -296,7 +299,7 @@
///
/// * [ScrollbarThemeData], which describes the configuration of a
/// scrollbar theme.
-class ScrollbarTheme extends InheritedWidget {
+class ScrollbarTheme extends InheritedTheme {
/// Constructs a scrollbar theme that configures all descendant [Scrollbar]
/// widgets.
const ScrollbarTheme({
@@ -322,5 +325,10 @@
}
@override
+ Widget wrap(BuildContext context, Widget child) {
+ return ScrollbarTheme(data: data, child: child);
+ }
+
+ @override
bool updateShouldNotify(ScrollbarTheme oldWidget) => data != oldWidget.data;
}
diff --git a/framework/lib/src/material/segmented_button.dart b/framework/lib/src/material/segmented_button.dart
index 23e9706..1234491 100644
--- a/framework/lib/src/material/segmented_button.dart
+++ b/framework/lib/src/material/segmented_button.dart
@@ -100,7 +100,7 @@
/// more than five options, consider using [FilterChip] or [ChoiceChip]
/// widgets.
///
- /// If [onSelectionChanged] is null, then the entire segemented button will
+ /// If [onSelectionChanged] is null, then the entire segmented button will
/// be disabled.
///
/// By default [selected] must only contain one entry. However, if
@@ -163,7 +163,7 @@
/// Determines if having no selected segments is allowed.
///
- /// If true, then it is acceptable for none of the segements to be selected.
+ /// If true, then it is acceptable for none of the segments to be selected.
/// This means that [selected] can be empty. If the user taps on a
/// selected segment, it will be removed from the selection set passed into
/// [onSelectionChanged].
@@ -183,7 +183,7 @@
/// dividers between segments.
/// * [ButtonStyle.shape]
///
- /// The following style properties are applied to each of the invidual
+ /// The following style properties are applied to each of the individual
/// button segments. For properties that are a [MaterialStateProperty],
/// they will be resolved with the current state of the segment:
///
diff --git a/framework/lib/src/material/segmented_button_theme.dart b/framework/lib/src/material/segmented_button_theme.dart
index 686455b..f8ec588 100644
--- a/framework/lib/src/material/segmented_button_theme.dart
+++ b/framework/lib/src/material/segmented_button_theme.dart
@@ -62,6 +62,9 @@
/// Linearly interpolates between two segmented button themes.
static SegmentedButtonThemeData lerp(SegmentedButtonThemeData? a, SegmentedButtonThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return SegmentedButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
selectedIcon: t < 0.5 ? a?.selectedIcon : b?.selectedIcon,
diff --git a/framework/lib/src/material/slider_theme.dart b/framework/lib/src/material/slider_theme.dart
index c01c918..7397b30 100644
--- a/framework/lib/src/material/slider_theme.dart
+++ b/framework/lib/src/material/slider_theme.dart
@@ -650,6 +650,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return SliderThemeData(
trackHeight: lerpDouble(a.trackHeight, b.trackHeight, t),
activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t),
diff --git a/framework/lib/src/material/snack_bar.dart b/framework/lib/src/material/snack_bar.dart
index 67cf2bd..5fcfc52 100644
--- a/framework/lib/src/material/snack_bar.dart
+++ b/framework/lib/src/material/snack_bar.dart
@@ -516,7 +516,7 @@
// the surrounding theme.
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
- // Invert the theme values for Material 2. Material 3 values are tokenzied to pre-inverted values.
+ // Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values.
final ThemeData effectiveTheme = theme.useMaterial3
? theme
: theme.copyWith(
diff --git a/framework/lib/src/material/snack_bar_theme.dart b/framework/lib/src/material/snack_bar_theme.dart
index 820e35a..b7724d6 100644
--- a/framework/lib/src/material/snack_bar_theme.dart
+++ b/framework/lib/src/material/snack_bar_theme.dart
@@ -178,6 +178,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static SnackBarThemeData lerp(SnackBarThemeData? a, SnackBarThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return SnackBarThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
actionTextColor: Color.lerp(a?.actionTextColor, b?.actionTextColor, t),
diff --git a/framework/lib/src/material/switch_theme.dart b/framework/lib/src/material/switch_theme.dart
index 4d204fe..8566d28 100644
--- a/framework/lib/src/material/switch_theme.dart
+++ b/framework/lib/src/material/switch_theme.dart
@@ -116,6 +116,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static SwitchThemeData lerp(SwitchThemeData? a, SwitchThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return SwitchThemeData(
thumbColor: MaterialStateProperty.lerp<Color?>(a?.thumbColor, b?.thumbColor, t, Color.lerp),
trackColor: MaterialStateProperty.lerp<Color?>(a?.trackColor, b?.trackColor, t, Color.lerp),
diff --git a/framework/lib/src/material/tab_bar_theme.dart b/framework/lib/src/material/tab_bar_theme.dart
index 3c011d1..ebd0eb4 100644
--- a/framework/lib/src/material/tab_bar_theme.dart
+++ b/framework/lib/src/material/tab_bar_theme.dart
@@ -133,6 +133,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static TabBarTheme lerp(TabBarTheme a, TabBarTheme b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return TabBarTheme(
indicator: Decoration.lerp(a.indicator, b.indicator, t),
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
diff --git a/framework/lib/src/material/tab_controller.dart b/framework/lib/src/material/tab_controller.dart
index 390b5cb..a323769 100644
--- a/framework/lib/src/material/tab_controller.dart
+++ b/framework/lib/src/material/tab_controller.dart
@@ -101,16 +101,20 @@
///
/// The `initialIndex` must be valid given [length] and must not be null. If
/// [length] is zero, then `initialIndex` must be 0 (the default).
- TabController({ int initialIndex = 0, Duration? animationDuration, required this.length, required TickerProvider vsync})
- : assert(length >= 0),
- assert(initialIndex >= 0 && (length == 0 || initialIndex < length)),
- _index = initialIndex,
- _previousIndex = initialIndex,
- _animationDuration = animationDuration ?? kTabScrollDuration,
- _animationController = AnimationController.unbounded(
- value: initialIndex.toDouble(),
- vsync: vsync,
- );
+ TabController({
+ int initialIndex = 0,
+ Duration? animationDuration,
+ required this.length,
+ required TickerProvider vsync,
+ }) : assert(length >= 0),
+ assert(initialIndex >= 0 && (length == 0 || initialIndex < length)),
+ _index = initialIndex,
+ _previousIndex = initialIndex,
+ _animationDuration = animationDuration ?? kTabScrollDuration,
+ _animationController = AnimationController.unbounded(
+ value: initialIndex.toDouble(),
+ vsync: vsync,
+ );
// Private constructor used by `_copyWith`. This allows a new TabController to
// be created without having to create a new animationController.
diff --git a/framework/lib/src/material/tabs.dart b/framework/lib/src/material/tabs.dart
index ebac40f..9175aaa 100644
--- a/framework/lib/src/material/tabs.dart
+++ b/framework/lib/src/material/tabs.dart
@@ -442,8 +442,8 @@
if (!(rect.size >= insets.collapsedSize)) {
throw FlutterError(
- 'indicatorPadding insets should be less than Tab Size\n'
- 'Rect Size : ${rect.size}, Insets: $insets',
+ 'indicatorPadding insets should be less than Tab Size\n'
+ 'Rect Size : ${rect.size}, Insets: $insets',
);
}
return insets.deflateRect(rect);
@@ -562,7 +562,7 @@
bool _viewportDimensionWasNonZero = false;
- // Position should be adjusted at least once.
+ // The scroll position should be adjusted at least once.
bool _needsPixelsCorrection = true;
@override
@@ -573,11 +573,10 @@
}
// If the viewport never had a non-zero dimension, we just want to jump
// to the initial scroll position to avoid strange scrolling effects in
- // release mode: In release mode, the viewport temporarily may have a
- // dimension of zero before the actual dimension is calculated. In that
- // scenario, setting the actual dimension would cause a strange scroll
- // effect without this guard because the super call below would starts a
- // ballistic scroll activity.
+ // release mode: the viewport temporarily may have a dimension of zero
+ // before the actual dimension is calculated. In that scenario, setting
+ // the actual dimension would cause a strange scroll effect without this
+ // guard because the super call below would start a ballistic scroll activity.
if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
_needsPixelsCorrection = false;
correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent));
@@ -643,7 +642,7 @@
/// See also:
///
/// * [TabBarView], which displays page views that correspond to each tab.
-/// * [TabBar], which is used to display the [Tab] that corresponds to each page of the [TabBarView].
+/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a Material Design tab bar.
///
@@ -658,7 +657,7 @@
/// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
///
/// If [indicator] is not null or provided from [TabBarTheme],
- /// then [indicatorWeight], [indicatorPadding], and [indicatorColor] are ignored.
+ /// then [indicatorWeight] and [indicatorColor] are ignored.
const TabBar({
super.key,
required this.tabs,
@@ -708,8 +707,8 @@
/// The amount of space by which to inset the tab bar.
///
- /// When [isScrollable] is false, this will yield the same result as if you had wrapped your
- /// [TabBar] in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset,
+ /// When [isScrollable] is false, this will yield the same result as if [TabBar] was wrapped
+ /// in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset,
/// allowing the padding to scroll with the tab bar, rather than enclosing it.
final EdgeInsetsGeometry? padding;
@@ -731,22 +730,19 @@
/// this property is ignored.
final double indicatorWeight;
-
- /// Padding for indicator.
- /// This property will now no longer be ignored even if indicator is declared
- /// or provided by [TabBarTheme]
+ /// The padding for the indicator.
+ ///
+ /// The default value of this property is [EdgeInsets.zero].
///
/// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
/// the indicator with the tab's text for [Tab] widgets and all but the
/// shortest [Tab.text] values.
- ///
- /// The default value of [indicatorPadding] is [EdgeInsets.zero].
final EdgeInsetsGeometry indicatorPadding;
/// Defines the appearance of the selected tab indicator.
///
/// If [indicator] is specified or provided from [TabBarTheme],
- /// the [indicatorColor], and [indicatorWeight] properties are ignored.
+ /// the [indicatorColor] and [indicatorWeight] properties are ignored.
///
/// The default, underline-style, selected tab indicator can be defined with
/// [UnderlineTabIndicator].
@@ -765,6 +761,8 @@
/// Whether this tab bar should automatically adjust the [indicatorColor].
///
+ /// The default value of this property is true.
+ ///
/// If [automaticIndicatorColorAdjustment] is true,
/// then the [indicatorColor] will be automatically adjusted to [Colors.white]
/// when the [indicatorColor] is same as [Material.color] of the [Material]
@@ -802,8 +800,8 @@
/// [MaterialState.selected] state, i.e. if the [Tab] is selected or not,
/// ignoring [unselectedLabelColor] even if it's non-null.
///
- /// Note: [labelStyle]'s color and [TabBarTheme.labelStyle]'s color do not
- /// affect the effective [labelColor].
+ /// The color specified in the [labelStyle] and the [TabBarTheme.labelStyle]
+ /// do not affect the effective [labelColor].
///
/// See also:
///
@@ -822,9 +820,9 @@
/// will be used, otherwise unselected tab labels are rendered with
/// [labelColor] at 70% opacity.
///
- /// Note: [unselectedLabelStyle]'s color and
- /// [TabBarTheme.unselectedLabelStyle]'s color are ignored in
- /// [unselectedLabelColor]'s precedence calculation.
+ /// The color specified in the [unselectedLabelStyle] and the
+ /// [TabBarTheme.unselectedLabelStyle] are ignored in [unselectedLabelColor]'s
+ /// precedence calculation.
///
/// See also:
///
@@ -1122,7 +1120,7 @@
indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys,
old: _indicatorPainter,
- dividerColor: theme.useMaterial3 ? widget.dividerColor ?? defaults.dividerColor : null,
+ dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? defaults.dividerColor : null,
labelPaddings: _labelPaddings,
);
}
diff --git a/framework/lib/src/material/text_button.dart b/framework/lib/src/material/text_button.dart
index cd9966a..0687a5a 100644
--- a/framework/lib/src/material/text_button.dart
+++ b/framework/lib/src/material/text_button.dart
@@ -270,7 +270,7 @@
/// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0
/// * `padding`
- /// * `textScaleFactor <= 1` - all(8)
+ /// * `textScaleFactor <= 1` - (horizontal(12), vertical(8))
/// * `1 < textScaleFactor <= 2` - lerp(all(8), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
@@ -320,7 +320,7 @@
/// * `surfaceTintColor` - null
/// * `elevation` - 0
/// * `padding`
- /// * `textScaleFactor <= 1` - all(8)
+ /// * `textScaleFactor <= 1` - lerp(horizontal(12), horizontal(4))
/// * `1 < textScaleFactor <= 2` - lerp(all(8), horizontal(8))
/// * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
/// * `3 < textScaleFactor` - horizontal(4)
@@ -338,6 +338,9 @@
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
+ ///
+ /// For the [TextButton.icon] factory, the end (generally the right) value of
+ /// [padding] is increased from 12 to 16.
/// {@endtemplate}
@override
ButtonStyle defaultStyleOf(BuildContext context) {
@@ -378,8 +381,9 @@
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
return ButtonStyleButton.scaledPadding(
- const EdgeInsets.all(8),
+ useMaterial3 ? const EdgeInsets.symmetric(horizontal: 12, vertical: 8) : const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.textScaleFactorOf(context),
@@ -489,8 +493,9 @@
@override
ButtonStyle defaultStyleOf(BuildContext context) {
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
- const EdgeInsets.all(8),
+ useMaterial3 ? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8) : const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 4),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.textScaleFactorOf(context),
diff --git a/framework/lib/src/material/text_button_theme.dart b/framework/lib/src/material/text_button_theme.dart
index 130817e..f834e4c 100644
--- a/framework/lib/src/material/text_button_theme.dart
+++ b/framework/lib/src/material/text_button_theme.dart
@@ -49,8 +49,8 @@
/// Linearly interpolate between two text button themes.
static TextButtonThemeData? lerp(TextButtonThemeData? a, TextButtonThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return TextButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/text_field.dart b/framework/lib/src/material/text_field.dart
index eeffe03..505d6d8 100644
--- a/framework/lib/src/material/text_field.dart
+++ b/framework/lib/src/material/text_field.dart
@@ -1125,8 +1125,7 @@
case TargetPlatform.windows:
case TargetPlatform.fuchsia:
case TargetPlatform.android:
- if (cause == SelectionChangedCause.longPress
- || cause == SelectionChangedCause.drag) {
+ if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.extent);
}
break;
diff --git a/framework/lib/src/material/text_selection_theme.dart b/framework/lib/src/material/text_selection_theme.dart
index 592019b..9f586d4 100644
--- a/framework/lib/src/material/text_selection_theme.dart
+++ b/framework/lib/src/material/text_selection_theme.dart
@@ -73,8 +73,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static TextSelectionThemeData? lerp(TextSelectionThemeData? a, TextSelectionThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return TextSelectionThemeData(
cursorColor: Color.lerp(a?.cursorColor, b?.cursorColor, t),
diff --git a/framework/lib/src/material/text_theme.dart b/framework/lib/src/material/text_theme.dart
index 42aefb8..6ede902 100644
--- a/framework/lib/src/material/text_theme.dart
+++ b/framework/lib/src/material/text_theme.dart
@@ -82,9 +82,9 @@
/// If you do decide to create your own text theme, consider using one of
/// those predefined themes as a starting point for [copyWith] or [apply].
///
- /// Please note that you can not mix and match the 2018 styles with the 2021
- /// styles. Only one or the other is allowed in this constructor. The 2018
- /// styles will be deprecated and removed eventually.
+ /// The 2018 styles cannot be mixed with the 2021 styles. Only one or the
+ /// other is allowed in this constructor. The 2018 styles are deprecated and
+ /// will eventually be removed.
const TextTheme({
TextStyle? displayLarge,
TextStyle? displayMedium,
@@ -799,6 +799,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static TextTheme lerp(TextTheme? a, TextTheme? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return TextTheme(
displayLarge: TextStyle.lerp(a?.displayLarge, b?.displayLarge, t),
displayMedium: TextStyle.lerp(a?.displayMedium, b?.displayMedium, t),
diff --git a/framework/lib/src/material/theme_data.dart b/framework/lib/src/material/theme_data.dart
index 1f85ff4..d469d08 100644
--- a/framework/lib/src/material/theme_data.dart
+++ b/framework/lib/src/material/theme_data.dart
@@ -7,6 +7,8 @@
import 'package:flute/cupertino.dart';
import 'package:flute/foundation.dart';
+import 'action_buttons.dart';
+import 'action_icons_theme.dart';
import 'app_bar_theme.dart';
import 'badge_theme.dart';
import 'banner_theme.dart';
@@ -337,6 +339,7 @@
TextTheme? textTheme,
Typography? typography,
// COMPONENT THEMES
+ ActionIconThemeData? actionIconTheme,
AppBarTheme? appBarTheme,
BadgeThemeData? badgeTheme,
MaterialBannerThemeData? bannerTheme,
@@ -382,13 +385,6 @@
TooltipThemeData? tooltipTheme,
// DEPRECATED (newest deprecations at the bottom)
@Deprecated(
- 'Use colorScheme.secondary instead. '
- 'For more information, consult the migration guide at '
- 'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
- 'This feature was deprecated after v2.3.0-0.1.pre.',
- )
- Color? accentColor,
- @Deprecated(
'This "fix" is now enabled by default. '
'This feature was deprecated after v2.5.0-1.0.pre.',
)
@@ -478,7 +474,6 @@
primaryColor ??= primarySurfaceColor;
primaryColorBrightness = ThemeData.estimateBrightnessForColor(primarySurfaceColor);
canvasColor ??= colorScheme.background;
- accentColor ??= colorScheme.secondary;
scaffoldBackgroundColor ??= colorScheme.background;
bottomAppBarColor ??= colorScheme.surface;
cardColor ??= colorScheme.surface;
@@ -496,8 +491,7 @@
primaryColorLight ??= isDark ? Colors.grey[500]! : primarySwatch[100]!;
primaryColorDark ??= isDark ? Colors.black : primarySwatch[700]!;
final bool primaryIsDark = estimatedPrimaryColorBrightness == Brightness.dark;
- toggleableActiveColor ??= isDark ? Colors.tealAccent[200]! : (accentColor ?? primarySwatch[600]!);
- accentColor ??= isDark ? Colors.tealAccent[200]! : primarySwatch[500]!;
+ toggleableActiveColor ??= isDark ? Colors.tealAccent[200]! : (colorScheme?.secondary ?? primarySwatch[600]!);
focusColor ??= isDark ? Colors.white.withOpacity(0.12) : Colors.black.withOpacity(0.12);
hoverColor ??= isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.04);
shadowColor ??= Colors.black;
@@ -510,7 +504,7 @@
colorScheme ??= ColorScheme.fromSwatch(
primarySwatch: primarySwatch,
primaryColorDark: primaryColorDark,
- accentColor: accentColor,
+ accentColor: isDark ? Colors.tealAccent[200]! : primarySwatch[500]!,
cardColor: cardColor,
backgroundColor: isDark ? Colors.grey[700]! : primarySwatch[200]!,
errorColor: Colors.red[700],
@@ -521,7 +515,7 @@
// Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess.
secondaryHeaderColor ??= isDark ? Colors.grey[700]! : primarySwatch[50]!;
dialogBackgroundColor ??= isDark ? Colors.grey[800]! : Colors.white;
- indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
+ indicatorColor ??= colorScheme.secondary == primaryColor ? Colors.white : colorScheme.secondary;
hintColor ??= isDark ? Colors.white60 : Colors.black.withOpacity(0.6);
// The default [buttonTheme] is here because it doesn't use the defaults for
// [disabledColor], [highlightColor], and [splashColor].
@@ -658,6 +652,7 @@
typography: typography,
primaryIconTheme: primaryIconTheme,
// COMPONENT THEMES
+ actionIconTheme: actionIconTheme,
appBarTheme: appBarTheme,
badgeTheme: badgeTheme,
bannerTheme: bannerTheme,
@@ -702,7 +697,6 @@
toggleButtonsTheme: toggleButtonsTheme,
tooltipTheme: tooltipTheme,
// DEPRECATED (newest deprecations at the bottom)
- accentColor: accentColor,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
primaryColorBrightness: primaryColorBrightness,
androidOverscrollIndicator: androidOverscrollIndicator,
@@ -769,6 +763,7 @@
required this.textTheme,
required this.typography,
// COMPONENT THEMES
+ required this.actionIconTheme,
required this.appBarTheme,
required this.badgeTheme,
required this.bannerTheme,
@@ -814,13 +809,6 @@
required this.tooltipTheme,
// DEPRECATED (newest deprecations at the bottom)
@Deprecated(
- 'Use colorScheme.secondary instead. '
- 'For more information, consult the migration guide at '
- 'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
- 'This feature was deprecated after v2.3.0-0.1.pre.',
- )
- Color? accentColor,
- @Deprecated(
'This "fix" is now enabled by default. '
'This feature was deprecated after v2.5.0-1.0.pre.',
)
@@ -865,7 +853,6 @@
}) : // DEPRECATED (newest deprecations at the bottom)
// should not be `required`, use getter pattern to avoid breakages.
- _accentColor = accentColor,
_fixTextFieldOutlineLabel = fixTextFieldOutlineLabel,
_primaryColorBrightness = primaryColorBrightness,
_toggleableActiveColor = toggleableActiveColor,
@@ -875,7 +862,6 @@
_bottomAppBarColor = bottomAppBarColor,
assert(toggleableActiveColor != null),
// DEPRECATED (newest deprecations at the bottom)
- assert(accentColor != null),
assert(fixTextFieldOutlineLabel != null),
assert(primaryColorBrightness != null),
assert(errorColor != null),
@@ -926,7 +912,6 @@
primaryColor: primarySurfaceColor,
primaryColorBrightness: ThemeData.estimateBrightnessForColor(primarySurfaceColor),
canvasColor: colorScheme.background,
- accentColor: colorScheme.secondary,
scaffoldBackgroundColor: colorScheme.background,
bottomAppBarColor: colorScheme.surface,
cardColor: colorScheme.surface,
@@ -1138,10 +1123,11 @@
/// will aim to only support Material 3.
///
/// ## Defaults
- /// If a [ThemeData] is constructed with [useMaterial3] set to true, then
- /// some properties will get updated defaults. Please note that
- /// [ThemeData.copyWith] with [useMaterial3] set to true will
- /// not change any of these properties in the resulting [ThemeData].
+ ///
+ /// If a [ThemeData] is _constructed_ with [useMaterial3] set to true, then
+ /// some properties will get updated defaults. However, the
+ /// [ThemeData.copyWith] method with [useMaterial3] set to true will _not_
+ /// change any of these properties in the resulting [ThemeData].
///
/// <style>table,td,th { border-collapse: collapse; padding: 0.45em; } td { border: 1px solid }</style>
///
@@ -1375,6 +1361,10 @@
// COMPONENT THEMES
+ /// A theme for customizing icons of [BackButtonIcon], [CloseButtonIcon],
+ /// [DrawerButtonIcon], or [EndDrawerButtonIcon].
+ final ActionIconThemeData? actionIconTheme;
+
/// A theme for customizing the color, elevation, brightness, iconTheme and
/// textTheme of [AppBar]s.
final AppBarTheme appBarTheme;
@@ -1533,25 +1523,6 @@
// DEPRECATED (newest deprecations at the bottom)
- /// Obsolete property that was originally used as the foreground
- /// color for widgets (knobs, text, overscroll edge effect, etc).
- ///
- /// The material library no longer uses this property. In most cases the
- /// [colorScheme]'s [ColorScheme.secondary] property is now used instead.
- ///
- /// Apps should migrate uses of this property to the theme's [colorScheme]
- /// [ColorScheme.secondary] color. In cases where a color is needed that
- /// contrasts well with the secondary color [ColorScheme.onSecondary]
- /// can be used.
- @Deprecated(
- 'Use colorScheme.secondary instead. '
- 'For more information, consult the migration guide at '
- 'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
- 'This feature was deprecated after v2.3.0-0.1.pre.',
- )
- Color get accentColor => _accentColor!;
- final Color? _accentColor;
-
/// An obsolete flag to allow apps to opt-out of a
/// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
/// coordinate of the floating label in a [TextField] [OutlineInputBorder].
@@ -1692,6 +1663,7 @@
TextTheme? textTheme,
Typography? typography,
// COMPONENT THEMES
+ ActionIconThemeData? actionIconTheme,
AppBarTheme? appBarTheme,
BadgeThemeData? badgeTheme,
MaterialBannerThemeData? bannerTheme,
@@ -1737,13 +1709,6 @@
TooltipThemeData? tooltipTheme,
// DEPRECATED (newest deprecations at the bottom)
@Deprecated(
- 'No longer used by the framework, please remove any reference to it. '
- 'For more information, consult the migration guide at '
- 'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
- 'This feature was deprecated after v2.3.0-0.1.pre.',
- )
- Color? accentColor,
- @Deprecated(
'This "fix" is now enabled by default. '
'This feature was deprecated after v2.5.0-1.0.pre.',
)
@@ -1832,6 +1797,7 @@
textTheme: textTheme ?? this.textTheme,
typography: typography ?? this.typography,
// COMPONENT THEMES
+ actionIconTheme: actionIconTheme ?? this.actionIconTheme,
appBarTheme: appBarTheme ?? this.appBarTheme,
badgeTheme: badgeTheme ?? this.badgeTheme,
bannerTheme: bannerTheme ?? this.bannerTheme,
@@ -1876,7 +1842,6 @@
toggleButtonsTheme: toggleButtonsTheme ?? this.toggleButtonsTheme,
tooltipTheme: tooltipTheme ?? this.tooltipTheme,
// DEPRECATED (newest deprecations at the bottom)
- accentColor: accentColor ?? _accentColor,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? _fixTextFieldOutlineLabel,
primaryColorBrightness: primaryColorBrightness ?? _primaryColorBrightness,
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
@@ -1978,6 +1943,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static ThemeData lerp(ThemeData a, ThemeData b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return ThemeData.raw(
// For the sanity of the reader, make sure these properties are in the same
// order in every place that they are separated by section comments (e.g.
@@ -2023,6 +1991,7 @@
textTheme: TextTheme.lerp(a.textTheme, b.textTheme, t),
typography: Typography.lerp(a.typography, b.typography, t),
// COMPONENT THEMES
+ actionIconTheme: ActionIconThemeData.lerp(a.actionIconTheme, b.actionIconTheme, t),
appBarTheme: AppBarTheme.lerp(a.appBarTheme, b.appBarTheme, t),
badgeTheme: BadgeThemeData.lerp(a.badgeTheme, b.badgeTheme, t),
bannerTheme: MaterialBannerThemeData.lerp(a.bannerTheme, b.bannerTheme, t),
@@ -2067,7 +2036,6 @@
toggleButtonsTheme: ToggleButtonsThemeData.lerp(a.toggleButtonsTheme, b.toggleButtonsTheme, t)!,
tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t)!,
// DEPRECATED (newest deprecations at the bottom)
- accentColor: Color.lerp(a.accentColor, b.accentColor, t),
fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel,
primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness,
androidOverscrollIndicator:t < 0.5 ? a.androidOverscrollIndicator : b.androidOverscrollIndicator,
@@ -2129,6 +2097,7 @@
other.textTheme == textTheme &&
other.typography == typography &&
// COMPONENT THEMES
+ other.actionIconTheme == actionIconTheme &&
other.appBarTheme == appBarTheme &&
other.badgeTheme == badgeTheme &&
other.bannerTheme == bannerTheme &&
@@ -2173,7 +2142,6 @@
other.toggleButtonsTheme == toggleButtonsTheme &&
other.tooltipTheme == tooltipTheme &&
// DEPRECATED (newest deprecations at the bottom)
- other.accentColor == accentColor &&
other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel &&
other.primaryColorBrightness == primaryColorBrightness &&
other.androidOverscrollIndicator == androidOverscrollIndicator &&
@@ -2232,6 +2200,7 @@
textTheme,
typography,
// COMPONENT THEMES
+ actionIconTheme,
appBarTheme,
badgeTheme,
bannerTheme,
@@ -2276,7 +2245,6 @@
toggleButtonsTheme,
tooltipTheme,
// DEPRECATED (newest deprecations at the bottom)
- accentColor,
fixTextFieldOutlineLabel,
primaryColorBrightness,
androidOverscrollIndicator,
@@ -2337,6 +2305,7 @@
properties.add(DiagnosticsProperty<TextTheme>('textTheme', textTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug));
// COMPONENT THEMES
+ properties.add(DiagnosticsProperty<ActionIconThemeData>('actionIconTheme', actionIconTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<AppBarTheme>('appBarTheme', appBarTheme, defaultValue: defaultData.appBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<BadgeThemeData>('badgeTheme', badgeTheme, defaultValue: defaultData.badgeTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme, level: DiagnosticLevel.debug));
@@ -2381,7 +2350,6 @@
properties.add(DiagnosticsProperty<ToggleButtonsThemeData>('toggleButtonsTheme', toggleButtonsTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<TooltipThemeData>('tooltipTheme', tooltipTheme, level: DiagnosticLevel.debug));
// DEPRECATED (newest deprecations at the bottom)
- properties.add(ColorProperty('accentColor', accentColor, defaultValue: defaultData.accentColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<bool>('fixTextFieldOutlineLabel', fixTextFieldOutlineLabel, level: DiagnosticLevel.debug));
properties.add(EnumProperty<Brightness>('primaryColorBrightness', primaryColorBrightness, defaultValue: defaultData.primaryColorBrightness, level: DiagnosticLevel.debug));
properties.add(EnumProperty<AndroidOverscrollIndicator>('androidOverscrollIndicator', androidOverscrollIndicator, defaultValue: null, level: DiagnosticLevel.debug));
@@ -2732,6 +2700,9 @@
/// Linearly interpolate between two densities.
static VisualDensity lerp(VisualDensity a, VisualDensity b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return VisualDensity(
horizontal: lerpDouble(a.horizontal, b.horizontal, t)!,
vertical: lerpDouble(a.vertical, b.vertical, t)!,
diff --git a/framework/lib/src/material/time_picker_theme.dart b/framework/lib/src/material/time_picker_theme.dart
index e2312b8..d35caa2 100644
--- a/framework/lib/src/material/time_picker_theme.dart
+++ b/framework/lib/src/material/time_picker_theme.dart
@@ -299,7 +299,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static TimePickerThemeData lerp(TimePickerThemeData? a, TimePickerThemeData? b, double t) {
-
+ if (identical(a, b) && a != null) {
+ return a;
+ }
// Workaround since BorderSide's lerp does not allow for null arguments.
BorderSide? lerpedBorderSide;
if (a?.dayPeriodBorderSide == null && b?.dayPeriodBorderSide == null) {
diff --git a/framework/lib/src/material/toggle_buttons_theme.dart b/framework/lib/src/material/toggle_buttons_theme.dart
index d4c9bd5..6cfa191 100644
--- a/framework/lib/src/material/toggle_buttons_theme.dart
+++ b/framework/lib/src/material/toggle_buttons_theme.dart
@@ -150,8 +150,8 @@
/// Linearly interpolate between two toggle buttons themes.
static ToggleButtonsThemeData? lerp(ToggleButtonsThemeData? a, ToggleButtonsThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return ToggleButtonsThemeData(
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
diff --git a/framework/lib/src/material/tooltip_theme.dart b/framework/lib/src/material/tooltip_theme.dart
index 53cde10..f917962 100644
--- a/framework/lib/src/material/tooltip_theme.dart
+++ b/framework/lib/src/material/tooltip_theme.dart
@@ -149,8 +149,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static TooltipThemeData? lerp(TooltipThemeData? a, TooltipThemeData? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
return TooltipThemeData(
height: lerpDouble(a?.height, b?.height, t),
diff --git a/framework/lib/src/material/typography.dart b/framework/lib/src/material/typography.dart
index 0a08ffc..093311b 100644
--- a/framework/lib/src/material/typography.dart
+++ b/framework/lib/src/material/typography.dart
@@ -329,6 +329,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static Typography lerp(Typography a, Typography b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
return Typography._(
TextTheme.lerp(a.black, b.black, t),
TextTheme.lerp(a.white, b.white, t),
diff --git a/framework/lib/src/painting/alignment.dart b/framework/lib/src/painting/alignment.dart
index 4149485..eff4d75 100644
--- a/framework/lib/src/painting/alignment.dart
+++ b/framework/lib/src/painting/alignment.dart
@@ -87,8 +87,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static AlignmentGeometry? lerp(AlignmentGeometry? a, AlignmentGeometry? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
@@ -337,8 +337,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static Alignment? lerp(Alignment? a, Alignment? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return Alignment(ui.lerpDouble(0.0, b!.x, t)!, ui.lerpDouble(0.0, b.y, t)!);
@@ -528,8 +528,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static AlignmentDirectional? lerp(AlignmentDirectional? a, AlignmentDirectional? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return AlignmentDirectional(ui.lerpDouble(0.0, b!.start, t)!, ui.lerpDouble(0.0, b.y, t)!);
diff --git a/framework/lib/src/painting/basic_types.dart b/framework/lib/src/painting/basic_types.dart
index 6675055..b2c1fae 100644
--- a/framework/lib/src/painting/basic_types.dart
+++ b/framework/lib/src/painting/basic_types.dart
@@ -48,93 +48,18 @@
export 'package:flute/foundation.dart' show VoidCallback;
-// Intentionally not exported:
-// - Image, instantiateImageCodec, decodeImageFromList:
-// We use ui.* to make it very explicit that these are low-level image APIs.
-// Generally, higher layers provide more reasonable APIs around images.
-// - lerpDouble:
-// Hopefully this will eventually become Double.lerp.
-// - Paragraph, ParagraphBuilder, ParagraphStyle, TextBox:
-// These are low-level text primitives. Use this package's TextPainter API.
-// - Picture, PictureRecorder, Scene, SceneBuilder:
-// These are low-level primitives. Generally, the rendering layer makes these moot.
-// - Gradient:
-// Use this package's higher-level Gradient API instead.
-// - window, WindowPadding
-// These are generally wrapped by other APIs so we always refer to them directly
-// as ui.* to avoid making them seem like high-level APIs.
-
-/// The description of the difference between two objects, in the context of how
-/// it will affect the rendering.
-///
-/// Used by [TextSpan.compareTo] and [TextStyle.compareTo].
-///
-/// The values in this enum are ordered such that they are in increasing order
-/// of cost. A value with index N implies all the values with index less than N.
-/// For example, [layout] (index 3) implies [paint] (2).
enum RenderComparison {
- /// The two objects are identical (meaning deeply equal, not necessarily
- /// [dart:core.identical]).
identical,
-
- /// The two objects are identical for the purpose of layout, but may be different
- /// in other ways.
- ///
- /// For example, maybe some event handlers changed.
metadata,
-
- /// The two objects are different but only in ways that affect paint, not layout.
- ///
- /// For example, only the color is changed.
- ///
- /// [RenderObject.markNeedsPaint] would be necessary to handle this kind of
- /// change in a render object.
paint,
-
- /// The two objects are different in ways that affect layout (and therefore paint).
- ///
- /// For example, the size is changed.
- ///
- /// This is the most drastic level of change possible.
- ///
- /// [RenderObject.markNeedsLayout] would be necessary to handle this kind of
- /// change in a render object.
layout,
}
-/// The two cardinal directions in two dimensions.
-///
-/// The axis is always relative to the current coordinate space. This means, for
-/// example, that a [horizontal] axis might actually be diagonally from top
-/// right to bottom left, due to some local [Transform] applied to the scene.
-///
-/// See also:
-///
-/// * [AxisDirection], which is a directional version of this enum (with values
-/// light left and right, rather than just horizontal).
-/// * [TextDirection], which disambiguates between left-to-right horizontal
-/// content and right-to-left horizontal content.
enum Axis {
- /// Left and right.
- ///
- /// See also:
- ///
- /// * [TextDirection], which disambiguates between left-to-right horizontal
- /// content and right-to-left horizontal content.
horizontal,
-
- /// Up and down.
vertical,
}
-/// Returns the opposite of the given [Axis].
-///
-/// Specifically, returns [Axis.horizontal] for [Axis.vertical], and
-/// vice versa.
-///
-/// See also:
-///
-/// * [flipAxisDirection], which does the same thing for [AxisDirection] values.
Axis flipAxis(Axis direction) {
switch (direction) {
case Axis.horizontal:
diff --git a/framework/lib/src/painting/border_radius.dart b/framework/lib/src/painting/border_radius.dart
index b1bdfaf..416423a 100644
--- a/framework/lib/src/painting/border_radius.dart
+++ b/framework/lib/src/painting/border_radius.dart
@@ -129,8 +129,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderRadiusGeometry? lerp(BorderRadiusGeometry? a, BorderRadiusGeometry? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
a ??= BorderRadius.zero;
b ??= BorderRadius.zero;
@@ -506,8 +506,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderRadius? lerp(BorderRadius? a, BorderRadius? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
@@ -727,8 +727,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderRadiusDirectional? lerp(BorderRadiusDirectional? a, BorderRadiusDirectional? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
diff --git a/framework/lib/src/painting/borders.dart b/framework/lib/src/painting/borders.dart
index 4316028..08f934b 100644
--- a/framework/lib/src/painting/borders.dart
+++ b/framework/lib/src/painting/borders.dart
@@ -26,8 +26,8 @@
/// A [Border] consists of four [BorderSide] objects: [Border.top],
/// [Border.left], [Border.right], and [Border.bottom].
///
-/// Note that setting [BorderSide.width] to 0.0 will result in hairline
-/// rendering. A more involved explanation is present in [BorderSide.width].
+/// Setting [BorderSide.width] to 0.0 will result in hairline rendering; see
+/// [BorderSide.width] for a more involved explanation.
///
/// {@tool snippet}
/// This sample shows how [BorderSide] objects can be used in a [Container], via
@@ -108,7 +108,7 @@
/// The width of this side of the border, in logical pixels.
///
/// Setting width to 0.0 will result in a hairline border. This means that
- /// the border will have the width of one physical pixel. Also, hairline
+ /// the border will have the width of one physical pixel. Hairline
/// rendering takes shortcuts when the path overlaps a pixel more than once.
/// This means that it will render faster than otherwise, but it might
/// double-hit pixels, giving it a slightly darker/lighter result.
@@ -143,6 +143,11 @@
/// - [strokeAlignCenter] provides padding with half [width].
/// - [strokeAlignOutside] provides zero padding, as stroke is drawn entirely outside.
///
+ /// This property is not honored by [toPaint] (because the [Paint] object
+ /// cannot represent it); it is intended that classes that use [BorderSide]
+ /// objects implement this property when painting borders by suitably
+ /// inflating or deflating their regions.
+ ///
/// {@tool dartpad}
/// This example shows an animation of how [strokeAlign] affects the drawing
/// when applied to borders of various shapes.
@@ -153,15 +158,21 @@
/// The border is drawn fully inside of the border path.
///
- /// This is the default.
+ /// This is a constant for use with [strokeAlign].
+ ///
+ /// This is the default value for [strokeAlign].
static const double strokeAlignInside = -1.0;
/// The border is drawn on the center of the border path, with half of the
/// [BorderSide.width] on the inside, and the other half on the outside of
/// the path.
+ ///
+ /// This is a constant for use with [strokeAlign].
static const double strokeAlignCenter = 0.0;
/// The border is drawn on the outside of the border path.
+ ///
+ /// This is a constant for use with [strokeAlign].
static const double strokeAlignOutside = 1.0;
/// Creates a copy of this border but with the given fields replaced with the new values.
@@ -206,6 +217,9 @@
/// Create a [Paint] object that, if used to stroke a line, will draw the line
/// in this border's style.
///
+ /// The [strokeAlign] property is not reflected in the [Paint]; consumers must
+ /// implement that directly by inflating or deflating their region appropriately.
+ ///
/// Not all borders use this method to paint their border sides. For example,
/// non-uniform rectangular [Border]s have beveled edges and so paint their
/// border sides as filled shapes rather than using a stroke.
@@ -246,6 +260,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderSide lerp(BorderSide a, BorderSide b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
if (t == 0.0) {
return a;
}
@@ -505,6 +522,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static ShapeBorder? lerp(ShapeBorder? a, ShapeBorder? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
ShapeBorder? result;
if (b != null) {
result = b.lerpFrom(a, t);
@@ -694,6 +714,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static OutlinedBorder? lerp(OutlinedBorder? a, OutlinedBorder? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
ShapeBorder? result;
if (b != null) {
result = b.lerpFrom(a, t);
diff --git a/framework/lib/src/painting/box_border.dart b/framework/lib/src/painting/box_border.dart
index e5a4ffc..72ec572 100644
--- a/framework/lib/src/painting/box_border.dart
+++ b/framework/lib/src/painting/box_border.dart
@@ -103,6 +103,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static BoxBorder? lerp(BoxBorder? a, BoxBorder? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
if ((a is Border?) && (b is Border?)) {
return Border.lerp(a, b, t);
}
@@ -469,8 +472,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static Border? lerp(Border? a, Border? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
@@ -811,8 +814,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderDirectional? lerp(BorderDirectional? a, BorderDirectional? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
diff --git a/framework/lib/src/painting/box_decoration.dart b/framework/lib/src/painting/box_decoration.dart
index a81eab4..f469060 100644
--- a/framework/lib/src/painting/box_decoration.dart
+++ b/framework/lib/src/painting/box_decoration.dart
@@ -288,8 +288,8 @@
/// and which use [BoxDecoration.lerp] when interpolating two
/// [BoxDecoration]s or a [BoxDecoration] to or from null.
static BoxDecoration? lerp(BoxDecoration? a, BoxDecoration? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
diff --git a/framework/lib/src/painting/box_shadow.dart b/framework/lib/src/painting/box_shadow.dart
index f050709..4b474e8 100644
--- a/framework/lib/src/painting/box_shadow.dart
+++ b/framework/lib/src/painting/box_shadow.dart
@@ -86,8 +86,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BoxShadow? lerp(BoxShadow? a, BoxShadow? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
@@ -110,8 +110,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static List<BoxShadow>? lerpList(List<BoxShadow>? a, List<BoxShadow>? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
a ??= <BoxShadow>[];
b ??= <BoxShadow>[];
diff --git a/framework/lib/src/painting/colors.dart b/framework/lib/src/painting/colors.dart
index 45a87fe..3ccbf50 100644
--- a/framework/lib/src/painting/colors.dart
+++ b/framework/lib/src/painting/colors.dart
@@ -194,8 +194,8 @@
///
/// Values outside of the valid range for each channel will be clamped.
static HSVColor? lerp(HSVColor? a, HSVColor? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!._scaleAlpha(t);
@@ -377,8 +377,8 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static HSLColor? lerp(HSLColor? a, HSLColor? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!._scaleAlpha(t);
@@ -479,13 +479,12 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static ColorSwatch<T>? lerp<T>(ColorSwatch<T>? a, ColorSwatch<T>? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
final Map<T, Color> swatch;
if (b == null) {
- if (a == null) {
- return null;
- } else {
- swatch = a._swatch.map((T key, Color color) => MapEntry<T, Color>(key, Color.lerp(color, null, t)!));
- }
+ swatch = a!._swatch.map((T key, Color color) => MapEntry<T, Color>(key, Color.lerp(color, null, t)!));
} else {
if (a == null) {
swatch = b._swatch.map((T key, Color color) => MapEntry<T, Color>(key, Color.lerp(null, color, t)!));
diff --git a/framework/lib/src/painting/decoration.dart b/framework/lib/src/painting/decoration.dart
index 85ef326..677bea0 100644
--- a/framework/lib/src/painting/decoration.dart
+++ b/framework/lib/src/painting/decoration.dart
@@ -69,7 +69,7 @@
///
/// When implementing this method in subclasses, return null if this class
/// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
- /// method instead.
+ /// method instead. Classes should implement both [lerpFrom] and [lerpTo].
///
/// Supporting interpolating from null is recommended as the [Decoration.lerp]
/// method uses this as a fallback when two classes can't interpolate between
@@ -95,11 +95,11 @@
/// Linearly interpolates from `this` to another [Decoration] (which may be of
/// a different class).
///
- /// This is called if `b`'s [lerpTo] did not know how to handle this class.
+ /// This is called if `b`'s [lerpFrom] did not know how to handle this class.
///
/// When implementing this method in subclasses, return null if this class
/// cannot interpolate from `b`. In that case, [lerp] will apply a default
- /// behavior instead.
+ /// behavior instead. Classes should implement both [lerpFrom] and [lerpTo].
///
/// Supporting interpolating to null is recommended as the [Decoration.lerp]
/// method uses this as a fallback when two classes can't interpolate between
@@ -129,8 +129,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static Decoration? lerp(Decoration? a, Decoration? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.lerpFrom(null, t) ?? b;
diff --git a/framework/lib/src/painting/edge_insets.dart b/framework/lib/src/painting/edge_insets.dart
index 26e9c2d..2a2d5dc 100644
--- a/framework/lib/src/painting/edge_insets.dart
+++ b/framework/lib/src/painting/edge_insets.dart
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:engine/ui.dart' as ui show WindowPadding, lerpDouble;
+import 'package:engine/ui.dart' as ui show ViewPadding, lerpDouble;
import 'package:flute/foundation.dart';
@@ -215,8 +215,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static EdgeInsetsGeometry? lerp(EdgeInsetsGeometry? a, EdgeInsetsGeometry? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
@@ -397,18 +397,27 @@
right = horizontal,
bottom = vertical;
- /// Creates insets that match the given window padding.
+ /// Creates insets that match the given view padding.
///
/// If you need the current system padding or view insets in the context of a
/// widget, consider using [MediaQuery.of] to obtain these values rather than
- /// using the value from [dart:ui.window], so that you get notified of
+ /// using the value from a [FlutterView] directly, so that you get notified of
/// changes.
- EdgeInsets.fromWindowPadding(ui.WindowPadding padding, double devicePixelRatio)
+ EdgeInsets.fromViewPadding(ui.ViewPadding padding, double devicePixelRatio)
: left = padding.left / devicePixelRatio,
top = padding.top / devicePixelRatio,
right = padding.right / devicePixelRatio,
bottom = padding.bottom / devicePixelRatio;
+ /// Deprecated. Will be removed in a future version of Flutter.
+ ///
+ /// Use [EdgeInsets.fromViewPadding] instead.
+ @Deprecated(
+ 'Use EdgeInsets.fromViewPadding instead. '
+ 'This feature was deprecated after v3.8.0-14.0.pre.',
+ )
+ factory EdgeInsets.fromWindowPadding(ui.ViewPadding padding, double devicePixelRatio) = EdgeInsets.fromViewPadding;
+
/// An [EdgeInsets] with zero offsets in each direction.
static const EdgeInsets zero = EdgeInsets.only();
@@ -602,8 +611,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static EdgeInsets? lerp(EdgeInsets? a, EdgeInsets? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
@@ -868,8 +877,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static EdgeInsetsDirectional? lerp(EdgeInsetsDirectional? a, EdgeInsetsDirectional? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
diff --git a/framework/lib/src/painting/flutter_logo.dart b/framework/lib/src/painting/flutter_logo.dart
index c3c31ee..ccef895 100644
--- a/framework/lib/src/painting/flutter_logo.dart
+++ b/framework/lib/src/painting/flutter_logo.dart
@@ -101,8 +101,8 @@
static FlutterLogoDecoration? lerp(FlutterLogoDecoration? a, FlutterLogoDecoration? b, double t) {
assert(a == null || a.debugAssertIsValid());
assert(b == null || b.debugAssertIsValid());
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return FlutterLogoDecoration._(
diff --git a/framework/lib/src/painting/fractional_offset.dart b/framework/lib/src/painting/fractional_offset.dart
index d32957c..fce6603 100644
--- a/framework/lib/src/painting/fractional_offset.dart
+++ b/framework/lib/src/painting/fractional_offset.dart
@@ -176,8 +176,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static FractionalOffset? lerp(FractionalOffset? a, FractionalOffset? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return FractionalOffset(ui.lerpDouble(0.5, b!.dx, t)!, ui.lerpDouble(0.5, b.dy, t)!);
diff --git a/framework/lib/src/painting/gradient.dart b/framework/lib/src/painting/gradient.dart
index 810eca6..cc63874 100644
--- a/framework/lib/src/painting/gradient.dart
+++ b/framework/lib/src/painting/gradient.dart
@@ -307,6 +307,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static Gradient? lerp(Gradient? a, Gradient? b, double t) {
+ if (identical(a, b)) {
+ return a;
+ }
Gradient? result;
if (b != null) {
result = b.lerpFrom(a, t); // if a is null, this must return non-null
@@ -317,9 +320,6 @@
if (result != null) {
return result;
}
- if (a == null && b == null) {
- return null;
- }
assert(a != null && b != null);
return t < 0.5 ? a!.scale(1.0 - (t * 2.0)) : b!.scale((t - 0.5) * 2.0);
}
@@ -486,8 +486,8 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static LinearGradient? lerp(LinearGradient? a, LinearGradient? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
@@ -765,8 +765,8 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static RadialGradient? lerp(RadialGradient? a, RadialGradient? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
@@ -1032,8 +1032,8 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static SweepGradient? lerp(SweepGradient? a, SweepGradient? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
diff --git a/framework/lib/src/painting/image_provider.dart b/framework/lib/src/painting/image_provider.dart
index fbc9ccc..19cdbc4 100644
--- a/framework/lib/src/painting/image_provider.dart
+++ b/framework/lib/src/painting/image_provider.dart
@@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:io';
+import 'dart:math' as math;
import 'package:engine/ui.dart' as ui;
import 'package:engine/ui.dart' show Locale, Size, TextDirection;
@@ -846,11 +847,13 @@
class ResizeImageKey {
// Private constructor so nobody from the outside can poison the image cache
// with this key. It's only accessible to [ResizeImage] internally.
- const ResizeImageKey._(this._providerCacheKey, this._width, this._height);
+ const ResizeImageKey._(this._providerCacheKey, this._policy, this._width, this._height, this._allowUpscaling);
final Object _providerCacheKey;
+ final ResizeImagePolicy _policy;
final int? _width;
final int? _height;
+ final bool _allowUpscaling;
@override
bool operator ==(Object other) {
@@ -859,12 +862,407 @@
}
return other is ResizeImageKey
&& other._providerCacheKey == _providerCacheKey
+ && other._policy == _policy
&& other._width == _width
- && other._height == _height;
+ && other._height == _height
+ && other._allowUpscaling == _allowUpscaling;
}
@override
- int get hashCode => Object.hash(_providerCacheKey, _width, _height);
+ int get hashCode => Object.hash(_providerCacheKey, _policy, _width, _height, _allowUpscaling);
+}
+
+/// Configures the behavior for [ResizeImage].
+///
+/// This is used in [ResizeImage.policy] to affect how the [ResizeImage.width]
+/// and [ResizeImage.height] properties are interpreted.
+enum ResizeImagePolicy {
+ /// Sizes the image to the exact width and height specified by
+ /// [ResizeImage.width] and [ResizeImage.height].
+ ///
+ /// If [ResizeImage.width] and [ResizeImage.height] are both non-null, the
+ /// output image will have the specified width and height (with the
+ /// corresponding aspect ratio) regardless of whether it matches the source
+ /// image's intrinsic aspect ratio. This case is similar to [BoxFit.fill].
+ ///
+ /// If only one of `width` and `height` is non-null, then the output image
+ /// will be scaled to the associated width or height, and the other dimension
+ /// will take whatever value is needed to maintain the image's original aspect
+ /// ratio. These cases are simnilar to [BoxFit.fitWidth] and
+ /// [BoxFit.fitHeight], respectively.
+ ///
+ /// If [ResizeImage.allowUpscaling] is false (the default), the width and the
+ /// height of the output image will each be clamped to the intrinsic width and
+ /// height of the image. This may result in a different aspect ratio than the
+ /// aspect ratio specified by the target width and height (e.g. if the height
+ /// gets clamped downwards but the width does not).
+ ///
+ /// ## Examples
+ ///
+ /// The examples below show how [ResizeImagePolicy.exact] works in various
+ /// scenarios. In each example, the source image has a size of 300x200
+ /// (landscape orientation), the red box is a 150x150 square, and the green
+ /// box is a 400x400 square.
+ ///
+ /// <table>
+ /// <tr>
+ /// <td>Scenario</td>
+ /// <td>Output</td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// width: 150,
+ /// height: 150,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_150x150_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// width: 150,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_150xnull_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// height: 150,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx150_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// width: 400,
+ /// height: 400,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400x400_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// width: 400,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400xnull_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// height: 400,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx400_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// width: 400,
+ /// height: 400,
+ /// allowUpscaling: true,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400x400_true.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// width: 400,
+ /// allowUpscaling: true,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400xnull_true.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// height: 400,
+ /// allowUpscaling: true,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx400_true.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// </table>
+ exact,
+
+ /// Scales the image as necessary to ensure that it fits within the bounding
+ /// box specified by [ResizeImage.width] and [ResizeImage.height] while
+ /// maintaining its aspect ratio.
+ ///
+ /// If [ResizeImage.allowUpscaling] is true, the image will be scaled up or
+ /// down to best fit the bounding box; otherwise it will only ever be scaled
+ /// down.
+ ///
+ /// This is conceptually similar to [BoxFit.contain].
+ ///
+ /// ## Examples
+ ///
+ /// The examples below show how [ResizeImagePolicy.fit] works in various
+ /// scenarios. In each example, the source image has a size of 300x200
+ /// (landscape orientation), the red box is a 150x150 square, and the green
+ /// box is a 400x400 square.
+ ///
+ /// <table>
+ /// <tr>
+ /// <td>Scenario</td>
+ /// <td>Output</td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// width: 150,
+ /// height: 150,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_150x150_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// width: 150,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_150xnull_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// height: 150,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx150_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// width: 400,
+ /// height: 400,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400x400_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// width: 400,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400xnull_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// height: 400,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx400_false.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// width: 400,
+ /// height: 400,
+ /// allowUpscaling: true,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400x400_true.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// width: 400,
+ /// allowUpscaling: true,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400xnull_true.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// <tr>
+ /// <td>
+ ///
+ /// ```dart
+ /// const ResizeImage(
+ /// AssetImage('dragon_cake.jpg'),
+ /// policy: ResizeImagePolicy.fit,
+ /// height: 400,
+ /// allowUpscaling: true,
+ /// )
+ /// ```
+ ///
+ /// </td>
+ /// <td>
+ ///
+ /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx400_true.png)
+ ///
+ /// </td>
+ /// </tr>
+ /// </table>
+ fit,
}
/// Instructs Flutter to decode the image at the specified dimensions
@@ -881,10 +1279,13 @@
/// The cached image will be directly decoded and stored at the resolution
/// defined by `width` and `height`. The image will lose detail and
/// use less memory if resized to a size smaller than the native size.
+ ///
+ /// At least one of `width` and `height` must be non-null.
const ResizeImage(
this.imageProvider, {
this.width,
this.height,
+ this.policy = ResizeImagePolicy.exact,
this.allowUpscaling = false,
}) : assert(width != null || height != null);
@@ -892,11 +1293,20 @@
final ImageProvider imageProvider;
/// The width the image should decode to and cache.
+ ///
+ /// At least one of this and [height] must be non-null.
final int? width;
/// The height the image should decode to and cache.
+ ///
+ /// At least one of this and [width] must be non-null.
final int? height;
+ /// The policy that determines how [width] and [height] are interpreted.
+ ///
+ /// Defaults to [ResizeImagePolicy.exact].
+ final ResizeImagePolicy policy;
+
/// Whether the [width] and [height] parameters should be clamped to the
/// intrinsic width and height of the image.
///
@@ -919,6 +1329,10 @@
}
@override
+ @Deprecated(
+ 'Implement loadImage for faster image loading. '
+ 'This feature was deprecated after v2.13.0-1.0.pre.',
+ )
ImageStreamCompleter load(ResizeImageKey key, DecoderCallback decode) {
Future<ui.Codec> decodeResize(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
assert(
@@ -936,6 +1350,10 @@
}
@override
+ @Deprecated(
+ 'Implement loadImage for image loading. '
+ 'This feature was deprecated after v3.7.0-1.4.pre.',
+ )
ImageStreamCompleter loadBuffer(ResizeImageKey key, DecoderBufferCallback decode) {
Future<ui.Codec> decodeResize(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
assert(
@@ -962,17 +1380,56 @@
'getTargetSize.',
);
return decode(buffer, getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
- int? targetWidth = width;
- int? targetHeight = height;
- if (!allowUpscaling) {
- if (targetWidth != null && targetWidth > intrinsicWidth) {
- targetWidth = intrinsicWidth;
- }
- if (targetHeight != null && targetHeight > intrinsicHeight) {
- targetHeight = intrinsicHeight;
- }
+ switch (policy) {
+ case ResizeImagePolicy.exact:
+ int? targetWidth = width;
+ int? targetHeight = height;
+
+ if (!allowUpscaling) {
+ if (targetWidth != null && targetWidth > intrinsicWidth) {
+ targetWidth = intrinsicWidth;
+ }
+ if (targetHeight != null && targetHeight > intrinsicHeight) {
+ targetHeight = intrinsicHeight;
+ }
+ }
+
+ return ui.TargetImageSize(width: targetWidth, height: targetHeight);
+ case ResizeImagePolicy.fit:
+ final double aspectRatio = intrinsicWidth / intrinsicHeight;
+ final int maxWidth = width ?? intrinsicWidth;
+ final int maxHeight = height ?? intrinsicHeight;
+ int targetWidth = intrinsicWidth;
+ int targetHeight = intrinsicHeight;
+
+ if (targetWidth > maxWidth) {
+ targetWidth = maxWidth;
+ targetHeight = targetWidth ~/ aspectRatio;
+ }
+
+ if (targetHeight > maxHeight) {
+ targetHeight = maxHeight;
+ targetWidth = (targetHeight * aspectRatio).floor();
+ }
+
+ if (allowUpscaling) {
+ if (width == null) {
+ assert(height != null);
+ targetHeight = height!;
+ targetWidth = (targetHeight * aspectRatio).floor();
+ } else if (height == null) {
+ targetWidth = width!;
+ targetHeight = targetWidth ~/ aspectRatio;
+ } else {
+ final int derivedMaxWidth = (maxHeight * aspectRatio).floor();
+ final int derivedMaxHeight = maxWidth ~/ aspectRatio;
+ targetWidth = math.min(maxWidth, derivedMaxWidth);
+ targetHeight = math.min(maxHeight, derivedMaxHeight);
+ }
+ }
+
+ return ui.TargetImageSize(width: targetWidth, height: targetHeight);
}
- return ui.TargetImageSize(width: targetWidth, height: targetHeight);
});
}
@@ -993,10 +1450,10 @@
if (completer == null) {
// This future has completed synchronously (completer was never assigned),
// so we can directly create the synchronous result to return.
- result = SynchronousFuture<ResizeImageKey>(ResizeImageKey._(key, width, height));
+ result = SynchronousFuture<ResizeImageKey>(ResizeImageKey._(key, policy, width, height, allowUpscaling));
} else {
// This future did not synchronously complete.
- completer.complete(ResizeImageKey._(key, width, height));
+ completer.complete(ResizeImageKey._(key, policy, width, height, allowUpscaling));
}
});
if (result != null) {
diff --git a/framework/lib/src/painting/image_resolution.dart b/framework/lib/src/painting/image_resolution.dart
index b7e88ee..50d95c5 100644
--- a/framework/lib/src/painting/image_resolution.dart
+++ b/framework/lib/src/painting/image_resolution.dart
@@ -4,15 +4,12 @@
import 'dart:async';
import 'dart:collection';
-import 'dart:convert';
import 'package:flute/foundation.dart';
import 'package:flute/services.dart';
import 'image_provider.dart';
-const String _kAssetManifestFileName = 'AssetManifest.json';
-
/// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
@@ -284,18 +281,18 @@
Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result;
- chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
- (Map<String, List<String>>? manifest) {
- final String chosenName = _chooseVariant(
+ AssetManifest.loadFromAssetBundle(chosenBundle)
+ .then((AssetManifest manifest) {
+ final Iterable<AssetMetadata>? candidateVariants = manifest.getAssetVariants(keyName);
+ final AssetMetadata chosenVariant = _chooseVariant(
keyName,
configuration,
- manifest == null ? null : manifest[keyName],
- )!;
- final double chosenScale = _parseScale(chosenName);
+ candidateVariants,
+ );
final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle,
- name: chosenName,
- scale: chosenScale,
+ name: chosenVariant.key,
+ scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
);
if (completer != null) {
// We already returned from this function, which means we are in the
@@ -309,14 +306,15 @@
// ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key);
}
- },
- ).catchError((Object error, StackTrace stack) {
- // We had an error. (This guarantees we weren't called synchronously.)
- // Forward the error to the caller.
- assert(completer != null);
- assert(result == null);
- completer!.completeError(error, stack);
- });
+ })
+ .onError((Object error, StackTrace stack) {
+ // We had an error. (This guarantees we weren't called synchronously.)
+ // Forward the error to the caller.
+ assert(completer != null);
+ assert(result == null);
+ completer!.completeError(error, stack);
+ });
+
if (result != null) {
// The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above.
@@ -328,35 +326,24 @@
return completer.future;
}
- /// Parses the asset manifest string into a strongly-typed map.
- @visibleForTesting
- static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
- if (jsonData == null) {
- return SynchronousFuture<Map<String, List<String>>?>(null);
+ AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
+ if (candidateVariants == null) {
+ return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
}
- // TODO(ianh): JSON decoding really shouldn't be on the main thread.
- final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
- final Iterable<String> keys = parsedJson.keys;
- final Map<String, List<String>> parsedManifest = <String, List<String>> {
- for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
- };
- // TODO(ianh): convert that data structure to the right types.
- return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
- }
- String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
- if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
- return main;
+ if (config.devicePixelRatio == null) {
+ return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
}
- // TODO(ianh): Consider moving this parsing logic into _manifestParser.
- final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
- for (final String candidate in candidates) {
- mapping[_parseScale(candidate)] = candidate;
+
+ final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
+ SplayTreeMap<double, AssetMetadata>();
+ for (final AssetMetadata candidate in candidateVariants) {
+ candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
}
// TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset
// docs)
- return _findBestVariant(mapping, config.devicePixelRatio!);
+ return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
}
// Returns the "best" asset variant amongst the available `candidates`.
@@ -371,17 +358,17 @@
// lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`.
- String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
- if (candidates.containsKey(value)) {
- return candidates[value]!;
+ AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
+ if (candidatesByDpr.containsKey(value)) {
+ return candidatesByDpr[value]!;
}
- final double? lower = candidates.lastKeyBefore(value);
- final double? upper = candidates.firstKeyAfter(value);
+ final double? lower = candidatesByDpr.lastKeyBefore(value);
+ final double? upper = candidatesByDpr.firstKeyAfter(value);
if (lower == null) {
- return candidates[upper];
+ return candidatesByDpr[upper]!;
}
if (upper == null) {
- return candidates[lower];
+ return candidatesByDpr[lower]!;
}
// On screens with low device-pixel ratios the artifacts from upscaling
@@ -389,32 +376,12 @@
// ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
- return candidates[upper];
+ return candidatesByDpr[upper]!;
} else {
- return candidates[lower];
+ return candidatesByDpr[lower]!;
}
}
- static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
-
- double _parseScale(String key) {
- if (key == assetName) {
- return _naturalResolution;
- }
-
- final Uri assetUri = Uri.parse(key);
- String directoryPath = '';
- if (assetUri.pathSegments.length > 1) {
- directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
- }
-
- final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
- if (match != null && match.groupCount > 0) {
- return double.parse(match.group(1)!);
- }
- return _naturalResolution; // i.e. default to 1.0x
- }
-
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
diff --git a/framework/lib/src/painting/linear_border.dart b/framework/lib/src/painting/linear_border.dart
index 6fbd234..89045e0 100644
--- a/framework/lib/src/painting/linear_border.dart
+++ b/framework/lib/src/painting/linear_border.dart
@@ -59,8 +59,8 @@
/// is null then we interpolate from `a` varying size from `a.size` to zero.
/// Otherwise both values are interpolated.
static LinearBorderEdge? lerp(LinearBorderEdge? a, LinearBorderEdge? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
a ??= LinearBorderEdge(alignment: b!.alignment, size: 0);
diff --git a/framework/lib/src/painting/shape_decoration.dart b/framework/lib/src/painting/shape_decoration.dart
index 43fc655..793922c 100644
--- a/framework/lib/src/painting/shape_decoration.dart
+++ b/framework/lib/src/painting/shape_decoration.dart
@@ -225,8 +225,8 @@
/// and which use [ShapeDecoration.lerp] when interpolating two
/// [ShapeDecoration]s or a [ShapeDecoration] to or from null.
static ShapeDecoration? lerp(ShapeDecoration? a, ShapeDecoration? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a != null && b != null) {
if (t == 0.0) {
diff --git a/framework/lib/src/painting/text_style.dart b/framework/lib/src/painting/text_style.dart
index 2b390a5..a530338 100644
--- a/framework/lib/src/painting/text_style.dart
+++ b/framework/lib/src/painting/text_style.dart
@@ -1077,7 +1077,7 @@
/// implementation uses the non-null value throughout the transition for
/// lerpable fields such as colors (for example, if one [TextStyle] specified
/// `fontSize` but the other didn't, the returned [TextStyle] will use the
- /// `fontSize` from the [TextStyle] that specified it, regarless of the `t`
+ /// `fontSize` from the [TextStyle] that specified it, regardless of the `t`
/// value).
///
/// This method throws when the given [TextStyle]s don't have the same
@@ -1094,10 +1094,9 @@
/// as if they have a [background] paint (creating a new [Paint] if necessary
/// based on the [backgroundColor] property).
static TextStyle? lerp(TextStyle? a, TextStyle? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
-
String? lerpDebugLabel;
assert(() {
lerpDebugLabel = 'lerp(${a?.debugLabel ?? _kDefaultDebugLabel} ⎯${t.toStringAsFixed(1)}→ ${b?.debugLabel ?? _kDefaultDebugLabel})';
diff --git a/framework/lib/src/physics/spring_simulation.dart b/framework/lib/src/physics/spring_simulation.dart
index dbd8153..f756ae5 100644
--- a/framework/lib/src/physics/spring_simulation.dart
+++ b/framework/lib/src/physics/spring_simulation.dart
@@ -176,7 +176,7 @@
) {
final double r = -spring.damping / (2.0 * spring.mass);
final double c1 = distance;
- final double c2 = velocity / (r * distance);
+ final double c2 = velocity - (r * distance);
return _CriticalSolution.withArgs(r, c1, c2);
}
diff --git a/framework/lib/src/rendering/binding.dart b/framework/lib/src/rendering/binding.dart
index da8c459..3e022aa 100644
--- a/framework/lib/src/rendering/binding.dart
+++ b/framework/lib/src/rendering/binding.dart
@@ -38,16 +38,15 @@
platformDispatcher
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
- ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
- ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
- ..onSemanticsAction = _handleSemanticsAction;
+ ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;
initRenderView();
- _handleSemanticsEnabledChanged();
addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker();
if (kIsWeb) {
addPostFrameCallback(_handleWebFirstFrame);
}
+ addSemanticsEnabledListener(_handleSemanticsEnabledChanged);
+ _handleSemanticsEnabledChanged();
}
/// The current [RendererBinding], if one has been created.
@@ -308,8 +307,6 @@
);
}
- SemanticsHandle? _semanticsHandle;
-
/// Creates a [MouseTracker] which manages state about currently connected
/// mice, for hover notification.
///
@@ -333,14 +330,10 @@
super.dispatchEvent(event, hitTestResult);
}
- void _handleSemanticsEnabledChanged() {
- setSemanticsEnabled(platformDispatcher.semanticsEnabled);
- }
+ SemanticsHandle? _semanticsHandle;
- /// Whether the render tree associated with this binding should produce a tree
- /// of [SemanticsNode] objects.
- void setSemanticsEnabled(bool enabled) {
- if (enabled) {
+ void _handleSemanticsEnabledChanged() {
+ if (semanticsEnabled) {
_semanticsHandle ??= _pipelineOwner.ensureSemantics();
} else {
_semanticsHandle?.dispose();
@@ -348,18 +341,9 @@
}
}
- void _handleWebFirstFrame(Duration _) {
- assert(kIsWeb);
- const MethodChannel methodChannel = MethodChannel('flutter/service_worker');
- methodChannel.invokeMethod<void>('first-frame');
- }
-
- void _handleSemanticsAction(int id, SemanticsAction action, ByteData? args) {
- _pipelineOwner.semanticsOwner?.performAction(
- id,
- action,
- args != null ? const StandardMessageCodec().decodeMessage(args) : null,
- );
+ @override
+ void performSemanticsAction(SemanticsActionEvent action) {
+ _pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
}
void _handleSemanticsOwnerCreated() {
@@ -374,6 +358,12 @@
renderView.clearSemantics();
}
+ void _handleWebFirstFrame(Duration _) {
+ assert(kIsWeb);
+ const MethodChannel methodChannel = MethodChannel('flutter/service_worker');
+ methodChannel.invokeMethod<void>('first-frame');
+ }
+
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
diff --git a/framework/lib/src/rendering/box.dart b/framework/lib/src/rendering/box.dart
index 057a831..1a0a020 100644
--- a/framework/lib/src/rendering/box.dart
+++ b/framework/lib/src/rendering/box.dart
@@ -468,8 +468,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static BoxConstraints? lerp(BoxConstraints? a, BoxConstraints? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b! * t;
diff --git a/framework/lib/src/rendering/editable.dart b/framework/lib/src/rendering/editable.dart
index 407b88c..624761c 100644
--- a/framework/lib/src/rendering/editable.dart
+++ b/framework/lib/src/rendering/editable.dart
@@ -729,7 +729,7 @@
// Check if the selection is visible with an approximation because a
// difference between rounded and unrounded values causes the caret to be
// reported as having a slightly (< 0.5) negative y offset. This rounding
- // happens in paragraph.cc's layout and TextPainer's
+ // happens in paragraph.cc's layout and TextPainter's
// _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
// this can be changed to be a strict check instead of an approximation.
const double visibleRegionSlop = 0.5;
@@ -775,7 +775,7 @@
@override
void markNeedsPaint() {
super.markNeedsPaint();
- // Tell the painers to repaint since text layout may have changed.
+ // Tell the painters to repaint since text layout may have changed.
_foregroundRenderObject?.markNeedsPaint();
_backgroundRenderObject?.markNeedsPaint();
}
diff --git a/framework/lib/src/rendering/object.dart b/framework/lib/src/rendering/object.dart
index c1f1159..d48d600 100644
--- a/framework/lib/src/rendering/object.dart
+++ b/framework/lib/src/rendering/object.dart
@@ -15,6 +15,7 @@
import 'debug.dart';
import 'layer.dart';
+import 'proxy_box.dart';
export 'package:flute/foundation.dart' show
DiagnosticPropertiesBuilder,
@@ -807,24 +808,8 @@
/// Used by [RenderObject.invokeLayoutCallback].
typedef LayoutCallback<T extends Constraints> = void Function(T constraints);
-/// A reference to the semantics tree.
-///
-/// The framework maintains the semantics tree (used for accessibility and
-/// indexing) only when there is at least one client holding an open
-/// [SemanticsHandle].
-///
-/// The framework notifies the client that it has updated the semantics tree by
-/// calling the [listener] callback. When the client no longer needs the
-/// semantics tree, the client can call [dispose] on the [SemanticsHandle],
-/// which stops these callbacks and closes the [SemanticsHandle]. When all the
-/// outstanding [SemanticsHandle] objects are closed, the framework stops
-/// updating the semantics tree.
-///
-/// To obtain a [SemanticsHandle], call [PipelineOwner.ensureSemantics] on the
-/// [PipelineOwner] for the render tree from which you wish to read semantics.
-/// You can obtain the [PipelineOwner] using the [RenderObject.owner] property.
-class SemanticsHandle {
- SemanticsHandle._(PipelineOwner owner, this.listener)
+class _LocalSemanticsHandle implements SemanticsHandle {
+ _LocalSemanticsHandle._(PipelineOwner owner, this.listener)
: _owner = owner {
if (listener != null) {
_owner.semanticsOwner!.addListener(listener!);
@@ -836,13 +821,7 @@
/// The callback that will be notified when the semantics tree updates.
final VoidCallback? listener;
- /// Closes the semantics handle and stops calling [listener] when the
- /// semantics updates.
- ///
- /// When all the outstanding [SemanticsHandle] objects for a given
- /// [PipelineOwner] are closed, the [PipelineOwner] will stop updating the
- /// semantics tree.
- @mustCallSuper
+ @override
void dispose() {
if (listener != null) {
_owner.semanticsOwner!.removeListener(listener!);
@@ -1170,7 +1149,12 @@
int _outstandingSemanticsHandles = 0;
/// Opens a [SemanticsHandle] and calls [listener] whenever the semantics tree
- /// updates.
+ /// generated from the render tree owned by this [PipelineOwner] updates.
+ ///
+ /// Calling this method only ensures that this particular [PipelineOwner] will
+ /// generate a semantics tree. Consider calling
+ /// [SemanticsBinding.ensureSemantics] instead to turn on semantics globally
+ /// for the entire app.
///
/// The [PipelineOwner] updates the semantics tree only when there are clients
/// that wish to use the semantics tree. These clients express their interest
@@ -1189,7 +1173,7 @@
_semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!);
onSemanticsOwnerCreated?.call();
}
- return SemanticsHandle._(this, listener);
+ return _LocalSemanticsHandle._(this, listener);
}
void _didDisposeSemanticsHandle() {
@@ -1502,7 +1486,6 @@
/// in other cases will lead to an inconsistent tree and probably cause crashes.
@override
void adoptChild(RenderObject child) {
- assert(_debugCanPerformMutations);
setupParentData(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
@@ -1516,7 +1499,6 @@
/// in other cases will lead to an inconsistent tree and probably cause crashes.
@override
void dropChild(RenderObject child) {
- assert(_debugCanPerformMutations);
assert(child.parentData != null);
child._cleanRelayoutBoundary();
child.parentData!.detach();
@@ -1659,7 +1641,7 @@
}
if (!activeLayoutRoot._debugMutationsLocked) {
- final AbstractNode? p = activeLayoutRoot.parent;
+ final AbstractNode? p = activeLayoutRoot.debugLayoutParent;
activeLayoutRoot = p is RenderObject ? p : null;
} else {
// activeLayoutRoot found.
@@ -1738,6 +1720,29 @@
return result;
}
+ /// The [RenderObject] that's expected to call [layout] on this [RenderObject]
+ /// in its [performLayout] implementation.
+ ///
+ /// This method is used to implement an assert that ensures the render subtree
+ /// actively performing layout can not get accidently mutated. It's only
+ /// implemented in debug mode and always returns null in release mode.
+ ///
+ /// The default implementation returns [parent] and overriding is rarely
+ /// needed. A [RenderObject] subclass that expects its
+ /// [RenderObject.performLayout] to be called from a different [RenderObject]
+ /// that's not its [parent] should override this property to return the actual
+ /// layout parent.
+ @protected
+ RenderObject? get debugLayoutParent {
+ RenderObject? layoutParent;
+ assert(() {
+ final AbstractNode? parent = this.parent;
+ layoutParent = parent is RenderObject? ? parent : null;
+ return true;
+ }());
+ return layoutParent;
+ }
+
@override
PipelineOwner? get owner => super.owner as PipelineOwner?;
@@ -2369,7 +2374,7 @@
/// needing to paint and needing a composited layer update, this method is only
/// called once.
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/102102 revisit the
- // contraint that the instance/type of layer cannot be changed at runtime.
+ // constraint that the instance/type of layer cannot be changed at runtime.
OffsetLayer updateCompositedLayer({required covariant OffsetLayer? oldLayer}) {
assert(isRepaintBoundary);
return oldLayer ?? OffsetLayer();
@@ -3244,7 +3249,7 @@
final SemanticsConfiguration config = _semanticsConfiguration;
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
- final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
+ bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
@@ -3252,6 +3257,7 @@
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
+ final bool hasTags = config.tagsForChildren?.isNotEmpty ?? false;
visitChildrenForSemantics((RenderObject renderChild) {
assert(!_needsLayout);
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
@@ -3267,7 +3273,9 @@
}
for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
fragment.addAncestor(this);
- fragment.addTags(config.tagsForChildren);
+ if (hasTags) {
+ fragment.addTags(config.tagsForChildren!);
+ }
if (hasChildConfigurationsDelegate && fragment.config != null) {
// This fragment need to go through delegate to determine whether it
// merge up or not.
@@ -3283,7 +3291,9 @@
for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
siblingMergingFragment.addAncestor(this);
- siblingMergingFragment.addTags(config.tagsForChildren);
+ if (hasTags) {
+ siblingMergingFragment.addTags(config.tagsForChildren!);
+ }
}
siblingMergeFragmentGroups.add(siblingMergeGroup);
}
@@ -3296,14 +3306,25 @@
for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
fragment.markAsExplicit();
}
- } else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
+ } else if (hasChildConfigurationsDelegate) {
final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
mergeUpFragments.addAll(
- result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
+ result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
+ final _InterestingSemanticsFragment? fragment = configToFragment[config];
+ if (fragment == null) {
+ // Parent fragment of Incomplete fragments can't be a forking
+ // fragment since they need to be merged.
+ producesForkingFragment = false;
+ return _IncompleteSemanticsFragment(config: config, owner: this);
+ }
+ return fragment;
+ }),
);
for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
siblingMergeFragmentGroups.add(
- group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
+ group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
+ return configToFragment[config] ?? _IncompleteSemanticsFragment(config: config, owner: this);
+ }).toList(),
);
}
}
@@ -3636,17 +3657,13 @@
@override
void attach(PipelineOwner owner) {
super.attach(owner);
- if (_child != null) {
- _child!.attach(owner);
- }
+ _child?.attach(owner);
}
@override
void detach() {
super.detach();
- if (_child != null) {
- _child!.detach();
- }
+ _child?.detach();
}
@override
@@ -4184,10 +4201,10 @@
Set<SemanticsTag>? _tagsForChildren;
/// Tag all children produced by [compileChildren] with `tags`.
- void addTags(Iterable<SemanticsTag>? tags) {
- if (tags == null || tags.isEmpty) {
- return;
- }
+ ///
+ /// `tags` must not be empty.
+ void addTags(Iterable<SemanticsTag> tags) {
+ assert(tags.isNotEmpty);
_tagsForChildren ??= <SemanticsTag>{};
_tagsForChildren!.addAll(tags);
}
@@ -4281,6 +4298,48 @@
}
}
+/// A fragment with partial information that must not form an explicit
+/// semantics node without merging into another _SwitchableSemanticsFragment.
+///
+/// This fragment is generated from synthetic SemanticsConfiguration returned from
+/// [SemanticsConfiguration.childConfigurationsDelegate].
+class _IncompleteSemanticsFragment extends _InterestingSemanticsFragment {
+ _IncompleteSemanticsFragment({
+ required this.config,
+ required super.owner,
+ }) : super(dropsSemanticsOfPreviousSiblings: false);
+
+ @override
+ void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
+ assert(false, 'This fragment must be a leaf node');
+ }
+
+ @override
+ void compileChildren({
+ required Rect? parentSemanticsClipRect,
+ required Rect? parentPaintClipRect,
+ required double elevationAdjustment,
+ required List<SemanticsNode> result,
+ required List<SemanticsNode> siblingNodes,
+ }) {
+ // There is nothing to do because this fragment must be a leaf node and
+ // must not be explicit.
+ }
+
+ @override
+ final SemanticsConfiguration config;
+
+ @override
+ void markAsExplicit() {
+ assert(
+ false,
+ 'SemanticsConfiguration created in '
+ 'SemanticsConfiguration.childConfigurationsDelegate must not produce '
+ 'its own semantics node'
+ );
+ }
+}
+
/// An [_InterestingSemanticsFragment] that can be told to only add explicit
/// [SemanticsNode]s to the parent.
///
@@ -4559,6 +4618,17 @@
}
}
+ @override
+ void addTags(Iterable<SemanticsTag> tags) {
+ super.addTags(tags);
+ // _ContainerSemanticsFragments add their tags to child fragments through
+ // this method. This fragment must make sure its _config is in sync.
+ if (tags.isNotEmpty) {
+ _ensureConfigIsWritable();
+ tags.forEach(_config.addTagForChildren);
+ }
+ }
+
void _ensureConfigIsWritable() {
if (!_isConfigWritable) {
_config = _config.copy();
diff --git a/framework/lib/src/rendering/paragraph.dart b/framework/lib/src/rendering/paragraph.dart
index 5b44dc1..0ebfeb7 100644
--- a/framework/lib/src/rendering/paragraph.dart
+++ b/framework/lib/src/rendering/paragraph.dart
@@ -119,7 +119,9 @@
static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
final TextPainter _textPainter;
- AttributedString? _cachedAttributedLabel;
+
+ List<AttributedString>? _cachedAttributedLabels;
+
List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
/// The text to display.
@@ -135,7 +137,7 @@
break;
case RenderComparison.paint:
_textPainter.text = value;
- _cachedAttributedLabel = null;
+ _cachedAttributedLabels = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
markNeedsPaint();
@@ -144,7 +146,7 @@
case RenderComparison.layout:
_textPainter.text = value;
_overflowShader = null;
- _cachedAttributedLabel = null;
+ _cachedAttributedLabels = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
markNeedsLayout();
@@ -1035,12 +1037,23 @@
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
_semanticsInfo = text.getSemanticsInformation();
+ bool needsAssembleSemanticsNode = false;
+ bool needsChildConfigrationsDelegate = false;
+ for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
+ if (info.recognizer != null) {
+ needsAssembleSemanticsNode = true;
+ break;
+ }
+ needsChildConfigrationsDelegate = needsChildConfigrationsDelegate || info.isPlaceholder;
+ }
- if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
+ if (needsAssembleSemanticsNode) {
config.explicitChildNodes = true;
config.isSemanticBoundary = true;
+ } else if (needsChildConfigrationsDelegate) {
+ config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate;
} else {
- if (_cachedAttributedLabel == null) {
+ if (_cachedAttributedLabels == null) {
final StringBuffer buffer = StringBuffer();
int offset = 0;
final List<StringAttribute> attributes = <StringAttribute>[];
@@ -1050,21 +1063,77 @@
final TextRange originalRange = infoAttribute.range;
attributes.add(
infoAttribute.copy(
- range: TextRange(start: offset + originalRange.start,
- end: offset + originalRange.end)
+ range: TextRange(
+ start: offset + originalRange.start,
+ end: offset + originalRange.end,
+ ),
),
);
}
buffer.write(label);
offset += label.length;
}
- _cachedAttributedLabel = AttributedString(buffer.toString(), attributes: attributes);
+ _cachedAttributedLabels = <AttributedString>[AttributedString(buffer.toString(), attributes: attributes)];
}
- config.attributedLabel = _cachedAttributedLabel!;
+ config.attributedLabel = _cachedAttributedLabels![0];
config.textDirection = textDirection;
}
}
+ ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate(List<SemanticsConfiguration> childConfigs) {
+ final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
+ int placeholderIndex = 0;
+ int childConfigsIndex = 0;
+ int attributedLabelCacheIndex = 0;
+ InlineSpanSemanticsInformation? seenTextInfo;
+ _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
+ for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
+ if (info.isPlaceholder) {
+ if (seenTextInfo != null) {
+ builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
+ attributedLabelCacheIndex += 1;
+ }
+ // Mark every childConfig belongs to this placeholder to merge up group.
+ while (childConfigsIndex < childConfigs.length &&
+ childConfigs[childConfigsIndex].tagsChildrenWith(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
+ builder.markAsMergeUp(childConfigs[childConfigsIndex]);
+ childConfigsIndex += 1;
+ }
+ placeholderIndex += 1;
+ } else {
+ seenTextInfo = info;
+ }
+ }
+
+ // Handle plain text info at the end.
+ if (seenTextInfo != null) {
+ builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
+ }
+ return builder.build();
+ }
+
+ SemanticsConfiguration _createSemanticsConfigForTextInfo(InlineSpanSemanticsInformation textInfo, int cacheIndex) {
+ assert(!textInfo.requiresOwnNode);
+ final List<AttributedString> cachedStrings = _cachedAttributedLabels ??= <AttributedString>[];
+ assert(cacheIndex <= cachedStrings.length);
+ final bool hasCache = cacheIndex < cachedStrings.length;
+
+ late AttributedString attributedLabel;
+ if (hasCache) {
+ attributedLabel = cachedStrings[cacheIndex];
+ } else {
+ assert(cachedStrings.length == cacheIndex);
+ attributedLabel = AttributedString(
+ textInfo.semanticsLabel ?? textInfo.text,
+ attributes: textInfo.stringAttributes,
+ );
+ cachedStrings.add(attributedLabel);
+ }
+ return SemanticsConfiguration()
+ ..textDirection = textDirection
+ ..attributedLabel = attributedLabel;
+ }
+
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
// can be re-used when [assembleSemanticsNode] is called again. This ensures
// stable ids for the [SemanticsNode]s of [TextSpan]s across
diff --git a/framework/lib/src/rendering/stack.dart b/framework/lib/src/rendering/stack.dart
index d490d6b..92da839 100644
--- a/framework/lib/src/rendering/stack.dart
+++ b/framework/lib/src/rendering/stack.dart
@@ -165,8 +165,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static RelativeRect? lerp(RelativeRect? a, RelativeRect? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return RelativeRect.fromLTRB(b!.left * t, b.top * t, b.right * t, b.bottom * t);
diff --git a/framework/lib/src/rendering/table_border.dart b/framework/lib/src/rendering/table_border.dart
index c4fecb7..bf7f5a3 100644
--- a/framework/lib/src/rendering/table_border.dart
+++ b/framework/lib/src/rendering/table_border.dart
@@ -153,8 +153,8 @@
///
/// {@macro dart.ui.shadow.lerp}
static TableBorder? lerp(TableBorder? a, TableBorder? b, double t) {
- if (a == null && b == null) {
- return null;
+ if (identical(a, b)) {
+ return a;
}
if (a == null) {
return b!.scale(t);
diff --git a/framework/lib/src/scheduler/binding.dart b/framework/lib/src/scheduler/binding.dart
index 8af3176..0adcf7c 100644
--- a/framework/lib/src/scheduler/binding.dart
+++ b/framework/lib/src/scheduler/binding.dart
@@ -189,7 +189,7 @@
/// See also:
///
/// * [PerformanceModeRequestHandle] for more information on the lifecycle of the handle.
-typedef _PerformanceModeCleaupCallback = VoidCallback;
+typedef _PerformanceModeCleanupCallback = VoidCallback;
/// An opaque handle that keeps a request for [DartPerformanceMode] active until
/// disposed.
@@ -197,9 +197,9 @@
/// To create a [PerformanceModeRequestHandle], use [SchedulerBinding.requestPerformanceMode].
/// The component that makes the request is responsible for disposing the handle.
class PerformanceModeRequestHandle {
- PerformanceModeRequestHandle._(_PerformanceModeCleaupCallback this._cleanup);
+ PerformanceModeRequestHandle._(_PerformanceModeCleanupCallback this._cleanup);
- _PerformanceModeCleaupCallback? _cleanup;
+ _PerformanceModeCleanupCallback? _cleanup;
/// Call this method to signal to [SchedulerBinding] that a request for a [DartPerformanceMode]
/// is no longer needed.
diff --git a/framework/lib/src/semantics/binding.dart b/framework/lib/src/semantics/binding.dart
index 73d563d..70f784b 100644
--- a/framework/lib/src/semantics/binding.dart
+++ b/framework/lib/src/semantics/binding.dart
@@ -2,22 +2,27 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:engine/ui.dart' as ui show AccessibilityFeatures, SemanticsUpdateBuilder;
+import 'package:engine/ui.dart' as ui show AccessibilityFeatures, SemanticsAction, SemanticsUpdateBuilder;
import 'package:flute/foundation.dart';
+import 'package:flute/services.dart';
import 'debug.dart';
export 'package:engine/ui.dart' show AccessibilityFeatures, SemanticsUpdateBuilder;
/// The glue between the semantics layer and the Flutter engine.
-// TODO(zanderso): move the remaining semantic related bindings here.
mixin SemanticsBinding on BindingBase {
@override
void initInstances() {
super.initInstances();
_instance = this;
_accessibilityFeatures = platformDispatcher.accessibilityFeatures;
+ platformDispatcher
+ ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
+ ..onSemanticsAction = _handleSemanticsAction
+ ..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
+ _handleSemanticsEnabledChanged();
}
/// The current [SemanticsBinding], if one has been created.
@@ -28,10 +33,123 @@
static SemanticsBinding get instance => BindingBase.checkInstance(_instance);
static SemanticsBinding? _instance;
+ /// Whether semantics information must be collected.
+ ///
+ /// Returns true if either the platform has requested semantics information
+ /// to be generated or if [ensureSemantics] has been called otherwise.
+ ///
+ /// To get notified when this value changes register a listener with
+ /// [addSemanticsEnabledListener].
+ bool get semanticsEnabled {
+ assert(_semanticsEnabled.value == (_outstandingHandles > 0));
+ return _semanticsEnabled.value;
+ }
+ late final ValueNotifier<bool> _semanticsEnabled = ValueNotifier<bool>(platformDispatcher.semanticsEnabled);
+
+ /// Adds a `listener` to be called when [semanticsEnabled] changes.
+ ///
+ /// See also:
+ ///
+ /// * [removeSemanticsEnabledListener] to remove the listener again.
+ /// * [ValueNotifier.addListener], which documents how and when listeners are
+ /// called.
+ void addSemanticsEnabledListener(VoidCallback listener) {
+ _semanticsEnabled.addListener(listener);
+ }
+
+ /// Removes a `listener` added by [addSemanticsEnabledListener].
+ ///
+ /// See also:
+ ///
+ /// * [ValueNotifier.removeListener], which documents how listeners are
+ /// removed.
+ void removeSemanticsEnabledListener(VoidCallback listener) {
+ _semanticsEnabled.removeListener(listener);
+ }
+
+ /// The number of clients registered to listen for semantics.
+ ///
+ /// The number is increased whenever [ensureSemantics] is called and decreased
+ /// when [SemanticsHandle.dispose] is called.
+ int get debugOutstandingSemanticsHandles => _outstandingHandles;
+ int _outstandingHandles = 0;
+
+ /// Creates a new [SemanticsHandle] and requests the collection of semantics
+ /// information.
+ ///
+ /// Semantics information are only collected when there are clients interested
+ /// in them. These clients express their interest by holding a
+ /// [SemanticsHandle].
+ ///
+ /// Clients can close their [SemanticsHandle] by calling
+ /// [SemanticsHandle.dispose]. Once all outstanding [SemanticsHandle] objects
+ /// are closed, semantics information are no longer collected.
+ SemanticsHandle ensureSemantics() {
+ assert(_outstandingHandles >= 0);
+ _outstandingHandles++;
+ assert(_outstandingHandles > 0);
+ _semanticsEnabled.value = true;
+ return SemanticsHandle._(_didDisposeSemanticsHandle);
+ }
+
+ void _didDisposeSemanticsHandle() {
+ assert(_outstandingHandles > 0);
+ _outstandingHandles--;
+ assert(_outstandingHandles >= 0);
+ _semanticsEnabled.value = _outstandingHandles > 0;
+ }
+
+ // Handle for semantics request from the platform.
+ SemanticsHandle? _semanticsHandle;
+
+ void _handleSemanticsEnabledChanged() {
+ if (platformDispatcher.semanticsEnabled) {
+ _semanticsHandle ??= ensureSemantics();
+ } else {
+ _semanticsHandle?.dispose();
+ _semanticsHandle = null;
+ }
+ }
+
+ void _handleSemanticsAction(int id, ui.SemanticsAction action, ByteData? args) {
+ performSemanticsAction(SemanticsActionEvent(
+ nodeId: id,
+ type: action,
+ arguments: args != null ? const StandardMessageCodec().decodeMessage(args) : null,
+ ));
+ }
+
+ /// Called whenever the platform requests an action to be performed on a
+ /// [SemanticsNode].
+ ///
+ /// This callback is invoked when a user interacts with the app via an
+ /// accessibility service (e.g. TalkBack and VoiceOver) and initiates an
+ /// action on the focused node.
+ ///
+ /// Bindings that mixin the [SemanticsBinding] must implement this method and
+ /// perform the given `action` on the [SemanticsNode] specified by
+ /// [SemanticsActionEvent.nodeId].
+ ///
+ /// See [dart:ui.PlatformDispatcher.onSemanticsAction].
+ @protected
+ void performSemanticsAction(SemanticsActionEvent action);
+
+ /// The currently active set of [AccessibilityFeatures].
+ ///
+ /// This is set when the binding is first initialized and updated whenever a
+ /// flag is changed.
+ ///
+ /// To listen to changes to accessibility features, create a
+ /// [WidgetsBindingObserver] and listen to
+ /// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
+ ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
+ late ui.AccessibilityFeatures _accessibilityFeatures;
+
/// Called when the platform accessibility features change.
///
/// See [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged].
@protected
+ @mustCallSuper
void handleAccessibilityFeaturesChanged() {
_accessibilityFeatures = platformDispatcher.accessibilityFeatures;
}
@@ -46,17 +164,6 @@
return ui.SemanticsUpdateBuilder();
}
- /// The currently active set of [AccessibilityFeatures].
- ///
- /// This is initialized the first time [runApp] is called and updated whenever
- /// a flag is changed.
- ///
- /// To listen to changes to accessibility features, create a
- /// [WidgetsBindingObserver] and listen to
- /// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
- ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
- late ui.AccessibilityFeatures _accessibilityFeatures;
-
/// The platform is requesting that animations be disabled or simplified.
///
/// This setting can be overridden for testing or debugging by setting
@@ -72,3 +179,49 @@
return value;
}
}
+
+/// An event to request a [SemanticsAction] of [type] to be performed on the
+/// [SemanticsNode] identified by [nodeId].
+///
+/// Used by [SemanticsBinding.performSemanticsAction].
+@immutable
+class SemanticsActionEvent {
+ /// Creates a [SemanticsActionEvent].
+ ///
+ /// The [type] and [nodeId] are required.
+ const SemanticsActionEvent({required this.type, required this.nodeId, this.arguments});
+
+ /// The type of action to be performed.
+ final ui.SemanticsAction type;
+
+ /// The id of the [SemanticsNode] on which the action is to be performed.
+ final int nodeId;
+
+ /// Optional arguments for the action.
+ final Object? arguments;
+}
+
+/// A reference to the semantics information generated by the framework.
+///
+/// Semantics information are only collected when there are clients interested
+/// in them. These clients express their interest by holding a
+/// [SemanticsHandle]. When the client no longer needs the
+/// semantics information, it must call [dispose] on the [SemanticsHandle] to
+/// close it. When all open [SemanticsHandle]s are disposed, the framework will
+/// stop updating the semantics information.
+///
+/// To obtain a [SemanticsHandle], call [SemanticsBinding.ensureSemantics].
+class SemanticsHandle {
+ SemanticsHandle._(this._onDispose);
+
+ final VoidCallback _onDispose;
+
+ /// Closes the semantics handle.
+ ///
+ /// When all the outstanding [SemanticsHandle] objects are closed, the
+ /// framework will stop generating semantics information.
+ @mustCallSuper
+ void dispose() {
+ _onDispose();
+ }
+}
diff --git a/framework/lib/src/semantics/semantics.dart b/framework/lib/src/semantics/semantics.dart
index 32fb160..d88b9a1 100644
--- a/framework/lib/src/semantics/semantics.dart
+++ b/framework/lib/src/semantics/semantics.dart
@@ -346,7 +346,7 @@
}
@override
- int get hashCode => Object.hash(string, attributes,);
+ int get hashCode => Object.hash(string, attributes);
@override
String toString() {
@@ -3129,9 +3129,9 @@
/// Owns [SemanticsNode] objects and notifies listeners of changes to the
/// render tree semantics.
///
-/// To listen for semantic updates, call [PipelineOwner.ensureSemantics] to
-/// obtain a [SemanticsHandle]. This will create a [SemanticsOwner] if
-/// necessary.
+/// To listen for semantic updates, call [SemanticsBinding.ensureSemantics] or
+/// [PipelineOwner.ensureSemantics] to obtain a [SemanticsHandle]. This will
+/// create a [SemanticsOwner] if necessary.
class SemanticsOwner extends ChangeNotifier {
/// Creates a [SemanticsOwner] that manages zero or more [SemanticsNode] objects.
SemanticsOwner({
@@ -3248,7 +3248,7 @@
}
// Default actions if no [handler] was provided.
- if (action == SemanticsAction.showOnScreen && _nodes[id]!._showOnScreen != null) {
+ if (action == SemanticsAction.showOnScreen && _nodes[id]?._showOnScreen != null) {
_nodes[id]!._showOnScreen!();
}
}
@@ -3805,7 +3805,8 @@
/// which of them should be merged upwards into the parent SemanticsNode.
///
/// The input list of [SemanticsConfiguration]s can be empty if the rendering
- /// object of this semantics configuration is a leaf node.
+ /// object of this semantics configuration is a leaf node or child rendering
+ /// objects do not contribute to the semantics.
ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {
diff --git a/framework/lib/src/services/asset_manifest.dart b/framework/lib/src/services/asset_manifest.dart
index 879a17c..4f76ae9 100644
--- a/framework/lib/src/services/asset_manifest.dart
+++ b/framework/lib/src/services/asset_manifest.dart
@@ -30,14 +30,12 @@
/// information.
List<String> listAssets();
- /// Retrieves metadata about an asset and its variants.
+ /// Retrieves metadata about an asset and its variants. Returns null if the
+ /// key was not found in the asset manifest.
///
- /// Note that this method considers a main asset to be a variant of itself and
+ /// This method considers a main asset to be a variant of itself and
/// includes it in the returned list.
- ///
- /// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
- /// avoid this, use a key obtained from the [listAssets] method.
- List<AssetMetadata> getAssetVariants(String key);
+ List<AssetMetadata>? getAssetVariants(String key);
}
// Lazily parses the binary asset manifest into a data structure that's easier to work
@@ -64,14 +62,14 @@
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
@override
- List<AssetMetadata> getAssetVariants(String key) {
+ List<AssetMetadata>? getAssetVariants(String key) {
// We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests. This is important to keep an app's first asset
// load fast.
if (!_typeCastedData.containsKey(key)) {
final Object? variantData = _data[key];
if (variantData == null) {
- throw ArgumentError('Asset key $key was not found within the asset manifest.');
+ return null;
}
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
.cast<Map<Object?, Object?>>()
diff --git a/framework/lib/src/services/dom.dart b/framework/lib/src/services/dom.dart
index fad6f16..96a44a4 100644
--- a/framework/lib/src/services/dom.dart
+++ b/framework/lib/src/services/dom.dart
@@ -115,7 +115,7 @@
@staticInterop
class DomEvent {}
-/// [DomEvent] reqiured extension.
+/// [DomEvent] required extension.
extension DomEventExtension on DomEvent {
/// Get the event type.
external String get type;
@@ -134,7 +134,7 @@
@staticInterop
class DomProgressEvent extends DomEvent {}
-/// [DomProgressEvent] reqiured extension.
+/// [DomProgressEvent] required extension.
extension DomProgressEventExtension on DomProgressEvent {
/// Amount of work done.
external int? get loaded;
@@ -182,7 +182,7 @@
@JS('window.document')
external DomDocument get domDocument;
-/// Cretaes a new DOM event.
+/// Creates a new DOM event.
DomEvent createDomEvent(String type, String name) {
final DomEvent event = domDocument.createEvent(type);
event.initEvent(name, true, true);
@@ -336,8 +336,11 @@
/// [DomCSSStyleSheet]'s required extension.
extension DomCSSStyleSheetExtension on DomCSSStyleSheet {
/// Inserts a rule into this style sheet.
- int insertRule(String rule, [int? index]) => js_util
- .callMethod(this, 'insertRule', <Object>[rule, if (index != null) index]);
+ int insertRule(String rule, [int? index]) =>
+ js_util.callMethod<double>(this, 'insertRule', <Object>[
+ rule,
+ if (index != null) index.toDouble()
+ ]).toInt();
}
/// A list of token.
diff --git a/framework/lib/src/services/hardware_keyboard.dart b/framework/lib/src/services/hardware_keyboard.dart
index 7fcbe1a..67cc896 100644
--- a/framework/lib/src/services/hardware_keyboard.dart
+++ b/framework/lib/src/services/hardware_keyboard.dart
@@ -864,7 +864,7 @@
return false;
case KeyDataTransitMode.keyDataThenRawKeyData:
// Having 0 as the physical and logical ID indicates an empty key data
- // (the only occassion either field can be 0,) transmitted to ensure
+ // (the only occasion either field can be 0,) transmitted to ensure
// that the transit mode is correctly inferred. These events should be
// ignored.
if (data.physical == 0 && data.logical == 0) {
diff --git a/framework/lib/src/services/platform_views.dart b/framework/lib/src/services/platform_views.dart
index 7825904..3c2e338 100644
--- a/framework/lib/src/services/platform_views.dart
+++ b/framework/lib/src/services/platform_views.dart
@@ -44,7 +44,16 @@
///
/// Typically a platform view identifier is passed to a platform view widget
/// which creates the platform view and manages its lifecycle.
- int getNextPlatformViewId() => _nextPlatformViewId++;
+ int getNextPlatformViewId() {
+ // On the Android side, the interface exposed to users uses 32-bit integers.
+ // See https://github.com/flutter/engine/pull/39476 for more details.
+
+ // We can safely assume that a Flutter application will not require more
+ // than MAX_INT32 platform views during its lifetime.
+ const int MAX_INT32 = 0x7FFFFFFF;
+ assert(_nextPlatformViewId <= MAX_INT32);
+ return _nextPlatformViewId++;
+ }
}
/// Callback signature for when a platform view was created.
diff --git a/framework/lib/src/services/raw_keyboard.dart b/framework/lib/src/services/raw_keyboard.dart
index 842af59..4cd8314 100644
--- a/framework/lib/src/services/raw_keyboard.dart
+++ b/framework/lib/src/services/raw_keyboard.dart
@@ -432,10 +432,10 @@
/// Returns true if a ALT modifier key is pressed, regardless of which side
/// of the keyboard it is on.
///
- /// Note that the ALTGR key that appears on some keyboards is considered to be
+ /// The `AltGr` key that appears on some keyboards is considered to be
/// the same as [LogicalKeyboardKey.altRight] on some platforms (notably
/// Android). On platforms that can distinguish between `altRight` and
- /// `altGr`, a press of `altGr` will not return true here, and will need to be
+ /// `altGr`, a press of `AltGr` will not return true here, and will need to be
/// tested for separately.
///
/// Use [isKeyPressed] if you need to know which alt key was pressed.
@@ -864,7 +864,7 @@
// exist in the modifier list. Enforce the pressing state.
if (event is RawKeyDownEvent && thisKeyModifier != null
&& !_keysPressed.containsKey(event.physicalKey)) {
- // This inconsistancy is found on Linux GTK for AltRight:
+ // This inconsistency is found on Linux GTK for AltRight:
// https://github.com/flutter/flutter/issues/93278
// And also on Android and iOS:
// https://github.com/flutter/flutter/issues/101090
diff --git a/framework/lib/src/services/system_chrome.dart b/framework/lib/src/services/system_chrome.dart
index 6b0ed70..ad42357 100644
--- a/framework/lib/src/services/system_chrome.dart
+++ b/framework/lib/src/services/system_chrome.dart
@@ -531,9 +531,9 @@
/// to configure the system styles when an app bar is not used. When an app
/// bar is used, apps should not enclose the app bar in an annotated region
/// because one is automatically created. If an app bar is used and the app
- /// bar is enclosed in an annotated region, the app bar overlay style supercedes
+ /// bar is enclosed in an annotated region, the app bar overlay style supersedes
/// the status bar properties defined in the enclosing annotated region overlay
- /// style and the enclosing annotated region overlay style supercedes the app bar
+ /// style and the enclosing annotated region overlay style supersedes the app bar
/// overlay style navigation bar properties.
///
/// {@tool sample}
diff --git a/framework/lib/src/services/text_boundary.dart b/framework/lib/src/services/text_boundary.dart
index 4e2414e..ff6f318 100644
--- a/framework/lib/src/services/text_boundary.dart
+++ b/framework/lib/src/services/text_boundary.dart
@@ -59,7 +59,7 @@
}
}
-/// A [TextBoundary] subclass for retriving the range of the grapheme the given
+/// A [TextBoundary] subclass for retrieving the range of the grapheme the given
/// `position` is in.
///
/// The class is implemented using the
diff --git a/framework/lib/src/services/text_editing_delta.dart b/framework/lib/src/services/text_editing_delta.dart
index 91872c6..8ca66b6 100644
--- a/framework/lib/src/services/text_editing_delta.dart
+++ b/framework/lib/src/services/text_editing_delta.dart
@@ -277,7 +277,7 @@
@override
TextEditingValue apply(TextEditingValue value) {
// To stay inline with the plain text model we should follow a last write wins
- // policy and apply the delta to the oldText. This is due to the asyncronous
+ // policy and apply the delta to the oldText. This is due to the asynchronous
// nature of the connection between the framework and platform text input plugins.
String newText = oldText;
assert(_debugTextRangeIsValid(TextRange.collapsed(insertionOffset), newText), 'Applying TextEditingDeltaInsertion failed, the insertionOffset: $insertionOffset is not within the bounds of $newText of length: ${newText.length}');
@@ -323,7 +323,7 @@
@override
TextEditingValue apply(TextEditingValue value) {
// To stay inline with the plain text model we should follow a last write wins
- // policy and apply the delta to the oldText. This is due to the asyncronous
+ // policy and apply the delta to the oldText. This is due to the asynchronous
// nature of the connection between the framework and platform text input plugins.
String newText = oldText;
assert(_debugTextRangeIsValid(deletedRange, newText), 'Applying TextEditingDeltaDeletion failed, the deletedRange: $deletedRange is not within the bounds of $newText of length: ${newText.length}');
@@ -379,7 +379,7 @@
@override
TextEditingValue apply(TextEditingValue value) {
// To stay inline with the plain text model we should follow a last write wins
- // policy and apply the delta to the oldText. This is due to the asyncronous
+ // policy and apply the delta to the oldText. This is due to the asynchronous
// nature of the connection between the framework and platform text input plugins.
String newText = oldText;
assert(_debugTextRangeIsValid(replacedRange, newText), 'Applying TextEditingDeltaReplacement failed, the replacedRange: $replacedRange is not within the bounds of $newText of length: ${newText.length}');
@@ -425,7 +425,7 @@
@override
TextEditingValue apply(TextEditingValue value) {
// To stay inline with the plain text model we should follow a last write wins
- // policy and apply the delta to the oldText. This is due to the asyncronous
+ // policy and apply the delta to the oldText. This is due to the asynchronous
// nature of the connection between the framework and platform text input plugins.
assert(_debugTextRangeIsValid(selection, oldText), 'Applying TextEditingDeltaNonTextUpdate failed, the selection range: $selection is not within the bounds of $oldText of length: ${oldText.length}');
assert(_debugTextRangeIsValid(composing, oldText), 'Applying TextEditingDeltaNonTextUpdate failed, the composing region: $composing is not within the bounds of $oldText of length: ${oldText.length}');
diff --git a/framework/lib/src/widgets/app.dart b/framework/lib/src/widgets/app.dart
index a5f6449..09880c2 100644
--- a/framework/lib/src/widgets/app.dart
+++ b/framework/lib/src/widgets/app.dart
@@ -252,8 +252,6 @@
/// without an explicit style.
/// * [MediaQuery], which establishes a subtree in which media queries resolve
/// to a [MediaQueryData].
-/// * [MediaQuery.fromWindow], which builds a [MediaQuery] with data derived
-/// from [WidgetsBinding.window].
/// * [Localizations], which defines the [Locale] for its `child`.
/// * [Title], a widget that describes this app in the operating system.
/// * [Navigator], a widget that manages a set of child widgets with a stack
diff --git a/framework/lib/src/widgets/basic.dart b/framework/lib/src/widgets/basic.dart
index 29c0181..d282ff6 100644
--- a/framework/lib/src/widgets/basic.dart
+++ b/framework/lib/src/widgets/basic.dart
@@ -6811,7 +6811,7 @@
/// A widget that annotates the widget tree with a description of the meaning of
/// the widgets.
///
-/// Used by assitive technologies, search engines, and other semantic analysis
+/// Used by assistive technologies, search engines, and other semantic analysis
/// software to determine the meaning of the application.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=NvtMt_DtFrQ}
diff --git a/framework/lib/src/widgets/binding.dart b/framework/lib/src/widgets/binding.dart
index 7633f50..f1e3846 100644
--- a/framework/lib/src/widgets/binding.dart
+++ b/framework/lib/src/widgets/binding.dart
@@ -260,7 +260,6 @@
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;
platformDispatcher.onLocaleChanged = handleLocaleChanged;
- platformDispatcher.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
assert(() {
FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);
@@ -902,15 +901,13 @@
/// Used by [runApp] to wrap the provided `rootWidget` in the default [View].
///
/// The [View] determines into what [FlutterView] the app is rendered into.
- /// For backwards-compatibility reasons, this method currently chooses
- /// [window] (which is a [FlutterView]) as the rendering target. This will
- /// change in a future version of Flutter.
+ /// This is currently [PlatformDispatcher.implicitView] from [platformDispatcher].
///
/// The `rootWidget` widget provided to this method must not already be
/// wrapped in a [View].
Widget wrapWithDefaultView(Widget rootWidget) {
return View(
- view: window,
+ view: platformDispatcher.implicitView!,
child: rootWidget,
);
}
diff --git a/framework/lib/src/widgets/default_text_editing_shortcuts.dart b/framework/lib/src/widgets/default_text_editing_shortcuts.dart
index edee66f..19e9d97 100644
--- a/framework/lib/src/widgets/default_text_editing_shortcuts.dart
+++ b/framework/lib/src/widgets/default_text_editing_shortcuts.dart
@@ -34,7 +34,7 @@
/// ```dart
/// @override
/// Widget build(BuildContext context) {
-/// // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
+/// // If using WidgetsApp or its descendants MaterialApp or CupertinoApp,
/// // then DefaultTextEditingShortcuts is already being inserted into the
/// // widget tree.
/// return const DefaultTextEditingShortcuts(
@@ -89,7 +89,7 @@
///
/// @override
/// Widget build(BuildContext context) {
-/// // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
+/// // If using WidgetsApp or its descendants MaterialApp or CupertinoApp,
/// // then DefaultTextEditingShortcuts is already being inserted into the
/// // widget tree.
/// return DefaultTextEditingShortcuts(
diff --git a/framework/lib/src/widgets/drag_target.dart b/framework/lib/src/widgets/drag_target.dart
index 950d30a..1d23dae 100644
--- a/framework/lib/src/widgets/drag_target.dart
+++ b/framework/lib/src/widgets/drag_target.dart
@@ -648,7 +648,7 @@
/// Called when a [Draggable] moves within this [DragTarget].
///
- /// Note that this includes entering and leaving the target.
+ /// This includes entering and leaving the target.
final DragTargetMove<T>? onMove;
/// How to behave during hit testing.
diff --git a/framework/lib/src/widgets/draggable_scrollable_sheet.dart b/framework/lib/src/widgets/draggable_scrollable_sheet.dart
index 090fa0e..dc27024 100644
--- a/framework/lib/src/widgets/draggable_scrollable_sheet.dart
+++ b/framework/lib/src/widgets/draggable_scrollable_sheet.dart
@@ -531,8 +531,8 @@
/// Start an activity that affects the sheet and register a cancel call back
/// that will be called if another activity starts.
///
- /// Note that `onCanceled` will get called even if the subsequent activity
- /// started after this one finished so `onCanceled` should be safe to call at
+ /// The `onCanceled` callback will get called even if the subsequent activity
+ /// started after this one finished, so `onCanceled` must be safe to call at
/// any time.
void startActivity({required VoidCallback onCanceled}) {
_cancelActivity?.call();
diff --git a/framework/lib/src/widgets/editable_text.dart b/framework/lib/src/widgets/editable_text.dart
index 02093ae..a239d99 100644
--- a/framework/lib/src/widgets/editable_text.dart
+++ b/framework/lib/src/widgets/editable_text.dart
@@ -2368,7 +2368,7 @@
SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) {
if (!_spellCheckResultsReceived
|| spellCheckResults!.suggestionSpans.last.range.end < cursorIndex) {
- // No spell check results have been recieved or the cursor index is out
+ // No spell check results have been received or the cursor index is out
// of range that suggestionSpans covers.
return null;
}
@@ -2533,7 +2533,7 @@
///
/// * [EditableText.getEditableButtonItems], which performs a similar role,
/// but for any editable field, not just specifically EditableText.
- /// * [SelectableRegionState.contextMenuButtonItems], which peforms a similar
+ /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar
/// role but for content that is selectable but not editable.
/// * [contextMenuAnchors], which provides the anchor points for the default
/// context menu.
@@ -2605,7 +2605,7 @@
if (_tickersEnabled && _cursorActive) {
_startCursorBlink();
} else if (!_tickersEnabled && _cursorTimer != null) {
- // Cannot use _stopCursorTimer because it would reset _cursorActive.
+ // Cannot use _stopCursorBlink because it would reset _cursorActive.
_cursorTimer!.cancel();
_cursorTimer = null;
}
@@ -2789,7 +2789,7 @@
}
// Wherever the value is changed by the user, schedule a showCaretOnScreen
- // to make sure the user can see the changes they just made. Programmatical
+ // to make sure the user can see the changes they just made. Programmatic
// changes to `textEditingValue` do not trigger the behavior even if the
// text field is focused.
_scheduleShowCaretOnScreen(withAnimation: true);
@@ -3557,6 +3557,8 @@
}
}
+ final TextSelection oldTextSelection = textEditingValue.selection;
+
// Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit();
@@ -3570,6 +3572,7 @@
(cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.keyboard))) {
_handleSelectionChanged(_value.selection, cause);
+ _bringIntoViewBySelectionState(oldTextSelection, value.selection, cause);
}
final String currentText = _value.text;
if (oldValue.text != currentText) {
@@ -3587,6 +3590,30 @@
endBatchEdit();
}
+ void _bringIntoViewBySelectionState(TextSelection oldSelection, TextSelection newSelection, SelectionChangedCause? cause) {
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ if (cause == SelectionChangedCause.longPress ||
+ cause == SelectionChangedCause.drag) {
+ bringIntoView(newSelection.extent);
+ }
+ break;
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.android:
+ if (cause == SelectionChangedCause.drag) {
+ if (oldSelection.baseOffset != newSelection.baseOffset) {
+ bringIntoView(newSelection.base);
+ } else if (oldSelection.extentOffset != newSelection.extentOffset) {
+ bringIntoView(newSelection.extent);
+ }
+ }
+ break;
+ }
+ }
+
void _onCursorColorTick() {
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
@@ -4565,14 +4592,14 @@
String text = _value.text;
text = widget.obscuringCharacter * text.length;
// Reveal the latest character in an obscured field only on mobile.
- // Newer verions of iOS (iOS 15+) no longer reveal the most recently
+ // Newer versions of iOS (iOS 15+) no longer reveal the most recently
// entered character.
const Set<TargetPlatform> mobilePlatforms = <TargetPlatform> {
TargetPlatform.android, TargetPlatform.fuchsia,
};
- final bool breiflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
+ final bool brieflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
&& mobilePlatforms.contains(defaultTargetPlatform);
- if (breiflyShowPassword) {
+ if (brieflyShowPassword) {
final int? o = _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
if (o != null && o >= 0 && o < text.length) {
text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
@@ -5285,6 +5312,10 @@
late final _Throttled<TextEditingValue> _throttledPush;
Timer? _throttleTimer;
+ // This is used to prevent a reentrant call to the history (a call to _undo or _redo
+ // should not call _push to add a new entry in the history).
+ bool _locked = false;
+
// This duration was chosen as a best fit for the behavior of Mac, Linux,
// and Windows undo/redo state save durations, but it is not perfect for any
// of them.
@@ -5305,13 +5336,19 @@
if (nextValue.text == widget.controller.text) {
return;
}
+ _locked = true;
widget.onTriggered(widget.controller.value.copyWith(
text: nextValue.text,
selection: nextValue.selection,
));
+ _locked = false;
}
void _push() {
+ // Do not try to push a new state when the change is related to an undo or redo.
+ if (_locked) {
+ return;
+ }
if (widget.controller.value == TextEditingValue.empty) {
return;
}
@@ -5385,8 +5422,8 @@
final List<T> _list = <T>[];
- // The index of the current value, or null if the list is emtpy.
- late int _index;
+ // The index of the current value, or -1 if the list is empty.
+ int _index = -1;
/// Returns the current value of the stack.
T? get currentValue => _list.isEmpty ? null : _list[_index];
diff --git a/framework/lib/src/widgets/focus_manager.dart b/framework/lib/src/widgets/focus_manager.dart
index a0226ea..dc264ad 100644
--- a/framework/lib/src/widgets/focus_manager.dart
+++ b/framework/lib/src/widgets/focus_manager.dart
@@ -1082,7 +1082,7 @@
_doRequestFocus(findFirstFocus: true);
}
- // Note that this is overridden in FocusScopeNode.
+ // This is overridden in FocusScopeNode.
void _doRequestFocus({required bool findFirstFocus}) {
if (!canRequestFocus) {
assert(_focusDebug(() => 'Node NOT requesting focus because canRequestFocus is false: $this'));
diff --git a/framework/lib/src/widgets/focus_scope.dart b/framework/lib/src/widgets/focus_scope.dart
index 81bcb45..84668c5 100644
--- a/framework/lib/src/widgets/focus_scope.dart
+++ b/framework/lib/src/widgets/focus_scope.dart
@@ -319,7 +319,7 @@
/// {@template flutter.widgets.Focus.descendantsAreTraversable}
/// If false, will make this widget's descendants untraversable.
///
- /// Defaults to true. Does not affect traversablility of this node (just its
+ /// Defaults to true. Does not affect traversability of this node (just its
/// descendants): for that, use [FocusNode.skipTraversal].
///
/// Does not affect the value of [FocusNode.skipTraversal] on the
@@ -370,21 +370,17 @@
/// given [BuildContext].
///
/// If no [Focus] node is found before reaching the nearest [FocusScope]
- /// widget, or there is no [Focus] widget in scope, then this method will
- /// throw an exception.
+ /// widget, or there is no [Focus] widget in the context, then this method
+ /// will throw an exception.
///
- /// The `context` and `scopeOk` arguments must not be null.
- ///
- /// Calling this function creates a dependency that will rebuild the given
- /// context when the focus changes.
+ /// {@macro flutter.widgets.focus_scope.Focus.maybeOf}
///
/// See also:
///
- /// * [maybeOf], which is similar to this function, but will return null
- /// instead of throwing if it doesn't find a [Focus] node.
- static FocusNode of(BuildContext context, { bool scopeOk = false }) {
- final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
- final FocusNode? node = marker?.notifier;
+ /// * [maybeOf], which is similar to this function, but will return null
+ /// instead of throwing if it doesn't find a [Focus] node.
+ static FocusNode of(BuildContext context, { bool scopeOk = false, bool createDependency = true }) {
+ final FocusNode? node = Focus.maybeOf(context, scopeOk: scopeOk, createDependency: createDependency);
assert(() {
if (node == null) {
throw FlutterError(
@@ -423,18 +419,24 @@
/// widget, or there is no [Focus] widget in scope, then this method will
/// return null.
///
- /// The `context` and `scopeOk` arguments must not be null.
- ///
- /// Calling this function creates a dependency that will rebuild the given
- /// context when the focus changes.
+ /// {@template flutter.widgets.focus_scope.Focus.maybeOf}
+ /// If `createDependency` is true (which is the default), calling this
+ /// function creates a dependency that will rebuild the given context when the
+ /// focus node gains or loses focus.
+ /// {@endtemplate}
///
/// See also:
///
- /// * [of], which is similar to this function, but will throw an exception if
- /// it doesn't find a [Focus] node instead of returning null.
- static FocusNode? maybeOf(BuildContext context, { bool scopeOk = false }) {
- final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
- final FocusNode? node = marker?.notifier;
+ /// * [of], which is similar to this function, but will throw an exception if
+ /// it doesn't find a [Focus] node, instead of returning null.
+ static FocusNode? maybeOf(BuildContext context, { bool scopeOk = false, bool createDependency = true }) {
+ final _FocusInheritedScope? scope;
+ if (createDependency) {
+ scope = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
+ } else {
+ scope = context.getInheritedWidgetOfExactType<_FocusInheritedScope>();
+ }
+ final FocusNode? node = scope?.notifier;
if (node == null) {
return null;
}
@@ -776,16 +778,16 @@
ValueChanged<bool>? onFocusChange,
}) = _FocusScopeWithExternalFocusNode;
- /// Returns the [FocusScopeNode] of the [FocusScope] that most tightly
- /// encloses the given [context].
+ /// Returns the [FocusNode.nearestScope] of the [Focus] or [FocusScope] that
+ /// most tightly encloses the given [context].
///
- /// If this node doesn't have a [Focus] widget ancestor, then the
- /// [FocusManager.rootScope] is returned.
+ /// If this node doesn't have a [Focus] or [FocusScope] widget ancestor, then
+ /// the [FocusManager.rootScope] is returned.
///
- /// The [context] argument must not be null.
- static FocusScopeNode of(BuildContext context) {
- final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
- return marker?.notifier?.nearestScope ?? context.owner!.focusManager.rootScope;
+ /// {@macro flutter.widgets.focus_scope.Focus.maybeOf}
+ static FocusScopeNode of(BuildContext context, { bool createDependency = true }) {
+ return Focus.maybeOf(context, scopeOk: true, createDependency: createDependency)?.nearestScope
+ ?? context.owner!.focusManager.rootScope;
}
@override
diff --git a/framework/lib/src/widgets/focus_traversal.dart b/framework/lib/src/widgets/focus_traversal.dart
index ec3a40d..c683bf0 100644
--- a/framework/lib/src/widgets/focus_traversal.dart
+++ b/framework/lib/src/widgets/focus_traversal.dart
@@ -47,11 +47,11 @@
// sorting their contents.
class _FocusTraversalGroupInfo {
_FocusTraversalGroupInfo(
- _FocusTraversalGroupScope? marker, {
+ _FocusTraversalGroupNode? group, {
FocusTraversalPolicy? defaultPolicy,
List<FocusNode>? members,
- }) : groupNode = marker?.focusNode,
- policy = marker?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(),
+ }) : groupNode = group,
+ policy = group?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(),
members = members ?? <FocusNode>[];
final FocusNode? groupNode;
@@ -114,7 +114,7 @@
/// current [FlutterView]. For example, [NextFocusAction] invoked via keyboard
/// (typically the TAB key) would receive [KeyEventResult.skipRemainingHandlers]
/// allowing the embedder handle the shortcut. On the web, typically the
- /// control is transfered to the browser, allowing the user to reach the
+ /// control is transferred to the browser, allowing the user to reach the
/// address bar, escape an `iframe`, or focus on HTML elements other than
/// those managed by Flutter.
leaveFlutterView,
@@ -318,45 +318,43 @@
@protected
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
- _FocusTraversalGroupScope? _getMarker(BuildContext? context) {
- return context?.getElementForInheritedWidgetOfExactType<_FocusTraversalGroupScope>()?.widget as _FocusTraversalGroupScope?;
- }
-
- // Sort all descendants, taking into account the FocusTraversalGroup
- // that they are each in, and filtering out non-traversable/focusable nodes.
- List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
- final _FocusTraversalGroupScope? scopeGroupMarker = _getMarker(scope.context);
- final FocusTraversalPolicy defaultPolicy = scopeGroupMarker?.policy ?? ReadingOrderTraversalPolicy();
- // Build the sorting data structure, separating descendants into groups.
+ Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode) {
+ final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
for (final FocusNode node in scope.descendants) {
- final _FocusTraversalGroupScope? groupMarker = _getMarker(node.context);
- final FocusNode? groupNode = groupMarker?.focusNode;
+ final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
// Group nodes need to be added to their parent's node, or to the "null"
// node if no parent is found. This creates the hierarchy of group nodes
// and makes it so the entire group is sorted along with the other members
// of the parent group.
if (node == groupNode) {
// To find the parent of the group node, we need to skip over the parent
- // of the Focus node in _FocusTraversalGroupState.build, and start
- // looking with that node's parent, since _getMarker will return the
- // context it was called on if it matches the type.
- final BuildContext? parentContext = _getAncestor(groupNode!.context!, count: 2);
- final _FocusTraversalGroupScope? parentMarker = _getMarker(parentContext);
- final FocusNode? parentNode = parentMarker?.focusNode;
- groups[parentNode] ??= _FocusTraversalGroupInfo(parentMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
- assert(!groups[parentNode]!.members.contains(node));
- groups[parentNode]!.members.add(groupNode);
+ // of the Focus node added in _FocusTraversalGroupState.build, and start
+ // looking with that node's parent, since _getGroupNode will return the
+ // node it was called on if it matches the type.
+ final _FocusTraversalGroupNode? parentGroup = FocusTraversalGroup._getGroupNode(groupNode!.parent!);
+ groups[parentGroup] ??= _FocusTraversalGroupInfo(parentGroup, members: <FocusNode>[], defaultPolicy: defaultPolicy);
+ assert(!groups[parentGroup]!.members.contains(node));
+ groups[parentGroup]!.members.add(groupNode);
continue;
}
// Skip non-focusable and non-traversable nodes in the same way that
// FocusScopeNode.traversalDescendants would.
if (node.canRequestFocus && !node.skipTraversal) {
- groups[groupNode] ??= _FocusTraversalGroupInfo(groupMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
+ groups[groupNode] ??= _FocusTraversalGroupInfo(groupNode, members: <FocusNode>[], defaultPolicy: defaultPolicy);
assert(!groups[groupNode]!.members.contains(node));
groups[groupNode]!.members.add(node);
}
}
+ return groups;
+ }
+
+ // Sort all descendants, taking into account the FocusTraversalGroup
+ // that they are each in, and filtering out non-traversable/focusable nodes.
+ List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
+ final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
+ // Build the sorting data structure, separating descendants into groups.
+ final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode);
// Sort the member lists using the individual policy sorts.
for (final FocusNode? key in groups.keys) {
@@ -381,8 +379,8 @@
}
// Visit the children of the scope, if any.
- if (groups.isNotEmpty && groups.containsKey(scopeGroupMarker?.focusNode)) {
- visitGroups(groups[scopeGroupMarker?.focusNode]!);
+ if (groups.isNotEmpty && groups.containsKey(scopeGroupNode)) {
+ visitGroups(groups[scopeGroupNode]!);
}
// Remove the FocusTraversalGroup nodes themselves, which aren't focusable.
@@ -941,7 +939,7 @@
// Find the directionality in force for a build context without creating a
// dependency.
static TextDirection? _findDirectionality(BuildContext context) {
- return (context.getElementForInheritedWidgetOfExactType<Directionality>()?.widget as Directionality?)?.textDirection;
+ return context.getInheritedWidgetOfExactType<Directionality>()?.textDirection;
}
/// Finds the common Directional ancestor of an entire list of groups.
@@ -1439,7 +1437,7 @@
/// If no [FocusTraversalOrder] ancestor exists, or the order is null, this
/// will assert in debug mode, and throw an exception in release mode.
static FocusOrder of(BuildContext context) {
- final FocusTraversalOrder? marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder?;
+ final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>();
assert(() {
if (marker == null) {
throw FlutterError(
@@ -1464,7 +1462,7 @@
///
/// If no [FocusTraversalOrder] ancestor exists, or the order is null, returns null.
static FocusOrder? maybeOf(BuildContext context) {
- final FocusTraversalOrder? marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder?;
+ final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>();
return marker?.order;
}
@@ -1556,56 +1554,107 @@
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
- /// Returns the focus policy set by the [FocusTraversalGroup] that most
- /// tightly encloses the given [BuildContext].
+ /// Returns the [FocusTraversalPolicy] that applies to the nearest ancestor of
+ /// the given [FocusNode].
///
- /// It does not create a rebuild dependency because changing the traversal
- /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
- /// result of an order change.
+ /// Will return null if no [FocusTraversalPolicy] ancestor applies to the
+ /// given [FocusNode].
///
- /// Will assert if no [FocusTraversalGroup] ancestor is found.
+ /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup]
+ /// into the widget tree, which will associate a policy with the focus tree
+ /// under the nearest ancestor [Focus] widget.
+ ///
+ /// This function differs from [maybeOf] in that it takes a [FocusNode] and
+ /// only traverses the focus tree to determine the policy in effect. Unlike
+ /// this function, the [maybeOf] function takes a [BuildContext] and first
+ /// walks up the widget tree to find the nearest ancestor [Focus] or
+ /// [FocusScope] widget, and then calls this function with the focus node
+ /// associated with that widget to determine the policy in effect.
+ static FocusTraversalPolicy? maybeOfNode(FocusNode node) {
+ return _getGroupNode(node)?.policy;
+ }
+
+ static _FocusTraversalGroupNode? _getGroupNode(FocusNode node) {
+ while (node.parent != null) {
+ if (node.context == null) {
+ return null;
+ }
+ if (node is _FocusTraversalGroupNode) {
+ return node;
+ }
+ node = node.parent!;
+ }
+ return null;
+ }
+
+ /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the
+ /// nearest ancestor [Focus] widget, given a [BuildContext].
+ ///
+ /// Will throw a [FlutterError] in debug mode, and throw a null check
+ /// exception in release mode, if no [Focus] ancestor is found, or if no
+ /// [FocusTraversalPolicy] applies to the associated [FocusNode].
+ ///
+ /// {@template flutter.widgets.focus_traversal.FocusTraversalGroup.of}
+ /// This function looks up the nearest ancestor [Focus] (or [FocusScope])
+ /// widget, and uses its [FocusNode] (or [FocusScopeNode]) to walk up the
+ /// focus tree to find the applicable [FocusTraversalPolicy] for that node.
+ ///
+ /// Calling this function does not create a rebuild dependency because
+ /// changing the traversal order doesn't change the widget tree, so nothing
+ /// needs to be rebuilt as a result of an order change.
+ ///
+ /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup]
+ /// into the widget tree, which will associate a policy with the focus tree
+ /// under the nearest ancestor [Focus] widget.
+ /// {@endtemplate}
///
/// See also:
///
- /// * [maybeOf] for a similar function that will return null if no
- /// [FocusTraversalGroup] ancestor is found.
+ /// * [maybeOf] for a similar function that will return null if no
+ /// [FocusTraversalGroup] ancestor is found.
+ /// * [maybeOfNode] for a function that will look for a policy using a given
+ /// [FocusNode], and return null if no policy applies.
static FocusTraversalPolicy of(BuildContext context) {
- final _FocusTraversalGroupScope? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupScope>();
+ final FocusTraversalPolicy? policy = maybeOf(context);
assert(() {
- if (inherited == null) {
+ if (policy == null) {
throw FlutterError(
- 'Unable to find a FocusTraversalGroup widget in the context.\n'
+ 'Unable to find a Focus or FocusScope widget in the given context, or the FocusNode '
+ 'from with the widget that was found is not associated with a FocusTraversalPolicy.\n'
'FocusTraversalGroup.of() was called with a context that does not contain a '
- 'FocusTraversalGroup.\n'
- 'No FocusTraversalGroup ancestor could be found starting from the context that was '
- 'passed to FocusTraversalGroup.of(). This can happen because there is not a '
- 'WidgetsApp or MaterialApp widget (those widgets introduce a FocusTraversalGroup), '
- 'or it can happen if the context comes from a widget above those widgets.\n'
+ 'Focus or FocusScope widget, or there was no FocusTraversalPolicy in effect.\n'
+ 'This can happen if there is not a FocusTraversalGroup that defines the policy, '
+ 'or if the context comes from a widget that is above the WidgetsApp, MaterialApp, '
+ 'or CupertinoApp widget (those widgets introduce an implicit default policy) \n'
'The context used was:\n'
' $context',
);
}
return true;
}());
- return inherited!.policy;
+ return policy!;
}
- /// Returns the focus policy set by the [FocusTraversalGroup] that most
- /// tightly encloses the given [BuildContext].
+ /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the
+ /// nearest ancestor [Focus] widget, or null, given a [BuildContext].
///
- /// It does not create a rebuild dependency because changing the traversal
- /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
- /// result of an order change.
+ /// Will return null if it doesn't find an ancestor [Focus] or [FocusScope]
+ /// widget, or doesn't find a [FocusTraversalPolicy] that applies to the node.
///
- /// Will return null if it doesn't find a [FocusTraversalGroup] ancestor.
+ /// {@macro flutter.widgets.focus_traversal.FocusTraversalGroup.of}
///
/// See also:
///
- /// * [of] for a similar function that will throw if no [FocusTraversalGroup]
- /// ancestor is found.
+ /// * [maybeOfNode] for a similar function that will look for a policy using a
+ /// given [FocusNode].
+ /// * [of] for a similar function that will throw if no [FocusTraversalPolicy]
+ /// applies.
static FocusTraversalPolicy? maybeOf(BuildContext context) {
- final _FocusTraversalGroupScope? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupScope>();
- return inherited?.policy;
+ final FocusNode? node = Focus.maybeOf(context, scopeOk: true, createDependency: false);
+ if (node == null) {
+ return null;
+ }
+ return FocusTraversalGroup.maybeOfNode(node);
}
@override
@@ -1618,21 +1667,28 @@
}
}
+// A special focus node subclass that only FocusTraversalGroup uses so that it
+// can be used to cache the policy in the focus tree, and so that the traversal
+// code can find groups in the focus tree.
+class _FocusTraversalGroupNode extends FocusNode {
+ _FocusTraversalGroupNode({
+ super.debugLabel,
+ required this.policy,
+ });
+
+ FocusTraversalPolicy policy;
+}
+
class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
// The internal focus node used to collect the children of this node into a
// group, and to provide a context for the traversal algorithm to sort the
- // group with.
- late final FocusNode focusNode;
-
- @override
- void initState() {
- super.initState();
- focusNode = FocusNode(
- canRequestFocus: false,
- skipTraversal: true,
- debugLabel: 'FocusTraversalGroup',
- );
- }
+ // group with. It's a special subclass of FocusNode just so that it can be
+ // identified when walking the focus tree during traversal, and hold the
+ // current policy.
+ late final _FocusTraversalGroupNode focusNode = _FocusTraversalGroupNode(
+ debugLabel: 'FocusTraversalGroup',
+ policy: widget.policy,
+ );
@override
void dispose() {
@@ -1641,36 +1697,25 @@
}
@override
- Widget build(BuildContext context) {
- return _FocusTraversalGroupScope(
- policy: widget.policy,
- focusNode: focusNode,
- child: Focus(
- focusNode: focusNode,
- canRequestFocus: false,
- skipTraversal: true,
- includeSemantics: false,
- descendantsAreFocusable: widget.descendantsAreFocusable,
- descendantsAreTraversable: widget.descendantsAreTraversable,
- child: widget.child,
- ),
- );
+ void didUpdateWidget (FocusTraversalGroup oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.policy != widget.policy) {
+ focusNode.policy = widget.policy;
+ }
}
-}
-
-// A "marker" inherited widget to make the group faster to find.
-class _FocusTraversalGroupScope extends InheritedWidget {
- const _FocusTraversalGroupScope({
- required this.policy,
- required this.focusNode,
- required super.child,
- });
-
- final FocusTraversalPolicy policy;
- final FocusNode focusNode;
@override
- bool updateShouldNotify(InheritedWidget oldWidget) => false;
+ Widget build(BuildContext context) {
+ return Focus(
+ focusNode: focusNode,
+ canRequestFocus: false,
+ skipTraversal: true,
+ includeSemantics: false,
+ descendantsAreFocusable: widget.descendantsAreFocusable,
+ descendantsAreTraversable: widget.descendantsAreTraversable,
+ child: widget.child,
+ );
+ }
}
/// An intent for use with the [RequestFocusAction], which supplies the
diff --git a/framework/lib/src/widgets/framework.dart b/framework/lib/src/widgets/framework.dart
index e576a78..1e6df76 100644
--- a/framework/lib/src/widgets/framework.dart
+++ b/framework/lib/src/widgets/framework.dart
@@ -2216,10 +2216,13 @@
/// be called apply to this method as well.
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect });
- /// Obtains the nearest widget of the given type `T`, which must be the type of a
- /// concrete [InheritedWidget] subclass, and registers this build context with
- /// that widget such that when that widget changes (or a new widget of that
- /// type is introduced, or the widget goes away), this build context is
+ /// Returns the nearest widget of the given type `T` and creates a dependency
+ /// on it, or null if no appropriate widget is found.
+ ///
+ /// The widget found will be a concrete [InheritedWidget] subclass, and
+ /// calling [dependOnInheritedWidgetOfExactType] registers this build context
+ /// with the returned widget. When that widget changes (or a new widget of
+ /// that type is introduced, or the widget goes away), this build context is
/// rebuilt so that it can obtain new values from that widget.
///
/// {@template flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType}
@@ -2230,8 +2233,8 @@
/// [State.initState] methods, because those methods would not get called
/// again if the inherited value were to change. To ensure that the widget
/// correctly updates itself when the inherited value changes, only call this
- /// (directly or indirectly) from build methods, layout and paint callbacks, or
- /// from [State.didChangeDependencies].
+ /// (directly or indirectly) from build methods, layout and paint callbacks,
+ /// or from [State.didChangeDependencies].
///
/// This method should not be called from [State.dispose] because the element
/// tree is no longer stable at that time. To refer to an ancestor from that
@@ -2240,8 +2243,8 @@
/// whenever the widget is removed from the tree.
///
/// It is also possible to call this method from interaction event handlers
- /// (e.g. gesture callbacks) or timers, to obtain a value once, if that value
- /// is not going to be cached and reused later.
+ /// (e.g. gesture callbacks) or timers, to obtain a value once, as long as
+ /// that value is not cached and/or reused later.
///
/// Calling this method is O(1) with a small constant factor, but will lead to
/// the widget being rebuilt more often.
@@ -2259,6 +2262,27 @@
/// {@endtemplate}
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
+ /// Returns the nearest widget of the given [InheritedWidget] subclass `T` or
+ /// null if an appropriate ancestor is not found.
+ ///
+ /// This method does not introduce a dependency the way that the more typical
+ /// [dependOnInheritedWidgetOfExactType] does, so this context will not be
+ /// rebuilt if the [InheritedWidget] changes. This function is meant for those
+ /// uncommon use cases where a dependency is undesirable.
+ ///
+ /// This method should not be called from [State.dispose] because the element
+ /// tree is no longer stable at that time. To refer to an ancestor from that
+ /// method, save a reference to the ancestor in [State.didChangeDependencies].
+ /// It is safe to use this method from [State.deactivate], which is called
+ /// whenever the widget is removed from the tree.
+ ///
+ /// It is also possible to call this method from interaction event handlers
+ /// (e.g. gesture callbacks) or timers, to obtain a value once, as long as
+ /// that value is not cached and/or reused later.
+ ///
+ /// Calling this method is O(1) with a small constant factor.
+ T? getInheritedWidgetOfExactType<T extends InheritedWidget>();
+
/// Obtains the element corresponding to the nearest widget of the given type `T`,
/// which must be the type of a concrete [InheritedWidget] subclass.
///
@@ -4111,7 +4135,7 @@
// implementation to decide whether to rebuild based on whether we had
// dependencies here.
}
- _inheritedWidgets = null;
+ _inheritedElements = null;
_lifecycleState = _ElementLifecycle.inactive;
}
@@ -4306,7 +4330,7 @@
return null;
}
- PersistentHashMap<Type, InheritedElement>? _inheritedWidgets;
+ PersistentHashMap<Type, InheritedElement>? _inheritedElements;
Set<InheritedElement>? _dependencies;
bool _hadUnsatisfiedDependencies = false;
@@ -4347,7 +4371,7 @@
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
- final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
+ final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
@@ -4356,9 +4380,14 @@
}
@override
+ T? getInheritedWidgetOfExactType<T extends InheritedWidget>() {
+ return getElementForInheritedWidgetOfExactType<T>()?.widget as T?;
+ }
+
+ @override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
- final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
+ final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
return ancestor;
}
@@ -4378,7 +4407,7 @@
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
- _inheritedWidgets = _parent?._inheritedWidgets;
+ _inheritedElements = _parent?._inheritedElements;
}
@override
@@ -4672,6 +4701,7 @@
performRebuild();
} finally {
assert(() {
+ owner!._debugElementWasRebuilt(this);
assert(owner!._debugCurrentBuildTarget == this);
owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
return true;
@@ -5374,8 +5404,8 @@
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final PersistentHashMap<Type, InheritedElement> incomingWidgets =
- _parent?._inheritedWidgets ?? const PersistentHashMap<Type, InheritedElement>.empty();
- _inheritedWidgets = incomingWidgets.put(widget.runtimeType, this);
+ _parent?._inheritedElements ?? const PersistentHashMap<Type, InheritedElement>.empty();
+ _inheritedElements = incomingWidgets.put(widget.runtimeType, this);
}
@override
@@ -5944,8 +5974,7 @@
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
- final List<Element> newChildren = oldChildren.length == newWidgets.length ?
- oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance);
+ final List<Element> newChildren = List<Element>.filled(newWidgets.length, _NullElement.instance);
Element? previousChild;
diff --git a/framework/lib/src/widgets/gesture_detector.dart b/framework/lib/src/widgets/gesture_detector.dart
index 21c413b..55dce62 100644
--- a/framework/lib/src/widgets/gesture_detector.dart
+++ b/framework/lib/src/widgets/gesture_detector.dart
@@ -941,7 +941,7 @@
/// force to initiate a force press. The amount of force is at least
/// [ForcePressGestureRecognizer.startPressure].
///
- /// Note that this callback will only be fired on devices with pressure
+ /// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressStartCallback? onForcePressStart;
@@ -949,7 +949,7 @@
/// force. The amount of force is at least
/// [ForcePressGestureRecognizer.peakPressure].
///
- /// Note that this callback will only be fired on devices with pressure
+ /// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressPeakCallback? onForcePressPeak;
@@ -958,13 +958,13 @@
/// plane of the screen, pressing the screen with varying forces or both
/// simultaneously.
///
- /// Note that this callback will only be fired on devices with pressure
+ /// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressUpdateCallback? onForcePressUpdate;
- /// The pointer is no longer in contact with the screen.
+ /// The pointer tracked by [onForcePressStart] is no longer in contact with the screen.
///
- /// Note that this callback will only be fired on devices with pressure
+ /// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressEndCallback? onForcePressEnd;
diff --git a/framework/lib/src/widgets/icon_theme_data.dart b/framework/lib/src/widgets/icon_theme_data.dart
index c1285bd..f0b442b 100644
--- a/framework/lib/src/widgets/icon_theme_data.dart
+++ b/framework/lib/src/widgets/icon_theme_data.dart
@@ -167,6 +167,9 @@
///
/// {@macro dart.ui.shadow.lerp}
static IconThemeData lerp(IconThemeData? a, IconThemeData? b, double t) {
+ if (identical(a, b) && a != null) {
+ return a;
+ }
return IconThemeData(
size: ui.lerpDouble(a?.size, b?.size, t),
fill: ui.lerpDouble(a?.fill, b?.fill, t),
diff --git a/framework/lib/src/widgets/implicit_animations.dart b/framework/lib/src/widgets/implicit_animations.dart
index 302e865..bf4f37b 100644
--- a/framework/lib/src/widgets/implicit_animations.dart
+++ b/framework/lib/src/widgets/implicit_animations.dart
@@ -185,7 +185,7 @@
end!.decompose(endTranslation, endRotation, endScale);
final Vector3 lerpTranslation =
beginTranslation * (1.0 - t) + endTranslation * t;
- // TODO(alangardner): Implement slerp for constant rotation
+ // TODO(alangardner): Implement lerp for constant rotation
final Quaternion lerpRotation =
(beginRotation.scaled(1.0 - t) + endRotation.scaled(t)).normalized();
final Vector3 lerpScale = beginScale * (1.0 - t) + endScale * t;
diff --git a/framework/lib/src/widgets/layout_builder.dart b/framework/lib/src/widgets/layout_builder.dart
index be1a907..eed7ce4 100644
--- a/framework/lib/src/widgets/layout_builder.dart
+++ b/framework/lib/src/widgets/layout_builder.dart
@@ -48,7 +48,7 @@
/// Called at layout time to construct the widget tree.
///
/// The builder must not return null.
- final Widget Function(BuildContext, ConstraintType) builder;
+ final Widget Function(BuildContext context, ConstraintType constraints) builder;
// updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
}
diff --git a/framework/lib/src/widgets/media_query.dart b/framework/lib/src/widgets/media_query.dart
index 7757bc9..d91181b 100644
--- a/framework/lib/src/widgets/media_query.dart
+++ b/framework/lib/src/widgets/media_query.dart
@@ -211,10 +211,10 @@
devicePixelRatio = view.devicePixelRatio,
textScaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor,
platformBrightness = platformData?.platformBrightness ?? view.platformDispatcher.platformBrightness,
- padding = EdgeInsets.fromWindowPadding(view.padding, view.devicePixelRatio),
- viewPadding = EdgeInsets.fromWindowPadding(view.viewPadding, view.devicePixelRatio),
- viewInsets = EdgeInsets.fromWindowPadding(view.viewInsets, view.devicePixelRatio),
- systemGestureInsets = EdgeInsets.fromWindowPadding(view.systemGestureInsets, view.devicePixelRatio),
+ padding = EdgeInsets.fromViewPadding(view.padding, view.devicePixelRatio),
+ viewPadding = EdgeInsets.fromViewPadding(view.viewPadding, view.devicePixelRatio),
+ viewInsets = EdgeInsets.fromViewPadding(view.viewInsets, view.devicePixelRatio),
+ systemGestureInsets = EdgeInsets.fromViewPadding(view.systemGestureInsets, view.devicePixelRatio),
accessibleNavigation = platformData?.accessibleNavigation ?? view.platformDispatcher.accessibilityFeatures.accessibleNavigation,
invertColors = platformData?.invertColors ?? view.platformDispatcher.accessibilityFeatures.invertColors,
disableAnimations = platformData?.disableAnimations ?? view.platformDispatcher.accessibilityFeatures.disableAnimations,
@@ -298,7 +298,7 @@
///
/// See also:
///
- /// * [ui.window], which provides some additional detail about this property
+ /// * [FlutterView], which provides some additional detail about this property
/// and how it relates to [padding] and [viewPadding].
final EdgeInsets viewInsets;
@@ -317,7 +317,7 @@
///
/// See also:
///
- /// * [ui.window], which provides some additional detail about this
+ /// * [FlutterView], which provides some additional detail about this
/// property and how it relates to [viewInsets] and [viewPadding].
/// * [SafeArea], a widget that consumes this padding with a [Padding] widget
/// and automatically removes it from the [MediaQuery] for its child.
@@ -341,7 +341,7 @@
///
/// See also:
///
- /// * [ui.window], which provides some additional detail about this
+ /// * [FlutterView], which provides some additional detail about this
/// property and how it relates to [padding] and [viewInsets].
final EdgeInsets viewPadding;
diff --git a/framework/lib/src/widgets/modal_barrier.dart b/framework/lib/src/widgets/modal_barrier.dart
index 9a15b11..a18a807 100644
--- a/framework/lib/src/widgets/modal_barrier.dart
+++ b/framework/lib/src/widgets/modal_barrier.dart
@@ -53,7 +53,7 @@
/// Updates the [SemanticsNode.rect] of its child based on the value inside
/// provided [ValueNotifier].
class _RenderSemanticsClipper extends RenderProxyBox {
- /// Creats a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child
+ /// Creates a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child
/// based on the value inside provided [ValueNotifier].
_RenderSemanticsClipper({
required ValueNotifier<EdgeInsets> clipDetailsNotifier,
diff --git a/framework/lib/src/widgets/overlay.dart b/framework/lib/src/widgets/overlay.dart
index f1629c9..dfb2351 100644
--- a/framework/lib/src/widgets/overlay.dart
+++ b/framework/lib/src/widgets/overlay.dart
@@ -17,6 +17,8 @@
// Examples can assume:
// late BuildContext context;
+// * OverlayEntry Implementation
+
/// A place in an [Overlay] that can contain a widget.
///
/// Overlay entries are inserted into an [Overlay] using the
@@ -127,21 +129,20 @@
/// Whether the [OverlayEntry] is currently mounted in the widget tree.
///
/// The [OverlayEntry] notifies its listeners when this value changes.
- bool get mounted => _overlayStateMounted.value;
+ bool get mounted => _overlayEntryStateNotifier.value != null;
- /// Whether the `_OverlayState`s built using this [OverlayEntry] is currently
- /// mounted.
- final ValueNotifier<bool> _overlayStateMounted = ValueNotifier<bool>(false);
+ /// The currently mounted `_OverlayEntryWidgetState` built using this [OverlayEntry].
+ final ValueNotifier<_OverlayEntryWidgetState?> _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null);
@override
void addListener(VoidCallback listener) {
assert(!_disposedByOwner);
- _overlayStateMounted.addListener(listener);
+ _overlayEntryStateNotifier.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
- _overlayStateMounted.removeListener(listener);
+ _overlayEntryStateNotifier.removeListener(listener);
}
OverlayState? _overlay;
@@ -154,9 +155,9 @@
/// This method removes this overlay entry from the overlay immediately. The
/// UI will be updated in the same frame if this method is called before the
/// overlay rebuild in this frame; otherwise, the UI will be updated in the
- /// next frame. This means that it is safe to call during builds, but also
+ /// next frame. This means that it is safe to call during builds, but also
/// that if you do call this after the overlay rebuild, the UI will not update
- /// until the next frame (i.e. many milliseconds later).
+ /// until the next frame (i.e. many milliseconds later).
void remove() {
assert(_overlay != null);
assert(!_disposedByOwner);
@@ -187,7 +188,7 @@
void _didUnmount() {
assert(!mounted);
if (_disposedByOwner) {
- _overlayStateMounted.dispose();
+ _overlayEntryStateNotifier.dispose();
}
}
@@ -210,7 +211,7 @@
assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.');
_disposedByOwner = true;
if (!mounted) {
- _overlayStateMounted.dispose();
+ _overlayEntryStateNotifier.dispose();
}
}
@@ -222,10 +223,12 @@
const _OverlayEntryWidget({
required Key key,
required this.entry,
+ required this.overlayState,
this.tickerEnabled = true,
}) : super(key: key);
final OverlayEntry entry;
+ final OverlayState overlayState;
final bool tickerEnabled;
@override
@@ -233,16 +236,102 @@
}
class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
+ late _RenderTheater _theater;
+
+ // Manages the stack of theater children whose paint order are sorted by their
+ // _zOrderIndex. The children added by OverlayPortal are added to this linked
+ // list, and they will be shown _above_ the OverlayEntry tied to this widget.
+ // The children with larger zOrderIndex values (i.e. those called `show`
+ // recently) will be painted last.
+ //
+ // This linked list is lazily created in `_add`, and the entries are added/removed
+ // via `_add`/`_remove`, called by OverlayPortals lower in the tree. `_add` or
+ // `_remove` does not cause this widget to rebuild, the linked list will be
+ // read by _RenderTheater as part of its render child model. This would ideally
+ // be in a RenderObject but there may not be RenderObjects between
+ // _RenderTheater and the render subtree OverlayEntry builds.
+ LinkedList<_OverlayEntryLocation>? _sortedTheaterSiblings;
+
+ // Worst-case O(N), N being the number of children added to the top spot in
+ // the same frame. This can be a bit expensive when there's a lot of global
+ // key reparenting in the same frame but N is usually a small number.
+ void _add(_OverlayEntryLocation child) {
+ assert(mounted);
+ final LinkedList<_OverlayEntryLocation> children = _sortedTheaterSiblings ??= LinkedList<_OverlayEntryLocation>();
+ assert(!children.contains(child));
+ _OverlayEntryLocation? insertPosition = children.isEmpty ? null : children.last;
+ while (insertPosition != null && insertPosition._zOrderIndex > child._zOrderIndex) {
+ insertPosition = insertPosition.previous;
+ }
+ if (insertPosition == null) {
+ children.addFirst(child);
+ } else {
+ insertPosition.insertAfter(child);
+ }
+ assert(children.contains(child));
+ }
+
+ void _remove(_OverlayEntryLocation child) {
+ assert(_sortedTheaterSiblings != null);
+ final bool wasInCollection = _sortedTheaterSiblings?.remove(child) ?? false;
+ assert(wasInCollection);
+ }
+
+ // Returns an Iterable that traverse the children in the child model in paint
+ // order (from farthest to the user to the closest to the user).
+ //
+ // The iterator should be safe to use even when the child model is being
+ // mutated. The reason for that is it's allowed to add/remove/move deferred
+ // children to a _RenderTheater during performLayout, but the affected
+ // children don't have to be laid out in the same performLayout call.
+ late final Iterable<RenderBox> _paintOrderIterable = _createChildIterable(reversed: false);
+ // An Iterable that traverse the children in the child model in
+ // hit-test order (from closest to the user to the farthest to the user).
+ late final Iterable<RenderBox> _hitTestOrderIterable = _createChildIterable(reversed: true);
+
+ // The following uses sync* because hit-testing is lazy, and LinkedList as a
+ // Iterable doesn't support current modification.
+ Iterable<RenderBox> _createChildIterable({ required bool reversed }) sync* {
+ final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings;
+ if (children == null || children.isEmpty) {
+ return;
+ }
+ _OverlayEntryLocation? candidate = reversed ? children.last : children.first;
+ while(candidate != null) {
+ final RenderBox? renderBox = candidate._overlayChildRenderBox;
+ candidate = reversed ? candidate.previous : candidate.next;
+ if (renderBox != null) {
+ yield renderBox;
+ }
+ }
+ }
+
@override
void initState() {
super.initState();
- widget.entry._overlayStateMounted.value = true;
+ widget.entry._overlayEntryStateNotifier.value = this;
+ _theater = context.findAncestorRenderObjectOfType<_RenderTheater>()!;
+ assert(_sortedTheaterSiblings == null);
+ }
+
+ @override
+ void didUpdateWidget(_OverlayEntryWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ // OverlayState's build method always returns a RenderObjectWidget _Theater,
+ // so it's safe to assume that state equality implies render object equality.
+ assert(oldWidget.entry == widget.entry);
+ if (oldWidget.overlayState != widget.overlayState) {
+ final _RenderTheater newTheater = context.findAncestorRenderObjectOfType<_RenderTheater>()!;
+ assert(_theater != newTheater);
+ _theater = newTheater;
+ }
}
@override
void dispose() {
- widget.entry._overlayStateMounted.value = false;
+ widget.entry._overlayEntryStateNotifier.value = null;
widget.entry._didUnmount();
+ _sortedTheaterSiblings = null;
super.dispose();
}
@@ -250,7 +339,11 @@
Widget build(BuildContext context) {
return TickerMode(
enabled: widget.tickerEnabled,
- child: widget.entry.builder(context),
+ child: _RenderTheaterMarker(
+ theater: _theater,
+ overlayEntryWidgetState: this,
+ child: widget.entry.builder(context),
+ ),
);
}
@@ -594,15 +687,15 @@
Widget build(BuildContext context) {
// This list is filled backwards and then reversed below before
// it is added to the tree.
- final List<Widget> children = <Widget>[];
+ final List<_OverlayEntryWidget> children = <_OverlayEntryWidget>[];
bool onstage = true;
int onstageCount = 0;
- for (int i = _entries.length - 1; i >= 0; i -= 1) {
- final OverlayEntry entry = _entries[i];
+ for (final OverlayEntry entry in _entries.reversed) {
if (onstage) {
onstageCount += 1;
children.add(_OverlayEntryWidget(
key: entry._key,
+ overlayState: this,
entry: entry,
));
if (entry.opaque) {
@@ -611,12 +704,13 @@
} else if (entry.maintainState) {
children.add(_OverlayEntryWidget(
key: entry._key,
+ overlayState: this,
entry: entry,
tickerEnabled: false,
));
}
}
- return _Theatre(
+ return _Theater(
skipCount: children.length - onstageCount,
clipBehavior: widget.clipBehavior,
children: children.reversed.toList(growable: false),
@@ -636,11 +730,11 @@
/// [skipCount] children.
///
/// The first [skipCount] children are considered "offstage".
-class _Theatre extends MultiChildRenderObjectWidget {
- const _Theatre({
+class _Theater extends MultiChildRenderObjectWidget {
+ const _Theater({
this.skipCount = 0,
this.clipBehavior = Clip.hardEdge,
- super.children,
+ required List<_OverlayEntryWidget> super.children,
}) : assert(skipCount >= 0),
assert(children.length >= skipCount);
@@ -649,11 +743,11 @@
final Clip clipBehavior;
@override
- _TheatreElement createElement() => _TheatreElement(this);
+ _TheaterElement createElement() => _TheaterElement(this);
@override
- _RenderTheatre createRenderObject(BuildContext context) {
- return _RenderTheatre(
+ _RenderTheater createRenderObject(BuildContext context) {
+ return _RenderTheater(
skipCount: skipCount,
textDirection: Directionality.of(context),
clipBehavior: clipBehavior,
@@ -661,7 +755,7 @@
}
@override
- void updateRenderObject(BuildContext context, _RenderTheatre renderObject) {
+ void updateRenderObject(BuildContext context, _RenderTheater renderObject) {
renderObject
..skipCount = skipCount
..textDirection = Directionality.of(context)
@@ -675,22 +769,114 @@
}
}
-class _TheatreElement extends MultiChildRenderObjectElement {
- _TheatreElement(_Theatre super.widget);
+class _TheaterElement extends MultiChildRenderObjectElement {
+ _TheaterElement(_Theater super.widget);
@override
- _RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
+ _RenderTheater get renderObject => super.renderObject as _RenderTheater;
+
+ @override
+ void insertRenderObjectChild(RenderBox child, IndexedSlot<Element?> slot) {
+ super.insertRenderObjectChild(child, slot);
+ final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
+ parentData.overlayEntry = ((widget as _Theater).children[slot.index] as _OverlayEntryWidget).entry;
+ assert(parentData.overlayEntry != null);
+ }
+
+ @override
+ void moveRenderObjectChild(RenderBox child, IndexedSlot<Element?> oldSlot, IndexedSlot<Element?> newSlot) {
+ super.moveRenderObjectChild(child, oldSlot, newSlot);
+ assert(() {
+ final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
+ return parentData.overlayEntry == ((widget as _Theater).children[newSlot.index] as _OverlayEntryWidget).entry;
+ }());
+ }
@override
void debugVisitOnstageChildren(ElementVisitor visitor) {
- final _Theatre theatre = widget as _Theatre;
- assert(children.length >= theatre.skipCount);
- children.skip(theatre.skipCount).forEach(visitor);
+ final _Theater theater = widget as _Theater;
+ assert(children.length >= theater.skipCount);
+ children.skip(theater.skipCount).forEach(visitor);
}
}
-class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> {
- _RenderTheatre({
+// A `RenderBox` that sizes itself to its parent's size, implements the stack
+// layout algorithm and renders its children in the given `theater`.
+mixin _RenderTheaterMixin on RenderBox {
+ _RenderTheater get theater;
+
+ Iterable<RenderBox> _childrenInPaintOrder();
+ Iterable<RenderBox> _childrenInHitTestOrder();
+
+ @override
+ void setupParentData(RenderBox child) {
+ if (child.parentData is! StackParentData) {
+ child.parentData = StackParentData();
+ }
+ }
+
+ @override
+ bool get sizedByParent => true;
+
+ @override
+ void performLayout() {
+ final Iterator<RenderBox> iterator = _childrenInPaintOrder().iterator;
+ // Same BoxConstraints as used by RenderStack for StackFit.expand.
+ final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(constraints.biggest);
+ final Alignment alignment = theater._resolvedAlignment;
+
+ while (iterator.moveNext()) {
+ final RenderBox child = iterator.current;
+ final StackParentData childParentData = child.parentData! as StackParentData;
+ if (!childParentData.isPositioned) {
+ child.layout(nonPositionedChildConstraints, parentUsesSize: true);
+ childParentData.offset = alignment.alongOffset(size - child.size as Offset);
+ } else {
+ assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.');
+ RenderStack.layoutPositionedChild(child, childParentData, size, alignment);
+ }
+ assert(child.parentData == childParentData);
+ }
+ }
+
+ @override
+ bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+ final Iterator<RenderBox> iterator = _childrenInHitTestOrder().iterator;
+ bool isHit = false;
+ while (!isHit && iterator.moveNext()) {
+ final RenderBox child = iterator.current;
+ final StackParentData childParentData = child.parentData! as StackParentData;
+ final RenderBox localChild = child;
+ bool childHitTest(BoxHitTestResult result, Offset position) => localChild.hitTest(result, position: position);
+ isHit = result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: childHitTest);
+ }
+ return isHit;
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ for (final RenderBox child in _childrenInPaintOrder()) {
+ final StackParentData childParentData = child.parentData! as StackParentData;
+ context.paintChild(child, childParentData.offset + offset);
+ }
+ }
+}
+
+class _TheaterParentData extends StackParentData {
+ // The OverlayEntry that directly created this child. This field is null for
+ // children that are created by an OverlayPortal.
+ OverlayEntry? overlayEntry;
+
+ // _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose
+ // method. This property is only accessed during layout, paint and hit-test so
+ // the `value!` should be safe.
+ Iterator<RenderBox>? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.iterator;
+ Iterator<RenderBox>? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._hitTestOrderIterable.iterator;
+ void visitChildrenOfOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.forEach(visitor);
+}
+
+class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData>, _RenderTheaterMixin {
+ _RenderTheater({
List<RenderBox>? children,
required TextDirection textDirection,
int skipCount = 0,
@@ -703,23 +889,52 @@
}
@override
+ _RenderTheater get theater => this;
+
+ @override
void setupParentData(RenderBox child) {
- if (child.parentData is! StackParentData) {
- child.parentData = StackParentData();
+ if (child.parentData is! _TheaterParentData) {
+ child.parentData = _TheaterParentData();
}
}
- Alignment? _resolvedAlignment;
-
- void _resolve() {
- if (_resolvedAlignment != null) {
- return;
+ @override
+ void attach(PipelineOwner owner) {
+ super.attach(owner);
+ RenderBox? child = firstChild;
+ while (child != null) {
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+ final Iterator<RenderBox>? iterator = childParentData.paintOrderIterator;
+ if (iterator != null) {
+ while(iterator.moveNext()) {
+ iterator.current.attach(owner);
+ }
+ }
+ child = childParentData.nextSibling;
}
- _resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
}
+ static void _detachChild(RenderObject child) => child.detach();
+
+ @override
+ void detach() {
+ super.detach();
+ RenderBox? child = firstChild;
+ while (child != null) {
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+ childParentData.visitChildrenOfOverlayEntry(_detachChild);
+ child = childParentData.nextSibling;
+ }
+ }
+
+ @override
+ void redepthChildren() => visitChildren(redepthChild);
+
+ Alignment? _alignmentCache;
+ Alignment get _resolvedAlignment => _alignmentCache ??= AlignmentDirectional.topStart.resolve(textDirection);
+
void _markNeedResolution() {
- _resolvedAlignment = null;
+ _alignmentCache = null;
markNeedsLayout();
}
@@ -755,6 +970,38 @@
}
}
+ // Adding/removing deferred child does not affect the layout of other children,
+ // or that of the Overlay, so there's no need to invalidate the layout of the
+ // Overlay.
+ //
+ // When _skipMarkNeedsLayout is true, markNeedsLayout does not do anything.
+ bool _skipMarkNeedsLayout = false;
+ void _addDeferredChild(_RenderDeferredLayoutBox child) {
+ assert(!_skipMarkNeedsLayout);
+ _skipMarkNeedsLayout = true;
+
+ adoptChild(child);
+ // When child has never been laid out before, mark its layout surrogate as
+ // needing layout so it's reachable via tree walk.
+ child._layoutSurrogate.markNeedsLayout();
+ _skipMarkNeedsLayout = false;
+ }
+
+ void _removeDeferredChild(_RenderDeferredLayoutBox child) {
+ assert(!_skipMarkNeedsLayout);
+ _skipMarkNeedsLayout = true;
+ dropChild(child);
+ _skipMarkNeedsLayout = false;
+ }
+
+ @override
+ void markNeedsLayout() {
+ if (_skipMarkNeedsLayout) {
+ return;
+ }
+ super.markNeedsLayout();
+ }
+
RenderBox? get _firstOnstageChild {
if (skipCount == super.childCount) {
return null;
@@ -770,8 +1017,6 @@
RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
- int get _onstageChildCount => childCount - skipCount;
-
@override
double computeMinIntrinsicWidth(double height) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
@@ -815,73 +1060,49 @@
}
@override
- bool get sizedByParent => true;
-
- @override
Size computeDryLayout(BoxConstraints constraints) {
assert(constraints.biggest.isFinite);
return constraints.biggest;
}
@override
- void performLayout() {
- if (_onstageChildCount == 0) {
- return;
- }
-
- _resolve();
- assert(_resolvedAlignment != null);
-
- // Same BoxConstraints as used by RenderStack for StackFit.expand.
- final BoxConstraints nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
-
+ // The following uses sync* because concurrent modifications should be allowed
+ // during layout.
+ Iterable<RenderBox> _childrenInPaintOrder() sync* {
RenderBox? child = _firstOnstageChild;
while (child != null) {
- final StackParentData childParentData = child.parentData! as StackParentData;
-
- if (!childParentData.isPositioned) {
- child.layout(nonPositionedConstraints, parentUsesSize: true);
- childParentData.offset = _resolvedAlignment!.alongOffset(size - child.size as Offset);
- } else {
- RenderStack.layoutPositionedChild(child, childParentData, size, _resolvedAlignment!);
+ yield child;
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+ final Iterator<RenderBox>? innerIterator = childParentData.paintOrderIterator;
+ if (innerIterator != null) {
+ while (innerIterator.moveNext()) {
+ yield innerIterator.current;
+ }
}
-
- assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
}
@override
- bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+ // The following uses sync* because hit testing should be lazy.
+ Iterable<RenderBox> _childrenInHitTestOrder() sync* {
RenderBox? child = _lastOnstageChild;
- for (int i = 0; i < _onstageChildCount; i++) {
- assert(child != null);
- final StackParentData childParentData = child!.parentData! as StackParentData;
- final bool isHit = result.addWithPaintOffset(
- offset: childParentData.offset,
- position: position,
- hitTest: (BoxHitTestResult result, Offset transformed) {
- assert(transformed == position - childParentData.offset);
- return child!.hitTest(result, position: transformed);
- },
- );
- if (isHit) {
- return true;
+ int childLeft = childCount - skipCount;
+ while (child != null) {
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+ final Iterator<RenderBox>? innerIterator = childParentData.hitTestOrderIterator;
+ if (innerIterator != null) {
+ while (innerIterator.moveNext()) {
+ yield innerIterator.current;
+ }
}
- child = childParentData.previousSibling;
+ yield child;
+ childLeft -= 1;
+ child = childLeft <= 0 ? null : childParentData.previousSibling;
}
- return false;
}
- @protected
- void paintStack(PaintingContext context, Offset offset) {
- RenderBox? child = _firstOnstageChild;
- while (child != null) {
- final StackParentData childParentData = child.parentData! as StackParentData;
- context.paintChild(child, childParentData.offset + offset);
- child = childParentData.nextSibling;
- }
- }
+ final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void paint(PaintingContext context, Offset offset) {
@@ -890,18 +1111,16 @@
needsCompositing,
offset,
Offset.zero & size,
- paintStack,
+ super.paint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
- paintStack(context, offset);
+ super.paint(context, offset);
}
}
- final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
-
@override
void dispose() {
_clipRectLayer.layer = null;
@@ -909,11 +1128,23 @@
}
@override
+ void visitChildren(RenderObjectVisitor visitor) {
+ RenderBox? child = firstChild;
+ while (child != null) {
+ visitor(child);
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+ childParentData.visitChildrenOfOverlayEntry(visitor);
+ child = childParentData.nextSibling;
+ }
+ }
+
+ @override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
RenderBox? child = _firstOnstageChild;
while (child != null) {
visitor(child);
- final StackParentData childParentData = child.parentData! as StackParentData;
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+ childParentData.visitChildrenOfOverlayEntry(visitor);
child = childParentData.nextSibling;
}
}
@@ -947,6 +1178,7 @@
RenderBox? child = firstChild;
final RenderBox? firstOnstageChild = _firstOnstageChild;
while (child != null) {
+ final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
if (child == firstOnstageChild) {
onstage = true;
count = 1;
@@ -967,7 +1199,26 @@
);
}
- final StackParentData childParentData = child.parentData! as StackParentData;
+ int subcount = 1;
+ childParentData.visitChildrenOfOverlayEntry((RenderObject renderObject) {
+ final RenderBox child = renderObject as RenderBox;
+ if (onstage) {
+ onstageChildren.add(
+ child.toDiagnosticsNode(
+ name: 'onstage $count - $subcount',
+ ),
+ );
+ } else {
+ offstageChildren.add(
+ child.toDiagnosticsNode(
+ name: 'offstage $count - $subcount',
+ style: DiagnosticsTreeStyle.offstage,
+ ),
+ );
+ }
+ subcount += 1;
+ });
+
child = childParentData.nextSibling;
count += 1;
}
@@ -984,3 +1235,876 @@
];
}
}
+
+
+// * OverlayPortal Implementation
+// OverlayPortal is inspired by the
+// [flutter_portal](https://pub.dev/packages/flutter_portal) package.
+//
+// ** RenderObject hierarchy
+// The widget works by inserting its overlay child's render subtree directly
+// under [Overlay]'s render object (_RenderTheater).
+// https://user-images.githubusercontent.com/31859944/171971838-62ed3975-4b5d-4733-a9c9-f79e263b8fcc.jpg
+//
+// To ensure the overlay child render subtree does not do layout twice, the
+// subtree must only perform layout after both its _RenderTheater and the
+// [OverlayPortal]'s render object (_RenderLayoutSurrogateProxyBox) have
+// finished layout. This is handled by _RenderDeferredLayoutBox.
+//
+// ** Z-Index of an overlay child
+// [_OverlayEntryLocation] is a (currently private) interface that allows an
+// [OverlayPortal] to insert its overlay child into a specific [Overlay], as
+// well as specifying the paint order between the overlay child and other
+// children of the _RenderTheater.
+//
+// Since [OverlayPortal] is only allowed to target ancestor [Overlay]s
+// (_RenderTheater must finish doing layout before _RenderDeferredLayoutBox),
+// the _RenderTheater should typically be acquired using an [InheritedWidget]
+// (currently, _RenderTheaterMarker) in case the [OverlayPortal] gets
+// reparented.
+
+/// A class to show, hide and bring to top an [OverlayPortal]'s overlay child
+/// in the target [Overlay].
+///
+/// A [OverlayPortalController] can only be given to at most one [OverlayPortal]
+/// at a time. When an [OverlayPortalController] is moved from one
+/// [OverlayPortal] to another, its [isShowing] state does not carry over.
+///
+/// [OverlayPortalController.show] and [OverlayPortalController.hide] can be
+/// called even before the controller is assigned to any [OverlayPortal], but
+/// they typically should not be called while the widget tree is being rebuilt.
+class OverlayPortalController {
+ /// Creates an [OverlayPortalController], optionally with a String identifier
+ /// `debugLabel`.
+ OverlayPortalController({ String? debugLabel }) : _debugLabel = debugLabel;
+
+ _OverlayPortalState? _attachTarget;
+
+ // A separate _zOrderIndex to allow `show()` or `hide()` to be called when the
+ // controller is not yet attached. Once this controller is attached,
+ // _attachTarget._zOrderIndex will be used as the source of truth, and this
+ // variable will be set to null.
+ int? _zOrderIndex;
+ final String? _debugLabel;
+
+ static int _wallTime = kIsWeb
+ ? -9007199254740992 // -2^53
+ : -1 << 63;
+
+ // Returns a unique and monotonically increasing timestamp that represents
+ // now.
+ //
+ // The value this method returns increments after each call.
+ int _now() {
+ final int now = _wallTime += 1;
+ assert(_zOrderIndex == null || _zOrderIndex! < now);
+ assert(_attachTarget?._zOrderIndex == null || _attachTarget!._zOrderIndex! < now);
+ return now;
+ }
+
+ /// Show the overlay child of the [OverlayPortal] this controller is attached
+ /// to, at the top of the target [Overlay].
+ ///
+ /// When there are more than one [OverlayPortal]s that target the same
+ /// [Overlay], the overlay child of the last [OverlayPortal] to have called
+ /// [show] appears at the top level, unobstructed.
+ ///
+ /// If [isShowing] is already true, calling this method brings the overlay
+ /// child it controls to the top.
+ ///
+ /// This method should typically not be called while the widget tree is being
+ /// rebuilt.
+ void show() {
+ final _OverlayPortalState? state = _attachTarget;
+ if (state != null) {
+ state.show(_now());
+ } else {
+ _zOrderIndex = _now();
+ }
+ }
+
+ /// Hide the [OverlayPortal]'s overlay child.
+ ///
+ /// Once hidden, the overlay child will be removed from the widget tree the
+ /// next time the widget tree rebuilds, and stateful widgets in the overlay
+ /// child may lose states as a result.
+ ///
+ /// This method should typically not be called while the widget tree is being
+ /// rebuilt.
+ void hide() {
+ final _OverlayPortalState? state = _attachTarget;
+ if (state != null) {
+ state.hide();
+ } else {
+ assert(_zOrderIndex != null);
+ _zOrderIndex = null;
+ }
+ }
+
+ /// Whether the associated [OverlayPortal] should build and show its overlay
+ /// child, using its `overlayChildBuilder`.
+ bool get isShowing {
+ final _OverlayPortalState? state = _attachTarget;
+ return state != null
+ ? state._zOrderIndex != null
+ : _zOrderIndex != null;
+ }
+
+ /// Conventience method for toggling the current [isShowing] status.
+ ///
+ /// This method should typically not be called while the widget tree is being
+ /// rebuilt.
+ void toggle() => isShowing ? hide() : show();
+
+ @override
+ String toString() {
+ final String? debugLabel = _debugLabel;
+ final String label = debugLabel == null ? '' : '($debugLabel)';
+ final String isDetached = _attachTarget != null ? '' : ' DETACHED';
+ return '${objectRuntimeType(this, 'OverlayPortalController')}$label$isDetached';
+ }
+}
+
+/// A widget that renders its overlay child on an [Overlay].
+///
+/// The overlay child is initially hidden until [OverlayPortalController.show]
+/// is called on the associated [controller]. The [OverlayPortal] uses
+/// [overlayChildBuilder] to build its overlay child and renders it on the
+/// specified [Overlay] as if it was inserted using an [OverlayEntry], while it
+/// can depend on the same set of [InheritedWidget]s (such as [Theme]) that this
+/// widget can depend on.
+///
+/// This widget requires an [Overlay] ancestor in the widget tree when its
+/// overlay child is showing.
+///
+/// When [OverlayPortalController.hide] is called, the widget built using
+/// [overlayChildBuilder] will be removed from the widget tree the next time the
+/// widget rebuilds. Stateful descendants in the overlay child subtree may lose
+/// states as a result.
+///
+/// {@tool dartpad}
+/// This example uses an [OverlayPortal] to build a tooltip that becomes visible
+/// when the user taps on the [child] widget. There's a [DefaultTextStyle] above
+/// the [OverlayPortal] controlling the [TextStyle] of both the [child] widget
+/// and the widget [overlayChildBuilder] builds, which isn't otherwise doable if
+/// the tooltip was added as an [OverlayEntry].
+///
+/// ** See code in examples/api/lib/widgets/overlay/overlay_portal.0.dart **
+/// {@end-tool}
+///
+/// ### Paint Order
+///
+/// In an [Overlay], an overlay child is painted after the [OverlayEntry]
+/// associated with its [OverlayPortal] (that is, the [OverlayEntry] closest to
+/// the [OverlayPortal] in the widget tree, which usually represents the
+/// enclosing [Route]), and before the next [OverlayEntry].
+///
+/// When an [OverlayEntry] has multiple associated [OverlayPortal]s, the paint
+/// order between their overlay children is the order in which
+/// [OverlayPortalController.show] was called. The last [OverlayPortal] to have
+/// called `show` gets to paint its overlay child in the foreground.
+///
+/// ### Differences between [OverlayPortal] and [OverlayEntry]
+///
+/// The main difference between [OverlayEntry] and [OverlayPortal] is that
+/// [OverlayEntry] builds its widget subtree as a child of the target [Overlay],
+/// while [OverlayPortal] uses [overlayChildBuilder] to build a child widget of
+/// itself. This allows [OverlayPortal]'s overlay child to depend on the same
+/// set of [InheritedWidget]s as [OverlayPortal], and it's also guaranteed that
+/// the overlay child will not outlive its [OverlayPortal].
+///
+/// On the other hand, [OverlayPortal]'s implementation is more complex. For
+/// instance, it does a bit more work than a regular widget during global key
+/// reparenting. If the content to be shown on the [Overlay] doesn't benefit
+/// from being a part of [OverlayPortal]'s subtree, consider using an
+/// [OverlayEntry] instead.
+///
+/// See also:
+///
+/// * [OverlayEntry], an alternative API for inserting widgets into an
+/// [Overlay].
+/// * [Positioned], which can be used to size and position the overlay child in
+/// relation to the target [Overlay]'s boundaries.
+/// * [CompositedTransformFollower], which can be used to position the overlay
+/// child in relation to the linked [CompositedTransformTarget] widget.
+class OverlayPortal extends StatefulWidget {
+ /// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
+ /// builds on the closest [Overlay] when [OverlayPortalController.show] is
+ /// called.
+ const OverlayPortal({
+ super.key,
+ required this.controller,
+ required this.overlayChildBuilder,
+ this.child,
+ }) : _targetRootOverlay = false;
+
+ /// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
+ /// builds on the root [Overlay] when [OverlayPortalController.show] is
+ /// called.
+ const OverlayPortal.targetsRootOverlay({
+ super.key,
+ required this.controller,
+ required this.overlayChildBuilder,
+ this.child,
+ }) : _targetRootOverlay = true;
+
+ /// The controller to show, hide and bring to top the overlay child.
+ final OverlayPortalController controller;
+
+ /// A [WidgetBuilder] used to build a widget below this widget in the tree,
+ /// that renders on the closest [Overlay].
+ ///
+ /// The said widget will only be built and shown in the closest [Overlay] once
+ /// [OverlayPortalController.show] is called on the associated [controller].
+ /// It will be painted in front of the [OverlayEntry] closest to this widget
+ /// in the widget tree (which is usually the enclosing [Route]).
+ ///
+ /// The built overlay child widget is inserted below this widget in the widget
+ /// tree, allowing it to depend on [InheritedWidget]s above it, and be
+ /// notified when the [InheritedWidget]s change.
+ ///
+ /// Unlike [child], the built overlay child can visually extend outside the
+ /// bounds of this widget without being clipped, and receive hit-test events
+ /// outside of this widget's bounds, as long as it does not extend outside of
+ /// the [Overlay] on which it is rendered.
+ final WidgetBuilder overlayChildBuilder;
+
+ /// A widget below this widget in the tree.
+ final Widget? child;
+
+ final bool _targetRootOverlay;
+
+ @override
+ State<OverlayPortal> createState() => _OverlayPortalState();
+}
+
+class _OverlayPortalState extends State<OverlayPortal> {
+ int? _zOrderIndex;
+ // The location of the overlay child within the overlay. This object will be
+ // used as the slot of the overlay child widget.
+ //
+ // The developer must call `show` to reveal the overlay so we can get a unique
+ // timestamp of the user interaction for sorting.
+ //
+ // Avoid invalidating the cache if possible, since the framework uses `==` to
+ // compare slots, and _OverlayEntryLocation can't override that operator since
+ // it's mutable.
+ bool _childModelMayHaveChanged = true;
+ _OverlayEntryLocation? _locationCache;
+ _OverlayEntryLocation _getLocation(int zOrderIndex, bool targetRootOverlay) {
+ final _OverlayEntryLocation? cachedLocation = _locationCache;
+ if (cachedLocation != null && !_childModelMayHaveChanged) {
+ assert(cachedLocation._zOrderIndex == zOrderIndex);
+ return cachedLocation;
+ }
+ _childModelMayHaveChanged = false;
+ final _RenderTheaterMarker? marker = _RenderTheaterMarker.maybeOf(context, targetRootOverlay: targetRootOverlay);
+ if (marker == null) {
+ throw FlutterError.fromParts(<DiagnosticsNode>[
+ ErrorSummary('No Overlay widget found.'),
+ ErrorDescription(
+ '${widget.runtimeType} widgets require an Overlay widget ancestor.\n'
+ 'An overlay lets widgets float on top of other widget children.',
+ ),
+ ErrorHint(
+ 'To introduce an Overlay widget, you can either directly '
+ 'include one, or use a widget that contains an Overlay itself, '
+ 'such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.',
+ ),
+ ...context.describeMissingAncestor(expectedAncestorType: Overlay),
+ ]);
+ }
+ final _OverlayEntryLocation returnValue;
+ if (cachedLocation == null) {
+ returnValue = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
+ } else if (cachedLocation._childModel != marker.overlayEntryWidgetState || cachedLocation._theater != marker.theater) {
+ cachedLocation._dispose();
+ returnValue = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
+ } else {
+ returnValue = cachedLocation;
+ }
+ assert(returnValue._zOrderIndex == zOrderIndex);
+ return _locationCache = returnValue;
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _setupController(widget.controller);
+ }
+
+ void _setupController(OverlayPortalController controller) {
+ assert(
+ controller._attachTarget == null || controller._attachTarget == this,
+ 'Failed to attach $controller to $this. It is already attached to ${controller._attachTarget}.'
+ );
+ final int? controllerZOrderIndex = controller._zOrderIndex;
+ final int? zOrderIndex = _zOrderIndex;
+ if (zOrderIndex == null || (controllerZOrderIndex != null && controllerZOrderIndex > zOrderIndex)) {
+ _zOrderIndex = controllerZOrderIndex;
+ }
+ controller._zOrderIndex = null;
+ controller._attachTarget = this;
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ _childModelMayHaveChanged = true;
+ }
+
+ @override
+ void didUpdateWidget(OverlayPortal oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ _childModelMayHaveChanged = _childModelMayHaveChanged || oldWidget._targetRootOverlay != widget._targetRootOverlay;
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller._attachTarget = null;
+ _setupController(widget.controller);
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller._attachTarget = null;
+ _locationCache?._dispose();
+ _locationCache = null;
+ super.dispose();
+ }
+
+ void show(int zOrderIndex) {
+ assert(
+ SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks,
+ '${widget.controller.runtimeType}.show() should not be called during build.'
+ );
+ setState(() { _zOrderIndex = zOrderIndex; });
+ _locationCache?._dispose();
+ _locationCache = null;
+ }
+
+ void hide() {
+ assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
+ setState(() { _zOrderIndex = null; });
+ _locationCache?._dispose();
+ _locationCache = null;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final int? zOrderIndex = _zOrderIndex;
+ if (zOrderIndex == null) {
+ return _OverlayPortal(
+ overlayLocation: null,
+ overlayChild: null,
+ child: widget.child,
+ );
+ }
+ return _OverlayPortal(
+ overlayLocation: _getLocation(zOrderIndex, widget._targetRootOverlay),
+ overlayChild: _DeferredLayout(child: Builder(builder: widget.overlayChildBuilder)),
+ child: widget.child,
+ );
+ }
+}
+
+/// A location in an [Overlay].
+///
+/// An [_OverlayEntryLocation] determines the [Overlay] the associated
+/// [OverlayPortal] should put its overlay child onto, as well as the overlay
+/// child's paint order in relation to other contents painted on the [Overlay].
+//
+// An _OverlayEntryLocation is a cursor pointing to a location in a particular
+// Overlay's child model, and provides methods to insert/remove/move a
+// _RenderDeferredLayoutBox to/from its target _theater.
+//
+// The occupant (a `RenderBox`) will be painted above the associated
+// [OverlayEntry], but below the [OverlayEntry] above that [OverlayEntry].
+//
+// Additionally, `_activate` and `_deactivate` are called when the overlay
+// child's `_OverlayPortalElement` activates/deactivates (for instance, during
+// global key reparenting).
+// `_OverlayPortalElement` removes its overlay child's render object from the
+// target `_RenderTheater` when it deactivates and puts it back on `activated`.
+// These 2 methods can be used to "hide" a child in the child model without
+// removing it, when the child is expensive/difficult to re-insert at the
+// correct location on `activated`.
+//
+// ### Equality
+//
+// An `_OverlayEntryLocation` will be used as an Element's slot. These 3 parts
+// uniquely identify a place in an overlay's child model:
+// - _theater
+// - _childModel (the OverlayEntry)
+// - _zOrderIndex
+//
+// Since it can't implement operator== (it's mutable), the same `_OverlayEntryLocation`
+// instance must not be used to represent more than one locations.
+class _OverlayEntryLocation extends LinkedListEntry<_OverlayEntryLocation> {
+ _OverlayEntryLocation(this._zOrderIndex, this._childModel, this._theater);
+
+ final int _zOrderIndex;
+ final _OverlayEntryWidgetState _childModel;
+ final _RenderTheater _theater;
+
+ _RenderDeferredLayoutBox? _overlayChildRenderBox;
+ void _addToChildModel(_RenderDeferredLayoutBox child) {
+ assert(_overlayChildRenderBox == null, 'Failed to add $child. This location ($this) is already occupied by $_overlayChildRenderBox.');
+ _overlayChildRenderBox = child;
+ _childModel._add(this);
+ _theater.markNeedsPaint();
+ _theater.markNeedsCompositingBitsUpdate();
+ _theater.markNeedsSemanticsUpdate();
+ }
+ void _removeFromChildModel(_RenderDeferredLayoutBox child) {
+ assert(child == _overlayChildRenderBox);
+ _overlayChildRenderBox = null;
+ assert(_childModel._sortedTheaterSiblings?.contains(this) ?? false);
+ _childModel._remove(this);
+ _theater.markNeedsPaint();
+ _theater.markNeedsCompositingBitsUpdate();
+ _theater.markNeedsSemanticsUpdate();
+ }
+
+ void _addChild(_RenderDeferredLayoutBox child) {
+ assert(_debugNotDisposed());
+ _addToChildModel(child);
+ _theater._addDeferredChild(child);
+ assert(child.parent == _theater);
+ }
+
+ void _removeChild(_RenderDeferredLayoutBox child) {
+ // This call is allowed even when this location is disposed.
+ _removeFromChildModel(child);
+ _theater._removeDeferredChild(child);
+ assert(child.parent == null);
+ }
+
+ void _moveChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation fromLocation) {
+ assert(fromLocation != this);
+ assert(_debugNotDisposed());
+ final _RenderTheater fromTheater = fromLocation._theater;
+ final _OverlayEntryWidgetState fromModel = fromLocation._childModel;
+
+ if (fromTheater != _theater) {
+ fromTheater._removeDeferredChild(child);
+ _theater._addDeferredChild(child);
+ }
+
+ if (fromModel != _childModel || fromLocation._zOrderIndex != _zOrderIndex) {
+ fromLocation._removeFromChildModel(child);
+ _addToChildModel(child);
+ }
+ }
+
+ void _activate(_RenderDeferredLayoutBox child) {
+ assert(_debugNotDisposed());
+ assert(_overlayChildRenderBox == null, '$_overlayChildRenderBox');
+ _theater.adoptChild(child);
+ _overlayChildRenderBox = child;
+ }
+
+ void _deactivate(_RenderDeferredLayoutBox child) {
+ assert(_debugNotDisposed());
+ _theater.dropChild(child);
+ _overlayChildRenderBox = null;
+ }
+
+ bool _debugNotDisposed() {
+ if (_debugDisposedStackTrace == null) {
+ return true;
+ }
+ throw StateError('$this is already disposed. Stack trace: $_debugDisposedStackTrace');
+ }
+
+ StackTrace? _debugDisposedStackTrace;
+ @mustCallSuper
+ void _dispose() {
+ assert(_debugNotDisposed());
+ assert(() {
+ _debugDisposedStackTrace = StackTrace.current;
+ return true;
+ }());
+ }
+}
+
+class _RenderTheaterMarker extends InheritedWidget {
+ const _RenderTheaterMarker({
+ required this.theater,
+ required this.overlayEntryWidgetState,
+ required super.child,
+ });
+
+ final _RenderTheater theater;
+ final _OverlayEntryWidgetState overlayEntryWidgetState;
+
+ @override
+ bool updateShouldNotify(_RenderTheaterMarker oldWidget) {
+ return oldWidget.theater != theater
+ || oldWidget.overlayEntryWidgetState != overlayEntryWidgetState;
+ }
+
+ static _RenderTheaterMarker? maybeOf(BuildContext context, { bool targetRootOverlay = false }) {
+ if (targetRootOverlay) {
+ final InheritedElement? ancestor = _rootRenderTheaterMarkerOf(context.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>());
+ assert(ancestor == null || ancestor.widget is _RenderTheaterMarker);
+ return ancestor != null ? context.dependOnInheritedElement(ancestor) as _RenderTheaterMarker? : null;
+ }
+ return context.dependOnInheritedWidgetOfExactType<_RenderTheaterMarker>();
+ }
+
+ static InheritedElement? _rootRenderTheaterMarkerOf(InheritedElement? theaterMarkerElement) {
+ assert(theaterMarkerElement == null || theaterMarkerElement.widget is _RenderTheaterMarker);
+ if (theaterMarkerElement == null) {
+ return null;
+ }
+ InheritedElement? ancestor;
+ theaterMarkerElement.visitAncestorElements((Element element) {
+ ancestor = element.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>();
+ return false;
+ });
+ return ancestor == null ? theaterMarkerElement : _rootRenderTheaterMarkerOf(ancestor);
+ }
+}
+
+class _OverlayPortal extends RenderObjectWidget {
+ /// Creates a widget that renders the given [overlayChild] in the [Overlay]
+ /// specified by `overlayLocation`.
+ ///
+ /// The `overlayLocation` parameter must not be null when [overlayChild] is not
+ /// null.
+ _OverlayPortal({
+ required this.overlayLocation,
+ required this.overlayChild,
+ required this.child,
+ }) : assert(overlayChild == null || overlayLocation != null),
+ assert(overlayLocation == null || overlayLocation._debugNotDisposed());
+
+ final Widget? overlayChild;
+
+ /// A widget below this widget in the tree.
+ final Widget? child;
+
+ final _OverlayEntryLocation? overlayLocation;
+
+ @override
+ RenderObjectElement createElement() => _OverlayPortalElement(this);
+
+ @override
+ RenderObject createRenderObject(BuildContext context) => _RenderLayoutSurrogateProxyBox();
+}
+
+class _OverlayPortalElement extends RenderObjectElement {
+ _OverlayPortalElement(_OverlayPortal super.widget);
+
+ @override
+ _RenderLayoutSurrogateProxyBox get renderObject => super.renderObject as _RenderLayoutSurrogateProxyBox;
+
+ Element? _overlayChild;
+ Element? _child;
+
+ @override
+ void mount(Element? parent, Object? newSlot) {
+ super.mount(parent, newSlot);
+ final _OverlayPortal widget = this.widget as _OverlayPortal;
+ _child = updateChild(_child, widget.child, null);
+ _overlayChild = updateChild(_overlayChild, widget.overlayChild, widget.overlayLocation);
+ }
+
+ @override
+ void update(_OverlayPortal newWidget) {
+ super.update(newWidget);
+ _child = updateChild(_child, newWidget.child, null);
+ _overlayChild = updateChild(_overlayChild, newWidget.overlayChild, newWidget.overlayLocation);
+ }
+
+ @override
+ void forgetChild(Element child) {
+ // The _overlayChild Element does not have a key because the _DeferredLayout
+ // widget does not take a Key, so only the regular _child can be taken
+ // during global key reparenting.
+ assert(child == _child);
+ _child = null;
+ super.forgetChild(child);
+ }
+
+ @override
+ void visitChildren(ElementVisitor visitor) {
+ final Element? child = _child;
+ final Element? overlayChild = _overlayChild;
+ if (child != null) {
+ visitor(child);
+ }
+ if (overlayChild != null) {
+ visitor(overlayChild);
+ }
+ }
+
+ @override
+ void activate() {
+ super.activate();
+ final Element? overlayChild = _overlayChild;
+ if (overlayChild != null) {
+ final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
+ if (box != null) {
+ assert(!box.attached);
+ assert(renderObject._deferredLayoutChild == box);
+ (overlayChild.slot! as _OverlayEntryLocation)._activate(box);
+ }
+ }
+ }
+
+ @override
+ void deactivate() {
+ final Element? overlayChild = _overlayChild;
+ // Instead of just detaching the render objects, removing them from the
+ // render subtree entirely such that if the widget gets reparented to a
+ // different overlay entry, the overlay child is inserted in the right
+ // position in the overlay's child list.
+ //
+ // This is also a workaround for the !renderObject.attached assert in the
+ // `RenderObjectElement.deactive()` method.
+ if (overlayChild != null) {
+ final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
+ if (box != null) {
+ (overlayChild.slot! as _OverlayEntryLocation)._deactivate(box);
+ }
+ }
+ super.deactivate();
+ }
+
+ @override
+ void insertRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
+ assert(child.parent == null, "$child's parent is not null: ${child.parent}");
+ if (slot != null) {
+ renderObject._deferredLayoutChild = child as _RenderDeferredLayoutBox;
+ slot._addChild(child);
+ } else {
+ renderObject.child = child;
+ }
+ }
+
+ // The [_DeferredLayout] widget does not have a key so there will be no
+ // reparenting between _overlayChild and _child, thus the non-null-typed slots.
+ @override
+ void moveRenderObjectChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation oldSlot, _OverlayEntryLocation newSlot) {
+ assert(newSlot._debugNotDisposed());
+ newSlot._moveChild(child, oldSlot);
+ }
+
+ @override
+ void removeRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
+ if (slot == null) {
+ renderObject.child = null;
+ return;
+ }
+ assert(renderObject._deferredLayoutChild == child);
+ slot._removeChild(child as _RenderDeferredLayoutBox);
+ renderObject._deferredLayoutChild = null;
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Element>('child', _child, defaultValue: null));
+ properties.add(DiagnosticsProperty<Element>('overlayChild', _overlayChild, defaultValue: null));
+ properties.add(DiagnosticsProperty<Object>('overlayLocation', _overlayChild?.slot, defaultValue: null));
+ }
+}
+
+class _DeferredLayout extends SingleChildRenderObjectWidget {
+ const _DeferredLayout({
+ // This widget must not be given a key: we currently do not support
+ // reparenting between the overlayChild and child.
+ required Widget child,
+ }) : super(child: child);
+
+ _RenderLayoutSurrogateProxyBox getLayoutParent(BuildContext context) {
+ return context.findAncestorRenderObjectOfType<_RenderLayoutSurrogateProxyBox>()!;
+ }
+
+ @override
+ _RenderDeferredLayoutBox createRenderObject(BuildContext context) {
+ final _RenderLayoutSurrogateProxyBox parent = getLayoutParent(context);
+ final _RenderDeferredLayoutBox renderObject = _RenderDeferredLayoutBox(parent);
+ parent._deferredLayoutChild = renderObject;
+ return renderObject;
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, _RenderDeferredLayoutBox renderObject) {
+ assert(renderObject._layoutSurrogate == getLayoutParent(context));
+ assert(getLayoutParent(context)._deferredLayoutChild == renderObject);
+ }
+}
+
+// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` is
+// laid out.
+//
+// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that:
+//
+// 1. It's a relayout boundary, and `markParentNeedsLayout` is overridden such
+// that it never dirties its `_RenderTheater`.
+//
+// 2. Its `layout` implementation is overridden such that `performLayout` does
+// not do anything when its called from `layout`, preventing the parent
+// `_RenderTheater` from laying out this subtree prematurely (but this
+// `RenderObject` may still be resized). Instead, `markNeedsLayout` will be
+// called from within `layout` to schedule a layout update for this relayout
+// boundary when needed.
+//
+// 3. When invoked from `PipelineOwner.flushLayout`, or
+// `_layoutSurrogate.performLayout`, this `RenderObject` behaves like an
+// `Overlay` that has only one entry.
+class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> {
+ _RenderDeferredLayoutBox(this._layoutSurrogate);
+
+ StackParentData get stackParentData => parentData! as StackParentData;
+ final _RenderLayoutSurrogateProxyBox _layoutSurrogate;
+
+ @override
+ Iterable<RenderBox> _childrenInPaintOrder() {
+ final RenderBox? child = this.child;
+ return child == null
+ ? const Iterable<RenderBox>.empty()
+ : Iterable<RenderBox>.generate(1, (int i) => child);
+ }
+ @override
+ Iterable<RenderBox> _childrenInHitTestOrder() => _childrenInPaintOrder();
+
+ @override
+ _RenderTheater get theater {
+ final AbstractNode? parent = this.parent;
+ return parent is _RenderTheater
+ ? parent
+ : throw FlutterError('$parent of $this is not a _RenderTheater');
+ }
+
+ @override
+ void redepthChildren() {
+ _layoutSurrogate.redepthChild(this);
+ super.redepthChildren();
+ }
+
+ bool _callingMarkParentNeedsLayout = false;
+ @override
+ void markParentNeedsLayout() {
+ // No re-entrant calls.
+ if (_callingMarkParentNeedsLayout) {
+ return;
+ }
+ _callingMarkParentNeedsLayout = true;
+ markNeedsLayout();
+ _layoutSurrogate.markNeedsLayout();
+ _callingMarkParentNeedsLayout = false;
+ }
+
+ bool _needsLayout = true;
+ @override
+ void markNeedsLayout() {
+ _needsLayout = true;
+ super.markNeedsLayout();
+ }
+
+ @override
+ RenderObject? get debugLayoutParent => _layoutSurrogate;
+
+ void layoutByLayoutSurrogate() {
+ assert(!_parentDoingLayout);
+ final _RenderTheater? theater = parent as _RenderTheater?;
+ if (theater == null || !attached) {
+ assert(false, '$this is not attached to parent');
+ return;
+ }
+ super.layout(BoxConstraints.tight(theater.constraints.biggest));
+ }
+
+ bool _parentDoingLayout = false;
+ @override
+ void layout(Constraints constraints, { bool parentUsesSize = false }) {
+ assert(_needsLayout == debugNeedsLayout);
+ // Only _RenderTheater calls this implementation.
+ assert(parent != null);
+ final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints;
+ assert(!_parentDoingLayout);
+ _parentDoingLayout = true;
+ super.layout(constraints, parentUsesSize: parentUsesSize);
+ assert(_parentDoingLayout);
+ _parentDoingLayout = false;
+ _needsLayout = false;
+ assert(!debugNeedsLayout);
+ if (scheduleDeferredLayout) {
+ final _RenderTheater parent = this.parent! as _RenderTheater;
+ // Invoking markNeedsLayout as a layout callback allows this node to be
+ // merged back to the `PipelineOwner` if it's not already dirty. Otherwise
+ // this may cause some dirty descendants to performLayout a second time.
+ parent.invokeLayoutCallback((BoxConstraints constraints) { markNeedsLayout(); });
+ }
+ }
+
+ @override
+ void performResize() {
+ size = constraints.biggest;
+ }
+
+ bool _debugMutationsLocked = false;
+ @override
+ void performLayout() {
+ assert(!_debugMutationsLocked);
+ if (_parentDoingLayout) {
+ _needsLayout = false;
+ return;
+ }
+ assert(() {
+ _debugMutationsLocked = true;
+ return true;
+ }());
+ // This method is directly being invoked from `PipelineOwner.flushLayout`,
+ // or from `_layoutSurrogate`'s performLayout.
+ assert(parent != null);
+ final RenderBox? child = this.child;
+ if (child == null) {
+ _needsLayout = false;
+ return;
+ }
+ super.performLayout();
+ assert(() {
+ _debugMutationsLocked = false;
+ return true;
+ }());
+ _needsLayout = false;
+ }
+
+ @override
+ void applyPaintTransform(RenderBox child, Matrix4 transform) {
+ final BoxParentData childParentData = child.parentData! as BoxParentData;
+ final Offset offset = childParentData.offset;
+ transform.translate(offset.dx, offset.dy);
+ }
+}
+
+// A RenderProxyBox that makes sure its `deferredLayoutChild` has a greater
+// depth than itself.
+class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
+ _RenderDeferredLayoutBox? _deferredLayoutChild;
+
+ @override
+ void redepthChildren() {
+ super.redepthChildren();
+ final _RenderDeferredLayoutBox? child = _deferredLayoutChild;
+ // If child is not attached, this method will be invoked by child's real
+ // parent when it's attached.
+ if (child != null && child.attached) {
+ assert(child.attached);
+ redepthChild(child);
+ }
+ }
+
+ @override
+ void performLayout() {
+ super.performLayout();
+ // Try to layout `_deferredLayoutChild` here now that its configuration
+ // and constraints are up-to-date. Additionally, during the very first
+ // layout, this makes sure that _deferredLayoutChild is reachable via tree
+ // walk.
+ _deferredLayoutChild?.layoutByLayoutSurrogate();
+ }
+}
diff --git a/framework/lib/src/widgets/overscroll_indicator.dart b/framework/lib/src/widgets/overscroll_indicator.dart
index 272d089..479ec56 100644
--- a/framework/lib/src/widgets/overscroll_indicator.dart
+++ b/framework/lib/src/widgets/overscroll_indicator.dart
@@ -201,6 +201,11 @@
if (!widget.notificationPredicate(notification)) {
return false;
}
+ if (notification.metrics.axis != widget.axis) {
+ // This widget is explicitly configured to one axis. If a notification
+ // from a different axis bubbles up, do nothing.
+ return false;
+ }
// Update the paint offset with the current scroll position. This makes
// sure that the glow effect correctly scrolls in line with the current
@@ -236,7 +241,6 @@
}
}
assert(controller != null);
- assert(notification.metrics.axis == widget.axis);
if (_accepted[isLeading]!) {
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
@@ -707,6 +711,11 @@
if (!widget.notificationPredicate(notification)) {
return false;
}
+ if (notification.metrics.axis != widget.axis) {
+ // This widget is explicitly configured to one axis. If a notification
+ // from a different axis bubbles up, do nothing.
+ return false;
+ }
if (notification is OverscrollNotification) {
_lastOverscrollNotification = notification;
@@ -716,7 +725,6 @@
_accepted = confirmationNotification.accepted;
}
- assert(notification.metrics.axis == widget.axis);
if (_accepted) {
_totalOverscroll += notification.overscroll;
diff --git a/framework/lib/src/widgets/platform_menu_bar.dart b/framework/lib/src/widgets/platform_menu_bar.dart
index 9127760..ccbc7c5 100644
--- a/framework/lib/src/widgets/platform_menu_bar.dart
+++ b/framework/lib/src/widgets/platform_menu_bar.dart
@@ -738,7 +738,8 @@
/// An optional callback that is called when this [PlatformMenuItem] is
/// selected.
///
- /// If unset, this menu item will be disabled.
+ /// At most one of [onSelected] and [onSelectedIntent] may be set. If neither
+ /// field is set, this menu item will be disabled.
final VoidCallback? onSelected;
/// Returns a callback, if any, to be invoked if the platform menu receives a
@@ -760,7 +761,8 @@
/// An optional intent that is invoked when this [PlatformMenuItem] is
/// selected.
///
- /// If unset, this menu item will be disabled.
+ /// At most one of [onSelected] and [onSelectedIntent] may be set. If neither
+ /// field is set, this menu item will be disabled.
final Intent? onSelectedIntent;
/// Returns all descendant [PlatformMenuItem]s of this item.
@@ -805,7 +807,7 @@
return <String, Object?>{
_kIdKey: getId(item),
_kLabelKey: item.label,
- _kEnabledKey: item.onSelected != null,
+ _kEnabledKey: item.onSelected != null || item.onSelectedIntent != null,
if (shortcut != null)...shortcut.serializeForMenu().toChannelRepresentation(),
};
}
diff --git a/framework/lib/src/widgets/platform_view.dart b/framework/lib/src/widgets/platform_view.dart
index 2513191..bf3fbb6 100644
--- a/framework/lib/src/widgets/platform_view.dart
+++ b/framework/lib/src/widgets/platform_view.dart
@@ -1088,7 +1088,7 @@
void initState() {
super.initState();
if (!widget.controller.isCreated) {
- // Schedule a rebuild once creation is complete and the final dislay
+ // Schedule a rebuild once creation is complete and the final display
// type is known.
widget.controller.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
}
diff --git a/framework/lib/src/widgets/scroll_metrics.dart b/framework/lib/src/widgets/scroll_metrics.dart
index e637e0f..cf11802 100644
--- a/framework/lib/src/widgets/scroll_metrics.dart
+++ b/framework/lib/src/widgets/scroll_metrics.dart
@@ -62,7 +62,7 @@
///
/// The actual [pixels] value might be [outOfRange].
///
- /// This value should typically be non-null and less than or equal to
+ /// This value is typically less than or equal to
/// [maxScrollExtent]. It can be negative infinity, if the scroll is unbounded.
double get minScrollExtent;
@@ -70,7 +70,7 @@
///
/// The actual [pixels] value might be [outOfRange].
///
- /// This value should typically be non-null and greater than or equal to
+ /// This value is typically greater than or equal to
/// [minScrollExtent]. It can be infinity, if the scroll is unbounded.
double get maxScrollExtent;
diff --git a/framework/lib/src/widgets/scroll_physics.dart b/framework/lib/src/widgets/scroll_physics.dart
index 350a298..77c3a69 100644
--- a/framework/lib/src/widgets/scroll_physics.dart
+++ b/framework/lib/src/widgets/scroll_physics.dart
@@ -365,6 +365,28 @@
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
+ ///
+ /// This method can potentially be called in every frame, even in the middle
+ /// of what the user perceives as a single ballistic scroll. For example, in
+ /// a [ListView] when previously off-screen items come into view and are laid
+ /// out, this method may be called with a new [ScrollMetrics.maxScrollExtent].
+ /// The method implementation should ensure that when the same ballistic
+ /// scroll motion is still intended, these calls have no side effects on the
+ /// physics beyond continuing that motion.
+ ///
+ /// Generally this is ensured by having the [Simulation] conform to a physical
+ /// metaphor of a particle in ballistic flight, where the forces on the
+ /// particle depend only on its position, velocity, and environment, and not
+ /// on the current time or any internal state. This means that the
+ /// time-derivative of [Simulation.dx] should be possible to write
+ /// mathematically as a function purely of the values of [Simulation.x],
+ /// [Simulation.dx], and the parameters used to construct the [Simulation],
+ /// independent of the time.
+ // TODO(gnprice): Some scroll physics in the framework violate that invariant; fix them.
+ // An audit found three cases violating the invariant:
+ // https://github.com/flutter/flutter/issues/120338
+ // https://github.com/flutter/flutter/issues/120340
+ // https://github.com/flutter/flutter/issues/109675
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
if (parent == null) {
return null;
diff --git a/framework/lib/src/widgets/scroll_simulation.dart b/framework/lib/src/widgets/scroll_simulation.dart
index 5e384c4..d377958 100644
--- a/framework/lib/src/widgets/scroll_simulation.dart
+++ b/framework/lib/src/widgets/scroll_simulation.dart
@@ -123,98 +123,129 @@
}
}
-/// An implementation of scroll physics that matches Android.
+/// An implementation of scroll physics that aligns with Android.
+///
+/// For any value of [velocity], this travels the same total distance as the
+/// Android scroll physics.
+///
+/// This scroll physics has been adjusted relative to Android's in order to make
+/// it ballistic, meaning that the deceleration at any moment is a function only
+/// of the current velocity [dx] and does not depend on how long ago the
+/// simulation was started. (This is required by Flutter's scrolling protocol,
+/// where [ScrollActivityDelegate.goBallistic] may restart a scroll activity
+/// using only its current velocity and the scroll position's own state.)
+/// Compared to this scroll physics, Android's moves faster at the very
+/// beginning, then slower, and it ends at the same place but a little later.
+///
+/// Times are measured in seconds, and positions in logical pixels.
///
/// See also:
///
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
//
-// This class is based on Scroller.java from Android:
-// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
+// This class is based on OverScroller.java from Android:
+// https://android.googlesource.com/platform/frameworks/base/+/android-13.0.0_r24/core/java/android/widget/OverScroller.java#738
+// and in particular class SplineOverScroller (at the end of the file), starting
+// at method "fling". (A very similar algorithm is in Scroller.java in the same
+// directory, but OverScroller is what's used by RecyclerView.)
//
-// The "See..." comments below refer to Scroller methods and values. Some
-// simplifications have been made.
+// In the Android implementation, times are in milliseconds, positions are in
+// physical pixels, but velocity is in physical pixels per whole second.
+//
+// The "See..." comments below refer to SplineOverScroller methods and values.
class ClampingScrollSimulation extends Simulation {
- /// Creates a scroll physics simulation that matches Android scrolling.
+ /// Creates a scroll physics simulation that aligns with Android scrolling.
ClampingScrollSimulation({
required this.position,
required this.velocity,
this.friction = 0.015,
super.tolerance,
- }) : assert(_flingVelocityPenetration(0.0) == _initialVelocityPenetration) {
- _duration = _flingDuration(velocity);
- _distance = (velocity * _duration / _initialVelocityPenetration).abs();
+ }) {
+ _duration = _flingDuration();
+ _distance = _flingDistance();
}
- /// The position of the particle at the beginning of the simulation.
+ /// The position of the particle at the beginning of the simulation, in
+ /// logical pixels.
final double position;
/// The velocity at which the particle is traveling at the beginning of the
- /// simulation.
+ /// simulation, in logical pixels per second.
final double velocity;
/// The amount of friction the particle experiences as it travels.
///
- /// The more friction the particle experiences, the sooner it stops.
+ /// The more friction the particle experiences, the sooner it stops and the
+ /// less far it travels.
+ ///
+ /// The default value causes the particle to travel the same total distance
+ /// as in the Android scroll physics.
+ // See mFlingFriction.
final double friction;
+ /// The total time the simulation will run, in seconds.
late double _duration;
+
+ /// The total, signed, distance the simulation will travel, in logical pixels.
late double _distance;
// See DECELERATION_RATE.
static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);
- // See computeDeceleration().
- static double _decelerationForFriction(double friction) {
- return friction * 61774.04968;
+ // See INFLEXION.
+ static const double _kInflexion = 0.35;
+
+ // See mPhysicalCoeff. This has a value of 0.84 times Earth gravity,
+ // expressed in units of logical pixels per second^2.
+ static const double _physicalCoeff =
+ 9.80665 // g, in meters per second^2
+ * 39.37 // 1 meter / 1 inch
+ * 160.0 // 1 inch / 1 logical pixel
+ * 0.84; // "look and feel tuning"
+
+ // See getSplineFlingDuration().
+ double _flingDuration() {
+ // See getSplineDeceleration(). That function's value is
+ // math.log(velocity.abs() / referenceVelocity).
+ final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
+
+ // This is the value getSplineFlingDuration() would return, but in seconds.
+ final double androidDuration =
+ math.pow(velocity.abs() / referenceVelocity,
+ 1 / (_kDecelerationRate - 1.0)) as double;
+
+ // We finish a bit sooner than Android, in order to travel the
+ // same total distance.
+ return _kDecelerationRate * _kInflexion * androidDuration;
}
- // See getSplineFlingDuration(). Returns a value in seconds.
- double _flingDuration(double velocity) {
- // See mPhysicalCoeff
- final double scaledFriction = friction * _decelerationForFriction(0.84);
-
- // See getSplineDeceleration().
- final double deceleration = math.log(0.35 * velocity.abs() / scaledFriction);
-
- return math.exp(deceleration / (_kDecelerationRate - 1.0));
- }
-
- // Based on a cubic curve fit to the Scroller.computeScrollOffset() values
- // produced for an initial velocity of 4000. The value of Scroller.getDuration()
- // and Scroller.getFinalY() were 686ms and 961 pixels respectively.
- //
- // Algebra courtesy of Wolfram Alpha.
- //
- // f(x) = scrollOffset, x is time in milliseconds
- // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x - 3.15307
- // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x, so f(0) is 0
- // f(686ms) = 961 pixels
- // Scale to f(0 <= t <= 1.0), x = t * 686
- // f(t) = 1165.03 t^3 - 3143.62 t^2 + 2945.87 t
- // Scale f(t) so that 0.0 <= f(t) <= 1.0
- // f(t) = (1165.03 t^3 - 3143.62 t^2 + 2945.87 t) / 961.0
- // = 1.2 t^3 - 3.27 t^2 + 3.065 t
- static const double _initialVelocityPenetration = 3.065;
- static double _flingDistancePenetration(double t) {
- return (1.2 * t * t * t) - (3.27 * t * t) + (_initialVelocityPenetration * t);
- }
-
- // The derivative of the _flingDistancePenetration() function.
- static double _flingVelocityPenetration(double t) {
- return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration;
+ // See getSplineFlingDistance(). This returns the same value but with the
+ // sign of [velocity], and in logical pixels.
+ double _flingDistance() {
+ final double distance = velocity * _duration / _kDecelerationRate;
+ assert(() {
+ // This is the more complicated calculation that getSplineFlingDistance()
+ // actually performs, which boils down to the much simpler formula above.
+ final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
+ final double logVelocity = math.log(velocity.abs() / referenceVelocity);
+ final double distanceAgain =
+ friction * _physicalCoeff
+ * math.exp(logVelocity * _kDecelerationRate / (_kDecelerationRate - 1.0));
+ return (distance.abs() - distanceAgain).abs() < tolerance.distance;
+ }());
+ return distance;
}
@override
double x(double time) {
final double t = clampDouble(time / _duration, 0.0, 1.0);
- return position + _distance * _flingDistancePenetration(t) * velocity.sign;
+ return position + _distance * (1.0 - math.pow(1.0 - t, _kDecelerationRate));
}
@override
double dx(double time) {
final double t = clampDouble(time / _duration, 0.0, 1.0);
- return _distance * _flingVelocityPenetration(t) * velocity.sign / _duration;
+ return velocity * math.pow(1.0 - t, _kDecelerationRate - 1.0);
}
@override
diff --git a/framework/lib/src/widgets/scrollable.dart b/framework/lib/src/widgets/scrollable.dart
index 57ff081..4fe3e13 100644
--- a/framework/lib/src/widgets/scrollable.dart
+++ b/framework/lib/src/widgets/scrollable.dart
@@ -359,7 +359,7 @@
/// If there is no [Scrollable] in the widget tree above the [context], this
/// method returns false.
static bool recommendDeferredLoadingForContext(BuildContext context) {
- final _ScrollableScope? widget = context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()?.widget as _ScrollableScope?;
+ final _ScrollableScope? widget = context.getInheritedWidgetOfExactType<_ScrollableScope>();
if (widget == null) {
return false;
}
@@ -1659,6 +1659,7 @@
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) {
+ _innerNode = null;
super.assembleSemanticsNode(node, config, children);
return;
}
diff --git a/framework/lib/src/widgets/scrollbar.dart b/framework/lib/src/widgets/scrollbar.dart
index 65ad29c..3f75590 100644
--- a/framework/lib/src/widgets/scrollbar.dart
+++ b/framework/lib/src/widgets/scrollbar.dart
@@ -396,7 +396,7 @@
Rect? _thumbRect;
// The current scroll position + _leadingThumbMainAxisOffset
late double _thumbOffset;
- // The fraction visible in relation to the trversable length of the track.
+ // The fraction visible in relation to the traversable length of the track.
late double _thumbExtent;
// Thumb Offsets
// The thumb is offset by padding and margins.
@@ -1825,47 +1825,32 @@
return;
}
- double scrollIncrement;
- // Is an increment calculator available?
- final ScrollIncrementCalculator? calculator = Scrollable.maybeOf(
- _cachedController!.position.context.notificationContext!,
- )?.widget.incrementCalculator;
- if (calculator != null) {
- scrollIncrement = calculator(
- ScrollIncrementDetails(
- type: ScrollIncrementType.page,
- metrics: _cachedController!.position,
- ),
- );
- } else {
- // Default page increment
- scrollIncrement = 0.8 * _cachedController!.position.viewportDimension;
- }
+ // Determines the scroll direction.
+ final AxisDirection scrollDirection;
- // Adjust scrollIncrement for direction
- switch (_cachedController!.position.axisDirection) {
+ switch (position.axisDirection) {
case AxisDirection.up:
- if (details.localPosition.dy > scrollbarPainter._thumbOffset) {
- scrollIncrement = -scrollIncrement;
- }
- break;
case AxisDirection.down:
- if (details.localPosition.dy < scrollbarPainter._thumbOffset) {
- scrollIncrement = -scrollIncrement;
- }
- break;
- case AxisDirection.right:
- if (details.localPosition.dx < scrollbarPainter._thumbOffset) {
- scrollIncrement = -scrollIncrement;
+ if (details.localPosition.dy > scrollbarPainter._thumbOffset) {
+ scrollDirection = AxisDirection.down;
+ } else {
+ scrollDirection = AxisDirection.up;
}
break;
case AxisDirection.left:
+ case AxisDirection.right:
if (details.localPosition.dx > scrollbarPainter._thumbOffset) {
- scrollIncrement = -scrollIncrement;
+ scrollDirection = AxisDirection.right;
+ } else {
+ scrollDirection = AxisDirection.left;
}
- break;
}
+ final ScrollableState? state = Scrollable.maybeOf(position.context.notificationContext!);
+ final ScrollIntent intent = ScrollIntent(direction: scrollDirection, type: ScrollIncrementType.page);
+ assert(state != null);
+ final double scrollIncrement = ScrollAction.getDirectionalIncrement(state!, intent);
+
_cachedController!.position.moveTo(
_cachedController!.position.pixels + scrollIncrement,
duration: const Duration(milliseconds: 100),
diff --git a/framework/lib/src/widgets/selectable_region.dart b/framework/lib/src/widgets/selectable_region.dart
index fb99008..e6b70e3 100644
--- a/framework/lib/src/widgets/selectable_region.dart
+++ b/framework/lib/src/widgets/selectable_region.dart
@@ -738,10 +738,11 @@
}
// Web is using native dom elements to enable clipboard functionality of the
- // toolbar: copy, paste, select, cut. It might also provide additional
- // functionality depending on the browser (such as translate). Due to this
- // we should not show a Flutter toolbar for the editable text elements.
- if (kIsWeb) {
+ // context menu: copy, paste, select, cut. It might also provide additional
+ // functionality depending on the browser (such as translate). Due to this,
+ // we should not show a Flutter toolbar for the editable text elements
+ // unless the browser's context menu is explicitly disabled.
+ if (kIsWeb && BrowserContextMenu.enabled) {
return false;
}
@@ -973,7 +974,7 @@
///
/// * [SelectableRegion.getSelectableButtonItems], which performs a similar role,
/// but for any selectable text, not just specifically SelectableRegion.
- /// * [EditableTextState.contextMenuButtonItems], which peforms a similar role
+ /// * [EditableTextState.contextMenuButtonItems], which performs a similar role
/// but for content that is not just selectable but also editable.
/// * [contextMenuAnchors], which provides the anchor points for the default
/// context menu.
diff --git a/framework/lib/src/widgets/shared_app_data.dart b/framework/lib/src/widgets/shared_app_data.dart
index 518787e..b9e4a69 100644
--- a/framework/lib/src/widgets/shared_app_data.dart
+++ b/framework/lib/src/widgets/shared_app_data.dart
@@ -121,7 +121,7 @@
/// The type parameter `K` is the type of the value's keyword and `V`
/// is the type of the value.
static void setValue<K extends Object, V>(BuildContext context, K key, V value) {
- final _SharedAppModel? model = context.getElementForInheritedWidgetOfExactType<_SharedAppModel>()?.widget as _SharedAppModel?;
+ final _SharedAppModel? model = context.getInheritedWidgetOfExactType<_SharedAppModel>();
assert(_debugHasSharedAppData(model, context, 'setValue'));
model!.sharedAppDataState.setValue<K, V>(key, value);
}
diff --git a/framework/lib/src/widgets/spell_check.dart b/framework/lib/src/widgets/spell_check.dart
index 63e95b6..20c53e5 100644
--- a/framework/lib/src/widgets/spell_check.dart
+++ b/framework/lib/src/widgets/spell_check.dart
@@ -75,7 +75,7 @@
spell check enabled : $_spellCheckEnabled
spell check service : $spellCheckService
misspelled text style : $misspelledTextStyle
- spell check suggesstions toolbar builder: $spellCheckSuggestionsToolbarBuilder
+ spell check suggestions toolbar builder: $spellCheckSuggestionsToolbarBuilder
'''
.trim();
}
diff --git a/framework/lib/src/widgets/tap_and_drag_gestures.dart b/framework/lib/src/widgets/tap_and_drag_gestures.dart
index 6e99d1d..4009786 100644
--- a/framework/lib/src/widgets/tap_and_drag_gestures.dart
+++ b/framework/lib/src/widgets/tap_and_drag_gestures.dart
@@ -335,7 +335,7 @@
/// coordinates (the present [globalPosition]) when this callback is triggered.
///
/// When considering a [GestureRecognizer] that tracks the number of consecutive taps,
- /// this offset is associated with the most recent [PointerDownEvent] that occured.
+ /// this offset is associated with the most recent [PointerDownEvent] that occurred.
final Offset offsetFromOrigin;
/// A local delta offset from the point where the drag initially contacted
@@ -343,7 +343,7 @@
/// coordinates (the present [localPosition]) when this callback is triggered.
///
/// When considering a [GestureRecognizer] that tracks the number of consecutive taps,
- /// this offset is associated with the most recent [PointerDownEvent] that occured.
+ /// this offset is associated with the most recent [PointerDownEvent] that occurred.
final Offset localOffsetFromOrigin;
/// If this tap is in a series of taps, then this value represents
@@ -678,7 +678,7 @@
/// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer`
///
/// Similar to [TapGestureRecognizer] and [DragGestureRecognizer],
-/// [TapAndDragGestureRecognizer] will not aggresively declare victory when it detects
+/// [TapAndDragGestureRecognizer] will not aggressively declare victory when it detects
/// a tap, so when it is competing with those gesture recognizers and others it has a chance
/// of losing.
///
diff --git a/framework/lib/src/widgets/text_selection.dart b/framework/lib/src/widgets/text_selection.dart
index 72d971b..566c405 100644
--- a/framework/lib/src/widgets/text_selection.dart
+++ b/framework/lib/src/widgets/text_selection.dart
@@ -718,7 +718,7 @@
));
final TextSelection currentSelection = TextSelection.fromPosition(position);
- _handleSelectionHandleChanged(currentSelection, isEnd: true);
+ _handleSelectionHandleChanged(currentSelection);
return;
}
@@ -749,7 +749,7 @@
break;
}
- _handleSelectionHandleChanged(newSelection, isEnd: true);
+ _handleSelectionHandleChanged(newSelection);
_selectionOverlay.updateMagnifier(_buildMagnifier(
currentTextPosition: newSelection.extent,
@@ -814,7 +814,7 @@
));
final TextSelection currentSelection = TextSelection.fromPosition(position);
- _handleSelectionHandleChanged(currentSelection, isEnd: false);
+ _handleSelectionHandleChanged(currentSelection);
return;
}
@@ -851,7 +851,7 @@
renderEditable: renderObject,
));
- _handleSelectionHandleChanged(newSelection, isEnd: false);
+ _handleSelectionHandleChanged(newSelection);
}
void _handleAnyDragEnd(DragEndDetails details) {
@@ -874,13 +874,11 @@
}
}
- void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
- final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
+ void _handleSelectionHandleChanged(TextSelection newSelection) {
selectionDelegate.userUpdateTextEditingValue(
_value.copyWith(selection: newSelection),
SelectionChangedCause.drag,
);
- selectionDelegate.bringIntoView(textPosition);
}
TextSelectionHandleType _chooseType(
@@ -983,13 +981,13 @@
/// since magnifiers may hide themselves. If this info is needed, check
/// [MagnifierController.shown].
/// {@endtemplate}
- void showMagnifier(MagnifierInfo initalMagnifierInfo) {
+ void showMagnifier(MagnifierInfo initialMagnifierInfo) {
if (_toolbar != null || _contextMenuControllerIsShown) {
hideToolbar();
}
- // Start from empty, so we don't utilize any rememnant values.
- _magnifierInfo.value = initalMagnifierInfo;
+ // Start from empty, so we don't utilize any remnant values.
+ _magnifierInfo.value = initialMagnifierInfo;
// Pre-build the magnifiers so we can tell if we've built something
// or not. If we don't build a magnifiers, then we should not
@@ -2430,7 +2428,7 @@
@protected
void onDoubleTapDown(TapDragDownDetails details) {
if (delegate.selectionEnabled) {
- renderEditable.selectWord(cause: SelectionChangedCause.tap);
+ renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
if (shouldShowSelectionToolbar) {
editableText.showToolbar();
}
@@ -3076,7 +3074,7 @@
/// waiting to receive the clipboard contents for the first time.
unknown,
- /// The content on the clipboard is not pastable, such as when it is empty.
+ /// The content on the clipboard is not pasteable, such as when it is empty.
notPasteable,
}
diff --git a/framework/lib/src/widgets/ticker_provider.dart b/framework/lib/src/widgets/ticker_provider.dart
index 7f87844..31a32a3 100644
--- a/framework/lib/src/widgets/ticker_provider.dart
+++ b/framework/lib/src/widgets/ticker_provider.dart
@@ -95,7 +95,7 @@
/// In the absence of a [TickerMode] widget, this function returns a
/// [ValueNotifier], whose [ValueNotifier.value] is always true.
static ValueNotifier<bool> getNotifier(BuildContext context) {
- final _EffectiveTickerMode? widget = context.getElementForInheritedWidgetOfExactType<_EffectiveTickerMode>()?.widget as _EffectiveTickerMode?;
+ final _EffectiveTickerMode? widget = context.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
return widget?.notifier ?? ValueNotifier<bool>(true);
}
diff --git a/framework/lib/src/widgets/widget_inspector.dart b/framework/lib/src/widgets/widget_inspector.dart
index f8d557f..2c7e708 100644
--- a/framework/lib/src/widgets/widget_inspector.dart
+++ b/framework/lib/src/widgets/widget_inspector.dart
@@ -932,7 +932,7 @@
groupName: _consoleObjectGroup,
subtreeDepth: 5,
includeProperties: true,
- maxDescendentsTruncatableNode: 5,
+ maxDescendantsTruncatableNode: 5,
service: this,
),
)!;
@@ -3566,7 +3566,7 @@
InspectorSerializationDelegate({
this.groupName,
this.summaryTree = false,
- this.maxDescendentsTruncatableNode = -1,
+ this.maxDescendantsTruncatableNode = -1,
this.expandPropertyValues = true,
this.subtreeDepth = 1,
this.includeProperties = false,
@@ -3587,8 +3587,8 @@
/// Whether the tree should only include nodes created by the local project.
final bool summaryTree;
- /// Maximum descendents of [DiagnosticsNode] before truncating.
- final int maxDescendentsTruncatableNode;
+ /// Maximum descendants of [DiagnosticsNode] before truncating.
+ final int maxDescendantsTruncatableNode;
@override
final bool includeProperties;
@@ -3663,10 +3663,10 @@
@override
List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
- if (maxDescendentsTruncatableNode >= 0 &&
+ if (maxDescendantsTruncatableNode >= 0 &&
owner!.allowTruncate == true &&
- nodes.length > maxDescendentsTruncatableNode) {
- nodes = service._truncateNodes(nodes, maxDescendentsTruncatableNode);
+ nodes.length > maxDescendantsTruncatableNode) {
+ nodes = service._truncateNodes(nodes, maxDescendantsTruncatableNode);
}
return nodes;
}
@@ -3676,7 +3676,7 @@
return InspectorSerializationDelegate(
groupName: groupName,
summaryTree: summaryTree,
- maxDescendentsTruncatableNode: maxDescendentsTruncatableNode,
+ maxDescendantsTruncatableNode: maxDescendantsTruncatableNode,
expandPropertyValues: expandPropertyValues ?? this.expandPropertyValues,
subtreeDepth: subtreeDepth ?? this.subtreeDepth,
includeProperties: includeProperties ?? this.includeProperties,
@@ -3711,8 +3711,8 @@
/// factory. The framework will then instrument that function in the same way
/// as it does for [Widget] constructors.
///
-/// Note that the function **must not** have optional positional parameters for
-/// tracking to work correctly.
+/// Tracking will not work correctly if the function has optional positional
+/// parameters.
///
/// Currently this annotation is only supported on extension methods.
///