make sync script Posix-compliant; update framework to 98b3e48ed4e722acb0c4bb897397fc817057b5f6
diff --git a/engine/analysis_options.yaml b/engine/analysis_options.yaml
index 92e155d..edeb29f 100644
--- a/engine/analysis_options.yaml
+++ b/engine/analysis_options.yaml
@@ -17,9 +17,6 @@
exclude:
# Fixture depends on dart:ui and raises false positives.
- flutter_frontend_server/test/fixtures/lib/main.dart
- strong-mode:
- implicit-casts: false
- implicit-dynamic: false
errors:
# treat missing required parameters as a warning (not a hint)
missing_required_param: warning
diff --git a/engine/lib/src/math.dart b/engine/lib/src/math.dart
index a52fdc0..240e79b 100644
--- a/engine/lib/src/math.dart
+++ b/engine/lib/src/math.dart
@@ -24,6 +24,20 @@
return x;
}
+double clampDouble(double x, double min, double max) {
+ assert(min <= max && !max.isNaN && !min.isNaN);
+ if (x < min) {
+ return min;
+ }
+ if (x > max) {
+ return max;
+ }
+ if (x.isNaN) {
+ return max;
+ }
+ return x;
+}
+
Float32List _toMatrix32(Float64List matrix64) {
final Float32List matrix32 = Float32List(16);
matrix32[15] = matrix64[15];
diff --git a/engine/lib/src/painting.dart b/engine/lib/src/painting.dart
index 08e5750..78e8bc3 100644
--- a/engine/lib/src/painting.dart
+++ b/engine/lib/src/painting.dart
@@ -1360,7 +1360,8 @@
}
}
class ImageShader extends Shader {
- ImageShader(Image image, TileMode tmx, TileMode tmy, Float64List matrix4) :
+ // ignore: avoid_unused_constructor_parameters
+ ImageShader(Image image, TileMode tmx, TileMode tmy, Float64List matrix4, { FilterQuality? filterQuality }) :
// ignore: unnecessary_null_comparison
assert(image != null), // image is checked on the engine side
assert(tmx != null), // ignore: unnecessary_null_comparison
@@ -2152,7 +2153,12 @@
throw UnsupportedError('ImmutableBuffer.fromFilePath is not supported in flute.');
}
- void _init(Uint8List list, _Callback<void> callback) { }
+ void _init(Uint8List list, _Callback<void> callback) {
+ _list = Uint8List.fromList(list);
+ }
+
+ Uint8List? _list;
+
final int length;
void dispose() { }
}
@@ -2315,3 +2321,83 @@
targetHeight: targetHeight,
);
}
+
+
+Future<Codec> instantiateImageCodecWithSize(
+ ImmutableBuffer buffer, {
+ TargetImageSizeCallback? getTargetSize,
+}) async {
+ if (getTargetSize == null) {
+ return instantiateImageCodec(buffer._list!);
+ } else {
+ final Codec codec = await instantiateImageCodec(buffer._list!);
+ try {
+ final FrameInfo info = await codec.getNextFrame();
+ try {
+ final int width = info.image.width;
+ final int height = info.image.height;
+ final TargetImageSize targetSize = getTargetSize(width, height);
+ return instantiateImageCodec(buffer._list!,
+ targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false);
+ } finally {
+ info.image.dispose();
+ }
+ } finally {
+ codec.dispose();
+ }
+ }
+}
+
+/// Signature for a callback that determines the size to which an image should
+/// be decoded given its intrinsic size.
+///
+/// See also:
+///
+/// * [instantiateImageCodecWithSize], which used this signature for its
+/// `getTargetSize` argument.
+typedef TargetImageSizeCallback = TargetImageSize Function(
+ int intrinsicWidth,
+ int intrinsicHeight,
+);
+
+/// A specification of the size to which an image should be decoded.
+///
+/// See also:
+///
+/// * [TargetImageSizeCallback], a callback that returns instances of this
+/// class when consulted by image decoding methods such as
+/// [instantiateImageCodecWithSize].
+class TargetImageSize {
+ /// Creates a new instance of this class.
+ ///
+ /// The `width` and `height` may both be null, but if they're non-null, they
+ /// must be positive.
+ const TargetImageSize({this.width, this.height})
+ : assert(width == null || width > 0),
+ assert(height == null || height > 0);
+
+ /// The width into which to load the image.
+ ///
+ /// If this is non-null, the image will be decoded into the specified width.
+ /// If this is null and [height] is also null, the image will be decoded into
+ /// its intrinsic size. If this is null and [height] is non-null, the image
+ /// will be decoded into a width that maintains its intrinsic aspect ratio
+ /// while respecting the [height] value.
+ ///
+ /// If this value is non-null, it must be positive.
+ final int? width;
+
+ /// The height into which to load the image.
+ ///
+ /// If this is non-null, the image will be decoded into the specified height.
+ /// If this is null and [width] is also null, the image will be decoded into
+ /// its intrinsic size. If this is null and [width] is non-null, the image
+ /// will be decoded into a height that maintains its intrinsic aspect ratio
+ /// while respecting the [width] value.
+ ///
+ /// If this value is non-null, it must be positive.
+ final int? height;
+
+ @override
+ String toString() => 'TargetImageSize($width x $height)';
+}
diff --git a/framework/lib/foundation.dart b/framework/lib/foundation.dart
index 40b35f2..dff7553 100644
--- a/framework/lib/foundation.dart
+++ b/framework/lib/foundation.dart
@@ -24,6 +24,7 @@
export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart';
export 'src/foundation/bitfield.dart';
+export 'src/foundation/capabilities.dart';
export 'src/foundation/change_notifier.dart';
export 'src/foundation/collections.dart';
export 'src/foundation/consolidate_response.dart';
diff --git a/framework/lib/material.dart b/framework/lib/material.dart
index 99b9a67..fb07102 100644
--- a/framework/lib/material.dart
+++ b/framework/lib/material.dart
@@ -65,6 +65,7 @@
export 'src/material/data_table_theme.dart';
export 'src/material/date.dart';
export 'src/material/date_picker.dart';
+export 'src/material/date_picker_theme.dart';
export 'src/material/debug.dart';
export 'src/material/desktop_text_selection.dart';
export 'src/material/desktop_text_selection_toolbar.dart';
@@ -159,6 +160,8 @@
export 'src/material/slider_theme.dart';
export 'src/material/snack_bar.dart';
export 'src/material/snack_bar_theme.dart';
+export 'src/material/spell_check_suggestions_toolbar.dart';
+export 'src/material/spell_check_suggestions_toolbar_layout_delegate.dart';
export 'src/material/stepper.dart';
export 'src/material/switch.dart';
export 'src/material/switch_list_tile.dart';
diff --git a/framework/lib/painting.dart b/framework/lib/painting.dart
index bd3e029..35f056e 100644
--- a/framework/lib/painting.dart
+++ b/framework/lib/painting.dart
@@ -47,6 +47,7 @@
export 'src/painting/image_resolution.dart';
export 'src/painting/image_stream.dart';
export 'src/painting/inline_span.dart';
+export 'src/painting/linear_border.dart';
export 'src/painting/matrix_utils.dart';
export 'src/painting/notched_shapes.dart';
export 'src/painting/oval_border.dart';
diff --git a/framework/lib/services.dart b/framework/lib/services.dart
index a0ff42f..7e7fb02 100644
--- a/framework/lib/services.dart
+++ b/framework/lib/services.dart
@@ -11,15 +11,18 @@
library services;
export 'src/services/asset_bundle.dart';
+export 'src/services/asset_manifest.dart';
export 'src/services/autofill.dart';
export 'src/services/binary_messenger.dart';
export 'src/services/binding.dart';
+export 'src/services/browser_context_menu.dart';
export 'src/services/clipboard.dart';
export 'src/services/debug.dart';
export 'src/services/deferred_component.dart';
export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart';
export 'src/services/hardware_keyboard.dart';
+export 'src/services/keyboard_inserted_content.dart';
export 'src/services/keyboard_key.g.dart';
export 'src/services/keyboard_maps.g.dart';
export 'src/services/message_codec.dart';
diff --git a/framework/lib/src/animation/animation.dart b/framework/lib/src/animation/animation.dart
index 09cf78c..cafcdfe 100644
--- a/framework/lib/src/animation/animation.dart
+++ b/framework/lib/src/animation/animation.dart
@@ -81,9 +81,9 @@
/// final double opacity = (value / 1000).clamp(0, 1);
/// return Opacity(opacity: opacity, child: child);
/// },
- /// child: Container(
+ /// child: const ColoredBox(
/// color: Colors.red,
- /// child: const Text('Hello, Animation'),
+ /// child: Text('Hello, Animation'),
/// ),
/// );
/// }
@@ -100,9 +100,9 @@
/// opacity: Animation<double>.fromValueListenable(_scrollPosition, transformer: (double value) {
/// return (value / 1000).clamp(0, 1);
/// }),
- /// child: Container(
+ /// child: const ColoredBox(
/// color: Colors.red,
- /// child: const Text('Hello, Animation'),
+ /// child: Text('Hello, Animation'),
/// ),
/// );
/// }
@@ -269,7 +269,6 @@
/// * "⏭": [AnimationStatus.completed] ([value] == 1.0)
/// * "⏮": [AnimationStatus.dismissed] ([value] == 0.0)
String toStringDetails() {
- assert(status != null);
switch (status) {
case AnimationStatus.forward:
return '\u25B6'; // >
diff --git a/framework/lib/src/animation/animation_controller.dart b/framework/lib/src/animation/animation_controller.dart
index a048589..4cc7e83 100644
--- a/framework/lib/src/animation/animation_controller.dart
+++ b/framework/lib/src/animation/animation_controller.dart
@@ -242,10 +242,7 @@
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
required TickerProvider vsync,
- }) : assert(lowerBound != null),
- assert(upperBound != null),
- assert(upperBound >= lowerBound),
- assert(vsync != null),
+ }) : assert(upperBound >= lowerBound),
_direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
@@ -275,9 +272,7 @@
this.debugLabel,
required TickerProvider vsync,
this.animationBehavior = AnimationBehavior.preserve,
- }) : assert(value != null),
- assert(vsync != null),
- lowerBound = double.negativeInfinity,
+ }) : lowerBound = double.negativeInfinity,
upperBound = double.infinity,
_direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
@@ -363,7 +358,6 @@
/// * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat],
/// which start the animation controller.
set value(double newValue) {
- assert(newValue != null);
stop();
_internalSetValue(newValue);
notifyListeners();
@@ -657,7 +651,6 @@
}());
assert(max >= min);
assert(max <= upperBound && min >= lowerBound);
- assert(reverse != null);
stop();
return _startSimulation(_RepeatingSimulation(_value, min, max, reverse, period!, _directionSetter));
}
@@ -744,7 +737,6 @@
}
TickerFuture _startSimulation(Simulation simulation) {
- assert(simulation != null);
assert(!isAnimating);
_simulation = simulation;
_lastElapsedDuration = Duration.zero;
@@ -856,9 +848,7 @@
class _InterpolationSimulation extends Simulation {
_InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale)
- : assert(_begin != null),
- assert(_end != null),
- assert(duration != null && duration.inMicroseconds > 0),
+ : assert(duration.inMicroseconds > 0),
_durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
final double _durationInSeconds;
diff --git a/framework/lib/src/animation/animations.dart b/framework/lib/src/animation/animations.dart
index a4e17c5..47fc7f2 100644
--- a/framework/lib/src/animation/animations.dart
+++ b/framework/lib/src/animation/animations.dart
@@ -255,8 +255,8 @@
/// If the parent animation is running forward from 0.0 to 1.0, this animation
/// is running in reverse from 1.0 to 0.0.
///
-/// Using a [ReverseAnimation] is different from simply using a [Tween] with a
-/// begin of 1.0 and an end of 0.0 because the tween does not change the status
+/// Using a [ReverseAnimation] is different from using a [Tween] with a
+/// `begin` of 1.0 and an `end` of 0.0 because the tween does not change the status
/// or direction of the animation.
///
/// See also:
@@ -271,8 +271,7 @@
/// Creates a reverse animation.
///
/// The parent argument must not be null.
- ReverseAnimation(this.parent)
- : assert(parent != null);
+ ReverseAnimation(this.parent);
/// The animation whose value and direction this animation is reversing.
final Animation<double> parent;
@@ -310,7 +309,6 @@
double get value => 1.0 - parent.value;
AnimationStatus _reverseStatus(AnimationStatus status) {
- assert(status != null);
switch (status) {
case AnimationStatus.forward: return AnimationStatus.reverse;
case AnimationStatus.reverse: return AnimationStatus.forward;
@@ -384,8 +382,7 @@
required this.parent,
required this.curve,
this.reverseCurve,
- }) : assert(parent != null),
- assert(curve != null) {
+ }) {
_updateCurveDirection(parent.status);
parent.addStatusListener(_updateCurveDirection);
}
@@ -518,7 +515,7 @@
Animation<double> this._currentTrain,
this._nextTrain, {
this.onSwitchedTrain,
- }) : assert(_currentTrain != null) {
+ }) {
if (_nextTrain != null) {
if (_currentTrain!.value == _nextTrain!.value) {
_currentTrain = _nextTrain;
@@ -644,8 +641,7 @@
CompoundAnimation({
required this.first,
required this.next,
- }) : assert(first != null),
- assert(next != null);
+ });
/// The first sub-animation. Its status takes precedence if neither are
/// animating.
diff --git a/framework/lib/src/animation/curves.dart b/framework/lib/src/animation/curves.dart
index bc52684..29755f8 100644
--- a/framework/lib/src/animation/curves.dart
+++ b/framework/lib/src/animation/curves.dart
@@ -35,7 +35,6 @@
/// implementation of [transform], which delegates the remaining logic to
/// [transformInternal].
T transform(double t) {
- assert(t != null);
assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
return transformInternal(t);
}
@@ -129,7 +128,7 @@
/// Creates a sawtooth curve.
///
/// The [count] argument must not be null.
- const SawTooth(this.count) : assert(count != null);
+ const SawTooth(this.count);
/// The number of repetitions of the sawtooth pattern in the unit interval.
final int count;
@@ -159,10 +158,7 @@
/// Creates an interval curve.
///
/// The arguments must not be null.
- const Interval(this.begin, this.end, { this.curve = Curves.linear })
- : assert(begin != null),
- assert(end != null),
- assert(curve != null);
+ const Interval(this.begin, this.end, { this.curve = Curves.linear });
/// The largest value for which this interval is 0.0.
///
@@ -207,7 +203,7 @@
/// Creates a threshold curve.
///
/// The [threshold] argument must not be null.
- const Threshold(this.threshold) : assert(threshold != null);
+ const Threshold(this.threshold);
/// The value before which the curve is 0.0 and after which the curve is 1.0.
///
@@ -307,11 +303,7 @@
/// cubic curves in [Curves].
///
/// The [a] (x1), [b] (y1), [c] (x2) and [d] (y2) arguments must not be null.
- const Cubic(this.a, this.b, this.c, this.d)
- : assert(a != null),
- assert(b != null),
- assert(c != null),
- assert(d != null);
+ const Cubic(this.a, this.b, this.c, this.d);
/// The x coordinate of the first control point.
///
@@ -518,9 +510,6 @@
// 4. Recursively sample the two parts.
//
// This algorithm concentrates samples in areas of high curvature.
- assert(tolerance != null);
- assert(start != null);
- assert(end != null);
assert(end > start);
// We want to pick a random seed that will keep the result stable if
// evaluated again, so we use the first non-generated control point.
@@ -577,7 +566,6 @@
/// single-valued, it will return the parameter for only one of the values at
/// the given `x` location.
double findInverse(double x) {
- assert(x != null);
double start = 0.0;
double end = 1.0;
late double mid;
@@ -612,7 +600,7 @@
/// Creates an object that holds a sample; used with [Curve2D] subclasses.
///
/// All arguments must not be null.
- const Curve2DSample(this.t, this.value) : assert(t != null), assert(value != null);
+ const Curve2DSample(this.t, this.value);
/// The parametric location of this sample point along the curve.
final double t;
@@ -682,9 +670,7 @@
double tension = 0.0,
Offset? startHandle,
Offset? endHandle,
- }) : assert(controlPoints != null),
- assert(tension != null),
- assert(tension <= 1.0, 'tension $tension must not be greater than 1.0.'),
+ }) : assert(tension <= 1.0, 'tension $tension must not be greater than 1.0.'),
assert(tension >= 0.0, 'tension $tension must not be negative.'),
assert(controlPoints.length > 3, 'There must be at least four control points to create a CatmullRomSpline.'),
_controlPoints = controlPoints,
@@ -702,9 +688,7 @@
double tension = 0.0,
Offset? startHandle,
Offset? endHandle,
- }) : assert(controlPoints != null),
- assert(tension != null),
- assert(tension <= 1.0, 'tension $tension must not be greater than 1.0.'),
+ }) : assert(tension <= 1.0, 'tension $tension must not be greater than 1.0.'),
assert(tension >= 0.0, 'tension $tension must not be negative.'),
assert(controlPoints.length > 3, 'There must be at least four control points to create a CatmullRomSpline.'),
_controlPoints = null,
@@ -860,8 +844,7 @@
///
/// * This [paper on using Catmull-Rom splines](http://faculty.cs.tamu.edu/schaefer/research/cr_cad.pdf).
CatmullRomCurve(this.controlPoints, {this.tension = 0.0})
- : assert(tension != null),
- assert(() {
+ : assert(() {
return validateControlPoints(
controlPoints,
tension: tension,
@@ -877,8 +860,7 @@
/// Same as [CatmullRomCurve.new], but it precomputes the internal curve data
/// structures for a more predictable computation load.
CatmullRomCurve.precompute(this.controlPoints, {this.tension = 0.0})
- : assert(tension != null),
- assert(() {
+ : assert(() {
return validateControlPoints(
controlPoints,
tension: tension,
@@ -963,7 +945,6 @@
double tension = 0.0,
List<String>? reasons,
}) {
- assert(tension != null);
if (controlPoints == null) {
assert(() {
reasons?.add('Supplied control points cannot be null');
@@ -1143,7 +1124,7 @@
/// Creates a flipped curve.
///
/// The [curve] argument must not be null.
- const FlippedCurve(this.curve) : assert(curve != null);
+ const FlippedCurve(this.curve);
/// The curve that is being flipped.
final Curve curve;
diff --git a/framework/lib/src/animation/tween.dart b/framework/lib/src/animation/tween.dart
index d6c12b9..817e948 100644
--- a/framework/lib/src/animation/tween.dart
+++ b/framework/lib/src/animation/tween.dart
@@ -364,8 +364,7 @@
class ReverseTween<T extends Object?> extends Tween<T> {
/// Construct a [Tween] that evaluates its [parent] in reverse.
ReverseTween(this.parent)
- : assert(parent != null),
- super(begin: parent.end, end: parent.begin);
+ : super(begin: parent.end, end: parent.begin);
/// This tween's value is the same as the parent's value evaluated in reverse.
///
@@ -544,8 +543,7 @@
/// Creates a curve tween.
///
/// The [curve] argument must not be null.
- CurveTween({ required this.curve })
- : assert(curve != null);
+ CurveTween({ required this.curve });
/// The curve to use when transforming the value of the animation.
Curve curve;
diff --git a/framework/lib/src/animation/tween_sequence.dart b/framework/lib/src/animation/tween_sequence.dart
index 252d273..a8e3208 100644
--- a/framework/lib/src/animation/tween_sequence.dart
+++ b/framework/lib/src/animation/tween_sequence.dart
@@ -51,8 +51,7 @@
/// best to reuse one, rather than rebuilding it on every frame, when that's
/// possible.
TweenSequence(List<TweenSequenceItem<T>> items)
- : assert(items != null),
- assert(items.isNotEmpty) {
+ : assert(items.isNotEmpty) {
_items.addAll(items);
double totalWeight = 0.0;
@@ -113,8 +112,7 @@
/// There's a small cost associated with building a `TweenSequence` so it's
/// best to reuse one, rather than rebuilding it on every frame, when that's
/// possible.
- FlippedTweenSequence(super.items)
- : assert(items != null);
+ FlippedTweenSequence(super.items);
@override
double transform(double t) => 1 - super.transform(1 - t);
@@ -128,9 +126,7 @@
const TweenSequenceItem({
required this.tween,
required this.weight,
- }) : assert(tween != null),
- assert(weight != null),
- assert(weight > 0.0);
+ }) : assert(weight > 0.0);
/// Defines the value of the [TweenSequence] for the interval within the
/// animation's duration indicated by [weight] and this item's position
diff --git a/framework/lib/src/cupertino/activity_indicator.dart b/framework/lib/src/cupertino/activity_indicator.dart
index 8089d10..b4d3799 100644
--- a/framework/lib/src/cupertino/activity_indicator.dart
+++ b/framework/lib/src/cupertino/activity_indicator.dart
@@ -36,9 +36,7 @@
this.color,
this.animating = true,
this.radius = _kDefaultIndicatorRadius,
- }) : assert(animating != null),
- assert(radius != null),
- assert(radius > 0.0),
+ }) : assert(radius > 0.0),
progress = 1.0;
/// Creates a non-animated iOS-style activity indicator that displays
@@ -52,9 +50,7 @@
this.color,
this.radius = _kDefaultIndicatorRadius,
this.progress = 1.0,
- }) : assert(radius != null),
- assert(radius > 0.0),
- assert(progress != null),
+ }) : assert(radius > 0.0),
assert(progress >= 0.0),
assert(progress <= 1.0),
animating = false;
diff --git a/framework/lib/src/cupertino/adaptive_text_selection_toolbar.dart b/framework/lib/src/cupertino/adaptive_text_selection_toolbar.dart
index 7f64616..43abb02 100644
--- a/framework/lib/src/cupertino/adaptive_text_selection_toolbar.dart
+++ b/framework/lib/src/cupertino/adaptive_text_selection_toolbar.dart
@@ -157,7 +157,7 @@
final List<ContextMenuButtonItem>? buttonItems;
/// Returns a List of Widgets generated by turning [buttonItems] into the
- /// the default context menu buttons for Cupertino on the current platform.
+ /// default context menu buttons for Cupertino on the current platform.
///
/// This is useful when building a text selection toolbar with the default
/// button appearance for the given platform, but where the toolbar and/or the
@@ -165,7 +165,7 @@
///
/// Does not build Material buttons. On non-Apple platforms, Cupertino buttons
/// will still be used, because the Cupertino library does not access the
- /// Material library. To get the native-looking buttons on every platform, use
+ /// Material library. To get the native-looking buttons on every platform,
/// use [AdaptiveTextSelectionToolbar.getAdaptiveButtons] in the Material
/// library.
///
diff --git a/framework/lib/src/cupertino/app.dart b/framework/lib/src/cupertino/app.dart
index 1e98a03..dab2763 100644
--- a/framework/lib/src/cupertino/app.dart
+++ b/framework/lib/src/cupertino/app.dart
@@ -176,16 +176,13 @@
this.actions,
this.restorationScopeId,
this.scrollBehavior,
+ @Deprecated(
+ 'Remove this parameter as it is now ignored. '
+ 'CupertinoApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
this.useInheritedMediaQuery = false,
- }) : assert(routes != null),
- assert(navigatorObservers != null),
- assert(title != null),
- assert(showPerformanceOverlay != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- assert(showSemanticsDebugger != null),
- assert(debugShowCheckedModeBanner != null),
- routeInformationProvider = null,
+ }) : routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
backButtonDispatcher = null,
@@ -220,14 +217,13 @@
this.actions,
this.restorationScopeId,
this.scrollBehavior,
+ @Deprecated(
+ 'Remove this parameter as it is now ignored. '
+ 'CupertinoApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
this.useInheritedMediaQuery = false,
}) : assert(routerDelegate != null || routerConfig != null),
- assert(title != null),
- assert(showPerformanceOverlay != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- assert(showSemanticsDebugger != null),
- assert(debugShowCheckedModeBanner != null),
navigatorObservers = null,
navigatorKey = null,
onGenerateRoute = null,
@@ -422,6 +418,11 @@
final ScrollBehavior? scrollBehavior;
/// {@macro flutter.widgets.widgetsApp.useInheritedMediaQuery}
+ @Deprecated(
+ 'This setting is now ignored. '
+ 'CupertinoApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
final bool useInheritedMediaQuery;
@override
@@ -555,7 +556,6 @@
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
- useInheritedMediaQuery: widget.useInheritedMediaQuery,
);
}
return WidgetsApp(
@@ -590,7 +590,6 @@
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
- useInheritedMediaQuery: widget.useInheritedMediaQuery,
);
}
diff --git a/framework/lib/src/cupertino/bottom_tab_bar.dart b/framework/lib/src/cupertino/bottom_tab_bar.dart
index bc96fb1..286cd02 100644
--- a/framework/lib/src/cupertino/bottom_tab_bar.dart
+++ b/framework/lib/src/cupertino/bottom_tab_bar.dart
@@ -74,16 +74,12 @@
width: 0.0, // 0.0 means one physical pixel
),
),
- }) : assert(items != null),
- assert(
+ }) : assert(
items.length >= 2,
"Tabs need at least 2 items to conform to Apple's HIG",
),
- assert(currentIndex != null),
assert(0 <= currentIndex && currentIndex < items.length),
- assert(iconSize != null),
- assert(height != null && height >= 0.0),
- assert(inactiveColor != null);
+ assert(height >= 0.0);
/// The interactive items laid out within the bottom navigation bar.
///
@@ -156,7 +152,7 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final double bottomPadding = MediaQuery.of(context).viewPadding.bottom;
+ final double bottomPadding = MediaQuery.viewPaddingOf(context).bottom;
final Color backgroundColor = CupertinoDynamicColor.resolve(
this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
diff --git a/framework/lib/src/cupertino/button.dart b/framework/lib/src/cupertino/button.dart
index ac84891..917481d 100644
--- a/framework/lib/src/cupertino/button.dart
+++ b/framework/lib/src/cupertino/button.dart
@@ -50,8 +50,6 @@
this.alignment = Alignment.center,
required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
- assert(disabledColor != null),
- assert(alignment != null),
_filled = false;
/// Creates an iOS-style button with a filled background.
@@ -71,8 +69,6 @@
this.alignment = Alignment.center,
required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
- assert(disabledColor != null),
- assert(alignment != null),
color = null,
_filled = true;
diff --git a/framework/lib/src/cupertino/colors.dart b/framework/lib/src/cupertino/colors.dart
index 7c5b9f6..2249ed2 100644
--- a/framework/lib/src/cupertino/colors.dart
+++ b/framework/lib/src/cupertino/colors.dart
@@ -144,6 +144,21 @@
darkHighContrastColor: Color.fromARGB(255, 48, 219, 91),
);
+ /// A mint color that can adapt to the given [BuildContext].
+ ///
+ /// See also:
+ ///
+ /// * [UIColor.systemMint](https://developer.apple.com/documentation/uikit/uicolor/3852741-systemmint),
+ /// the `UIKit` equivalent.
+ static const CupertinoDynamicColor systemMint =
+ CupertinoDynamicColor.withBrightnessAndContrast(
+ debugLabel: 'systemMint',
+ color: Color.fromARGB(255, 0, 199, 190),
+ darkColor: Color.fromARGB(255, 99, 230, 226),
+ highContrastColor: Color.fromARGB(255, 12, 129, 123),
+ darkHighContrastColor: Color.fromARGB(255, 102, 212, 207),
+ );
+
/// An indigo color that can adapt to the given [BuildContext].
///
/// See also:
@@ -186,6 +201,21 @@
darkHighContrastColor: Color.fromARGB(255, 255, 100, 130),
);
+ /// A brown color that can adapt to the given [BuildContext].
+ ///
+ /// See also:
+ ///
+ /// * [UIColor.systemBrown](https://developer.apple.com/documentation/uikit/uicolor/3173142-systembrown),
+ /// the `UIKit` equivalent.
+ static const CupertinoDynamicColor systemBrown =
+ CupertinoDynamicColor.withBrightnessAndContrast(
+ debugLabel: 'systemBrown',
+ color: Color.fromARGB(255, 162, 132, 94),
+ darkColor: Color.fromARGB(255, 172, 142, 104),
+ highContrastColor: Color.fromARGB(255, 127, 101, 69),
+ darkHighContrastColor: Color.fromARGB(255, 181, 148, 105),
+ );
+
/// A purple color that can adapt to the given [BuildContext].
///
/// See also:
@@ -228,6 +258,21 @@
darkHighContrastColor: Color.fromARGB(255, 112, 215, 255),
);
+ /// A cyan color that can adapt to the given [BuildContext].
+ ///
+ /// See also:
+ ///
+ /// * [UIColor.systemCyan](https://developer.apple.com/documentation/uikit/uicolor/3852740-systemcyan),
+ /// the `UIKit` equivalent.
+ static const CupertinoDynamicColor systemCyan =
+ CupertinoDynamicColor.withBrightnessAndContrast(
+ debugLabel: 'systemCyan',
+ color: Color.fromARGB(255, 50, 173, 230),
+ darkColor: Color.fromARGB(255, 100, 210, 255),
+ highContrastColor: Color.fromARGB(255, 0, 113, 164),
+ darkHighContrastColor: Color.fromARGB(255, 112, 215, 255),
+ );
+
/// A yellow color that can adapt to the given [BuildContext].
///
/// See also:
@@ -780,16 +825,7 @@
this.darkHighContrastElevatedColor,
this._debugResolveContext,
this._debugLabel,
- ) : assert(color != null),
- assert(darkColor != null),
- assert(highContrastColor != null),
- assert(darkHighContrastColor != null),
- assert(elevatedColor != null),
- assert(darkElevatedColor != null),
- assert(highContrastElevatedColor != null),
- assert(darkHighContrastElevatedColor != null),
- assert(_effectiveColor != null),
- // The super constructor has to be called with a dummy value in order to mark
+ ) : // The super constructor has to be called with a dummy value in order to mark
// this constructor const.
// The field `value` is overridden in the class implementation.
super(0);
@@ -908,7 +944,6 @@
/// * [maybeResolve], which is similar to this function, but will allow a
/// null `resolvable` color.
static Color resolve(Color resolvable, BuildContext context) {
- assert(context != null);
return (resolvable is CupertinoDynamicColor)
? resolvable.resolveFrom(context)
: resolvable;
@@ -931,7 +966,6 @@
if (resolvable == null) {
return null;
}
- assert(context != null);
return (resolvable is CupertinoDynamicColor)
? resolvable.resolveFrom(context)
: resolvable;
@@ -990,11 +1024,11 @@
CupertinoDynamicColor resolveFrom(BuildContext context) {
Brightness brightness = Brightness.light;
if (_isPlatformBrightnessDependent) {
- brightness = CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light;
+ brightness = CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light;
}
bool isHighContrastEnabled = false;
if (_isHighContrastDependent) {
- isHighContrastEnabled = MediaQuery.maybeOf(context)?.highContrast ?? false;
+ isHighContrastEnabled = MediaQuery.maybeHighContrastOf(context) ?? false;
}
final CupertinoUserInterfaceLevelData level = _isInterfaceElevationDependent
diff --git a/framework/lib/src/cupertino/context_menu.dart b/framework/lib/src/cupertino/context_menu.dart
index 5909065..15788df 100644
--- a/framework/lib/src/cupertino/context_menu.dart
+++ b/framework/lib/src/cupertino/context_menu.dart
@@ -99,10 +99,10 @@
/// [Expanded] widget so that it will grow to fill the Overlay if its size is
/// unconstrained.
///
-/// When closed, the CupertinoContextMenu simply displays the child as if the
-/// CupertinoContextMenu were not there. Sizing and positioning is unaffected.
+/// When closed, the [CupertinoContextMenu] displays the child as if the
+/// [CupertinoContextMenu] were not there. Sizing and positioning is unaffected.
/// The menu can be closed like other [PopupRoute]s, such as by tapping the
-/// background or by calling `Navigator.pop(context)`. Unlike PopupRoute, it can
+/// background or by calling `Navigator.pop(context)`. Unlike [PopupRoute], it can
/// also be closed by swiping downwards.
///
/// The [previewBuilder] parameter is most commonly used to display a slight
@@ -111,8 +111,8 @@
/// Photos app on iOS.
///
/// {@tool dartpad}
-/// This sample shows a very simple CupertinoContextMenu for the Flutter logo.
-/// Simply long press on it to open.
+/// This sample shows a very simple [CupertinoContextMenu] for the Flutter logo.
+/// Long press on it to open.
///
/// ** See code in examples/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart **
/// {@end-tool}
@@ -142,8 +142,7 @@
'This feature was deprecated after v3.4.0-34.1.pre.',
)
this.previewBuilder = _defaultPreviewBuilder,
- }) : assert(actions != null && actions.isNotEmpty),
- assert(child != null),
+ }) : assert(actions.isNotEmpty),
builder = ((BuildContext context, Animation<double> animation) => child);
/// Creates a context menu with a custom [builder] controlling the widget.
@@ -158,7 +157,7 @@
super.key,
required this.actions,
required this.builder,
- }) : assert(actions != null && actions.isNotEmpty),
+ }) : assert(actions.isNotEmpty),
child = null,
previewBuilder = null;
@@ -256,10 +255,9 @@
/// and when it is fully open. This will overwrite the default animation that
/// matches the behavior of an iOS 16.0 context menu.
///
- /// This builder can be used instead of the child when either the intended
- /// child has a property that would conflict with the default animation, like
- /// a border radius or a shadow, or if simply a more custom animation is
- /// needed.
+ /// This builder can be used instead of the child when the intended child has
+ /// a property that would conflict with the default animation, such as a
+ /// border radius or a shadow, or if a more custom animation is needed.
///
/// In addition to the current [BuildContext], the function is also called
/// with an [Animation]. The complete animation goes from 0 to 1 when
@@ -493,7 +491,7 @@
// it.
_ContextMenuLocation get _contextMenuLocation {
final Rect childRect = _getRect(_childGlobalKey);
- final double screenWidth = MediaQuery.of(context).size.width;
+ final double screenWidth = MediaQuery.sizeOf(context).width;
final double center = screenWidth / 2;
final bool centerDividesChild = childRect.left < center
@@ -795,8 +793,7 @@
super.filter,
required Rect previousChildRect,
super.settings,
- }) : assert(actions != null && actions.isNotEmpty),
- assert(contextMenuLocation != null),
+ }) : assert(actions.isNotEmpty),
_actions = actions,
_builder = builder,
_contextMenuLocation = contextMenuLocation,
@@ -1082,8 +1079,7 @@
this.onDismiss,
required this.orientation,
this.sheetGlobalKey,
- }) : assert(contextMenuLocation != null),
- assert(orientation != null);
+ });
final List<Widget>? actions;
final Widget child;
@@ -1311,7 +1307,7 @@
Widget _buildChildAnimation(BuildContext context, Widget? child) {
_lastScale = _getScale(
widget.orientation,
- MediaQuery.of(context).size.height,
+ MediaQuery.sizeOf(context).height,
_moveAnimation.value.dy,
);
return Transform.scale(
@@ -1410,9 +1406,7 @@
required this.actions,
required _ContextMenuLocation contextMenuLocation,
required Orientation orientation,
- }) : assert(actions != null && actions.isNotEmpty),
- assert(contextMenuLocation != null),
- assert(orientation != null),
+ }) : assert(actions.isNotEmpty),
_contextMenuLocation = contextMenuLocation,
_orientation = orientation;
diff --git a/framework/lib/src/cupertino/context_menu_action.dart b/framework/lib/src/cupertino/context_menu_action.dart
index 2906e4f..0f011ea 100644
--- a/framework/lib/src/cupertino/context_menu_action.dart
+++ b/framework/lib/src/cupertino/context_menu_action.dart
@@ -20,9 +20,7 @@
this.isDestructiveAction = false,
this.onPressed,
this.trailingIcon,
- }) : assert(child != null),
- assert(isDefaultAction != null),
- assert(isDestructiveAction != null);
+ });
/// The widget that will be placed inside the action.
final Widget child;
diff --git a/framework/lib/src/cupertino/date_picker.dart b/framework/lib/src/cupertino/date_picker.dart
index 92234d3..94522f1 100644
--- a/framework/lib/src/cupertino/date_picker.dart
+++ b/framework/lib/src/cupertino/date_picker.dart
@@ -81,8 +81,7 @@
_DatePickerLayoutDelegate({
required this.columnWidths,
required this.textDirectionFactor,
- }) : assert(columnWidths != null),
- assert(textDirectionFactor != null);
+ });
// The list containing widths of all columns.
final List<double> columnWidths;
@@ -277,14 +276,10 @@
this.dateOrder,
this.backgroundColor,
}) : initialDateTime = initialDateTime ?? DateTime.now(),
- assert(mode != null),
- assert(onDateTimeChanged != null),
- assert(minimumYear != null),
assert(
minuteInterval > 0 && 60 % minuteInterval == 0,
'minute interval is not a positive integer factor of 60',
) {
- assert(this.initialDateTime != null);
assert(
mode != CupertinoDatePickerMode.dateAndTime || minimumDate == null || !this.initialDateTime.isBefore(minimumDate!),
'initial date is before minimum date',
@@ -944,7 +939,6 @@
}
void _scrollToDate(DateTime newDate, DateTime fromDate, bool minCheck) {
- assert(newDate != null);
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
if (fromDate.year != newDate.year || fromDate.month != newDate.month || fromDate.day != newDate.day) {
_animateColumnControllerToItem(dateController, selectedDayFromInitial);
@@ -1349,7 +1343,6 @@
}
void _scrollToDate(DateTime newDate) {
- assert(newDate != null);
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
if (selectedYear != newDate.year) {
_animateColumnControllerToItem(yearController, newDate.year);
@@ -1551,15 +1544,12 @@
this.alignment = Alignment.center,
this.backgroundColor,
required this.onTimerDurationChanged,
- }) : assert(mode != null),
- assert(onTimerDurationChanged != null),
- assert(initialTimerDuration >= Duration.zero),
+ }) : assert(initialTimerDuration >= Duration.zero),
assert(initialTimerDuration < const Duration(days: 1)),
assert(minuteInterval > 0 && 60 % minuteInterval == 0),
assert(secondInterval > 0 && 60 % secondInterval == 0),
assert(initialTimerDuration.inMinutes % minuteInterval == 0),
- assert(initialTimerDuration.inSeconds % secondInterval == 0),
- assert(alignment != null);
+ assert(initialTimerDuration.inSeconds % secondInterval == 0);
/// The mode of the timer picker.
final CupertinoTimerPickerMode mode;
diff --git a/framework/lib/src/cupertino/desktop_text_selection.dart b/framework/lib/src/cupertino/desktop_text_selection.dart
index 6f529bc..b5dbada 100644
--- a/framework/lib/src/cupertino/desktop_text_selection.dart
+++ b/framework/lib/src/cupertino/desktop_text_selection.dart
@@ -158,12 +158,12 @@
}
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
+ final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context);
final Offset midpointAnchor = Offset(
clampDouble(widget.selectionMidpoint.dx - widget.globalEditableRegion.left,
- mediaQuery.padding.left,
- mediaQuery.size.width - mediaQuery.padding.right,
+ mediaQueryPadding.left,
+ MediaQuery.sizeOf(context).width - mediaQueryPadding.right,
),
widget.selectionMidpoint.dy - widget.globalEditableRegion.top,
);
@@ -171,7 +171,7 @@
final List<Widget> items = <Widget>[];
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
final Widget onePhysicalPixelVerticalDivider =
- SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
+ SizedBox(width: 1.0 / MediaQuery.devicePixelRatioOf(context));
void addToolbarButton(
String text,
diff --git a/framework/lib/src/cupertino/desktop_text_selection_toolbar.dart b/framework/lib/src/cupertino/desktop_text_selection_toolbar.dart
index 831a758..939b3e4 100644
--- a/framework/lib/src/cupertino/desktop_text_selection_toolbar.dart
+++ b/framework/lib/src/cupertino/desktop_text_selection_toolbar.dart
@@ -85,9 +85,8 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
+ final double paddingAbove = MediaQuery.paddingOf(context).top + _kToolbarScreenPadding;
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
return Padding(
diff --git a/framework/lib/src/cupertino/desktop_text_selection_toolbar_button.dart b/framework/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
index f9e11cb..0a88961 100644
--- a/framework/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
+++ b/framework/lib/src/cupertino/desktop_text_selection_toolbar_button.dart
@@ -37,8 +37,7 @@
super.key,
required this.onPressed,
required Widget this.child,
- }) : assert(child != null),
- buttonItem = null;
+ }) : buttonItem = null;
/// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] whose child is
/// a [Text] widget styled like the default Mac context menu button.
@@ -66,8 +65,7 @@
CupertinoDesktopTextSelectionToolbarButton.buttonItem({
super.key,
required ContextMenuButtonItem this.buttonItem,
- }) : assert(buttonItem != null),
- onPressed = buttonItem.onPressed,
+ }) : onPressed = buttonItem.onPressed,
child = null;
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
diff --git a/framework/lib/src/cupertino/dialog.dart b/framework/lib/src/cupertino/dialog.dart
index 876026b..d398462 100644
--- a/framework/lib/src/cupertino/dialog.dart
+++ b/framework/lib/src/cupertino/dialog.dart
@@ -149,8 +149,8 @@
// Accessibility mode on iOS is determined by the text scale factor that the
// user has selected.
bool _isInAccessibilityMode(BuildContext context) {
- final MediaQueryData? data = MediaQuery.maybeOf(context);
- return data != null && data.textScaleFactor > _kMaxRegularTextScaleFactor;
+ final double? factor = MediaQuery.maybeTextScaleFactorOf(context);
+ return factor != null && factor > _kMaxRegularTextScaleFactor;
}
/// An iOS-style alert dialog.
@@ -201,7 +201,7 @@
this.actionScrollController,
this.insetAnimationDuration = const Duration(milliseconds: 100),
this.insetAnimationCurve = Curves.decelerate,
- }) : assert(actions != null);
+ });
/// The (optional) title of the dialog is displayed in a large font at the top
/// of the dialog.
@@ -257,7 +257,7 @@
final Curve insetAnimationCurve;
Widget _buildContent(BuildContext context) {
- final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
+ final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
final List<Widget> children = <Widget>[
if (title != null || content != null)
@@ -289,7 +289,7 @@
),
];
- return Container(
+ return ColoredBox(
color: CupertinoDynamicColor.resolve(_kDialogColor, context),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -317,7 +317,7 @@
Widget build(BuildContext context) {
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
final bool isInAccessibilityMode = _isInAccessibilityMode(context);
- final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
+ final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
return CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
child: MediaQuery(
@@ -331,7 +331,7 @@
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return AnimatedPadding(
- padding: MediaQuery.of(context).viewInsets +
+ padding: MediaQuery.viewInsetsOf(context) +
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
duration: insetAnimationDuration,
curve: insetAnimationCurve,
@@ -555,7 +555,7 @@
content.add(Flexible(child: titleSection));
}
- return Container(
+ return ColoredBox(
color: CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -611,12 +611,12 @@
if (cancelButton != null) _buildCancelButton(),
];
- final Orientation orientation = MediaQuery.of(context).orientation;
+ final Orientation orientation = MediaQuery.orientationOf(context);
final double actionSheetWidth;
if (orientation == Orientation.portrait) {
- actionSheetWidth = MediaQuery.of(context).size.width - (_kActionSheetEdgeHorizontalPadding * 2);
+ actionSheetWidth = MediaQuery.sizeOf(context).width - (_kActionSheetEdgeHorizontalPadding * 2);
} else {
- actionSheetWidth = MediaQuery.of(context).size.height - (_kActionSheetEdgeHorizontalPadding * 2);
+ actionSheetWidth = MediaQuery.sizeOf(context).height - (_kActionSheetEdgeHorizontalPadding * 2);
}
return SafeArea(
@@ -665,8 +665,7 @@
this.isDefaultAction = false,
this.isDestructiveAction = false,
required this.child,
- }) : assert(child != null),
- assert(onPressed != null);
+ });
/// The callback that is called when the button is tapped.
///
@@ -701,7 +700,7 @@
}
return MouseRegion(
- cursor: onPressed != null && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
+ cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
child: GestureDetector(
onTap: onPressed,
behavior: HitTestBehavior.opaque,
@@ -797,7 +796,7 @@
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderCupertinoDialog(
- dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio,
+ dividerThickness: _kDividerThickness / MediaQuery.devicePixelRatioOf(context),
isInAccessibilityMode: _isInAccessibilityMode(context) && !isActionSheet,
dividerColor: CupertinoDynamicColor.resolve(dividerColor, context),
isActionSheet: isActionSheet,
@@ -896,7 +895,6 @@
}
void _placeChildInSlot(RenderObject child, _AlertDialogSections slot) {
- assert(slot != null);
switch (slot) {
case _AlertDialogSections.contentSection:
renderObject.contentSection = child as RenderBox;
@@ -1443,7 +1441,7 @@
this.scrollController,
this.hasCancelButton = false,
this.isActionSheet = false,
- }) : assert(children != null);
+ });
final List<Widget> children;
@@ -1464,7 +1462,7 @@
@override
Widget build(BuildContext context) {
- final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
+ final double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final List<Widget> interactiveButtons = <Widget>[];
for (int i = 0; i < children.length; i += 1) {
@@ -1593,9 +1591,7 @@
this.isDestructiveAction = false,
this.textStyle,
required this.child,
- }) : assert(child != null),
- assert(isDefaultAction != null),
- assert(isDestructiveAction != null);
+ });
/// The callback that is called when the button is tapped or otherwise
/// activated.
@@ -1767,7 +1763,7 @@
//
// See [_RenderCupertinoDialogActions] for specific layout policy details.
class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
- _CupertinoDialogActionsRenderWidget({
+ const _CupertinoDialogActionsRenderWidget({
required List<Widget> actionButtons,
double dividerThickness = 0.0,
bool hasCancelButton = false,
diff --git a/framework/lib/src/cupertino/icons.dart b/framework/lib/src/cupertino/icons.dart
index 94188e6..3332770 100644
--- a/framework/lib/src/cupertino/icons.dart
+++ b/framework/lib/src/cupertino/icons.dart
@@ -30,9 +30,9 @@
/// ![The following code snippet would generate a row of icons consisting of a pink heart, a green bell, and a blue umbrella, each progressively bigger than the last.](https://flutter.github.io/assets-for-api-docs/assets/cupertino/cupertino_icon.png)
///
/// ```dart
-/// Row(
+/// const Row(
/// mainAxisAlignment: MainAxisAlignment.spaceAround,
-/// children: const <Widget>[
+/// children: <Widget>[
/// Icon(
/// CupertinoIcons.heart_fill,
/// color: Colors.pink,
@@ -59,6 +59,7 @@
/// See also:
///
/// * [Icon], used to show these icons.
+@staticIconProvider
class CupertinoIcons {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
diff --git a/framework/lib/src/cupertino/interface_level.dart b/framework/lib/src/cupertino/interface_level.dart
index 6f35554..c11f17f 100644
--- a/framework/lib/src/cupertino/interface_level.dart
+++ b/framework/lib/src/cupertino/interface_level.dart
@@ -45,8 +45,7 @@
super.key,
required CupertinoUserInterfaceLevelData data,
required super.child,
- }) : assert(data != null),
- _data = data;
+ }) : _data = data;
final CupertinoUserInterfaceLevelData _data;
@@ -65,7 +64,6 @@
/// * [maybeOf], which is similar, but will return null if no
/// [CupertinoUserInterfaceLevel] encloses the given context.
static CupertinoUserInterfaceLevelData of(BuildContext context) {
- assert(context != null);
final CupertinoUserInterfaceLevel? query = context.dependOnInheritedWidgetOfExactType<CupertinoUserInterfaceLevel>();
if (query != null) {
return query._data;
@@ -95,7 +93,6 @@
/// * [of], which is similar, but will throw an exception if no
/// [CupertinoUserInterfaceLevel] encloses the given context.
static CupertinoUserInterfaceLevelData? maybeOf(BuildContext context) {
- assert(context != null);
final CupertinoUserInterfaceLevel? query = context.dependOnInheritedWidgetOfExactType<CupertinoUserInterfaceLevel>();
if (query != null) {
return query._data;
diff --git a/framework/lib/src/cupertino/list_section.dart b/framework/lib/src/cupertino/list_section.dart
index 26bf0d9..e917133 100644
--- a/framework/lib/src/cupertino/list_section.dart
+++ b/framework/lib/src/cupertino/list_section.dart
@@ -347,7 +347,7 @@
@override
Widget build(BuildContext context) {
final Color dividerColor = CupertinoColors.separator.resolveFrom(context);
- final double dividerHeight = 1.0 / MediaQuery.of(context).devicePixelRatio;
+ final double dividerHeight = 1.0 / MediaQuery.devicePixelRatioOf(context);
// Long divider is used for wrapping the top and bottom of rows.
// Only used in CupertinoListSectionType.base mode.
diff --git a/framework/lib/src/cupertino/magnifier.dart b/framework/lib/src/cupertino/magnifier.dart
index a1ed06e..05a92b9 100644
--- a/framework/lib/src/cupertino/magnifier.dart
+++ b/framework/lib/src/cupertino/magnifier.dart
@@ -174,7 +174,7 @@
CupertinoMagnifier.kMagnifierAboveFocalPoint),
);
- final Rect screenRect = Offset.zero & MediaQuery.of(context).size;
+ final Rect screenRect = Offset.zero & MediaQuery.sizeOf(context);
// Adjust the magnifier position so that it never exists outside the horizontal
// padding.
diff --git a/framework/lib/src/cupertino/nav_bar.dart b/framework/lib/src/cupertino/nav_bar.dart
index 405e02b..6d73f55 100644
--- a/framework/lib/src/cupertino/nav_bar.dart
+++ b/framework/lib/src/cupertino/nav_bar.dart
@@ -33,6 +33,8 @@
const double _kNavBarEdgePadding = 16.0;
+const double _kNavBarBottomPadding = 8.0;
+
const double _kNavBarBackButtonTapWidth = 50.0;
/// Title text transfer fade.
@@ -147,8 +149,20 @@
overlayStyle = SystemUiOverlayStyle.dark;
break;
}
+ // [SystemUiOverlayStyle.light] and [SystemUiOverlayStyle.dark] set some system
+ // navigation bar properties,
+ // Before https://github.com/flutter/flutter/pull/104827 those properties
+ // had no effect, now they are used if there is no AnnotatedRegion on the
+ // bottom of the screen.
+ // For backward compatibility, create a `SystemUiOverlayStyle` without the
+ // system navigation bar properties.
result = AnnotatedRegion<SystemUiOverlayStyle>(
- value: overlayStyle,
+ value: SystemUiOverlayStyle(
+ statusBarColor: overlayStyle.statusBarColor,
+ statusBarBrightness: overlayStyle.statusBarBrightness,
+ statusBarIconBrightness: overlayStyle.statusBarIconBrightness,
+ systemStatusBarContrastEnforced: overlayStyle.systemStatusBarContrastEnforced,
+ ),
child: result,
);
}
@@ -255,15 +269,7 @@
this.padding,
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
- }) : assert(automaticallyImplyLeading != null),
- assert(automaticallyImplyMiddle != null),
- assert(transitionBetweenRoutes != null),
- assert(
- heroTag != null,
- 'heroTag cannot be null. Use transitionBetweenRoutes = false to '
- 'disable Hero transition on this navigation bar.',
- ),
- assert(
+ }) : assert(
!transitionBetweenRoutes || identical(heroTag, _defaultHeroTag),
'Cannot specify a heroTag override if this navigation bar does not '
'transition due to transitionBetweenRoutes = false.',
@@ -597,9 +603,7 @@
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
this.stretch = false,
- }) : assert(automaticallyImplyLeading != null),
- assert(automaticallyImplyTitle != null),
- assert(
+ }) : assert(
automaticallyImplyTitle == true || largeTitle != null,
'No largeTitle has been provided but automaticallyImplyTitle is also '
'false. Either provide a largeTitle or set automaticallyImplyTitle to '
@@ -754,7 +758,7 @@
actionsForegroundColor: CupertinoTheme.of(context).primaryColor,
transitionBetweenRoutes: widget.transitionBetweenRoutes,
heroTag: widget.heroTag,
- persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
+ persistentHeight: _kNavBarPersistentHeight + MediaQuery.paddingOf(context).top,
alwaysShowMiddle: widget.alwaysShowMiddle && widget.middle != null,
stretchConfiguration: widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
),
@@ -779,9 +783,7 @@
required this.persistentHeight,
required this.alwaysShowMiddle,
required this.stretchConfiguration,
- }) : assert(persistentHeight != null),
- assert(alwaysShowMiddle != null),
- assert(transitionBetweenRoutes != null);
+ });
final _NavigationBarStaticComponentsKeys keys;
final _NavigationBarStaticComponents components;
@@ -833,31 +835,27 @@
right: 0.0,
bottom: 0.0,
child: ClipRect(
- // The large title starts at the persistent bar.
- // It's aligned with the bottom of the sliver and expands clipped
- // and behind the persistent bar.
- child: OverflowBox(
- minHeight: 0.0,
- maxHeight: double.infinity,
- alignment: AlignmentDirectional.bottomStart,
- child: Padding(
- padding: const EdgeInsetsDirectional.only(
- start: _kNavBarEdgePadding,
- bottom: 8.0, // Bottom has a different padding.
- ),
- child: SafeArea(
- top: false,
- bottom: false,
- child: AnimatedOpacity(
- opacity: showLargeTitle ? 1.0 : 0.0,
- duration: _kNavBarTitleFadeDuration,
- child: Semantics(
- header: true,
- child: DefaultTextStyle(
- style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- child: components.largeTitle!,
+ child: Padding(
+ padding: const EdgeInsetsDirectional.only(
+ start: _kNavBarEdgePadding,
+ bottom: _kNavBarBottomPadding
+ ),
+ child: SafeArea(
+ top: false,
+ bottom: false,
+ child: AnimatedOpacity(
+ opacity: showLargeTitle ? 1.0 : 0.0,
+ duration: _kNavBarTitleFadeDuration,
+ child: Semantics(
+ header: true,
+ child: DefaultTextStyle(
+ style: CupertinoTheme.of(context)
+ .textTheme
+ .navLargeTitleTextStyle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ child: _LargeTitle(
+ child: components.largeTitle,
),
),
),
@@ -921,6 +919,123 @@
}
}
+/// The large title of the navigation bar.
+///
+/// Magnifies on over-scroll when [CupertinoSliverNavigationBar.stretch]
+/// parameter is true.
+class _LargeTitle extends SingleChildRenderObjectWidget {
+ const _LargeTitle({ super.child });
+
+ @override
+ _RenderLargeTitle createRenderObject(BuildContext context) {
+ return _RenderLargeTitle(alignment: AlignmentDirectional.bottomStart.resolve(Directionality.of(context)));
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, _RenderLargeTitle renderObject) {
+ renderObject.alignment = AlignmentDirectional.bottomStart.resolve(Directionality.of(context));
+ }
+}
+
+class _RenderLargeTitle extends RenderShiftedBox {
+ _RenderLargeTitle({
+ required Alignment alignment,
+ }) : _alignment = alignment,
+ super(null);
+
+ Alignment get alignment => _alignment;
+ Alignment _alignment;
+ set alignment(Alignment value) {
+ if (_alignment == value) {
+ return;
+ }
+ _alignment = value;
+
+ markNeedsLayout();
+ }
+
+ double _scale = 1.0;
+
+ @override
+ void performLayout() {
+ final RenderBox? child = this.child;
+ Size childSize = Size.zero;
+
+ size = constraints.biggest;
+
+ if (child == null) {
+ return;
+ }
+
+ final BoxConstraints childConstriants = constraints.widthConstraints().loosen();
+ child.layout(childConstriants, parentUsesSize: true);
+
+ final double maxScale = child.size.width != 0.0
+ ? clampDouble(constraints.maxWidth / child.size.width, 1.0, 1.1)
+ : 1.1;
+ _scale = clampDouble(
+ 1.0 + (constraints.maxHeight - (_kNavBarLargeTitleHeightExtension - _kNavBarBottomPadding)) / (_kNavBarLargeTitleHeightExtension - _kNavBarBottomPadding) * 0.03,
+ 1.0,
+ maxScale,
+ );
+
+ childSize = child.size * _scale;
+ final BoxParentData childParentData = child.parentData! as BoxParentData;
+ childParentData.offset = alignment.alongOffset(size - childSize as Offset);
+ }
+
+ @override
+ void applyPaintTransform(RenderBox child, Matrix4 transform) {
+ assert(child == this.child);
+
+ super.applyPaintTransform(child, transform);
+
+ transform.scale(_scale, _scale);
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ final RenderBox? child = this.child;
+
+ if (child == null) {
+ layer = null;
+ } else {
+ final BoxParentData childParentData = child.parentData! as BoxParentData;
+
+ layer = context.pushTransform(
+ needsCompositing,
+ offset + childParentData.offset,
+ Matrix4.diagonal3Values(_scale, _scale, 1.0),
+ (PaintingContext context, Offset offset) => context.paintChild(child, offset),
+ oldLayer: layer as TransformLayer?,
+ );
+ }
+ }
+
+ @override
+ bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+ final RenderBox? child = this.child;
+
+ if (child == null) {
+ return false;
+ }
+
+ final Offset childOffset = (child.parentData! as BoxParentData).offset;
+
+ final Matrix4 transform = Matrix4.identity()
+ ..scale(1.0/_scale, 1.0/_scale, 1.0)
+ ..translate(-childOffset.dx, -childOffset.dy);
+
+ return result.addWithRawTransform(
+ transform: transform,
+ position: position,
+ hitTest: (BoxHitTestResult result, Offset transformed) {
+ return child.hitTest(result, position: transformed);
+ }
+ );
+ }
+}
+
/// The top part of the navigation bar that's never scrolled away.
///
/// Consists of the entire navigation bar without background and border when used
@@ -989,7 +1104,7 @@
}
return SizedBox(
- height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
+ height: _kNavBarPersistentHeight + MediaQuery.paddingOf(context).top,
child: SafeArea(
bottom: false,
child: paddedToolbar,
@@ -1453,7 +1568,7 @@
// null here and unused.
Widget _buildPreviousTitleWidget(BuildContext context, String? previousTitle, Widget? child) {
if (previousTitle == null) {
- return const SizedBox(height: 0.0, width: 0.0);
+ return const SizedBox.shrink();
}
Text textWidget = Text(
@@ -1487,7 +1602,7 @@
builder: _buildPreviousTitleWidget,
);
} else {
- return const SizedBox(height: 0.0, width: 0.0);
+ return const SizedBox.shrink();
}
}
}
@@ -1511,9 +1626,7 @@
required this.hasUserMiddle,
required this.largeExpanded,
required this.child,
- }) : assert(componentsKeys != null),
- assert(largeExpanded != null),
- assert(!largeExpanded || largeTitleTextStyle != null),
+ }) : assert(!largeExpanded || largeTitleTextStyle != null),
super(key: componentsKeys.navBarBoxKey);
final _NavigationBarStaticComponentsKeys componentsKeys;
@@ -1655,12 +1768,16 @@
// The actual outer box is big enough to contain both the bottom and top
// navigation bars. It's not a direct Rect lerp because some components
// can actually be outside the linearly lerp'ed Rect in the middle of
- // the animation, such as the topLargeTitle.
- return SizedBox(
- height: math.max(heightTween.begin!, heightTween.end!) + MediaQuery.of(context).padding.top,
- width: double.infinity,
- child: Stack(
- children: children,
+ // the animation, such as the topLargeTitle. The textScaleFactor is kept
+ // at 1 to avoid odd transitions between pages.
+ return MediaQuery(
+ data: MediaQuery.of(context).copyWith(textScaleFactor: 1),
+ child: SizedBox(
+ height: math.max(heightTween.begin!, heightTween.end!) + MediaQuery.paddingOf(context).top,
+ width: double.infinity,
+ child: Stack(
+ children: children,
+ ),
),
);
}
@@ -1981,7 +2098,7 @@
return null;
}
- if (bottomLargeTitle != null && topBackLabel != null) {
+ if (topBackLabel != null) {
// Move from current position to the top page's back label position.
return slideFromLeadingEdge(
fromKey: bottomComponents.largeTitleKey,
@@ -2008,7 +2125,7 @@
);
}
- if (bottomLargeTitle != null && topLeading != null) {
+ if (topLeading != null) {
// Unlike bottom middle, the bottom large title moves when it can't
// transition to the top back label position.
final RelativeRect from = positionInTransitionBox(bottomComponents.largeTitleKey, from: bottomNavBarBox);
@@ -2139,7 +2256,6 @@
// text is too long, the topBackLabel will say 'Back' instead of the original
// text.
if (bottomLargeTitle != null &&
- topBackLabel != null &&
bottomLargeExpanded) {
return slideFromLeadingEdge(
fromKey: bottomComponents.largeTitleKey,
@@ -2163,7 +2279,7 @@
// The topBackLabel always comes from the large title first if available
// and expanded instead of middle.
- if (bottomMiddle != null && topBackLabel != null) {
+ if (bottomMiddle != null) {
return slideFromLeadingEdge(
fromKey: bottomComponents.middleKey,
fromNavBarBox: bottomNavBarBox,
@@ -2333,10 +2449,6 @@
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
- assert(animation != null);
- assert(flightDirection != null);
- assert(fromHeroContext != null);
- assert(toHeroContext != null);
assert(fromHeroContext.widget is Hero);
assert(toHeroContext.widget is Hero);
@@ -2349,8 +2461,6 @@
final _TransitionableNavigationBar fromNavBar = fromHeroWidget.child as _TransitionableNavigationBar;
final _TransitionableNavigationBar toNavBar = toHeroWidget.child as _TransitionableNavigationBar;
- assert(fromNavBar.componentsKeys != null);
- assert(toNavBar.componentsKeys != null);
assert(
fromNavBar.componentsKeys.navBarBoxKey.currentContext!.owner != null,
diff --git a/framework/lib/src/cupertino/page_scaffold.dart b/framework/lib/src/cupertino/page_scaffold.dart
index 05e740b..a605143 100644
--- a/framework/lib/src/cupertino/page_scaffold.dart
+++ b/framework/lib/src/cupertino/page_scaffold.dart
@@ -39,8 +39,7 @@
this.backgroundColor,
this.resizeToAvoidBottomInset = true,
required this.child,
- }) : assert(child != null),
- assert(resizeToAvoidBottomInset != null);
+ });
/// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the
/// top of the screen.
diff --git a/framework/lib/src/cupertino/picker.dart b/framework/lib/src/cupertino/picker.dart
index b6fa83c..7f42a1e 100644
--- a/framework/lib/src/cupertino/picker.dart
+++ b/framework/lib/src/cupertino/picker.dart
@@ -83,13 +83,9 @@
required List<Widget> children,
this.selectionOverlay = const CupertinoPickerDefaultSelectionOverlay(),
bool looping = false,
- }) : assert(children != null),
- assert(diameterRatio != null),
- assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
+ }) : assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(magnification > 0),
- assert(itemExtent != null),
assert(itemExtent > 0),
- assert(squeeze != null),
assert(squeeze > 0),
childDelegate = looping
? ListWheelChildLoopingListDelegate(children: children)
@@ -126,13 +122,9 @@
required NullableIndexedWidgetBuilder itemBuilder,
int? childCount,
this.selectionOverlay = const CupertinoPickerDefaultSelectionOverlay(),
- }) : assert(itemBuilder != null),
- assert(diameterRatio != null),
- assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
+ }) : assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(magnification > 0),
- assert(itemExtent != null),
assert(itemExtent > 0),
- assert(squeeze != null),
assert(squeeze > 0),
childDelegate = ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount);
@@ -254,7 +246,6 @@
hasSuitableHapticHardware = false;
break;
}
- assert(hasSuitableHapticHardware != null);
if (hasSuitableHapticHardware && index != _lastHapticIndex) {
_lastHapticIndex = index;
HapticFeedback.selectionClick();
@@ -355,9 +346,7 @@
this.background = CupertinoColors.tertiarySystemFill,
this.capStartEdge = true,
this.capEndEdge = true,
- }) : assert(background != null),
- assert(capStartEdge != null),
- assert(capEndEdge != null);
+ });
/// Whether to use the default use rounded corners and margin on the start side.
final bool capStartEdge;
diff --git a/framework/lib/src/cupertino/refresh.dart b/framework/lib/src/cupertino/refresh.dart
index 0c122ba..3ada10c 100644
--- a/framework/lib/src/cupertino/refresh.dart
+++ b/framework/lib/src/cupertino/refresh.dart
@@ -20,9 +20,7 @@
this.refreshIndicatorLayoutExtent = 0.0,
this.hasLayoutExtent = false,
super.child,
- }) : assert(refreshIndicatorLayoutExtent != null),
- assert(refreshIndicatorLayoutExtent >= 0.0),
- assert(hasLayoutExtent != null);
+ }) : assert(refreshIndicatorLayoutExtent >= 0.0);
// The amount of space the indicator should occupy in the sliver in a
// resting state when in the refreshing mode.
@@ -61,9 +59,7 @@
required double refreshIndicatorExtent,
required bool hasLayoutExtent,
RenderBox? child,
- }) : assert(refreshIndicatorExtent != null),
- assert(refreshIndicatorExtent >= 0.0),
- assert(hasLayoutExtent != null),
+ }) : assert(refreshIndicatorExtent >= 0.0),
_refreshIndicatorExtent = refreshIndicatorExtent,
_hasLayoutExtent = hasLayoutExtent {
this.child = child;
@@ -74,7 +70,6 @@
double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent;
double _refreshIndicatorExtent;
set refreshIndicatorLayoutExtent(double value) {
- assert(value != null);
assert(value >= 0.0);
if (value == _refreshIndicatorExtent) {
return;
@@ -89,7 +84,6 @@
bool get hasLayoutExtent => _hasLayoutExtent;
bool _hasLayoutExtent;
set hasLayoutExtent(bool value) {
- assert(value != null);
if (value == _hasLayoutExtent) {
return;
}
@@ -303,9 +297,7 @@
this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent,
this.builder = buildRefreshIndicator,
this.onRefresh,
- }) : assert(refreshTriggerPullDistance != null),
- assert(refreshTriggerPullDistance > 0.0),
- assert(refreshIndicatorExtent != null),
+ }) : assert(refreshTriggerPullDistance > 0.0),
assert(refreshIndicatorExtent >= 0.0),
assert(
refreshTriggerPullDistance >= refreshIndicatorExtent,
diff --git a/framework/lib/src/cupertino/route.dart b/framework/lib/src/cupertino/route.dart
index 587fa13..bb158ad 100644
--- a/framework/lib/src/cupertino/route.dart
+++ b/framework/lib/src/cupertino/route.dart
@@ -225,21 +225,11 @@
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget child = buildContent(context);
- final Widget result = Semantics(
+ return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: child,
);
- assert(() {
- if (child == null) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('The builder for route "${settings.name}" returned null.'),
- ErrorDescription('Route builders must never return null.'),
- ]);
- }
- return true;
- }());
- return result;
}
// Called by _CupertinoBackGestureDetector when a pop ("back") drag start
@@ -343,9 +333,7 @@
this.maintainState = true,
super.fullscreenDialog,
super.allowSnapshotting = true,
- }) : assert(builder != null),
- assert(maintainState != null),
- assert(fullscreenDialog != null) {
+ }) {
assert(opaque);
}
@@ -373,8 +361,7 @@
_PageBasedCupertinoPageRoute({
required CupertinoPage<T> page,
super.allowSnapshotting = true,
- }) : assert(page != null),
- super(settings: page) {
+ }) : super(settings: page) {
assert(opaque);
}
@@ -424,9 +411,7 @@
super.name,
super.arguments,
super.restorationId,
- }) : assert(child != null),
- assert(maintainState != null),
- assert(fullscreenDialog != null);
+ });
/// The content to be shown in the [Route] created by this page.
final Widget child;
@@ -468,8 +453,7 @@
required Animation<double> secondaryRouteAnimation,
required this.child,
required bool linearTransition,
- }) : assert(linearTransition != null),
- _primaryPositionAnimation =
+ }) : _primaryPositionAnimation =
(linearTransition
? primaryRouteAnimation
: CurvedAnimation(
@@ -607,9 +591,7 @@
required this.enabledCallback,
required this.onStartPopGesture,
required this.child,
- }) : assert(enabledCallback != null),
- assert(onStartPopGesture != null),
- assert(child != null);
+ });
final Widget child;
@@ -690,8 +672,8 @@
// For devices with notches, the drag area needs to be larger on the side
// that has the notch.
double dragAreaWidth = Directionality.of(context) == TextDirection.ltr ?
- MediaQuery.of(context).padding.left :
- MediaQuery.of(context).padding.right;
+ MediaQuery.paddingOf(context).left :
+ MediaQuery.paddingOf(context).right;
dragAreaWidth = max(dragAreaWidth, _kBackGestureWidth);
return Stack(
fit: StackFit.passthrough,
@@ -731,8 +713,7 @@
_CupertinoBackGestureController({
required this.navigator,
required this.controller,
- }) : assert(navigator != null),
- assert(controller != null) {
+ }) {
navigator.didStartUserGesture();
}
@@ -854,7 +835,6 @@
_CupertinoEdgeShadowDecoration? b,
double t,
) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -921,8 +901,7 @@
_CupertinoEdgeShadowPainter(
this._decoration,
super.onChange,
- ) : assert(_decoration != null),
- assert(_decoration._colors == null || _decoration._colors!.length > 1);
+ ) : assert(_decoration._colors == null || _decoration._colors!.length > 1);
final _CupertinoEdgeShadowDecoration _decoration;
@@ -1031,14 +1010,12 @@
this.barrierLabel = 'Dismiss',
this.barrierColor = kCupertinoModalBarrierColor,
bool barrierDismissible = true,
- bool? semanticsDismissible,
+ bool semanticsDismissible = false,
super.filter,
super.settings,
this.anchorPoint,
- }) {
- _barrierDismissible = barrierDismissible;
- _semanticsDismissible = semanticsDismissible;
- }
+ }) : _barrierDismissible = barrierDismissible,
+ _semanticsDismissible = semanticsDismissible;
/// A builder that builds the widget tree for the [CupertinoModalPopupRoute].
///
@@ -1050,9 +1027,9 @@
/// widget needs to update dynamically.
final WidgetBuilder builder;
- bool? _barrierDismissible;
+ final bool _barrierDismissible;
- bool? _semanticsDismissible;
+ final bool _semanticsDismissible;
@override
final String barrierLabel;
@@ -1061,10 +1038,10 @@
final Color? barrierColor;
@override
- bool get barrierDismissible => _barrierDismissible ?? true;
+ bool get barrierDismissible => _barrierDismissible;
@override
- bool get semanticsDismissible => _semanticsDismissible ?? false;
+ bool get semanticsDismissible => _semanticsDismissible;
@override
Duration get transitionDuration => _kModalPopupTransitionDuration;
@@ -1188,11 +1165,10 @@
Color barrierColor = kCupertinoModalBarrierColor,
bool barrierDismissible = true,
bool useRootNavigator = true,
- bool? semanticsDismissible,
+ bool semanticsDismissible = false,
RouteSettings? routeSettings,
Offset? anchorPoint,
}) {
- assert(useRootNavigator != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push(
CupertinoModalPopupRoute<T>(
builder: builder,
@@ -1297,8 +1273,6 @@
RouteSettings? routeSettings,
Offset? anchorPoint,
}) {
- assert(builder != null);
- assert(useRootNavigator != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>(
builder: builder,
@@ -1361,8 +1335,7 @@
super.transitionBuilder = _buildCupertinoDialogTransitions,
super.settings,
super.anchorPoint,
- }) : assert(barrierDismissible != null),
- super(
+ }) : super(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
},
diff --git a/framework/lib/src/cupertino/scrollbar.dart b/framework/lib/src/cupertino/scrollbar.dart
index b7bce96..ee75dc1 100644
--- a/framework/lib/src/cupertino/scrollbar.dart
+++ b/framework/lib/src/cupertino/scrollbar.dart
@@ -29,7 +29,7 @@
/// An iOS style scrollbar.
///
-/// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in
+/// To add a scrollbar to a [ScrollView], wrap the scroll view widget in
/// a [CupertinoScrollbar] widget.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=DbkIQSvwnZc}
@@ -87,12 +87,8 @@
'This feature was deprecated after v2.9.0-1.0.pre.',
)
bool? isAlwaysShown,
- }) : assert(thickness != null),
- assert(thickness < double.infinity),
- assert(thicknessWhileDragging != null),
+ }) : assert(thickness < double.infinity),
assert(thicknessWhileDragging < double.infinity),
- assert(radius != null),
- assert(radiusWhileDragging != null),
assert(
isAlwaysShown == null || thumbVisibility == null,
'Scrollbar thumb appearance should only be controlled with thumbVisibility, '
@@ -171,7 +167,7 @@
..mainAxisMargin = _kScrollbarMainAxisMargin
..crossAxisMargin = _kScrollbarCrossAxisMargin
..radius = _radius
- ..padding = MediaQuery.of(context).padding
+ ..padding = MediaQuery.paddingOf(context)
..minLength = _kScrollbarMinLength
..minOverscrollLength = _kScrollbarMinOverscrollLength
..scrollbarOrientation = widget.scrollbarOrientation;
diff --git a/framework/lib/src/cupertino/search_field.dart b/framework/lib/src/cupertino/search_field.dart
index e42ac78..e973781 100644
--- a/framework/lib/src/cupertino/search_field.dart
+++ b/framework/lib/src/cupertino/search_field.dart
@@ -122,18 +122,12 @@
this.focusNode,
this.smartQuotesType,
this.smartDashesType,
+ this.enableIMEPersonalizedLearning = true,
this.autofocus = false,
this.onTap,
this.autocorrect = true,
this.enabled,
- }) : assert(padding != null),
- assert(itemColor != null),
- assert(itemSize != null),
- assert(prefixInsets != null),
- assert(suffixInsets != null),
- assert(suffixIcon != null),
- assert(suffixMode != null),
- assert(
+ }) : assert(
!((decoration != null) && (backgroundColor != null)),
'Cannot provide both a background color and a decoration\n'
'To provide both, use "decoration: BoxDecoration(color: '
@@ -318,6 +312,9 @@
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
final SmartDashesType? smartDashesType;
+ /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
+ final bool enableIMEPersonalizedLearning;
+
/// Disables the text field when false.
///
/// Text fields in disabled states have a light grey background and don't
@@ -460,6 +457,7 @@
autocorrect: widget.autocorrect,
smartQuotesType: widget.smartQuotesType,
smartDashesType: widget.smartDashesType,
+ enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
textInputAction: TextInputAction.search,
);
}
diff --git a/framework/lib/src/cupertino/segmented_control.dart b/framework/lib/src/cupertino/segmented_control.dart
index 4df14ac..1292642 100644
--- a/framework/lib/src/cupertino/segmented_control.dart
+++ b/framework/lib/src/cupertino/segmented_control.dart
@@ -99,9 +99,7 @@
this.borderColor,
this.pressedColor,
this.padding,
- }) : assert(children != null),
- assert(children.length >= 2),
- assert(onValueChanged != null),
+ }) : assert(children.length >= 2),
assert(
groupValue == null || children.keys.any((T child) => child == groupValue),
'The groupValue must be either null or one of the keys in the children map.',
@@ -407,7 +405,7 @@
}
class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
- _SegmentedControlRenderWidget({
+ const _SegmentedControlRenderWidget({
super.key,
super.children,
required this.selectedIndex,
@@ -458,8 +456,7 @@
required TextDirection textDirection,
required List<Color> backgroundColors,
required Color borderColor,
- }) : assert(textDirection != null),
- _textDirection = textDirection,
+ }) : _textDirection = textDirection,
_selectedIndex = selectedIndex,
_pressedIndex = pressedIndex,
_backgroundColors = backgroundColors,
@@ -685,7 +682,6 @@
}
void _paintChild(PaintingContext context, Offset offset, RenderBox child, int childIndex) {
- assert(child != null);
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
@@ -708,7 +704,6 @@
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
- assert(position != null);
RenderBox? child = lastChild;
while (child != null) {
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
diff --git a/framework/lib/src/cupertino/slider.dart b/framework/lib/src/cupertino/slider.dart
index ecc6cb0..cd962c9 100644
--- a/framework/lib/src/cupertino/slider.dart
+++ b/framework/lib/src/cupertino/slider.dart
@@ -71,12 +71,8 @@
this.divisions,
this.activeColor,
this.thumbColor = CupertinoColors.white,
- }) : assert(value != null),
- assert(min != null),
- assert(max != null),
- assert(value >= min && value <= max),
- assert(divisions == null || divisions > 0),
- assert(thumbColor != null);
+ }) : assert(value >= min && value <= max),
+ assert(divisions == null || divisions > 0);
/// The currently selected value for this slider.
///
@@ -337,9 +333,7 @@
required TickerProvider vsync,
required TextDirection textDirection,
MouseCursor cursor = MouseCursor.defer,
- }) : assert(value != null && value >= 0.0 && value <= 1.0),
- assert(textDirection != null),
- assert(cursor != null),
+ }) : assert(value >= 0.0 && value <= 1.0),
_cursor = cursor,
_value = value,
_divisions = divisions,
@@ -363,7 +357,7 @@
double get value => _value;
double _value;
set value(double newValue) {
- assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
+ assert(newValue >= 0.0 && newValue <= 1.0);
if (newValue == _value) {
return;
}
@@ -435,7 +429,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textDirection == value) {
return;
}
diff --git a/framework/lib/src/cupertino/sliding_segmented_control.dart b/framework/lib/src/cupertino/sliding_segmented_control.dart
index 1e94b55..ca0affe 100644
--- a/framework/lib/src/cupertino/sliding_segmented_control.dart
+++ b/framework/lib/src/cupertino/sliding_segmented_control.dart
@@ -320,10 +320,7 @@
this.thumbColor = _kThumbColor,
this.padding = _kHorizontalItemPadding,
this.backgroundColor = CupertinoColors.tertiarySystemFill,
- }) : assert(children != null),
- assert(children.length >= 2),
- assert(padding != null),
- assert(onValueChanged != null),
+ }) : assert(children.length >= 2),
assert(
groupValue == null || children.keys.contains(groupValue),
'The groupValue must be either null or one of the keys in the children map.',
@@ -523,7 +520,6 @@
// _Segment widget) to make the overall animation look natural when the thumb
// is not sliding.
void _playThumbScaleAnimation({ required bool isExpanding }) {
- assert(isExpanding != null);
thumbScaleAnimation = thumbScaleController.drive(
Tween<double>(
begin: thumbScaleAnimation.value,
@@ -715,7 +711,7 @@
}
class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
- _SegmentedControlRenderWidget({
+ const _SegmentedControlRenderWidget({
super.key,
super.children,
required this.highlightedIndex,
@@ -793,8 +789,7 @@
required this.state,
}) : _highlightedIndex = highlightedIndex,
_thumbColor = thumbColor,
- _thumbScale = thumbScale,
- assert(state != null);
+ _thumbScale = thumbScale;
final _SegmentedControlState<T> state;
@@ -1074,13 +1069,11 @@
// Paint the separator to the right of the given child.
final Paint separatorPaint = Paint();
void _paintSeparator(PaintingContext context, Offset offset, RenderBox child) {
- assert(child != null);
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
context.paintChild(child, offset + childParentData.offset);
}
void _paintChild(PaintingContext context, Offset offset, RenderBox child) {
- assert(child != null);
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
context.paintChild(child, childParentData.offset + offset);
}
@@ -1119,7 +1112,6 @@
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
- assert(position != null);
RenderBox? child = lastChild;
while (child != null) {
final _SegmentedControlContainerBoxParentData childParentData =
diff --git a/framework/lib/src/cupertino/switch.dart b/framework/lib/src/cupertino/switch.dart
index bec0371..dedaf4e 100644
--- a/framework/lib/src/cupertino/switch.dart
+++ b/framework/lib/src/cupertino/switch.dart
@@ -14,6 +14,7 @@
import 'package:flute/widgets.dart';
import 'colors.dart';
+import 'theme.dart';
import 'thumb_painter.dart';
// Examples can assume:
@@ -72,9 +73,10 @@
this.activeColor,
this.trackColor,
this.thumbColor,
+ this.applyTheme,
+ this.focusColor,
this.dragStartBehavior = DragStartBehavior.start,
- }) : assert(value != null),
- assert(dragStartBehavior != null);
+ });
/// Whether this switch is on or off.
///
@@ -105,13 +107,15 @@
/// ```
final ValueChanged<bool>? onChanged;
- /// The color to use when this switch is on.
+ /// The color to use for the track when the switch is on.
///
- /// Defaults to [CupertinoColors.systemGreen] when null and ignores
- /// the [CupertinoTheme] in accordance to native iOS behavior.
+ /// If null and [applyTheme] is false, defaults to [CupertinoColors.systemGreen]
+ /// in accordance to native iOS behavior. Otherwise, defaults to
+ /// [CupertinoThemeData.primaryColor].
final Color? activeColor;
- /// The color to use for the background when the switch is off.
+
+ /// The color to use for the track when the switch is off.
///
/// Defaults to [CupertinoColors.secondarySystemFill] when null.
final Color? trackColor;
@@ -121,6 +125,21 @@
/// Defaults to [CupertinoColors.white] when null.
final Color? thumbColor;
+ /// The color to use for the focus highlight for keyboard interactions.
+ ///
+ /// Defaults to a slightly transparent [activeColor].
+ final Color? focusColor;
+
+ /// {@template flutter.cupertino.CupertinoSwitch.applyTheme}
+ /// Whether to apply the ambient [CupertinoThemeData].
+ ///
+ /// If true, the track uses [CupertinoThemeData.primaryColor] for the track
+ /// when the switch is on.
+ ///
+ /// Defaults to [CupertinoThemeData.applyThemeToAll].
+ /// {@endtemplate}
+ final bool? applyTheme;
+
/// {@template flutter.cupertino.CupertinoSwitch.dragStartBehavior}
/// Determines the way that drag start behavior is handled.
///
@@ -164,8 +183,14 @@
late AnimationController _reactionController;
late Animation<double> _reaction;
+ late bool isFocused;
+
bool get isInteractive => widget.onChanged != null;
+ late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
+ ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
+ };
+
// A non-null boolean value that changes to true at the end of a drag if the
// switch must be animated to the position indicated by the widget's value.
bool needsPositionAnimation = false;
@@ -174,6 +199,8 @@
void initState() {
super.initState();
+ isFocused = false;
+
_tap = TapGestureRecognizer()
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
@@ -239,7 +266,7 @@
_reactionController.forward();
}
- void _handleTap() {
+ void _handleTap([Intent? _]) {
if (isInteractive) {
widget.onChanged!(!widget.value);
_emitVibration();
@@ -308,8 +335,19 @@
}
}
+ void _onShowFocusHighlight(bool showHighlight) {
+ setState(() { isFocused = showHighlight; });
+ }
+
@override
Widget build(BuildContext context) {
+ final CupertinoThemeData theme = CupertinoTheme.of(context);
+ final Color activeColor = CupertinoDynamicColor.resolve(
+ widget.activeColor
+ ?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null)
+ ?? CupertinoColors.systemGreen,
+ context,
+ );
if (needsPositionAnimation) {
_resumePositionAnimation();
}
@@ -317,17 +355,29 @@
cursor: isInteractive && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
child: Opacity(
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0,
- child: _CupertinoSwitchRenderObjectWidget(
- value: widget.value,
- activeColor: CupertinoDynamicColor.resolve(
- widget.activeColor ?? CupertinoColors.systemGreen,
- context,
+ child: FocusableActionDetector(
+ onShowFocusHighlight: _onShowFocusHighlight,
+ actions: _actionMap,
+ enabled: isInteractive,
+ child: _CupertinoSwitchRenderObjectWidget(
+ value: widget.value,
+ 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
+ // color pickers on the switches in the macOS settings.
+ focusColor: CupertinoDynamicColor.resolve(
+ widget.focusColor ??
+ HSLColor
+ .fromColor(activeColor.withOpacity(0.80))
+ .withLightness(0.69).withSaturation(0.835)
+ .toColor(),
+ context),
+ onChanged: widget.onChanged,
+ textDirection: Directionality.of(context),
+ isFocused: isFocused,
+ state: this,
),
- trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
- thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor ?? CupertinoColors.white, context),
- onChanged: widget.onChanged,
- textDirection: Directionality.of(context),
- state: this,
),
),
);
@@ -350,8 +400,10 @@
required this.activeColor,
required this.trackColor,
required this.thumbColor,
+ required this.focusColor,
required this.onChanged,
required this.textDirection,
+ required this.isFocused,
required this.state,
});
@@ -359,9 +411,11 @@
final Color activeColor;
final Color trackColor;
final Color thumbColor;
+ final Color focusColor;
final ValueChanged<bool>? onChanged;
final _CupertinoSwitchState state;
final TextDirection textDirection;
+ final bool isFocused;
@override
_RenderCupertinoSwitch createRenderObject(BuildContext context) {
@@ -370,8 +424,10 @@
activeColor: activeColor,
trackColor: trackColor,
thumbColor: thumbColor,
+ focusColor: focusColor,
onChanged: onChanged,
textDirection: textDirection,
+ isFocused: isFocused,
state: state,
);
}
@@ -384,8 +440,10 @@
..activeColor = activeColor
..trackColor = trackColor
..thumbColor = thumbColor
+ ..focusColor = focusColor
..onChanged = onChanged
- ..textDirection = textDirection;
+ ..textDirection = textDirection
+ ..isFocused = isFocused;
}
}
@@ -409,18 +467,19 @@
required Color activeColor,
required Color trackColor,
required Color thumbColor,
+ required Color focusColor,
ValueChanged<bool>? onChanged,
required TextDirection textDirection,
+ required bool isFocused,
required _CupertinoSwitchState state,
- }) : assert(value != null),
- assert(activeColor != null),
- assert(state != null),
- _value = value,
+ }) : _value = value,
_activeColor = activeColor,
_trackColor = trackColor,
+ _focusColor = focusColor,
_thumbPainter = CupertinoThumbPainter.switchThumb(color: thumbColor),
_onChanged = onChanged,
_textDirection = textDirection,
+ _isFocused = isFocused,
_state = state,
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) {
state.position.addListener(markNeedsPaint);
@@ -432,7 +491,6 @@
bool get value => _value;
bool _value;
set value(bool value) {
- assert(value != null);
if (value == _value) {
return;
}
@@ -443,7 +501,6 @@
Color get activeColor => _activeColor;
Color _activeColor;
set activeColor(Color value) {
- assert(value != null);
if (value == _activeColor) {
return;
}
@@ -454,7 +511,6 @@
Color get trackColor => _trackColor;
Color _trackColor;
set trackColor(Color value) {
- assert(value != null);
if (value == _trackColor) {
return;
}
@@ -465,7 +521,6 @@
Color get thumbColor => _thumbPainter.color;
CupertinoThumbPainter _thumbPainter;
set thumbColor(Color value) {
- assert(value != null);
if (value == thumbColor) {
return;
}
@@ -473,6 +528,16 @@
markNeedsPaint();
}
+ Color get focusColor => _focusColor;
+ Color _focusColor;
+ set focusColor(Color value) {
+ if (value == _focusColor) {
+ return;
+ }
+ _focusColor = value;
+ markNeedsPaint();
+ }
+
ValueChanged<bool>? get onChanged => _onChanged;
ValueChanged<bool>? _onChanged;
set onChanged(ValueChanged<bool>? value) {
@@ -490,7 +555,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textDirection == value) {
return;
}
@@ -498,6 +562,16 @@
markNeedsPaint();
}
+ bool get isFocused => _isFocused;
+ bool _isFocused;
+ set isFocused(bool value) {
+ if(value == _isFocused) {
+ return;
+ }
+ _isFocused = value;
+ markNeedsPaint();
+ }
+
bool get isInteractive => onChanged != null;
@override
@@ -553,6 +627,18 @@
final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
canvas.drawRRect(trackRRect, paint);
+ if(_isFocused) {
+ // Paints a border around the switch in the focus color.
+ final RRect borderTrackRRect = trackRRect.inflate(1.75);
+
+ final Paint borderPaint = Paint()
+ ..color = focusColor
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 3.5;
+
+ canvas.drawRRect(borderTrackRRect, borderPaint);
+ }
+
final double currentThumbExtension = CupertinoThumbPainter.extension * currentReactionValue;
final double thumbLeft = lerpDouble(
trackRect.left + _kTrackInnerStart - CupertinoThumbPainter.radius,
diff --git a/framework/lib/src/cupertino/tab_scaffold.dart b/framework/lib/src/cupertino/tab_scaffold.dart
index 8eddfe8..20ce8b8 100644
--- a/framework/lib/src/cupertino/tab_scaffold.dart
+++ b/framework/lib/src/cupertino/tab_scaffold.dart
@@ -36,7 +36,6 @@
/// greater than or equal to 0, and less than the total number of tabs.
CupertinoTabController({ int initialIndex = 0 })
: _index = initialIndex,
- assert(initialIndex != null),
assert(initialIndex >= 0);
bool _isDisposed = false;
@@ -52,7 +51,6 @@
int get index => _index;
int _index;
set index(int value) {
- assert(value != null);
assert(value >= 0);
if (_index == value) {
return;
@@ -135,9 +133,7 @@
this.backgroundColor,
this.resizeToAvoidBottomInset = true,
this.restorationId,
- }) : assert(tabBar != null),
- assert(tabBuilder != null),
- assert(
+ }) : assert(
controller == null || controller.index < tabBar.items.length,
"The CupertinoTabController's current index ${controller.index} is "
'out of bounds for the tab bar with ${tabBar.items.length} tabs',
@@ -324,12 +320,10 @@
contentPadding = EdgeInsets.only(bottom: existingMediaQuery.viewInsets.bottom);
}
- if (widget.tabBar != null &&
- // Only pad the content with the height of the tab bar if the tab
- // isn't already entirely obstructed by a keyboard or other view insets.
- // Don't double pad.
- (!widget.resizeToAvoidBottomInset ||
- widget.tabBar.preferredSize.height > existingMediaQuery.viewInsets.bottom)) {
+ // Only pad the content with the height of the tab bar if the tab
+ // isn't already entirely obstructed by a keyboard or other view insets.
+ // Don't double pad.
+ if (!widget.resizeToAvoidBottomInset || widget.tabBar.preferredSize.height > existingMediaQuery.viewInsets.bottom) {
// TODO(xster): Use real size after partial layout instead of preferred size.
// https://github.com/flutter/flutter/issues/12912
final double bottomPadding =
@@ -406,9 +400,7 @@
required this.currentTabIndex,
required this.tabCount,
required this.tabBuilder,
- }) : assert(currentTabIndex != null),
- assert(tabCount != null && tabCount > 0),
- assert(tabBuilder != null);
+ }) : assert(tabCount > 0);
final int currentTabIndex;
final int tabCount;
@@ -530,8 +522,7 @@
/// The `initialIndex` must not be null and defaults to 0. The value must be
/// greater than or equal to 0, and less than the total number of tabs.
RestorableCupertinoTabController({ int initialIndex = 0 })
- : assert(initialIndex != null),
- assert(initialIndex >= 0),
+ : assert(initialIndex >= 0),
_initialIndex = initialIndex;
final int _initialIndex;
diff --git a/framework/lib/src/cupertino/tab_view.dart b/framework/lib/src/cupertino/tab_view.dart
index 4c4d791..d091c86 100644
--- a/framework/lib/src/cupertino/tab_view.dart
+++ b/framework/lib/src/cupertino/tab_view.dart
@@ -50,7 +50,7 @@
this.onUnknownRoute,
this.navigatorObservers = const <NavigatorObserver>[],
this.restorationScopeId,
- }) : assert(navigatorObservers != null);
+ });
/// The widget builder for the default route of the tab view
/// ([Navigator.defaultRouteName], which is `/`).
@@ -175,13 +175,13 @@
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
final String? name = settings.name;
- WidgetBuilder? routeBuilder;
+ final WidgetBuilder? routeBuilder;
String? title;
if (name == Navigator.defaultRouteName && widget.builder != null) {
routeBuilder = widget.builder;
title = widget.defaultTitle;
- } else if (widget.routes != null) {
- routeBuilder = widget.routes![name];
+ } else {
+ routeBuilder = widget.routes?[name];
}
if (routeBuilder != null) {
return CupertinoPageRoute<dynamic>(
@@ -190,10 +190,7 @@
settings: settings,
);
}
- if (widget.onGenerateRoute != null) {
- return widget.onGenerateRoute!(settings);
- }
- return null;
+ return widget.onGenerateRoute?.call(settings);
}
Route<dynamic>? _onUnknownRoute(RouteSettings settings) {
diff --git a/framework/lib/src/cupertino/text_field.dart b/framework/lib/src/cupertino/text_field.dart
index 9ac4d24..a8ec7e6 100644
--- a/framework/lib/src/cupertino/text_field.dart
+++ b/framework/lib/src/cupertino/text_field.dart
@@ -102,7 +102,7 @@
final _CupertinoTextFieldState _state;
@override
- void onSingleTapUp(TapUpDetails details) {
+ void onSingleTapUp(TapDragUpDetails details) {
// Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger
// this handler. If the clear button widget recognizes the up event,
@@ -120,7 +120,7 @@
}
@override
- void onDragSelectionEnd(DragEndDetails details) {
+ void onDragSelectionEnd(TapDragEndDetails details) {
_state._requestKeyboard();
}
}
@@ -274,6 +274,7 @@
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
+ this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
@@ -281,35 +282,21 @@
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.spellCheckConfiguration,
this.magnifierConfiguration,
- }) : assert(textAlign != null),
- assert(readOnly != null),
- assert(autofocus != null),
- assert(obscuringCharacter != null && obscuringCharacter.length == 1),
- assert(obscureText != null),
- assert(autocorrect != null),
+ }) : assert(obscuringCharacter.length == 1),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
- assert(enableSuggestions != null),
- assert(scrollPadding != null),
- assert(dragStartBehavior != null),
- assert(selectionHeightStyle != null),
- assert(selectionWidthStyle != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength > 0),
- assert(clearButtonMode != null),
- assert(prefixMode != null),
- assert(suffixMode != null),
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
assert(
!identical(textInputAction, TextInputAction.newline) ||
@@ -317,7 +304,6 @@
!identical(keyboardType, TextInputType.text),
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
),
- assert(enableIMEPersonalizedLearning != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
@@ -418,6 +404,7 @@
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
+ this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
@@ -425,35 +412,21 @@
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.spellCheckConfiguration,
this.magnifierConfiguration,
- }) : assert(textAlign != null),
- assert(readOnly != null),
- assert(autofocus != null),
- assert(obscuringCharacter != null && obscuringCharacter.length == 1),
- assert(obscureText != null),
- assert(autocorrect != null),
+ }) : assert(obscuringCharacter.length == 1),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
- assert(enableSuggestions != null),
- assert(scrollPadding != null),
- assert(dragStartBehavior != null),
- assert(selectionHeightStyle != null),
- assert(selectionWidthStyle != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength > 0),
- assert(clearButtonMode != null),
- assert(prefixMode != null),
- assert(suffixMode != null),
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
assert(
!identical(textInputAction, TextInputAction.newline) ||
@@ -461,8 +434,6 @@
!identical(keyboardType, TextInputType.text),
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
),
- assert(clipBehavior != null),
- assert(enableIMEPersonalizedLearning != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
@@ -754,6 +725,9 @@
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
+ /// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
+ final ContentInsertionConfiguration? contentInsertionConfiguration;
+
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, will build a default menu based on the platform.
@@ -850,6 +824,7 @@
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
+ properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
}
static final TextMagnifierConfiguration _iosMagnifierConfiguration = TextMagnifierConfiguration(
@@ -1102,9 +1077,6 @@
}
Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) {
- assert(editableText != null);
- assert(textStyle != null);
- assert(placeholderStyle != null);
// If there are no surrounding widgets, just return the core editable text
// part.
if (!_hasDecoration) {
@@ -1224,7 +1196,7 @@
}
final bool enabled = widget.enabled ?? true;
- final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0);
+ final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0);
final List<TextInputFormatter> formatters = <TextInputFormatter>[
...?widget.inputFormatters,
if (widget.maxLength != null)
@@ -1267,7 +1239,7 @@
? side
: side.copyWith(color: CupertinoDynamicColor.resolve(side.color, context));
}
- resolvedBorder = border == null || border.runtimeType != Border
+ resolvedBorder = border.runtimeType != Border
? border
: Border(
top: resolveBorderSide(border.top),
@@ -1362,6 +1334,7 @@
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
+ contentInsertionConfiguration: widget.contentInsertionConfiguration,
contextMenuBuilder: widget.contextMenuBuilder,
spellCheckConfiguration: spellCheckConfiguration,
),
diff --git a/framework/lib/src/cupertino/text_form_field_row.dart b/framework/lib/src/cupertino/text_form_field_row.dart
index 17227c2..f7991e3 100644
--- a/framework/lib/src/cupertino/text_form_field_row.dart
+++ b/framework/lib/src/cupertino/text_form_field_row.dart
@@ -13,7 +13,7 @@
/// Creates a [CupertinoFormRow] containing a [FormField] that wraps
/// a [CupertinoTextField].
///
-/// A [Form] ancestor is not required. The [Form] simply makes it easier to
+/// A [Form] ancestor is not required. The [Form] allows one to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
@@ -158,28 +158,19 @@
),
EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder,
}) : assert(initialValue == null || controller == null),
- assert(textAlign != null),
- assert(autofocus != null),
- assert(readOnly != null),
- assert(obscuringCharacter != null && obscuringCharacter.length == 1),
- assert(obscureText != null),
- assert(autocorrect != null),
- assert(enableSuggestions != null),
- assert(scrollPadding != null),
+ assert(obscuringCharacter.length == 1),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength > 0),
- assert(enableInteractiveSelection != null),
super(
initialValue: controller?.text ?? initialValue ?? '',
builder: (FormFieldState<String> field) {
diff --git a/framework/lib/src/cupertino/text_selection.dart b/framework/lib/src/cupertino/text_selection.dart
index 7aef851..3cf5d19 100644
--- a/framework/lib/src/cupertino/text_selection.dart
+++ b/framework/lib/src/cupertino/text_selection.dart
@@ -260,14 +260,14 @@
}
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
+ final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context);
// The toolbar should appear below the TextField when there is not enough
// space above the TextField to show it, assuming there's always enough
// space at the bottom in this case.
final double anchorX = clampDouble(widget.selectionMidpoint.dx + widget.globalEditableRegion.left,
- _kArrowScreenPadding + mediaQuery.padding.left,
- mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
+ _kArrowScreenPadding + mediaQueryPadding.left,
+ MediaQuery.sizeOf(context).width - mediaQueryPadding.right - _kArrowScreenPadding,
);
final double topAmountInEditableRegion = widget.endpoints.first.point.dy - widget.textLineHeight;
@@ -289,7 +289,7 @@
final List<Widget> items = <Widget>[];
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
final Widget onePhysicalPixelVerticalDivider =
- SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
+ SizedBox(width: 1.0 / MediaQuery.devicePixelRatioOf(context));
void addToolbarButton(
String text,
diff --git a/framework/lib/src/cupertino/text_selection_toolbar.dart b/framework/lib/src/cupertino/text_selection_toolbar.dart
index b92ea6a..3d55da0 100644
--- a/framework/lib/src/cupertino/text_selection_toolbar.dart
+++ b/framework/lib/src/cupertino/text_selection_toolbar.dart
@@ -5,11 +5,13 @@
import 'dart:collection';
import 'package:engine/ui.dart' as ui;
-import 'package:flute/foundation.dart' show clampDouble;
+import 'package:flute/foundation.dart' show Brightness, clampDouble;
import 'package:flute/rendering.dart';
import 'package:flute/widgets.dart';
+import 'colors.dart';
import 'text_selection_toolbar_button.dart';
+import 'theme.dart';
// Values extracted from https://developer.apple.com/design/resources/.
// The height of the toolbar, including the arrow.
@@ -17,9 +19,6 @@
// Vertical distance between the tip of the arrow and the line of text the arrow
// is pointing to. The value used here is eyeballed.
const double _kToolbarContentDistance = 8.0;
-// Minimal padding from all edges of the selection toolbar to all edges of the
-// screen.
-const double _kToolbarScreenPadding = 8.0;
const Size _kToolbarArrowSize = Size(14.0, 7.0);
// Minimal padding from tip of the selection toolbar arrow to horizontal edges of the
@@ -29,9 +28,27 @@
// Values extracted from https://developer.apple.com/design/resources/.
const Radius _kToolbarBorderRadius = Radius.circular(8);
-// Colors extracted from https://developer.apple.com/design/resources/.
-// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
-const Color _kToolbarDividerColor = Color(0xFF808080);
+const CupertinoDynamicColor _kToolbarDividerColor = CupertinoDynamicColor.withBrightness(
+ // This value was extracted from a screenshot of iOS 16.0.3, as light mode
+ // didn't appear in the Apple design resources assets linked below.
+ color: Color(0xFFB6B6B6),
+ // Color extracted from https://developer.apple.com/design/resources/.
+ // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
+ darkColor: Color(0xFF808080),
+);
+
+// These values were extracted from a screenshot of iOS 16.0.3, as light mode
+// didn't appear in the Apple design resources assets linked above.
+final BoxDecoration _kToolbarShadow = BoxDecoration(
+ borderRadius: const BorderRadius.all(_kToolbarBorderRadius),
+ boxShadow: <BoxShadow>[
+ BoxShadow(
+ color: CupertinoColors.black.withOpacity(0.1),
+ blurRadius: 16.0,
+ offset: Offset(0, _kToolbarArrowSize.height / 2),
+ ),
+ ],
+);
/// The type for a Function that builds a toolbar's container with the given
/// child.
@@ -53,7 +70,7 @@
class _CupertinoToolbarButtonDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
+ return SizedBox(width: 1.0 / MediaQuery.devicePixelRatioOf(context));
}
}
@@ -103,7 +120,17 @@
/// default Cupertino toolbar.
final CupertinoToolbarBuilder toolbarBuilder;
- // Add the visial vertical line spacer between children buttons.
+ /// Minimal padding from all edges of the selection toolbar to all edges of the
+ /// viewport.
+ ///
+ /// See also:
+ ///
+ /// * [SpellCheckSuggestionsToolbar], which uses this same value for its
+ /// padding from the edges of the viewport.
+ /// * [TextSelectionToolbar], which uses this same value as well.
+ static const double kToolbarScreenPadding = 8.0;
+
+ // Add the visual vertical line spacer between children buttons.
static List<Widget> _addChildrenSpacers(List<Widget> children) {
final List<Widget> nextChildren = <Widget>[];
for (int i = 0; i < children.length; i++) {
@@ -119,22 +146,31 @@
// Builds a toolbar just like the default iOS toolbar, with the right color
// background and a rounded cutout with an arrow.
static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
- return _CupertinoTextSelectionToolbarShape(
+ final Widget outputChild = _CupertinoTextSelectionToolbarShape(
anchor: anchor,
isAbove: isAbove,
child: DecoratedBox(
- decoration: const BoxDecoration(color: _kToolbarDividerColor),
+ decoration: BoxDecoration(
+ color: _kToolbarDividerColor.resolveFrom(context),
+ ),
child: child,
),
);
+ if (CupertinoTheme.brightnessOf(context) == Brightness.dark) {
+ return outputChild;
+ }
+ return DecoratedBox(
+ decoration: _kToolbarShadow,
+ child: outputChild,
+ );
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
+ final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context);
- final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
+ final double paddingAbove = mediaQueryPadding.top + kToolbarScreenPadding;
final double toolbarHeightNeeded = paddingAbove
+ _kToolbarContentDistance
+ _kToolbarHeight;
@@ -142,8 +178,8 @@
// The arrow, which points to the anchor, has some margin so it can't get
// too close to the horizontal edges of the screen.
- final double leftMargin = _kArrowScreenPadding + mediaQuery.padding.left;
- final double rightMargin = mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding;
+ final double leftMargin = _kArrowScreenPadding + mediaQueryPadding.left;
+ final double rightMargin = MediaQuery.sizeOf(context).width - mediaQueryPadding.right - _kArrowScreenPadding;
final Offset anchorAboveAdjusted = Offset(
clampDouble(anchorAbove.dx, leftMargin, rightMargin),
@@ -151,15 +187,15 @@
);
final Offset anchorBelowAdjusted = Offset(
clampDouble(anchorBelow.dx, leftMargin, rightMargin),
- anchorBelow.dy - _kToolbarContentDistance + paddingAbove,
+ anchorBelow.dy + _kToolbarContentDistance - paddingAbove,
);
return Padding(
padding: EdgeInsets.fromLTRB(
- _kToolbarScreenPadding,
+ kToolbarScreenPadding,
paddingAbove,
- _kToolbarScreenPadding,
- _kToolbarScreenPadding,
+ kToolbarScreenPadding,
+ kToolbarScreenPadding,
),
child: CustomSingleChildLayout(
delegate: TextSelectionToolbarLayoutDelegate(
@@ -226,7 +262,6 @@
super.child,
);
-
@override
bool get isRepaintBoundary => true;
@@ -398,8 +433,7 @@
required this.isAbove,
required this.toolbarBuilder,
required this.children,
- }) : assert(children != null),
- assert(children.length > 0);
+ }) : assert(children.length > 0);
final Offset anchor;
final List<Widget> children;
@@ -480,12 +514,12 @@
onPressed: _handlePreviousPage,
text: 'â—€',
),
- dividerWidth: 1.0 / MediaQuery.of(context).devicePixelRatio,
+ dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context),
nextButton: CupertinoTextSelectionToolbarButton.text(
onPressed: _handleNextPage,
text: 'â–¶',
),
- nextButtonDisabled: CupertinoTextSelectionToolbarButton.text(
+ nextButtonDisabled: const CupertinoTextSelectionToolbarButton.text(
text: 'â–¶',
),
children: widget.children,
@@ -505,13 +539,7 @@
required this.dividerWidth,
required this.nextButton,
required this.nextButtonDisabled,
- }) : assert(children != null),
- assert(children.isNotEmpty),
- assert(backButton != null),
- assert(dividerWidth != null),
- assert(nextButton != null),
- assert(nextButtonDisabled != null),
- assert(page != null);
+ }) : assert(children.isNotEmpty);
final Widget backButton;
final List<Widget> children;
@@ -704,9 +732,7 @@
_RenderCupertinoTextSelectionToolbarItems({
required double dividerWidth,
required int page,
- }) : assert(dividerWidth != null),
- assert(page != null),
- _dividerWidth = dividerWidth,
+ }) : _dividerWidth = dividerWidth,
_page = page,
super();
@@ -801,8 +827,9 @@
double paginationButtonsWidth = 0.0;
if (currentPage == 0) {
// If this is the last child, it's ok to fit without a forward button.
+ // Note childCount doesn't include slotted children which come before the list ones.
paginationButtonsWidth =
- i == childCount - 1 ? 0.0 : _nextButton!.size.width;
+ i == childCount + 2 ? 0.0 : _nextButton!.size.width;
} else {
paginationButtonsWidth = subsequentPageButtonsWidth;
}
diff --git a/framework/lib/src/cupertino/text_selection_toolbar_button.dart b/framework/lib/src/cupertino/text_selection_toolbar_button.dart
index c302fe3..0807b4d 100644
--- a/framework/lib/src/cupertino/text_selection_toolbar_button.dart
+++ b/framework/lib/src/cupertino/text_selection_toolbar_button.dart
@@ -18,7 +18,17 @@
// Colors extracted from https://developer.apple.com/design/resources/.
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
-const Color _kToolbarBackgroundColor = Color(0xEB202020);
+const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
+ // This value was extracted from a screenshot of iOS 16.0.3, as light mode
+ // didn't appear in the Apple design resources assets linked above.
+ color: Color(0xEBF7F7F7),
+ darkColor: Color(0xEB202020),
+);
+
+const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness(
+ color: CupertinoColors.black,
+ darkColor: CupertinoColors.white,
+);
// Eyeballed value.
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 18.0);
@@ -32,23 +42,17 @@
super.key,
this.onPressed,
required Widget this.child,
- }) : assert(child != null),
+ }) : text = null,
buttonItem = null;
/// Create an instance of [CupertinoTextSelectionToolbarButton] whose child is
/// a [Text] widget styled like the default iOS text selection toolbar button.
- CupertinoTextSelectionToolbarButton.text({
+ const CupertinoTextSelectionToolbarButton.text({
super.key,
this.onPressed,
- required String text,
+ required this.text,
}) : buttonItem = null,
- child = Text(
- text,
- overflow: TextOverflow.ellipsis,
- style: _kToolbarButtonFontStyle.copyWith(
- color: onPressed != null ? CupertinoColors.white : CupertinoColors.inactiveGray,
- ),
- );
+ child = null;
/// Create an instance of [CupertinoTextSelectionToolbarButton] from the given
/// [ContextMenuButtonItem].
@@ -57,8 +61,8 @@
CupertinoTextSelectionToolbarButton.buttonItem({
super.key,
required ContextMenuButtonItem this.buttonItem,
- }) : assert(buttonItem != null),
- child = null,
+ }) : child = null,
+ text = null,
onPressed = buttonItem.onPressed;
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
@@ -79,6 +83,10 @@
/// {@endtemplate}
final ContextMenuButtonItem? buttonItem;
+ /// The text used in the button's label when using
+ /// [CupertinoTextSelectionToolbarButton.text].
+ final String? text;
+
/// Returns the default button label String for the button of the given
/// [ContextMenuButtonItem]'s [ContextMenuButtonType].
static String getButtonLabel(BuildContext context, ContextMenuButtonItem buttonItem) {
@@ -97,6 +105,7 @@
return localizations.pasteButtonLabel;
case ContextMenuButtonType.selectAll:
return localizations.selectAllButtonLabel;
+ case ContextMenuButtonType.delete:
case ContextMenuButtonType.custom:
return '';
}
@@ -105,12 +114,15 @@
@override
Widget build(BuildContext context) {
final Widget child = this.child ?? Text(
- getButtonLabel(context, buttonItem!),
- overflow: TextOverflow.ellipsis,
- style: _kToolbarButtonFontStyle.copyWith(
- color: onPressed != null ? CupertinoColors.white : CupertinoColors.inactiveGray,
- ),
- );
+ text ?? getButtonLabel(context, buttonItem!),
+ overflow: TextOverflow.ellipsis,
+ style: _kToolbarButtonFontStyle.copyWith(
+ color: onPressed != null
+ ? _kToolbarTextColor.resolveFrom(context)
+ : CupertinoColors.inactiveGray,
+ ),
+ );
+
return CupertinoButton(
borderRadius: null,
color: _kToolbarBackgroundColor,
diff --git a/framework/lib/src/cupertino/text_theme.dart b/framework/lib/src/cupertino/text_theme.dart
index 5252fc8..908504e 100644
--- a/framework/lib/src/cupertino/text_theme.dart
+++ b/framework/lib/src/cupertino/text_theme.dart
@@ -260,6 +260,41 @@
properties.add(DiagnosticsProperty<TextStyle>('pickerTextStyle', pickerTextStyle, defaultValue: defaultData.pickerTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('dateTimePickerTextStyle', dateTimePickerTextStyle, defaultValue: defaultData.dateTimePickerTextStyle));
}
+
+ @override
+ bool operator == (Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is CupertinoTextThemeData
+ && other._defaults == _defaults
+ && other._primaryColor == _primaryColor
+ && other._textStyle == _textStyle
+ && other._actionTextStyle == _actionTextStyle
+ && other._tabLabelTextStyle == _tabLabelTextStyle
+ && other._navTitleTextStyle == _navTitleTextStyle
+ && other._navLargeTitleTextStyle == _navLargeTitleTextStyle
+ && other._navActionTextStyle == _navActionTextStyle
+ && other._pickerTextStyle == _pickerTextStyle
+ && other._dateTimePickerTextStyle == _dateTimePickerTextStyle;
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ _defaults,
+ _primaryColor,
+ _textStyle,
+ _actionTextStyle,
+ _tabLabelTextStyle,
+ _navTitleTextStyle,
+ _navLargeTitleTextStyle,
+ _navActionTextStyle,
+ _pickerTextStyle,
+ _dateTimePickerTextStyle,
+ );
}
@@ -268,8 +303,7 @@
const _TextThemeDefaultsBuilder(
this.labelColor,
this.inactiveGrayColor,
- ) : assert(labelColor != null),
- assert(inactiveGrayColor != null);
+ );
final Color labelColor;
final Color inactiveGrayColor;
@@ -297,4 +331,20 @@
? this
: _TextThemeDefaultsBuilder(resolvedLabelColor, resolvedInactiveGray);
}
+
+ @override
+ bool operator == (Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is _TextThemeDefaultsBuilder
+ && other.labelColor == labelColor
+ && other.inactiveGrayColor == inactiveGrayColor;
+ }
+
+ @override
+ int get hashCode => Object.hash(labelColor, inactiveGrayColor);
}
diff --git a/framework/lib/src/cupertino/theme.dart b/framework/lib/src/cupertino/theme.dart
index 78729d7..1eb09aa 100644
--- a/framework/lib/src/cupertino/theme.dart
+++ b/framework/lib/src/cupertino/theme.dart
@@ -22,6 +22,7 @@
// Values extracted from navigation bar. For toolbar or tabbar the dark color is 0xF0161616.
),
CupertinoColors.systemBackground,
+ false,
_CupertinoTextThemeDefaults(CupertinoColors.label, CupertinoColors.inactiveGray),
);
@@ -52,8 +53,7 @@
super.key,
required this.data,
required this.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// The [CupertinoThemeData] styling for this theme.
final CupertinoThemeData data;
@@ -86,7 +86,7 @@
/// [MediaQueryData.platformBrightness] for descendant Cupertino widgets.
static Brightness brightnessOf(BuildContext context) {
final _InheritedCupertinoTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedCupertinoTheme>();
- return inheritedTheme?.theme.data.brightness ?? MediaQuery.of(context).platformBrightness;
+ return inheritedTheme?.theme.data.brightness ?? MediaQuery.platformBrightnessOf(context);
}
/// Retrieves the [Brightness] to use for descendant Cupertino widgets, based
@@ -106,7 +106,7 @@
/// [MediaQuery] exists, instead of returning null.
static Brightness? maybeBrightnessOf(BuildContext context) {
final _InheritedCupertinoTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedCupertinoTheme>();
- return inheritedTheme?.theme.data.brightness ?? MediaQuery.maybeOf(context)?.platformBrightness;
+ return inheritedTheme?.theme.data.brightness ?? MediaQuery.maybePlatformBrightnessOf(context);
}
/// The widget below this widget in the tree.
@@ -136,7 +136,7 @@
const _InheritedCupertinoTheme({
required this.theme,
required super.child,
- }) : assert(theme != null);
+ });
final CupertinoTheme theme;
@@ -172,6 +172,7 @@
CupertinoTextThemeData? textTheme,
Color? barBackgroundColor,
Color? scaffoldBackgroundColor,
+ bool? applyThemeToAll,
}) : this.raw(
brightness,
primaryColor,
@@ -179,6 +180,7 @@
textTheme,
barBackgroundColor,
scaffoldBackgroundColor,
+ applyThemeToAll,
);
/// Same as the default constructor but with positional arguments to avoid
@@ -193,6 +195,7 @@
CupertinoTextThemeData? textTheme,
Color? barBackgroundColor,
Color? scaffoldBackgroundColor,
+ bool? applyThemeToAll,
) : this._rawWithDefaults(
brightness,
primaryColor,
@@ -200,6 +203,7 @@
textTheme,
barBackgroundColor,
scaffoldBackgroundColor,
+ applyThemeToAll,
_kDefaultTheme,
);
@@ -210,6 +214,7 @@
CupertinoTextThemeData? textTheme,
Color? barBackgroundColor,
Color? scaffoldBackgroundColor,
+ bool? applyThemeToAll,
this._defaults,
) : super(
brightness: brightness,
@@ -218,6 +223,7 @@
textTheme: textTheme,
barBackgroundColor: barBackgroundColor,
scaffoldBackgroundColor: scaffoldBackgroundColor,
+ applyThemeToAll: applyThemeToAll,
);
final _CupertinoThemeDefaults _defaults;
@@ -240,6 +246,9 @@
Color get scaffoldBackgroundColor => super.scaffoldBackgroundColor ?? _defaults.scaffoldBackgroundColor;
@override
+ bool get applyThemeToAll => super.applyThemeToAll ?? _defaults.applyThemeToAll;
+
+ @override
NoDefaultCupertinoThemeData noDefault() {
return NoDefaultCupertinoThemeData(
brightness: super.brightness,
@@ -248,6 +257,7 @@
textTheme: super.textTheme,
barBackgroundColor: super.barBackgroundColor,
scaffoldBackgroundColor: super.scaffoldBackgroundColor,
+ applyThemeToAll: super.applyThemeToAll,
);
}
@@ -262,6 +272,7 @@
super.textTheme?.resolveFrom(context),
convertColor(super.barBackgroundColor),
convertColor(super.scaffoldBackgroundColor),
+ applyThemeToAll,
_defaults.resolveFrom(context, super.textTheme == null),
);
}
@@ -274,6 +285,7 @@
CupertinoTextThemeData? textTheme,
Color? barBackgroundColor,
Color? scaffoldBackgroundColor,
+ bool? applyThemeToAll,
}) {
return CupertinoThemeData._rawWithDefaults(
brightness ?? super.brightness,
@@ -282,6 +294,7 @@
textTheme ?? super.textTheme,
barBackgroundColor ?? super.barBackgroundColor,
scaffoldBackgroundColor ?? super.scaffoldBackgroundColor,
+ applyThemeToAll ?? super.applyThemeToAll,
_defaults,
);
}
@@ -295,8 +308,38 @@
properties.add(createCupertinoColorProperty('primaryContrastingColor', primaryContrastingColor, defaultValue: defaultData.primaryContrastingColor));
properties.add(createCupertinoColorProperty('barBackgroundColor', barBackgroundColor, defaultValue: defaultData.barBackgroundColor));
properties.add(createCupertinoColorProperty('scaffoldBackgroundColor', scaffoldBackgroundColor, defaultValue: defaultData.scaffoldBackgroundColor));
+ properties.add(DiagnosticsProperty<bool>('applyThemeToAll', applyThemeToAll, defaultValue: defaultData.applyThemeToAll));
textTheme.debugFillProperties(properties);
}
+
+ @override
+ bool operator == (Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is CupertinoThemeData
+ && other.brightness == brightness
+ && other.primaryColor == primaryColor
+ && other.primaryContrastingColor == primaryContrastingColor
+ && other.textTheme == textTheme
+ && other.barBackgroundColor == barBackgroundColor
+ && other.scaffoldBackgroundColor == scaffoldBackgroundColor
+ && other.applyThemeToAll == applyThemeToAll;
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ brightness,
+ primaryColor,
+ primaryContrastingColor,
+ textTheme,
+ barBackgroundColor,
+ scaffoldBackgroundColor,
+ applyThemeToAll,
+ );
}
/// Styling specifications for a cupertino theme without default values for
@@ -322,6 +365,7 @@
this.textTheme,
this.barBackgroundColor,
this.scaffoldBackgroundColor,
+ this.applyThemeToAll,
});
/// The brightness override for Cupertino descendants.
@@ -389,6 +433,22 @@
/// Defaults to [CupertinoColors.systemBackground].
final Color? scaffoldBackgroundColor;
+ /// Flag to apply this theme to all descendant Cupertino widgets.
+ ///
+ /// Certain Cupertino widgets previously didn't use theming, matching past
+ /// versions of iOS. For example, [CupertinoSwitch]s always used
+ /// [CupertinoColors.systemGreen] when active.
+ ///
+ /// Today, however, these widgets can indeed be themed on iOS. Moreover on
+ /// macOS, the accent color is reflected in these widgets. Turning this flag
+ /// on ensures that descendant Cupertino widgets will be themed accordingly.
+ ///
+ /// This flag currently applies to the following widgets:
+ /// - [CupertinoSwitch] & [Switch.adaptive]
+ ///
+ /// Defaults to false.
+ final bool? applyThemeToAll;
+
/// Returns an instance of the theme data whose property getters only return
/// the construction time specifications with no derived values.
///
@@ -412,6 +472,7 @@
textTheme: textTheme?.resolveFrom(context),
barBackgroundColor: convertColor(barBackgroundColor),
scaffoldBackgroundColor: convertColor(scaffoldBackgroundColor),
+ applyThemeToAll: applyThemeToAll,
);
}
@@ -428,6 +489,7 @@
CupertinoTextThemeData? textTheme,
Color? barBackgroundColor ,
Color? scaffoldBackgroundColor,
+ bool? applyThemeToAll,
}) {
return NoDefaultCupertinoThemeData(
brightness: brightness ?? this.brightness,
@@ -436,6 +498,7 @@
textTheme: textTheme ?? this.textTheme,
barBackgroundColor: barBackgroundColor ?? this.barBackgroundColor,
scaffoldBackgroundColor: scaffoldBackgroundColor ?? this.scaffoldBackgroundColor,
+ applyThemeToAll: applyThemeToAll ?? this.applyThemeToAll,
);
}
}
@@ -448,6 +511,7 @@
this.primaryContrastingColor,
this.barBackgroundColor,
this.scaffoldBackgroundColor,
+ this.applyThemeToAll,
this.textThemeDefaults,
);
@@ -456,6 +520,7 @@
final Color primaryContrastingColor;
final Color barBackgroundColor;
final Color scaffoldBackgroundColor;
+ final bool applyThemeToAll;
final _CupertinoTextThemeDefaults textThemeDefaults;
_CupertinoThemeDefaults resolveFrom(BuildContext context, bool resolveTextTheme) {
@@ -467,6 +532,7 @@
convertColor(primaryContrastingColor),
convertColor(barBackgroundColor),
convertColor(scaffoldBackgroundColor),
+ applyThemeToAll,
resolveTextTheme ? textThemeDefaults.resolveFrom(context) : textThemeDefaults,
);
}
@@ -490,7 +556,6 @@
}
CupertinoTextThemeData createDefaults({ required Color primaryColor }) {
- assert(primaryColor != null);
return _DefaultCupertinoTextThemeData(
primaryColor: primaryColor,
labelColor: labelColor,
@@ -507,9 +572,7 @@
required this.labelColor,
required this.inactiveGray,
required super.primaryColor,
- }) : assert(labelColor != null),
- assert(inactiveGray != null),
- assert(primaryColor != null);
+ });
final Color labelColor;
final Color inactiveGray;
diff --git a/framework/lib/src/cupertino/thumb_painter.dart b/framework/lib/src/cupertino/thumb_painter.dart
index feeab3e..b0ebeca 100644
--- a/framework/lib/src/cupertino/thumb_painter.dart
+++ b/framework/lib/src/cupertino/thumb_painter.dart
@@ -47,7 +47,7 @@
const CupertinoThumbPainter({
this.color = CupertinoColors.white,
this.shadows = _kSliderBoxShadows,
- }) : assert(shadows != null);
+ });
/// Creates an object that paints an iOS-style switch thumb.
const CupertinoThumbPainter.switchThumb({
diff --git a/framework/lib/src/foundation/_capabilities_io.dart b/framework/lib/src/foundation/_capabilities_io.dart
new file mode 100644
index 0000000..202dc7b
--- /dev/null
+++ b/framework/lib/src/foundation/_capabilities_io.dart
@@ -0,0 +1,10 @@
+// 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.
+
+/// The dart:io implementation of [isCanvasKit].
+///
+/// This bool shouldn't be used outside of web.
+bool get isCanvasKit {
+ throw UnimplementedError('isCanvasKit is not implemented for dart:io.');
+}
diff --git a/framework/lib/src/foundation/_capabilities_web.dart b/framework/lib/src/foundation/_capabilities_web.dart
new file mode 100644
index 0000000..5b5de84
--- /dev/null
+++ b/framework/lib/src/foundation/_capabilities_web.dart
@@ -0,0 +1,13 @@
+// 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:js/js.dart';
+
+// This value is set by the engine. It is used to determine if the application is
+// using canvaskit.
+@JS('window.flutterCanvasKit')
+external Object? get _windowFlutterCanvasKit;
+
+/// The web implementation of [isCanvasKit]
+bool get isCanvasKit => _windowFlutterCanvasKit != null;
diff --git a/framework/lib/src/foundation/_isolates_io.dart b/framework/lib/src/foundation/_isolates_io.dart
index 63c5f54..08bd8ca 100644
--- a/framework/lib/src/foundation/_isolates_io.dart
+++ b/framework/lib/src/foundation/_isolates_io.dart
@@ -11,7 +11,8 @@
export 'isolates.dart' show ComputeCallback;
/// The dart:io implementation of [isolate.compute].
-Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, {String? debugLabel}) async {
+@pragma('vm:prefer-inline')
+Future<R> compute<M, R>(isolates.ComputeCallback<M, R> callback, M message, {String? debugLabel}) async {
debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
return Isolate.run<R>(() {
diff --git a/framework/lib/src/foundation/_isolates_web.dart b/framework/lib/src/foundation/_isolates_web.dart
index 2a2d343..4350e54 100644
--- a/framework/lib/src/foundation/_isolates_web.dart
+++ b/framework/lib/src/foundation/_isolates_web.dart
@@ -7,7 +7,8 @@
export 'isolates.dart' show ComputeCallback;
/// The dart:html implementation of [isolate.compute].
-Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
+@pragma('dart2js:tryInline')
+Future<R> compute<M, R>(isolates.ComputeCallback<M, R> callback, M message, { String? debugLabel }) async {
// To avoid blocking the UI immediately for an expensive function call, we
// pump a single frame to allow the framework to complete the current set
// of work.
diff --git a/framework/lib/src/foundation/annotations.dart b/framework/lib/src/foundation/annotations.dart
index b57bfac..3bc5e02 100644
--- a/framework/lib/src/foundation/annotations.dart
+++ b/framework/lib/src/foundation/annotations.dart
@@ -39,7 +39,7 @@
/// class that overrides the inline documentations' own description.
class Category {
/// Create an annotation to provide a categorization of a class.
- const Category(this.sections) : assert(sections != null);
+ const Category(this.sections);
/// The strings the correspond to the section and subsection of the
/// category represented by this object.
@@ -74,7 +74,7 @@
/// class that overrides the inline documentations' own description.
class DocumentationIcon {
/// Create an annotation to provide a URL to an image describing a class.
- const DocumentationIcon(this.url) : assert(url != null);
+ const DocumentationIcon(this.url);
/// The URL to an image that represents the annotated class.
final String url;
@@ -111,7 +111,7 @@
/// represents the class.
class Summary {
/// Create an annotation to provide a short description of a class.
- const Summary(this.text) : assert(text != null);
+ const Summary(this.text);
/// The text of the summary of the annotated class.
final String text;
diff --git a/framework/lib/src/foundation/assertions.dart b/framework/lib/src/foundation/assertions.dart
index 67455b2..59abde7 100644
--- a/framework/lib/src/foundation/assertions.dart
+++ b/framework/lib/src/foundation/assertions.dart
@@ -54,9 +54,7 @@
required this.package,
required this.className,
required this.method,
- }) : assert(className != null),
- assert(method != null),
- assert(package != null);
+ });
/// An `<asynchronous suspension>` line in a stack trace.
static const PartialStackFrame asynchronousSuspension = PartialStackFrame(
@@ -128,8 +126,7 @@
const RepetitiveStackFrameFilter({
required this.frames,
required this.replacement,
- }) : assert(frames != null),
- assert(replacement != null);
+ });
/// The shape of this repetitive stack pattern.
final List<PartialStackFrame> frames;
@@ -140,7 +137,7 @@
/// The string to replace the frames with.
///
/// If the same replacement string is used multiple times in a row, the
- /// [FlutterError.defaultStackFilter] will simply update a counter after this
+ /// [FlutterError.defaultStackFilter] will insert a repeat count after this
/// line rather than repeating it.
final String replacement;
@@ -177,8 +174,7 @@
String message, {
DiagnosticsTreeStyle style = DiagnosticsTreeStyle.flat,
DiagnosticLevel level = DiagnosticLevel.info,
- }) : assert(message != null),
- super(
+ }) : super(
null,
<Object>[message],
showName: false,
@@ -217,8 +213,7 @@
List<Object> messageParts, {
DiagnosticsTreeStyle style = DiagnosticsTreeStyle.flat,
DiagnosticLevel level = DiagnosticLevel.info,
- }) : assert(messageParts != null),
- super(
+ }) : super(
null,
messageParts,
showName: false,
@@ -406,7 +401,7 @@
this.stackFilter,
this.informationCollector,
this.silent = false,
- }) : assert(exception != null);
+ });
/// Creates a copy of the error details but with the given fields replaced
/// with new values.
@@ -672,9 +667,7 @@
super.debugFillProperties(properties);
final DiagnosticsNode verb = ErrorDescription('thrown${ context != null ? ErrorDescription(" $context") : ""}');
final Diagnosticable? diagnosticable = _exceptionToDiagnosticable();
- if (false) { // ignore: deprecated_member_use
- properties.add(ErrorDescription('The null value was $verb.'));
- } else if (exception is num) {
+ if (exception is num) {
properties.add(ErrorDescription('The number $exception was $verb.'));
} else {
final DiagnosticsNode errorName;
@@ -1003,8 +996,6 @@
///
/// The default behavior for the [onError] handler is to call this function.
static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport = false }) {
- assert(details != null);
- assert(details.exception != null);
bool isInDebugMode = false;
assert(() {
// In debug mode, we ignore the "silent" flag.
@@ -1184,8 +1175,6 @@
/// ```
/// {@end-tool}
static void reportError(FlutterErrorDetails details) {
- assert(details != null);
- assert(details.exception != null);
onError?.call(details);
}
}
diff --git a/framework/lib/src/foundation/basic_types.dart b/framework/lib/src/foundation/basic_types.dart
index dc58411..9bc8c6a 100644
--- a/framework/lib/src/foundation/basic_types.dart
+++ b/framework/lib/src/foundation/basic_types.dart
@@ -231,7 +231,7 @@
/// Creates a new factory.
///
/// The `constructor` parameter must not be null.
- const Factory(this.constructor) : assert(constructor != null);
+ const Factory(this.constructor);
/// Creates a new object of type T.
final ValueGetter<T> constructor;
diff --git a/framework/lib/src/foundation/binding.dart b/framework/lib/src/foundation/binding.dart
index 4babc7f..ac6d4ac 100644
--- a/framework/lib/src/foundation/binding.dart
+++ b/framework/lib/src/foundation/binding.dart
@@ -187,7 +187,7 @@
/// such as a [TestWindow].
///
/// The [window] is a singleton meant for use by applications that only have a
- /// single main window. In addition to the properties of [ui.FlutterWindow],
+ /// single main window. In addition to the properties of [ui.FlutterView],
/// [window] provides access to platform-specific properties and callbacks
/// available on the [platformDispatcher].
///
@@ -327,7 +327,6 @@
]);
}
try {
- assert(instance != null);
if (instance._debugConstructed && _debugInitializedType == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Binding initialized without calling initInstances.'),
@@ -553,10 +552,8 @@
Future<void> lockEvents(Future<void> Function() callback) {
final developer.TimelineTask timelineTask = developer.TimelineTask()..start('Lock events');
- assert(callback != null);
_lockCount += 1;
final Future<void> future = callback();
- assert(future != null, 'The lockEvents() callback returned null; it should return a Future<void> that completes when the lock is to expire.');
future.whenComplete(() {
_lockCount -= 1;
if (!locked) {
@@ -627,8 +624,6 @@
required String name,
required AsyncCallback callback,
}) {
- assert(name != null);
- assert(callback != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
@@ -658,9 +653,6 @@
required AsyncValueGetter<bool> getter,
required AsyncValueSetter<bool> setter,
}) {
- assert(name != null);
- assert(getter != null);
- assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
@@ -692,9 +684,6 @@
required AsyncValueGetter<double> getter,
required AsyncValueSetter<double> setter,
}) {
- assert(name != null);
- assert(getter != null);
- assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
@@ -754,9 +743,6 @@
required AsyncValueGetter<String> getter,
required AsyncValueSetter<String> setter,
}) {
- assert(name != null);
- assert(getter != null);
- assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
@@ -825,8 +811,6 @@
required String name,
required ServiceExtensionCallback callback,
}) {
- assert(name != null);
- assert(callback != null);
final String methodName = 'ext.flutter.$name';
developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
assert(method == methodName);
diff --git a/framework/lib/src/foundation/capabilities.dart b/framework/lib/src/foundation/capabilities.dart
new file mode 100644
index 0000000..11c82ee
--- /dev/null
+++ b/framework/lib/src/foundation/capabilities.dart
@@ -0,0 +1,11 @@
+// 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 '_capabilities_io.dart'
+ as capabilities;
+
+/// Returns true if the application is using CanvasKit.
+///
+/// Only to be used for web.
+bool get isCanvasKit => capabilities.isCanvasKit;
diff --git a/framework/lib/src/foundation/change_notifier.dart b/framework/lib/src/foundation/change_notifier.dart
index a65df16..1706bc8 100644
--- a/framework/lib/src/foundation/change_notifier.dart
+++ b/framework/lib/src/foundation/change_notifier.dart
@@ -104,7 +104,7 @@
/// It is O(1) for adding listeners and O(N) for removing listeners and dispatching
/// notifications (where N is the number of listeners).
///
-/// {@macro flutter.flutter.animatedbuilder_changenotifier.rebuild}
+/// {@macro flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild}
///
/// See also:
///
diff --git a/framework/lib/src/foundation/consolidate_response.dart b/framework/lib/src/foundation/consolidate_response.dart
index 4d0f44c..94160ec 100644
--- a/framework/lib/src/foundation/consolidate_response.dart
+++ b/framework/lib/src/foundation/consolidate_response.dart
@@ -51,7 +51,6 @@
bool autoUncompress = true,
BytesReceivedCallback? onBytesReceived,
}) {
- assert(autoUncompress != null);
final Completer<Uint8List> completer = Completer<Uint8List>.sync();
final _OutputBuffer output = _OutputBuffer();
diff --git a/framework/lib/src/foundation/constants.dart b/framework/lib/src/foundation/constants.dart
index 0e5ccb6..576de1e 100644
--- a/framework/lib/src/foundation/constants.dart
+++ b/framework/lib/src/foundation/constants.dart
@@ -69,6 +69,3 @@
/// A constant that is true if the application was compiled to run on the web.
const bool kIsWeb = bool.fromEnvironment('dart.library.js_util');
-
-/// A constant that is true if the application is using canvasKit
-const bool isCanvasKit = bool.fromEnvironment('FLUTTER_WEB_USE_SKIA');
diff --git a/framework/lib/src/foundation/diagnostics.dart b/framework/lib/src/foundation/diagnostics.dart
index 80906dd..fa15201 100644
--- a/framework/lib/src/foundation/diagnostics.dart
+++ b/framework/lib/src/foundation/diagnostics.dart
@@ -253,28 +253,7 @@
this.beforeName = '',
this.suffixLineOne = '',
this.mandatoryFooter = '',
- }) : assert(prefixLineOne != null),
- assert(prefixOtherLines != null),
- assert(prefixLastChildLineOne != null),
- assert(prefixOtherLinesRootNode != null),
- assert(linkCharacter != null),
- assert(propertyPrefixIfChildren != null),
- assert(propertyPrefixNoChildren != null),
- assert(lineBreak != null),
- assert(lineBreakProperties != null),
- assert(afterName != null),
- assert(afterDescriptionIfBody != null),
- assert(afterDescription != null),
- assert(beforeProperties != null),
- assert(afterProperties != null),
- assert(propertySeparator != null),
- assert(bodyIndent != null),
- assert(footer != null),
- assert(showChildren != null),
- assert(addBlankLineIfNoChildren != null),
- assert(isNameOnOwnLine != null),
- assert(isBlankLineBetweenPropertiesAndChildren != null),
- childLinkSpace = ' ' * linkCharacter.length;
+ }) : childLinkSpace = ' ' * linkCharacter.length;
/// Prefix to add to the first line to display a child with this style.
final String prefixLineOne;
@@ -1113,8 +1092,7 @@
int wrapWidth = 100,
int wrapWidthProperties = 65,
int maxDescendentsTruncatableNode = -1,
- }) : assert(minLevel != null),
- _minLevel = minLevel,
+ }) : _minLevel = minLevel,
_wrapWidth = wrapWidth,
_wrapWidthProperties = wrapWidthProperties,
_maxDescendentsTruncatableNode = maxDescendentsTruncatableNode;
@@ -1218,7 +1196,7 @@
List<DiagnosticsNode> children = node.getChildren();
- String? description = node.toDescription(parentConfiguration: parentConfiguration);
+ String description = node.toDescription(parentConfiguration: parentConfiguration);
if (config.beforeName.isNotEmpty) {
builder.write(config.beforeName);
}
@@ -1229,7 +1207,7 @@
if (uppercaseTitle) {
name = name?.toUpperCase();
}
- if (description == null || description.isEmpty) {
+ if (description.isEmpty) {
if (node.showName && name != null) {
builder.write(name, allowWrap: wrapName);
}
@@ -1388,7 +1366,6 @@
for (int i = 0; i < children.length; i++) {
final DiagnosticsNode child = children[i];
- assert(child != null);
final TextTreeConfiguration childConfig = _childTextConfiguration(child, config)!;
if (i == children.length - 1) {
final String lastChildPrefixLineOne = '$prefixChildrenRaw${childConfig.prefixLastChildLineOne}';
@@ -1463,12 +1440,10 @@
this.showName = true,
this.showSeparator = true,
this.linePrefix,
- }) : assert(showName != null),
- assert(showSeparator != null),
- // A name ending with ':' indicates that the user forgot that the ':' will
- // be automatically added for them when generating descriptions of the
- // property.
- assert(
+ }) : assert(
+ // A name ending with ':' indicates that the user forgot that the ':' will
+ // be automatically added for them when generating descriptions of the
+ // property.
name == null || !name.endsWith(':'),
'Names of diagnostic nodes must not end with colons.\n'
'name:\n'
@@ -1490,8 +1465,6 @@
DiagnosticLevel level = DiagnosticLevel.info,
bool allowWrap = true,
}) {
- assert(style != null);
- assert(level != null);
return DiagnosticsProperty<void>(
'',
null,
@@ -1728,7 +1701,6 @@
}) {
String result = super.toString();
assert(style != null);
- assert(minLevel != null);
assert(() {
if (_isSingleLine(style)) {
result = toStringDeep(parentConfiguration: parentConfiguration, minLevel: minLevel);
@@ -1866,11 +1838,7 @@
String message, {
DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
DiagnosticLevel level = DiagnosticLevel.info,
- }) : assert(name != null),
- assert(message != null),
- assert(style != null),
- assert(level != null),
- super(name, null, description: message, style: style, level: level);
+ }) : super(name, null, description: message, style: style, level: level);
}
/// Property which encloses its string [value] in quotes.
@@ -1894,10 +1862,7 @@
super.ifEmpty,
super.style,
super.level,
- }) : assert(showName != null),
- assert(quoted != null),
- assert(style != null),
- assert(level != null);
+ });
/// Whether the value is enclosed in double quotes.
final bool quoted;
@@ -2005,9 +1970,7 @@
super.showName,
super.style,
super.level,
- }) : assert(showName != null),
- assert(style != null),
- assert(level != null);
+ });
/// Property with a [value] that is computed only when needed.
///
@@ -2024,9 +1987,7 @@
super.tooltip,
super.defaultValue,
super.level,
- }) : assert(showName != null),
- assert(level != null),
- super.lazy();
+ }) : super.lazy();
@override
String numberToString() => debugFormatDouble(value);
@@ -2048,9 +2009,7 @@
super.defaultValue,
super.style,
super.level,
- }) : assert(showName != null),
- assert(level != null),
- assert(style != null);
+ });
@override
String numberToString() => value.toString();
@@ -2075,8 +2034,7 @@
super.tooltip,
super.unit,
super.level,
- }) : assert(showName != null),
- assert(level != null);
+ });
@override
String valueToString({ TextTreeConfiguration? parentConfiguration }) {
@@ -2150,9 +2108,7 @@
bool showName = false,
Object? defaultValue,
DiagnosticLevel level = DiagnosticLevel.info,
- }) : assert(showName != null),
- assert(level != null),
- assert(ifTrue != null || ifFalse != null),
+ }) : assert(ifTrue != null || ifFalse != null),
super(
name,
value,
@@ -2254,10 +2210,7 @@
super.showName,
super.showSeparator,
super.level,
- }) : assert(style != null),
- assert(showName != null),
- assert(showSeparator != null),
- assert(level != null);
+ });
@override
String valueToString({TextTreeConfiguration? parentConfiguration}) {
@@ -2335,7 +2288,7 @@
super.value, {
super.defaultValue,
super.level,
- }) : assert(level != null);
+ });
@override
String valueToString({ TextTreeConfiguration? parentConfiguration }) {
@@ -2382,9 +2335,7 @@
super.ifNull,
super.showName = false,
super.level,
- }) : assert(ifPresent != null || ifNull != null),
- assert(showName != null),
- assert(level != null);
+ }) : assert(ifPresent != null || ifNull != null);
/// Shorthand constructor to describe whether the property has a value.
///
@@ -2396,9 +2347,7 @@
String super.name,
super.value, {
super.level,
- }) : assert(name != null),
- assert(level != null),
- ifPresent = 'has $name',
+ }) : ifPresent = 'has $name',
super(
showName: false,
);
@@ -2495,17 +2444,13 @@
super.showName,
super.showSeparator,
super.level,
- }) : assert(value != null),
- assert(showName != null),
- assert(showSeparator != null),
- assert(level != null);
+ });
@override
Map<String, T?> get value => super.value!;
@override
String valueToString({TextTreeConfiguration? parentConfiguration}) {
- assert(value != null);
if (!_hasNonNullEntry() && ifEmpty != null) {
return ifEmpty!;
}
@@ -2598,11 +2543,7 @@
this.allowNameWrap = true,
DiagnosticsTreeStyle super.style = DiagnosticsTreeStyle.singleLine,
DiagnosticLevel level = DiagnosticLevel.info,
- }) : assert(showName != null),
- assert(showSeparator != null),
- assert(style != null),
- assert(level != null),
- _description = description,
+ }) : _description = description,
_valueComputed = true,
_value = value,
_computeValue = null,
@@ -2620,7 +2561,7 @@
/// The [showName], [showSeparator], [style], [missingIfNull], and [level]
/// arguments must not be null.
///
- /// The [level] argument is just a suggestion and can be overridden if
+ /// The [level] argument is just a suggestion and can be overridden
/// if something else about the property causes it to have a lower or higher
/// level. For example, if calling `computeValue` throws an exception, [level]
/// will always return [DiagnosticLevel.error].
@@ -2640,12 +2581,7 @@
this.allowNameWrap = true,
DiagnosticsTreeStyle super.style = DiagnosticsTreeStyle.singleLine,
DiagnosticLevel level = DiagnosticLevel.info,
- }) : assert(showName != null),
- assert(showSeparator != null),
- assert(defaultValue == kNoDefaultValue || defaultValue is T?),
- assert(missingIfNull != null),
- assert(style != null),
- assert(level != null),
+ }) : assert(defaultValue == kNoDefaultValue || defaultValue is T?),
_description = description,
_valueComputed = false,
_value = null,
@@ -2765,7 +2701,6 @@
///
/// `text` must not be null.
String _addTooltip(String text) {
- assert(text != null);
return tooltip == null ? text : '$text ($tooltip)';
}
@@ -2938,7 +2873,7 @@
super.name,
required this.value,
required super.style,
- }) : assert(value != null);
+ });
@override
final T value;
@@ -3153,6 +3088,8 @@
/// Add additional properties associated with the node.
///
+ /// {@youtube 560 315 https://www.youtube.com/watch?v=DnC7eT-vh1k}
+ ///
/// Use the most specific [DiagnosticsProperty] existing subclass to describe
/// each property instead of the [DiagnosticsProperty] base class. There are
/// only a small number of [DiagnosticsProperty] subclasses each covering a
diff --git a/framework/lib/src/foundation/isolates.dart b/framework/lib/src/foundation/isolates.dart
index 29c9991..cea2408 100644
--- a/framework/lib/src/foundation/isolates.dart
+++ b/framework/lib/src/foundation/isolates.dart
@@ -9,33 +9,17 @@
/// Signature for the callback passed to [compute].
///
-/// {@macro flutter.foundation.compute.types}
-///
-/// Instances of [ComputeCallback] must be functions that can be sent to an
-/// isolate.
/// {@macro flutter.foundation.compute.callback}
///
-/// {@macro flutter.foundation.compute.types}
-typedef ComputeCallback<Q, R> = FutureOr<R> Function(Q message);
+typedef ComputeCallback<M, R> = FutureOr<R> Function(M message);
/// The signature of [compute], which spawns an isolate, runs `callback` on
/// that isolate, passes it `message`, and (eventually) returns the value
/// returned by `callback`.
-///
-/// {@macro flutter.foundation.compute.usecase}
-///
-/// The function used as `callback` must be one that can be sent to an isolate.
-/// {@macro flutter.foundation.compute.callback}
-///
-/// {@macro flutter.foundation.compute.types}
-///
-/// The `debugLabel` argument can be specified to provide a name to add to the
-/// [Timeline]. This is useful when profiling an application.
-typedef ComputeImpl = Future<R> Function<Q, R>(ComputeCallback<Q, R> callback, Q message, { String? debugLabel });
+typedef ComputeImpl = Future<R> Function<M, R>(ComputeCallback<M, R> callback, M message, { String? debugLabel });
-/// A function that spawns an isolate and runs the provided `callback` on that
-/// isolate, passes it the provided `message`, and (eventually) returns the
-/// value returned by `callback`.
+/// Asynchronously runs the given [callback] - with the provided [message] -
+/// in the background and completes with the result.
///
/// {@template flutter.foundation.compute.usecase}
/// This is useful for operations that take longer than a few milliseconds, and
@@ -68,34 +52,26 @@
/// ```
/// {@end-tool}
///
-/// The function used as `callback` must be one that can be sent to an isolate.
+/// On web platforms this will run [callback] on the current eventloop.
+/// On native platforms this will run [callback] in a separate isolate.
+///
/// {@template flutter.foundation.compute.callback}
-/// Qualifying functions include:
///
-/// * top-level functions
-/// * static methods
-/// * closures that only capture objects that can be sent to an isolate
+/// The `callback`, the `message` given to it as well as the result have to be
+/// objects that can be sent across isolates (as they may be transitively copied
+/// if needed). The majority of objects can be sent across isolates.
///
-/// Using closures must be done with care. Due to
-/// [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983) a
-/// closure may capture objects that, while not directly used in the closure
-/// itself, may prevent it from being sent to an isolate.
+/// See [SendPort.send] for more information about exceptions as well as a note
+/// of warning about sending closures, which can capture more state than needed.
+///
/// {@endtemplate}
///
-/// {@template flutter.foundation.compute.types}
-/// The [compute] method accepts the following parameters:
+/// On native platforms `await compute(fun, message)` is equivalent to
+/// `await Isolate.run(() => fun(message))`. See also [Isolate.run].
///
-/// * `Q` is the type of the message that kicks off the computation.
-/// * `R` is the type of the value returned.
-///
-/// There are limitations on the values that can be sent and received to and
-/// from isolates. These limitations constrain the values of `Q` and `R` that
-/// are possible. See the discussion at [SendPort.send].
-///
-/// The same limitations apply to any errors generated by the computation.
-/// {@endtemplate}
-///
-/// See also:
-///
-/// * [ComputeImpl], for the [compute] function's signature.
-const ComputeImpl compute = isolates.compute;
+/// The `debugLabel` - if provided - is used as name for the isolate that
+/// executes `callback`. [Timeline] events produced by that isolate will have
+/// the name associated with them. This is useful when profiling an application.
+Future<R> compute<M, R>(ComputeCallback<M, R> callback, M message, {String? debugLabel}) {
+ return isolates.compute<M, R>(callback, message, debugLabel: debugLabel);
+}
diff --git a/framework/lib/src/foundation/math.dart b/framework/lib/src/foundation/math.dart
index 053192a..7c559bb 100644
--- a/framework/lib/src/foundation/math.dart
+++ b/framework/lib/src/foundation/math.dart
@@ -8,16 +8,5 @@
/// floating point numbers.
//
// See also: //dev/benchmarks/microbenchmarks/lib/foundation/clamp.dart
-double clampDouble(double x, double min, double max) {
- assert(min <= max && !max.isNaN && !min.isNaN);
- if (x < min) {
- return min;
- }
- if (x > max) {
- return max;
- }
- if (x.isNaN) {
- return max;
- }
- return x;
-}
+
+export 'package:engine/ui.dart' show clampDouble;
\ No newline at end of file
diff --git a/framework/lib/src/foundation/node.dart b/framework/lib/src/foundation/node.dart
index f8c377b..6e830f1 100644
--- a/framework/lib/src/foundation/node.dart
+++ b/framework/lib/src/foundation/node.dart
@@ -92,7 +92,6 @@
/// method, as in `super.attach(owner)`.
@mustCallSuper
void attach(covariant Object owner) {
- assert(owner != null);
assert(_owner == null);
_owner = owner;
}
@@ -124,7 +123,6 @@
@protected
@mustCallSuper
void adoptChild(covariant AbstractNode child) {
- assert(child != null);
assert(child._parent == null);
assert(() {
AbstractNode node = this;
@@ -147,7 +145,6 @@
@protected
@mustCallSuper
void dropChild(covariant AbstractNode child) {
- assert(child != null);
assert(child._parent == this);
assert(child.attached == attached);
child._parent = null;
diff --git a/framework/lib/src/foundation/serialization.dart b/framework/lib/src/foundation/serialization.dart
index b0ade7d..bdeaecc 100644
--- a/framework/lib/src/foundation/serialization.dart
+++ b/framework/lib/src/foundation/serialization.dart
@@ -170,8 +170,7 @@
/// The byte order used is [Endian.host] throughout.
class ReadBuffer {
/// Creates a [ReadBuffer] for reading from the specified [data].
- ReadBuffer(this.data)
- : assert(data != null);
+ ReadBuffer(this.data);
/// The underlying data being read.
final ByteData data;
diff --git a/framework/lib/src/foundation/stack_frame.dart b/framework/lib/src/foundation/stack_frame.dart
index 1a18d2d..5e22e1e 100644
--- a/framework/lib/src/foundation/stack_frame.dart
+++ b/framework/lib/src/foundation/stack_frame.dart
@@ -35,16 +35,7 @@
required this.method,
this.isConstructor = false,
required this.source,
- }) : assert(number != null),
- assert(column != null),
- assert(line != null),
- assert(method != null),
- assert(packageScheme != null),
- assert(package != null),
- assert(packagePath != null),
- assert(className != null),
- assert(isConstructor != null),
- assert(source != null);
+ });
/// A stack frame representing an asynchronous suspension.
static const StackFrame asynchronousSuspension = StackFrame(
@@ -74,13 +65,11 @@
///
/// This is normally useful with [StackTrace.current].
static List<StackFrame> fromStackTrace(StackTrace stack) {
- assert(stack != null);
return fromStackString(stack.toString());
}
/// Parses a list of [StackFrame]s from the [StackTrace.toString] method.
static List<StackFrame> fromStackString(String stack) {
- assert(stack != null);
return stack
.trim()
.split('\n')
@@ -181,7 +170,6 @@
/// Parses a single [StackFrame] from a single line of a [StackTrace].
static StackFrame? fromStackTraceLine(String line) {
- assert(line != null);
if (line == '<asynchronous suspension>') {
return asynchronousSuspension;
} else if (line == '...') {
diff --git a/framework/lib/src/gestures/arena.dart b/framework/lib/src/gestures/arena.dart
index f8a399e..b832a5d 100644
--- a/framework/lib/src/gestures/arena.dart
+++ b/framework/lib/src/gestures/arena.dart
@@ -97,6 +97,10 @@
}
}
+/// Used for disambiguating the meaning of sequences of pointer events.
+///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=Q85LBtBdi0U}
+///
/// The first member to accept or the last member to not reject wins.
///
/// See <https://flutter.dev/gestures/#gesture-disambiguation> for more
@@ -266,7 +270,6 @@
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
assert(state == _arenas[pointer]);
- assert(state != null);
assert(state.eagerWinner == null || state.eagerWinner == member);
assert(!state.isOpen);
_arenas.remove(pointer);
diff --git a/framework/lib/src/gestures/binding.dart b/framework/lib/src/gestures/binding.dart
index 707381c..c937a63 100644
--- a/framework/lib/src/gestures/binding.dart
+++ b/framework/lib/src/gestures/binding.dart
@@ -83,8 +83,6 @@
// Add `event` for resampling or dispatch it directly if
// not a touch event.
void addOrDispatch(PointerEvent event) {
- final SchedulerBinding scheduler = SchedulerBinding.instance;
- assert(scheduler != null);
// Add touch event to resampler or dispatch pointer event directly.
if (event.kind == PointerDeviceKind.touch) {
// Save last event time for debugPrint of resampling margin.
@@ -108,7 +106,6 @@
// The `samplingClock` is the clock used to determine frame time age.
void sample(Duration samplingOffset, SamplingClock clock) {
final SchedulerBinding scheduler = SchedulerBinding.instance;
- assert(scheduler != null);
// Initialize `_frameTime` if needed. This will be used for periodic
// sampling when frame callbacks are not received.
@@ -290,9 +287,18 @@
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
- _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
- if (!locked) {
- _flushPointerEventQueue();
+ try {
+ _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
+ if (!locked) {
+ _flushPointerEventQueue();
+ }
+ } catch (error, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: error,
+ stack: stack,
+ library: 'gestures library',
+ context: ErrorDescription('while handling a pointer data packet'),
+ ));
}
}
@@ -360,7 +366,7 @@
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
- assert(!_hitTests.containsKey(event.pointer), 'Pointer of $event unexpectedly has a HitTestResult associated with it.');
+ assert(!_hitTests.containsKey(event.pointer), 'Pointer of ${event.toString(minLevel: DiagnosticLevel.debug)} unexpectedly has a HitTestResult associated with it.');
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
@@ -368,7 +374,7 @@
}
assert(() {
if (debugPrintHitTestResults) {
- debugPrint('$event: $hitTestResult');
+ debugPrint('${event.toString(minLevel: DiagnosticLevel.debug)}: $hitTestResult');
}
return true;
}());
@@ -391,7 +397,6 @@
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
- assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
diff --git a/framework/lib/src/gestures/converter.dart b/framework/lib/src/gestures/converter.dart
index 9d00c3a..6aa7741 100644
--- a/framework/lib/src/gestures/converter.dart
+++ b/framework/lib/src/gestures/converter.dart
@@ -52,9 +52,8 @@
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) {
return data
.where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown)
- .map((ui.PointerData datum) {
+ .map<PointerEvent?>((ui.PointerData datum) {
final Offset position = Offset(datum.physicalX, datum.physicalY) / devicePixelRatio;
- assert(position != null);
final Offset delta = Offset(datum.physicalDeltaX, datum.physicalDeltaY) / devicePixelRatio;
final double radiusMinor = _toLogicalPixels(datum.radiusMinor, devicePixelRatio);
final double radiusMajor = _toLogicalPixels(datum.radiusMajor, devicePixelRatio);
@@ -62,7 +61,6 @@
final double radiusMax = _toLogicalPixels(datum.radiusMax, devicePixelRatio);
final Duration timeStamp = datum.timeStamp;
final PointerDeviceKind kind = datum.kind;
- assert(datum.change != null);
switch (datum.signalKind ?? ui.PointerSignalKind.none) {
case ui.PointerSignalKind.none:
switch (datum.change) {
@@ -249,6 +247,9 @@
);
}
case ui.PointerSignalKind.scroll:
+ if (!datum.scrollDeltaX.isFinite || !datum.scrollDeltaY.isFinite || devicePixelRatio <= 0) {
+ return null;
+ }
final Offset scrollDelta =
Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio;
return PointerScrollEvent(
@@ -282,7 +283,7 @@
// enumeration to PointerSignalKind.
throw StateError('Unreachable');
}
- });
+ }).whereType<PointerEvent>();
}
static double _toLogicalPixels(double physicalPixels, double devicePixelRatio) => physicalPixels / devicePixelRatio;
diff --git a/framework/lib/src/gestures/drag_details.dart b/framework/lib/src/gestures/drag_details.dart
index cd85feb..39cf290 100644
--- a/framework/lib/src/gestures/drag_details.dart
+++ b/framework/lib/src/gestures/drag_details.dart
@@ -25,8 +25,7 @@
DragDownDetails({
this.globalPosition = Offset.zero,
Offset? localPosition,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
///
@@ -73,8 +72,7 @@
this.globalPosition = Offset.zero,
Offset? localPosition,
this.kind,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// Recorded timestamp of the source pointer event that triggered the drag
/// event.
@@ -109,10 +107,12 @@
String toString() => '${objectRuntimeType(this, 'DragStartDetails')}($globalPosition)';
}
+/// {@template flutter.gestures.dragdetails.GestureDragStartCallback}
/// Signature for when a pointer has contacted the screen and has begun to move.
///
/// The `details` object provides the position of the touch when it first
/// touched the surface.
+/// {@endtemplate}
///
/// See [DragGestureRecognizer.onStart].
typedef GestureDragStartCallback = void Function(DragStartDetails details);
@@ -126,7 +126,7 @@
/// * [DragStartDetails], the details for [GestureDragStartCallback].
/// * [DragEndDetails], the details for [GestureDragEndCallback].
class DragUpdateDetails {
- /// Creates details for a [DragUpdateDetails].
+ /// Creates details for a [GestureDragUpdateCallback].
///
/// The [delta] argument must not be null.
///
@@ -140,8 +140,7 @@
this.primaryDelta,
required this.globalPosition,
Offset? localPosition,
- }) : assert(delta != null),
- assert(
+ }) : assert(
primaryDelta == null
|| (primaryDelta == delta.dx && delta.dy == 0.0)
|| (primaryDelta == delta.dy && delta.dx == 0.0),
@@ -195,11 +194,13 @@
String toString() => '${objectRuntimeType(this, 'DragUpdateDetails')}($delta)';
}
+/// {@template flutter.gestures.dragdetails.GestureDragUpdateCallback}
/// Signature for when a pointer that is in contact with the screen and moving
/// has moved again.
///
/// The `details` object provides the position of the touch and the distance it
/// has traveled since the last update.
+/// {@endtemplate}
///
/// See [DragGestureRecognizer.onUpdate].
typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details);
@@ -219,8 +220,7 @@
DragEndDetails({
this.velocity = Velocity.zero,
this.primaryVelocity,
- }) : assert(velocity != null),
- assert(
+ }) : assert(
primaryVelocity == null
|| primaryVelocity == velocity.pixelsPerSecond.dx
|| primaryVelocity == velocity.pixelsPerSecond.dy,
diff --git a/framework/lib/src/gestures/eager.dart b/framework/lib/src/gestures/eager.dart
index 6193a20..3c83435 100644
--- a/framework/lib/src/gestures/eager.dart
+++ b/framework/lib/src/gestures/eager.dart
@@ -18,12 +18,8 @@
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
EagerGestureRecognizer({
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
diff --git a/framework/lib/src/gestures/events.dart b/framework/lib/src/gestures/events.dart
index 7e02867..7e6277c 100644
--- a/framework/lib/src/gestures/events.dart
+++ b/framework/lib/src/gestures/events.dart
@@ -843,8 +843,7 @@
}
class _TransformedPointerAddedEvent extends _TransformedPointerEvent with _CopyPointerAddedEvent implements PointerAddedEvent {
- _TransformedPointerAddedEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerAddedEvent(this.original, this.transform);
@override
final PointerAddedEvent original;
@@ -934,8 +933,7 @@
}
class _TransformedPointerRemovedEvent extends _TransformedPointerEvent with _CopyPointerRemovedEvent implements PointerRemovedEvent {
- _TransformedPointerRemovedEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerRemovedEvent(this.original, this.transform);
@override
final PointerRemovedEvent original;
@@ -1051,8 +1049,7 @@
}
class _TransformedPointerHoverEvent extends _TransformedPointerEvent with _CopyPointerHoverEvent implements PointerHoverEvent {
- _TransformedPointerHoverEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerHoverEvent(this.original, this.transform);
@override
final PointerHoverEvent original;
@@ -1198,8 +1195,7 @@
}
class _TransformedPointerEnterEvent extends _TransformedPointerEvent with _CopyPointerEnterEvent implements PointerEnterEvent {
- _TransformedPointerEnterEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerEnterEvent(this.original, this.transform);
@override
final PointerEnterEvent original;
@@ -1344,8 +1340,7 @@
}
class _TransformedPointerExitEvent extends _TransformedPointerEvent with _CopyPointerExitEvent implements PointerExitEvent {
- _TransformedPointerExitEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerExitEvent(this.original, this.transform);
@override
final PointerExitEvent original;
@@ -1453,8 +1448,7 @@
}
class _TransformedPointerDownEvent extends _TransformedPointerEvent with _CopyPointerDownEvent implements PointerDownEvent {
- _TransformedPointerDownEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerDownEvent(this.original, this.transform);
@override
final PointerDownEvent original;
@@ -1571,8 +1565,7 @@
}
class _TransformedPointerMoveEvent extends _TransformedPointerEvent with _CopyPointerMoveEvent implements PointerMoveEvent {
- _TransformedPointerMoveEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerMoveEvent(this.original, this.transform);
@override
final PointerMoveEvent original;
@@ -1684,8 +1677,7 @@
}
class _TransformedPointerUpEvent extends _TransformedPointerEvent with _CopyPointerUpEvent implements PointerUpEvent {
- _TransformedPointerUpEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerUpEvent(this.original, this.transform);
@override
final PointerUpEvent original;
@@ -1784,12 +1776,7 @@
super.position,
this.scrollDelta = Offset.zero,
super.embedderId,
- }) : assert(timeStamp != null),
- assert(kind != null),
- assert(device != null),
- assert(position != null),
- assert(scrollDelta != null),
- assert(!identical(kind, PointerDeviceKind.trackpad));
+ });
@override
final Offset scrollDelta;
@@ -1810,8 +1797,7 @@
}
class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _CopyPointerScrollEvent implements PointerScrollEvent {
- _TransformedPointerScrollEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerScrollEvent(this.original, this.transform);
@override
final PointerScrollEvent original;
@@ -1889,10 +1875,7 @@
super.device,
super.position,
super.embedderId,
- }) : assert(timeStamp != null),
- assert(kind != null),
- assert(device != null),
- assert(position != null);
+ });
@override
PointerScrollInertiaCancelEvent transformed(Matrix4? transform) {
@@ -1904,8 +1887,7 @@
}
class _TransformedPointerScrollInertiaCancelEvent extends _TransformedPointerEvent with _CopyPointerScrollInertiaCancelEvent implements PointerScrollInertiaCancelEvent {
- _TransformedPointerScrollInertiaCancelEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerScrollInertiaCancelEvent(this.original, this.transform);
@override
final PointerScrollInertiaCancelEvent original;
@@ -1980,12 +1962,7 @@
super.position,
super.embedderId,
this.scale = 1.0,
- }) : assert(timeStamp != null),
- assert(kind != null),
- assert(device != null),
- assert(position != null),
- assert(embedderId != null),
- assert(scale != null);
+ });
@override
final double scale;
@@ -2000,8 +1977,7 @@
}
class _TransformedPointerScaleEvent extends _TransformedPointerEvent with _CopyPointerScaleEvent implements PointerScaleEvent {
- _TransformedPointerScaleEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerScaleEvent(this.original, this.transform);
@override
final PointerScaleEvent original;
@@ -2069,13 +2045,7 @@
super.position,
super.embedderId,
super.synthesized,
- }) : assert(timeStamp != null),
- assert(device != null),
- assert(pointer != null),
- assert(position != null),
- assert(embedderId != null),
- assert(synthesized != null),
- super(kind: PointerDeviceKind.trackpad);
+ }) : super(kind: PointerDeviceKind.trackpad);
@override
PointerPanZoomStartEvent transformed(Matrix4? transform) {
@@ -2087,8 +2057,7 @@
}
class _TransformedPointerPanZoomStartEvent extends _TransformedPointerEvent with _CopyPointerPanZoomStartEvent implements PointerPanZoomStartEvent {
- _TransformedPointerPanZoomStartEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerPanZoomStartEvent(this.original, this.transform);
@override
final PointerPanZoomStartEvent original;
@@ -2180,17 +2149,7 @@
this.scale = 1.0,
this.rotation = 0.0,
super.synthesized,
- }) : assert(timeStamp != null),
- assert(device != null),
- assert(pointer != null),
- assert(position != null),
- assert(embedderId != null),
- assert(pan != null),
- assert(panDelta != null),
- assert(scale != null),
- assert(rotation != null),
- assert(synthesized != null),
- super(kind: PointerDeviceKind.trackpad);
+ }) : super(kind: PointerDeviceKind.trackpad);
@override
final Offset pan;
@@ -2215,8 +2174,7 @@
}
class _TransformedPointerPanZoomUpdateEvent extends _TransformedPointerEvent with _CopyPointerPanZoomUpdateEvent implements PointerPanZoomUpdateEvent {
- _TransformedPointerPanZoomUpdateEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerPanZoomUpdateEvent(this.original, this.transform);
@override
Offset get pan => original.pan;
@@ -2304,13 +2262,7 @@
super.position,
super.embedderId,
super.synthesized,
- }) : assert(timeStamp != null),
- assert(device != null),
- assert(pointer != null),
- assert(position != null),
- assert(embedderId != null),
- assert(synthesized != null),
- super(kind: PointerDeviceKind.trackpad);
+ }) : super(kind: PointerDeviceKind.trackpad);
@override
PointerPanZoomEndEvent transformed(Matrix4? transform) {
@@ -2322,8 +2274,7 @@
}
class _TransformedPointerPanZoomEndEvent extends _TransformedPointerEvent with _CopyPointerPanZoomEndEvent implements PointerPanZoomEndEvent {
- _TransformedPointerPanZoomEndEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerPanZoomEndEvent(this.original, this.transform);
@override
final PointerPanZoomEndEvent original;
@@ -2473,8 +2424,7 @@
}
class _TransformedPointerCancelEvent extends _TransformedPointerEvent with _CopyPointerCancelEvent implements PointerCancelEvent {
- _TransformedPointerCancelEvent(this.original, this.transform)
- : assert(original != null), assert(transform != null);
+ _TransformedPointerCancelEvent(this.original, this.transform);
@override
final PointerCancelEvent original;
diff --git a/framework/lib/src/gestures/force_press.dart b/framework/lib/src/gestures/force_press.dart
index 5fe458b..b2d27e0 100644
--- a/framework/lib/src/gestures/force_press.dart
+++ b/framework/lib/src/gestures/force_press.dart
@@ -52,9 +52,7 @@
required this.globalPosition,
Offset? localPosition,
required this.pressure,
- }) : assert(globalPosition != null),
- assert(pressure != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the function was called.
final Offset globalPosition;
@@ -127,16 +125,9 @@
this.peakPressure = 0.85,
this.interpolation = _inverseLerp,
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
- }) : assert(startPressure != null),
- assert(peakPressure != null),
- assert(interpolation != null),
- assert(peakPressure > startPressure);
+ super.allowedButtonsFilter,
+ }) : assert(peakPressure > startPressure);
/// A pointer is in contact with the screen and has just pressed with a force
/// exceeding the [startPressure]. Consequently, if there were other gesture
diff --git a/framework/lib/src/gestures/gesture_settings.dart b/framework/lib/src/gestures/gesture_settings.dart
index 496a2d2..afaa5ed 100644
--- a/framework/lib/src/gestures/gesture_settings.dart
+++ b/framework/lib/src/gestures/gesture_settings.dart
@@ -24,11 +24,11 @@
this.touchSlop,
});
- /// Create a new [DeviceGestureSettings] from the provided [window].
- factory DeviceGestureSettings.fromWindow(ui.FlutterView window) {
- final double? physicalTouchSlop = window.viewConfiguration.gestureSettings.physicalTouchSlop;
+ /// Create a new [DeviceGestureSettings] from the provided [view].
+ factory DeviceGestureSettings.fromView(ui.FlutterView view) {
+ final double? physicalTouchSlop = view.viewConfiguration.gestureSettings.physicalTouchSlop;
return DeviceGestureSettings(
- touchSlop: physicalTouchSlop == null ? null : physicalTouchSlop / window.devicePixelRatio
+ touchSlop: physicalTouchSlop == null ? null : physicalTouchSlop / view.devicePixelRatio
);
}
diff --git a/framework/lib/src/gestures/hit_test.dart b/framework/lib/src/gestures/hit_test.dart
index 221d9d6..547c9d8 100644
--- a/framework/lib/src/gestures/hit_test.dart
+++ b/framework/lib/src/gestures/hit_test.dart
@@ -215,7 +215,6 @@
/// around this function for hit testing on [RenderBox]s.
@protected
void pushTransform(Matrix4 transform) {
- assert(transform != null);
assert(
_debugVectorMoreOrLessEquals(transform.getRow(2), Vector4(0, 0, 1, 0)) &&
_debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)),
@@ -255,7 +254,6 @@
/// around this function for hit testing on [RenderSliver]s.
@protected
void pushOffset(Offset offset) {
- assert(offset != null);
_localTransforms.add(_OffsetTransformPart(offset));
}
diff --git a/framework/lib/src/gestures/long_press.dart b/framework/lib/src/gestures/long_press.dart
index febef8c..d91b8c9 100644
--- a/framework/lib/src/gestures/long_press.dart
+++ b/framework/lib/src/gestures/long_press.dart
@@ -115,8 +115,7 @@
this.globalPosition = Offset.zero,
Offset? localPosition,
this.kind,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
@@ -142,8 +141,7 @@
const LongPressStartDetails({
this.globalPosition = Offset.zero,
Offset? localPosition,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer initially contacted the screen.
final Offset globalPosition;
@@ -168,9 +166,7 @@
Offset? localPosition,
this.offsetFromOrigin = Offset.zero,
Offset? localOffsetFromOrigin,
- }) : assert(globalPosition != null),
- assert(offsetFromOrigin != null),
- localPosition = localPosition ?? globalPosition,
+ }) : localPosition = localPosition ?? globalPosition,
localOffsetFromOrigin = localOffsetFromOrigin ?? offsetFromOrigin;
/// The global position of the pointer when it triggered this update.
@@ -205,8 +201,7 @@
this.globalPosition = Offset.zero,
Offset? localPosition,
this.velocity = Velocity.zero,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer lifted from the screen.
final Offset globalPosition;
@@ -247,17 +242,15 @@
/// The [duration] argument can be used to overwrite the default duration
/// after which the long press will be recognized.
///
+ /// {@macro flutter.gestures.tap.TapGestureRecognizer.allowedButtonsFilter}
+ ///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
LongPressGestureRecognizer({
Duration? duration,
super.postAcceptSlopTolerance = null,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
super.debugOwner,
+ super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
}) : super(
deadline: duration ?? kLongPressTimeout,
);
@@ -268,6 +261,12 @@
// different set of buttons, the gesture is canceled.
int? _initialButtons;
+ // Accept the input if, and only if, a single button is pressed.
+ static bool _defaultButtonAcceptBehavior(int buttons) =>
+ buttons == kPrimaryButton ||
+ buttons == kSecondaryButton ||
+ buttons == kTertiaryButton;
+
/// Called when a pointer has contacted the screen at a particular location
/// with a primary button, which might be the start of a long-press.
///
diff --git a/framework/lib/src/gestures/monodrag.dart b/framework/lib/src/gestures/monodrag.dart
index 5b87812..4930910 100644
--- a/framework/lib/src/gestures/monodrag.dart
+++ b/framework/lib/src/gestures/monodrag.dart
@@ -26,11 +26,13 @@
accepted,
}
+/// {@template flutter.gestures.monodrag.GestureDragEndCallback}
/// Signature for when a pointer that was previously in contact with the screen
/// and moving is no longer in contact with the screen.
///
/// The velocity at which the pointer was moving when it stopped contacting
/// the screen is available in the `details`.
+/// {@endtemplate}
///
/// Used by [DragGestureRecognizer.onEnd].
typedef GestureDragEndCallback = void Function(DragEndDetails details);
@@ -57,9 +59,8 @@
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
-/// [DragGestureRecognizer] competes on pointer events of [kPrimaryButton]
-/// only when it has at least one non-null callback. If it has no callbacks, it
-/// is a no-op.
+/// [DragGestureRecognizer] competes on pointer events only when it has at
+/// least one non-null callback. If it has no callbacks, it is a no-op.
///
/// See also:
///
@@ -74,18 +75,17 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
DragGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
this.dragStartBehavior = DragStartBehavior.start,
this.velocityTrackerBuilder = _defaultBuilder,
super.supportedDevices,
- }) : assert(dragStartBehavior != null);
+ super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
+ });
static VelocityTracker _defaultBuilder(PointerEvent event) => VelocityTracker.withKind(event.kind);
+ // Accept the input if, and only if, [kPrimaryButton] is pressed.
+ static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
+
/// Configure the behavior of offsets passed to [onStart].
///
/// If set to [DragStartBehavior.start], the [onStart] callback will be called
@@ -120,12 +120,14 @@
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragDownDetails], which is passed as an argument to this callback.
GestureDragDownCallback? onDown;
+ /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onStart}
/// A pointer has contacted the screen with a primary button and has begun to
/// move.
+ /// {@endtemplate}
///
/// The position of the pointer is provided in the callback's `details`
/// argument, which is a [DragStartDetails] object. The [dragStartBehavior]
@@ -133,32 +135,36 @@
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragStartDetails], which is passed as an argument to this callback.
GestureDragStartCallback? onStart;
+ /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onUpdate}
/// A pointer that is in contact with the screen with a primary button and
/// moving has moved again.
+ /// {@endtemplate}
///
/// The distance traveled by the pointer since the last update is provided in
/// the callback's `details` argument, which is a [DragUpdateDetails] object.
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragUpdateDetails], which is passed as an argument to this callback.
GestureDragUpdateCallback? onUpdate;
+ /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onEnd}
/// A pointer that was previously in contact with the screen with a primary
/// button and moving is no longer in contact with the screen and was moving
/// at a specific velocity when it stopped contacting the screen.
+ /// {@endtemplate}
///
/// The velocity is provided in the callback's `details` argument, which is a
/// [DragEndDetails] object.
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragEndDetails], which is passed as an argument to this callback.
GestureDragEndCallback? onEnd;
@@ -166,10 +172,10 @@
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
GestureDragCancelCallback? onCancel;
- /// The minimum distance an input pointer drag must have moved to
+ /// The minimum distance an input pointer drag must have moved
/// to be considered a fling gesture.
///
/// This value is typically compared with the distance traveled along the
@@ -243,18 +249,12 @@
@override
bool isPointerAllowed(PointerEvent event) {
if (_initialButtons == null) {
- switch (event.buttons) {
- case kPrimaryButton:
- if (onDown == null &&
- onStart == null &&
- onUpdate == null &&
- onEnd == null &&
- onCancel == null) {
- return false;
- }
- break;
- default:
- return false;
+ if (onDown == null &&
+ onStart == null &&
+ onUpdate == null &&
+ onEnd == null &&
+ onCancel == null) {
+ return false;
}
} else {
// There can be multiple drags simultaneously. Their effects are combined.
@@ -308,7 +308,6 @@
event is PointerPanZoomStartEvent ||
event is PointerPanZoomUpdateEvent)) {
final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
- assert(tracker != null);
if (event is PointerPanZoomStartEvent) {
tracker.addPosition(event.timeStamp, Offset.zero);
} else if (event is PointerPanZoomUpdateEvent) {
@@ -441,7 +440,6 @@
}
void _checkDown() {
- assert(_initialButtons == kPrimaryButton);
if (onDown != null) {
final DragDownDetails details = DragDownDetails(
globalPosition: _initialPosition.global,
@@ -452,7 +450,6 @@
}
void _checkStart(Duration timestamp, int pointer) {
- assert(_initialButtons == kPrimaryButton);
if (onStart != null) {
final DragStartDetails details = DragStartDetails(
sourceTimeStamp: timestamp,
@@ -471,7 +468,6 @@
required Offset globalPosition,
Offset? localPosition,
}) {
- assert(_initialButtons == kPrimaryButton);
if (onUpdate != null) {
final DragUpdateDetails details = DragUpdateDetails(
sourceTimeStamp: sourceTimeStamp,
@@ -485,13 +481,11 @@
}
void _checkEnd(int pointer) {
- assert(_initialButtons == kPrimaryButton);
if (onEnd == null) {
return;
}
final VelocityTracker tracker = _velocityTrackers[pointer]!;
- assert(tracker != null);
final DragEndDetails details;
final String Function() debugReport;
@@ -522,7 +516,6 @@
}
void _checkCancel() {
- assert(_initialButtons == kPrimaryButton);
if (onCancel != null) {
invokeCallback<void>('onCancel', onCancel!);
}
@@ -556,12 +549,8 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
VerticalDragGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
@@ -602,12 +591,8 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
HorizontalDragGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
@@ -646,6 +631,7 @@
PanGestureRecognizer({
super.debugOwner,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
diff --git a/framework/lib/src/gestures/multidrag.dart b/framework/lib/src/gestures/multidrag.dart
index 197fcfb..19d61a0 100644
--- a/framework/lib/src/gestures/multidrag.dart
+++ b/framework/lib/src/gestures/multidrag.dart
@@ -35,8 +35,7 @@
///
/// The [initialPosition] argument must not be null.
MultiDragPointerState(this.initialPosition, this.kind, this.gestureSettings)
- : assert(initialPosition != null),
- _velocityTracker = VelocityTracker.withKind(kind);
+ : _velocityTracker = VelocityTracker.withKind(kind);
/// Device specific gesture configuration that should be preferred over
/// framework constants.
@@ -133,7 +132,6 @@
void _startDrag(Drag client) {
assert(_arenaEntry != null);
assert(_client == null);
- assert(client != null);
assert(pendingDelta != null);
_client = client;
final DragUpdateDetails details = DragUpdateDetails(
@@ -217,14 +215,13 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
MultiDragGestureRecognizer({
required super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
});
+ // Accept the input if, and only if, [kPrimaryButton] is pressed.
+ static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
+
/// Called when this class recognizes the start of a drag gesture.
///
/// The remaining notifications for this drag gesture are delivered to the
@@ -236,8 +233,6 @@
@override
void addAllowedPointer(PointerDownEvent event) {
assert(_pointers != null);
- assert(event.pointer != null);
- assert(event.position != null);
assert(!_pointers!.containsKey(event.pointer));
final MultiDragPointerState state = createNewPointerState(event);
_pointers![event.pointer] = state;
@@ -253,9 +248,6 @@
void _handleEvent(PointerEvent event) {
assert(_pointers != null);
- assert(event.pointer != null);
- assert(event.timeStamp != null);
- assert(event.position != null);
assert(_pointers!.containsKey(event.pointer));
final MultiDragPointerState state = _pointers![event.pointer]!;
if (event is PointerMoveEvent) {
@@ -292,7 +284,6 @@
Drag? _startDrag(Offset initialPosition, int pointer) {
assert(_pointers != null);
final MultiDragPointerState state = _pointers![pointer]!;
- assert(state != null);
assert(state._pendingDelta != null);
Drag? drag;
if (onStart != null) {
@@ -311,7 +302,6 @@
assert(_pointers != null);
if (_pointers!.containsKey(pointer)) {
final MultiDragPointerState state = _pointers![pointer]!;
- assert(state != null);
state.rejected();
_removeState(pointer);
} // else we already preemptively forgot about it (e.g. we got an up event)
@@ -376,12 +366,8 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
ImmediateMultiDragGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
@@ -433,12 +419,8 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
HorizontalMultiDragGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
@@ -490,12 +472,8 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
VerticalMultiDragGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
@override
@@ -508,8 +486,7 @@
}
class _DelayedPointerState extends MultiDragPointerState {
- _DelayedPointerState(super.initialPosition, Duration delay, super.kind, super.deviceGestureSettings)
- : assert(delay != null) {
+ _DelayedPointerState(super.initialPosition, Duration delay, super.kind, super.deviceGestureSettings) {
_timer = Timer(delay, _delayPassed);
}
@@ -600,13 +577,9 @@
DelayedMultiDragGestureRecognizer({
this.delay = kLongPressTimeout,
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
- }) : assert(delay != null);
+ super.allowedButtonsFilter,
+ });
/// The amount of time the pointer must remain in the same place for the drag
/// to be recognized.
diff --git a/framework/lib/src/gestures/multitap.dart b/framework/lib/src/gestures/multitap.dart
index 426e00b..b4d6c84 100644
--- a/framework/lib/src/gestures/multitap.dart
+++ b/framework/lib/src/gestures/multitap.dart
@@ -43,8 +43,7 @@
/// CountdownZoned tracks whether the specified duration has elapsed since
/// creation, honoring [Zone].
class _CountdownZoned {
- _CountdownZoned({ required Duration duration })
- : assert(duration != null) {
+ _CountdownZoned({ required Duration duration }) {
Timer(duration, _onTimeout);
}
@@ -65,10 +64,7 @@
required this.entry,
required Duration doubleTapMinTime,
required this.gestureSettings,
- }) : assert(doubleTapMinTime != null),
- assert(event != null),
- assert(event.buttons != null),
- pointer = event.pointer,
+ }) : pointer = event.pointer,
_initialGlobalPosition = event.position,
initialButtons = event.buttons,
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
@@ -113,8 +109,8 @@
/// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession.
///
-/// [DoubleTapGestureRecognizer] competes on pointer events of [kPrimaryButton]
-/// only when it has a non-null callback. If it has no callbacks, it is a no-op.
+/// [DoubleTapGestureRecognizer] competes on pointer events when it
+/// has a non-null callback. If it has no callbacks, it is a no-op.
///
class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps.
@@ -122,14 +118,14 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
DoubleTapGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
});
+ // The default value for [allowedButtonsFilter].
+ // Accept the input if, and only if, [kPrimaryButton] is pressed.
+ static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
+
// Implementation notes:
//
// The double tap recognizer can be in one of four states. There's no
@@ -165,7 +161,7 @@
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [TapDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onDoubleTapDown], which exposes this callback.
GestureTapDownCallback? onDoubleTapDown;
@@ -178,7 +174,7 @@
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [GestureDetector.onDoubleTap], which exposes this callback.
GestureDoubleTapCallback? onDoubleTap;
@@ -192,7 +188,7 @@
///
/// See also:
///
- /// * [kPrimaryButton], the button this callback responds to.
+ /// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [GestureDetector.onDoubleTapCancel], which exposes this callback.
GestureTapCancelCallback? onDoubleTapCancel;
@@ -203,19 +199,19 @@
@override
bool isPointerAllowed(PointerDownEvent event) {
if (_firstTap == null) {
- switch (event.buttons) {
- case kPrimaryButton:
- if (onDoubleTapDown == null &&
- onDoubleTap == null &&
- onDoubleTapCancel == null) {
- return false;
- }
- break;
- default:
- return false;
+ if (onDoubleTapDown == null &&
+ onDoubleTap == null &&
+ onDoubleTapCancel == null) {
+ return false;
}
}
- return super.isPointerAllowed(event);
+
+ // If second tap is not allowed, reset the state.
+ final bool isPointerAllowed = super.isPointerAllowed(event);
+ if (isPointerAllowed == false) {
+ _reset();
+ }
+ return isPointerAllowed;
}
@override
@@ -367,7 +363,6 @@
}
void _checkUp(int buttons) {
- assert(buttons == kPrimaryButton);
if (onDoubleTap != null) {
invokeCallback<void>('onDoubleTap', onDoubleTap!);
}
@@ -486,12 +481,8 @@
MultiTapGestureRecognizer({
this.longTapDelay = Duration.zero,
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
/// A pointer that might cause a tap has contacted the screen at a particular
@@ -679,8 +670,7 @@
/// The `count` argument must be greater than zero.
SerialTapCancelDetails({
this.count = 1,
- }) : assert(count != null),
- assert(count > 0);
+ }) : assert(count > 0);
/// The number of consecutive taps that were in progress when the gesture was
/// interrupted.
@@ -813,6 +803,7 @@
SerialTapGestureRecognizer({
super.debugOwner,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
/// A pointer has contacted the screen at a particular location, which might
diff --git a/framework/lib/src/gestures/pointer_signal_resolver.dart b/framework/lib/src/gestures/pointer_signal_resolver.dart
index 6cf84e0..6ffcf76 100644
--- a/framework/lib/src/gestures/pointer_signal_resolver.dart
+++ b/framework/lib/src/gestures/pointer_signal_resolver.dart
@@ -64,8 +64,6 @@
/// See the documentation for the [PointerSignalResolver] class on when and
/// how this method should be used.
void register(PointerSignalEvent event, PointerSignalResolvedCallback callback) {
- assert(event != null);
- assert(callback != null);
assert(_currentEvent == null || _isSameEvent(_currentEvent!, event));
if (_firstRegisteredCallback != null) {
return;
diff --git a/framework/lib/src/gestures/recognizer.dart b/framework/lib/src/gestures/recognizer.dart
index 243c86a..0a30fbf 100644
--- a/framework/lib/src/gestures/recognizer.dart
+++ b/framework/lib/src/gestures/recognizer.dart
@@ -48,6 +48,11 @@
start,
}
+/// Signature for `allowedButtonsFilter` in [GestureRecognizer].
+/// Used to filter the input buttons of incoming pointer events.
+/// The parameter `buttons` comes from [PointerEvent.buttons].
+typedef AllowedButtonsFilter = bool Function(int buttons);
+
/// The base class that all gesture recognizers inherit from.
///
/// Provides a basic API that can be used by classes that work with
@@ -73,14 +78,10 @@
/// {@endtemplate}
GestureRecognizer({
this.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- PointerDeviceKind? kind,
Set<PointerDeviceKind>? supportedDevices,
- }) : assert(kind == null || supportedDevices == null),
- _supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind };
+ AllowedButtonsFilter? allowedButtonsFilter,
+ }) : _supportedDevices = supportedDevices,
+ _allowedButtonsFilter = allowedButtonsFilter ?? _defaultButtonAcceptBehavior;
/// The recognizer's owner.
///
@@ -98,6 +99,29 @@
/// tracked and recognized.
final Set<PointerDeviceKind>? _supportedDevices;
+ /// {@template flutter.gestures.multidrag._allowedButtonsFilter}
+ /// Called when interaction starts. This limits the dragging behavior
+ /// for custom clicks (such as scroll click). Its parameter comes
+ /// from [PointerEvent.buttons].
+ ///
+ /// Due to how [kPrimaryButton], [kSecondaryButton], etc., use integers,
+ /// bitwise operations can help filter how buttons are pressed.
+ /// For example, if someone simultaneously presses the primary and secondary
+ /// buttons, the default behavior will return false. The following code
+ /// accepts any button press with primary:
+ /// `(int buttons) => buttons & kPrimaryButton != 0`.
+ ///
+ /// When value is `(int buttons) => false`, allow no interactions.
+ /// When value is `(int buttons) => true`, allow all interactions.
+ ///
+ /// Defaults to all buttons.
+ /// {@endtemplate}
+ final AllowedButtonsFilter _allowedButtonsFilter;
+
+ // The default value for [allowedButtonsFilter].
+ // Accept any input.
+ static bool _defaultButtonAcceptBehavior(int buttons) => true;
+
/// Holds a mapping between pointer IDs and the kind of devices they are
/// coming from.
final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
@@ -185,9 +209,9 @@
/// Checks whether or not a pointer is allowed to be tracked by this recognizer.
@protected
bool isPointerAllowed(PointerDownEvent event) {
- // Currently, it only checks for device kind. But in the future we could check
- // for other things e.g. mouse button.
- return _supportedDevices == null || _supportedDevices!.contains(event.kind);
+ return (_supportedDevices == null ||
+ _supportedDevices!.contains(event.kind)) &&
+ _allowedButtonsFilter(event.buttons);
}
/// Handles a pointer pan/zoom being added that's not allowed by this recognizer.
@@ -237,7 +261,6 @@
@protected
@pragma('vm:notify-debugger-on-exception')
T? invokeCallback<T>(String name, RecognizerCallback<T> callback, { String Function()? debugReport }) {
- assert(callback != null);
T? result;
try {
assert(() {
@@ -292,12 +315,8 @@
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
OneSequenceGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
});
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
@@ -427,7 +446,8 @@
void startTrackingPointer(int pointer, [Matrix4? transform]) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
- assert(!_entries.containsValue(pointer));
+ // TODO(goderbauer): Enable assert after recognizers properly clean up their defunct `_entries`, see https://github.com/flutter/flutter/issues/117356.
+ // assert(!_entries.containsKey(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
@@ -504,12 +524,8 @@
this.preAcceptSlopTolerance = kTouchSlop,
this.postAcceptSlopTolerance = kTouchSlop,
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
}) : assert(
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
'The preAcceptSlopTolerance must be positive or null',
diff --git a/framework/lib/src/gestures/scale.dart b/framework/lib/src/gestures/scale.dart
index 8ee9c51..8d7e0ab 100644
--- a/framework/lib/src/gestures/scale.dart
+++ b/framework/lib/src/gestures/scale.dart
@@ -15,6 +15,18 @@
export 'recognizer.dart' show DragStartBehavior;
export 'velocity_tracker.dart' show Velocity;
+/// The default conversion factor when treating mouse scrolling as scaling.
+///
+/// The value was arbitrarily chosen to feel natural for most mousewheels on
+/// all supported platforms.
+const double kDefaultMouseScrollToScaleFactor = 200;
+
+/// The default conversion factor when treating trackpad scrolling as scaling.
+///
+/// This factor matches the default [kDefaultMouseScrollToScaleFactor] of 200 to
+/// feel natural for most trackpads, and the convention that scrolling up means
+/// zooming in.
+const Offset kDefaultTrackpadScrollToScaleFactor = Offset(0, -1/kDefaultMouseScrollToScaleFactor);
/// The possible states of a [ScaleGestureRecognizer].
enum _ScaleState {
@@ -36,17 +48,49 @@
}
class _PointerPanZoomData {
- _PointerPanZoomData({
- required this.focalPoint,
- required this.scale,
- required this.rotation
- });
- Offset focalPoint;
- double scale;
- double rotation;
+ _PointerPanZoomData.fromStartEvent(
+ this.parent,
+ PointerPanZoomStartEvent event
+ ) : _position = event.position,
+ _pan = Offset.zero,
+ _scale = 1,
+ _rotation = 0;
+
+ _PointerPanZoomData.fromUpdateEvent(
+ this.parent,
+ PointerPanZoomUpdateEvent event
+ ) : _position = event.position,
+ _pan = event.pan,
+ _scale = event.scale,
+ _rotation = event.rotation;
+
+ final ScaleGestureRecognizer parent;
+ final Offset _position;
+ final Offset _pan;
+ final double _scale;
+ final double _rotation;
+
+ Offset get focalPoint {
+ if (parent.trackpadScrollCausesScale) {
+ return _position;
+ }
+ return _position + _pan;
+ }
+
+ double get scale {
+ if (parent.trackpadScrollCausesScale) {
+ return _scale * math.exp(
+ (_pan.dx * parent.trackpadScrollToScaleFactor.dx) +
+ (_pan.dy * parent.trackpadScrollToScaleFactor.dy)
+ );
+ }
+ return _scale;
+ }
+
+ double get rotation => _rotation;
@override
- String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)';
+ String toString() => '_PointerPanZoomData(parent: $parent, _position: $_position, _pan: $_pan, _scale: $_scale, _rotation: $_rotation)';
}
/// Details for [GestureScaleStartCallback].
@@ -54,8 +98,11 @@
/// Creates details for [GestureScaleStartCallback].
///
/// The [focalPoint] argument must not be null.
- ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, this.pointerCount = 0 })
- : assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
+ ScaleStartDetails({
+ this.focalPoint = Offset.zero,
+ Offset? localFocalPoint,
+ this.pointerCount = 0,
+ }) : localFocalPoint = localFocalPoint ?? focalPoint;
/// The initial focal point of the pointers in contact with the screen.
///
@@ -104,12 +151,9 @@
this.rotation = 0.0,
this.pointerCount = 0,
this.focalPointDelta = Offset.zero,
- }) : assert(focalPoint != null),
- assert(focalPointDelta != null),
- assert(scale != null && scale >= 0.0),
- assert(horizontalScale != null && horizontalScale >= 0.0),
- assert(verticalScale != null && verticalScale >= 0.0),
- assert(rotation != null),
+ }) : assert(scale >= 0.0),
+ assert(horizontalScale >= 0.0),
+ assert(verticalScale >= 0.0),
localFocalPoint = localFocalPoint ?? focalPoint;
/// The amount the gesture's focal point has moved in the coordinate space of
@@ -201,12 +245,14 @@
/// Creates details for [GestureScaleEndCallback].
///
/// The [velocity] argument must not be null.
- ScaleEndDetails({ this.velocity = Velocity.zero, this.pointerCount = 0 })
- : assert(velocity != null);
+ ScaleEndDetails({ this.velocity = Velocity.zero, this.scaleVelocity = 0, this.pointerCount = 0 });
/// The velocity of the last pointer to be lifted off of the screen.
final Velocity velocity;
+ /// The final velocity of the scale factor reported by the gesture.
+ final double scaleVelocity;
+
/// The number of pointers being tracked by the gesture recognizer.
///
/// Typically this is the number of fingers being used to pan the widget using the gesture
@@ -214,7 +260,7 @@
final int pointerCount;
@override
- String toString() => 'ScaleEndDetails(velocity: $velocity, pointerCount: $pointerCount)';
+ String toString() => 'ScaleEndDetails(velocity: $velocity, scaleVelocity: $scaleVelocity, pointerCount: $pointerCount)';
}
/// Signature for when the pointers in contact with the screen have established
@@ -229,7 +275,6 @@
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);
bool _isFlingGesture(Velocity velocity) {
- assert(velocity != null);
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
}
@@ -249,9 +294,7 @@
this.pointerStartId = 0,
this.pointerEndLocation = Offset.zero,
this.pointerEndId = 1,
- }) : assert(pointerStartLocation != null && pointerEndLocation != null),
- assert(pointerStartId != null && pointerEndId != null),
- assert(pointerStartId != pointerEndId);
+ }) : assert(pointerStartId != pointerEndId);
// The location and the id of the pointer that marks the start of the line.
final Offset pointerStartLocation;
@@ -268,23 +311,22 @@
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
/// calculates their focal point, indicated scale, and rotation. When a focal
-/// pointer is established, the recognizer calls [onStart]. As the focal point,
-/// scale, rotation change, the recognizer calls [onUpdate]. When the pointers
-/// are no longer in contact with the screen, the recognizer calls [onEnd].
+/// point is established, the recognizer calls [onStart]. As the focal point,
+/// scale, and rotation change, the recognizer calls [onUpdate]. When the
+/// pointers are no longer in contact with the screen, the recognizer calls
+/// [onEnd].
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create a gesture recognizer for interactions intended for scaling content.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
ScaleGestureRecognizer({
super.debugOwner,
- @Deprecated(
- 'Migrate to supportedDevices. '
- 'This feature was deprecated after v2.3.0-1.0.pre.',
- )
- super.kind,
super.supportedDevices,
+ super.allowedButtonsFilter,
this.dragStartBehavior = DragStartBehavior.down,
- }) : assert(dragStartBehavior != null);
+ this.trackpadScrollCausesScale = false,
+ this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
+ });
/// Determines what point is used as the starting point in all calculations
/// involving this gesture.
@@ -331,6 +373,26 @@
Matrix4? _lastTransform;
+ /// {@template flutter.gestures.scale.trackpadScrollCausesScale}
+ /// Whether scrolling up/down on a trackpad should cause scaling instead of
+ /// panning.
+ ///
+ /// Defaults to false.
+ /// {@endtemplate}
+ bool trackpadScrollCausesScale;
+
+ /// {@template flutter.gestures.scale.trackpadScrollToScaleFactor}
+ /// A factor to control the direction and magnitude of scale when converting
+ /// trackpad scrolling.
+ ///
+ /// Incoming trackpad pan offsets will be divided by this factor to get scale
+ /// values. Increasing this offset will reduce the amount of scaling caused by
+ /// a fixed amount of trackpad scrolling.
+ ///
+ /// Defaults to [kDefaultTrackpadScrollToScaleFactor].
+ /// {@endtemplate}
+ Offset trackpadScrollToScaleFactor;
+
late Offset _initialFocalPoint;
Offset? _currentFocalPoint;
late double _initialSpan;
@@ -345,6 +407,7 @@
final Map<int, Offset> _pointerLocations = <int, Offset>{};
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
+ VelocityTracker? _scaleVelocityTracker;
late Offset _delta;
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
double _initialPanZoomScaleFactor = 1;
@@ -465,23 +528,16 @@
_lastTransform = event.transform;
} else if (event is PointerPanZoomStartEvent) {
assert(_pointerPanZooms[event.pointer] == null);
- _pointerPanZooms[event.pointer] = _PointerPanZoomData(
- focalPoint: event.position,
- scale: 1,
- rotation: 0
- );
+ _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event);
didChangeConfiguration = true;
shouldStartIfAccepted = true;
+ _lastTransform = event.transform;
} else if (event is PointerPanZoomUpdateEvent) {
assert(_pointerPanZooms[event.pointer] != null);
- if (!event.synthesized) {
+ if (!event.synthesized && !trackpadScrollCausesScale) {
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
}
- _pointerPanZooms[event.pointer] = _PointerPanZoomData(
- focalPoint: event.position + event.pan,
- scale: event.scale,
- rotation: event.rotation
- );
+ _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event);
_lastTransform = event.transform;
shouldStartIfAccepted = true;
} else if (event is PointerPanZoomEndEvent) {
@@ -494,7 +550,7 @@
_update();
if (!didChangeConfiguration || _reconfigure(event.pointer)) {
- _advanceStateMachine(shouldStartIfAccepted, event.kind);
+ _advanceStateMachine(shouldStartIfAccepted, event);
}
stopTrackingIfPointerNoLongerDown(event);
}
@@ -606,18 +662,20 @@
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
}
- invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount)));
+ invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
} else {
- invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
+ invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
}
}
_state = _ScaleState.accepted;
+ _scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
return false;
}
+ _scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
return true;
}
- void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
+ void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
if (_state == _ScaleState.ready) {
_state = _ScaleState.possible;
}
@@ -625,7 +683,7 @@
if (_state == _ScaleState.possible) {
final double spanDelta = (_currentSpan - _initialSpan).abs();
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
- if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
+ if (spanDelta > computeScaleSlop(event.kind) || focalPointDelta > computePanSlop(event.kind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
resolve(GestureDisposition.accepted);
}
} else if (_state.index >= _ScaleState.accepted.index) {
@@ -637,19 +695,22 @@
_dispatchOnStartCallbackIfNeeded();
}
- if (_state == _ScaleState.started && onUpdate != null) {
- invokeCallback<void>('onUpdate', () {
- onUpdate!(ScaleUpdateDetails(
- scale: _scaleFactor,
- horizontalScale: _horizontalScaleFactor,
- verticalScale: _verticalScaleFactor,
- focalPoint: _currentFocalPoint!,
- localFocalPoint: _localFocalPoint,
- rotation: _computeRotationFactor(),
- pointerCount: _pointerCount,
- focalPointDelta: _delta,
- ));
- });
+ if (_state == _ScaleState.started) {
+ _scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
+ if (onUpdate != null) {
+ invokeCallback<void>('onUpdate', () {
+ onUpdate!(ScaleUpdateDetails(
+ scale: _scaleFactor,
+ horizontalScale: _horizontalScaleFactor,
+ verticalScale: _verticalScaleFactor,
+ focalPoint: _currentFocalPoint!,
+ localFocalPoint: _localFocalPoint,
+ rotation: _computeRotationFactor(),
+ pointerCount: _pointerCount,
+ focalPointDelta: _delta,
+ ));
+ });
+ }
}
}
diff --git a/framework/lib/src/gestures/tap.dart b/framework/lib/src/gestures/tap.dart
index b58dfff..efa3a05 100644
--- a/framework/lib/src/gestures/tap.dart
+++ b/framework/lib/src/gestures/tap.dart
@@ -32,8 +32,7 @@
this.globalPosition = Offset.zero,
Offset? localPosition,
this.kind,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
@@ -45,11 +44,13 @@
final Offset localPosition;
}
+/// {@template flutter.gestures.tap.GestureTapDownCallback}
/// Signature for when a pointer that might cause a tap has contacted the
/// screen.
///
/// The position at which the pointer contacted the screen is available in the
/// `details`.
+/// {@endtemplate}
///
/// See also:
///
@@ -69,8 +70,7 @@
required this.kind,
this.globalPosition = Offset.zero,
Offset? localPosition,
- }) : assert(globalPosition != null),
- localPosition = localPosition ?? globalPosition;
+ }) : localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
@@ -82,11 +82,13 @@
final PointerDeviceKind kind;
}
+/// {@template flutter.gestures.tap.GestureTapUpCallback}
/// Signature for when a pointer that will trigger a tap has stopped contacting
/// the screen.
///
/// The position at which the pointer stopped contacting the screen is available
/// in the `details`.
+/// {@endtemplate}
///
/// See also:
///
@@ -145,7 +147,11 @@
/// Creates a tap gesture recognizer.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
- BaseTapGestureRecognizer({ super.debugOwner, super.supportedDevices })
+ BaseTapGestureRecognizer({
+ super.debugOwner,
+ super.supportedDevices,
+ super.allowedButtonsFilter,
+ })
: super(deadline: kPressTimeout);
bool _sentTapDown = false;
@@ -198,7 +204,6 @@
@override
void addAllowedPointer(PointerDownEvent event) {
- assert(event != null);
if (state == GestureRecognizerState.ready) {
// If there is no result in the previous gesture arena,
// we ignore them and prepare to accept a new pointer.
@@ -216,7 +221,7 @@
if (_down != null) {
// This happens when this tap gesture has been rejected while the pointer
// is down (i.e. due to movement), when another allowed pointer is added,
- // in which case all pointers are simply ignored. The `_down` being null
+ // in which case all pointers are ignored. The `_down` being null
// means that _reset() has been called, since it is always set at the
// first allowed down event and will not be cleared except for reset(),
super.addAllowedPointer(event);
@@ -350,6 +355,16 @@
/// one non-null `onTertiaryTap*` callback. If it has no callbacks, it is a
/// no-op.
///
+/// {@template flutter.gestures.tap.TapGestureRecognizer.allowedButtonsFilter}
+/// The [allowedButtonsFilter] argument only gives this recognizer the
+/// ability to limit the buttons it accepts. It does not provide the
+/// ability to recognize any buttons beyond the ones it already accepts:
+/// kPrimaryButton, kSecondaryButton or kTertiaryButton. Therefore, a
+/// combined value of `kPrimaryButton & kSecondaryButton` would be ignored,
+/// but `kPrimaryButton | kSecondaryButton` would be allowed, as long as
+/// only one of them is selected at a time.
+/// {@endtemplate}
+///
/// See also:
///
/// * [GestureDetector.onTap], which uses this recognizer.
@@ -358,10 +373,16 @@
/// Creates a tap gesture recognizer.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
- TapGestureRecognizer({ super.debugOwner, super.supportedDevices });
+ TapGestureRecognizer({
+ super.debugOwner,
+ super.supportedDevices,
+ super.allowedButtonsFilter,
+ });
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onTapDown}
/// A pointer has contacted the screen at a particular location with a primary
/// button, which might be the start of a tap.
+ /// {@endtemplate}
///
/// This triggers after the down event, once a short timeout ([deadline]) has
/// elapsed, or once the gestures has won the arena, whichever comes first.
@@ -378,8 +399,10 @@
/// * [GestureDetector.onTapDown], which exposes this callback.
GestureTapDownCallback? onTapDown;
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onTapUp}
/// A pointer has stopped contacting the screen at a particular location,
/// which is recognized as a tap of a primary button.
+ /// {@endtemplate}
///
/// This triggers on the up event, if the recognizer wins the arena with it
/// or has previously won, immediately followed by [onTap].
@@ -411,8 +434,10 @@
/// * [GestureDetector.onTap], which exposes this callback.
GestureTapCallback? onTap;
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onTapCancel}
/// A pointer that previously triggered [onTapDown] will not end up causing
/// a tap.
+ /// {@endtemplate}
///
/// This triggers once the gesture loses the arena if [onTapDown] has
/// previously been triggered.
@@ -428,8 +453,10 @@
/// * [GestureDetector.onTapCancel], which exposes this callback.
GestureTapCancelCallback? onTapCancel;
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTap}
/// A pointer has stopped contacting the screen, which is recognized as a tap
/// of a secondary button.
+ /// {@endtemplate}
///
/// This triggers on the up event, if the recognizer wins the arena with it or
/// has previously won, immediately following [onSecondaryTapUp].
@@ -444,8 +471,10 @@
/// * [GestureDetector.onSecondaryTap], which exposes this callback.
GestureTapCallback? onSecondaryTap;
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTapDown}
/// A pointer has contacted the screen at a particular location with a
/// secondary button, which might be the start of a secondary tap.
+ /// {@endtemplate}
///
/// This triggers after the down event, once a short timeout ([deadline]) has
/// elapsed, or once the gestures has won the arena, whichever comes first.
@@ -462,8 +491,10 @@
/// * [GestureDetector.onSecondaryTapDown], which exposes this callback.
GestureTapDownCallback? onSecondaryTapDown;
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTapUp}
/// A pointer has stopped contacting the screen at a particular location,
/// which is recognized as a tap of a secondary button.
+ /// {@endtemplate}
///
/// This triggers on the up event if the recognizer wins the arena with it
/// or has previously won.
@@ -482,8 +513,10 @@
/// * [GestureDetector.onSecondaryTapUp], which exposes this callback.
GestureTapUpCallback? onSecondaryTapUp;
+ /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTapCancel}
/// A pointer that previously triggered [onSecondaryTapDown] will not end up
/// causing a tap.
+ /// {@endtemplate}
///
/// This triggers once the gesture loses the arena if [onSecondaryTapDown]
/// has previously been triggered.
diff --git a/framework/lib/src/gestures/velocity_tracker.dart b/framework/lib/src/gestures/velocity_tracker.dart
index 1e61fda..973e7cd 100644
--- a/framework/lib/src/gestures/velocity_tracker.dart
+++ b/framework/lib/src/gestures/velocity_tracker.dart
@@ -18,7 +18,7 @@
/// The [pixelsPerSecond] argument must not be null.
const Velocity({
required this.pixelsPerSecond,
- }) : assert(pixelsPerSecond != null);
+ });
/// A velocity that isn't moving at all.
static const Velocity zero = Velocity(pixelsPerSecond: Offset.zero);
@@ -50,8 +50,8 @@
/// If the magnitude of this Velocity is within the specified bounds then
/// just return this.
Velocity clampMagnitude(double minValue, double maxValue) {
- assert(minValue != null && minValue >= 0.0);
- assert(maxValue != null && maxValue >= 0.0 && maxValue >= minValue);
+ assert(minValue >= 0.0);
+ assert(maxValue >= 0.0 && maxValue >= minValue);
final double valueSquared = pixelsPerSecond.distanceSquared;
if (valueSquared > maxValue * maxValue) {
return Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * maxValue);
@@ -97,10 +97,7 @@
required this.confidence,
required this.duration,
required this.offset,
- }) : assert(pixelsPerSecond != null),
- assert(confidence != null),
- assert(duration != null),
- assert(offset != null);
+ });
/// The number of pixels per second of velocity in the x and y directions.
final Offset pixelsPerSecond;
@@ -124,9 +121,7 @@
}
class _PointAtTime {
- const _PointAtTime(this.point, this.time)
- : assert(point != null),
- assert(time != null);
+ const _PointAtTime(this.point, this.time);
final Duration time;
final Offset point;
diff --git a/framework/lib/src/material/about.dart b/framework/lib/src/material/about.dart
index fd6b1bb..dea95b4 100644
--- a/framework/lib/src/material/about.dart
+++ b/framework/lib/src/material/about.dart
@@ -184,8 +184,6 @@
RouteSettings? routeSettings,
Offset? anchorPoint,
}) {
- assert(context != null);
- assert(useRootNavigator != null);
showDialog<void>(
context: context,
useRootNavigator: useRootNavigator,
@@ -230,8 +228,6 @@
String? applicationLegalese,
bool useRootNavigator = false,
}) {
- assert(context != null);
- assert(useRootNavigator != null);
Navigator.of(context, rootNavigator: useRootNavigator).push(MaterialPageRoute<void>(
builder: (BuildContext context) => LicensePage(
applicationName: applicationName,
@@ -486,8 +482,7 @@
required this.version,
this.icon,
this.legalese,
- }) : assert(name != null),
- assert(version != null);
+ });
final String name;
final String version;
@@ -542,8 +537,7 @@
required this.about,
required this.isLateral,
required this.selectedId,
- }) : assert(about != null),
- assert(isLateral != null);
+ });
final Widget about;
final bool isLateral;
@@ -571,6 +565,17 @@
builder: (BuildContext context, BoxConstraints constraints) {
switch (snapshot.connectionState) {
case ConnectionState.done:
+ if (snapshot.hasError) {
+ assert(() {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: snapshot.error!,
+ stack: snapshot.stackTrace,
+ context: ErrorDescription('while decoding the license file'),
+ ));
+ return true;
+ }());
+ return Center(child: Text(snapshot.error.toString()));
+ }
_initDefaultDetailPage(snapshot.data!, context);
return ValueListenableBuilder<int?>(
valueListenable: widget.selectedId,
@@ -745,7 +750,7 @@
final List<LicenseEntry> licenseEntries;
@override
- bool operator ==(final dynamic other) {
+ bool operator ==(final Object other) {
if (other is _DetailArguments) {
return other.packageName == packageName;
}
@@ -867,7 +872,8 @@
title: _PackageLicensePageTitle(
title,
subtitle,
- theme.appBarTheme.textTheme ?? theme.primaryTextTheme,
+ theme.primaryTextTheme,
+ theme.appBarTheme.titleTextStyle,
),
),
body: Center(
@@ -899,7 +905,7 @@
automaticallyImplyLeading: false,
pinned: true,
backgroundColor: theme.cardColor,
- title: _PackageLicensePageTitle(title, subtitle, theme.textTheme),
+ title: _PackageLicensePageTitle(title, subtitle, theme.textTheme, theme.textTheme.titleLarge),
),
SliverPadding(
padding: padding,
@@ -929,21 +935,24 @@
this.title,
this.subtitle,
this.theme,
+ this.titleTextStyle,
);
final String title;
final String subtitle;
final TextTheme theme;
+ final TextStyle? titleTextStyle;
@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: theme.titleLarge?.copyWith(color: color)),
+ Text(title, style: effectiveTitleTextStyle?.copyWith(color: color)),
Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
],
);
@@ -976,7 +985,7 @@
const double _narrowGutterSize = 12.0;
double _getGutterSize(BuildContext context) =>
- MediaQuery.of(context).size.width >= _materialGutterThreshold ? _wideGutterSize : _narrowGutterSize;
+ MediaQuery.sizeOf(context).width >= _materialGutterThreshold ? _wideGutterSize : _narrowGutterSize;
/// Signature for the builder callback used by [_MasterDetailFlow].
typedef _MasterViewBuilder = Widget Function(BuildContext context, bool isLateralUI);
@@ -1033,14 +1042,11 @@
const _MasterDetailFlow({
required this.detailPageBuilder,
required this.masterViewBuilder,
- this.automaticallyImplyLeading = true,
+ this.automaticallyImplyLeading = true, // ignore: unused_element
this.detailPageFABlessGutterWidth,
- this.displayMode = _LayoutMode.auto,
+ this.displayMode = _LayoutMode.auto, // ignore: unused_element
this.title,
- }) : assert(masterViewBuilder != null),
- assert(automaticallyImplyLeading != null),
- assert(detailPageBuilder != null),
- assert(displayMode != null);
+ });
/// Builder for the master view for lateral navigation.
///
@@ -1301,8 +1307,7 @@
this.title,
required this.automaticallyImplyLeading,
this.detailPageFABlessGutterWidth,
- }) : assert(detailPageBuilder != null),
- assert(masterViewBuilder != null);
+ });
final _MasterViewBuilder masterViewBuilder;
@@ -1451,8 +1456,7 @@
const _DetailView({
required _DetailPageBuilder builder,
Object? arguments,
- }) : assert(builder != null),
- _builder = builder,
+ }) : _builder = builder,
_arguments = arguments;
final _DetailPageBuilder _builder;
@@ -1463,7 +1467,7 @@
if (_arguments == null) {
return const SizedBox.shrink();
}
- final double screenHeight = MediaQuery.of(context).size.height;
+ final double screenHeight = MediaQuery.sizeOf(context).height;
final double minHeight = (screenHeight - kToolbarHeight) / screenHeight;
return DraggableScrollableSheet(
diff --git a/framework/lib/src/material/action_chip.dart b/framework/lib/src/material/action_chip.dart
index 7d3c17f..e3e30a1 100644
--- a/framework/lib/src/material/action_chip.dart
+++ b/framework/lib/src/material/action_chip.dart
@@ -20,7 +20,7 @@
/// Action chips can be tapped to trigger an action or show progress and
/// confirmation. For Material 3, a disabled state is supported for Action
/// chips and is specified with [onPressed] being null. For previous versions
-/// of Material Design, it is recommended to remove the Action chip from the
+/// of Material Design, it is recommended to remove the Action chip from
/// the interface entirely rather than display a disabled chip.
///
/// Action chips are displayed after primary content, such as below a card or
@@ -86,10 +86,7 @@
this.shadowColor,
this.surfaceTintColor,
this.iconTheme,
- }) : assert(label != null),
- assert(clipBehavior != null),
- assert(autofocus != null),
- assert(pressElevation == null || pressElevation >= 0.0),
+ }) : assert(pressElevation == null || pressElevation >= 0.0),
assert(elevation == null || elevation >= 0.0);
@override
@@ -178,7 +175,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _ActionChipDefaultsM3 extends ChipThemeData {
const _ActionChipDefaultsM3(this.context, this.isEnabled)
@@ -239,7 +236,7 @@
EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 8.0),
const EdgeInsets.symmetric(horizontal: 4.0),
- clampDouble(MediaQuery.of(context).textScaleFactor - 1.0, 0.0, 1.0),
+ clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
)!;
}
diff --git a/framework/lib/src/material/adaptive_text_selection_toolbar.dart b/framework/lib/src/material/adaptive_text_selection_toolbar.dart
index 5f9af12..5b94777 100644
--- a/framework/lib/src/material/adaptive_text_selection_toolbar.dart
+++ b/framework/lib/src/material/adaptive_text_selection_toolbar.dart
@@ -211,6 +211,8 @@
return localizations.pasteButtonLabel;
case ContextMenuButtonType.selectAll:
return localizations.selectAllButtonLabel;
+ case ContextMenuButtonType.delete:
+ return localizations.deleteButtonTooltip.toUpperCase();
case ContextMenuButtonType.custom:
return '';
}
@@ -218,7 +220,7 @@
}
/// Returns a List of Widgets generated by turning [buttonItems] into the
- /// the default context menu buttons for the current platform.
+ /// default context menu buttons for the current platform.
///
/// This is useful when building a text selection toolbar with the default
/// button appearance for the given platform, but where the toolbar and/or the
diff --git a/framework/lib/src/material/animated_icons/animated_icons.dart b/framework/lib/src/material/animated_icons/animated_icons.dart
index a28f015..bc8ea27 100644
--- a/framework/lib/src/material/animated_icons/animated_icons.dart
+++ b/framework/lib/src/material/animated_icons/animated_icons.dart
@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-part of material_animated_icons;
+// TODO(goderbauer): Clean up the part-of hack currently used for testing the private implementation.
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
// The code for drawing animated icons is kept in a private API, as we are not
// yet ready for exposing a public API for (partial) vector graphics support.
@@ -38,8 +39,7 @@
this.size,
this.semanticLabel,
this.textDirection,
- }) : assert(progress != null),
- assert(icon != null);
+ });
/// The animation progress for the animated icon.
///
diff --git a/framework/lib/src/material/animated_icons/animated_icons_data.dart b/framework/lib/src/material/animated_icons/animated_icons_data.dart
index 242ba5a..6efca7d 100644
--- a/framework/lib/src/material/animated_icons/animated_icons_data.dart
+++ b/framework/lib/src/material/animated_icons/animated_icons_data.dart
@@ -8,7 +8,7 @@
// while the _AnimatedIconData interface which used to deliver the icon data is
// kept private.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
/// Identifier for the supported Material Design animated icons.
///
diff --git a/framework/lib/src/material/animated_icons/data/add_event.g.dart b/framework/lib/src/material/animated_icons/data/add_event.g.dart
index 27305b8..88d4600 100644
--- a/framework/lib/src/material/animated_icons/data/add_event.g.dart
+++ b/framework/lib/src/material/animated_icons/data/add_event.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$add_event = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/arrow_menu.g.dart b/framework/lib/src/material/animated_icons/data/arrow_menu.g.dart
index 2649d8c..549c870 100644
--- a/framework/lib/src/material/animated_icons/data/arrow_menu.g.dart
+++ b/framework/lib/src/material/animated_icons/data/arrow_menu.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$arrow_menu = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/close_menu.g.dart b/framework/lib/src/material/animated_icons/data/close_menu.g.dart
index a0b86c0..311a745 100644
--- a/framework/lib/src/material/animated_icons/data/close_menu.g.dart
+++ b/framework/lib/src/material/animated_icons/data/close_menu.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$close_menu = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/ellipsis_search.g.dart b/framework/lib/src/material/animated_icons/data/ellipsis_search.g.dart
index ef8802d..9f539c6 100644
--- a/framework/lib/src/material/animated_icons/data/ellipsis_search.g.dart
+++ b/framework/lib/src/material/animated_icons/data/ellipsis_search.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$ellipsis_search = _AnimatedIconData(
Size(96.0, 96.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/event_add.g.dart b/framework/lib/src/material/animated_icons/data/event_add.g.dart
index 8a012f2..1b64f89 100644
--- a/framework/lib/src/material/animated_icons/data/event_add.g.dart
+++ b/framework/lib/src/material/animated_icons/data/event_add.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$event_add = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/home_menu.g.dart b/framework/lib/src/material/animated_icons/data/home_menu.g.dart
index 447a370..559c453 100644
--- a/framework/lib/src/material/animated_icons/data/home_menu.g.dart
+++ b/framework/lib/src/material/animated_icons/data/home_menu.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$home_menu = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/list_view.g.dart b/framework/lib/src/material/animated_icons/data/list_view.g.dart
index 35814a0..25d544c 100644
--- a/framework/lib/src/material/animated_icons/data/list_view.g.dart
+++ b/framework/lib/src/material/animated_icons/data/list_view.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$list_view = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/menu_arrow.g.dart b/framework/lib/src/material/animated_icons/data/menu_arrow.g.dart
index 78613e3..4717a93 100644
--- a/framework/lib/src/material/animated_icons/data/menu_arrow.g.dart
+++ b/framework/lib/src/material/animated_icons/data/menu_arrow.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$menu_arrow = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/menu_close.g.dart b/framework/lib/src/material/animated_icons/data/menu_close.g.dart
index 43e194f..27d3408 100644
--- a/framework/lib/src/material/animated_icons/data/menu_close.g.dart
+++ b/framework/lib/src/material/animated_icons/data/menu_close.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$menu_close = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/menu_home.g.dart b/framework/lib/src/material/animated_icons/data/menu_home.g.dart
index dfdcc79..53c3ec7 100644
--- a/framework/lib/src/material/animated_icons/data/menu_home.g.dart
+++ b/framework/lib/src/material/animated_icons/data/menu_home.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$menu_home = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/pause_play.g.dart b/framework/lib/src/material/animated_icons/data/pause_play.g.dart
index b9d117e..2d9d926 100644
--- a/framework/lib/src/material/animated_icons/data/pause_play.g.dart
+++ b/framework/lib/src/material/animated_icons/data/pause_play.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$pause_play = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/play_pause.g.dart b/framework/lib/src/material/animated_icons/data/play_pause.g.dart
index a850aa3..71fa193 100644
--- a/framework/lib/src/material/animated_icons/data/play_pause.g.dart
+++ b/framework/lib/src/material/animated_icons/data/play_pause.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$play_pause = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/search_ellipsis.g.dart b/framework/lib/src/material/animated_icons/data/search_ellipsis.g.dart
index 93ae867..6760e2a 100644
--- a/framework/lib/src/material/animated_icons/data/search_ellipsis.g.dart
+++ b/framework/lib/src/material/animated_icons/data/search_ellipsis.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$search_ellipsis = _AnimatedIconData(
Size(96.0, 96.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/animated_icons/data/view_list.g.dart b/framework/lib/src/material/animated_icons/data/view_list.g.dart
index af9606b..65ec982 100644
--- a/framework/lib/src/material/animated_icons/data/view_list.g.dart
+++ b/framework/lib/src/material/animated_icons/data/view_list.g.dart
@@ -4,7 +4,7 @@
// AUTOGENERATED FILE DO NOT EDIT!
// This file was generated by vitool.
-part of material_animated_icons;
+part of material_animated_icons; // ignore: use_string_in_part_of_directives
const _AnimatedIconData _$view_list = _AnimatedIconData(
Size(48.0, 48.0),
<_PathFrames>[
diff --git a/framework/lib/src/material/app.dart b/framework/lib/src/material/app.dart
index 1defb57..fb82772 100644
--- a/framework/lib/src/material/app.dart
+++ b/framework/lib/src/material/app.dart
@@ -241,17 +241,13 @@
this.actions,
this.restorationScopeId,
this.scrollBehavior,
+ @Deprecated(
+ 'Remove this parameter as it is now ignored. '
+ 'MaterialApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
this.useInheritedMediaQuery = false,
- }) : assert(routes != null),
- assert(navigatorObservers != null),
- assert(title != null),
- assert(debugShowMaterialGrid != null),
- assert(showPerformanceOverlay != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- assert(showSemanticsDebugger != null),
- assert(debugShowCheckedModeBanner != null),
- routeInformationProvider = null,
+ }) : routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
backButtonDispatcher = null,
@@ -294,15 +290,13 @@
this.actions,
this.restorationScopeId,
this.scrollBehavior,
+ @Deprecated(
+ 'Remove this parameter as it is now ignored. '
+ 'MaterialApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
this.useInheritedMediaQuery = false,
}) : assert(routerDelegate != null || routerConfig != null),
- assert(title != null),
- assert(debugShowMaterialGrid != null),
- assert(showPerformanceOverlay != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- assert(showSemanticsDebugger != null),
- assert(debugShowCheckedModeBanner != null),
navigatorObservers = null,
navigatorKey = null,
onGenerateRoute = null,
@@ -749,6 +743,11 @@
final bool debugShowMaterialGrid;
/// {@macro flutter.widgets.widgetsApp.useInheritedMediaQuery}
+ @Deprecated(
+ 'This setting is now ignored. '
+ 'MaterialApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
final bool useInheritedMediaQuery;
@override
@@ -999,7 +998,6 @@
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
- useInheritedMediaQuery: widget.useInheritedMediaQuery,
);
}
@@ -1035,7 +1033,6 @@
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
- useInheritedMediaQuery: widget.useInheritedMediaQuery,
);
}
diff --git a/framework/lib/src/material/app_bar.dart b/framework/lib/src/material/app_bar.dart
index cce53af..884b05f 100644
--- a/framework/lib/src/material/app_bar.dart
+++ b/framework/lib/src/material/app_bar.dart
@@ -11,12 +11,14 @@
import 'app_bar_theme.dart';
import 'back_button.dart';
+import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'flexible_space_bar.dart';
import 'icon_button.dart';
+import 'icon_button_theme.dart';
import 'icons.dart';
import 'material.dart';
import 'material_localizations.dart';
@@ -127,7 +129,7 @@
///
/// If the app bar's [actions] contains [TextButton]s, they will not
/// be visible if their foreground (text) color is the same as the
-/// the app bar's background color.
+/// app bar's background color.
///
/// The default app bar [backgroundColor] is the overall theme's
/// [ColorScheme.primary] if the overall theme's brightness is
@@ -199,11 +201,6 @@
this.brightness,
this.iconTheme,
this.actionsIconTheme,
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- this.textTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
@@ -220,12 +217,8 @@
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
- }) : assert(automaticallyImplyLeading != null),
- assert(elevation == null || elevation >= 0.0),
- assert(notificationPredicate != null),
- assert(primary != null),
- assert(toolbarOpacity != null),
- assert(bottomOpacity != null),
+ this.forceMaterialTransparency = false,
+ }) : assert(elevation == null || elevation >= 0.0),
preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height);
/// Used by [Scaffold] to compute its [AppBar]'s overall height. The returned value is
@@ -593,7 +586,7 @@
/// See also:
///
/// * [actionsIconTheme], which defines the appearance of icons in
- /// in the [actions] list.
+ /// the [actions] list.
final IconThemeData? iconTheme;
/// {@template flutter.material.appbar.actionsIconTheme}
@@ -614,23 +607,6 @@
/// * [iconTheme], which defines the appearance of all of the toolbar icons.
final IconThemeData? actionsIconTheme;
- /// {@template flutter.material.appbar.textTheme}
- /// This property is deprecated, please use [toolbarTextStyle] and
- /// [titleTextStyle] instead.
- ///
- /// The typographic styles to use for text in the app bar. Typically this is
- /// set along with [backgroundColor], [iconTheme].
- ///
- /// If this property is null, then [AppBarTheme.textTheme] of
- /// [ThemeData.appBarTheme] is used. If that is also null, then
- /// [ThemeData.primaryTextTheme] is used.
- /// {@endtemplate}
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- final TextTheme? textTheme;
-
/// {@template flutter.material.appbar.primary}
/// Whether this app bar is being displayed at the top of the screen.
///
@@ -717,7 +693,7 @@
///
/// If true, preserves the original defaults for the [backgroundColor],
/// [iconTheme], [actionsIconTheme] properties, and the original use of
- /// the [textTheme] and [brightness] properties.
+ /// the [brightness] property.
///
/// If this property is null, then [AppBarTheme.backwardsCompatibility] of
/// [ThemeData.appBarTheme] is used. If that is also null, the default
@@ -748,7 +724,7 @@
///
/// * [titleTextStyle], which overrides the default text style for the [title].
/// * [DefaultTextStyle], which overrides the default text style for all of the
- /// the widgets in a subtree.
+ /// widgets in a subtree.
final TextStyle? toolbarTextStyle;
/// {@template flutter.material.appbar.titleTextStyle}
@@ -766,7 +742,7 @@
/// [title], [leading], and [actions] widgets, also known as the
/// AppBar's "toolbar".
/// * [DefaultTextStyle], which overrides the default text style for all of the
- /// the widgets in a subtree.
+ /// widgets in a subtree.
final TextStyle? titleTextStyle;
/// {@template flutter.material.appbar.systemOverlayStyle}
@@ -789,9 +765,23 @@
/// * [SystemChrome.setSystemUIOverlayStyle]
final SystemUiOverlayStyle? systemOverlayStyle;
+ /// {@template flutter.material.appbar.forceMaterialTransparency}
+ /// Forces the AppBar's Material widget type to be [MaterialType.transparency]
+ /// (instead of Material's default type).
+ ///
+ /// This will remove the visual display of [backgroundColor] and [elevation],
+ /// and affect other characteristics of the AppBar's Material widget.
+ ///
+ /// Provided for cases where the app bar is to be transparent, and gestures
+ /// must pass through the app bar to widgets beneath the app bar (i.e. with
+ /// [Scaffold.extendBodyBehindAppBar] set to true).
+ ///
+ /// Defaults to false.
+ /// {@endtemplate}
+ final bool forceMaterialTransparency;
+
bool _getEffectiveCenterTitle(ThemeData theme) {
bool platformCenter() {
- assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@@ -893,6 +883,7 @@
assert(!widget.primary || debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = Theme.of(context);
+ final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context);
final AppBarTheme appBarTheme = AppBarTheme.of(context);
final AppBarTheme defaults = theme.useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context);
final ScaffoldState? scaffold = Scaffold.maybeOf(context);
@@ -955,16 +946,16 @@
?? overallIconTheme;
TextStyle? toolbarTextStyle = backwardsCompatibility
- ? widget.textTheme?.bodyMedium
- ?? appBarTheme.textTheme?.bodyMedium
+ ? widget.toolbarTextStyle
+ ?? appBarTheme.toolbarTextStyle
?? theme.primaryTextTheme.bodyMedium
: widget.toolbarTextStyle
?? appBarTheme.toolbarTextStyle
?? defaults.toolbarTextStyle?.copyWith(color: foregroundColor);
TextStyle? titleTextStyle = backwardsCompatibility
- ? widget.textTheme?.titleLarge
- ?? appBarTheme.textTheme?.titleLarge
+ ? widget.titleTextStyle
+ ?? appBarTheme.titleTextStyle
?? theme.primaryTextTheme.titleLarge
: widget.titleTextStyle
?? appBarTheme.titleTextStyle
@@ -1003,13 +994,42 @@
}
}
if (leading != null) {
- // Based on the Material Design 3 specs, the leading IconButton should have
- // a size of 48x48, and a highlight size of 40x40. Users can also put other
- // type of widgets on leading with the original config.
if (theme.useMaterial3) {
- leading = ConstrainedBox(
+ final IconButtonThemeData effectiveIconButtonTheme;
+
+ // This comparison is to check if there is a custom [overallIconTheme]. If true, it means that no
+ // custom [overallIconTheme] is provided, so [iconButtonTheme] is applied. Otherwise, we generate
+ // a new [IconButtonThemeData] based on the values from [overallIconTheme]. If [iconButtonTheme] only
+ // has null values, the default [overallIconTheme] will be applied below by [IconTheme.merge]
+ if (overallIconTheme == defaults.iconTheme) {
+ effectiveIconButtonTheme = iconButtonTheme;
+ } else {
+ // The [IconButton.styleFrom] method is used to generate a correct [overlayColor] based on the [foregroundColor].
+ final ButtonStyle leadingIconButtonStyle = IconButton.styleFrom(
+ foregroundColor: overallIconTheme.color,
+ iconSize: overallIconTheme.size,
+ );
+
+ effectiveIconButtonTheme = IconButtonThemeData(
+ style: iconButtonTheme.style?.copyWith(
+ foregroundColor: leadingIconButtonStyle.foregroundColor,
+ overlayColor: leadingIconButtonStyle.overlayColor,
+ iconSize: leadingIconButtonStyle.iconSize,
+ )
+ );
+ }
+
+ leading = IconButtonTheme(
+ data: effectiveIconButtonTheme,
+ child: leading is IconButton ? Center(child: leading) : leading,
+ );
+
+ // Based on the Material Design 3 specs, the leading IconButton should have
+ // a size of 48x48, and a highlight size of 40x40. Users can also put other
+ // type of widgets on leading with the original config.
+ leading = ConstrainedBox(
constraints: BoxConstraints.tightFor(width: widget.leadingWidth ?? _kLeadingWidth),
- child: leading is IconButton ? Center(child: leading) : leading,
+ child: leading,
);
} else {
leading = ConstrainedBox(
@@ -1085,9 +1105,30 @@
// Allow the trailing actions to have their own theme if necessary.
if (actions != null) {
- actions = IconTheme.merge(
- data: actionsIconTheme,
- child: actions,
+ final IconButtonThemeData effectiveActionsIconButtonTheme;
+ if (actionsIconTheme == defaults.actionsIconTheme) {
+ effectiveActionsIconButtonTheme = iconButtonTheme;
+ } else {
+ final ButtonStyle actionsIconButtonStyle = IconButton.styleFrom(
+ foregroundColor: actionsIconTheme.color,
+ iconSize: actionsIconTheme.size,
+ );
+
+ effectiveActionsIconButtonTheme = IconButtonThemeData(
+ style: iconButtonTheme.style?.copyWith(
+ foregroundColor: actionsIconButtonStyle.foregroundColor,
+ overlayColor: actionsIconButtonStyle.overlayColor,
+ iconSize: actionsIconButtonStyle.iconSize,
+ )
+ );
+ }
+
+ actions = IconButtonTheme(
+ data: effectiveActionsIconButtonTheme,
+ child: IconTheme.merge(
+ data: actionsIconTheme,
+ child: actions,
+ ),
);
}
@@ -1193,6 +1234,9 @@
child: Material(
color: backgroundColor,
elevation: effectiveElevation,
+ type: widget.forceMaterialTransparency
+ ? MaterialType.transparency
+ : MaterialType.canvas,
shadowColor: widget.shadowColor
?? appBarTheme.shadowColor
?? defaults.shadowColor,
@@ -1228,7 +1272,6 @@
required this.brightness,
required this.iconTheme,
required this.actionsIconTheme,
- required this.textTheme,
required this.primary,
required this.centerTitle,
required this.excludeHeaderSemantics,
@@ -1249,11 +1292,8 @@
required this.toolbarTextStyle,
required this.titleTextStyle,
required this.systemOverlayStyle,
+ required this.forceMaterialTransparency,
}) : assert(primary || topPadding == 0.0),
- assert(
- !floating || (snapConfiguration == null && showOnScreenConfiguration == null) || vsync != null,
- 'vsync cannot be null when snapConfiguration or showOnScreenConfiguration is not null, and floating is true',
- ),
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
final Widget? leading;
@@ -1272,7 +1312,6 @@
final Brightness? brightness;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
- final TextTheme? textTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
@@ -1290,6 +1329,7 @@
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final double _bottomHeight;
+ final bool forceMaterialTransparency;
@override
double get minExtent => collapsedHeight;
@@ -1348,7 +1388,6 @@
brightness: brightness,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
- textTheme: textTheme,
primary: primary,
centerTitle: centerTitle,
excludeHeaderSemantics: excludeHeaderSemantics,
@@ -1362,6 +1401,7 @@
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
+ forceMaterialTransparency: forceMaterialTransparency,
),
);
return appBar;
@@ -1383,7 +1423,6 @@
|| brightness != oldDelegate.brightness
|| iconTheme != oldDelegate.iconTheme
|| actionsIconTheme != oldDelegate.actionsIconTheme
- || textTheme != oldDelegate.textTheme
|| primary != oldDelegate.primary
|| centerTitle != oldDelegate.centerTitle
|| titleSpacing != oldDelegate.titleSpacing
@@ -1401,7 +1440,8 @@
|| backwardsCompatibility != oldDelegate.backwardsCompatibility
|| toolbarTextStyle != oldDelegate.toolbarTextStyle
|| titleTextStyle != oldDelegate.titleTextStyle
- || systemOverlayStyle != oldDelegate.systemOverlayStyle;
+ || systemOverlayStyle != oldDelegate.systemOverlayStyle
+ || forceMaterialTransparency != oldDelegate.forceMaterialTransparency;
}
@override
@@ -1522,11 +1562,6 @@
this.brightness,
this.iconTheme,
this.actionsIconTheme,
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- this.textTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
@@ -1550,15 +1585,8 @@
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
- }) : assert(automaticallyImplyLeading != null),
- assert(forceElevated != null),
- assert(primary != null),
- assert(floating != null),
- assert(pinned != null),
- assert(snap != null),
- assert(stretch != null),
- assert(toolbarHeight != null),
- assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
+ this.forceMaterialTransparency = false,
+ }) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0),
assert(collapsedHeight == null || collapsedHeight >= toolbarHeight, 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].');
@@ -1628,6 +1656,7 @@
actions: actions,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
title: title,
+ foregroundColor: foregroundColor,
variant: _ScrollUnderFlexibleVariant.medium,
centerCollapsedTitle: centerTitle,
primary: primary,
@@ -1729,6 +1758,7 @@
actions: actions,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
title: title,
+ foregroundColor: foregroundColor,
variant: _ScrollUnderFlexibleVariant.large,
centerCollapsedTitle: centerTitle,
primary: primary,
@@ -1854,15 +1884,6 @@
/// This property is used to configure an [AppBar].
final IconThemeData? actionsIconTheme;
- /// {@macro flutter.material.appbar.textTheme}
- ///
- /// This property is used to configure an [AppBar].
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- final TextTheme? textTheme;
-
/// {@macro flutter.material.appbar.primary}
///
/// This property is used to configure an [AppBar].
@@ -2038,6 +2059,11 @@
/// This property is used to configure an [AppBar].
final SystemUiOverlayStyle? systemOverlayStyle;
+ /// {@macro flutter.material.appbar.forceMaterialTransparency}
+ ///
+ /// This property is used to configure an [AppBar].
+ final bool forceMaterialTransparency;
+
@override
State<SliverAppBar> createState() => _SliverAppBarState();
}
@@ -2097,7 +2123,7 @@
Widget build(BuildContext context) {
assert(!widget.primary || debugCheckHasMediaQuery(context));
final double bottomHeight = widget.bottom?.preferredSize.height ?? 0.0;
- final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
+ final double topPadding = widget.primary ? MediaQuery.paddingOf(context).top : 0.0;
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding
: (widget.collapsedHeight ?? widget.toolbarHeight) + bottomHeight + topPadding;
@@ -2126,7 +2152,6 @@
brightness: widget.brightness,
iconTheme: widget.iconTheme,
actionsIconTheme: widget.actionsIconTheme,
- textTheme: widget.textTheme,
primary: widget.primary,
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
@@ -2146,6 +2171,7 @@
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
+ forceMaterialTransparency: widget.forceMaterialTransparency,
),
),
);
@@ -2156,7 +2182,7 @@
// center it within its (NavigationToolbar) parent, and allow the
// parent to constrain the title's actual height.
class _AppBarTitleBox extends SingleChildRenderObjectWidget {
- const _AppBarTitleBox({ required Widget super.child }) : assert(child != null);
+ const _AppBarTitleBox({ required Widget super.child });
@override
_RenderAppBarTitleBox createRenderObject(BuildContext context) {
@@ -2197,12 +2223,14 @@
class _ScrollUnderFlexibleSpace extends StatelessWidget {
const _ScrollUnderFlexibleSpace({
this.title,
+ this.foregroundColor,
required this.variant,
this.centerCollapsedTitle,
this.primary = true,
});
final Widget? title;
+ final Color? foregroundColor;
final _ScrollUnderFlexibleVariant variant;
final bool? centerCollapsedTitle;
final bool primary;
@@ -2210,8 +2238,9 @@
@override
Widget build(BuildContext context) {
late final ThemeData theme = Theme.of(context);
+ late final AppBarTheme appBarTheme = AppBarTheme.of(context);
final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
- final double topPadding = primary ? MediaQuery.of(context).viewPadding.top : 0;
+ final double topPadding = primary ? MediaQuery.viewPaddingOf(context).top : 0;
final double collapsedHeight = settings.minExtent - topPadding;
final double scrollUnderHeight = settings.maxExtent - settings.minExtent;
final _ScrollUnderFlexibleConfig config;
@@ -2229,13 +2258,13 @@
if (title != null) {
collapsedTitle = config.collapsedTextStyle != null
? DefaultTextStyle(
- style: config.collapsedTextStyle!,
+ style: config.collapsedTextStyle!.copyWith(color: foregroundColor ?? appBarTheme.foregroundColor),
child: title!,
)
: title;
expandedTitle = config.expandedTextStyle != null
? DefaultTextStyle(
- style: config.expandedTextStyle!,
+ style: config.expandedTextStyle!.copyWith(color: foregroundColor ?? appBarTheme.foregroundColor),
child: title!,
)
: title;
@@ -2244,7 +2273,6 @@
late final bool centerTitle;
{
bool platformCenter() {
- assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@@ -2256,9 +2284,7 @@
return true;
}
}
- centerTitle = centerCollapsedTitle
- ?? theme.appBarTheme.centerTitle
- ?? platformCenter();
+ centerTitle = centerCollapsedTitle ?? appBarTheme.centerTitle ?? platformCenter();
}
final bool isCollapsed = settings.isScrolledUnder ?? false;
@@ -2277,7 +2303,7 @@
alignment: centerTitle
? Alignment.center
: AlignmentDirectional.centerStart,
- child: collapsedTitle
+ child: collapsedTitle,
),
),
),
@@ -2346,7 +2372,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _AppBarDefaultsM3 extends AppBarTheme {
_AppBarDefaultsM3(this.context)
diff --git a/framework/lib/src/material/app_bar_theme.dart b/framework/lib/src/material/app_bar_theme.dart
index bb4734c..0229bcd 100644
--- a/framework/lib/src/material/app_bar_theme.dart
+++ b/framework/lib/src/material/app_bar_theme.dart
@@ -8,7 +8,6 @@
import 'package:flute/services.dart';
import 'package:flute/widgets.dart';
-import 'text_theme.dart';
import 'theme.dart';
/// Overrides the default values of visual properties for descendant
@@ -43,11 +42,6 @@
this.shape,
this.iconTheme,
this.actionsIconTheme,
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- this.textTheme,
this.centerTitle,
this.titleSpacing,
this.toolbarHeight,
@@ -161,24 +155,6 @@
/// [AppBar.foregroundColor] in all descendant [AppBar] widgets.
final IconThemeData? actionsIconTheme;
- /// This property is deprecated, please use [toolbarTextStyle] and
- /// [titleTextStyle] instead.
- ///
- /// Overrides the default value of the obsolete [AppBar.textTheme]
- /// property in all descendant [AppBar] widgets.
- ///
- /// See also:
- ///
- /// * [toolbarTextStyle], which overrides the default value of
- /// [AppBar.toolbarTextStyle in all descendant [AppBar] widgets.
- /// * [titleTextStyle], which overrides the default value of
- /// [AppBar.titleTextStyle in all descendant [AppBar] widgets.
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- final TextTheme? textTheme;
-
/// Overrides the default value of [AppBar.centerTitle]
/// property in all descendant [AppBar] widgets.
final bool? centerTitle;
@@ -252,11 +228,6 @@
Color? surfaceTintColor,
ShapeBorder? shape,
IconThemeData? iconTheme,
- @Deprecated(
- 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. '
- 'This feature was deprecated after v2.4.0-0.0.pre.',
- )
- TextTheme? textTheme,
bool? centerTitle,
double? titleSpacing,
double? toolbarHeight,
@@ -284,7 +255,6 @@
shape: shape ?? this.shape,
iconTheme: iconTheme ?? this.iconTheme,
actionsIconTheme: actionsIconTheme ?? this.actionsIconTheme,
- textTheme: textTheme ?? this.textTheme,
centerTitle: centerTitle ?? this.centerTitle,
titleSpacing: titleSpacing ?? this.titleSpacing,
toolbarHeight: toolbarHeight ?? this.toolbarHeight,
@@ -306,7 +276,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static AppBarTheme lerp(AppBarTheme? a, AppBarTheme? b, double t) {
- assert(t != null);
return AppBarTheme(
brightness: t < 0.5 ? a?.brightness : b?.brightness,
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
@@ -318,7 +287,6 @@
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
iconTheme: IconThemeData.lerp(a?.iconTheme, b?.iconTheme, t),
actionsIconTheme: IconThemeData.lerp(a?.actionsIconTheme, b?.actionsIconTheme, t),
- textTheme: TextTheme.lerp(a?.textTheme, b?.textTheme, t),
centerTitle: t < 0.5 ? a?.centerTitle : b?.centerTitle,
titleSpacing: lerpDouble(a?.titleSpacing, b?.titleSpacing, t),
toolbarHeight: lerpDouble(a?.toolbarHeight, b?.toolbarHeight, t),
@@ -341,7 +309,6 @@
shape,
iconTheme,
actionsIconTheme,
- textTheme,
centerTitle,
titleSpacing,
toolbarHeight,
@@ -370,7 +337,6 @@
&& other.shape == shape
&& other.iconTheme == iconTheme
&& other.actionsIconTheme == actionsIconTheme
- && other.textTheme == textTheme
&& other.centerTitle == centerTitle
&& other.titleSpacing == titleSpacing
&& other.toolbarHeight == toolbarHeight
@@ -393,7 +359,6 @@
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<IconThemeData>('iconTheme', iconTheme, defaultValue: null));
properties.add(DiagnosticsProperty<IconThemeData>('actionsIconTheme', actionsIconTheme, defaultValue: null));
- properties.add(DiagnosticsProperty<TextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('centerTitle', centerTitle, defaultValue: null));
properties.add(DiagnosticsProperty<double>('titleSpacing', titleSpacing, defaultValue: null));
properties.add(DiagnosticsProperty<double>('toolbarHeight', toolbarHeight, defaultValue: null));
diff --git a/framework/lib/src/material/autocomplete.dart b/framework/lib/src/material/autocomplete.dart
index 50c7112..2e465cf 100644
--- a/framework/lib/src/material/autocomplete.dart
+++ b/framework/lib/src/material/autocomplete.dart
@@ -43,8 +43,7 @@
this.optionsMaxHeight = 200.0,
this.optionsViewBuilder,
this.initialValue,
- }) : assert(displayStringForOption != null),
- assert(optionsBuilder != null);
+ });
/// {@macro flutter.widgets.RawAutocomplete.displayStringForOption}
final AutocompleteOptionToString<T> displayStringForOption;
diff --git a/framework/lib/src/material/back_button.dart b/framework/lib/src/material/back_button.dart
index 04cffca..3e88494 100644
--- a/framework/lib/src/material/back_button.dart
+++ b/framework/lib/src/material/back_button.dart
@@ -2,6 +2,7 @@
// 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';
@@ -27,22 +28,39 @@
/// the current platform (as obtained from the [Theme]).
const BackButtonIcon({ super.key });
- /// Returns the appropriate "back" icon for the given `platform`.
- static IconData _getIconData(TargetPlatform platform) {
- switch (platform) {
+ @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:
- return Icons.arrow_back;
+ data = Icons.arrow_back;
+ break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
- return Icons.arrow_back_ios;
+ 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;
+ }
- @override
- Widget build(BuildContext context) => Icon(_getIconData(Theme.of(context).platform));
+ return Icon(data, semanticLabel: semanticsLabel);
+ }
}
/// A Material Design back button.
@@ -149,8 +167,23 @@
@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: const Icon(Icons.close),
+ icon: Icon(Icons.close, semanticLabel: semanticsLabel),
color: color,
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () {
diff --git a/framework/lib/src/material/badge.dart b/framework/lib/src/material/badge.dart
index ffa202c..2d8d65d 100644
--- a/framework/lib/src/material/badge.dart
+++ b/framework/lib/src/material/badge.dart
@@ -193,7 +193,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _BadgeDefaultsM3 extends BadgeThemeData {
_BadgeDefaultsM3(this.context) : super(
diff --git a/framework/lib/src/material/badge_theme.dart b/framework/lib/src/material/badge_theme.dart
index 29ee237..1b0298a 100644
--- a/framework/lib/src/material/badge_theme.dart
+++ b/framework/lib/src/material/badge_theme.dart
@@ -153,7 +153,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the default color and size overrides for descendant [Badge] widgets.
final BadgeThemeData data;
diff --git a/framework/lib/src/material/banner.dart b/framework/lib/src/material/banner.dart
index 14617bb..45c19bf 100644
--- a/framework/lib/src/material/banner.dart
+++ b/framework/lib/src/material/banner.dart
@@ -106,15 +106,13 @@
this.shadowColor,
this.dividerColor,
this.padding,
+ this.margin,
this.leadingPadding,
this.forceActionsBelow = false,
this.overflowAlignment = OverflowBarAlignment.end,
this.animation,
this.onVisible,
- }) : assert(elevation == null || elevation >= 0.0),
- assert(content != null),
- assert(actions != null),
- assert(forceActionsBelow != null);
+ }) : assert(elevation == null || elevation >= 0.0);
/// The content of the [MaterialBanner].
///
@@ -188,6 +186,12 @@
/// `EdgeInsetsDirectional.only(start: 16.0, top: 2.0)`.
final EdgeInsetsGeometry? padding;
+ /// Empty space to surround the [MaterialBanner].
+ ///
+ /// If the [margin] is null then this defaults to
+ /// 0 if the banner's [elevation] is 0, 10 otherwise.
+ final EdgeInsetsGeometry? margin;
+
/// The amount of space by which to inset the [leading] widget.
///
/// This defaults to `EdgeInsetsDirectional.only(end: 16.0)`.
@@ -240,6 +244,7 @@
leading: leading,
backgroundColor: backgroundColor,
padding: padding,
+ margin: margin,
leadingPadding: leadingPadding,
forceActionsBelow: forceActionsBelow,
overflowAlignment: overflowAlignment,
@@ -293,7 +298,7 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQueryData = MediaQuery.of(context);
+ final bool accessibleNavigation = MediaQuery.accessibleNavigationOf(context);
assert(widget.actions.isNotEmpty);
@@ -321,6 +326,7 @@
);
final double elevation = widget.elevation ?? bannerTheme.elevation ?? 0.0;
+ final EdgeInsetsGeometry margin = widget.margin ?? EdgeInsets.only(bottom: elevation > 0 ? 10.0 : 0.0);
final Color backgroundColor = widget.backgroundColor
?? bannerTheme.backgroundColor
?? defaults.backgroundColor!;
@@ -337,7 +343,7 @@
?? defaults.contentTextStyle;
Widget materialBanner = Container(
- margin: EdgeInsets.only(bottom: elevation > 0 ? 10.0 : 0.0),
+ margin: margin,
child: Material(
elevation: elevation,
color: backgroundColor,
@@ -399,7 +405,7 @@
onDismiss: () {
ScaffoldMessenger.of(context).removeCurrentMaterialBanner(reason: MaterialBannerClosedReason.dismiss);
},
- child: mediaQueryData.accessibleNavigation
+ child: accessibleNavigation
? materialBanner
: SlideTransition(
position: slideOutAnimation,
@@ -408,7 +414,7 @@
);
final Widget materialBannerTransition;
- if (mediaQueryData.accessibleNavigation) {
+ if (accessibleNavigation) {
materialBannerTransition = materialBanner;
} else {
materialBannerTransition = AnimatedBuilder(
@@ -453,7 +459,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _BannerDefaultsM3 extends MaterialBannerThemeData {
const _BannerDefaultsM3(this.context)
diff --git a/framework/lib/src/material/banner_theme.dart b/framework/lib/src/material/banner_theme.dart
index d52edd6..0083340 100644
--- a/framework/lib/src/material/banner_theme.dart
+++ b/framework/lib/src/material/banner_theme.dart
@@ -101,7 +101,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static MaterialBannerThemeData lerp(MaterialBannerThemeData? a, MaterialBannerThemeData? b, double t) {
- assert(t != null);
return MaterialBannerThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
diff --git a/framework/lib/src/material/bottom_app_bar.dart b/framework/lib/src/material/bottom_app_bar.dart
index d37e99d..41e83cb 100644
--- a/framework/lib/src/material/bottom_app_bar.dart
+++ b/framework/lib/src/material/bottom_app_bar.dart
@@ -72,9 +72,7 @@
this.padding,
this.surfaceTintColor,
this.height,
- }) : assert(elevation == null || elevation >= 0.0),
- assert(notchMargin != null),
- assert(clipBehavior != null);
+ }) : assert(elevation == null || elevation >= 0.0);
/// The widget below this widget in the tree.
///
@@ -196,10 +194,9 @@
key: materialKey,
type: isMaterial3 ? MaterialType.canvas : MaterialType.transparency,
elevation: elevation,
+ color: isMaterial3 ? effectiveColor : null,
surfaceTintColor: surfaceTintColor,
- child: child == null
- ? null
- : SafeArea(child: child),
+ child: SafeArea(child: child),
),
),
);
@@ -212,10 +209,7 @@
required this.shape,
required this.materialKey,
required this.notchMargin,
- }) : assert(geometry != null),
- assert(shape != null),
- assert(notchMargin != null),
- super(reclip: geometry);
+ }) : super(reclip: geometry);
final ValueListenable<ScaffoldGeometry> geometry;
final NotchedShape shape;
@@ -275,7 +269,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _BottomAppBarDefaultsM3 extends BottomAppBarTheme {
const _BottomAppBarDefaultsM3(this.context)
diff --git a/framework/lib/src/material/bottom_app_bar_theme.dart b/framework/lib/src/material/bottom_app_bar_theme.dart
index ff45376..e59c538 100644
--- a/framework/lib/src/material/bottom_app_bar_theme.dart
+++ b/framework/lib/src/material/bottom_app_bar_theme.dart
@@ -94,7 +94,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BottomAppBarTheme lerp(BottomAppBarTheme? a, BottomAppBarTheme? b, double t) {
- assert(t != null);
return BottomAppBarTheme(
color: Color.lerp(a?.color, b?.color, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
diff --git a/framework/lib/src/material/bottom_navigation_bar.dart b/framework/lib/src/material/bottom_navigation_bar.dart
index 46990f9..8895478 100644
--- a/framework/lib/src/material/bottom_navigation_bar.dart
+++ b/framework/lib/src/material/bottom_navigation_bar.dart
@@ -199,21 +199,20 @@
this.enableFeedback,
this.landscapeLayout,
this.useLegacyColorScheme = true,
- }) : assert(items != null),
- assert(items.length >= 2),
+ }) : assert(items.length >= 2),
assert(
items.every((BottomNavigationBarItem item) => item.label != null),
'Every item must have a non-null label',
),
assert(0 <= currentIndex && currentIndex < items.length),
assert(elevation == null || elevation >= 0.0),
- assert(iconSize != null && iconSize >= 0.0),
+ assert(iconSize >= 0.0),
assert(
selectedItemColor == null || fixedColor == null,
'Either selectedItemColor or fixedColor can be specified, but not both',
),
- assert(selectedFontSize != null && selectedFontSize >= 0.0),
- assert(unselectedFontSize != null && unselectedFontSize >= 0.0),
+ assert(selectedFontSize >= 0.0),
+ assert(unselectedFontSize >= 0.0),
selectedItemColor = selectedItemColor ?? fixedColor;
/// Defines the appearance of the button items that are arrayed within the
@@ -421,13 +420,7 @@
required this.mouseCursor,
required this.enableFeedback,
required this.layout,
- }) : assert(type != null),
- assert(item != null),
- assert(animation != null),
- assert(selected != null),
- assert(selectedLabelStyle != null),
- assert(unselectedLabelStyle != null),
- assert(mouseCursor != null);
+ });
final BottomNavigationBarType type;
final BottomNavigationBarItem item;
@@ -607,8 +600,7 @@
@override
Widget build(BuildContext context) {
- final MediaQueryData data = MediaQuery.of(context);
- if (data.orientation == Orientation.landscape && layout == BottomNavigationBarLandscapeLayout.linear) {
+ if (MediaQuery.orientationOf(context) == Orientation.landscape && layout == BottomNavigationBarLandscapeLayout.linear) {
return Align(
heightFactor: 1,
child: Row(
@@ -634,8 +626,7 @@
required this.item,
required this.selectedIconTheme,
required this.unselectedIconTheme,
- }) : assert(selected != null),
- assert(item != null);
+ });
final ColorTween colorTween;
final Animation<double> animation;
@@ -678,13 +669,7 @@
required this.unselectedLabelStyle,
required this.showSelectedLabels,
required this.showUnselectedLabels,
- }) : assert(colorTween != null),
- assert(animation != null),
- assert(item != null),
- assert(selectedLabelStyle != null),
- assert(unselectedLabelStyle != null),
- assert(showSelectedLabels != null),
- assert(showUnselectedLabels != null);
+ });
final ColorTween colorTween;
final Animation<double> animation;
@@ -935,7 +920,6 @@
List<Widget> _createTiles(BottomNavigationBarLandscapeLayout layout) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- assert(localizations != null);
final ThemeData themeData = Theme.of(context);
final BottomNavigationBarThemeData bottomTheme = BottomNavigationBarTheme.of(context);
@@ -1114,7 +1098,7 @@
final BottomNavigationBarLandscapeLayout layout = widget.landscapeLayout
?? bottomTheme.landscapeLayout
?? BottomNavigationBarLandscapeLayout.spread;
- final double additionalBottomPadding = MediaQuery.of(context).viewPadding.bottom;
+ final double additionalBottomPadding = MediaQuery.viewPaddingOf(context).bottom;
Color? backgroundColor;
switch (_effectiveType) {
@@ -1180,14 +1164,13 @@
@override
Widget build(BuildContext context) {
- final MediaQueryData data = MediaQuery.of(context);
Widget alignedChild = child;
- if (data.orientation == Orientation.landscape && layout == BottomNavigationBarLandscapeLayout.centered) {
+ if (MediaQuery.orientationOf(context) == Orientation.landscape && layout == BottomNavigationBarLandscapeLayout.centered) {
alignedChild = Align(
alignment: Alignment.bottomCenter,
heightFactor: 1,
child: SizedBox(
- width: data.size.height,
+ width: MediaQuery.sizeOf(context).height,
child: child,
),
);
@@ -1207,9 +1190,7 @@
required this.index,
required this.color,
required TickerProvider vsync,
- }) : assert(state != null),
- assert(index != null),
- assert(color != null) {
+ }) {
controller = AnimationController(
duration: kThemeAnimationDuration,
vsync: vsync,
@@ -1252,8 +1233,7 @@
_RadialPainter({
required this.circles,
required this.textDirection,
- }) : assert(circles != null),
- assert(textDirection != null);
+ });
final List<_Circle> circles;
final TextDirection textDirection;
diff --git a/framework/lib/src/material/bottom_navigation_bar_theme.dart b/framework/lib/src/material/bottom_navigation_bar_theme.dart
index e4e6ac9..7d150ef 100644
--- a/framework/lib/src/material/bottom_navigation_bar_theme.dart
+++ b/framework/lib/src/material/bottom_navigation_bar_theme.dart
@@ -174,7 +174,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BottomNavigationBarThemeData lerp(BottomNavigationBarThemeData? a, BottomNavigationBarThemeData? b, double t) {
- assert(t != null);
return BottomNavigationBarThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
@@ -280,7 +279,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties used for all descendant [BottomNavigationBar] widgets.
final BottomNavigationBarThemeData data;
diff --git a/framework/lib/src/material/bottom_sheet.dart b/framework/lib/src/material/bottom_sheet.dart
index 1ba592f..887d7e2 100644
--- a/framework/lib/src/material/bottom_sheet.dart
+++ b/framework/lib/src/material/bottom_sheet.dart
@@ -5,6 +5,7 @@
import 'package:engine/ui.dart' show lerpDouble;
import 'package:flute/foundation.dart';
+import 'package:flute/rendering.dart';
import 'package:flute/widgets.dart';
import 'bottom_sheet_theme.dart';
@@ -83,10 +84,7 @@
this.constraints,
required this.onClosing,
required this.builder,
- }) : assert(enableDrag != null),
- assert(onClosing != null),
- assert(builder != null),
- assert(elevation == null || elevation >= 0.0);
+ }) : assert(elevation == null || elevation >= 0.0);
/// The animation controller that controls the bottom sheet's entrance and
/// exit animations.
@@ -319,16 +317,130 @@
// See scaffold.dart
+typedef _SizeChangeCallback<Size> = void Function(Size);
-// MODAL BOTTOM SHEETS
-class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
- _ModalBottomSheetLayout(this.progress, this.isScrollControlled);
+class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
- final double progress;
+ const _BottomSheetLayoutWithSizeListener({
+ required this.animationValue,
+ required this.isScrollControlled,
+ required this.onChildSizeChanged,
+ super.child,
+ });
+
+ final double animationValue;
final bool isScrollControlled;
+ final _SizeChangeCallback<Size> onChildSizeChanged;
@override
- BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
+ _RenderBottomSheetLayoutWithSizeListener createRenderObject(BuildContext context) {
+ return _RenderBottomSheetLayoutWithSizeListener(
+ animationValue: animationValue,
+ isScrollControlled: isScrollControlled,
+ onChildSizeChanged: onChildSizeChanged,
+ );
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, _RenderBottomSheetLayoutWithSizeListener renderObject) {
+ renderObject.onChildSizeChanged = onChildSizeChanged;
+ renderObject.animationValue = animationValue;
+ renderObject.isScrollControlled = isScrollControlled;
+ }
+}
+
+class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox {
+ _RenderBottomSheetLayoutWithSizeListener({
+ RenderBox? child,
+ required _SizeChangeCallback<Size> onChildSizeChanged,
+ required double animationValue,
+ required bool isScrollControlled,
+ }) : _animationValue = animationValue,
+ _isScrollControlled = isScrollControlled,
+ _onChildSizeChanged = onChildSizeChanged,
+ super(child);
+
+ Size _lastSize = Size.zero;
+
+ _SizeChangeCallback<Size> get onChildSizeChanged => _onChildSizeChanged;
+ _SizeChangeCallback<Size> _onChildSizeChanged;
+ set onChildSizeChanged(_SizeChangeCallback<Size> newCallback) {
+ if (_onChildSizeChanged == newCallback) {
+ return;
+ }
+
+ _onChildSizeChanged = newCallback;
+ markNeedsLayout();
+ }
+
+ double get animationValue => _animationValue;
+ double _animationValue;
+ set animationValue(double newValue) {
+ if (_animationValue == newValue) {
+ return;
+ }
+
+ _animationValue = newValue;
+ markNeedsLayout();
+ }
+
+ bool get isScrollControlled => _isScrollControlled;
+ bool _isScrollControlled;
+ set isScrollControlled(bool newValue) {
+ if (_isScrollControlled == newValue) {
+ return;
+ }
+
+ _isScrollControlled = newValue;
+ markNeedsLayout();
+ }
+
+ Size _getSize(BoxConstraints constraints) {
+ return constraints.constrain(constraints.biggest);
+ }
+
+ @override
+ double computeMinIntrinsicWidth(double height) {
+ final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
+ if (width.isFinite) {
+ return width;
+ }
+ return 0.0;
+ }
+
+ @override
+ double computeMaxIntrinsicWidth(double height) {
+ final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
+ if (width.isFinite) {
+ return width;
+ }
+ return 0.0;
+ }
+
+ @override
+ double computeMinIntrinsicHeight(double width) {
+ final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
+ if (height.isFinite) {
+ return height;
+ }
+ return 0.0;
+ }
+
+ @override
+ double computeMaxIntrinsicHeight(double width) {
+ final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
+ if (height.isFinite) {
+ return height;
+ }
+ return 0.0;
+ }
+
+ @override
+ Size computeDryLayout(BoxConstraints constraints) {
+ return _getSize(constraints);
+ }
+
+ BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints(
minWidth: constraints.maxWidth,
maxWidth: constraints.maxWidth,
@@ -338,14 +450,26 @@
);
}
- @override
- Offset getPositionForChild(Size size, Size childSize) {
- return Offset(0.0, size.height - childSize.height * progress);
+ Offset _getPositionForChild(Size size, Size childSize) {
+ return Offset(0.0, size.height - childSize.height * animationValue);
}
@override
- bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
- return progress != oldDelegate.progress;
+ void performLayout() {
+ size = _getSize(constraints);
+ if (child != null) {
+ final BoxConstraints childConstraints = _getConstraintsForChild(constraints);
+ assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
+ child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
+ final BoxParentData childParentData = child!.parentData! as BoxParentData;
+ childParentData.offset = _getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child!.size);
+ final Size childSize = childConstraints.isTight ? childConstraints.smallest : child!.size;
+
+ if (_lastSize != childSize) {
+ _lastSize = childSize;
+ _onChildSizeChanged.call(_lastSize);
+ }
+ }
}
}
@@ -360,8 +484,7 @@
this.constraints,
this.isScrollControlled = false,
this.enableDrag = true,
- }) : assert(isScrollControlled != null),
- assert(enableDrag != null);
+ });
final ModalBottomSheetRoute<T> route;
final bool isScrollControlled;
@@ -392,6 +515,10 @@
}
}
+ EdgeInsets _getNewClipDetails(Size topLayerSize) {
+ return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height);
+ }
+
void handleDragStart(DragStartDetails details) {
// Allow the bottom sheet to track the user's finger accurately.
animationCurve = Curves.linear;
@@ -409,7 +536,6 @@
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final String routeLabel = _getRouteLabel(localizations);
@@ -436,7 +562,7 @@
// 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.accessibleNavigation ? 1.0 : widget.route.animation!.value,
+ MediaQuery.accessibleNavigationOf(context) ? 1.0 : widget.route.animation!.value,
);
return Semantics(
scopesRoute: true,
@@ -444,8 +570,14 @@
label: routeLabel,
explicitChildNodes: true,
child: ClipRect(
- child: CustomSingleChildLayout(
- delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
+ child: _BottomSheetLayoutWithSizeListener(
+ onChildSizeChanged: (Size size) {
+ widget.route._didChangeBarrierSemanticsClip(
+ _getNewClipDetails(size),
+ );
+ },
+ animationValue: animationValue,
+ isScrollControlled: widget.isScrollControlled,
child: child,
),
),
@@ -517,6 +649,7 @@
required this.builder,
this.capturedThemes,
this.barrierLabel,
+ this.barrierOnTapHint,
this.backgroundColor,
this.elevation,
this.shape,
@@ -530,9 +663,7 @@
this.transitionAnimationController,
this.anchorPoint,
this.useSafeArea = false,
- }) : assert(isScrollControlled != null),
- assert(isDismissible != null),
- assert(enableDrag != null);
+ });
/// A builder for the contents of the sheet.
///
@@ -647,6 +778,35 @@
/// Default is false.
final bool useSafeArea;
+ /// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
+ /// The semantic hint text that informs users what will happen if they
+ /// tap on the widget. Announced in the format of 'Double tap to ...'.
+ ///
+ /// If the field is null, the default hint will be used, which results in
+ /// announcement of 'Double tap to activate'.
+ /// {@endtemplate}
+ ///
+ /// See also:
+ ///
+ /// * [barrierDismissible], which controls the behavior of the barrier when
+ /// tapped.
+ /// * [ModalBarrier], which uses this field as onTapHint when it has an onTap action.
+ final String? barrierOnTapHint;
+
+ final ValueNotifier<EdgeInsets> _clipDetailsNotifier = ValueNotifier<EdgeInsets>(EdgeInsets.zero);
+
+ /// Updates the details regarding how the [SemanticsNode.rect] (focus) of
+ /// the barrier for this [ModalBottomSheetRoute] should be clipped.
+ ///
+ /// returns true if the clipDetails did change and false otherwise.
+ bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) {
+ if (_clipDetailsNotifier.value == newClipDetails) {
+ return false;
+ }
+ _clipDetailsNotifier.value = newClipDetails;
+ return true;
+ }
+
@override
Duration get transitionDuration => _bottomSheetEnterDuration;
@@ -711,6 +871,35 @@
return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
}
+
+ @override
+ Widget buildModalBarrier() {
+ if (barrierColor.alpha != 0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
+ assert(barrierColor != barrierColor.withOpacity(0.0));
+ final Animation<Color?> color = animation!.drive(
+ ColorTween(
+ begin: barrierColor.withOpacity(0.0),
+ end: barrierColor, // changedInternalState is called if barrierColor updates
+ ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
+ );
+ return AnimatedModalBarrier(
+ color: color,
+ dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
+ semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
+ barrierSemanticsDismissible: semanticsDismissible,
+ clipDetailsNotifier: _clipDetailsNotifier,
+ semanticsOnTapHint: barrierOnTapHint,
+ );
+ } else {
+ return ModalBarrier(
+ dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
+ semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
+ barrierSemanticsDismissible: semanticsDismissible,
+ clipDetailsNotifier: _clipDetailsNotifier,
+ semanticsOnTapHint: barrierOnTapHint,
+ );
+ }
+ }
}
// TODO(guidezpl): Look into making this public. A copy of this class is in
@@ -736,15 +925,14 @@
const _BottomSheetSuspendedCurve(
this.startingPoint, {
this.curve = Curves.easeOutCubic,
- }) : assert(startingPoint != null),
- assert(curve != null);
+ });
/// The progress value at which [curve] should begin.
- ///
- /// This defaults to [Curves.easeOutCubic].
final double startingPoint;
/// The curve to use when [startingPoint] is reached.
+ ///
+ /// This defaults to [Curves.easeOutCubic].
final Curve curve;
@override
@@ -835,21 +1023,17 @@
AnimationController? transitionAnimationController,
Offset? anchorPoint,
}) {
- assert(context != null);
- assert(builder != null);
- assert(isScrollControlled != null);
- assert(useRootNavigator != null);
- assert(isDismissible != null);
- assert(enableDrag != null);
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
+ final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return navigator.push(ModalBottomSheetRoute<T>(
builder: builder,
capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
isScrollControlled: isScrollControlled,
- barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
+ barrierLabel: localizations.scrimLabel,
+ barrierOnTapHint: localizations.scrimOnTapHint(localizations.bottomSheetLabel),
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
@@ -920,8 +1104,6 @@
bool? enableDrag,
AnimationController? transitionAnimationController,
}) {
- assert(context != null);
- assert(builder != null);
assert(debugCheckHasScaffold(context));
return Scaffold.of(context).showBottomSheet<T>(
@@ -945,7 +1127,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _BottomSheetDefaultsM3 extends BottomSheetThemeData {
const _BottomSheetDefaultsM3(this.context)
diff --git a/framework/lib/src/material/bottom_sheet_theme.dart b/framework/lib/src/material/bottom_sheet_theme.dart
index 1c52523..c0ebc22 100644
--- a/framework/lib/src/material/bottom_sheet_theme.dart
+++ b/framework/lib/src/material/bottom_sheet_theme.dart
@@ -118,7 +118,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BottomSheetThemeData? lerp(BottomSheetThemeData? a, BottomSheetThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/material/button.dart b/framework/lib/src/material/button.dart
index 41b6ff0..3ee3642 100644
--- a/framework/lib/src/material/button.dart
+++ b/framework/lib/src/material/button.dart
@@ -74,17 +74,11 @@
this.child,
this.enableFeedback = true,
}) : materialTapTargetSize = materialTapTargetSize ?? MaterialTapTargetSize.padded,
- assert(shape != null),
- assert(elevation != null && elevation >= 0.0),
- assert(focusElevation != null && focusElevation >= 0.0),
- assert(hoverElevation != null && hoverElevation >= 0.0),
- assert(highlightElevation != null && highlightElevation >= 0.0),
- assert(disabledElevation != null && disabledElevation >= 0.0),
- assert(padding != null),
- assert(constraints != null),
- assert(animationDuration != null),
- assert(clipBehavior != null),
- assert(autofocus != null);
+ assert(elevation >= 0.0),
+ assert(focusElevation >= 0.0),
+ assert(hoverElevation >= 0.0),
+ assert(highlightElevation >= 0.0),
+ assert(disabledElevation >= 0.0);
/// Called when the button is tapped or otherwise activated.
///
diff --git a/framework/lib/src/material/button_bar.dart b/framework/lib/src/material/button_bar.dart
index 4542083..069c285 100644
--- a/framework/lib/src/material/button_bar.dart
+++ b/framework/lib/src/material/button_bar.dart
@@ -233,7 +233,7 @@
class _ButtonBarRow extends Flex {
/// Creates a button bar that attempts to display in a row, but displays in
/// a column if there is insufficient horizontal space.
- _ButtonBarRow({
+ const _ButtonBarRow({
required super.children,
super.mainAxisSize,
super.mainAxisAlignment,
@@ -300,8 +300,7 @@
super.verticalDirection,
super.textBaseline,
this.overflowButtonSpacing,
- }) : assert(textDirection != null),
- assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0);
+ }) : assert(overflowButtonSpacing == null || overflowButtonSpacing >= 0);
bool _hasCheckedLayoutWidth = false;
double? overflowButtonSpacing;
diff --git a/framework/lib/src/material/button_bar_theme.dart b/framework/lib/src/material/button_bar_theme.dart
index 2efbcdd..20df2ae 100644
--- a/framework/lib/src/material/button_bar_theme.dart
+++ b/framework/lib/src/material/button_bar_theme.dart
@@ -146,7 +146,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static ButtonBarThemeData? lerp(ButtonBarThemeData? a, ButtonBarThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -240,7 +239,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties used for all descendant [ButtonBar] widgets.
final ButtonBarThemeData data;
diff --git a/framework/lib/src/material/button_style.dart b/framework/lib/src/material/button_style.dart
index b15cba5..18cf679 100644
--- a/framework/lib/src/material/button_style.dart
+++ b/framework/lib/src/material/button_style.dart
@@ -492,7 +492,6 @@
/// Linearly interpolate between two [ButtonStyle]s.
static ButtonStyle? lerp(ButtonStyle? a, ButtonStyle? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/material/button_style_button.dart b/framework/lib/src/material/button_style_button.dart
index 0d975eb..eafdde4 100644
--- a/framework/lib/src/material/button_style_button.dart
+++ b/framework/lib/src/material/button_style_button.dart
@@ -43,8 +43,7 @@
required this.clipBehavior,
this.statesController,
required this.child,
- }) : assert(autofocus != null),
- assert(clipBehavior != null);
+ });
/// Called when the button is tapped or otherwise activated.
///
@@ -175,10 +174,6 @@
EdgeInsetsGeometry geometry3x,
double textScaleFactor,
) {
- assert(geometry1x != null);
- assert(geometry2x != null);
- assert(geometry3x != null);
- assert(textScaleFactor != null);
if (textScaleFactor <= 1) {
return geometry1x;
@@ -260,7 +255,6 @@
final ButtonStyle? widgetStyle = widget.style;
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
- assert(defaultStyle != null);
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
final T? widgetValue = getProperty(widgetStyle);
diff --git a/framework/lib/src/material/button_theme.dart b/framework/lib/src/material/button_theme.dart
index dae7eaf..7e38bf1 100644
--- a/framework/lib/src/material/button_theme.dart
+++ b/framework/lib/src/material/button_theme.dart
@@ -87,11 +87,8 @@
ColorScheme? colorScheme,
MaterialTapTargetSize? materialTapTargetSize,
required super.child,
- }) : assert(textTheme != null),
- assert(minWidth != null && minWidth >= 0.0),
- assert(height != null && height >= 0.0),
- assert(alignedDropdown != null),
- assert(layoutBehavior != null),
+ }) : assert(minWidth >= 0.0),
+ assert(height >= 0.0),
data = ButtonThemeData(
textTheme: textTheme,
minWidth: minWidth,
@@ -117,7 +114,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the color and geometry of buttons.
final ButtonThemeData data;
@@ -194,11 +191,8 @@
Color? splashColor,
this.colorScheme,
MaterialTapTargetSize? materialTapTargetSize,
- }) : assert(textTheme != null),
- assert(minWidth != null && minWidth >= 0.0),
- assert(height != null && height >= 0.0),
- assert(alignedDropdown != null),
- assert(layoutBehavior != null),
+ }) : assert(minWidth >= 0.0),
+ assert(height >= 0.0),
_buttonColor = buttonColor,
_disabledColor = disabledColor,
_focusColor = focusColor,
@@ -235,7 +229,7 @@
/// Defaults to [ButtonBarLayoutBehavior.padded].
final ButtonBarLayoutBehavior layoutBehavior;
- /// Simply a convenience that returns [minWidth] and [height] as a
+ /// Convenience that returns [minWidth] and [height] as a
/// [BoxConstraints] object.
BoxConstraints get constraints {
return BoxConstraints(
@@ -382,13 +376,12 @@
/// A set of thirteen colors that can be used to derive the button theme's
/// colors.
///
- /// This property was added much later than the theme's set of highly
- /// specific colors, like [ThemeData.buttonColor], [ThemeData.highlightColor],
- /// [ThemeData.splashColor] etc.
+ /// This property was added much later than the theme's set of highly specific
+ /// colors, like [ThemeData.highlightColor] and [ThemeData.splashColor] etc.
///
- /// The colors for new button classes can be defined exclusively in terms
- /// of [colorScheme]. When it's possible, the existing buttons will
- /// (continue to) gradually migrate to it.
+ /// The colors for new button classes can be defined exclusively in terms of
+ /// [colorScheme]. When it's possible, the existing buttons will (continue to)
+ /// gradually migrate to it.
final ColorScheme? colorScheme;
// The minimum size of a button's tap target.
diff --git a/framework/lib/src/material/calendar_date_picker.dart b/framework/lib/src/material/calendar_date_picker.dart
index cfe617e..754fa2e 100644
--- a/framework/lib/src/material/calendar_date_picker.dart
+++ b/framework/lib/src/material/calendar_date_picker.dart
@@ -11,12 +11,14 @@
import 'color_scheme.dart';
import 'date.dart';
+import 'date_picker_theme.dart';
import 'debug.dart';
import 'divider.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material_localizations.dart';
+import 'material_state.dart';
import 'text_theme.dart';
import 'theme.dart';
@@ -93,15 +95,10 @@
this.onDisplayedMonthChanged,
this.initialCalendarMode = DatePickerMode.day,
this.selectableDayPredicate,
- }) : assert(initialDate != null),
- assert(firstDate != null),
- assert(lastDate != null),
- initialDate = DateUtils.dateOnly(initialDate),
+ }) : initialDate = DateUtils.dateOnly(initialDate),
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate),
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
- assert(onDateChanged != null),
- assert(initialCalendarMode != null) {
+ currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
@@ -188,8 +185,10 @@
_textDirection = Directionality.of(context);
if (!_announcedInitialDate) {
_announcedInitialDate = true;
+ final bool isToday = DateUtils.isSameDay(widget.currentDate, _selectedDate);
+ final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
SemanticsService.announce(
- _localizations.formatFullDate(_selectedDate),
+ '${_localizations.formatFullDate(_selectedDate)}$semanticLabelSuffix',
_textDirection,
);
}
@@ -282,7 +281,7 @@
firstDate: widget.firstDate,
lastDate: widget.lastDate,
initialDate: _currentDisplayedMonthDate,
- selectedDate: _selectedDate,
+ selectedDate: _currentDisplayedMonthDate,
onChanged: _handleYearChanged,
),
);
@@ -440,12 +439,7 @@
required this.onChanged,
required this.onDisplayedMonthChanged,
this.selectableDayPredicate,
- }) : assert(selectedDate != null),
- assert(currentDate != null),
- assert(onChanged != null),
- assert(firstDate != null),
- assert(lastDate != null),
- assert(!firstDate.isAfter(lastDate)),
+ }) : assert(!firstDate.isAfter(lastDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate));
@@ -827,13 +821,7 @@
required this.selectedDate,
required this.onChanged,
this.selectableDayPredicate,
- }) : assert(currentDate != null),
- assert(displayedMonth != null),
- assert(firstDate != null),
- assert(lastDate != null),
- assert(selectedDate != null),
- assert(onChanged != null),
- assert(!firstDate.isAfter(lastDate)),
+ }) : assert(!firstDate.isAfter(lastDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate));
@@ -934,18 +922,11 @@
@override
Widget build(BuildContext context) {
- final ColorScheme colorScheme = Theme.of(context).colorScheme;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final TextTheme textTheme = Theme.of(context).textTheme;
- final TextStyle? headerStyle = textTheme.bodySmall?.apply(
- color: colorScheme.onSurface.withOpacity(0.60),
- );
- final TextStyle dayStyle = textTheme.bodySmall!;
- final Color enabledDayColor = colorScheme.onSurface.withOpacity(0.87);
- final Color disabledDayColor = colorScheme.onSurface.withOpacity(0.38);
- final Color selectedDayColor = colorScheme.onPrimary;
- final Color selectedDayBackground = colorScheme.primary;
- final Color todayColor = colorScheme.primary;
+ final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
+ final TextStyle? weekdayStyle = datePickerTheme.weekdayStyle ?? defaults.weekdayStyle;
+ final TextStyle? dayStyle = datePickerTheme.dayStyle ?? defaults.dayStyle;
final int year = widget.displayedMonth.year;
final int month = widget.displayedMonth.month;
@@ -953,7 +934,19 @@
final int daysInMonth = DateUtils.getDaysInMonth(year, month);
final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
- final List<Widget> dayItems = _dayHeaders(headerStyle, localizations);
+ T? effectiveValue<T>(T? Function(DatePickerThemeData? theme) getProperty) {
+ return getProperty(datePickerTheme) ?? getProperty(defaults);
+ }
+
+ T? resolve<T>(MaterialStateProperty<T>? Function(DatePickerThemeData? theme) getProperty, Set<MaterialState> states) {
+ return effectiveValue(
+ (DatePickerThemeData? theme) {
+ return getProperty(theme)?.resolve(states);
+ },
+ );
+ }
+
+ final List<Widget> dayItems = _dayHeaders(weekdayStyle, localizations);
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
// a leap year.
int day = -dayOffset;
@@ -963,42 +956,42 @@
dayItems.add(Container());
} else {
final DateTime dayToBuild = DateTime(year, month, day);
- final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
- dayToBuild.isBefore(widget.firstDate) ||
- (widget.selectableDayPredicate != null && !widget.selectableDayPredicate!(dayToBuild));
+ final bool isDisabled =
+ dayToBuild.isAfter(widget.lastDate) ||
+ dayToBuild.isBefore(widget.firstDate) ||
+ (widget.selectableDayPredicate != null && !widget.selectableDayPredicate!(dayToBuild));
final bool isSelectedDay = DateUtils.isSameDay(widget.selectedDate, dayToBuild);
final bool isToday = DateUtils.isSameDay(widget.currentDate, dayToBuild);
+ final String semanticLabelSuffix = isToday ? ', ${localizations.currentDateLabel}' : '';
- BoxDecoration? decoration;
- Color dayColor = enabledDayColor;
- if (isSelectedDay) {
- // The selected day gets a circle background highlight, and a
- // contrasting text color.
- dayColor = selectedDayColor;
- decoration = BoxDecoration(
- color: selectedDayBackground,
- shape: BoxShape.circle,
- );
- } else if (isToday) {
- // The current day gets a different text color (if enabled) and a circle stroke
- // border.
- if (isDisabled) {
- dayColor = disabledDayColor;
- } else {
- dayColor = todayColor;
- }
- decoration = BoxDecoration(
- border: Border.all(color: dayColor),
- shape: BoxShape.circle,
- );
- } else if (isDisabled) {
- dayColor = disabledDayColor;
- }
+ final Set<MaterialState> states = <MaterialState>{
+ if (isDisabled) MaterialState.disabled,
+ if (isSelectedDay) MaterialState.selected,
+ };
+
+ final Color? dayForegroundColor = resolve<Color?>((DatePickerThemeData? theme) => isToday ? theme?.todayForegroundColor : theme?.dayForegroundColor, states);
+ final Color? dayBackgroundColor = resolve<Color?>((DatePickerThemeData? theme) => isToday ? theme?.todayBackgroundColor : theme?.dayBackgroundColor, states);
+ final MaterialStateProperty<Color?> dayOverlayColor = MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)),
+ );
+ final BoxDecoration decoration = isToday
+ ? BoxDecoration(
+ color: dayBackgroundColor,
+ border: Border.fromBorderSide(
+ (datePickerTheme.todayBorder ?? defaults.todayBorder!)
+ .copyWith(color: dayForegroundColor)
+ ),
+ shape: BoxShape.circle,
+ )
+ : BoxDecoration(
+ color: dayBackgroundColor,
+ shape: BoxShape.circle,
+ );
Widget dayWidget = Container(
decoration: decoration,
child: Center(
- child: Text(localizations.formatDecimal(day), style: dayStyle.apply(color: dayColor)),
+ child: Text(localizations.formatDecimal(day), style: dayStyle?.apply(color: dayForegroundColor)),
),
);
@@ -1011,7 +1004,8 @@
focusNode: _dayFocusNodes[day - 1],
onTap: () => widget.onChanged(dayToBuild),
radius: _dayPickerRowHeight / 2 + 4,
- splashColor: selectedDayBackground.withOpacity(0.38),
+ statesController: MaterialStatesController(states),
+ overlayColor: dayOverlayColor,
child: Semantics(
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
@@ -1019,7 +1013,7 @@
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
- label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}',
+ label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}$semanticLabelSuffix',
selected: isSelectedDay,
excludeSemantics: true,
child: dayWidget,
@@ -1101,11 +1095,7 @@
required this.selectedDate,
required this.onChanged,
this.dragStartBehavior = DragStartBehavior.start,
- }) : assert(firstDate != null),
- assert(lastDate != null),
- assert(selectedDate != null),
- assert(onChanged != null),
- assert(!firstDate.isAfter(lastDate)),
+ }) : assert(!firstDate.isAfter(lastDate)),
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
initialDate = DateUtils.dateOnly(initialDate ?? selectedDate);
@@ -1167,8 +1157,20 @@
}
Widget _buildYearItem(BuildContext context, int index) {
- final ColorScheme colorScheme = Theme.of(context).colorScheme;
- final TextTheme textTheme = Theme.of(context).textTheme;
+ final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
+
+ T? effectiveValue<T>(T? Function(DatePickerThemeData? theme) getProperty) {
+ return getProperty(datePickerTheme) ?? getProperty(defaults);
+ }
+
+ T? resolve<T>(MaterialStateProperty<T>? Function(DatePickerThemeData? theme) getProperty, Set<MaterialState> states) {
+ return effectiveValue(
+ (DatePickerThemeData? theme) {
+ return getProperty(theme)?.resolve(states);
+ },
+ );
+ }
// Backfill the _YearPicker with disabled years if necessary.
final int offset = _itemCount < minYears ? (minYears - _itemCount) ~/ 2 : 0;
@@ -1179,33 +1181,32 @@
const double decorationHeight = 36.0;
const double decorationWidth = 72.0;
- final Color textColor;
- if (isSelected) {
- textColor = colorScheme.onPrimary;
- } else if (isDisabled) {
- textColor = colorScheme.onSurface.withOpacity(0.38);
- } else if (isCurrentYear) {
- textColor = colorScheme.primary;
- } else {
- textColor = colorScheme.onSurface.withOpacity(0.87);
- }
- final TextStyle? itemStyle = textTheme.bodyLarge?.apply(color: textColor);
+ final Set<MaterialState> states = <MaterialState>{
+ if (isDisabled) MaterialState.disabled,
+ if (isSelected) MaterialState.selected,
+ };
- BoxDecoration? decoration;
- if (isSelected) {
- decoration = BoxDecoration(
- color: colorScheme.primary,
- borderRadius: BorderRadius.circular(decorationHeight / 2),
+ final Color? textColor = resolve<Color?>((DatePickerThemeData? theme) => isCurrentYear ? theme?.todayForegroundColor : theme?.yearForegroundColor, states);
+ final Color? background = resolve<Color?>((DatePickerThemeData? theme) => isCurrentYear ? theme?.todayBackgroundColor : theme?.yearBackgroundColor, states);
+ final MaterialStateProperty<Color?> overlayColor =
+ MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) =>
+ effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)),
);
- } else if (isCurrentYear && !isDisabled) {
- decoration = BoxDecoration(
- border: Border.all(
- color: colorScheme.primary,
- ),
- borderRadius: BorderRadius.circular(decorationHeight / 2),
- );
- }
+ BoxBorder? border;
+ if (isCurrentYear) {
+ final BorderSide? todayBorder = datePickerTheme.todayBorder ?? defaults.todayBorder;
+ if (todayBorder != null) {
+ border = Border.fromBorderSide(todayBorder.copyWith(color: textColor));
+ }
+ }
+ final BoxDecoration decoration = BoxDecoration(
+ border: border,
+ color: background,
+ borderRadius: BorderRadius.circular(decorationHeight / 2),
+ );
+
+ final TextStyle? itemStyle = (datePickerTheme.yearStyle ?? defaults.yearStyle)?.apply(color: textColor);
Widget yearItem = Center(
child: Container(
decoration: decoration,
@@ -1229,6 +1230,8 @@
yearItem = InkWell(
key: ValueKey<int>(year),
onTap: () => widget.onChanged(DateTime(year, widget.initialDate.month)),
+ statesController: MaterialStatesController(states),
+ overlayColor: overlayColor,
child: yearItem,
);
}
diff --git a/framework/lib/src/material/card.dart b/framework/lib/src/material/card.dart
index 00ef5a7..998f4e4 100644
--- a/framework/lib/src/material/card.dart
+++ b/framework/lib/src/material/card.dart
@@ -71,15 +71,15 @@
this.clipBehavior,
this.child,
this.semanticContainer = true,
- }) : assert(elevation == null || elevation >= 0.0),
- assert(borderOnForeground != null);
+ }) : assert(elevation == null || elevation >= 0.0);
/// The card's background color.
///
/// Defines the card's [Material.color].
///
- /// If this property is null then [CardTheme.color] of [ThemeData.cardTheme]
- /// is used. If that's null then [ThemeData.cardColor] is used.
+ /// If this property is null then the ambient [CardTheme.color] is used. If that is null,
+ /// and [ThemeData.useMaterial3] is true, then [ColorScheme.surface] of
+ /// [ThemeData.colorScheme] is used. Otherwise, [ThemeData.cardColor] is used.
final Color? color;
/// The color to paint the shadow below the card.
@@ -214,7 +214,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _CardDefaultsM3 extends CardTheme {
const _CardDefaultsM3(this.context)
diff --git a/framework/lib/src/material/card_theme.dart b/framework/lib/src/material/card_theme.dart
index aa6f80e..db849ee 100644
--- a/framework/lib/src/material/card_theme.dart
+++ b/framework/lib/src/material/card_theme.dart
@@ -114,7 +114,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static CardTheme lerp(CardTheme? a, CardTheme? b, double t) {
- assert(t != null);
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 3f83fec..d4e8346 100644
--- a/framework/lib/src/material/checkbox.dart
+++ b/framework/lib/src/material/checkbox.dart
@@ -34,7 +34,7 @@
///
/// {@tool dartpad}
/// This example shows how you can override the default theme of
-/// of a [Checkbox] with a [MaterialStateProperty].
+/// a [Checkbox] with a [MaterialStateProperty].
/// In this example, the checkbox's color will be `Colors.blue` when the [Checkbox]
/// is being pressed, hovered, or focused. Otherwise, the checkbox's color will
/// be `Colors.red`.
@@ -88,9 +88,7 @@
this.shape,
this.side,
this.isError = false,
- }) : assert(tristate != null),
- assert(tristate || value != null),
- assert(autofocus != null);
+ }) : assert(tristate || value != null);
/// Whether this checkbox is checked.
///
@@ -321,6 +319,7 @@
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
+ /// * [MaterialState.error].
///
/// If this property is not a [MaterialStateBorderSide] and it is
/// non-null, then it is only rendered when the checkbox's value is
@@ -396,7 +395,8 @@
BorderSide? _resolveSide(BorderSide? side) {
if (side is MaterialStateBorderSide) {
- return MaterialStateProperty.resolveAs<BorderSide?>(side, states);
+ final Set<MaterialState> sideStates = widget.isError ? (states..add(MaterialState.error)) : states;
+ return MaterialStateProperty.resolveAs<BorderSide?>(side, sideStates);
}
if (!states.contains(MaterialState.selected)) {
return side;
@@ -764,7 +764,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _CheckboxDefaultsM3 extends CheckboxThemeData {
_CheckboxDefaultsM3(BuildContext context)
diff --git a/framework/lib/src/material/checkbox_list_tile.dart b/framework/lib/src/material/checkbox_list_tile.dart
index 8040099..97f25c7 100644
--- a/framework/lib/src/material/checkbox_list_tile.dart
+++ b/framework/lib/src/material/checkbox_list_tile.dart
@@ -50,7 +50,7 @@
///
/// {@tool snippet}
/// ```dart
-/// Container(
+/// ColoredBox(
/// color: Colors.green,
/// child: Material(
/// child: CheckboxListTile(
@@ -85,6 +85,13 @@
/// ** See code in examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart **
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This sample demonstrates how [CheckboxListTile] positions the checkbox widget
+/// relative to the text in different configurations.
+///
+/// ** See code in examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart **
+/// {@end-tool}
+///
/// ## Semantics in CheckboxListTile
///
/// Since the entirety of the CheckboxListTile is interactive, it should represent
@@ -109,7 +116,7 @@
/// LinkedLabelCheckbox, that includes an interactive [RichText] widget that
/// handles tap gestures.
///
-/// ** See code in examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart **
+/// ** See code in examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart **
/// {@end-tool}
///
/// ## CheckboxListTile isn't exactly what I want
@@ -125,7 +132,7 @@
/// Here is an example of a custom LabeledCheckbox widget, but you can easily
/// make your own configurable widget.
///
-/// ** See code in examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.2.dart **
+/// ** See code in examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart **
/// {@end-tool}
///
/// See also:
@@ -178,13 +185,8 @@
this.focusNode,
this.onFocusChange,
this.enableFeedback,
- }) : assert(tristate != null),
- assert(tristate || value != null),
- assert(isThreeLine != null),
- assert(!isThreeLine || subtitle != null),
- assert(selected != null),
- assert(controlAffinity != null),
- assert(autofocus != null);
+ }) : assert(tristate || value != null),
+ assert(!isThreeLine || subtitle != null);
/// Whether this checkbox is checked.
final bool? value;
@@ -219,7 +221,7 @@
/// The color to use when this checkbox is checked.
///
- /// Defaults to accent color of the current [Theme].
+ /// Defaults to [ColorScheme.secondary] of the current [Theme].
final Color? activeColor;
/// The color to use for the check icon when this checkbox is checked.
diff --git a/framework/lib/src/material/checkbox_theme.dart b/framework/lib/src/material/checkbox_theme.dart
index 807cd5f..7d99ecd 100644
--- a/framework/lib/src/material/checkbox_theme.dart
+++ b/framework/lib/src/material/checkbox_theme.dart
@@ -192,10 +192,10 @@
// Special case because BorderSide.lerp() doesn't support null arguments
static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) {
- if (a == null && b == null) {
+ if (a == null || b == null) {
return null;
}
- return BorderSide.lerp(a!, b!, t);
+ return BorderSide.lerp(a, b, t);
}
}
diff --git a/framework/lib/src/material/chip.dart b/framework/lib/src/material/chip.dart
index 2d0907e..e6c40cf 100644
--- a/framework/lib/src/material/chip.dart
+++ b/framework/lib/src/material/chip.dart
@@ -593,10 +593,7 @@
'This feature was deprecated after v2.10.0-0.3.pre.'
)
this.useDeleteButtonTooltip = true,
- }) : assert(label != null),
- assert(autofocus != null),
- assert(clipBehavior != null),
- assert(elevation == null || elevation >= 0.0);
+ }) : assert(elevation == null || elevation >= 0.0);
@override
final Widget? avatar;
@@ -766,12 +763,7 @@
'This feature was deprecated after v2.10.0-0.3.pre.'
)
this.useDeleteButtonTooltip = true,
- }) : assert(label != null),
- assert(isEnabled != null),
- assert(selected != null),
- assert(clipBehavior != null),
- assert(autofocus != null),
- assert(pressElevation == null || pressElevation >= 0.0),
+ }) : assert(pressElevation == null || pressElevation >= 0.0),
assert(elevation == null || elevation >= 0.0),
deleteIcon = deleteIcon ?? _kDefaultDeleteIcon;
@@ -1158,7 +1150,7 @@
final EdgeInsetsGeometry defaultLabelPadding = EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 8.0),
const EdgeInsets.symmetric(horizontal: 4.0),
- clampDouble(MediaQuery.of(context).textScaleFactor - 1.0, 0.0, 1.0),
+ clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
)!;
final ThemeData theme = Theme.of(context);
@@ -1381,7 +1373,7 @@
required this.deleteDrawerAnimation,
required this.enableAnimation,
this.avatarBorder,
- }) : assert(theme != null);
+ });
final _ChipRenderTheme theme;
final bool? value;
@@ -1518,9 +1510,7 @@
required this.deleteDrawerAnimation,
required this.enableAnimation,
this.avatarBorder,
- }) : assert(theme != null),
- assert(textDirection != null),
- _theme = theme,
+ }) : _theme = theme,
_textDirection = textDirection {
checkmarkAnimation.addListener(markNeedsPaint);
avatarDrawerAnimation.addListener(markNeedsLayout);
@@ -1917,7 +1907,7 @@
);
Color get _disabledColor {
- if (enableAnimation == null || enableAnimation.isCompleted) {
+ if (enableAnimation.isCompleted) {
return Colors.white;
}
final ColorTween enableTween;
@@ -2196,7 +2186,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _ChipDefaultsM3 extends ChipThemeData {
const _ChipDefaultsM3(this.context, this.isEnabled)
@@ -2257,7 +2247,7 @@
EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 8.0),
const EdgeInsets.symmetric(horizontal: 4.0),
- clampDouble(MediaQuery.of(context).textScaleFactor - 1.0, 0.0, 1.0),
+ clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
)!;
}
diff --git a/framework/lib/src/material/chip_theme.dart b/framework/lib/src/material/chip_theme.dart
index 3583fc4..63e65bc 100644
--- a/framework/lib/src/material/chip_theme.dart
+++ b/framework/lib/src/material/chip_theme.dart
@@ -47,8 +47,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// Specifies the color, shape, and text style values for descendant chip
/// widgets.
@@ -225,8 +224,6 @@
}) {
assert(primaryColor != null || brightness != null, 'One of primaryColor or brightness must be specified');
assert(primaryColor == null || brightness == null, 'Only one of primaryColor or brightness may be specified');
- assert(secondaryColor != null);
- assert(labelStyle != null);
if (primaryColor != null) {
brightness = ThemeData.estimateBrightnessForColor(primaryColor);
@@ -486,7 +483,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static ChipThemeData? lerp(ChipThemeData? a, ChipThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/material/choice_chip.dart b/framework/lib/src/material/choice_chip.dart
index aaf6965..9bd44bd 100644
--- a/framework/lib/src/material/choice_chip.dart
+++ b/framework/lib/src/material/choice_chip.dart
@@ -83,11 +83,7 @@
this.iconTheme,
this.selectedShadowColor,
this.avatarBorder = const CircleBorder(),
- }) : assert(selected != null),
- assert(label != null),
- assert(clipBehavior != null),
- assert(autofocus != null),
- assert(pressElevation == null || pressElevation >= 0.0),
+ }) : assert(pressElevation == null || pressElevation >= 0.0),
assert(elevation == null || elevation >= 0.0);
@override
@@ -191,7 +187,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _ChoiceChipDefaultsM3 extends ChipThemeData {
const _ChoiceChipDefaultsM3(this.context, this.isEnabled, this.isSelected)
@@ -212,7 +208,7 @@
Color? get backgroundColor => null;
@override
- Color? get shadowColor => Theme.of(context).colorScheme.shadow;
+ Color? get shadowColor => Colors.transparent;
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
@@ -259,7 +255,7 @@
EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 8.0),
const EdgeInsets.symmetric(horizontal: 4.0),
- clampDouble(MediaQuery.of(context).textScaleFactor - 1.0, 0.0, 1.0),
+ clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
)!;
}
diff --git a/framework/lib/src/material/color_scheme.dart b/framework/lib/src/material/color_scheme.dart
index 68e8f2e..2e55a26 100644
--- a/framework/lib/src/material/color_scheme.dart
+++ b/framework/lib/src/material/color_scheme.dart
@@ -9,9 +9,11 @@
import 'colors.dart';
import 'theme_data.dart';
+/// {@template flutter.material.color_scheme.ColorScheme}
/// A set of 30 colors based on the
/// [Material spec](https://m3.material.io/styles/color/the-color-system/color-roles)
/// that can be used to configure the color properties of most components.
+/// {@endtemplate}
///
/// The main accent color groups in the scheme are [primary], [secondary],
/// and [tertiary].
@@ -117,18 +119,7 @@
'This feature was deprecated after v2.6.0-0.0.pre.'
)
Color? secondaryVariant,
- }) : assert(brightness != null),
- assert(primary != null),
- assert(onPrimary != null),
- assert(secondary != null),
- assert(onSecondary != null),
- assert(error != null),
- assert(onError != null),
- assert(background != null),
- assert(onBackground != null),
- assert(surface != null),
- assert(onSurface != null),
- _primaryContainer = primaryContainer,
+ }) : _primaryContainer = primaryContainer,
_onPrimaryContainer = onPrimaryContainer,
_secondaryContainer = secondaryContainer,
_onSecondaryContainer = onSecondaryContainer,
@@ -295,18 +286,7 @@
'This feature was deprecated after v2.6.0-0.0.pre.'
)
Color? secondaryVariant = const Color(0xff018786),
- }) : assert(brightness != null),
- assert(primary != null),
- assert(onPrimary != null),
- assert(secondary != null),
- assert(onSecondary != null),
- assert(error != null),
- assert(onError != null),
- assert(background != null),
- assert(onBackground != null),
- assert(surface != null),
- assert(onSurface != null),
- _primaryContainer = primaryContainer,
+ }) : _primaryContainer = primaryContainer,
_onPrimaryContainer = onPrimaryContainer,
_secondaryContainer = secondaryContainer,
_onSecondaryContainer = onSecondaryContainer,
@@ -373,18 +353,7 @@
'This feature was deprecated after v2.6.0-0.0.pre.'
)
Color? secondaryVariant = const Color(0xff03dac6),
- }) : assert(brightness != null),
- assert(primary != null),
- assert(onPrimary != null),
- assert(secondary != null),
- assert(onSecondary != null),
- assert(error != null),
- assert(onError != null),
- assert(background != null),
- assert(onBackground != null),
- assert(surface != null),
- assert(onSurface != null),
- _primaryContainer = primaryContainer,
+ }) : _primaryContainer = primaryContainer,
_onPrimaryContainer = onPrimaryContainer,
_secondaryContainer = secondaryContainer,
_onSecondaryContainer = onSecondaryContainer,
@@ -451,18 +420,7 @@
'This feature was deprecated after v2.6.0-0.0.pre.'
)
Color? secondaryVariant = const Color(0xff018786),
- }) : assert(brightness != null),
- assert(primary != null),
- assert(onPrimary != null),
- assert(secondary != null),
- assert(onSecondary != null),
- assert(error != null),
- assert(onError != null),
- assert(background != null),
- assert(onBackground != null),
- assert(surface != null),
- assert(onSurface != null),
- _primaryContainer = primaryContainer,
+ }) : _primaryContainer = primaryContainer,
_onPrimaryContainer = onPrimaryContainer,
_secondaryContainer = secondaryContainer,
_onSecondaryContainer = onSecondaryContainer,
@@ -529,18 +487,7 @@
'This feature was deprecated after v2.6.0-0.0.pre.'
)
Color? secondaryVariant = const Color(0xff66fff9),
- }) : assert(brightness != null),
- assert(primary != null),
- assert(onPrimary != null),
- assert(secondary != null),
- assert(onSecondary != null),
- assert(error != null),
- assert(onError != null),
- assert(background != null),
- assert(onBackground != null),
- assert(surface != null),
- assert(onSurface != null),
- _primaryContainer = primaryContainer,
+ }) : _primaryContainer = primaryContainer,
_onPrimaryContainer = onPrimaryContainer,
_secondaryContainer = secondaryContainer,
_onSecondaryContainer = onSecondaryContainer,
@@ -576,8 +523,6 @@
Color? errorColor,
Brightness brightness = Brightness.light,
}) {
- assert(primarySwatch != null);
- assert(brightness != null);
final bool isDark = brightness == Brightness.dark;
final bool primaryIsDark = _brightnessFor(primarySwatch) == Brightness.dark;
diff --git a/framework/lib/src/material/data_table.dart b/framework/lib/src/material/data_table.dart
index c790a70..ba4f434 100644
--- a/framework/lib/src/material/data_table.dart
+++ b/framework/lib/src/material/data_table.dart
@@ -44,7 +44,7 @@
this.tooltip,
this.numeric = false,
this.onSort,
- }) : assert(label != null);
+ });
/// The column heading.
///
@@ -107,7 +107,7 @@
this.onLongPress,
this.color,
required this.cells,
- }) : assert(cells != null);
+ });
/// Creates the configuration for a row of a [DataTable], deriving
/// the key from a row index.
@@ -120,8 +120,7 @@
this.onLongPress,
this.color,
required this.cells,
- }) : assert(cells != null),
- key = ValueKey<int?>(index);
+ }) : key = ValueKey<int?>(index);
/// A [Key] that uniquely identifies this row. This is used to
/// ensure that if a row is added or removed, any stateful widgets
@@ -234,7 +233,7 @@
this.onTapDown,
this.onDoubleTap,
this.onTapCancel,
- }) : assert(child != null);
+ });
/// A cell that has no content and has zero width and height.
static const DataCell empty = DataCell(SizedBox.shrink());
@@ -407,15 +406,10 @@
this.checkboxHorizontalMargin,
this.border,
this.clipBehavior = Clip.none,
- }) : assert(columns != null),
- assert(columns.isNotEmpty),
+ }) : assert(columns.isNotEmpty),
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
- assert(sortAscending != null),
- assert(showCheckboxColumn != null),
- assert(rows != null),
assert(!rows.any((DataRow row) => row.cells.length != columns.length)),
assert(dividerThickness == null || dividerThickness >= 0),
- assert(clipBehavior != null),
_onlyTextColumn = _initOnlyTextColumn(columns);
/// The configuration and labels for the columns in the table.
@@ -973,7 +967,7 @@
int displayColumnIndex = 0;
if (displayCheckboxColumn) {
tableColumns[0] = FixedColumnWidth(effectiveCheckboxHorizontalMarginStart + Checkbox.width + effectiveCheckboxHorizontalMarginEnd);
- tableRows[0].children![0] = _buildCheckbox(
+ tableRows[0].children[0] = _buildCheckbox(
context: context,
checked: someChecked ? null : allChecked,
onRowTap: null,
@@ -983,7 +977,7 @@
);
rowIndex = 1;
for (final DataRow row in rows) {
- tableRows[rowIndex].children![0] = _buildCheckbox(
+ tableRows[rowIndex].children[0] = _buildCheckbox(
context: context,
checked: row.selected,
onRowTap: row.onSelectChanged == null ? null : () => row.onSelectChanged?.call(!row.selected),
@@ -1026,7 +1020,7 @@
} else {
tableColumns[displayColumnIndex] = const IntrinsicColumnWidth();
}
- tableRows[0].children![displayColumnIndex] = _buildHeadingCell(
+ tableRows[0].children[displayColumnIndex] = _buildHeadingCell(
context: context,
padding: padding,
label: column.label,
@@ -1040,7 +1034,7 @@
rowIndex = 1;
for (final DataRow row in rows) {
final DataCell cell = row.cells[dataColumnIndex];
- tableRows[rowIndex].children![displayColumnIndex] = _buildDataCell(
+ tableRows[rowIndex].children[displayColumnIndex] = _buildDataCell(
context: context,
padding: padding,
label: cell.child,
diff --git a/framework/lib/src/material/data_table_theme.dart b/framework/lib/src/material/data_table_theme.dart
index 5f8d741..1ff4b55 100644
--- a/framework/lib/src/material/data_table_theme.dart
+++ b/framework/lib/src/material/data_table_theme.dart
@@ -122,7 +122,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static DataTableThemeData lerp(DataTableThemeData a, DataTableThemeData b, double t) {
- assert(t != null);
return DataTableThemeData(
decoration: Decoration.lerp(a.decoration, b.decoration, t),
dataRowColor: MaterialStateProperty.lerp<Color?>(a.dataRowColor, b.dataRowColor, t, Color.lerp),
@@ -214,7 +213,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties used for all descendant [DataTable] widgets.
final DataTableThemeData data;
diff --git a/framework/lib/src/material/date.dart b/framework/lib/src/material/date.dart
index a06c921..5186c48 100644
--- a/framework/lib/src/material/date.dart
+++ b/framework/lib/src/material/date.dart
@@ -212,9 +212,7 @@
DateTimeRange({
required this.start,
required this.end,
- }) : assert(start != null),
- assert(end != null),
- assert(!start.isAfter(end));
+ }) : assert(!start.isAfter(end));
/// The start of the range of dates.
final DateTime start;
diff --git a/framework/lib/src/material/date_picker.dart b/framework/lib/src/material/date_picker.dart
index 553979f..e9e0c90 100644
--- a/framework/lib/src/material/date_picker.dart
+++ b/framework/lib/src/material/date_picker.dart
@@ -4,16 +4,18 @@
import 'dart:math' as math;
-import 'package:flute/gestures.dart' show DragStartBehavior;
import 'package:flute/rendering.dart';
import 'package:flute/services.dart';
import 'package:flute/widgets.dart';
import 'app_bar.dart';
import 'back_button.dart';
+import 'button_style.dart';
import 'calendar_date_picker.dart';
import 'color_scheme.dart';
+import 'colors.dart';
import 'date.dart';
+import 'date_picker_theme.dart';
import 'debug.dart';
import 'dialog.dart';
import 'dialog_theme.dart';
@@ -26,15 +28,20 @@
import 'input_decorator.dart';
import 'material.dart';
import 'material_localizations.dart';
+import 'material_state.dart';
import 'scaffold.dart';
import 'text_button.dart';
import 'text_field.dart';
import 'text_theme.dart';
import 'theme.dart';
-const Size _calendarPortraitDialogSize = Size(330.0, 518.0);
+// The M3 sizes are coming from the tokens, but are hand coded,
+// as the current token DB does not contain landscape versions.
+const Size _calendarPortraitDialogSizeM2 = Size(330.0, 518.0);
+const Size _calendarPortraitDialogSizeM3 = Size(328.0, 512.0);
const Size _calendarLandscapeDialogSize = Size(496.0, 346.0);
-const Size _inputPortraitDialogSize = Size(330.0, 270.0);
+const Size _inputPortraitDialogSizeM2 = Size(330.0, 270.0);
+const Size _inputPortraitDialogSizeM3 = Size(328.0, 270.0);
const Size _inputLandscapeDialogSize = Size(496, 160.0);
const Size _inputRangeLandscapeDialogSize = Size(496, 164.0);
const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
@@ -158,10 +165,6 @@
TextInputType? keyboardType,
Offset? anchorPoint,
}) async {
- assert(context != null);
- assert(initialDate != null);
- assert(firstDate != null);
- assert(lastDate != null);
initialDate = DateUtils.dateOnly(initialDate);
firstDate = DateUtils.dateOnly(firstDate);
lastDate = DateUtils.dateOnly(lastDate);
@@ -181,9 +184,6 @@
selectableDayPredicate == null || selectableDayPredicate(initialDate),
'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.',
);
- assert(initialEntryMode != null);
- assert(useRootNavigator != null);
- assert(initialDatePickerMode != null);
assert(debugCheckHasMaterialLocalizations(context));
Widget dialog = DatePickerDialog(
@@ -259,15 +259,10 @@
this.fieldLabelText,
this.keyboardType,
this.restorationId,
- }) : assert(initialDate != null),
- assert(firstDate != null),
- assert(lastDate != null),
- initialDate = DateUtils.dateOnly(initialDate),
+ }) : initialDate = DateUtils.dateOnly(initialDate),
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate),
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
- assert(initialEntryMode != null),
- assert(initialCalendarMode != null) {
+ currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
@@ -425,13 +420,15 @@
}
Size _dialogSize(BuildContext context) {
- final Orientation orientation = MediaQuery.of(context).orientation;
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
+ final Orientation orientation = MediaQuery.orientationOf(context);
+
switch (_entryMode.value) {
case DatePickerEntryMode.calendar:
case DatePickerEntryMode.calendarOnly:
switch (orientation) {
case Orientation.portrait:
- return _calendarPortraitDialogSize;
+ return useMaterial3 ? _calendarPortraitDialogSizeM3 : _calendarPortraitDialogSizeM2;
case Orientation.landscape:
return _calendarLandscapeDialogSize;
}
@@ -439,7 +436,7 @@
case DatePickerEntryMode.inputOnly:
switch (orientation) {
case Orientation.portrait:
- return _inputPortraitDialogSize;
+ return useMaterial3 ? _inputPortraitDialogSizeM3 : _inputPortraitDialogSizeM2;
case Orientation.landscape:
return _inputLandscapeDialogSize;
}
@@ -454,21 +451,28 @@
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
+ final bool useMaterial3 = theme.useMaterial3;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final Orientation orientation = MediaQuery.of(context).orientation;
+ final Orientation orientation = MediaQuery.orientationOf(context);
+ final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
final TextTheme textTheme = theme.textTheme;
+
// Constrain the textScaleFactor to the largest supported value to prevent
// layout issues.
- final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.3);
+ final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 1.3);
+ final Color? headerForegroundColor = datePickerTheme.headerForegroundColor ?? defaults.headerForegroundColor;
+ final TextStyle? headlineStyle = useMaterial3
+ ? (datePickerTheme.headerHeadlineStyle ?? defaults.headerHeadlineStyle)?.copyWith(
+ color: headerForegroundColor,
+ )
+ // Material2 has support for landscape and the current M3 spec doesn't
+ // address this layout, so handling it seperately here.
+ : (orientation == Orientation.landscape
+ ? textTheme.headlineSmall?.copyWith(color: headerForegroundColor)
+ : textTheme.headlineMedium?.copyWith(color: headerForegroundColor));
final String dateText = localizations.formatMediumDate(_selectedDate.value);
- final Color onPrimarySurface = colorScheme.brightness == Brightness.light
- ? colorScheme.onPrimary
- : colorScheme.onSurface;
- final TextStyle? dateStyle = orientation == Orientation.landscape
- ? textTheme.headlineSmall?.copyWith(color: onPrimarySurface)
- : textTheme.headlineMedium?.copyWith(color: onPrimarySurface);
final Widget actions = Container(
alignment: AlignmentDirectional.centerEnd,
@@ -480,7 +484,7 @@
TextButton(
onPressed: _handleCancel,
child: Text(widget.cancelText ?? (
- theme.useMaterial3
+ useMaterial3
? localizations.cancelButtonLabel
: localizations.cancelButtonLabel.toUpperCase()
)),
@@ -546,8 +550,8 @@
case DatePickerEntryMode.calendar:
picker = calendarDatePicker();
entryModeButton = IconButton(
- icon: const Icon(Icons.edit),
- color: onPrimarySurface,
+ icon: Icon(useMaterial3 ? Icons.edit_outlined : Icons.edit),
+ color: headerForegroundColor,
tooltip: localizations.inputDateModeButtonLabel,
onPressed: _handleEntryModeToggle,
);
@@ -562,7 +566,7 @@
picker = inputDatePicker();
entryModeButton = IconButton(
icon: const Icon(Icons.calendar_today),
- color: onPrimarySurface,
+ color: headerForegroundColor,
tooltip: localizations.calendarModeButtonLabel,
onPressed: _handleEntryModeToggle,
);
@@ -576,19 +580,29 @@
final Widget header = _DatePickerHeader(
helpText: widget.helpText ?? (
- Theme.of(context).useMaterial3
+ useMaterial3
? localizations.datePickerHelpText
: localizations.datePickerHelpText.toUpperCase()
),
titleText: dateText,
- titleStyle: dateStyle,
+ titleStyle: headlineStyle,
orientation: orientation,
isShort: orientation == Orientation.landscape,
entryModeButton: entryModeButton,
);
final Size dialogSize = _dialogSize(context) * textScaleFactor;
+ final DialogTheme dialogTheme = theme.dialogTheme;
return Dialog(
+ backgroundColor: datePickerTheme.backgroundColor ?? defaults.backgroundColor,
+ elevation: useMaterial3
+ ? datePickerTheme.elevation ?? defaults.elevation!
+ : datePickerTheme.elevation ?? dialogTheme.elevation ?? 24,
+ shadowColor: datePickerTheme.shadowColor ?? defaults.shadowColor,
+ surfaceTintColor: datePickerTheme.surfaceTintColor ?? defaults.surfaceTintColor,
+ shape: useMaterial3
+ ? datePickerTheme.shape ?? defaults.shape
+ : datePickerTheme.shape ?? dialogTheme.shape ?? defaults.shape,
insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
clipBehavior: Clip.antiAlias,
child: AnimatedContainer(
@@ -608,6 +622,7 @@
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
+ if (useMaterial3) const Divider(),
Expanded(child: picker),
actions,
],
@@ -618,6 +633,7 @@
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
+ if (useMaterial3) const VerticalDivider(),
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -712,9 +728,7 @@
required this.orientation,
this.isShort = false,
this.entryModeButton,
- }) : assert(helpText != null),
- assert(orientation != null),
- assert(isShort != null);
+ });
static const double _datePickerHeaderLandscapeWidth = 152.0;
static const double _datePickerHeaderPortraitHeight = 120.0;
@@ -751,17 +765,12 @@
@override
Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
- final TextTheme textTheme = theme.textTheme;
-
- // The header should use the primary color in light themes and surface color in dark
- final bool isDark = colorScheme.brightness == Brightness.dark;
- final Color primarySurfaceColor = isDark ? colorScheme.surface : colorScheme.primary;
- final Color onPrimarySurfaceColor = isDark ? colorScheme.onSurface : colorScheme.onPrimary;
-
- final TextStyle? helpStyle = textTheme.labelSmall?.copyWith(
- color: onPrimarySurfaceColor,
+ final DatePickerThemeData themeData = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
+ final Color? backgroundColor = themeData.headerBackgroundColor ?? defaults.headerBackgroundColor;
+ final Color? foregroundColor = themeData.headerForegroundColor ?? defaults.headerForegroundColor;
+ final TextStyle? helpStyle = (themeData.headerHelpStyle ?? defaults.headerHelpStyle)?.copyWith(
+ color: foregroundColor,
);
final Text help = Text(
@@ -783,7 +792,7 @@
return SizedBox(
height: _datePickerHeaderPortraitHeight,
child: Material(
- color: primarySurfaceColor,
+ color: backgroundColor,
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: 24,
@@ -811,7 +820,7 @@
return SizedBox(
width: _datePickerHeaderLandscapeWidth,
child: Material(
- color: primarySurfaceColor,
+ color: backgroundColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@@ -963,19 +972,12 @@
TransitionBuilder? builder,
Offset? anchorPoint,
}) async {
- assert(context != null);
- assert(
- initialDateRange == null || (initialDateRange.start != null && initialDateRange.end != null),
- 'initialDateRange must be null or have non-null start and end dates.',
- );
assert(
initialDateRange == null || !initialDateRange.start.isAfter(initialDateRange.end),
"initialDateRange's start date must not be after it's end date.",
);
initialDateRange = initialDateRange == null ? null : DateUtils.datesOnly(initialDateRange);
- assert(firstDate != null);
firstDate = DateUtils.dateOnly(firstDate);
- assert(lastDate != null);
lastDate = DateUtils.dateOnly(lastDate);
assert(
!lastDate.isBefore(firstDate),
@@ -998,8 +1000,6 @@
"initialDateRange's end date must be on or before lastDate $lastDate.",
);
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now());
- assert(initialEntryMode != null);
- assert(useRootNavigator != null);
assert(debugCheckHasMaterialLocalizations(context));
Widget dialog = DateRangePickerDialog(
@@ -1316,19 +1316,20 @@
@override
Widget build(BuildContext context) {
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- final Orientation orientation = mediaQuery.orientation;
- final double textScaleFactor = math.min(mediaQuery.textScaleFactor, 1.3);
+ final ThemeData theme = Theme.of(context);
+ final bool useMaterial3 = theme.useMaterial3;
+ final Orientation orientation = MediaQuery.orientationOf(context);
+ final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 1.3);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final ColorScheme colors = Theme.of(context).colorScheme;
- final Color onPrimarySurface = colors.brightness == Brightness.light
- ? colors.onPrimary
- : colors.onSurface;
+ final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
final Widget contents;
final Size size;
- ShapeBorder? shape;
- final double elevation;
+ final double? elevation;
+ final Color? shadowColor;
+ final Color? surfaceTintColor;
+ final ShapeBorder? shape;
final EdgeInsets insetPadding;
final bool showEntryModeButton =
_entryMode.value == DatePickerEntryMode.calendar ||
@@ -1349,28 +1350,29 @@
onCancel: _handleCancel,
entryModeButton: showEntryModeButton
? IconButton(
- icon: const Icon(Icons.edit),
+ icon: Icon(useMaterial3 ? Icons.edit_outlined : Icons.edit),
padding: EdgeInsets.zero,
- color: onPrimarySurface,
tooltip: localizations.inputDateModeButtonLabel,
onPressed: _handleEntryModeToggle,
)
: null,
confirmText: widget.saveText ?? (
- Theme.of(context).useMaterial3
+ useMaterial3
? localizations.saveButtonLabel
: localizations.saveButtonLabel.toUpperCase()
),
helpText: widget.helpText ?? (
- Theme.of(context).useMaterial3
+ useMaterial3
? localizations.dateRangePickerHelpText
: localizations.dateRangePickerHelpText.toUpperCase()
),
);
- size = mediaQuery.size;
+ size = MediaQuery.sizeOf(context);
insetPadding = EdgeInsets.zero;
- shape = const RoundedRectangleBorder();
- elevation = 0;
+ elevation = datePickerTheme.rangePickerElevation ?? defaults.rangePickerElevation!;
+ shadowColor = datePickerTheme.rangePickerShadowColor ?? defaults.rangePickerShadowColor!;
+ surfaceTintColor = datePickerTheme.rangePickerSurfaceTintColor ?? defaults.rangePickerSurfaceTintColor!;
+ shape = datePickerTheme.rangePickerShape ?? defaults.rangePickerShape;
break;
case DatePickerEntryMode.input:
@@ -1416,35 +1418,46 @@
? IconButton(
icon: const Icon(Icons.calendar_today),
padding: EdgeInsets.zero,
- color: onPrimarySurface,
tooltip: localizations.calendarModeButtonLabel,
onPressed: _handleEntryModeToggle,
)
: null,
confirmText: widget.confirmText ?? localizations.okButtonLabel,
cancelText: widget.cancelText ?? (
- Theme.of(context).useMaterial3
+ useMaterial3
? localizations.cancelButtonLabel
: localizations.cancelButtonLabel.toUpperCase()
),
helpText: widget.helpText ?? (
- Theme.of(context).useMaterial3
+ useMaterial3
? localizations.dateRangePickerHelpText
: localizations.dateRangePickerHelpText.toUpperCase()
),
);
- final DialogTheme dialogTheme = Theme.of(context).dialogTheme;
- size = orientation == Orientation.portrait ? _inputPortraitDialogSize : _inputRangeLandscapeDialogSize;
+ final DialogTheme dialogTheme = theme.dialogTheme;
+ size = orientation == Orientation.portrait
+ ? (useMaterial3 ? _inputPortraitDialogSizeM3 : _inputPortraitDialogSizeM2)
+ : _inputRangeLandscapeDialogSize;
+ elevation = useMaterial3
+ ? datePickerTheme.elevation ?? defaults.elevation!
+ : datePickerTheme.elevation ?? dialogTheme.elevation ?? 24;
+ shadowColor = datePickerTheme.shadowColor ?? defaults.shadowColor;
+ surfaceTintColor = datePickerTheme.surfaceTintColor ?? defaults.surfaceTintColor;
+ shape = useMaterial3
+ ? datePickerTheme.shape ?? defaults.shape
+ : datePickerTheme.shape ?? dialogTheme.shape ?? defaults.shape;
+
insetPadding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0);
- shape = dialogTheme.shape;
- elevation = dialogTheme.elevation ?? 24;
break;
}
return Dialog(
insetPadding: insetPadding,
- shape: shape,
+ backgroundColor: datePickerTheme.backgroundColor ?? defaults.backgroundColor,
elevation: elevation,
+ shadowColor: shadowColor,
+ surfaceTintColor: surfaceTintColor,
+ shape: shape,
clipBehavior: Clip.antiAlias,
child: AnimatedContainer(
width: size.width,
@@ -1497,26 +1510,29 @@
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
+ final bool useMaterial3 = theme.useMaterial3;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final Orientation orientation = MediaQuery.of(context).orientation;
- final TextTheme textTheme = theme.textTheme;
- final Color headerForeground = colorScheme.brightness == Brightness.light
- ? colorScheme.onPrimary
- : colorScheme.onSurface;
- final Color headerDisabledForeground = headerForeground.withOpacity(0.38);
+ final Orientation orientation = MediaQuery.orientationOf(context);
+ final DatePickerThemeData themeData = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
+ final Color? dialogBackground = themeData.rangePickerBackgroundColor ?? defaults.rangePickerBackgroundColor;
+ final Color? headerForeground = themeData.rangePickerHeaderForegroundColor ?? defaults.rangePickerHeaderForegroundColor;
+ final Color? headerDisabledForeground = headerForeground?.withOpacity(0.38);
+ final TextStyle? headlineStyle = themeData.rangePickerHeaderHeadlineStyle ?? defaults.rangePickerHeaderHeadlineStyle;
+ final TextStyle? headlineHelpStyle = (themeData.rangePickerHeaderHelpStyle ?? defaults.rangePickerHeaderHelpStyle)?.apply(color: headerForeground);
final String startDateText = _formatRangeStartDate(localizations, selectedStartDate, selectedEndDate);
final String endDateText = _formatRangeEndDate(localizations, selectedStartDate, selectedEndDate, DateTime.now());
- final TextStyle? headlineStyle = textTheme.headlineSmall;
final TextStyle? startDateStyle = headlineStyle?.apply(
color: selectedStartDate != null ? headerForeground : headerDisabledForeground,
);
final TextStyle? endDateStyle = headlineStyle?.apply(
color: selectedEndDate != null ? headerForeground : headerDisabledForeground,
);
- final TextStyle saveButtonStyle = textTheme.labelLarge!.apply(
- color: onConfirm != null ? headerForeground : headerDisabledForeground,
+ final ButtonStyle buttonStyle = TextButton.styleFrom(
+ foregroundColor: headerForeground,
+ disabledForegroundColor: headerDisabledForeground
);
+ final IconThemeData iconTheme = IconThemeData(color: headerForeground);
return SafeArea(
top: false,
@@ -1524,6 +1540,11 @@
right: false,
child: Scaffold(
appBar: AppBar(
+ iconTheme: iconTheme,
+ actionsIconTheme: iconTheme,
+ elevation: useMaterial3 ? 0 : null,
+ scrolledUnderElevation: useMaterial3 ? 0 : null,
+ backgroundColor: useMaterial3 ? Colors.transparent : null,
leading: CloseButton(
onPressed: onCancel,
),
@@ -1531,15 +1552,16 @@
if (orientation == Orientation.landscape && entryModeButton != null)
entryModeButton!,
TextButton(
+ style: buttonStyle,
onPressed: onConfirm,
- child: Text(confirmText, style: saveButtonStyle),
+ child: Text(confirmText),
),
const SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 64),
child: Row(children: <Widget>[
- SizedBox(width: MediaQuery.of(context).size.width < 360 ? 42 : 72),
+ SizedBox(width: MediaQuery.sizeOf(context).width < 360 ? 42 : 72),
Expanded(
child: Semantics(
label: '$helpText $startDateText to $endDateText',
@@ -1547,12 +1569,7 @@
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- Text(
- helpText,
- style: textTheme.labelSmall!.apply(
- color: headerForeground,
- ),
- ),
+ Text(helpText, style: headlineHelpStyle),
const SizedBox(height: 8),
Row(
children: <Widget>[
@@ -1582,11 +1599,15 @@
if (orientation == Orientation.portrait && entryModeButton != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: entryModeButton,
+ child: IconTheme(
+ data: iconTheme,
+ child: entryModeButton!,
+ ),
),
]),
),
),
+ backgroundColor: dialogBackground,
body: _CalendarDateRangePicker(
initialStartDate: selectedStartDate,
initialEndDate: selectedEndDate,
@@ -1625,8 +1646,6 @@
required this.onEndDateChanged,
}) : initialStartDate = initialStartDate != null ? DateUtils.dateOnly(initialStartDate) : null,
initialEndDate = initialEndDate != null ? DateUtils.dateOnly(initialEndDate) : null,
- assert(firstDate != null),
- assert(lastDate != null),
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate),
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
@@ -2017,7 +2036,7 @@
return Container(
constraints: BoxConstraints(
- maxWidth: MediaQuery.of(context).orientation == Orientation.landscape
+ maxWidth: MediaQuery.orientationOf(context) == Orientation.landscape
? _maxCalendarWidthLandscape
: _maxCalendarWidthPortrait,
maxHeight: _monthItemRowHeight,
@@ -2066,10 +2085,9 @@
required this.dayChildWidth,
required this.edgeChildWidth,
required this.reverseCrossAxis,
- }) : assert(crossAxisCount != null && crossAxisCount > 0),
- assert(dayChildWidth != null && dayChildWidth >= 0),
- assert(edgeChildWidth != null && edgeChildWidth >= 0),
- assert(reverseCrossAxis != null);
+ }) : assert(crossAxisCount > 0),
+ assert(dayChildWidth >= 0),
+ assert(edgeChildWidth >= 0);
/// The number of children in the cross axis.
final int crossAxisCount;
@@ -2160,19 +2178,12 @@
required this.firstDate,
required this.lastDate,
required this.displayedMonth,
- this.dragStartBehavior = DragStartBehavior.start,
- }) : assert(firstDate != null),
- assert(lastDate != null),
- assert(!firstDate.isAfter(lastDate)),
+ }) : assert(!firstDate.isAfter(lastDate)),
assert(selectedDateStart == null || !selectedDateStart.isBefore(firstDate)),
assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
assert(selectedDateStart == null || !selectedDateStart.isAfter(lastDate)),
assert(selectedDateEnd == null || !selectedDateEnd.isAfter(lastDate)),
- assert(selectedDateStart == null || selectedDateEnd == null || !selectedDateStart.isAfter(selectedDateEnd)),
- assert(currentDate != null),
- assert(onChanged != null),
- assert(displayedMonth != null),
- assert(dragStartBehavior != null);
+ assert(selectedDateStart == null || selectedDateEnd == null || !selectedDateStart.isAfter(selectedDateEnd));
/// The currently selected start date.
///
@@ -2199,25 +2210,6 @@
/// The month whose days are displayed by this picker.
final DateTime displayedMonth;
- /// Determines the way that drag start behavior is handled.
- ///
- /// If set to [DragStartBehavior.start], the drag gesture used to scroll a
- /// date picker wheel will begin at the position where the drag gesture won
- /// the arena. If set to [DragStartBehavior.down] it will begin at the position
- /// where a down event is first detected.
- ///
- /// In general, setting this to [DragStartBehavior.start] will make drag
- /// animation smoother and setting it to [DragStartBehavior.down] will make
- /// drag behavior feel slightly more reactive.
- ///
- /// By default, the drag start behavior is [DragStartBehavior.start].
- ///
- /// See also:
- ///
- /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
- /// the different behaviors.
- final DragStartBehavior dragStartBehavior;
-
@override
_MonthItemState createState() => _MonthItemState();
}
@@ -2255,7 +2247,8 @@
}
Color _highlightColor(BuildContext context) {
- return Theme.of(context).colorScheme.primary.withOpacity(0.12);
+ return DatePickerTheme.of(context).rangeSelectionBackgroundColor
+ ?? DatePickerTheme.defaults(context).rangeSelectionBackgroundColor!;
}
void _dayFocusChanged(bool focused) {
@@ -2286,6 +2279,8 @@
final ColorScheme colorScheme = theme.colorScheme;
final TextTheme textTheme = theme.textTheme;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
+ final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
final TextDirection textDirection = Directionality.of(context);
final Color highlightColor = _highlightColor(context);
final int day = dayToBuild.day;
@@ -2302,14 +2297,42 @@
dayToBuild.isAfter(widget.selectedDateStart!) &&
dayToBuild.isBefore(widget.selectedDateEnd!);
+ T? effectiveValue<T>(T? Function(DatePickerThemeData? theme) getProperty) {
+ return getProperty(datePickerTheme) ?? getProperty(defaults);
+ }
+
+ T? resolve<T>(MaterialStateProperty<T>? Function(DatePickerThemeData? theme) getProperty, Set<MaterialState> states) {
+ return effectiveValue(
+ (DatePickerThemeData? theme) {
+ return getProperty(theme)?.resolve(states);
+ },
+ );
+ }
+
+ final Set<MaterialState> states = <MaterialState>{
+ if (isDisabled) MaterialState.disabled,
+ if (isSelectedDayStart || isSelectedDayEnd) MaterialState.selected,
+ };
+
+ final Color? dayForegroundColor = resolve<Color?>((DatePickerThemeData? theme) => theme?.dayForegroundColor, states);
+ final Color? dayBackgroundColor = resolve<Color?>((DatePickerThemeData? theme) => theme?.dayBackgroundColor, states);
+ final MaterialStateProperty<Color?> dayOverlayColor = MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) => effectiveValue(
+ (DatePickerThemeData? theme) =>
+ isInRange
+ ? theme?.rangeSelectionOverlayColor?.resolve(states)
+ : theme?.dayOverlayColor?.resolve(states),
+ )
+ );
+
_HighlightPainter? highlightPainter;
if (isSelectedDayStart || isSelectedDayEnd) {
// The selected start and end dates gets a circle background
// highlight, and a contrasting text color.
- itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onPrimary);
+ itemStyle = textTheme.bodyMedium?.apply(color: dayForegroundColor);
decoration = BoxDecoration(
- color: colorScheme.primary,
+ color: dayBackgroundColor,
shape: BoxShape.circle,
);
@@ -2348,7 +2371,8 @@
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
- String semanticLabel = '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}';
+ final String semanticLabelSuffix = DateUtils.isSameDay(widget.currentDate, dayToBuild) ? ', ${localizations.currentDateLabel}' : '';
+ String semanticLabel = '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}$semanticLabelSuffix';
if (isSelectedDayStart) {
semanticLabel = localizations.dateRangeStartDateSemanticLabel(semanticLabel);
} else if (isSelectedDayEnd) {
@@ -2380,7 +2404,8 @@
focusNode: _dayFocusNodes[day - 1],
onTap: () => widget.onChanged(dayToBuild),
radius: _monthItemRowHeight / 2 + 4,
- splashColor: colorScheme.primary.withOpacity(0.38),
+ statesController: MaterialStatesController(states),
+ overlayColor: dayOverlayColor,
onFocusChange: _dayFocusChanged,
child: dayWidget,
);
@@ -2403,8 +2428,7 @@
final int daysInMonth = DateUtils.getDaysInMonth(year, month);
final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil();
- final double gridHeight =
- weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
+ final double gridHeight = weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
final List<Widget> dayItems = <Widget>[];
for (int i = 0; true; i += 1) {
@@ -2468,7 +2492,7 @@
paddedDayItems.addAll(weekList);
}
- final double maxWidth = MediaQuery.of(context).orientation == Orientation.landscape
+ final double maxWidth = MediaQuery.orientationOf(context) == Orientation.landscape
? _maxCalendarWidthLandscape
: _maxCalendarWidthPortrait;
return Column(
@@ -2619,18 +2643,17 @@
@override
Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
+ final bool useMaterial3 = Theme.of(context).useMaterial3;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final Orientation orientation = MediaQuery.of(context).orientation;
- final TextTheme textTheme = theme.textTheme;
+ final Orientation orientation = MediaQuery.orientationOf(context);
+ final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
+ final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
- final Color onPrimarySurfaceColor = colorScheme.brightness == Brightness.light
- ? colorScheme.onPrimary
- : colorScheme.onSurface;
- final TextStyle? dateStyle = orientation == Orientation.landscape
- ? textTheme.headlineSmall?.apply(color: onPrimarySurfaceColor)
- : textTheme.headlineMedium?.apply(color: onPrimarySurfaceColor);
+ final Color? headerForegroundColor = datePickerTheme.headerForegroundColor ?? defaults.headerForegroundColor;
+ final TextStyle? headlineStyle = (datePickerTheme.headerHeadlineStyle ?? defaults.headerHeadlineStyle)?.copyWith(
+ color: headerForegroundColor,
+ );
+
final String dateText = _formatDateRange(context, selectedStartDate, selectedEndDate, currentDate!);
final String semanticDateText = selectedStartDate != null && selectedEndDate != null
? '${localizations.formatMediumDate(selectedStartDate!)} – ${localizations.formatMediumDate(selectedEndDate!)}'
@@ -2638,13 +2661,13 @@
final Widget header = _DatePickerHeader(
helpText: helpText ?? (
- Theme.of(context).useMaterial3
+ useMaterial3
? localizations.dateRangePickerHelpText
: localizations.dateRangePickerHelpText.toUpperCase()
),
titleText: dateText,
titleSemanticsLabel: semanticDateText,
- titleStyle: dateStyle,
+ titleStyle: headlineStyle,
orientation: orientation,
isShort: orientation == Orientation.landscape,
entryModeButton: entryModeButton,
@@ -2660,7 +2683,7 @@
TextButton(
onPressed: onCancel,
child: Text(cancelText ?? (
- theme.useMaterial3
+ useMaterial3
? localizations.cancelButtonLabel
: localizations.cancelButtonLabel.toUpperCase()
)),
@@ -2732,14 +2755,8 @@
this.autovalidate = false,
}) : initialStartDate = initialStartDate == null ? null : DateUtils.dateOnly(initialStartDate),
initialEndDate = initialEndDate == null ? null : DateUtils.dateOnly(initialEndDate),
- assert(firstDate != null),
firstDate = DateUtils.dateOnly(firstDate),
- assert(lastDate != null),
- lastDate = DateUtils.dateOnly(lastDate),
- assert(firstDate != null),
- assert(lastDate != null),
- assert(autofocus != null),
- assert(autovalidate != null);
+ lastDate = DateUtils.dateOnly(lastDate);
/// The [DateTime] that represents the start of the initial date range selection.
final DateTime? initialStartDate;
@@ -2915,8 +2932,13 @@
@override
Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ final bool useMaterial3 = theme.useMaterial3;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme;
+ final InputDecorationTheme inputTheme = theme.inputDecorationTheme;
+ final InputBorder inputBorder = inputTheme.border
+ ?? (useMaterial3 ? const OutlineInputBorder() : const UnderlineInputBorder());
+
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@@ -2924,7 +2946,7 @@
child: TextField(
controller: _startController,
decoration: InputDecoration(
- border: inputTheme.border ?? const UnderlineInputBorder(),
+ border: inputBorder,
filled: inputTheme.filled,
hintText: widget.fieldStartHintText ?? localizations.dateHelpText,
labelText: widget.fieldStartLabelText ?? localizations.dateRangeStartLabel,
@@ -2940,7 +2962,7 @@
child: TextField(
controller: _endController,
decoration: InputDecoration(
- border: inputTheme.border ?? const UnderlineInputBorder(),
+ border: inputBorder,
filled: inputTheme.filled,
hintText: widget.fieldEndHintText ?? localizations.dateHelpText,
labelText: widget.fieldEndLabelText ?? localizations.dateRangeEndLabel,
diff --git a/framework/lib/src/material/date_picker_theme.dart b/framework/lib/src/material/date_picker_theme.dart
new file mode 100644
index 0000000..c9a394f
--- /dev/null
+++ b/framework/lib/src/material/date_picker_theme.dart
@@ -0,0 +1,975 @@
+// 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:engine/ui.dart' show lerpDouble;
+
+import 'package:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'color_scheme.dart';
+import 'colors.dart';
+import 'material_state.dart';
+import 'text_theme.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// late BuildContext context;
+
+/// Overrides the default values of visual properties for descendant
+/// [DatePickerDialog] widgets.
+///
+/// Descendant widgets obtain the current [DatePickerThemeData] object with
+/// [DatePickerTheme.of]. Instances of [DatePickerTheme] can
+/// be customized with [DatePickerThemeData.copyWith].
+///
+/// Typically a [DatePickerTheme] is specified as part of the overall
+/// [Theme] with [ThemeData.datePickerTheme].
+///
+/// All [DatePickerThemeData] properties are null by default. When null,
+/// the [DatePickerDialog] computes its own default values, typically based on
+/// the overall theme's [ThemeData.colorScheme], [ThemeData.textTheme], and
+/// [ThemeData.iconTheme].
+@immutable
+class DatePickerThemeData with Diagnosticable {
+ /// Creates a [DatePickerThemeData] that can be used to override default properties
+ /// in a [DatePickerTheme] widget.
+ const DatePickerThemeData({
+ this.backgroundColor,
+ this.elevation,
+ this.shadowColor,
+ this.surfaceTintColor,
+ this.shape,
+ this.headerBackgroundColor,
+ this.headerForegroundColor,
+ this.headerHeadlineStyle,
+ this.headerHelpStyle,
+ this.weekdayStyle,
+ this.dayStyle,
+ this.dayForegroundColor,
+ this.dayBackgroundColor,
+ this.dayOverlayColor,
+ this.todayForegroundColor,
+ this.todayBackgroundColor,
+ this.todayBorder,
+ this.yearStyle,
+ this.yearForegroundColor,
+ this.yearBackgroundColor,
+ this.yearOverlayColor,
+ this.rangePickerBackgroundColor,
+ this.rangePickerElevation,
+ this.rangePickerShadowColor,
+ this.rangePickerSurfaceTintColor,
+ this.rangePickerShape,
+ this.rangePickerHeaderBackgroundColor,
+ this.rangePickerHeaderForegroundColor,
+ this.rangePickerHeaderHeadlineStyle,
+ this.rangePickerHeaderHelpStyle,
+ this.rangeSelectionBackgroundColor,
+ this.rangeSelectionOverlayColor,
+ });
+
+ /// Overrides the default value of [Dialog.backgroundColor].
+ final Color? backgroundColor;
+
+ /// Overrides the default value of [Dialog.elevation].
+ ///
+ /// See also:
+ /// [Material.elevation], which explains how elevation is related to a component's shadow.
+ final double? elevation;
+
+ /// Overrides the default value of [Dialog.shadowColor].
+ ///
+ /// See also:
+ /// [Material.shadowColor], which explains how the shadow is rendered.
+ final Color? shadowColor;
+
+ /// Overrides the default value of [Dialog.surfaceTintColor].
+ ///
+ /// See also:
+ /// [Material.surfaceTintColor], which explains how this color is related to
+ /// [elevation] and [backgroundColor].
+ final Color? surfaceTintColor;
+
+ /// Overrides the default value of [Dialog.shape].
+ ///
+ /// If [elevation] is greater than zero then a shadow is shown and the shadow's
+ /// shape mirrors the shape of the dialog.
+ final ShapeBorder? shape;
+
+ /// Overrides the header's default background fill color.
+ ///
+ /// The dialog's header displays the currently selected date.
+ final Color? headerBackgroundColor;
+
+ /// Overrides the header's default color used for text labels and icons.
+ ///
+ /// The dialog's header displays the currently selected date.
+ ///
+ /// This is used instead of the [TextStyle.color] property of [headerHeadlineStyle]
+ /// and [headerHelpStyle].
+ final Color? headerForegroundColor;
+
+ /// Overrides the header's default headline text style.
+ ///
+ /// The dialog's header displays the currently selected date.
+ ///
+ /// The [TextStyle.color] of the [headerHeadlineStyle] is not used,
+ /// [headerForegroundColor] is used instead.
+ final TextStyle? headerHeadlineStyle;
+
+ /// Overrides the header's default help text style.
+ ///
+ /// The help text (also referred to as "supporting text" in the Material
+ /// spec) is usually a prompt to the user at the top of the header
+ /// (i.e. 'Select date').
+ ///
+ /// The [TextStyle.color] of the [headerHelpStyle] is not used,
+ /// [headerForegroundColor] is used instead.
+ ///
+ /// See also:
+ /// [DatePickerDialog.helpText], which specifies the help text.
+ final TextStyle? headerHelpStyle;
+
+ /// Overrides the default text style used for the row of weekday
+ /// labels at the top of the date picker grid.
+ final TextStyle? weekdayStyle;
+
+ /// Overrides the default text style used for each individual day
+ /// label in the grid of the date picker.
+ ///
+ /// The [TextStyle.color] of the [dayStyle] is not used,
+ /// [dayForegroundColor] is used instead.
+ final TextStyle? dayStyle;
+
+ /// Overrides the default color used to paint the day labels in the
+ /// grid of the date picker.
+ ///
+ /// This will be used instead of the color provided in [dayStyle].
+ final MaterialStateProperty<Color?>? dayForegroundColor;
+
+ /// Overrides the default color used to paint the background of the
+ /// day labels in the grid of the date picker.
+ final MaterialStateProperty<Color?>? dayBackgroundColor;
+
+ /// Overriddes 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;
+
+ /// Overrides the default color used to paint the
+ /// [DatePickerDialog.currentDate] label in the grid of the dialog's
+ /// [CalendarDatePicker] and the corresponding year in the dialog's
+ /// [YearPicker].
+ ///
+ /// This will be used instead of the [TextStyle.color] provided in [dayStyle].
+ final MaterialStateProperty<Color?>? todayForegroundColor;
+
+ /// Overrides the default color used to paint the background of the
+ /// [DatePickerDialog.currentDate] label in the grid of the date picker.
+ final MaterialStateProperty<Color?>? todayBackgroundColor;
+
+ /// Overrides the border used to paint the
+ /// [DatePickerDialog.currentDate] label in the grid of the date
+ /// picker.
+ ///
+ /// The border side's [BorderSide.color] is not used,
+ /// [todayForegroundColor] is used instead.
+ final BorderSide? todayBorder;
+
+ /// Overrides the default text style used to paint each of the year
+ /// entries in the year selector of the date picker.
+ ///
+ /// The [TextStyle.color] of the [yearStyle] is not used,
+ /// [yearForegroundColor] is used instead.
+ final TextStyle? yearStyle;
+
+ /// Overrides the default color used to paint the year labels in the year
+ /// selector of the date picker.
+ ///
+ /// This will be used instead of the color provided in [yearStyle].
+ final MaterialStateProperty<Color?>? yearForegroundColor;
+
+ /// Overrides the default color used to paint the background of the
+ /// year labels in the year selector of the of the date picker.
+ final MaterialStateProperty<Color?>? yearBackgroundColor;
+
+ /// Overrides the default highlight color that's typically used to
+ /// indicate that a year in the year selector is focused, hovered,
+ /// or pressed.
+ final MaterialStateProperty<Color?>? yearOverlayColor;
+
+ /// Overrides the default [Scaffold.backgroundColor] for
+ /// [DateRangePickerDialog].
+ final Color? rangePickerBackgroundColor;
+
+ /// Overrides the default elevation of the full screen
+ /// [DateRangePickerDialog].
+ ///
+ /// See also:
+ /// [Material.elevation], which explains how elevation is related to a component's shadow.
+ final double? rangePickerElevation;
+
+ /// Overrides the color of the shadow painted below a full screen
+ /// [DateRangePickerDialog].
+ ///
+ /// See also:
+ /// [Material.shadowColor], which explains how the shadow is rendered.
+ final Color? rangePickerShadowColor;
+
+ /// Overrides the default color of the surface tint overlay applied
+ /// to the [backgroundColor] of a full screen
+ /// [DateRangePickerDialog]'s to indicate elevation.
+ ///
+ /// See also:
+ /// [Material.surfaceTintColor], which explains how this color is related to
+ /// [elevation].
+ final Color? rangePickerSurfaceTintColor;
+
+ /// Overrides the default overall shape of a full screen
+ /// [DateRangePickerDialog].
+ ///
+ /// If [elevation] is greater than zero then a shadow is shown and the shadow's
+ /// shape mirrors the shape of the dialog.
+ ///
+ /// [Material.surfaceTintColor], which explains how this color is related to
+ /// [elevation].
+ final ShapeBorder? rangePickerShape;
+
+ /// Overrides the default background fill color for [DateRangePickerDialog].
+ ///
+ /// The dialog's header displays the currently selected date range.
+ final Color? rangePickerHeaderBackgroundColor;
+
+ /// Overrides the default color used for text labels and icons in
+ /// the header of a full screen [DateRangePickerDialog]
+ ///
+ /// The dialog's header displays the currently selected date range.
+ ///
+ /// This is used instead of any colors provided by
+ /// [rangePickerHeaderHeadlineStyle] or [rangePickerHeaderHelpStyle].
+ final Color? rangePickerHeaderForegroundColor;
+
+ /// Overrides the default text style used for the headline text in
+ /// the header of a full screen [DateRangePickerDialog].
+ ///
+ /// The dialog's header displays the currently selected date range.
+ ///
+ /// The [TextStyle.color] of [rangePickerHeaderHeadlineStyle] is not used,
+ /// [rangePickerHeaderForegroundColor] is used instead.
+ final TextStyle? rangePickerHeaderHeadlineStyle;
+
+ /// Overrides the default text style used for the help text of the
+ /// header of a full screen [DateRangePickerDialog].
+ ///
+ /// The help text (also referred to as "supporting text" in the Material
+ /// spec) is usually a prompt to the user at the top of the header
+ /// (i.e. 'Select date').
+ ///
+ /// The [TextStyle.color] of the [rangePickerHeaderHelpStyle] is not used,
+ /// [rangePickerHeaderForegroundColor] is used instead.
+ ///
+ /// See also:
+ /// [DateRangePickerDialog.helpText], which specifies the help text.
+ final TextStyle? rangePickerHeaderHelpStyle;
+
+ /// Overrides the default background color used to paint days
+ /// selected between the start and end dates in a
+ /// [DateRangePickerDialog].
+ final Color? rangeSelectionBackgroundColor;
+
+ /// Overrides the default highlight color that's typically used to
+ /// indicate that a date in the selected range of a
+ /// [DateRangePickerDialog] is focused, hovered, or pressed.
+ final MaterialStateProperty<Color?>? rangeSelectionOverlayColor;
+
+ /// Creates a copy of this object with the given fields replaced with the
+ /// new values.
+ DatePickerThemeData copyWith({
+ Color? backgroundColor,
+ double? elevation,
+ Color? shadowColor,
+ Color? surfaceTintColor,
+ ShapeBorder? shape,
+ Color? headerBackgroundColor,
+ Color? headerForegroundColor,
+ TextStyle? headerHeadlineStyle,
+ TextStyle? headerHelpStyle,
+ TextStyle? weekdayStyle,
+ TextStyle? dayStyle,
+ MaterialStateProperty<Color?>? dayForegroundColor,
+ MaterialStateProperty<Color?>? dayBackgroundColor,
+ MaterialStateProperty<Color?>? dayOverlayColor,
+ MaterialStateProperty<Color?>? todayForegroundColor,
+ MaterialStateProperty<Color?>? todayBackgroundColor,
+ BorderSide? todayBorder,
+ TextStyle? yearStyle,
+ MaterialStateProperty<Color?>? yearForegroundColor,
+ MaterialStateProperty<Color?>? yearBackgroundColor,
+ MaterialStateProperty<Color?>? yearOverlayColor,
+ Color? rangePickerBackgroundColor,
+ double? rangePickerElevation,
+ Color? rangePickerShadowColor,
+ Color? rangePickerSurfaceTintColor,
+ ShapeBorder? rangePickerShape,
+ Color? rangePickerHeaderBackgroundColor,
+ Color? rangePickerHeaderForegroundColor,
+ TextStyle? rangePickerHeaderHeadlineStyle,
+ TextStyle? rangePickerHeaderHelpStyle,
+ Color? rangeSelectionBackgroundColor,
+ MaterialStateProperty<Color?>? rangeSelectionOverlayColor,
+ }) {
+ return DatePickerThemeData(
+ backgroundColor: backgroundColor ?? this.backgroundColor,
+ elevation: elevation ?? this.elevation,
+ shadowColor: shadowColor ?? this.shadowColor,
+ surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
+ shape: shape ?? this.shape,
+ headerBackgroundColor: headerBackgroundColor ?? this.headerBackgroundColor,
+ headerForegroundColor: headerForegroundColor ?? this.headerForegroundColor,
+ headerHeadlineStyle: headerHeadlineStyle ?? this.headerHeadlineStyle,
+ headerHelpStyle: headerHelpStyle ?? this.headerHelpStyle,
+ weekdayStyle: weekdayStyle ?? this.weekdayStyle,
+ dayStyle: dayStyle ?? this.dayStyle,
+ dayForegroundColor: dayForegroundColor ?? this.dayForegroundColor,
+ dayBackgroundColor: dayBackgroundColor ?? this.dayBackgroundColor,
+ dayOverlayColor: dayOverlayColor ?? this.dayOverlayColor,
+ todayForegroundColor: todayForegroundColor ?? this.todayForegroundColor,
+ todayBackgroundColor: todayBackgroundColor ?? this.todayBackgroundColor,
+ todayBorder: todayBorder ?? this.todayBorder,
+ yearStyle: yearStyle ?? this.yearStyle,
+ yearForegroundColor: yearForegroundColor ?? this.yearForegroundColor,
+ yearBackgroundColor: yearBackgroundColor ?? this.yearBackgroundColor,
+ yearOverlayColor: yearOverlayColor ?? this.yearOverlayColor,
+ rangePickerBackgroundColor: rangePickerBackgroundColor ?? this.rangePickerBackgroundColor,
+ rangePickerElevation: rangePickerElevation ?? this.rangePickerElevation,
+ rangePickerShadowColor: rangePickerShadowColor ?? this.rangePickerShadowColor,
+ rangePickerSurfaceTintColor: rangePickerSurfaceTintColor ?? this.rangePickerSurfaceTintColor,
+ rangePickerShape: rangePickerShape ?? this.rangePickerShape,
+ rangePickerHeaderBackgroundColor: rangePickerHeaderBackgroundColor ?? this.rangePickerHeaderBackgroundColor,
+ rangePickerHeaderForegroundColor: rangePickerHeaderForegroundColor ?? this.rangePickerHeaderForegroundColor,
+ rangePickerHeaderHeadlineStyle: rangePickerHeaderHeadlineStyle ?? this.rangePickerHeaderHeadlineStyle,
+ rangePickerHeaderHelpStyle: rangePickerHeaderHelpStyle ?? this.rangePickerHeaderHelpStyle,
+ rangeSelectionBackgroundColor: rangeSelectionBackgroundColor ?? this.rangeSelectionBackgroundColor,
+ rangeSelectionOverlayColor: rangeSelectionOverlayColor ?? this.rangeSelectionOverlayColor,
+ );
+ }
+
+ /// Linearly interpolates between two [DatePickerThemeData].
+ static DatePickerThemeData lerp(DatePickerThemeData? a, DatePickerThemeData? b, double t) {
+ return DatePickerThemeData(
+ backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
+ elevation: lerpDouble(a?.elevation, b?.elevation, t),
+ shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
+ surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
+ shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
+ headerBackgroundColor: Color.lerp(a?.headerBackgroundColor, b?.headerBackgroundColor, t),
+ headerForegroundColor: Color.lerp(a?.headerForegroundColor, b?.headerForegroundColor, t),
+ headerHeadlineStyle: TextStyle.lerp(a?.headerHeadlineStyle, b?.headerHeadlineStyle, t),
+ headerHelpStyle: TextStyle.lerp(a?.headerHelpStyle, b?.headerHelpStyle, t),
+ weekdayStyle: TextStyle.lerp(a?.weekdayStyle, b?.weekdayStyle, t),
+ dayStyle: TextStyle.lerp(a?.dayStyle, b?.dayStyle, t),
+ dayForegroundColor: MaterialStateProperty.lerp<Color?>(a?.dayForegroundColor, b?.dayForegroundColor, t, Color.lerp),
+ dayBackgroundColor: MaterialStateProperty.lerp<Color?>(a?.dayBackgroundColor, b?.dayBackgroundColor, t, Color.lerp),
+ dayOverlayColor: MaterialStateProperty.lerp<Color?>(a?.dayOverlayColor, b?.dayOverlayColor, t, Color.lerp),
+ todayForegroundColor: MaterialStateProperty.lerp<Color?>(a?.todayForegroundColor, b?.todayForegroundColor, t, Color.lerp),
+ todayBackgroundColor: MaterialStateProperty.lerp<Color?>(a?.todayBackgroundColor, b?.todayBackgroundColor, t, Color.lerp),
+ todayBorder: _lerpBorderSide(a?.todayBorder, b?.todayBorder, t),
+ yearStyle: TextStyle.lerp(a?.yearStyle, b?.yearStyle, t),
+ yearForegroundColor: MaterialStateProperty.lerp<Color?>(a?.yearForegroundColor, b?.yearForegroundColor, t, Color.lerp),
+ yearBackgroundColor: MaterialStateProperty.lerp<Color?>(a?.yearBackgroundColor, b?.yearBackgroundColor, t, Color.lerp),
+ yearOverlayColor: MaterialStateProperty.lerp<Color?>(a?.yearOverlayColor, b?.yearOverlayColor, t, Color.lerp),
+ rangePickerBackgroundColor: Color.lerp(a?.rangePickerBackgroundColor, b?.rangePickerBackgroundColor, t),
+ rangePickerElevation: lerpDouble(a?.rangePickerElevation, b?.rangePickerElevation, t),
+ rangePickerShadowColor: Color.lerp(a?.rangePickerShadowColor, b?.rangePickerShadowColor, t),
+ rangePickerSurfaceTintColor: Color.lerp(a?.rangePickerSurfaceTintColor, b?.rangePickerSurfaceTintColor, t),
+ rangePickerShape: ShapeBorder.lerp(a?.rangePickerShape, b?.rangePickerShape, t),
+ rangePickerHeaderBackgroundColor: Color.lerp(a?.rangePickerHeaderBackgroundColor, b?.rangePickerHeaderBackgroundColor, t),
+ rangePickerHeaderForegroundColor: Color.lerp(a?.rangePickerHeaderForegroundColor, b?.rangePickerHeaderForegroundColor, t),
+ rangePickerHeaderHeadlineStyle: TextStyle.lerp(a?.rangePickerHeaderHeadlineStyle, b?.rangePickerHeaderHeadlineStyle, t),
+ rangePickerHeaderHelpStyle: TextStyle.lerp(a?.rangePickerHeaderHelpStyle, b?.rangePickerHeaderHelpStyle, t),
+ rangeSelectionBackgroundColor: Color.lerp(a?.rangeSelectionBackgroundColor, b?.rangeSelectionBackgroundColor, t),
+ rangeSelectionOverlayColor: MaterialStateProperty.lerp<Color?>(a?.rangeSelectionOverlayColor, b?.rangeSelectionOverlayColor, t, Color.lerp),
+ );
+ }
+
+ static BorderSide? _lerpBorderSide(BorderSide? a, BorderSide? b, double t) {
+ if (a == null && b == null) {
+ return null;
+ }
+ if (a == null) {
+ return BorderSide.lerp(BorderSide(width: 0, color: b!.color.withAlpha(0)), b, t);
+ }
+ return BorderSide.lerp(a, BorderSide(width: 0, color: a.color.withAlpha(0)), t);
+ }
+
+ @override
+ int get hashCode => Object.hashAll(<Object?>[
+ backgroundColor,
+ elevation,
+ shadowColor,
+ surfaceTintColor,
+ shape,
+ headerBackgroundColor,
+ headerForegroundColor,
+ headerHeadlineStyle,
+ headerHelpStyle,
+ weekdayStyle,
+ dayStyle,
+ dayForegroundColor,
+ dayBackgroundColor,
+ dayOverlayColor,
+ todayForegroundColor,
+ todayBackgroundColor,
+ todayBorder,
+ yearStyle,
+ yearForegroundColor,
+ yearBackgroundColor,
+ yearOverlayColor,
+ rangePickerBackgroundColor,
+ rangePickerElevation,
+ rangePickerShadowColor,
+ rangePickerSurfaceTintColor,
+ rangePickerShape,
+ rangePickerHeaderBackgroundColor,
+ rangePickerHeaderForegroundColor,
+ rangePickerHeaderHeadlineStyle,
+ rangePickerHeaderHelpStyle,
+ rangeSelectionBackgroundColor,
+ rangeSelectionOverlayColor,
+ ]);
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ return other is DatePickerThemeData
+ && other.backgroundColor == backgroundColor
+ && other.elevation == elevation
+ && other.shadowColor == shadowColor
+ && other.surfaceTintColor == surfaceTintColor
+ && other.shape == shape
+ && other.headerBackgroundColor == headerBackgroundColor
+ && other.headerForegroundColor == headerForegroundColor
+ && other.headerHeadlineStyle == headerHeadlineStyle
+ && other.headerHelpStyle == headerHelpStyle
+ && other.weekdayStyle == weekdayStyle
+ && other.dayStyle == dayStyle
+ && other.dayForegroundColor == dayForegroundColor
+ && other.dayBackgroundColor == dayBackgroundColor
+ && other.dayOverlayColor == dayOverlayColor
+ && other.todayForegroundColor == todayForegroundColor
+ && other.todayBackgroundColor == todayBackgroundColor
+ && other.todayBorder == todayBorder
+ && other.yearStyle == yearStyle
+ && other.yearForegroundColor == yearForegroundColor
+ && other.yearBackgroundColor == yearBackgroundColor
+ && other.yearOverlayColor == yearOverlayColor
+ && other.rangePickerBackgroundColor == rangePickerBackgroundColor
+ && other.rangePickerElevation == rangePickerElevation
+ && other.rangePickerShadowColor == rangePickerShadowColor
+ && other.rangePickerSurfaceTintColor == rangePickerSurfaceTintColor
+ && other.rangePickerShape == rangePickerShape
+ && other.rangePickerHeaderBackgroundColor == rangePickerHeaderBackgroundColor
+ && other.rangePickerHeaderForegroundColor == rangePickerHeaderForegroundColor
+ && other.rangePickerHeaderHeadlineStyle == rangePickerHeaderHeadlineStyle
+ && other.rangePickerHeaderHelpStyle == rangePickerHeaderHelpStyle
+ && other.rangeSelectionBackgroundColor == rangeSelectionBackgroundColor
+ && other.rangeSelectionOverlayColor == rangeSelectionOverlayColor;
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
+ properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
+ properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
+ properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
+ properties.add(ColorProperty('headerBackgroundColor', headerBackgroundColor, defaultValue: null));
+ properties.add(ColorProperty('headerForegroundColor', headerForegroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('headerHeadlineStyle', headerHeadlineStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('headerHelpStyle', headerHelpStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('weekDayStyle', weekdayStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('dayStyle', dayStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dayForegroundColor', dayForegroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dayBackgroundColor', dayBackgroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dayOverlayColor', dayOverlayColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('todayForegroundColor', todayForegroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('todayBackgroundColor', todayBackgroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<BorderSide?>('todayBorder', todayBorder, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('yearStyle', yearStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('yearForegroundColor', yearForegroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('yearBackgroundColor', yearBackgroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('yearOverlayColor', yearOverlayColor, defaultValue: null));
+ properties.add(ColorProperty('rangePickerBackgroundColor', rangePickerBackgroundColor, defaultValue: null));
+ properties.add(DoubleProperty('rangePickerElevation', rangePickerElevation, defaultValue: null));
+ properties.add(ColorProperty('rangePickerShadowColor', rangePickerShadowColor, defaultValue: null));
+ properties.add(ColorProperty('rangePickerSurfaceTintColor', rangePickerSurfaceTintColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<ShapeBorder>('rangePickerShape', rangePickerShape, defaultValue: null));
+ properties.add(ColorProperty('rangePickerHeaderBackgroundColor', rangePickerHeaderBackgroundColor, defaultValue: null));
+ properties.add(ColorProperty('rangePickerHeaderForegroundColor', rangePickerHeaderForegroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('rangePickerHeaderHeadlineStyle', rangePickerHeaderHeadlineStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('rangePickerHeaderHelpStyle', rangePickerHeaderHelpStyle, defaultValue: null));
+ properties.add(ColorProperty('rangeSelectionBackgroundColor', rangeSelectionBackgroundColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('rangeSelectionOverlayColor', rangeSelectionOverlayColor, defaultValue: null));
+ }
+}
+
+/// An inherited widget that defines the visual properties for
+/// [DatePickerDialog]s in this widget's subtree.
+///
+/// Values specified here are used for [DatePickerDialog] properties that are not
+/// given an explicit non-null value.
+class DatePickerTheme extends InheritedTheme {
+ /// Creates a [DatePickerTheme] that controls visual parameters for
+ /// descendent [DatePickerDialog]s.
+ const DatePickerTheme({
+ super.key,
+ required this.data,
+ required super.child,
+ });
+
+ /// Specifies the visual properties used by descendant [DatePickerDialog]
+ /// widgets.
+ final DatePickerThemeData data;
+
+ /// The [data] from the closest instance of this class that encloses the given
+ /// context.
+ ///
+ /// If there is no [DatePickerTheme] in scope, this will return
+ /// [ThemeData.datePickerTheme] from the ambient [Theme].
+ ///
+ /// Typical usage is as follows:
+ ///
+ /// ```dart
+ /// DatePickerThemeData theme = DatePickerTheme.of(context);
+ /// ```
+ ///
+ /// See also:
+ ///
+ /// * [maybeOf], which returns null if it doesn't find a
+ /// [DatePickerTheme] ancestor.
+ /// * [defaults], which will return the default properties used when no
+ /// other [DatePickerTheme] has been provided.
+ static DatePickerThemeData of(BuildContext context) {
+ return maybeOf(context) ?? Theme.of(context).datePickerTheme;
+ }
+
+ /// The data from the closest instance of this class that encloses the given
+ /// context, if any.
+ ///
+ /// Use this function if you want to allow situations where no
+ /// [DatePickerTheme] is in scope. Prefer using [DatePickerTheme.of]
+ /// in situations where a [DatePickerThemeData] is expected to be
+ /// non-null.
+ ///
+ /// If there is no [DatePickerTheme] in scope, then this function will
+ /// return null.
+ ///
+ /// Typical usage is as follows:
+ ///
+ /// ```dart
+ /// DatePickerThemeData? theme = DatePickerTheme.maybeOf(context);
+ /// if (theme == null) {
+ /// // Do something else instead.
+ /// }
+ /// ```
+ ///
+ /// See also:
+ ///
+ /// * [of], which will return [ThemeData.datePickerTheme] if it doesn't
+ /// find a [DatePickerTheme] ancestor, instead of returning null.
+ /// * [defaults], which will return the default properties used when no
+ /// other [DatePickerTheme] has been provided.
+ static DatePickerThemeData? maybeOf(BuildContext context) {
+ return context.dependOnInheritedWidgetOfExactType<DatePickerTheme>()?.data;
+ }
+
+ /// A DatePickerThemeData used as the default properties for date pickers.
+ ///
+ /// This is only used for properties not already specified in the ambient
+ /// [DatePickerTheme.of].
+ ///
+ /// See also:
+ ///
+ /// * [of], which will return [ThemeData.datePickerTheme] if it doesn't
+ /// find a [DatePickerTheme] ancestor, instead of returning null.
+ /// * [maybeOf], which returns null if it doesn't find a
+ /// [DatePickerTheme] ancestor.
+ static DatePickerThemeData defaults(BuildContext context) {
+ return Theme.of(context).useMaterial3
+ ? _DatePickerDefaultsM3(context)
+ : _DatePickerDefaultsM2(context);
+ }
+
+ @override
+ Widget wrap(BuildContext context, Widget child) {
+ return DatePickerTheme(data: data, child: child);
+ }
+
+ @override
+ bool updateShouldNotify(DatePickerTheme oldWidget) => data != oldWidget.data;
+}
+
+// Hand coded defaults based on Material Design 2.
+class _DatePickerDefaultsM2 extends DatePickerThemeData {
+ _DatePickerDefaultsM2(this.context)
+ : super(
+ elevation: 24.0,
+ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))),
+ rangePickerElevation: 0.0,
+ rangePickerShape: const RoundedRectangleBorder(),
+ );
+
+ final BuildContext context;
+ late final ThemeData _theme = Theme.of(context);
+ late final ColorScheme _colors = _theme.colorScheme;
+ late final TextTheme _textTheme = _theme.textTheme;
+ late final bool _isDark = _colors.brightness == Brightness.dark;
+
+ @override
+ Color? get headerBackgroundColor => _isDark ? _colors.surface : _colors.primary;
+
+ @override
+ Color? get headerForegroundColor => _isDark ? _colors.onSurface : _colors.onPrimary;
+
+ @override
+ TextStyle? get headerHeadlineStyle => _textTheme.headlineSmall;
+
+ @override
+ TextStyle? get headerHelpStyle => _textTheme.labelSmall;
+
+ @override
+ TextStyle? get weekdayStyle => _textTheme.bodySmall?.apply(
+ color: _colors.onSurface.withOpacity(0.60),
+ );
+
+ @override
+ TextStyle? get dayStyle => _textTheme.bodySmall;
+
+ @override
+ MaterialStateProperty<Color?>? get dayForegroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ } else if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.38);
+ }
+ return _colors.onSurface;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get dayBackgroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.primary;
+ }
+ return null;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get dayOverlayColor =>
+ 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.38);
+ }
+ } else {
+ 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);
+ }
+ }
+ return null;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get todayForegroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ } else if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.38);
+ }
+ return _colors.primary;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get todayBackgroundColor => dayBackgroundColor;
+
+ @override
+ BorderSide? get todayBorder => BorderSide(color: _colors.primary);
+
+ @override
+ TextStyle? get yearStyle => _textTheme.bodyLarge;
+
+ @override
+ Color? get rangePickerBackgroundColor => _colors.surface;
+
+ @override
+ Color? get rangePickerShadowColor => Colors.transparent;
+
+ @override
+ Color? get rangePickerSurfaceTintColor => Colors.transparent;
+
+ @override
+ Color? get rangePickerHeaderBackgroundColor => _isDark ? _colors.surface : _colors.primary;
+
+ @override
+ Color? get rangePickerHeaderForegroundColor => _isDark ? _colors.onSurface : _colors.onPrimary;
+
+ @override
+ TextStyle? get rangePickerHeaderHeadlineStyle => _textTheme.headlineSmall;
+
+ @override
+ TextStyle? get rangePickerHeaderHelpStyle => _textTheme.labelSmall;
+
+ @override
+ Color? get rangeSelectionBackgroundColor => _colors.primary.withOpacity(0.12);
+
+ @override
+ MaterialStateProperty<Color?>? get rangeSelectionOverlayColor =>
+ 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.38);
+ }
+ } else {
+ 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);
+ }
+ }
+ return null;
+ });
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES - DatePicker
+
+// 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 _DatePickerDefaultsM3 extends DatePickerThemeData {
+ _DatePickerDefaultsM3(this.context)
+ : super(
+ elevation: 6.0,
+ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0))),
+ rangePickerElevation: 0.0,
+ rangePickerShape: const RoundedRectangleBorder(),
+ );
+
+ final BuildContext context;
+ late final ThemeData _theme = Theme.of(context);
+ late final ColorScheme _colors = _theme.colorScheme;
+ late final TextTheme _textTheme = _theme.textTheme;
+
+ @override
+ Color? get backgroundColor => _colors.surface;
+
+ @override
+ Color? get shadowColor => Colors.transparent;
+
+ @override
+ Color? get surfaceTintColor => _colors.surfaceTint;
+
+ @override
+ Color? get headerBackgroundColor => Colors.transparent;
+
+ @override
+ Color? get headerForegroundColor => _colors.onSurfaceVariant;
+
+ @override
+ TextStyle? get headerHeadlineStyle => _textTheme.headlineLarge;
+
+ @override
+ TextStyle? get headerHelpStyle => _textTheme.labelLarge;
+
+ @override
+ TextStyle? get weekdayStyle => _textTheme.bodyLarge?.apply(
+ color: _colors.onSurface,
+ );
+
+ @override
+ TextStyle? get dayStyle => _textTheme.bodyLarge;
+
+ @override
+ MaterialStateProperty<Color?>? get dayForegroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ } else if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurface.withOpacity(0.38);
+ }
+ return _colors.onSurface;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get dayBackgroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.primary;
+ }
+ return null;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get dayOverlayColor =>
+ 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);
+ }
+ } else {
+ 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);
+ }
+ }
+ return null;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get todayForegroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ } else if (states.contains(MaterialState.disabled)) {
+ return _colors.primary.withOpacity(0.38);
+ }
+ return _colors.primary;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get todayBackgroundColor => dayBackgroundColor;
+
+ @override
+ BorderSide? get todayBorder => BorderSide(color: _colors.primary);
+
+ @override
+ TextStyle? get yearStyle => _textTheme.bodyLarge;
+
+ @override
+ MaterialStateProperty<Color?>? get yearForegroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ } else if (states.contains(MaterialState.disabled)) {
+ return _colors.onSurfaceVariant.withOpacity(0.38);
+ }
+ return _colors.onSurfaceVariant;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get yearBackgroundColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.primary;
+ }
+ return null;
+ });
+
+ @override
+ MaterialStateProperty<Color?>? get yearOverlayColor =>
+ 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);
+ }
+ } else {
+ 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);
+ }
+ }
+ return null;
+ });
+
+ @override
+ Color? get rangePickerShadowColor => Colors.transparent;
+
+ @override
+ Color? get rangePickerSurfaceTintColor => Colors.transparent;
+
+ @override
+ Color? get rangeSelectionBackgroundColor => _colors.primaryContainer;
+
+ @override
+ MaterialStateProperty<Color?>? get rangeSelectionOverlayColor =>
+ MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.hovered)) {
+ return null;
+ }
+ if (states.contains(MaterialState.focused)) {
+ return null;
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return null;
+ }
+ return null;
+ });
+
+ @override
+ Color? get rangePickerHeaderBackgroundColor => Colors.transparent;
+
+ @override
+ Color? get rangePickerHeaderForegroundColor => _colors.onSurfaceVariant;
+
+ @override
+ TextStyle? get rangePickerHeaderHeadlineStyle => _textTheme.titleLarge;
+
+ @override
+ TextStyle? get rangePickerHeaderHelpStyle => _textTheme.titleSmall;
+
+
+}
+
+// END GENERATED TOKEN PROPERTIES - DatePicker
diff --git a/framework/lib/src/material/debug.dart b/framework/lib/src/material/debug.dart
index 260ab95..7f212ef 100644
--- a/framework/lib/src/material/debug.dart
+++ b/framework/lib/src/material/debug.dart
@@ -11,7 +11,8 @@
// Examples can assume:
// late BuildContext context;
-/// Asserts that the given context has a [Material] ancestor.
+/// Asserts that the given context has a [Material] ancestor within the closest
+/// [LookupBoundary].
///
/// Used by many Material Design widgets to make sure that they are
/// only used in contexts where they can print ink onto some material.
@@ -32,12 +33,17 @@
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMaterial(BuildContext context) {
assert(() {
- if (context.widget is! Material && context.findAncestorWidgetOfExactType<Material>() == null) {
+ if (LookupBoundary.findAncestorWidgetOfExactType<Material>(context) == null) {
+ final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Material>(context);
throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('No Material widget found.'),
+ ErrorSummary('No Material widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
+ if (hiddenByBoundary)
+ ErrorDescription(
+ 'There is an ancestor Material widget, but it is hidden by a LookupBoundary.'
+ ),
ErrorDescription(
'${context.widget.runtimeType} widgets require a Material '
- 'widget ancestor.\n'
+ 'widget ancestor within the closest LookupBoundary.\n'
'In Material Design, most widgets are conceptually "printed" on '
"a sheet of material. In Flutter's material library, that "
'material is represented by the Material widget. It is the '
diff --git a/framework/lib/src/material/desktop_text_selection.dart b/framework/lib/src/material/desktop_text_selection.dart
index 3b59c24..5de5c8e 100644
--- a/framework/lib/src/material/desktop_text_selection.dart
+++ b/framework/lib/src/material/desktop_text_selection.dart
@@ -177,12 +177,11 @@
return const SizedBox.shrink();
}
- final MediaQueryData mediaQuery = MediaQuery.of(context);
-
+ final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context);
final Offset midpointAnchor = Offset(
clampDouble(widget.selectionMidpoint.dx - widget.globalEditableRegion.left,
- mediaQuery.padding.left,
- mediaQuery.size.width - mediaQuery.padding.right,
+ mediaQueryPadding.left,
+ MediaQuery.sizeOf(context).width - mediaQueryPadding.right,
),
widget.selectionMidpoint.dy - widget.globalEditableRegion.top,
);
diff --git a/framework/lib/src/material/desktop_text_selection_toolbar.dart b/framework/lib/src/material/desktop_text_selection_toolbar.dart
index 8bdf670..adb41ea 100644
--- a/framework/lib/src/material/desktop_text_selection_toolbar.dart
+++ b/framework/lib/src/material/desktop_text_selection_toolbar.dart
@@ -64,9 +64,8 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
+ final double paddingAbove = MediaQuery.paddingOf(context).top + _kToolbarScreenPadding;
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
return Padding(
diff --git a/framework/lib/src/material/dialog.dart b/framework/lib/src/material/dialog.dart
index ce4a864..414e8d7 100644
--- a/framework/lib/src/material/dialog.dart
+++ b/framework/lib/src/material/dialog.dart
@@ -60,8 +60,7 @@
this.shape,
this.alignment,
this.child,
- }) : assert(clipBehavior != null),
- assert(elevation == null || elevation >= 0.0),
+ }) : assert(elevation == null || elevation >= 0.0),
_fullscreen = false;
/// Creates a fullscreen dialog.
@@ -217,7 +216,7 @@
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final DialogTheme dialogTheme = DialogTheme.of(context);
- final EdgeInsets effectivePadding = MediaQuery.of(context).viewInsets + (insetPadding ?? EdgeInsets.zero);
+ final EdgeInsets effectivePadding = MediaQuery.viewInsetsOf(context) + (insetPadding ?? EdgeInsets.zero);
final DialogTheme defaults = theme.useMaterial3
? (_fullscreen ? _DialogFullscreenDefaultsM3(context) : _DialogDefaultsM3(context))
: _DialogDefaultsM2(context);
@@ -303,9 +302,9 @@
/// builder: (BuildContext context) {
/// return AlertDialog(
/// title: const Text('AlertDialog Title'),
-/// content: SingleChildScrollView(
+/// content: const SingleChildScrollView(
/// child: ListBody(
-/// children: const <Widget>[
+/// children: <Widget>[
/// Text('This is a demo alert dialog.'),
/// Text('Would you like to approve of this message?'),
/// ],
@@ -385,7 +384,7 @@
this.shape,
this.alignment,
this.scrollable = false,
- }) : assert(clipBehavior != null);
+ });
/// An optional icon to display at the top of the dialog.
///
@@ -642,7 +641,7 @@
// The paddingScaleFactor is used to adjust the padding of Dialog's
// children.
- final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.of(context).textScaleFactor);
+ final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.textScaleFactorOf(context));
final TextDirection? textDirection = Directionality.maybeOf(context);
Widget? iconWidget;
@@ -973,8 +972,7 @@
this.clipBehavior = Clip.none,
this.shape,
this.alignment,
- }) : assert(titlePadding != null),
- assert(contentPadding != null);
+ });
/// The (optional) title of the dialog is displayed in a large font at the top
/// of the dialog.
@@ -1075,7 +1073,7 @@
// The paddingScaleFactor is used to adjust the padding of Dialog
// children.
- final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.of(context).textScaleFactor);
+ final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.textScaleFactorOf(context));
final TextDirection? textDirection = Directionality.maybeOf(context);
Widget? titleWidget;
@@ -1238,8 +1236,11 @@
///
/// {@macro flutter.widgets.RestorationManager}
///
-/// The `traversalEdgeBehavior` argument controls the transfer of focus
-/// beyond the first and the last items of the dialog route.
+/// If not null, `traversalEdgeBehavior` argument specifies the transfer of
+/// focus beyond the first and the last items of the dialog route. By default,
+/// uses [TraversalEdgeBehavior.closedLoop], because it's typical for dialogs
+/// to allow users to cycle through widgets inside it without leaving the
+/// dialog.
///
/// ** See code in examples/api/lib/material/dialog/show_dialog.2.dart **
/// {@end-tool}
@@ -1268,10 +1269,6 @@
Offset? anchorPoint,
TraversalEdgeBehavior? traversalEdgeBehavior,
}) {
- assert(builder != null);
- assert(barrierDismissible != null);
- assert(useSafeArea != null);
- assert(useRootNavigator != null);
assert(_debugIsActive(context));
assert(debugCheckHasMaterialLocalizations(context));
@@ -1293,7 +1290,7 @@
settings: routeSettings,
themes: themes,
anchorPoint: anchorPoint,
- traversalEdgeBehavior: traversalEdgeBehavior,
+ traversalEdgeBehavior: traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop,
));
}
@@ -1373,8 +1370,7 @@
super.settings,
super.anchorPoint,
super.traversalEdgeBehavior,
- }) : assert(barrierDismissible != null),
- super(
+ }) : super(
pageBuilder: (BuildContext buildContext, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget pageChild = Builder(builder: builder);
Widget dialog = themes?.wrap(pageChild) ?? pageChild;
@@ -1437,7 +1433,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _DialogDefaultsM3 extends DialogTheme {
_DialogDefaultsM3(this.context)
@@ -1482,7 +1478,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _DialogFullscreenDefaultsM3 extends DialogTheme {
const _DialogFullscreenDefaultsM3(this.context);
diff --git a/framework/lib/src/material/dialog_theme.dart b/framework/lib/src/material/dialog_theme.dart
index 5896d87..c39da59 100644
--- a/framework/lib/src/material/dialog_theme.dart
+++ b/framework/lib/src/material/dialog_theme.dart
@@ -111,7 +111,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static DialogTheme lerp(DialogTheme? a, DialogTheme? b, double t) {
- assert(t != null);
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.dart b/framework/lib/src/material/divider.dart
index d40d117..5ddf599 100644
--- a/framework/lib/src/material/divider.dart
+++ b/framework/lib/src/material/divider.dart
@@ -328,7 +328,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _DividerDefaultsM3 extends DividerThemeData {
const _DividerDefaultsM3(this.context) : super(
diff --git a/framework/lib/src/material/divider_theme.dart b/framework/lib/src/material/divider_theme.dart
index c7926ed..79d631d 100644
--- a/framework/lib/src/material/divider_theme.dart
+++ b/framework/lib/src/material/divider_theme.dart
@@ -87,7 +87,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static DividerThemeData lerp(DividerThemeData? a, DividerThemeData? b, double t) {
- assert(t != null);
return DividerThemeData(
color: Color.lerp(a?.color, b?.color, t),
space: lerpDouble(a?.space, b?.space, t),
@@ -144,7 +143,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties for descendant [Divider]s, [VerticalDivider]s, dividers
/// between [ListTile]s, and dividers between rows in [DataTable]s.
diff --git a/framework/lib/src/material/drawer.dart b/framework/lib/src/material/drawer.dart
index c0943f1..8bca807 100644
--- a/framework/lib/src/material/drawer.dart
+++ b/framework/lib/src/material/drawer.dart
@@ -115,7 +115,7 @@
/// {@end-tool}
///
/// An open drawer may be closed with a swipe to close gesture, pressing the
-/// the escape key, by tapping the scrim, or by calling pop route function such as
+/// escape key, by tapping the scrim, or by calling pop route function such as
/// [Navigator.pop]. For example a drawer item might close the drawer when tapped:
///
/// ```dart
@@ -325,10 +325,7 @@
this.scrimColor,
this.edgeDragWidth,
this.enableOpenDragGesture = true,
- }) : assert(child != null),
- assert(dragStartBehavior != null),
- assert(alignment != null),
- super(key: key);
+ }) : super(key: key);
/// The widget below this widget in the tree.
///
@@ -382,11 +379,11 @@
/// drawer.
///
/// By default, the value used is 20.0 added to the padding edge of
- /// `MediaQuery.of(context).padding` that corresponds to [alignment].
+ /// `MediaQuery.paddingOf(context)` that corresponds to [alignment].
/// This ensures that the drag area for notched devices is not obscured. For
/// example, if [alignment] is set to [DrawerAlignment.start] and
/// `TextDirection.of(context)` is set to [TextDirection.ltr],
- /// 20.0 will be added to `MediaQuery.of(context).padding.left`.
+ /// 20.0 will be added to `MediaQuery.paddingOf(context).left`.
final double? edgeDragWidth;
/// Whether or not the drawer is opened or closed.
@@ -659,7 +656,6 @@
}
AlignmentDirectional get _drawerOuterAlignment {
- assert(widget.alignment != null);
switch (widget.alignment) {
case DrawerAlignment.start:
return AlignmentDirectional.centerStart;
@@ -669,7 +665,6 @@
}
AlignmentDirectional get _drawerInnerAlignment {
- assert(widget.alignment != null);
switch (widget.alignment) {
case DrawerAlignment.start:
return AlignmentDirectional.centerEnd;
@@ -680,7 +675,6 @@
Widget _buildDrawer(BuildContext context) {
final bool drawerIsStart = widget.alignment == DrawerAlignment.start;
- final EdgeInsets padding = MediaQuery.of(context).padding;
final TextDirection textDirection = Directionality.of(context);
final bool isDesktop;
switch (Theme.of(context).platform) {
@@ -698,6 +692,7 @@
double? dragAreaWidth = widget.edgeDragWidth;
if (widget.edgeDragWidth == null) {
+ final EdgeInsets padding = MediaQuery.paddingOf(context);
switch (textDirection) {
case TextDirection.ltr:
dragAreaWidth = _kEdgeDragWidth +
@@ -741,7 +736,6 @@
platformHasBackButton = false;
break;
}
- assert(platformHasBackButton != null);
final Widget child = _DrawerControllerScope(
controller: widget,
@@ -827,13 +821,14 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _DrawerDefaultsM3 extends DrawerThemeData {
- const _DrawerDefaultsM3(this.context)
+ _DrawerDefaultsM3(this.context)
: super(elevation: 1.0);
final BuildContext context;
+ late final TextDirection direction = Directionality.of(context);
@override
Color? get backgroundColor => Theme.of(context).colorScheme.surface;
@@ -844,18 +839,22 @@
@override
Color? get shadowColor => Colors.transparent;
- // This don't appear to be tokens for this value, but it is
- // shown in the spec.
+ // There isn't currently a token for this value, but it is shown in the spec,
+ // so hard coding here for now.
@override
- ShapeBorder? get shape => const RoundedRectangleBorder(
- borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0)),
+ ShapeBorder? get shape => RoundedRectangleBorder(
+ borderRadius: const BorderRadiusDirectional.horizontal(
+ end: Radius.circular(16.0),
+ ).resolve(direction),
);
- // This don't appear to be tokens for this value, but it is
- // shown in the spec.
+ // There isn't currently a token for this value, but it is shown in the spec,
+ // so hard coding here for now.
@override
- ShapeBorder? get endShape => const RoundedRectangleBorder(
- borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0)),
+ ShapeBorder? get endShape => RoundedRectangleBorder(
+ borderRadius: const BorderRadiusDirectional.horizontal(
+ start: Radius.circular(16.0),
+ ).resolve(direction),
);
}
diff --git a/framework/lib/src/material/drawer_header.dart b/framework/lib/src/material/drawer_header.dart
index 639a36f..287ae5e 100644
--- a/framework/lib/src/material/drawer_header.dart
+++ b/framework/lib/src/material/drawer_header.dart
@@ -76,7 +76,7 @@
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMediaQuery(context));
final ThemeData theme = Theme.of(context);
- final double statusBarHeight = MediaQuery.of(context).padding.top;
+ final double statusBarHeight = MediaQuery.paddingOf(context).top;
return Container(
height: statusBarHeight + _kDrawerHeaderHeight,
margin: margin,
diff --git a/framework/lib/src/material/drawer_theme.dart b/framework/lib/src/material/drawer_theme.dart
index 26e2048..6986893 100644
--- a/framework/lib/src/material/drawer_theme.dart
+++ b/framework/lib/src/material/drawer_theme.dart
@@ -99,7 +99,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static DrawerThemeData? lerp(DrawerThemeData? a, DrawerThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -174,7 +173,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the background color, scrim color, elevation, and shape for
/// descendant [Drawer] widgets.
diff --git a/framework/lib/src/material/dropdown.dart b/framework/lib/src/material/dropdown.dart
index f357fc9..4286f51 100644
--- a/framework/lib/src/material/dropdown.dart
+++ b/framework/lib/src/material/dropdown.dart
@@ -434,8 +434,7 @@
this.menuMaxHeight,
required this.enableFeedback,
this.borderRadius,
- }) : assert(style != null),
- itemHeights = List<double>.filled(items.length, itemHeight ?? kMinInteractiveDimension);
+ }) : itemHeights = List<double>.filled(items.length, itemHeight ?? kMinInteractiveDimension);
final List<_MenuItem<T>> items;
final EdgeInsetsGeometry padding;
@@ -664,7 +663,7 @@
super.key,
required this.onLayout,
required this.item,
- }) : assert(onLayout != null), super(child: item);
+ }) : super(child: item);
final ValueChanged<Size> onLayout;
final DropdownMenuItem<T>? item;
@@ -681,7 +680,7 @@
}
class _RenderMenuItem extends RenderProxyBox {
- _RenderMenuItem(this.onLayout, [RenderBox? child]) : assert(onLayout != null), super(child);
+ _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
ValueChanged<Size> onLayout;
@@ -703,7 +702,7 @@
super.key,
this.alignment = AlignmentDirectional.centerStart,
required this.child,
- }) : assert(child != null);
+ });
/// The widget below this widget in the tree.
///
@@ -747,7 +746,7 @@
this.enabled = true,
super.alignment,
required super.child,
- }) : assert(child != null);
+ });
/// Called when the dropdown menu item is tapped.
final VoidCallback? onTap;
@@ -775,7 +774,7 @@
const DropdownButtonHideUnderline({
super.key,
required super.child,
- }) : assert(child != null);
+ });
/// Returns whether the underline of [DropdownButton] widgets should
/// be hidden.
@@ -898,11 +897,6 @@
'Either zero or 2 or more [DropdownMenuItem]s were detected '
'with the same value',
),
- assert(elevation != null),
- assert(iconSize != null),
- assert(isDense != null),
- assert(isExpanded != null),
- assert(autofocus != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
_inputDecoration = null,
_isEmpty = false,
@@ -947,14 +941,7 @@
'Either zero or 2 or more [DropdownMenuItem]s were detected '
'with the same value',
),
- assert(elevation != null),
- assert(iconSize != null),
- assert(isDense != null),
- assert(isExpanded != null),
- assert(autofocus != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
- assert(isEmpty != null),
- assert(isFocused != null),
_inputDecoration = inputDecoration,
_isEmpty = isEmpty,
_isFocused = isFocused;
@@ -1333,7 +1320,7 @@
// Similarly, we don't reduce the height of the button so much that its icon
// would be clipped.
double get _denseButtonHeight {
- final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
+ final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
final double fontSize = _textStyle!.fontSize ?? Theme.of(context).textTheme.titleMedium!.fontSize!;
final double scaledFontSize = textScaleFactor * fontSize;
return math.max(scaledFontSize, math.max(widget.iconSize, _kDenseButtonHeight));
@@ -1369,11 +1356,11 @@
bool get _enabled => widget.items != null && widget.items!.isNotEmpty && widget.onChanged != null;
Orientation _getOrientation(BuildContext context) {
- Orientation? result = MediaQuery.maybeOf(context)?.orientation;
+ Orientation? result = MediaQuery.maybeOrientationOf(context);
if (result == null) {
- // If there's no MediaQuery, then use the window aspect to determine
+ // If there's no MediaQuery, then use the view aspect to determine
// orientation.
- final Size size = WidgetsBinding.instance.window.physicalSize;
+ final Size size = View.of(context).physicalSize;
result = size.width > size.height ? Orientation.landscape : Orientation.portrait;
}
return result;
@@ -1530,7 +1517,7 @@
/// This is a convenience widget that wraps a [DropdownButton] widget in a
/// [FormField].
///
-/// A [Form] ancestor is not required. The [Form] simply makes it easier to
+/// A [Form] ancestor is not required. The [Form] allows one to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
@@ -1590,12 +1577,7 @@
'Either zero or 2 or more [DropdownMenuItem]s were detected '
'with the same value',
),
- assert(elevation != null),
- assert(iconSize != null),
- assert(isDense != null),
- assert(isExpanded != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
- assert(autofocus != null),
decoration = decoration ?? InputDecoration(focusColor: focusColor),
super(
initialValue: value,
diff --git a/framework/lib/src/material/dropdown_menu.dart b/framework/lib/src/material/dropdown_menu.dart
index c301b79..a4d5ab9 100644
--- a/framework/lib/src/material/dropdown_menu.dart
+++ b/framework/lib/src/material/dropdown_menu.dart
@@ -135,6 +135,7 @@
this.controller,
this.initialSelection,
this.onSelected,
+ this.requestFocusOnTap,
required this.dropdownMenuEntries,
});
@@ -228,6 +229,19 @@
/// Defaults to null. If null, only the text field is updated.
final ValueChanged<T?>? onSelected;
+ /// Determine if the dropdown button requests focus and the on-screen virtual
+ /// keyboard is shown in response to a touch event.
+ ///
+ /// By default, on mobile platforms, tapping on the text field and opening
+ /// the menu will not cause a focus request and the virtual keyboard will not
+ /// appear. The default behavior for desktop platforms is for the dropdown to
+ /// take the focus.
+ ///
+ /// Defaults to null. Setting this field to true or false, rather than allowing
+ /// the implementation to choose based on the platform, can be useful for
+ /// applications that want to override the default behavior.
+ final bool? requestFocusOnTap;
+
/// Descriptions of the menu items in the [DropdownMenu].
///
/// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]
@@ -242,7 +256,6 @@
class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _anchorKey = GlobalKey();
final GlobalKey _leadingKey = GlobalKey();
- final FocusNode _textFocusNode = FocusNode();
final MenuController _controller = MenuController();
late final TextEditingController _textEditingController;
late bool _enableFilter;
@@ -288,6 +301,23 @@
}
}
+ bool canRequestFocus() {
+ if (widget.requestFocusOnTap != null) {
+ return widget.requestFocusOnTap!;
+ }
+
+ switch (Theme.of(context).platform) {
+ case TargetPlatform.iOS:
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ return false;
+ case TargetPlatform.macOS:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ return true;
+ }
+ }
+
void refreshLeadingPadding() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
@@ -428,7 +458,6 @@
@override
void dispose() {
- _textEditingController.dispose();
super.dispose();
}
@@ -489,13 +518,12 @@
builder: (BuildContext context, MenuController controller, Widget? child) {
assert(_initialMenu != null);
final Widget trailingButton = Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ padding: const EdgeInsets.all(4.0),
child: IconButton(
isSelected: controller.isOpen,
icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down),
selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up),
onPressed: () {
- _textFocusNode.requestFocus();
handlePressed(controller);
},
),
@@ -511,7 +539,9 @@
width: widget.width,
children: <Widget>[
TextField(
- focusNode: _textFocusNode,
+ canRequestFocus: canRequestFocus(),
+ enableInteractiveSelection: canRequestFocus(),
+ textAlignVertical: TextAlignVertical.center,
style: effectiveTextStyle,
controller: _textEditingController,
onEditingComplete: () {
@@ -575,7 +605,7 @@
}
class _DropdownMenuBody extends MultiChildRenderObjectWidget {
- _DropdownMenuBody({
+ const _DropdownMenuBody({
super.key,
super.children,
this.width,
diff --git a/framework/lib/src/material/dropdown_menu_theme.dart b/framework/lib/src/material/dropdown_menu_theme.dart
index 3027be3..7d891c8 100644
--- a/framework/lib/src/material/dropdown_menu_theme.dart
+++ b/framework/lib/src/material/dropdown_menu_theme.dart
@@ -112,7 +112,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the visual properties used by descendant [DropdownMenu]
/// widgets.
@@ -162,7 +162,6 @@
/// * [of], which will return [ThemeData.dropdownMenuTheme] if it doesn't
/// find a [DropdownMenuTheme] ancestor, instead of returning null.
static DropdownMenuThemeData? maybeOf(BuildContext context) {
- assert(context != null);
return context.dependOnInheritedWidgetOfExactType<DropdownMenuTheme>()?.data;
}
diff --git a/framework/lib/src/material/elevated_button.dart b/framework/lib/src/material/elevated_button.dart
index 4fdd820..2ba8c92 100644
--- a/framework/lib/src/material/elevated_button.dart
+++ b/framework/lib/src/material/elevated_button.dart
@@ -252,7 +252,7 @@
/// each state, and "others" means all other states.
///
/// The `textScaleFactor` is the value of
- /// `MediaQuery.of(context).textScaleFactor` and the names of the
+ /// `MediaQuery.textScaleFactorOf(context)` and the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
/// abbreviated for readability.
///
@@ -394,7 +394,7 @@
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
}
@@ -486,9 +486,7 @@
super.statesController,
required Widget icon,
required Widget label,
- }) : assert(icon != null),
- assert(label != null),
- super(
+ }) : super(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _ElevatedButtonWithIconChild(icon: icon, label: label),
@@ -500,7 +498,7 @@
const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
return super.defaultStyleOf(context).copyWith(
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
@@ -516,7 +514,7 @@
@override
Widget build(BuildContext context) {
- final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
+ final double scale = MediaQuery.textScaleFactorOf(context);
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
return Row(
mainAxisSize: MainAxisSize.min,
@@ -532,7 +530,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _ElevatedButtonDefaultsM3 extends ButtonStyle {
_ElevatedButtonDefaultsM3(this.context)
diff --git a/framework/lib/src/material/elevated_button_theme.dart b/framework/lib/src/material/elevated_button_theme.dart
index 629ae7e..bc59beb 100644
--- a/framework/lib/src/material/elevated_button_theme.dart
+++ b/framework/lib/src/material/elevated_button_theme.dart
@@ -49,7 +49,6 @@
/// Linearly interpolate between two elevated button themes.
static ElevatedButtonThemeData? lerp(ElevatedButtonThemeData? a, ElevatedButtonThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -98,7 +97,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The configuration of this theme.
final ElevatedButtonThemeData data;
diff --git a/framework/lib/src/material/elevation_overlay.dart b/framework/lib/src/material/elevation_overlay.dart
index f989e60..21312dd 100644
--- a/framework/lib/src/material/elevation_overlay.dart
+++ b/framework/lib/src/material/elevation_overlay.dart
@@ -160,7 +160,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
// Surface tint opacities based on elevations according to the
// Material Design 3 specification:
diff --git a/framework/lib/src/material/expand_icon.dart b/framework/lib/src/material/expand_icon.dart
index 48b3ee5..3581d75 100644
--- a/framework/lib/src/material/expand_icon.dart
+++ b/framework/lib/src/material/expand_icon.dart
@@ -39,9 +39,7 @@
this.color,
this.disabledColor,
this.expandedColor,
- }) : assert(isExpanded != null),
- assert(size != null),
- assert(padding != null);
+ });
/// Whether the icon is in an expanded state.
///
@@ -66,7 +64,7 @@
/// This property must not be null. It defaults to 8.0 padding on all sides.
final EdgeInsetsGeometry padding;
-
+ /// {@template flutter.material.ExpandIcon.color}
/// The color of the icon.
///
/// Defaults to [Colors.black54] when the theme's
@@ -74,6 +72,7 @@
/// [Colors.white60] when it is [Brightness.dark]. This adheres to the
/// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
/// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
+ /// {@endtemplate}
final Color? color;
/// The color of the icon when it is disabled,
diff --git a/framework/lib/src/material/expansion_panel.dart b/framework/lib/src/material/expansion_panel.dart
index 051f395..f318c6b 100644
--- a/framework/lib/src/material/expansion_panel.dart
+++ b/framework/lib/src/material/expansion_panel.dart
@@ -16,6 +16,7 @@
const EdgeInsets _kPanelHeaderExpandedDefaultPadding = EdgeInsets.symmetric(
vertical: 64.0 - _kPanelHeaderCollapsedHeight,
);
+const EdgeInsets _kExpandIconPadding = EdgeInsets.all(12.0);
class _SaltedKey<S, V> extends LocalKey {
const _SaltedKey(this.salt, this.value);
@@ -81,10 +82,7 @@
this.isExpanded = false,
this.canTapOnHeader = false,
this.backgroundColor,
- }) : assert(headerBuilder != null),
- assert(body != null),
- assert(isExpanded != null),
- assert(canTapOnHeader != null);
+ });
/// The widget builder that builds the expansion panels' header.
final ExpansionPanelHeaderBuilder headerBuilder;
@@ -131,7 +129,7 @@
required super.body,
super.canTapOnHeader,
super.backgroundColor,
- }) : assert(value != null);
+ });
/// The value that uniquely identifies a radio panel so that the currently
/// selected radio panel can be identified.
@@ -168,9 +166,8 @@
this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
this.dividerColor,
this.elevation = 2,
- }) : assert(children != null),
- assert(animationDuration != null),
- _allowOnlyOnePanelOpen = false,
+ this.expandIconColor,
+ }) : _allowOnlyOnePanelOpen = false,
initialOpenPanelValue = null;
/// Creates a radio expansion panel list widget.
@@ -195,9 +192,8 @@
this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
this.dividerColor,
this.elevation = 2,
- }) : assert(children != null),
- assert(animationDuration != null),
- _allowOnlyOnePanelOpen = true;
+ this.expandIconColor,
+ }) : _allowOnlyOnePanelOpen = true;
/// The children of the expansion panel list. They are laid out in a similar
/// fashion to [ListBody].
@@ -207,14 +203,14 @@
/// is pressed. The arguments passed to the callback are the index of the
/// pressed panel and whether the panel is currently expanded or not.
///
- /// If ExpansionPanelList.radio is used, the callback may be called a
+ /// If [ExpansionPanelList.radio] is used, the callback may be called a
/// second time if a different panel was previously open. The arguments
/// 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
+ /// 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 simply meant to inform the parent widget of
+ /// [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
@@ -249,6 +245,9 @@
/// By default, the value of elevation is 2.
final double elevation;
+ /// {@macro flutter.material.ExpandIcon.color}
+ final Color? expandIconColor;
+
@override
State<StatefulWidget> createState() => _ExpansionPanelListState();
}
@@ -356,8 +355,9 @@
Widget expandIconContainer = Container(
margin: const EdgeInsetsDirectional.only(end: 8.0),
child: ExpandIcon(
+ color: widget.expandIconColor,
isExpanded: _isChildExpanded(index),
- padding: const EdgeInsets.all(16.0),
+ padding: _kExpandIconPadding,
onPressed: !child.canTapOnHeader
? (bool isExpanded) => _handlePressed(isExpanded, index)
: null,
diff --git a/framework/lib/src/material/expansion_tile.dart b/framework/lib/src/material/expansion_tile.dart
index fefc032..855b9ce 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}
@@ -73,9 +74,7 @@
this.collapsedShape,
this.clipBehavior,
this.controlAffinity,
- }) : assert(initiallyExpanded != null),
- assert(maintainState != null),
- assert(
+ }) : assert(
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
'CrossAxisAlignment.baseline is not supported since the expanded children '
'are aligned in a column, not a row. Try to use another constant.',
@@ -218,7 +217,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:
///
@@ -229,6 +228,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;
@@ -237,7 +245,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:
///
@@ -249,8 +258,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:
///
@@ -443,7 +454,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
@@ -460,13 +473,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;
@@ -500,3 +513,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_152
+
+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 => Theme.of(context).textTheme.bodyLarge!.color;
+
+ @override
+ Color? get iconColor => _colors.primary;
+
+ @override
+ Color? get collapsedTextColor => Theme.of(context).textTheme.bodyLarge!.color;
+
+ @override
+ Color? get collapsedIconColor => _colors.onSurface;
+}
+
+// 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 08dec3a..8d62465 100644
--- a/framework/lib/src/material/expansion_tile_theme.dart
+++ b/framework/lib/src/material/expansion_tile_theme.dart
@@ -124,7 +124,6 @@
/// Linearly interpolate between ExpansionTileThemeData objects.
static ExpansionTileThemeData? lerp(ExpansionTileThemeData? a, ExpansionTileThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -217,8 +216,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// Specifies color, alignment, and text style values for
/// descendant [ExpansionTile] widgets.
diff --git a/framework/lib/src/material/filled_button.dart b/framework/lib/src/material/filled_button.dart
index 15b0854..a7f52b6 100644
--- a/framework/lib/src/material/filled_button.dart
+++ b/framework/lib/src/material/filled_button.dart
@@ -288,7 +288,7 @@
/// each state, and "others" means all other states.
///
/// The `textScaleFactor` is the value of
- /// `MediaQuery.of(context).textScaleFactor` and the names of the
+ /// `MediaQuery.textScaleFactorOf(context)` and the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
/// abbreviated for readability.
///
@@ -368,7 +368,7 @@
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
}
@@ -436,9 +436,7 @@
super.statesController,
required Widget icon,
required Widget label,
- }) : assert(icon != null),
- assert(label != null),
- super(
+ }) : super(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _FilledButtonWithIconChild(icon: icon, label: label)
@@ -457,9 +455,7 @@
super.statesController,
required Widget icon,
required Widget label,
- }) : assert(icon != null),
- assert(label != null),
- super.tonal(
+ }) : super.tonal(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _FilledButtonWithIconChild(icon: icon, label: label)
@@ -471,7 +467,7 @@
const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
return super.defaultStyleOf(context).copyWith(
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
@@ -487,7 +483,7 @@
@override
Widget build(BuildContext context) {
- final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
+ final double scale = MediaQuery.textScaleFactorOf(context);
// Adjust the gap based on the text scale factor. Start at 8, and lerp
// to 4 based on how large the text is.
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
@@ -505,7 +501,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _FilledButtonDefaultsM3 extends ButtonStyle {
_FilledButtonDefaultsM3(this.context)
@@ -629,7 +625,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _FilledTonalButtonDefaultsM3 extends ButtonStyle {
_FilledTonalButtonDefaultsM3(this.context)
diff --git a/framework/lib/src/material/filled_button_theme.dart b/framework/lib/src/material/filled_button_theme.dart
index 1a2187b..8e3a4d3 100644
--- a/framework/lib/src/material/filled_button_theme.dart
+++ b/framework/lib/src/material/filled_button_theme.dart
@@ -49,7 +49,6 @@
/// Linearly interpolate between two filled button themes.
static FilledButtonThemeData? lerp(FilledButtonThemeData? a, FilledButtonThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -98,7 +97,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The configuration of this theme.
final FilledButtonThemeData data;
diff --git a/framework/lib/src/material/filter_chip.dart b/framework/lib/src/material/filter_chip.dart
index 61da51c..cf8c62f 100644
--- a/framework/lib/src/material/filter_chip.dart
+++ b/framework/lib/src/material/filter_chip.dart
@@ -88,11 +88,7 @@
this.showCheckmark,
this.checkmarkColor,
this.avatarBorder = const CircleBorder(),
- }) : assert(selected != null),
- assert(label != null),
- assert(clipBehavior != null),
- assert(autofocus != null),
- assert(pressElevation == null || pressElevation >= 0.0),
+ }) : assert(pressElevation == null || pressElevation >= 0.0),
assert(elevation == null || elevation >= 0.0);
@override
@@ -199,7 +195,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _FilterChipDefaultsM3 extends ChipThemeData {
const _FilterChipDefaultsM3(this.context, this.isEnabled, this.isSelected)
@@ -220,7 +216,7 @@
Color? get backgroundColor => null;
@override
- Color? get shadowColor => Theme.of(context).colorScheme.shadow;
+ Color? get shadowColor => Colors.transparent;
@override
Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
@@ -267,7 +263,7 @@
EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 8.0),
const EdgeInsets.symmetric(horizontal: 4.0),
- clampDouble(MediaQuery.of(context).textScaleFactor - 1.0, 0.0, 1.0),
+ clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
)!;
}
diff --git a/framework/lib/src/material/flexible_space_bar.dart b/framework/lib/src/material/flexible_space_bar.dart
index e2aa07d..c1f6890 100644
--- a/framework/lib/src/material/flexible_space_bar.dart
+++ b/framework/lib/src/material/flexible_space_bar.dart
@@ -84,8 +84,7 @@
this.collapseMode = CollapseMode.parallax,
this.stretchModes = const <StretchMode>[StretchMode.zoomBackground],
this.expandedTitleScale = 1.5,
- }) : assert(collapseMode != null),
- assert(expandedTitleScale >= 1);
+ }) : assert(expandedTitleScale >= 1);
/// The primary contents of the flexible space bar when expanded.
///
@@ -117,7 +116,7 @@
/// bottom-left or its center.
///
/// Typically this property is used to adjust how far the title is
- /// is inset from the bottom-left and it is specified along with
+ /// inset from the bottom-left and it is specified along with
/// [centerTitle] false.
///
/// By default the value of this property is
@@ -159,7 +158,6 @@
required double currentExtent,
required Widget child,
}) {
- assert(currentExtent != null);
return FlexibleSpaceBarSettings(
toolbarOpacity: toolbarOpacity ?? 1.0,
minExtent: minExtent ?? currentExtent,
@@ -179,7 +177,6 @@
if (widget.centerTitle != null) {
return widget.centerTitle!;
}
- assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@@ -197,7 +194,6 @@
return Alignment.bottomCenter;
}
final TextDirection textDirection = Directionality.of(context);
- assert(textDirection != null);
switch (textDirection) {
case TextDirection.rtl:
return Alignment.bottomRight;
@@ -223,10 +219,6 @@
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
- assert(
- settings != null,
- 'A FlexibleSpaceBar must be wrapped in the widget returned by FlexibleSpaceBar.createSettings().',
- );
final List<Widget> children = <Widget>[];
@@ -388,10 +380,9 @@
required this.currentExtent,
required super.child,
this.isScrolledUnder,
- }) : assert(toolbarOpacity != null),
- assert(minExtent != null && minExtent >= 0),
- assert(maxExtent != null && maxExtent >= 0),
- assert(currentExtent != null && currentExtent >= 0),
+ }) : assert(minExtent >= 0),
+ assert(maxExtent >= 0),
+ assert(currentExtent >= 0),
assert(toolbarOpacity >= 0.0),
assert(minExtent <= maxExtent),
assert(minExtent <= currentExtent),
diff --git a/framework/lib/src/material/floating_action_button.dart b/framework/lib/src/material/floating_action_button.dart
index cea314f..a8d8823 100644
--- a/framework/lib/src/material/floating_action_button.dart
+++ b/framework/lib/src/material/floating_action_button.dart
@@ -127,10 +127,6 @@
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
- assert(mini != null),
- assert(clipBehavior != null),
- assert(isExtended != null),
- assert(autofocus != null),
_floatingActionButtonType = mini ? _FloatingActionButtonType.small : _FloatingActionButtonType.regular,
_extendedLabel = null,
extendedIconLabelSpacing = null,
@@ -174,8 +170,6 @@
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
- assert(clipBehavior != null),
- assert(autofocus != null),
_floatingActionButtonType = _FloatingActionButtonType.small,
mini = true,
isExtended = false,
@@ -221,8 +215,6 @@
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
- assert(clipBehavior != null),
- assert(autofocus != null),
_floatingActionButtonType = _FloatingActionButtonType.large,
mini = false,
isExtended = false,
@@ -273,9 +265,6 @@
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
- assert(isExtended != null),
- assert(clipBehavior != null),
- assert(autofocus != null),
mini = false,
_floatingActionButtonType = _FloatingActionButtonType.extended,
child = icon,
@@ -804,7 +793,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _FABDefaultsM3 extends FloatingActionButtonThemeData {
_FABDefaultsM3(this.context, this.type, this.hasChild)
diff --git a/framework/lib/src/material/floating_action_button_location.dart b/framework/lib/src/material/floating_action_button_location.dart
index 1f6f034..29fea42 100644
--- a/framework/lib/src/material/floating_action_button_location.dart
+++ b/framework/lib/src/material/floating_action_button_location.dart
@@ -652,7 +652,6 @@
/// Calculates x-offset for start-aligned [FloatingActionButtonLocation]s.
@override
double getOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) {
- assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
return StandardFabLocation._rightOffsetX(scaffoldGeometry, adjustment);
@@ -676,7 +675,6 @@
/// Calculates x-offset for end-aligned [FloatingActionButtonLocation]s.
@override
double getOffsetX(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) {
- assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
return StandardFabLocation._leftOffsetX(scaffoldGeometry, adjustment);
diff --git a/framework/lib/src/material/floating_action_button_theme.dart b/framework/lib/src/material/floating_action_button_theme.dart
index d8495b8..1c744d5 100644
--- a/framework/lib/src/material/floating_action_button_theme.dart
+++ b/framework/lib/src/material/floating_action_button_theme.dart
@@ -193,7 +193,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static FloatingActionButtonThemeData? lerp(FloatingActionButtonThemeData? a, FloatingActionButtonThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/material/flutter_logo.dart b/framework/lib/src/material/flutter_logo.dart
index 6abc740..2def362 100644
--- a/framework/lib/src/material/flutter_logo.dart
+++ b/framework/lib/src/material/flutter_logo.dart
@@ -28,10 +28,7 @@
this.style = FlutterLogoStyle.markOnly,
this.duration = const Duration(milliseconds: 750),
this.curve = Curves.fastOutSlowIn,
- }) : assert(textColor != null),
- assert(style != null),
- assert(duration != null),
- assert(curve != null);
+ });
/// The size of the logo in logical pixels.
///
diff --git a/framework/lib/src/material/grid_tile.dart b/framework/lib/src/material/grid_tile.dart
index e2560b7..01d5169 100644
--- a/framework/lib/src/material/grid_tile.dart
+++ b/framework/lib/src/material/grid_tile.dart
@@ -25,7 +25,7 @@
this.header,
this.footer,
required this.child,
- }) : assert(child != null);
+ });
/// The widget to show over the top of this grid tile.
///
diff --git a/framework/lib/src/material/icon_button.dart b/framework/lib/src/material/icon_button.dart
index 6020d4e..21f541b 100644
--- a/framework/lib/src/material/icon_button.dart
+++ b/framework/lib/src/material/icon_button.dart
@@ -198,9 +198,7 @@
this.isSelected,
this.selectedIcon,
required this.icon,
- }) : assert(splashRadius == null || splashRadius > 0),
- assert(autofocus != null),
- assert(icon != null);
+ }) : assert(splashRadius == null || splashRadius > 0);
/// The size of the icon inside the button.
///
@@ -962,7 +960,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _IconButtonDefaultsM3 extends ButtonStyle {
_IconButtonDefaultsM3(this.context)
@@ -1011,7 +1009,7 @@
return _colors.onSurfaceVariant.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
- return _colors.onSurfaceVariant.withOpacity(0.08);
+ return _colors.onSurfaceVariant.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurfaceVariant.withOpacity(0.12);
diff --git a/framework/lib/src/material/icon_button_theme.dart b/framework/lib/src/material/icon_button_theme.dart
index 71010c2..1f5261d 100644
--- a/framework/lib/src/material/icon_button_theme.dart
+++ b/framework/lib/src/material/icon_button_theme.dart
@@ -49,7 +49,6 @@
/// Linearly interpolate between two icon button themes.
static IconButtonThemeData? lerp(IconButtonThemeData? a, IconButtonThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -96,7 +95,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The configuration of this theme.
final IconButtonThemeData data;
diff --git a/framework/lib/src/material/icons.dart b/framework/lib/src/material/icons.dart
index 2da17ac..70e4f81 100644
--- a/framework/lib/src/material/icons.dart
+++ b/framework/lib/src/material/icons.dart
@@ -119,9 +119,9 @@
/// ![The following code snippet would generate a row of icons consisting of a pink heart, a green musical note, and a blue umbrella, each progressively bigger than the last.](https://flutter.github.io/assets-for-api-docs/assets/widgets/icon.png)
///
/// ```dart
-/// Row(
+/// const Row(
/// mainAxisAlignment: MainAxisAlignment.spaceAround,
-/// children: const <Widget>[
+/// children: <Widget>[
/// Icon(
/// Icons.favorite,
/// color: Colors.pink,
@@ -149,6 +149,7 @@
/// * [IconButton]
/// * <https://material.io/resources/icons>
/// * [AnimatedIcons], for the list of available animated Material Icons.
+@staticIconProvider
class Icons {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
diff --git a/framework/lib/src/material/ink_decoration.dart b/framework/lib/src/material/ink_decoration.dart
index 8a95a83..3d5dd9d 100644
--- a/framework/lib/src/material/ink_decoration.dart
+++ b/framework/lib/src/material/ink_decoration.dart
@@ -193,10 +193,6 @@
this.height,
this.child,
}) : assert(padding == null || padding.isNonNegative),
- assert(image != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
decoration = BoxDecoration(
image: DecorationImage(
image: image,
@@ -241,10 +237,10 @@
final double? height;
EdgeInsetsGeometry get _paddingIncludingDecoration {
- if (decoration == null || decoration!.padding == null) {
+ if (decoration == null) {
return padding ?? EdgeInsets.zero;
}
- final EdgeInsetsGeometry decorationPadding = decoration!.padding!;
+ final EdgeInsetsGeometry decorationPadding = decoration!.padding;
if (padding == null) {
return decorationPadding;
}
@@ -337,8 +333,7 @@
required super.controller,
required super.referenceBox,
super.onRemoved,
- }) : assert(configuration != null),
- _configuration = configuration {
+ }) : _configuration = configuration {
this.decoration = decoration;
controller.addInkFeature(this);
}
@@ -369,7 +364,6 @@
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
- assert(value != null);
if (value == _configuration) {
return;
}
diff --git a/framework/lib/src/material/ink_highlight.dart b/framework/lib/src/material/ink_highlight.dart
index 10d6b31..a2f14ac 100644
--- a/framework/lib/src/material/ink_highlight.dart
+++ b/framework/lib/src/material/ink_highlight.dart
@@ -48,11 +48,7 @@
RectCallback? rectCallback,
super.onRemoved,
Duration fadeDuration = _kDefaultHighlightFadeDuration,
- }) : assert(color != null),
- assert(shape != null),
- assert(textDirection != null),
- assert(fadeDuration != null),
- _shape = shape,
+ }) : _shape = shape,
_radius = radius,
_borderRadius = borderRadius ?? BorderRadius.zero,
_customBorder = customBorder,
@@ -109,7 +105,6 @@
}
void _paintHighlight(Canvas canvas, Rect rect, Paint paint) {
- assert(_shape != null);
canvas.save();
if (_customBorder != null) {
canvas.clipPath(_customBorder!.getOuterPath(rect, textDirection: _textDirection));
diff --git a/framework/lib/src/material/ink_ripple.dart b/framework/lib/src/material/ink_ripple.dart
index 009df09..8a2fe57 100644
--- a/framework/lib/src/material/ink_ripple.dart
+++ b/framework/lib/src/material/ink_ripple.dart
@@ -119,17 +119,13 @@
ShapeBorder? customBorder,
double? radius,
super.onRemoved,
- }) : assert(color != null),
- assert(position != null),
- assert(textDirection != null),
- _position = position,
+ }) : _position = position,
_borderRadius = borderRadius ?? BorderRadius.zero,
_customBorder = customBorder,
_textDirection = textDirection,
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position),
_clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
super(controller: controller, color: color) {
- assert(_borderRadius != null);
// Immediately begin fading-in the initial splash.
_fadeInController = AnimationController(duration: _kFadeInDuration, vsync: controller.vsync)
@@ -232,10 +228,14 @@
void paintFeature(Canvas canvas, Matrix4 transform) {
final int alpha = _fadeInController.isAnimating ? _fadeIn.value : _fadeOut.value;
final Paint paint = Paint()..color = color.withAlpha(alpha);
+ Rect? rect;
+ if (_clipCallback != null) {
+ rect = _clipCallback!();
+ }
// Splash moves to the center of the reference box.
final Offset center = Offset.lerp(
_position,
- referenceBox.size.center(Offset.zero),
+ rect != null ? rect.center : referenceBox.size.center(Offset.zero),
Curves.ease.transform(_radiusController.value),
)!;
paintInkCircle(
diff --git a/framework/lib/src/material/ink_splash.dart b/framework/lib/src/material/ink_splash.dart
index 66e98c5..805e519 100644
--- a/framework/lib/src/material/ink_splash.dart
+++ b/framework/lib/src/material/ink_splash.dart
@@ -125,8 +125,7 @@
ShapeBorder? customBorder,
double? radius,
super.onRemoved,
- }) : assert(textDirection != null),
- _position = position,
+ }) : _position = position,
_borderRadius = borderRadius ?? BorderRadius.zero,
_customBorder = customBorder,
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position!),
@@ -134,7 +133,6 @@
_repositionToReferenceBox = !containedInkWell,
_textDirection = textDirection,
super(controller: controller, color: color) {
- assert(_borderRadius != null);
_radiusController = AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint)
..forward();
diff --git a/framework/lib/src/material/ink_well.dart b/framework/lib/src/material/ink_well.dart
index 3e264af..018de53 100644
--- a/framework/lib/src/material/ink_well.dart
+++ b/framework/lib/src/material/ink_well.dart
@@ -42,9 +42,7 @@
required super.referenceBox,
required Color color,
super.onRemoved,
- }) : assert(controller != null),
- assert(referenceBox != null),
- _color = color;
+ }) : _color = color;
/// Called when the user input that triggered this feature's appearance was confirmed.
///
@@ -111,12 +109,6 @@
BorderRadius borderRadius = BorderRadius.zero,
RectCallback? clipCallback,
}) {
- assert(canvas != null);
- assert(transform != null);
- assert(paint != null);
- assert(center != null);
- assert(radius != null);
- assert(borderRadius != null);
final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
canvas.save();
@@ -323,12 +315,7 @@
this.onFocusChange,
this.autofocus = false,
this.statesController,
- }) : assert(containedInkWell != null),
- assert(highlightShape != null),
- assert(enableFeedback != null),
- assert(excludeFromSemantics != null),
- assert(autofocus != null),
- assert(canRequestFocus != null);
+ });
/// The widget below this widget in the tree.
///
@@ -688,12 +675,7 @@
this.getRectCallback,
required this.debugCheckContext,
this.statesController,
- }) : assert(containedInkWell != null),
- assert(highlightShape != null),
- assert(enableFeedback != null),
- assert(excludeFromSemantics != null),
- assert(autofocus != null),
- assert(canRequestFocus != null);
+ });
final Widget? child;
final GestureTapCallback? onTap;
@@ -781,7 +763,6 @@
@override
void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) {
- assert(childState != null);
final bool lastAnyPressed = _anyChildInkResponsePressed;
if (value) {
_activeChildren.add(childState);
@@ -842,14 +823,14 @@
widget.radius != oldWidget.radius ||
widget.borderRadius != oldWidget.borderRadius ||
widget.highlightShape != oldWidget.highlightShape) {
- final InkHighlight? hoverHighLight = _highlights[_HighlightType.hover];
- if (hoverHighLight != null) {
- hoverHighLight.dispose();
+ final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
+ if (hoverHighlight != null) {
+ hoverHighlight.dispose();
updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
}
- final InkHighlight? focusHighLight = _highlights[_HighlightType.focus];
- if (focusHighLight != null) {
- focusHighLight.dispose();
+ final InkHighlight? focusHighlight = _highlights[_HighlightType.focus];
+ if (focusHighlight != null) {
+ focusHighlight.dispose();
// Do not call updateFocusHighlights() here because it is called below
}
}
@@ -857,6 +838,13 @@
statesController.update(MaterialState.disabled, !enabled);
if (!enabled) {
statesController.update(MaterialState.pressed, false);
+ // Remove the existing hover highlight immediately when enabled is false.
+ // Do not rely on updateHighlight or InkHighlight.deactivate to not break
+ // the expected lifecycle which is updating _hovering when the mouse exit.
+ // Manually updating _hovering here or calling InkHighlight.deactivate
+ // will lead to onHover not being called or call when it is not allowed.
+ final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
+ hoverHighlight?.dispose();
}
// Don't call widget.onHover because many widgets, including the button
// widgets, apply setState to an ancestor context from onHover.
@@ -937,7 +925,7 @@
_highlights[type] = InkHighlight(
controller: Material.of(context),
referenceBox: referenceBox,
- color: resolvedOverlayColor,
+ color: enabled ? resolvedOverlayColor : resolvedOverlayColor.withAlpha(0),
shape: widget.highlightShape,
radius: widget.radius,
borderRadius: widget.borderRadius,
@@ -1018,7 +1006,7 @@
}
bool get _shouldShowFocus {
- final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
+ final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return enabled && _hasFocus;
@@ -1166,7 +1154,7 @@
}
bool get _canRequestFocus {
- final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
+ final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return enabled && widget.canRequestFocus;
diff --git a/framework/lib/src/material/input_border.dart b/framework/lib/src/material/input_border.dart
index a18cfd2..b1952eb 100644
--- a/framework/lib/src/material/input_border.dart
+++ b/framework/lib/src/material/input_border.dart
@@ -37,7 +37,7 @@
/// [InputDecorator.isFocused].
const InputBorder({
this.borderSide = BorderSide.none,
- }) : assert(borderSide != null);
+ });
/// No input border.
///
@@ -157,7 +157,7 @@
topLeft: Radius.circular(4.0),
topRight: Radius.circular(4.0),
),
- }) : assert(borderRadius != null);
+ });
/// The radii of the border's rounded rectangle corners.
///
@@ -259,12 +259,13 @@
if (other.runtimeType != runtimeType) {
return false;
}
- return other is InputBorder
- && other.borderSide == borderSide;
+ return other is UnderlineInputBorder
+ && other.borderSide == borderSide
+ && other.borderRadius == borderRadius;
}
@override
- int get hashCode => borderSide.hashCode;
+ int get hashCode => Object.hash(borderSide, borderRadius);
}
/// Draws a rounded rectangle around an [InputDecorator]'s container.
@@ -307,8 +308,7 @@
super.borderSide = const BorderSide(),
this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
this.gapPadding = 4.0,
- }) : assert(borderRadius != null),
- assert(gapPadding != null && gapPadding >= 0.0);
+ }) : assert(gapPadding >= 0.0);
// The label text's gap can extend into the corners (even both the top left
// and the top right corner). To avoid the more complicated problem of finding
@@ -515,7 +515,6 @@
double gapPercentage = 0.0,
TextDirection? textDirection,
}) {
- assert(gapExtent != null);
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
assert(_cornersAreCircular(borderRadius));
diff --git a/framework/lib/src/material/input_chip.dart b/framework/lib/src/material/input_chip.dart
index c2d483c..ac5c40d 100644
--- a/framework/lib/src/material/input_chip.dart
+++ b/framework/lib/src/material/input_chip.dart
@@ -114,12 +114,7 @@
'This feature was deprecated after v2.10.0-0.3.pre.'
)
this.useDeleteButtonTooltip = true,
- }) : assert(selected != null),
- assert(isEnabled != null),
- assert(label != null),
- assert(clipBehavior != null),
- assert(autofocus != null),
- assert(pressElevation == null || pressElevation >= 0.0),
+ }) : assert(pressElevation == null || pressElevation >= 0.0),
assert(elevation == null || elevation >= 0.0);
@override
@@ -249,7 +244,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _InputChipDefaultsM3 extends ChipThemeData {
const _InputChipDefaultsM3(this.context, this.isEnabled)
@@ -310,7 +305,7 @@
EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 8.0),
const EdgeInsets.symmetric(horizontal: 4.0),
- clampDouble(MediaQuery.of(context).textScaleFactor - 1.0, 0.0, 1.0),
+ clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
)!;
}
diff --git a/framework/lib/src/material/input_date_picker_form_field.dart b/framework/lib/src/material/input_date_picker_form_field.dart
index 7b6ac92..63acaff 100644
--- a/framework/lib/src/material/input_date_picker_form_field.dart
+++ b/framework/lib/src/material/input_date_picker_form_field.dart
@@ -58,10 +58,7 @@
this.fieldLabelText,
this.keyboardType,
this.autofocus = false,
- }) : assert(firstDate != null),
- assert(lastDate != null),
- assert(autofocus != null),
- initialDate = initialDate != null ? DateUtils.dateOnly(initialDate) : null,
+ }) : initialDate = initialDate != null ? DateUtils.dateOnly(initialDate) : null,
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate) {
assert(
@@ -237,11 +234,16 @@
@override
Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ final bool useMaterial3 = theme.useMaterial3;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme;
+ final InputDecorationTheme inputTheme = theme.inputDecorationTheme;
+ final InputBorder inputBorder = inputTheme.border
+ ?? (useMaterial3 ? const OutlineInputBorder() : const UnderlineInputBorder());
+
return TextFormField(
decoration: InputDecoration(
- border: inputTheme.border ?? const UnderlineInputBorder(),
+ border: inputBorder,
filled: inputTheme.filled,
hintText: widget.fieldHintText ?? localizations.dateHelpText,
labelText: widget.fieldLabelText ?? localizations.dateInputLabel,
diff --git a/framework/lib/src/material/input_decorator.dart b/framework/lib/src/material/input_decorator.dart
index 708283d..7b2b408 100644
--- a/framework/lib/src/material/input_decorator.dart
+++ b/framework/lib/src/material/input_decorator.dart
@@ -12,6 +12,8 @@
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
+import 'icon_button.dart';
+import 'icon_button_theme.dart';
import 'input_border.dart';
import 'material.dart';
import 'material_state.dart';
@@ -22,7 +24,9 @@
// Examples can assume:
// late Widget _myIcon;
-const Duration _kTransitionDuration = Duration(milliseconds: 200);
+// The duration value extracted from:
+// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/textfield/TextInputLayout.java
+const Duration _kTransitionDuration = Duration(milliseconds: 167);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
const double _kFinalLabelScale = 0.75;
@@ -152,9 +156,7 @@
required this.fillColor,
required this.hoverColor,
required this.isHovering,
- }) : assert(border != null),
- assert(gap != null),
- assert(fillColor != null);
+ });
final InputBorder border;
final _InputBorderGap gap;
@@ -192,6 +194,7 @@
_borderAnimation = CurvedAnimation(
parent: _controller,
curve: _kTransitionCurve,
+ reverseCurve: _kTransitionCurve.flipped,
);
_border = _InputBorderTween(
begin: widget.border,
@@ -505,8 +508,7 @@
/// behave.
@immutable
class FloatingLabelAlignment {
- const FloatingLabelAlignment._(this._x) : assert(_x != null),
- assert(_x >= -1.0 && _x <= 1.0);
+ const FloatingLabelAlignment._(this._x) : assert(_x >= -1.0 && _x <= 1.0);
// -1 denotes start, 0 denotes center, and 1 denotes end.
final double _x;
@@ -589,11 +591,7 @@
this.helperError,
this.counter,
this.container,
- }) : assert(contentPadding != null),
- assert(isCollapsed != null),
- assert(floatingLabelHeight != null),
- assert(floatingLabelProgress != null),
- assert(floatingLabelAlignment != null);
+ });
final EdgeInsetsGeometry contentPadding;
final bool isCollapsed;
@@ -705,11 +703,7 @@
required bool expands,
required bool material3,
TextAlignVertical? textAlignVertical,
- }) : assert(decoration != null),
- assert(textDirection != null),
- assert(textBaseline != null),
- assert(expands != null),
- _decoration = decoration,
+ }) : _decoration = decoration,
_textDirection = textDirection,
_textBaseline = textBaseline,
_textAlignVertical = textAlignVertical,
@@ -763,7 +757,6 @@
_Decoration get decoration => _decoration;
_Decoration _decoration;
set decoration(_Decoration value) {
- assert(value != null);
if (_decoration == value) {
return;
}
@@ -774,7 +767,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textDirection == value) {
return;
}
@@ -785,7 +777,6 @@
TextBaseline get textBaseline => _textBaseline;
TextBaseline _textBaseline;
set textBaseline(TextBaseline value) {
- assert(value != null);
if (_textBaseline == value) {
return;
}
@@ -814,7 +805,6 @@
bool get isFocused => _isFocused;
bool _isFocused;
set isFocused(bool value) {
- assert(value != null);
if (_isFocused == value) {
return;
}
@@ -825,7 +815,6 @@
bool get expands => _expands;
bool _expands = false;
set expands(bool value) {
- assert(value != null);
if (_expands == value) {
return;
}
@@ -978,12 +967,12 @@
0.0,
constraints.maxWidth - (
_boxSize(icon).width
- + contentPadding.left
+ + (prefixIcon != null ? 0 : (textDirection == TextDirection.ltr ? contentPadding.left : contentPadding.right))
+ _boxSize(prefixIcon).width
+ _boxSize(prefix).width
+ _boxSize(suffix).width
+ _boxSize(suffixIcon).width
- + contentPadding.right),
+ + (suffixIcon != null ? 0 : (textDirection == TextDirection.ltr ? contentPadding.right : contentPadding.left))),
);
// Increase the available width for the label when it is scaled down.
final double invertedLabelScale = lerpDouble(1.00, 1 / _kFinalLabelScale, decoration.floatingLabelProgress)!;
@@ -1326,6 +1315,35 @@
return Size.zero;
}
+ ChildSemanticsConfigurationsResult _childSemanticsConfigurationDelegate(List<SemanticsConfiguration> childConfigs) {
+ final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
+ List<SemanticsConfiguration>? prefixMergeGroup;
+ List<SemanticsConfiguration>? suffixMergeGroup;
+ for (final SemanticsConfiguration childConfig in childConfigs) {
+ if (childConfig.tagsChildrenWith(_InputDecoratorState._kPrefixSemanticsTag)) {
+ prefixMergeGroup ??= <SemanticsConfiguration>[];
+ prefixMergeGroup.add(childConfig);
+ } else if (childConfig.tagsChildrenWith(_InputDecoratorState._kSuffixSemanticsTag)) {
+ suffixMergeGroup ??= <SemanticsConfiguration>[];
+ suffixMergeGroup.add(childConfig);
+ } else {
+ builder.markAsMergeUp(childConfig);
+ }
+ }
+ if (prefixMergeGroup != null) {
+ builder.markAsSiblingMergeGroup(prefixMergeGroup);
+ }
+ if (suffixMergeGroup != null) {
+ builder.markAsSiblingMergeGroup(suffixMergeGroup);
+ }
+ return builder.build();
+ }
+
+ @override
+ void describeSemanticsConfiguration(SemanticsConfiguration config) {
+ config.childConfigurationsDelegate = _childSemanticsConfigurationDelegate;
+ }
+
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
@@ -1542,7 +1560,7 @@
final double t = decoration.floatingLabelProgress;
// The center of the outline border label ends up a little below the
// center of the top border line.
- final bool isOutlineBorder = decoration.border != null && decoration.border.isOutline;
+ final bool isOutlineBorder = decoration.border.isOutline;
// Temporary opt-in fix for https://github.com/flutter/flutter/issues/54028
// Center the scaled label relative to the border.
final double floatingY = isOutlineBorder ? (-labelHeight * _kFinalLabelScale) / 2.0 + borderWeight / 2.0 : contentPadding.top;
@@ -1600,7 +1618,6 @@
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
- assert(position != null);
for (final RenderBox child in children) {
// The label must be handled specially since we've transformed it.
final Offset offset = _boxParentData(child).offset;
@@ -1639,10 +1656,7 @@
required this.textBaseline,
required this.isFocused,
required this.expands,
- }) : assert(decoration != null),
- assert(textDirection != null),
- assert(textBaseline != null),
- assert(expands != null);
+ });
final _Decoration decoration;
final TextDirection textDirection;
@@ -1713,12 +1727,16 @@
this.text,
this.style,
this.child,
+ this.semanticsSortKey,
+ required this.semanticsTag,
});
final bool labelIsFloating;
final String? text;
final TextStyle? style;
final Widget? child;
+ final SemanticsSortKey? semanticsSortKey;
+ final SemanticsTag semanticsTag;
@override
Widget build(BuildContext context) {
@@ -1728,7 +1746,11 @@
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: labelIsFloating ? 1.0 : 0.0,
- child: child ?? (text == null ? null : Text(text!, style: style)),
+ child: Semantics(
+ sortKey: semanticsSortKey,
+ tagForChildren: semanticsTag,
+ child: child ?? (text == null ? null : Text(text!, style: style)),
+ ),
),
);
}
@@ -1773,11 +1795,7 @@
this.expands = false,
this.isEmpty = false,
this.child,
- }) : assert(decoration != null),
- assert(isFocused != null),
- assert(isHovering != null),
- assert(expands != null),
- assert(isEmpty != null);
+ });
/// The text and styles to use when decorating the child.
///
@@ -1896,9 +1914,15 @@
}
class _InputDecoratorState extends State<InputDecorator> with TickerProviderStateMixin {
- late AnimationController _floatingLabelController;
- late AnimationController _shakingLabelController;
+ late final AnimationController _floatingLabelController;
+ late final Animation<double> _floatingLabelAnimation;
+ late final AnimationController _shakingLabelController;
final _InputBorderGap _borderGap = _InputBorderGap();
+ static const OrdinalSortKey _kPrefixSemanticsSortOrder = OrdinalSortKey(0);
+ static const OrdinalSortKey _kInputSemanticsSortOrder = OrdinalSortKey(1);
+ static const OrdinalSortKey _kSuffixSemanticsSortOrder = OrdinalSortKey(2);
+ static const SemanticsTag _kPrefixSemanticsTag = SemanticsTag('_InputDecoratorState.prefix');
+ static const SemanticsTag _kSuffixSemanticsTag = SemanticsTag('_InputDecoratorState.suffix');
@override
void initState() {
@@ -1914,6 +1938,11 @@
value: labelIsInitiallyFloating ? 1.0 : 0.0,
);
_floatingLabelController.addListener(_handleChange);
+ _floatingLabelAnimation = CurvedAnimation(
+ parent: _floatingLabelController,
+ curve: _kTransitionCurve,
+ reverseCurve: _kTransitionCurve.flipped,
+ );
_shakingLabelController = AnimationController(
duration: _kTransitionDuration,
@@ -2166,7 +2195,6 @@
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
- alwaysIncludeSemantics: isEmpty || (decoration.labelText == null && decoration.label == null),
child: Text(
hintText,
style: hintStyle,
@@ -2191,7 +2219,7 @@
final Widget container = _BorderContainer(
border: border,
gap: _borderGap,
- gapAnimation: _floatingLabelController.view,
+ gapAnimation: _floatingLabelAnimation,
fillColor: _getFillColor(themeData, defaults),
hoverColor: _getHoverColor(themeData),
isHovering: isHovering,
@@ -2218,22 +2246,42 @@
),
);
- final Widget? prefix = decoration.prefix == null && decoration.prefixText == null ? null :
- _AffixText(
- labelIsFloating: widget._labelShouldWithdraw,
- text: decoration.prefixText,
- style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
- child: decoration.prefix,
- );
+ final bool hasPrefix = decoration.prefix != null || decoration.prefixText != null;
+ final bool hasSuffix = decoration.suffix != null || decoration.suffixText != null;
- final Widget? suffix = decoration.suffix == null && decoration.suffixText == null ? null :
- _AffixText(
- labelIsFloating: widget._labelShouldWithdraw,
- text: decoration.suffixText,
- style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
- child: decoration.suffix,
- );
+ Widget? input = widget.child;
+ // If at least two out of the three are visible, it needs semantics sort
+ // order.
+ final bool needsSemanticsSortOrder = widget._labelShouldWithdraw && (input != null ? (hasPrefix || hasSuffix) : (hasPrefix && hasSuffix));
+ final Widget? prefix = hasPrefix
+ ? _AffixText(
+ labelIsFloating: widget._labelShouldWithdraw,
+ text: decoration.prefixText,
+ style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
+ semanticsSortKey: needsSemanticsSortOrder ? _kPrefixSemanticsSortOrder : null,
+ semanticsTag: _kPrefixSemanticsTag,
+ child: decoration.prefix,
+ )
+ : null;
+
+ final Widget? suffix = hasSuffix
+ ? _AffixText(
+ labelIsFloating: widget._labelShouldWithdraw,
+ text: decoration.suffixText,
+ style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
+ semanticsSortKey: needsSemanticsSortOrder ? _kSuffixSemanticsSortOrder : null,
+ semanticsTag: _kSuffixSemanticsTag,
+ child: decoration.suffix,
+ )
+ : null;
+
+ if (input != null && needsSemanticsSortOrder) {
+ input = Semantics(
+ sortKey: _kInputSemanticsSortOrder,
+ child: input,
+ );
+ }
final bool decorationIsDense = decoration.isDense ?? false;
final double iconSize = decorationIsDense ? 18.0 : 24.0;
@@ -2261,18 +2309,28 @@
cursor: SystemMouseCursors.basic,
child: ConstrainedBox(
constraints: decoration.prefixIconConstraints ??
- themeData.visualDensity.effectiveConstraints(
- const BoxConstraints(
- minWidth: kMinInteractiveDimension,
- minHeight: kMinInteractiveDimension,
- ),
+ themeData.visualDensity.effectiveConstraints(
+ const BoxConstraints(
+ minWidth: kMinInteractiveDimension,
+ minHeight: kMinInteractiveDimension,
),
+ ),
child: IconTheme.merge(
data: IconThemeData(
color: _getPrefixIconColor(themeData, defaults),
size: iconSize,
),
- child: decoration.prefixIcon!,
+ child: IconButtonTheme(
+ data: IconButtonThemeData(
+ style: IconButton.styleFrom(
+ foregroundColor: _getPrefixIconColor(themeData, defaults),
+ iconSize: iconSize,
+ ),
+ ),
+ child: Semantics(
+ child: decoration.prefixIcon,
+ ),
+ ),
),
),
),
@@ -2286,22 +2344,32 @@
cursor: SystemMouseCursors.basic,
child: ConstrainedBox(
constraints: decoration.suffixIconConstraints ??
- themeData.visualDensity.effectiveConstraints(
- const BoxConstraints(
- minWidth: kMinInteractiveDimension,
- minHeight: kMinInteractiveDimension,
+ themeData.visualDensity.effectiveConstraints(
+ const BoxConstraints(
+ minWidth: kMinInteractiveDimension,
+ minHeight: kMinInteractiveDimension,
+ ),
+ ),
+ child: IconTheme.merge(
+ data: IconThemeData(
+ color: _getSuffixIconColor(themeData, defaults),
+ size: iconSize,
+ ),
+ child: IconButtonTheme(
+ data: IconButtonThemeData(
+ style: IconButton.styleFrom(
+ foregroundColor: _getSuffixIconColor(themeData, defaults),
+ iconSize: iconSize,
+ ),
+ ),
+ child: Semantics(
+ child: decoration.suffixIcon,
),
),
- child: IconTheme.merge(
- data: IconThemeData(
- color: _getSuffixIconColor(themeData, defaults),
- size: iconSize,
),
- child: decoration.suffixIcon!,
),
),
- ),
- );
+ );
final Widget helperError = _HelperError(
textAlign: textAlign,
@@ -2367,14 +2435,14 @@
isCollapsed: decoration.isCollapsed,
floatingLabelHeight: floatingLabelHeight,
floatingLabelAlignment: decoration.floatingLabelAlignment!,
- floatingLabelProgress: _floatingLabelController.value,
+ floatingLabelProgress: _floatingLabelAnimation.value,
border: border,
borderGap: _borderGap,
alignLabelWithHint: decoration.alignLabelWithHint ?? false,
isDense: decoration.isDense,
visualDensity: themeData.visualDensity,
icon: icon,
- input: widget.child,
+ input: input,
label: label,
hint: hint,
prefix: prefix,
@@ -2542,8 +2610,7 @@
this.semanticCounterText,
this.alignLabelWithHint,
this.constraints,
- }) : assert(enabled != null),
- assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'),
+ }) : assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'),
assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'),
assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.');
@@ -2564,8 +2631,7 @@
this.hoverColor,
this.border = InputBorder.none,
this.enabled = true,
- }) : assert(enabled != null),
- icon = null,
+ }) : icon = null,
iconColor = null,
label = null,
labelText = null,
@@ -3780,11 +3846,7 @@
this.border,
this.alignLabelWithHint = false,
this.constraints,
- }) : assert(isDense != null),
- assert(isCollapsed != null),
- assert(floatingLabelAlignment != null),
- assert(filled != null),
- assert(alignLabelWithHint != null);
+ });
/// {@macro flutter.material.inputDecoration.labelStyle}
final TextStyle? labelStyle;
@@ -4480,7 +4542,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _InputDecoratorDefaultsM3 extends InputDecorationTheme {
_InputDecoratorDefaultsM3(this.context)
@@ -4580,7 +4642,7 @@
@override
TextStyle? get labelStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
- final TextStyle textStyle= _textTheme.bodyLarge ?? const TextStyle();
+ final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle();
if(states.contains(MaterialState.error)) {
if (states.contains(MaterialState.focused)) {
return textStyle.copyWith(color:_colors.error);
@@ -4604,7 +4666,7 @@
@override
TextStyle? get floatingLabelStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
- final TextStyle textStyle= _textTheme.bodyLarge ?? const TextStyle();
+ final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle();
if(states.contains(MaterialState.error)) {
if (states.contains(MaterialState.focused)) {
return textStyle.copyWith(color:_colors.error);
@@ -4628,7 +4690,7 @@
@override
TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
- final TextStyle textStyle= _textTheme.bodySmall ?? const TextStyle();
+ final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle();
if (states.contains(MaterialState.disabled)) {
return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
}
@@ -4637,7 +4699,7 @@
@override
TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
- final TextStyle textStyle= _textTheme.bodySmall ?? const TextStyle();
+ final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle();
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 d485f5f..c8af8a0 100644
--- a/framework/lib/src/material/list_tile.dart
+++ b/framework/lib/src/material/list_tile.dart
@@ -7,14 +7,18 @@
import 'package:flute/rendering.dart';
import 'package:flute/widgets.dart';
+import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'divider.dart';
+import 'icon_button.dart';
+import 'icon_button_theme.dart';
import 'ink_decoration.dart';
import 'ink_well.dart';
import 'list_tile_theme.dart';
import 'material_state.dart';
+import 'text_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
@@ -99,9 +103,9 @@
///
/// {@tool snippet}
/// ```dart
-/// Container(
+/// const ColoredBox(
/// color: Colors.green,
-/// child: const Material(
+/// child: Material(
/// child: ListTile(
/// title: Text('ListTile with red background'),
/// tileColor: Colors.red,
@@ -137,15 +141,29 @@
/// ** See code in examples/api/lib/material/list_tile/list_tile.1.dart **
/// {@end-tool}
///
-/// {@tool snippet}
+/// {@tool dartpad}
+/// This sample shows the creation of a [ListTile] using [ThemeData.useMaterial3] flag,
+/// as described in: https://m3.material.io/components/lists/overview.
///
+/// ** See code in examples/api/lib/material/list_tile/list_tile.2.dart **
+/// {@end-tool}
+///
+/// {@tool dartpad}
+/// This sample shows [ListTile]'s [textColor] and [iconColor] can use
+/// [MaterialStateColor] color to change the color of the text and icon
+/// when the [ListTile] is enabled, selected, or disabled.
+///
+/// ** See code in examples/api/lib/material/list_tile/list_tile.3.dart **
+/// {@end-tool}
+///
+/// {@tool snippet}
/// To use a [ListTile] within a [Row], it needs to be wrapped in an
/// [Expanded] widget. [ListTile] requires fixed width constraints,
/// whereas a [Row] does not constrain its children.
///
/// ```dart
-/// Row(
-/// children: const <Widget>[
+/// const Row(
+/// children: <Widget>[
/// Expanded(
/// child: ListTile(
/// leading: FlutterLogo(),
@@ -229,7 +247,7 @@
///
/// ![Custom list item a](https://flutter.github.io/assets-for-api-docs/assets/widgets/custom_list_item_a.png)
///
-/// ** See code in examples/api/lib/material/list_tile/list_tile.4.dart **
+/// ** See code in examples/api/lib/material/list_tile/custom_list_item.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
@@ -239,7 +257,7 @@
///
/// ![Custom list item b](https://flutter.github.io/assets-for-api-docs/assets/widgets/custom_list_item_b.png)
///
-/// ** See code in examples/api/lib/material/list_tile/list_tile.5.dart **
+/// ** See code in examples/api/lib/material/list_tile/custom_list_item.1.dart **
/// {@end-tool}
///
/// See also:
@@ -254,10 +272,10 @@
/// * [ListTile.divideTiles], a utility for inserting [Divider]s in between [ListTile]s.
/// * [CheckboxListTile], [RadioListTile], and [SwitchListTile], widgets
/// that combine [ListTile] with other controls.
-/// * <https://material.io/design/components/lists.html>
+/// * Material 3 [ListTile] specifications are referenced from <https://m3.material.io/components/lists/specs>
+/// and Material 2 [ListTile] specifications are referenced from <https://material.io/design/components/lists.html>
/// * Cookbook: [Use lists](https://flutter.dev/docs/cookbook/lists/basic-list)
/// * Cookbook: [Implement swipe to dismiss](https://flutter.dev/docs/cookbook/gestures/dismissible)
-// TODO(plg): Add link to m3 spec below m2 spec link when available
class ListTile extends StatelessWidget {
/// Creates a list tile.
///
@@ -278,6 +296,9 @@
this.selectedColor,
this.iconColor,
this.textColor,
+ this.titleTextStyle,
+ this.subtitleTextStyle,
+ this.leadingAndTrailingTextStyle,
this.contentPadding,
this.enabled = true,
this.onTap,
@@ -296,11 +317,7 @@
this.horizontalTitleGap,
this.minVerticalPadding,
this.minLeadingWidth,
- }) : assert(isThreeLine != null),
- assert(enabled != null),
- assert(selected != null),
- assert(autofocus != null),
- assert(!isThreeLine || subtitle != null);
+ }) : assert(!isThreeLine || subtitle != null);
/// A widget to display before the title.
///
@@ -364,6 +381,8 @@
/// If this property is null then its value is based on [ListTileTheme.dense].
///
/// Dense list tiles default to a smaller height.
+ ///
+ /// It is not recommended to set [dense] to true when [ThemeData.useMaterial3] is true.
final bool? dense;
/// Defines how compact the list tile's layout will be.
@@ -402,7 +421,16 @@
/// Defines the default color for [leading] and [trailing] icons.
///
- /// If this property is null then [ListTileThemeData.iconColor] is used.
+ /// If this property is null and [selected] is false then [ListTileThemeData.iconColor]
+ /// is used. If that is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface]
+ /// is used, otherwise if [ThemeData.brightness] is [Brightness.light], [Colors.black54] is used,
+ /// and if [ThemeData.brightness] is [Brightness.dark], the value is null.
+ ///
+ /// If this property is null and [selected] is true then [ListTileThemeData.selectedColor]
+ /// is used. If that is also null then [ColorScheme.primary] is used.
+ ///
+ /// If this color is a [MaterialStateColor] it will be resolved against
+ /// [MaterialState.selected] and [MaterialState.disabled] states.
///
/// See also:
///
@@ -410,10 +438,18 @@
/// [ListTileThemeData].
final Color? iconColor;
- /// Defines the default color for the [title] and [subtitle].
+ /// Defines the text color for the [title], [subtitle], [leading], and [trailing].
///
- /// If this property is null then [ListTileThemeData.textColor] is used. If that
- /// is also null then [ColorScheme.primary] is used.
+ /// If this property is null and [selected] is false then [ListTileThemeData.textColor]
+ /// is used. If that is also null then default text color is used for the [title], [subtitle]
+ /// [leading], and [trailing]. Except for [subtitle], if [ThemeData.useMaterial3] is false,
+ /// [TextTheme.bodySmall] is used.
+ ///
+ /// If this property is null and [selected] is true then [ListTileThemeData.selectedColor]
+ /// is used. If that is also null then [ColorScheme.primary] is used.
+ ///
+ /// If this color is a [MaterialStateColor] it will be resolved against
+ /// [MaterialState.selected] and [MaterialState.disabled] states.
///
/// See also:
///
@@ -421,6 +457,28 @@
/// [ListTileThemeData].
final Color? textColor;
+ /// The text style for ListTile's [title].
+ ///
+ /// If this property is null, then [ListTileThemeData.titleTextStyle] is used.
+ /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.bodyLarge]
+ /// will be used. Otherwise, If ListTile style is [ListTileStyle.list],
+ /// [TextTheme.titleMedium] will be used and if ListTile style is [ListTileStyle.drawer],
+ /// [TextTheme.bodyLarge] will be used.
+ final TextStyle? titleTextStyle;
+
+ /// The text style for ListTile's [subtitle].
+ ///
+ /// If this property is null, then [ListTileThemeData.subtitleTextStyle] is used.
+ /// If that is also null, [TextTheme.bodyMedium] will be used.
+ final TextStyle? subtitleTextStyle;
+
+ /// The text style for ListTile's [leading] and [trailing].
+ ///
+ /// If this property is null, then [ListTileThemeData.leadingAndTrailingTextStyle] is used.
+ /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.labelSmall]
+ /// will be used, otherwise [TextTheme.bodyMedium] will be used.
+ final TextStyle? leadingAndTrailingTextStyle;
+
/// Defines the font used for the [title].
///
/// If this property is null then [ListTileThemeData.style] is used. If that
@@ -512,8 +570,11 @@
/// {@template flutter.material.ListTile.tileColor}
/// Defines the background color of `ListTile` when [selected] is false.
///
- /// When the value is null, the [tileColor] is set to [ListTileTheme.tileColor]
- /// if it's not null and to [Colors.transparent] if it's null.
+ /// If this property is null and [selected] is false then [ListTileThemeData.tileColor]
+ /// is used. If that is also null and [selected] is true, [selectedTileColor] is used.
+ /// When that is also null, the [ListTileTheme.selectedTileColor] is used, otherwise
+ /// [Colors.transparent] is used.
+ ///
/// {@endtemplate}
final Color? tileColor;
@@ -562,7 +623,6 @@
///
/// * [Divider], which you can use to obtain this effect manually.
static Iterable<Widget> divideTiles({ BuildContext? context, required Iterable<Widget> tiles, Color? color }) {
- assert(tiles != null);
assert(color != null || context != null);
tiles = tiles.toList();
@@ -588,91 +648,15 @@
];
}
- Color? _iconColor(ThemeData theme, ListTileThemeData tileTheme) {
- if (!enabled) {
- return theme.disabledColor;
- }
-
- if (selected) {
- return selectedColor ?? tileTheme.selectedColor ?? theme.listTileTheme.selectedColor ?? theme.colorScheme.primary;
- }
-
- final Color? color = iconColor
- ?? tileTheme.iconColor
- ?? theme.listTileTheme.iconColor
- // If [ThemeData.useMaterial3] is set to true the disabled icon color
- // will be set to Theme.colorScheme.onSurface(0.38), if false, defaults to null,
- // as described in: https://m3.material.io/components/icon-buttons/specs.
- ?? (theme.useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.38) : null);
- if (color != null) {
- return color;
- }
-
- switch (theme.brightness) {
- case Brightness.light:
- // For the sake of backwards compatibility, the default for unselected
- // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73).
- return Colors.black45;
- case Brightness.dark:
- return null; // null - use current icon theme color
- }
- }
-
- Color? _textColor(ThemeData theme, ListTileThemeData tileTheme, Color? defaultColor) {
- if (!enabled) {
- return theme.disabledColor;
- }
-
- if (selected) {
- return selectedColor ?? tileTheme.selectedColor ?? theme.listTileTheme.selectedColor ?? theme.colorScheme.primary;
- }
-
- return textColor ?? tileTheme.textColor ?? theme.listTileTheme.textColor ?? defaultColor;
- }
-
bool _isDenseLayout(ThemeData theme, ListTileThemeData tileTheme) {
return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false;
}
- TextStyle _titleTextStyle(ThemeData theme, ListTileThemeData tileTheme) {
- final TextStyle textStyle;
- switch(style ?? tileTheme.style ?? theme.listTileTheme.style ?? ListTileStyle.list) {
- case ListTileStyle.drawer:
- textStyle = theme.useMaterial3 ? theme.textTheme.bodyMedium! : theme.textTheme.bodyLarge!;
- break;
- case ListTileStyle.list:
- textStyle = theme.useMaterial3 ? theme.textTheme.titleMedium! : theme.textTheme.titleMedium!;
- break;
- }
- final Color? color = _textColor(theme, tileTheme, textStyle.color);
- return _isDenseLayout(theme, tileTheme)
- ? textStyle.copyWith(fontSize: 13.0, color: color)
- : textStyle.copyWith(color: color);
- }
-
- TextStyle _subtitleTextStyle(ThemeData theme, ListTileThemeData tileTheme) {
- final TextStyle textStyle = theme.useMaterial3 ? theme.textTheme.bodyMedium! : theme.textTheme.bodyMedium!;
- final Color? color = _textColor(
- theme,
- tileTheme,
- theme.useMaterial3 ? theme.textTheme.bodySmall!.color : theme.textTheme.bodySmall!.color,
- );
- return _isDenseLayout(theme, tileTheme)
- ? textStyle.copyWith(color: color, fontSize: 12.0)
- : textStyle.copyWith(color: color);
- }
-
- TextStyle _trailingAndLeadingTextStyle(ThemeData theme, ListTileThemeData tileTheme) {
- final TextStyle textStyle = theme.useMaterial3 ? theme.textTheme.bodyMedium! : theme.textTheme.bodyMedium!;
- final Color? color = _textColor(theme, tileTheme, textStyle.color);
- return textStyle.copyWith(color: color);
- }
-
- Color _tileBackgroundColor(ThemeData theme, ListTileThemeData tileTheme) {
+ Color _tileBackgroundColor(ThemeData theme, ListTileThemeData tileTheme, ListTileThemeData defaults) {
final Color? color = selected
? selectedTileColor ?? tileTheme.selectedTileColor ?? theme.listTileTheme.selectedTileColor
: tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor;
- return color ?? Colors.transparent;
+ return color ?? defaults.tileColor!;
}
@override
@@ -680,23 +664,66 @@
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
final ListTileThemeData tileTheme = ListTileTheme.of(context);
- final IconThemeData iconThemeData = IconThemeData(color: _iconColor(theme, tileTheme));
+ final ListTileStyle listTileStyle = style
+ ?? tileTheme.style
+ ?? theme.listTileTheme.style
+ ?? ListTileStyle.list;
+ final ListTileThemeData defaults = theme.useMaterial3
+ ? _LisTileDefaultsM3(context)
+ : _LisTileDefaultsM2(context, listTileStyle);
+ final Set<MaterialState> states = <MaterialState>{
+ if (!enabled) MaterialState.disabled,
+ if (selected) MaterialState.selected,
+ };
- TextStyle? leadingAndTrailingTextStyle;
+ Color? resolveColor(Color? explicitColor, Color? selectedColor, Color? enabledColor, [Color? disabledColor]) {
+ return _IndividualOverrides(
+ explicitColor: explicitColor,
+ selectedColor: selectedColor,
+ enabledColor: enabledColor,
+ disabledColor: disabledColor,
+ ).resolve(states);
+ }
+
+ final Color? effectiveIconColor = resolveColor(iconColor, selectedColor, iconColor)
+ ?? resolveColor(tileTheme.iconColor, tileTheme.selectedColor, tileTheme.iconColor)
+ ?? resolveColor(theme.listTileTheme.iconColor, theme.listTileTheme.selectedColor, theme.listTileTheme.iconColor)
+ ?? resolveColor(defaults.iconColor, defaults.selectedColor, defaults.iconColor, theme.disabledColor);
+ final Color? effectiveColor = resolveColor(textColor, selectedColor, textColor)
+ ?? resolveColor(tileTheme.textColor, tileTheme.selectedColor, tileTheme.textColor)
+ ?? resolveColor(theme.listTileTheme.textColor, theme.listTileTheme.selectedColor, theme.listTileTheme.textColor)
+ ?? resolveColor(defaults.textColor, defaults.selectedColor, defaults.textColor, theme.disabledColor);
+ final IconThemeData iconThemeData = IconThemeData(color: effectiveIconColor);
+ final IconButtonThemeData iconButtonThemeData = IconButtonThemeData(
+ style: IconButton.styleFrom(foregroundColor: effectiveIconColor),
+ );
+
+ TextStyle? leadingAndTrailingStyle;
if (leading != null || trailing != null) {
- leadingAndTrailingTextStyle = _trailingAndLeadingTextStyle(theme, tileTheme);
+ leadingAndTrailingStyle = leadingAndTrailingTextStyle
+ ?? tileTheme.leadingAndTrailingTextStyle
+ ?? defaults.leadingAndTrailingTextStyle!;
+ final Color? leadingAndTrailingTextColor = effectiveColor;
+ leadingAndTrailingStyle = leadingAndTrailingStyle.copyWith(color: leadingAndTrailingTextColor);
}
Widget? leadingIcon;
if (leading != null) {
leadingIcon = AnimatedDefaultTextStyle(
- style: leadingAndTrailingTextStyle!,
+ style: leadingAndTrailingStyle!,
duration: kThemeChangeDuration,
child: leading!,
);
}
- final TextStyle titleStyle = _titleTextStyle(theme, tileTheme);
+ TextStyle titleStyle = titleTextStyle
+ ?? tileTheme.titleTextStyle
+ ?? defaults.titleTextStyle!;
+ final Color? titleColor = effectiveColor;
+ titleStyle = titleStyle.copyWith(
+ color: titleColor,
+ fontSize: _isDenseLayout(theme, tileTheme) ? 13.0 : null,
+ );
final Widget titleText = AnimatedDefaultTextStyle(
style: titleStyle,
duration: kThemeChangeDuration,
@@ -706,7 +733,14 @@
Widget? subtitleText;
TextStyle? subtitleStyle;
if (subtitle != null) {
- subtitleStyle = _subtitleTextStyle(theme, tileTheme);
+ subtitleStyle = subtitleTextStyle
+ ?? tileTheme.subtitleTextStyle
+ ?? defaults.subtitleTextStyle!;
+ final Color? subtitleColor = effectiveColor ?? theme.textTheme.bodySmall!.color;
+ subtitleStyle = subtitleStyle.copyWith(
+ color: subtitleColor,
+ fontSize: _isDenseLayout(theme, tileTheme) ? 12.0 : null,
+ );
subtitleText = AnimatedDefaultTextStyle(
style: subtitleStyle,
duration: kThemeChangeDuration,
@@ -717,26 +751,23 @@
Widget? trailingIcon;
if (trailing != null) {
trailingIcon = AnimatedDefaultTextStyle(
- style: leadingAndTrailingTextStyle!,
+ style: leadingAndTrailingStyle!,
duration: kThemeChangeDuration,
child: trailing!,
);
}
- const EdgeInsets defaultContentPadding = EdgeInsets.symmetric(horizontal: 16.0);
final TextDirection textDirection = Directionality.of(context);
final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection)
?? tileTheme.contentPadding?.resolve(textDirection)
- ?? defaultContentPadding;
-
- final Set<MaterialState> states = <MaterialState>{
+ ?? defaults.contentPadding!.resolve(textDirection);
+ // Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
+ final Set<MaterialState> mouseStates = <MaterialState>{
if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled,
- if (selected) MaterialState.selected,
};
-
- final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(mouseCursor, states)
- ?? tileTheme.mouseCursor?.resolve(states)
- ?? MaterialStateMouseCursor.clickable.resolve(states);
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates)
+ ?? tileTheme.mouseCursor?.resolve(mouseStates)
+ ?? MaterialStateMouseCursor.clickable.resolve(mouseStates);
return InkWell(
customBorder: shape ?? tileTheme.shape,
@@ -757,7 +788,7 @@
child: Ink(
decoration: ShapeDecoration(
shape: shape ?? tileTheme.shape ?? const Border(),
- color: _tileBackgroundColor(theme, tileTheme),
+ color: _tileBackgroundColor(theme, tileTheme, defaults),
),
child: SafeArea(
top: false,
@@ -765,24 +796,28 @@
minimum: resolvedContentPadding,
child: IconTheme.merge(
data: iconThemeData,
- child: _ListTile(
- leading: leadingIcon,
- title: titleText,
- subtitle: subtitleText,
- trailing: trailingIcon,
- isDense: _isDenseLayout(theme, tileTheme),
- visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity,
- isThreeLine: isThreeLine,
- textDirection: textDirection,
- titleBaselineType: titleStyle.textBaseline!,
- subtitleBaselineType: subtitleStyle?.textBaseline,
- horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
- minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? 4,
- minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? 40,
+ child: IconButtonTheme(
+ data: iconButtonThemeData,
+ child: _ListTile(
+ leading: leadingIcon,
+ title: titleText,
+ subtitle: subtitleText,
+ trailing: trailingIcon,
+ isDense: _isDenseLayout(theme, tileTheme),
+ visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity,
+ isThreeLine: isThreeLine,
+ textDirection: textDirection,
+ titleBaselineType: titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!,
+ subtitleBaselineType: subtitleStyle?.textBaseline ?? defaults.subtitleTextStyle!.textBaseline!,
+ horizontalTitleGap: horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
+ minVerticalPadding: minVerticalPadding ?? tileTheme.minVerticalPadding ?? defaults.minVerticalPadding!,
+ minLeadingWidth: minLeadingWidth ?? tileTheme.minLeadingWidth ?? defaults.minLeadingWidth!,
+ material3: theme.useMaterial3,
+ ),
),
),
),
- ),
+ ),
),
);
}
@@ -802,6 +837,9 @@
properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null));
properties.add(ColorProperty('iconColor', iconColor, defaultValue: null));
properties.add(ColorProperty('textColor', textColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('titleTextStyle', titleTextStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('subtitleTextStyle', subtitleTextStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('leadingAndTrailingTextStyle', leadingAndTrailingTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('contentPadding', contentPadding, defaultValue: null));
properties.add(FlagProperty('enabled', value: enabled, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: true));
properties.add(DiagnosticsProperty<Function>('onTap', onTap, defaultValue: null));
@@ -821,6 +859,34 @@
}
}
+class _IndividualOverrides extends MaterialStateProperty<Color?> {
+ _IndividualOverrides({
+ this.explicitColor,
+ this.enabledColor,
+ this.selectedColor,
+ this.disabledColor,
+ });
+
+ final Color? explicitColor;
+ final Color? enabledColor;
+ final Color? selectedColor;
+ final Color? disabledColor;
+
+ @override
+ Color? resolve(Set<MaterialState> states) {
+ if (explicitColor is MaterialStateColor) {
+ return MaterialStateProperty.resolveAs<Color?>(explicitColor, states);
+ }
+ if (states.contains(MaterialState.disabled)) {
+ return disabledColor;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return selectedColor;
+ }
+ return enabledColor;
+ }
+}
+
// Identifies the children of a _ListTileElement.
enum _ListTileSlot {
leading,
@@ -844,14 +910,8 @@
required this.minVerticalPadding,
required this.minLeadingWidth,
this.subtitleBaselineType,
- }) : assert(isThreeLine != null),
- assert(isDense != null),
- assert(visualDensity != null),
- assert(textDirection != null),
- assert(titleBaselineType != null),
- assert(horizontalTitleGap != null),
- assert(minVerticalPadding != null),
- assert(minLeadingWidth != null);
+ required this.material3,
+ });
final Widget? leading;
final Widget title;
@@ -866,6 +926,7 @@
final double horizontalTitleGap;
final double minVerticalPadding;
final double minLeadingWidth;
+ final bool material3;
@override
Iterable<_ListTileSlot> get slots => _ListTileSlot.values;
@@ -896,6 +957,7 @@
horizontalTitleGap: horizontalTitleGap,
minVerticalPadding: minVerticalPadding,
minLeadingWidth: minLeadingWidth,
+ material3: material3,
);
}
@@ -910,7 +972,8 @@
..subtitleBaselineType = subtitleBaselineType
..horizontalTitleGap = horizontalTitleGap
..minLeadingWidth = minLeadingWidth
- ..minVerticalPadding = minVerticalPadding;
+ ..minVerticalPadding = minVerticalPadding
+ ..material3 = material3;
}
}
@@ -925,15 +988,8 @@
required double horizontalTitleGap,
required double minVerticalPadding,
required double minLeadingWidth,
- }) : assert(isDense != null),
- assert(visualDensity != null),
- assert(isThreeLine != null),
- assert(textDirection != null),
- assert(titleBaselineType != null),
- assert(horizontalTitleGap != null),
- assert(minVerticalPadding != null),
- assert(minLeadingWidth != null),
- _isDense = isDense,
+ required bool material3,
+ }) : _isDense = isDense,
_visualDensity = visualDensity,
_isThreeLine = isThreeLine,
_textDirection = textDirection,
@@ -941,7 +997,8 @@
_subtitleBaselineType = subtitleBaselineType,
_horizontalTitleGap = horizontalTitleGap,
_minVerticalPadding = minVerticalPadding,
- _minLeadingWidth = minLeadingWidth;
+ _minLeadingWidth = minLeadingWidth,
+ _material3 = material3;
RenderBox? get leading => childForSlot(_ListTileSlot.leading);
RenderBox? get title => childForSlot(_ListTileSlot.title);
@@ -966,7 +1023,6 @@
bool get isDense => _isDense;
bool _isDense;
set isDense(bool value) {
- assert(value != null);
if (_isDense == value) {
return;
}
@@ -977,7 +1033,6 @@
VisualDensity get visualDensity => _visualDensity;
VisualDensity _visualDensity;
set visualDensity(VisualDensity value) {
- assert(value != null);
if (_visualDensity == value) {
return;
}
@@ -988,7 +1043,6 @@
bool get isThreeLine => _isThreeLine;
bool _isThreeLine;
set isThreeLine(bool value) {
- assert(value != null);
if (_isThreeLine == value) {
return;
}
@@ -999,7 +1053,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textDirection == value) {
return;
}
@@ -1010,7 +1063,6 @@
TextBaseline get titleBaselineType => _titleBaselineType;
TextBaseline _titleBaselineType;
set titleBaselineType(TextBaseline value) {
- assert(value != null);
if (_titleBaselineType == value) {
return;
}
@@ -1033,7 +1085,6 @@
double get _effectiveHorizontalTitleGap => _horizontalTitleGap + visualDensity.horizontal * 2.0;
set horizontalTitleGap(double value) {
- assert(value != null);
if (_horizontalTitleGap == value) {
return;
}
@@ -1045,7 +1096,6 @@
double _minVerticalPadding;
set minVerticalPadding(double value) {
- assert(value != null);
if (_minVerticalPadding == value) {
return;
}
@@ -1057,7 +1107,6 @@
double _minLeadingWidth;
set minLeadingWidth(double value) {
- assert(value != null);
if (_minLeadingWidth == value) {
return;
}
@@ -1065,6 +1114,16 @@
markNeedsLayout();
}
+ bool get material3 => _material3;
+ bool _material3;
+ set material3(bool value) {
+ if (_material3 == value) {
+ return;
+ }
+ _material3 = value;
+ markNeedsLayout();
+ }
+
@override
bool get sizedByParent => false;
@@ -1253,23 +1312,33 @@
}
}
- // This attempts to implement the redlines for the vertical position of the
- // leading and trailing icons on the spec page:
- // https://material.io/design/components/lists.html#specs
- // The interpretation for these redlines is as follows:
- // - For large tiles (> 72dp), both leading and trailing controls should be
- // a fixed distance from top. As per guidelines this is set to 16dp.
- // - For smaller tiles, trailing should always be centered. Leading can be
- // centered or closer to the top. It should never be further than 16dp
- // to the top.
final double leadingY;
final double trailingY;
- if (tileHeight > 72.0) {
- leadingY = 16.0;
- trailingY = 16.0;
+ if (material3) {
+ if (isThreeLine) {
+ leadingY = _minVerticalPadding;
+ trailingY = _minVerticalPadding;
+ } else {
+ leadingY = (tileHeight - leadingSize.height) / 2.0;
+ trailingY = (tileHeight - trailingSize.height) / 2.0;
+ }
} else {
- leadingY = math.min((tileHeight - leadingSize.height) / 2.0, 16.0);
- trailingY = (tileHeight - trailingSize.height) / 2.0;
+ // This attempts to implement the redlines for the vertical position of the
+ // leading and trailing icons on the spec page:
+ // https://material.io/design/components/lists.html#specs
+ // The interpretation for these redlines is as follows:
+ // - For large tiles (> 72dp), both leading and trailing controls should be
+ // a fixed distance from top. As per guidelines this is set to 16dp.
+ // - For smaller tiles, trailing should always be centered. Leading can be
+ // centered or closer to the top. It should never be further than 16dp
+ // to the top.
+ if (tileHeight > 72.0) {
+ leadingY = 16.0;
+ trailingY = 16.0;
+ } else {
+ leadingY = math.min((tileHeight - leadingSize.height) / 2.0, 16.0);
+ trailingY = (tileHeight - trailingSize.height) / 2.0;
+ }
}
switch (textDirection) {
@@ -1325,7 +1394,6 @@
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
- assert(position != null);
for (final RenderBox child in children) {
final BoxParentData parentData = child.parentData! as BoxParentData;
final bool isHit = result.addWithPaintOffset(
@@ -1343,3 +1411,96 @@
return false;
}
}
+
+class _LisTileDefaultsM2 extends ListTileThemeData {
+ _LisTileDefaultsM2(this.context, ListTileStyle style)
+ : super(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
+ minLeadingWidth: 40,
+ minVerticalPadding: 4,
+ shape: const Border(),
+ style: style,
+ );
+
+ final BuildContext context;
+ late final ThemeData _theme = Theme.of(context);
+ late final TextTheme _textTheme = _theme.textTheme;
+
+ @override
+ Color? get tileColor => Colors.transparent;
+
+ @override
+ TextStyle? get titleTextStyle {
+ switch (style!) {
+ case ListTileStyle.drawer:
+ return _textTheme.bodyLarge;
+ case ListTileStyle.list:
+ return _textTheme.titleMedium;
+ }
+ }
+
+ @override
+ TextStyle? get subtitleTextStyle => _textTheme.bodyMedium;
+
+ @override
+ TextStyle? get leadingAndTrailingTextStyle => _textTheme.bodyMedium;
+
+ @override
+ Color? get selectedColor => _theme.colorScheme.primary;
+
+ @override
+ Color? get iconColor {
+ switch (_theme.brightness) {
+ case Brightness.light:
+ // For the sake of backwards compatibility, the default for unselected
+ // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73).
+ return Colors.black45;
+ case Brightness.dark:
+ return null; // null, Use current icon theme color
+ }
+ }
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES - LisTile
+
+// 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 _LisTileDefaultsM3 extends ListTileThemeData {
+ _LisTileDefaultsM3(this.context)
+ : super(
+ contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 24.0),
+ minLeadingWidth: 24,
+ minVerticalPadding: 8,
+ shape: const RoundedRectangleBorder(),
+ );
+
+ final BuildContext context;
+ late final ThemeData _theme = Theme.of(context);
+ late final ColorScheme _colors = _theme.colorScheme;
+ late final TextTheme _textTheme = _theme.textTheme;
+
+ @override
+ Color? get tileColor => Colors.transparent;
+
+ @override
+ TextStyle? get titleTextStyle => _textTheme.bodyLarge;
+
+ @override
+ TextStyle? get subtitleTextStyle => _textTheme.bodyMedium;
+
+ @override
+ TextStyle? get leadingAndTrailingTextStyle => _textTheme.labelSmall;
+
+ @override
+ Color? get selectedColor => _colors.primary;
+
+ @override
+ Color? get iconColor => _colors.onSurface;
+}
+
+// END GENERATED TOKEN PROPERTIES - LisTile
diff --git a/framework/lib/src/material/list_tile_theme.dart b/framework/lib/src/material/list_tile_theme.dart
index 0b4983d..9f3aee6 100644
--- a/framework/lib/src/material/list_tile_theme.dart
+++ b/framework/lib/src/material/list_tile_theme.dart
@@ -51,6 +51,9 @@
this.selectedColor,
this.iconColor,
this.textColor,
+ this.titleTextStyle,
+ this.subtitleTextStyle,
+ this.leadingAndTrailingTextStyle,
this.contentPadding,
this.tileColor,
this.selectedTileColor,
@@ -80,6 +83,15 @@
/// Overrides the default value of [ListTile.textColor].
final Color? textColor;
+ /// Overrides the default value of [ListTile.titleTextStyle].
+ final TextStyle? titleTextStyle;
+
+ /// Overrides the default value of [ListTile.subtitleTextStyle].
+ final TextStyle? subtitleTextStyle;
+
+ /// Overrides the default value of [ListTile.leadingAndTrailingTextStyle].
+ final TextStyle? leadingAndTrailingTextStyle;
+
/// Overrides the default value of [ListTile.contentPadding].
final EdgeInsetsGeometry? contentPadding;
@@ -116,6 +128,9 @@
Color? selectedColor,
Color? iconColor,
Color? textColor,
+ TextStyle? titleTextStyle,
+ TextStyle? subtitleTextStyle,
+ TextStyle? leadingAndTrailingTextStyle,
EdgeInsetsGeometry? contentPadding,
Color? tileColor,
Color? selectedTileColor,
@@ -134,6 +149,9 @@
selectedColor: selectedColor ?? this.selectedColor,
iconColor: iconColor ?? this.iconColor,
textColor: textColor ?? this.textColor,
+ titleTextStyle: titleTextStyle ?? this.titleTextStyle,
+ subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle,
+ leadingAndTrailingTextStyle: leadingAndTrailingTextStyle ?? this.leadingAndTrailingTextStyle,
contentPadding: contentPadding ?? this.contentPadding,
tileColor: tileColor ?? this.tileColor,
selectedTileColor: selectedTileColor ?? this.selectedTileColor,
@@ -148,7 +166,6 @@
/// Linearly interpolate between ListTileThemeData objects.
static ListTileThemeData? lerp(ListTileThemeData? a, ListTileThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -159,6 +176,9 @@
selectedColor: Color.lerp(a?.selectedColor, b?.selectedColor, t),
iconColor: Color.lerp(a?.iconColor, b?.iconColor, t),
textColor: Color.lerp(a?.textColor, b?.textColor, t),
+ titleTextStyle: TextStyle.lerp(a?.titleTextStyle, b?.titleTextStyle, t),
+ subtitleTextStyle: TextStyle.lerp(a?.subtitleTextStyle, b?.subtitleTextStyle, t),
+ leadingAndTrailingTextStyle: TextStyle.lerp(a?.leadingAndTrailingTextStyle, b?.leadingAndTrailingTextStyle, t),
contentPadding: EdgeInsetsGeometry.lerp(a?.contentPadding, b?.contentPadding, t),
tileColor: Color.lerp(a?.tileColor, b?.tileColor, t),
selectedTileColor: Color.lerp(a?.selectedTileColor, b?.selectedTileColor, t),
@@ -179,6 +199,9 @@
selectedColor,
iconColor,
textColor,
+ titleTextStyle,
+ subtitleTextStyle,
+ leadingAndTrailingTextStyle,
contentPadding,
tileColor,
selectedTileColor,
@@ -204,6 +227,9 @@
&& other.style == style
&& other.selectedColor == selectedColor
&& other.iconColor == iconColor
+ && other.titleTextStyle == titleTextStyle
+ && other.subtitleTextStyle == subtitleTextStyle
+ && other.leadingAndTrailingTextStyle == leadingAndTrailingTextStyle
&& other.textColor == textColor
&& other.contentPadding == contentPadding
&& other.tileColor == tileColor
@@ -225,6 +251,9 @@
properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null));
properties.add(ColorProperty('iconColor', iconColor, defaultValue: null));
properties.add(ColorProperty('textColor', textColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('titleTextStyle', titleTextStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('subtitleTextStyle', subtitleTextStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('leadingAndTrailingTextStyle', leadingAndTrailingTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('contentPadding', contentPadding, defaultValue: null));
properties.add(ColorProperty('tileColor', tileColor, defaultValue: null));
properties.add(ColorProperty('selectedTileColor', selectedTileColor, defaultValue: null));
@@ -450,7 +479,6 @@
double? minLeadingWidth,
required Widget child,
}) {
- assert(child != null);
return Builder(
builder: (BuildContext context) {
final ListTileThemeData parent = ListTileTheme.of(context);
diff --git a/framework/lib/src/material/magnifier.dart b/framework/lib/src/material/magnifier.dart
index 9880c94..075d40e 100644
--- a/framework/lib/src/material/magnifier.dart
+++ b/framework/lib/src/material/magnifier.dart
@@ -134,7 +134,7 @@
void _determineMagnifierPositionAndFocalPoint() {
final MagnifierInfo selectionInfo =
widget.magnifierInfo.value;
- final Rect screenRect = Offset.zero & MediaQuery.of(context).size;
+ final Rect screenRect = Offset.zero & MediaQuery.sizeOf(context);
// Since by default we draw at the top left corner, this offset
// shifts the magnifier so we draw at the center, and then also includes
diff --git a/framework/lib/src/material/material.dart b/framework/lib/src/material/material.dart
index 54fd5a0..74e7cdc 100644
--- a/framework/lib/src/material/material.dart
+++ b/framework/lib/src/material/material.dart
@@ -194,13 +194,9 @@
this.clipBehavior = Clip.none,
this.animationDuration = kThemeChangeDuration,
this.child,
- }) : assert(type != null),
- assert(elevation != null && elevation >= 0.0),
+ }) : assert(elevation >= 0.0),
assert(!(shape != null && borderRadius != null)),
- assert(animationDuration != null),
- assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
- assert(borderOnForeground != null),
- assert(clipBehavior != null);
+ assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null)));
/// The widget below this widget in the tree.
///
@@ -258,6 +254,7 @@
/// The color to paint the shadow below the material.
///
+ /// {@template flutter.material.material.shadowColor}
/// If null and [ThemeData.useMaterial3] is true then [ThemeData]'s
/// [ColorScheme.shadow] will be used. If [ThemeData.useMaterial3] is false
/// then [ThemeData.shadowColor] will be used.
@@ -270,11 +267,13 @@
/// property if it is null.
/// * [ThemeData.applyElevationOverlayColor], which turns elevation overlay
/// on or off for dark themes.
+ /// {@endtemplate}
final Color? shadowColor;
/// The color of the surface tint overlay applied to the material color
/// to indicate elevation.
///
+ /// {@template flutter.material.material.surfaceTintColor}
/// Material Design 3 introduced a new way for some components to indicate
/// their elevation by using a surface tint color overlay on top of the
/// base material [color]. This overlay is painted with an opacity that is
@@ -295,6 +294,7 @@
/// tint.
/// * https://m3.material.io/styles/color/the-color-system/color-roles
/// which specifies how the overlay is applied.
+ /// {@endtemplate}
final Color? surfaceTintColor;
/// The typographical style to use for text within this material.
@@ -302,11 +302,13 @@
/// Defines the material's shape as well its shadow.
///
+ /// {@template flutter.material.material.shape}
/// If shape is non null, the [borderRadius] is ignored and the material's
/// clip boundary and shadow are defined by the shape.
///
/// A shadow is only displayed if the [elevation] is greater than
/// zero.
+ /// {@endtemplate}
final ShapeBorder? shape;
/// Whether to paint the [shape] border in front of the [child].
@@ -343,7 +345,7 @@
final BorderRadiusGeometry? borderRadius;
/// The ink controller from the closest instance of this class that
- /// encloses the given context.
+ /// encloses the given context within the closest [LookupBoundary].
///
/// Typical usage is as follows:
///
@@ -358,11 +360,11 @@
/// * [Material.of], which is similar to this method, but asserts if
/// no [Material] ancestor is found.
static MaterialInkController? maybeOf(BuildContext context) {
- return context.findAncestorRenderObjectOfType<_RenderInkFeatures>();
+ return LookupBoundary.findAncestorRenderObjectOfType<_RenderInkFeatures>(context);
}
/// The ink controller from the closest instance of [Material] that encloses
- /// the given context.
+ /// the given context within the closest [LookupBoundary].
///
/// If no [Material] widget ancestor can be found then this method will assert
/// in debug mode, and throw an exception in release mode.
@@ -383,6 +385,16 @@
final MaterialInkController? controller = maybeOf(context);
assert(() {
if (controller == null) {
+ if (LookupBoundary.debugIsHidingAncestorRenderObjectOfType<_RenderInkFeatures>(context)) {
+ throw FlutterError(
+ 'Material.of() was called with a context that does not have access to a Material widget.\n'
+ 'The context provided to Material.of() does have a Material widget ancestor, but it is '
+ 'hidden by a LookupBoundary. This can happen because you are using a widget that looks '
+ 'for a Material ancestor, but no such ancestor exists within the closest LookupBoundary.\n'
+ 'The context used was:\n'
+ ' $context',
+ );
+ }
throw FlutterError(
'Material.of() was called with a context that does not contain a Material widget.\n'
'No Material widget ancestor could be found starting from the context that was passed to '
@@ -447,7 +459,7 @@
final Color? backgroundColor = _getBackgroundColor(context);
final Color modelShadowColor = widget.shadowColor ?? (theme.useMaterial3 ? theme.colorScheme.shadow : theme.shadowColor);
// If no shadow color is specified, use 0 for elevation in the model so a drop shadow won't be painted.
- final double modelElevation = modelShadowColor != null ? widget.elevation : 0;
+ final double modelElevation = widget.elevation;
assert(
backgroundColor != null || widget.type == MaterialType.transparency,
'If Material type is not MaterialType.transparency, a color must '
@@ -587,8 +599,7 @@
required this.vsync,
required this.absorbHitTest,
this.color,
- }) : assert(vsync != null),
- super(child);
+ }) : super(child);
// This class should exist in a 1:1 relationship with a MaterialState object,
// since there's no current support for dynamically changing the ticker
@@ -700,9 +711,7 @@
required MaterialInkController controller,
required this.referenceBox,
this.onRemoved,
- }) : assert(controller != null),
- assert(referenceBox != null),
- _controller = controller as _RenderInkFeatures;
+ }) : _controller = controller as _RenderInkFeatures;
/// The [MaterialInkController] associated with this [InkFeature].
///
@@ -807,11 +816,7 @@
required this.surfaceTintColor,
super.curve,
required super.duration,
- }) : assert(child != null),
- assert(shape != null),
- assert(clipBehavior != null),
- assert(elevation != null && elevation >= 0.0),
- assert(color != null);
+ }) : assert(elevation >= 0.0);
/// The widget below this widget in the tree.
///
diff --git a/framework/lib/src/material/material_button.dart b/framework/lib/src/material/material_button.dart
index c0f6580..2e4bc15 100644
--- a/framework/lib/src/material/material_button.dart
+++ b/framework/lib/src/material/material_button.dart
@@ -82,9 +82,7 @@
this.height,
this.enableFeedback = true,
this.child,
- }) : assert(clipBehavior != null),
- assert(autofocus != null),
- assert(elevation == null || elevation >= 0.0),
+ }) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
diff --git a/framework/lib/src/material/material_localizations.dart b/framework/lib/src/material/material_localizations.dart
index 98f07e4..d6f2b4d 100644
--- a/framework/lib/src/material/material_localizations.dart
+++ b/framework/lib/src/material/material_localizations.dart
@@ -160,6 +160,19 @@
/// as a hint text in the text field.
String get searchFieldLabel;
+ /// Label indicating that a given date is the current date.
+ String get currentDateLabel;
+
+ /// Label for the scrim rendered underneath the content of a modal route.
+ String get scrimLabel;
+
+ /// Label for a BottomSheet.
+ String get bottomSheetLabel;
+
+ /// Hint text announced when tapping on the scrim underneath the content of
+ /// a modal route.
+ String scrimOnTapHint(String modalRouteContentName);
+
/// The format used to lay out the time picker.
///
/// The documentation for [TimeOfDayFormat] enum values provides details on
@@ -580,6 +593,9 @@
/// The shortcut label for the keyboard key [LogicalKeyboardKey.select].
String get keyboardKeySelect;
+ /// The shortcut label for the keyboard key [LogicalKeyboardKey.shift].
+ String get keyboardKeyShift;
+
/// The shortcut label for the keyboard key [LogicalKeyboardKey.space].
String get keyboardKeySpace;
@@ -1016,6 +1032,18 @@
String get searchFieldLabel => 'Search';
@override
+ String get currentDateLabel => 'Today';
+
+ @override
+ String get scrimLabel => 'Scrim';
+
+ @override
+ String get bottomSheetLabel => 'Bottom Sheet';
+
+ @override
+ String scrimOnTapHint(String modalRouteContentName) => 'Close $modalRouteContentName';
+
+ @override
String aboutListTileTitle(String applicationName) => 'About $applicationName';
@override
@@ -1316,5 +1344,8 @@
String get keyboardKeySelect => 'Select';
@override
+ String get keyboardKeyShift => 'Shift';
+
+ @override
String get keyboardKeySpace => 'Space';
}
diff --git a/framework/lib/src/material/material_state.dart b/framework/lib/src/material/material_state.dart
index e33b063..16357a0 100644
--- a/framework/lib/src/material/material_state.dart
+++ b/framework/lib/src/material/material_state.dart
@@ -409,7 +409,6 @@
OutlinedBorder? resolve(Set<MaterialState> states);
}
-
/// Defines a [TextStyle] that is also a [MaterialStateProperty].
///
/// This class exists to enable widgets with [TextStyle] valued properties
@@ -603,6 +602,8 @@
/// on a widget's interactive "state", which is defined as a set
/// of [MaterialState]s.
///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=CylXr3AF3uU}
+///
/// Material state properties represent values that depend on a widget's material
/// "state". The state is encoded as a set of [MaterialState] values, like
/// [MaterialState.focused], [MaterialState.hovered], [MaterialState.pressed]. For
diff --git a/framework/lib/src/material/material_state_mixin.dart b/framework/lib/src/material/material_state_mixin.dart
index 6363a0b..740f333 100644
--- a/framework/lib/src/material/material_state_mixin.dart
+++ b/framework/lib/src/material/material_state_mixin.dart
@@ -38,7 +38,7 @@
/// Widget build(BuildContext context) {
/// return InkWell(
/// onFocusChange: updateMaterialState(MaterialState.focused),
-/// child: Container(
+/// child: ColoredBox(
/// color: widget.color.resolve(materialStates),
/// child: widget.child,
/// ),
@@ -88,7 +88,7 @@
/// class MyWidgetState extends State<MyWidget> with MaterialStateMixin<MyWidget> {
/// @override
/// Widget build(BuildContext context) {
- /// return Container(
+ /// return ColoredBox(
/// color: isPressed ? Colors.black : Colors.white,
/// child: InkWell(
/// onHighlightChanged: updateMaterialState(
diff --git a/framework/lib/src/material/menu_anchor.dart b/framework/lib/src/material/menu_anchor.dart
index faced68..d47d624 100644
--- a/framework/lib/src/material/menu_anchor.dart
+++ b/framework/lib/src/material/menu_anchor.dart
@@ -44,11 +44,11 @@
// has a submenu.
const double _kDefaultSubmenuIconSize = 24;
-// The default spacing between the the leading icon, label, trailing icon, and
+// The default spacing between the leading icon, label, trailing icon, and
// shortcut label in a _MenuItemLabel.
const double _kLabelItemDefaultSpacing = 12;
-// The minimum spacing between the the leading icon, label, trailing icon, and
+// The minimum spacing between the leading icon, label, trailing icon, and
// shortcut label in a _MenuItemLabel.
const double _kLabelItemMinSpacing = 4;
@@ -315,7 +315,7 @@
_position?.isScrollingNotifier.removeListener(_handleScroll);
_position = Scrollable.maybeOf(context)?.position;
_position?.isScrollingNotifier.addListener(_handleScroll);
- final Size newSize = MediaQuery.of(context).size;
+ final Size newSize = MediaQuery.sizeOf(context);
if (_viewSize != null && newSize != _viewSize) {
// Close the menus if the view changes size.
_root._close();
@@ -362,7 +362,7 @@
);
}
- return _MenuAnchorMarker(
+ return _MenuAnchorScope(
anchorKey: _anchorKey,
anchor: this,
isOpen: _isOpen,
@@ -511,7 +511,7 @@
// Copy all the themes from the supplied outer context to the
// overlay.
outerContext,
- _MenuAnchorMarker(
+ _MenuAnchorScope(
// Re-advertize the anchor here in the overlay, since
// otherwise a search for the anchor by descendants won't find
// it.
@@ -571,7 +571,7 @@
// dependency relationship that will rebuild the context when the node
// changes.
static _MenuAnchorState? _maybeOf(BuildContext context) {
- return context.dependOnInheritedWidgetOfExactType<_MenuAnchorMarker>()?.anchor;
+ return context.dependOnInheritedWidgetOfExactType<_MenuAnchorScope>()?.anchor;
}
}
@@ -700,10 +700,10 @@
/// platform instead of by Flutter (on macOS, for example).
/// * [ShortcutRegistry], a registry of shortcuts that apply for the entire
/// application.
-/// * [VoidCallbackIntent] to define intents that will call a [VoidCallback] and
+/// * [VoidCallbackIntent], to define intents that will call a [VoidCallback] and
/// work with the [Actions] and [Shortcuts] system.
-/// * [CallbackShortcuts] to define shortcuts that simply call a callback and
-/// don't involve using [Actions].
+/// * [CallbackShortcuts], to define shortcuts that call a callback without
+/// involving [Actions].
class MenuBar extends StatelessWidget {
/// Creates a const [MenuBar].
///
@@ -789,10 +789,10 @@
/// platform instead of by Flutter (on macOS, for example).
/// * [ShortcutRegistry], a registry of shortcuts that apply for the entire
/// application.
-/// * [VoidCallbackIntent] to define intents that will call a [VoidCallback] and
+/// * [VoidCallbackIntent], to define intents that will call a [VoidCallback] and
/// work with the [Actions] and [Shortcuts] system.
-/// * [CallbackShortcuts] to define shortcuts that simply call a callback and
-/// don't involve using [Actions].
+/// * [CallbackShortcuts] to define shortcuts that call a callback without
+/// involving [Actions].
class MenuItemButton extends StatefulWidget {
/// Creates a const [MenuItemButton].
///
@@ -1796,21 +1796,23 @@
@override
Widget build(BuildContext context) {
- final Offset menuPaddingOffset;
+ Offset menuPaddingOffset = widget.alignmentOffset ?? Offset.zero;
final EdgeInsets menuPadding = _computeMenuPadding(context);
- switch (_anchor?._root._orientation ?? Axis.vertical) {
+ // Move the submenu over by the size of the menu padding, so that
+ // the first menu item aligns with the submenu button that opens it.
+ switch (_anchor?._orientation ?? Axis.vertical) {
case Axis.horizontal:
switch (Directionality.of(context)) {
case TextDirection.rtl:
- menuPaddingOffset = widget.alignmentOffset ?? Offset(-menuPadding.right, 0);
+ menuPaddingOffset += Offset(menuPadding.right, 0);
break;
case TextDirection.ltr:
- menuPaddingOffset = widget.alignmentOffset ?? Offset(-menuPadding.left, 0);
+ menuPaddingOffset += Offset(-menuPadding.left, 0);
break;
}
break;
case Axis.vertical:
- menuPaddingOffset = widget.alignmentOffset ?? Offset(0, -menuPadding.top);
+ menuPaddingOffset += Offset(0, -menuPadding.top);
break;
}
@@ -1900,24 +1902,13 @@
}
EdgeInsets _computeMenuPadding(BuildContext context) {
- final MenuStyle? themeStyle = MenuTheme.of(context).style;
- final MenuStyle defaultStyle = _MenuDefaultsM3(context);
-
- T? effectiveValue<T>(T? Function(MenuStyle? style) getProperty) {
- return getProperty(widget.menuStyle) ?? getProperty(themeStyle) ?? getProperty(defaultStyle);
- }
-
- T? resolve<T>(MaterialStateProperty<T>? Function(MenuStyle? style) getProperty) {
- return effectiveValue(
- (MenuStyle? style) {
- return getProperty(style)?.resolve(widget.statesController?.value ?? const <MaterialState>{});
- },
- );
- }
-
- return resolve<EdgeInsetsGeometry?>(
- (MenuStyle? style) => style?.padding,
- )?.resolve(Directionality.of(context)) ?? EdgeInsets.zero;
+ final MaterialStateProperty<EdgeInsetsGeometry?> insets =
+ widget.menuStyle?.padding ??
+ MenuTheme.of(context).style?.padding ??
+ _MenuDefaultsM3(context).padding!;
+ return insets
+ .resolve(widget.statesController?.value ?? const <MaterialState>{})!
+ .resolve(Directionality.of(context));
}
void _handleFocusChange() {
@@ -1983,9 +1974,6 @@
LogicalKeyboardKey.arrowUp: '↑',
LogicalKeyboardKey.arrowDown: '↓',
LogicalKeyboardKey.enter: '↵',
- LogicalKeyboardKey.shift: '⇧',
- LogicalKeyboardKey.shiftLeft: '⇧',
- LogicalKeyboardKey.shiftRight: '⇧',
};
static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
@@ -2016,15 +2004,32 @@
/// Returns the label to be shown to the user in the UI when a
/// [MenuSerializableShortcut] is used as a keyboard shortcut.
///
- /// To keep the representation short, this will return graphical key
- /// representations when it can. For instance, the default
- /// [LogicalKeyboardKey.shift] will return '⇧', and the arrow keys will return
- /// arrows. When [defaultTargetPlatform] is [TargetPlatform.macOS] or
- /// [TargetPlatform.iOS], the key [LogicalKeyboardKey.meta] will show as '⌘',
- /// [LogicalKeyboardKey.control] will show as 'Ë„', and
- /// [LogicalKeyboardKey.alt] will show as '⌥'.
+ /// When [defaultTargetPlatform] is [TargetPlatform.macOS] or
+ /// [TargetPlatform.iOS], this will return graphical key representations when
+ /// it can. For instance, the default [LogicalKeyboardKey.shift] will return
+ /// '⇧', and the arrow keys will return arrows. The key
+ /// [LogicalKeyboardKey.meta] will show as '⌘', [LogicalKeyboardKey.control]
+ /// will show as '˄', and [LogicalKeyboardKey.alt] will show as '⌥'.
+ ///
+ /// The keys are joined by spaces on macOS and iOS, and by "+" on other
+ /// platforms.
String getShortcutLabel(MenuSerializableShortcut shortcut, MaterialLocalizations localizations) {
final ShortcutSerialization serialized = shortcut.serializeForMenu();
+ final String keySeparator;
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ // Use "⌃ ⇧ A" style on macOS and iOS.
+ keySeparator = ' ';
+ break;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ // Use "Ctrl+Shift+A" style.
+ keySeparator = '+';
+ break;
+ }
if (serialized.trigger != null) {
final List<String> modifiers = <String>[];
final LogicalKeyboardKey trigger = serialized.trigger!;
@@ -2060,7 +2065,7 @@
return <String>[
...modifiers,
if (shortcutTrigger != null && shortcutTrigger.isNotEmpty) shortcutTrigger,
- ].join(' ');
+ ].join(keySeparator);
} else if (serialized.character != null) {
return serialized.character!;
}
@@ -2175,14 +2180,23 @@
if (modifier == LogicalKeyboardKey.shift ||
modifier == LogicalKeyboardKey.shiftLeft ||
modifier == LogicalKeyboardKey.shiftRight) {
- return _shortcutGraphicEquivalents[LogicalKeyboardKey.shift]!;
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ return localizations.keyboardKeyShift;
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ return '⇧';
+ }
}
throw ArgumentError('Keyboard key ${modifier.keyLabel} is not a modifier.');
}
}
-class _MenuAnchorMarker extends InheritedWidget {
- const _MenuAnchorMarker({
+class _MenuAnchorScope extends InheritedWidget {
+ const _MenuAnchorScope({
required super.child,
required this.anchorKey,
required this.anchor,
@@ -2194,7 +2208,7 @@
final bool isOpen;
@override
- bool updateShouldNotify(_MenuAnchorMarker oldWidget) {
+ bool updateShouldNotify(_MenuAnchorScope oldWidget) {
return anchorKey != oldWidget.anchorKey
|| anchor != oldWidget.anchor
|| isOpen != oldWidget.isOpen;
@@ -2905,7 +2919,6 @@
// 4) Is part of an anchor that either doesn't have a submenu, or doesn't
// have any submenus currently open (only the "deepest" open menu should
// have accelerator shortcuts registered).
- assert(_displayLabel != null);
if (_showAccelerators && _acceleratorIndex != -1 && _binding?.onInvoke != null && !(_binding!.hasSubmenu && (_anchor?._isOpen ?? false))) {
final String acceleratorCharacter = _displayLabel[_acceleratorIndex].toLowerCase();
_shortcutRegistryEntry = _shortcutRegistry?.addAll(
@@ -3190,14 +3203,15 @@
@override
bool shouldRelayout(_MenuLayout oldDelegate) {
- return anchorRect != oldDelegate.anchorRect ||
- textDirection != oldDelegate.textDirection ||
- alignment != oldDelegate.alignment ||
- alignmentOffset != oldDelegate.alignmentOffset ||
- menuPosition != oldDelegate.menuPosition ||
- orientation != oldDelegate.orientation ||
- parentOrientation != oldDelegate.parentOrientation ||
- !setEquals(avoidBounds, oldDelegate.avoidBounds);
+ return anchorRect != oldDelegate.anchorRect
+ || textDirection != oldDelegate.textDirection
+ || alignment != oldDelegate.alignment
+ || alignmentOffset != oldDelegate.alignmentOffset
+ || menuPosition != oldDelegate.menuPosition
+ || menuPadding != oldDelegate.menuPadding
+ || orientation != oldDelegate.orientation
+ || parentOrientation != oldDelegate.parentOrientation
+ || !setEquals(avoidBounds, oldDelegate.avoidBounds);
}
Rect _closestScreen(Iterable<Rect> screens, Offset point) {
@@ -3301,7 +3315,7 @@
final double dy = densityAdjustment.dy;
final double dx = math.max(0, densityAdjustment.dx);
final EdgeInsetsGeometry resolvedPadding = padding
- .add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
+ .add(EdgeInsets.symmetric(horizontal: dx, vertical: dy))
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); // ignore_clamp_double_lint
BoxConstraints effectiveConstraints = visualDensity.effectiveConstraints(
@@ -3430,7 +3444,7 @@
);
final VisualDensity visualDensity =
- effectiveValue((MenuStyle? style) => style?.visualDensity) ?? VisualDensity.standard;
+ effectiveValue((MenuStyle? style) => style?.visualDensity) ?? Theme.of(context).visualDensity;
final AlignmentGeometry alignment = effectiveValue((MenuStyle? style) => style?.alignment)!;
final BuildContext anchorContext = anchor._anchorKey.currentContext!;
final RenderBox overlay = Overlay.of(anchorContext).context.findRenderObject()! as RenderBox;
@@ -3571,17 +3585,18 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _MenuBarDefaultsM3 extends MenuStyle {
_MenuBarDefaultsM3(this.context)
- : super(
- elevation: const MaterialStatePropertyAll<double?>(3.0),
- shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
- alignment: AlignmentDirectional.bottomStart,
- );
+ : super(
+ elevation: const MaterialStatePropertyAll<double?>(3.0),
+ shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
+ alignment: AlignmentDirectional.bottomStart,
+ );
+
static const RoundedRectangleBorder _defaultMenuBorder =
- RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
+ RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
final BuildContext context;
@@ -3617,11 +3632,12 @@
class _MenuButtonDefaultsM3 extends ButtonStyle {
_MenuButtonDefaultsM3(this.context)
- : super(
- animationDuration: kThemeChangeDuration,
- enableFeedback: true,
- alignment: AlignmentDirectional.centerStart,
- );
+ : super(
+ animationDuration: kThemeChangeDuration,
+ enableFeedback: true,
+ alignment: AlignmentDirectional.centerStart,
+ );
+
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@@ -3752,20 +3768,21 @@
const EdgeInsets.symmetric(horizontal: 12),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.maybeTextScaleFactorOf(context) ?? 1,
);
}
}
class _MenuDefaultsM3 extends MenuStyle {
_MenuDefaultsM3(this.context)
- : super(
- elevation: const MaterialStatePropertyAll<double?>(3.0),
- shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
- alignment: AlignmentDirectional.topEnd,
- );
+ : super(
+ elevation: const MaterialStatePropertyAll<double?>(3.0),
+ shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
+ alignment: AlignmentDirectional.topEnd,
+ );
+
static const RoundedRectangleBorder _defaultMenuBorder =
- RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
+ RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
final BuildContext context;
diff --git a/framework/lib/src/material/menu_bar_theme.dart b/framework/lib/src/material/menu_bar_theme.dart
index c4b266a..ffda944 100644
--- a/framework/lib/src/material/menu_bar_theme.dart
+++ b/framework/lib/src/material/menu_bar_theme.dart
@@ -74,7 +74,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties to set for [MenuBar] in this widget's descendants.
final MenuBarThemeData data;
diff --git a/framework/lib/src/material/menu_button_theme.dart b/framework/lib/src/material/menu_button_theme.dart
index 9af32d0..6eef7a4 100644
--- a/framework/lib/src/material/menu_button_theme.dart
+++ b/framework/lib/src/material/menu_button_theme.dart
@@ -106,7 +106,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The configuration of this theme.
final MenuButtonThemeData data;
diff --git a/framework/lib/src/material/menu_style.dart b/framework/lib/src/material/menu_style.dart
index 62c30f7..fa0dc89 100644
--- a/framework/lib/src/material/menu_style.dart
+++ b/framework/lib/src/material/menu_style.dart
@@ -304,7 +304,6 @@
/// Linearly interpolate between two [MenuStyle]s.
static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/material/menu_theme.dart b/framework/lib/src/material/menu_theme.dart
index bb2213c..685ce48 100644
--- a/framework/lib/src/material/menu_theme.dart
+++ b/framework/lib/src/material/menu_theme.dart
@@ -91,7 +91,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties for [MenuBar] and [MenuItemButton] in this widget's
/// descendants.
diff --git a/framework/lib/src/material/mergeable_material.dart b/framework/lib/src/material/mergeable_material.dart
index f69988e..87b71d8 100644
--- a/framework/lib/src/material/mergeable_material.dart
+++ b/framework/lib/src/material/mergeable_material.dart
@@ -21,7 +21,7 @@
/// const constructors so that they can be used in const expressions.
///
/// The argument is the [key], which must not be null.
- const MergeableMaterialItem(this.key) : assert(key != null);
+ const MergeableMaterialItem(this.key);
/// The key for this item of the list.
///
@@ -42,8 +42,7 @@
required LocalKey key,
required this.child,
this.color,
- }) : assert(key != null),
- super(key);
+ }) : super(key);
/// The contents of this slice.
///
@@ -69,8 +68,7 @@
const MaterialGap({
required LocalKey key,
this.size = 16.0,
- }) : assert(key != null),
- super(key);
+ }) : super(key);
/// The main axis extent of this gap. For example, if the [MergeableMaterial]
/// is vertical, then this is the height of the gap.
@@ -657,7 +655,7 @@
}
class _MergeableMaterialListBody extends ListBody {
- _MergeableMaterialListBody({
+ const _MergeableMaterialListBody({
required super.children,
super.mainAxis,
required this.items,
diff --git a/framework/lib/src/material/navigation_bar.dart b/framework/lib/src/material/navigation_bar.dart
index bede640..c9bfa99 100644
--- a/framework/lib/src/material/navigation_bar.dart
+++ b/framework/lib/src/material/navigation_bar.dart
@@ -17,8 +17,8 @@
import 'theme.dart';
import 'tooltip.dart';
-const double _kIndicatorHeight = 64;
-const double _kIndicatorWidth = 32;
+const double _kIndicatorHeight = 32;
+const double _kIndicatorWidth = 64;
// Examples can assume:
// late BuildContext context;
@@ -26,6 +26,8 @@
/// Material 3 Navigation Bar component.
///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=DVGYddFaLv0}
+///
/// Navigation bars offer a persistent and convenient way to switch between
/// primary destinations in an app.
///
@@ -53,6 +55,14 @@
/// {@end-tool}
///
/// {@tool dartpad}
+/// This example showcases [NavigationBar] label behaviors. When tapping on one
+/// of the label behavior options, the [labelBehavior] of the [NavigationBar]
+/// will be updated.
+///
+/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.1.dart **
+/// {@end-tool}
+///
+/// {@tool dartpad}
/// This example shows a [NavigationBar] as it is used within a [Scaffold]
/// widget when there are nested navigators that provide local navigation. The
/// [NavigationBar] has four [NavigationDestination] widgets with different
@@ -60,7 +70,7 @@
/// item's index and displays a corresponding page with its own local navigator
/// in the body of a [Scaffold].
///
-/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.1.dart **
+/// ** See code in examples/api/lib/material/navigation_bar/navigation_bar.2.dart **
/// {@end-tool}
/// See also:
///
@@ -85,9 +95,11 @@
this.elevation,
this.shadowColor,
this.surfaceTintColor,
+ this.indicatorColor,
+ this.indicatorShape,
this.height,
this.labelBehavior,
- }) : assert(destinations != null && destinations.length >= 2),
+ }) : assert(destinations.length >= 2),
assert(0 <= selectedIndex && selectedIndex < destinations.length);
/// Determines the transition time for each destination as it goes between
@@ -150,6 +162,20 @@
/// overlay is applied.
final Color? surfaceTintColor;
+ /// The color of the [indicatorShape] when this destination is selected.
+ ///
+ /// If null, [NavigationBarThemeData.indicatorColor] is used. If that
+ /// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.secondaryContainer]
+ /// is used. Otherwise, [ColorScheme.secondary] with an opacity of 0.24 is used.
+ final Color? indicatorColor;
+
+ /// The shape of the selected inidicator.
+ ///
+ /// If null, [NavigationBarThemeData.indicatorShape] is used. If that
+ /// is also null and [ThemeData.useMaterial3] is true, [StadiumBorder] is used.
+ /// Otherwise, [RoundedRectangleBorder] with a circular border radius of 16 is used.
+ final ShapeBorder? indicatorShape;
+
/// The height of the [NavigationBar] itself.
///
/// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is
@@ -212,9 +238,12 @@
builder: (BuildContext context, Animation<double> animation) {
return _NavigationDestinationInfo(
index: i,
+ selectedIndex: selectedIndex,
totalNumberOfDestinations: destinations.length,
selectedAnimation: animation,
labelBehavior: effectiveLabelBehavior,
+ indicatorColor: indicatorColor,
+ indicatorShape: indicatorShape,
onTap: _handleTap(i),
child: destinations[i],
);
@@ -307,12 +336,13 @@
@override
Widget build(BuildContext context) {
+ final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
const Set<MaterialState> selectedState = <MaterialState>{MaterialState.selected};
const Set<MaterialState> unselectedState = <MaterialState>{};
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
final NavigationBarThemeData defaults = _defaultsFor(context);
- final Animation<double> animation = _NavigationDestinationInfo.of(context).selectedAnimation;
+ final Animation<double> animation = info.selectedAnimation;
return _NavigationDestinationBuilder(
label: label,
@@ -334,8 +364,8 @@
children: <Widget>[
NavigationIndicator(
animation: animation,
- color: navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
- shape: navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
+ color: info.indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
+ shape: info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
),
_StatusTransitionWidgetBuilder(
animation: animation,
@@ -434,11 +464,17 @@
final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
final NavigationBarThemeData defaults = _defaultsFor(context);
+ final GlobalKey labelKey = GlobalKey();
+ final bool selected = info.selectedIndex == info.index;
return _NavigationBarDestinationSemantics(
child: _NavigationBarDestinationTooltip(
message: tooltip ?? label,
child: _IndicatorInkWell(
+ key: UniqueKey(),
+ labelKey: labelKey,
+ labelBehavior: info.labelBehavior,
+ selected: selected,
customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
onTap: info.onTap,
child: Row(
@@ -446,6 +482,7 @@
Expanded(
child: _NavigationBarDestinationLayout(
icon: buildIcon(context),
+ labelKey: labelKey,
label: buildLabel(context),
),
),
@@ -459,24 +496,46 @@
class _IndicatorInkWell extends InkResponse {
const _IndicatorInkWell({
- super.child,
- super.onTap,
+ super.key,
+ required this.labelKey,
+ required this.labelBehavior,
+ required this.selected,
super.customBorder,
+ super.onTap,
+ super.child,
}) : super(
containedInkWell: true,
highlightColor: Colors.transparent,
);
+ final GlobalKey labelKey;
+ final NavigationDestinationLabelBehavior labelBehavior;
+ final bool selected;
+
@override
RectCallback? getRectCallback(RenderBox referenceBox) {
+ final RenderBox labelBox = labelKey.currentContext!.findRenderObject()! as RenderBox;
+ final Rect labelRect = labelBox.localToGlobal(Offset.zero) & labelBox.size;
+ final double labelPadding;
+ switch (labelBehavior) {
+ case NavigationDestinationLabelBehavior.alwaysShow:
+ labelPadding = labelRect.height / 2;
+ break;
+ case NavigationDestinationLabelBehavior.onlyShowSelected:
+ labelPadding = selected ? labelRect.height / 2 : 0;
+ break;
+ case NavigationDestinationLabelBehavior.alwaysHide:
+ labelPadding = 0;
+ break;
+ }
final double indicatorOffsetX = referenceBox.size.width / 2;
- const double indicatorOffsetY = 30.0;
+ final double indicatorOffsetY = referenceBox.size.height / 2 - labelPadding;
return () {
return Rect.fromCenter(
center: Offset(indicatorOffsetX, indicatorOffsetY),
- width: _kIndicatorHeight,
- height: _kIndicatorWidth,
+ width: _kIndicatorWidth,
+ height: _kIndicatorHeight,
);
};
}
@@ -492,9 +551,12 @@
/// [child] and descendants.
const _NavigationDestinationInfo({
required this.index,
+ required this.selectedIndex,
required this.totalNumberOfDestinations,
required this.selectedAnimation,
required this.labelBehavior,
+ required this.indicatorColor,
+ required this.indicatorShape,
required this.onTap,
required super.child,
});
@@ -529,7 +591,13 @@
/// "Tab 1 of 3", for example.
final int index;
- /// How many total destinations are are in this navigation bar.
+ /// This is the index of the currently selected destination.
+ ///
+ /// This is required for `_IndicatorInkWell` to apply label padding to ripple animations
+ /// when label behavior is [NavigationDestinationLabelBehavior.onlyShowSelected].
+ final int selectedIndex;
+
+ /// How many total destinations are in this navigation bar.
///
/// This is required for semantics, so that each destination can have a label
/// "Tab 1 of 4", for example.
@@ -545,6 +613,16 @@
/// label, or hide all labels.
final NavigationDestinationLabelBehavior labelBehavior;
+ /// The color of the selection indicator.
+ ///
+ /// This is used by destinations to override the indicator color.
+ final Color? indicatorColor;
+
+ /// The shape of the selection indicator.
+ ///
+ /// This is used by destinations to override the indicator shape.
+ final ShapeBorder? indicatorShape;
+
/// The callback that should be called when this destination is tapped.
///
/// This is computed by calling [NavigationBar.onDestinationSelected]
@@ -593,8 +671,8 @@
super.key,
required this.animation,
this.color,
- this.width = _kIndicatorHeight,
- this.height = _kIndicatorWidth,
+ this.width = _kIndicatorWidth,
+ this.height = _kIndicatorHeight,
this.borderRadius = const BorderRadius.all(Radius.circular(16)),
this.shape,
});
@@ -699,6 +777,7 @@
/// 3 [NavigationBar].
const _NavigationBarDestinationLayout({
required this.icon,
+ required this.labelKey,
required this.label,
});
@@ -707,6 +786,11 @@
/// See [NavigationDestination.icon].
final Widget icon;
+ /// The global key for the label of this destination.
+ ///
+ /// This is used to determine the position of the label relative to the icon.
+ final GlobalKey labelKey;
+
/// The label widget that sits below the icon.
///
/// This widget will sometimes be faded out, depending on
@@ -716,7 +800,6 @@
final Widget label;
static final Key _iconKey = UniqueKey();
- static final Key _labelKey = UniqueKey();
@override
Widget build(BuildContext context) {
@@ -740,7 +823,7 @@
alwaysIncludeSemantics: true,
opacity: animation,
child: RepaintBoundary(
- key: _labelKey,
+ key: labelKey,
child: label,
),
),
@@ -862,9 +945,6 @@
@override
Widget build(BuildContext context) {
- if (message == null) {
- return child;
- }
return Tooltip(
message: message,
// TODO(johnsonmh): Make this value configurable/themable.
@@ -994,7 +1074,7 @@
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
- textScaleFactor: clampDouble(MediaQuery.of(context).textScaleFactor,
+ textScaleFactor: clampDouble(MediaQuery.textScaleFactorOf(context),
0.0,
upperLimit,
),
@@ -1288,7 +1368,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
_NavigationBarDefaultsM3(this.context)
diff --git a/framework/lib/src/material/navigation_bar_theme.dart b/framework/lib/src/material/navigation_bar_theme.dart
index dbf0593..9fc4b8b 100644
--- a/framework/lib/src/material/navigation_bar_theme.dart
+++ b/framework/lib/src/material/navigation_bar_theme.dart
@@ -125,7 +125,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static NavigationBarThemeData? lerp(NavigationBarThemeData? a, NavigationBarThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -213,7 +212,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the background color, label text style, icon theme, and label
/// type values for descendant [NavigationBar] widgets.
diff --git a/framework/lib/src/material/navigation_drawer.dart b/framework/lib/src/material/navigation_drawer.dart
index d21d39d..39c06fb 100644
--- a/framework/lib/src/material/navigation_drawer.dart
+++ b/framework/lib/src/material/navigation_drawer.dart
@@ -57,6 +57,8 @@
this.shadowColor,
this.surfaceTintColor,
this.elevation,
+ this.indicatorColor,
+ this.indicatorShape,
this.onDestinationSelected,
this.selectedIndex = 0,
});
@@ -90,6 +92,18 @@
/// is also null, it will be 1.0.
final double? elevation;
+ /// The color of the [indicatorShape] when this destination is selected.
+ ///
+ /// If this is null, [NavigationDrawerThemeData.indicatorColor] is used.
+ /// If that is also null, defaults to [ColorScheme.secondaryContainer].
+ final Color? indicatorColor;
+
+ /// The shape of the selected inidicator.
+ ///
+ /// If this is null, [NavigationDrawerThemeData.indicatorShape] is used.
+ /// If that is also null, defaults to [StadiumBorder].
+ final ShapeBorder? indicatorShape;
+
/// Defines the appearance of the items within the navigation drawer.
///
/// The list contains [NavigationDrawerDestination] widgets and/or customized
@@ -125,6 +139,8 @@
index: index,
totalNumberOfDestinations: totalNumberOfDestinations,
selectedAnimation: animation,
+ indicatorColor: indicatorColor,
+ indicatorShape: indicatorShape,
onTap: () {
if (onDestinationSelected != null) {
onDestinationSelected!(index);
@@ -148,9 +164,11 @@
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
elevation: elevation,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: wrappedChildren,
+ child: SafeArea(
+ bottom: false,
+ child: ListView(
+ children: wrappedChildren,
+ ),
),
);
}
@@ -315,9 +333,9 @@
alignment: Alignment.center,
children: <Widget>[
NavigationIndicator(
- animation: _NavigationDrawerDestinationInfo.of(context).selectedAnimation,
- color: navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!,
- shape: navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
+ animation: info.selectedAnimation,
+ color: info.indicatorColor ?? navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!,
+ shape: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width,
height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height,
),
@@ -433,6 +451,8 @@
required this.index,
required this.totalNumberOfDestinations,
required this.selectedAnimation,
+ required this.indicatorColor,
+ required this.indicatorShape,
required this.onTap,
required super.child,
});
@@ -468,7 +488,7 @@
/// "Tab 1 of 3", for example.
final int index;
- /// How many total destinations are are in this navigation drawer.
+ /// How many total destinations are in this navigation drawer.
///
/// This is required for semantics, so that each destination can have a label
/// "Tab 1 of 4", for example.
@@ -478,6 +498,16 @@
/// to 1 (selected).
final Animation<double> selectedAnimation;
+ /// The color of the indicator.
+ ///
+ /// This is used by destinations to override the indicator color.
+ final Color? indicatorColor;
+
+ /// The shape of the indicator.
+ ///
+ /// This is used by destinations to override the indicator shape.
+ final ShapeBorder? indicatorShape;
+
/// The callback that should be called when this destination is tapped.
///
/// This is computed by calling [NavigationDrawer.onDestinationSelected]
@@ -628,7 +658,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
const _NavigationDrawerDefaultsM3(this.context)
diff --git a/framework/lib/src/material/navigation_drawer_theme.dart b/framework/lib/src/material/navigation_drawer_theme.dart
index e132a4c..1153a47 100644
--- a/framework/lib/src/material/navigation_drawer_theme.dart
+++ b/framework/lib/src/material/navigation_drawer_theme.dart
@@ -126,7 +126,6 @@
/// {@macro dart.ui.shadow.lerp}
static NavigationDrawerThemeData? lerp(
NavigationDrawerThemeData? a, NavigationDrawerThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -227,7 +226,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the background color, label text style, icon theme, and label
/// type values for descendant [NavigationDrawer] widgets.
diff --git a/framework/lib/src/material/navigation_rail.dart b/framework/lib/src/material/navigation_rail.dart
index 73b84a7..27d244c 100644
--- a/framework/lib/src/material/navigation_rail.dart
+++ b/framework/lib/src/material/navigation_rail.dart
@@ -16,6 +16,7 @@
import 'theme.dart';
const double _kCircularIndicatorDiameter = 56;
+const double _kIndicatorHeight = 32;
/// A Material Design widget that is meant to be displayed at the left or right of an
/// app to navigate between a small number of views, typically between three and
@@ -110,13 +111,13 @@
this.minExtendedWidth,
this.useIndicator,
this.indicatorColor,
- }) : assert(destinations != null && destinations.length >= 2),
+ this.indicatorShape,
+ }) : assert(destinations.length >= 2),
assert(selectedIndex == null || (0 <= selectedIndex && selectedIndex < destinations.length)),
assert(elevation == null || elevation > 0),
assert(minWidth == null || minWidth > 0),
assert(minExtendedWidth == null || minExtendedWidth > 0),
assert((minWidth == null || minExtendedWidth == null) || minExtendedWidth >= minWidth),
- assert(extended != null),
assert(!extended || (labelType == null || labelType == NavigationRailLabelType.none));
/// Sets the color of the Container that holds all of the [NavigationRail]'s
@@ -306,8 +307,18 @@
/// Overrides the default value of [NavigationRail]'s selection indicator color,
/// when [useIndicator] is true.
+ ///
+ /// If this is null, [NavigationRailThemeData.indicatorColor] is used. If
+ /// that is null, defaults to [ColorScheme.secondaryContainer].
final Color? indicatorColor;
+ /// Overrides the default value of [NavigationRail]'s selection indicator shape,
+ /// when [useIndicator] is true.
+ ///
+ /// If this is null, [NavigationRailThemeData.indicatorShape] is used. If
+ /// that is null, defaults to [StadiumBorder].
+ final ShapeBorder? indicatorShape;
+
/// Returns the animation that controls the [NavigationRail.extended] state.
///
/// This can be used to synchronize animations in the [leading] or [trailing]
@@ -396,7 +407,7 @@
final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? defaults.labelType!;
final bool useIndicator = widget.useIndicator ?? navigationRailTheme.useIndicator ?? defaults.useIndicator!;
final Color? indicatorColor = widget.indicatorColor ?? navigationRailTheme.indicatorColor ?? defaults.indicatorColor;
- final ShapeBorder? indicatorShape = navigationRailTheme.indicatorShape ?? defaults.indicatorShape;
+ final ShapeBorder? indicatorShape = widget.indicatorShape ?? navigationRailTheme.indicatorShape ?? defaults.indicatorShape;
// For backwards compatibility, in M2 the opacity of the unselected icons needs
// to be set to the default if it isn't in the given theme. This can be removed
@@ -534,19 +545,7 @@
required this.useIndicator,
this.indicatorColor,
this.indicatorShape,
- }) : assert(minWidth != null),
- assert(minExtendedWidth != null),
- assert(icon != null),
- assert(label != null),
- assert(destinationAnimation != null),
- assert(extendedTransitionAnimation != null),
- assert(labelType != null),
- assert(selected != null),
- assert(iconTheme != null),
- assert(labelTextStyle != null),
- assert(onTap != null),
- assert(indexLabel != null),
- _positionAnimation = CurvedAnimation(
+ }) : _positionAnimation = CurvedAnimation(
parent: ReverseAnimation(destinationAnimation),
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut.flipped,
@@ -579,7 +578,8 @@
);
final bool material3 = Theme.of(context).useMaterial3;
- final double indicatorInkOffsetY;
+ final EdgeInsets destionationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context));
+ Offset indicatorOffset;
final Widget themedIcon = IconTheme(
data: iconTheme,
@@ -596,8 +596,10 @@
case NavigationRailLabelType.none:
// Split the destination spacing across the top and bottom to keep the icon centered.
final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null;
- indicatorInkOffsetY = _verticalDestinationPaddingNoLabel - (_verticalIconLabelSpacingM3 / 2);
-
+ indicatorOffset = Offset(
+ minWidth / 2 + destionationPadding.left,
+ _verticalDestinationSpacingM3 / 2 + destionationPadding.top,
+ );
final Widget iconPart = Column(
children: <Widget>[
if (spacing != null) spacing,
@@ -625,9 +627,7 @@
children: <Widget>[
iconPart,
// For semantics when label is not showing,
- SizedBox(
- width: 0,
- height: 0,
+ SizedBox.shrink(
child: Visibility.maintain(
visible: false,
child: label,
@@ -675,8 +675,12 @@
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);
- indicatorInkOffsetY = _verticalDestinationPaddingWithLabel;
-
+ final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
+ final double indicatorVerticalPadding = destionationPadding.top;
+ indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
+ if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
+ indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
+ }
content = Container(
constraints: BoxConstraints(
minWidth: minWidth,
@@ -719,7 +723,12 @@
final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel);
final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0);
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel);
- indicatorInkOffsetY = _verticalDestinationPaddingWithLabel;
+ final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
+ final double indicatorVerticalPadding = destionationPadding.top;
+ indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
+ if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
+ indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
+ }
content = Container(
constraints: BoxConstraints(
minWidth: minWidth,
@@ -761,7 +770,7 @@
splashColor: colors.primary.withOpacity(0.12),
hoverColor: colors.primary.withOpacity(0.04),
useMaterial3: material3,
- indicatorOffsetY: indicatorInkOffsetY,
+ indicatorOffset: indicatorOffset,
child: content,
),
),
@@ -783,7 +792,7 @@
super.splashColor,
super.hoverColor,
required this.useMaterial3,
- required this.indicatorOffsetY,
+ required this.indicatorOffset,
}) : super(
containedInkWell: true,
highlightShape: BoxShape.rectangle,
@@ -792,18 +801,17 @@
);
final bool useMaterial3;
- final double indicatorOffsetY;
+ final Offset indicatorOffset;
@override
RectCallback? getRectCallback(RenderBox referenceBox) {
- final double indicatorOffsetX = referenceBox.size.width / 2;
-
if (useMaterial3) {
return () {
- return Rect.fromCenter(
- center: Offset(indicatorOffsetX, indicatorOffsetY),
- width: _kCircularIndicatorDiameter,
- height: 32,
+ return Rect.fromLTWH(
+ indicatorOffset.dx - (_kCircularIndicatorDiameter / 2),
+ indicatorOffset.dy,
+ _kCircularIndicatorDiameter,
+ _kIndicatorHeight,
);
};
}
@@ -900,11 +908,11 @@
const NavigationRailDestination({
required this.icon,
Widget? selectedIcon,
+ this.indicatorColor,
+ this.indicatorShape,
required this.label,
this.padding,
- }) : selectedIcon = selectedIcon ?? icon,
- assert(icon != null),
- assert(label != null);
+ }) : selectedIcon = selectedIcon ?? icon;
/// The icon of the destination.
///
@@ -933,6 +941,12 @@
/// icons.
final Widget selectedIcon;
+ /// The color of the [indicatorShape] when this destination is selected.
+ final Color? indicatorColor;
+
+ /// The shape of the selection inidicator.
+ final ShapeBorder? indicatorShape;
+
/// The label for the destination.
///
/// The label must be provided when used with the [NavigationRail]. When the
@@ -949,7 +963,7 @@
const _ExtendedNavigationRailAnimation({
required this.animation,
required super.child,
- }) : assert(child != null);
+ });
final Animation<double> animation;
@@ -965,6 +979,7 @@
const Widget _verticalSpacer = SizedBox(height: 8.0);
const double _verticalIconLabelSpacingM3 = 4.0;
const double _verticalDestinationSpacingM3 = 12.0;
+const double _horizontalDestinationSpacingM3 = 12.0;
// Hand coded defaults based on Material Design 2.
class _NavigationRailDefaultsM2 extends NavigationRailThemeData {
@@ -1017,7 +1032,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _NavigationRailDefaultsM3 extends NavigationRailThemeData {
_NavigationRailDefaultsM3(this.context)
diff --git a/framework/lib/src/material/navigation_rail_theme.dart b/framework/lib/src/material/navigation_rail_theme.dart
index 6056e94..72780de 100644
--- a/framework/lib/src/material/navigation_rail_theme.dart
+++ b/framework/lib/src/material/navigation_rail_theme.dart
@@ -143,7 +143,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static NavigationRailThemeData? lerp(NavigationRailThemeData? a, NavigationRailThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -241,7 +240,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the background color, elevation, label text style, icon theme,
/// group alignment, and label type and border values for descendant
diff --git a/framework/lib/src/material/outlined_button.dart b/framework/lib/src/material/outlined_button.dart
index bd02a20..0f26afe 100644
--- a/framework/lib/src/material/outlined_button.dart
+++ b/framework/lib/src/material/outlined_button.dart
@@ -351,7 +351,7 @@
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
}
@@ -417,9 +417,7 @@
super.statesController,
required Widget icon,
required Widget label,
- }) : assert(icon != null),
- assert(label != null),
- super(
+ }) : super(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _OutlinedButtonWithIconChild(icon: icon, label: label),
@@ -437,7 +435,7 @@
@override
Widget build(BuildContext context) {
- final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
+ final double scale = MediaQuery.textScaleFactorOf(context);
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
return Row(
mainAxisSize: MainAxisSize.min,
@@ -453,7 +451,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _OutlinedButtonDefaultsM3 extends ButtonStyle {
_OutlinedButtonDefaultsM3(this.context)
diff --git a/framework/lib/src/material/outlined_button_theme.dart b/framework/lib/src/material/outlined_button_theme.dart
index 96948f2..bd12404 100644
--- a/framework/lib/src/material/outlined_button_theme.dart
+++ b/framework/lib/src/material/outlined_button_theme.dart
@@ -49,7 +49,6 @@
/// Linearly interpolate between two outlined button themes.
static OutlinedButtonThemeData? lerp(OutlinedButtonThemeData? a, OutlinedButtonThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -98,7 +97,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The configuration of this theme.
final OutlinedButtonThemeData data;
diff --git a/framework/lib/src/material/page.dart b/framework/lib/src/material/page.dart
index 021ec49..be482a4 100644
--- a/framework/lib/src/material/page.dart
+++ b/framework/lib/src/material/page.dart
@@ -40,9 +40,7 @@
this.maintainState = true,
super.fullscreenDialog,
super.allowSnapshotting = true,
- }) : assert(builder != null),
- assert(maintainState != null),
- assert(fullscreenDialog != null) {
+ }) {
assert(opaque);
}
@@ -109,15 +107,6 @@
Animation<double> secondaryAnimation,
) {
final Widget result = buildContent(context);
- assert(() {
- if (result == null) {
- throw FlutterError(
- 'The builder for route "${settings.name}" returned null.\n'
- 'Route builders must never return null.',
- );
- }
- return true;
- }());
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
@@ -163,9 +152,7 @@
super.name,
super.arguments,
super.restorationId,
- }) : assert(child != null),
- assert(maintainState != null),
- assert(fullscreenDialog != null);
+ });
/// The content to be shown in the [Route] created by this page.
final Widget child;
@@ -193,8 +180,7 @@
_PageBasedMaterialPageRoute({
required MaterialPage<T> page,
super.allowSnapshotting,
- }) : assert(page != null),
- super(settings: page) {
+ }) : super(settings: page) {
assert(opaque);
}
diff --git a/framework/lib/src/material/page_transitions_theme.dart b/framework/lib/src/material/page_transitions_theme.dart
index c65f24f..f41fbc7 100644
--- a/framework/lib/src/material/page_transitions_theme.dart
+++ b/framework/lib/src/material/page_transitions_theme.dart
@@ -157,9 +157,9 @@
required this.animation,
required this.secondaryAnimation,
required this.allowSnapshotting,
+ required this.allowEnterRouteSnapshotting,
this.child,
- }) : assert(animation != null),
- assert(secondaryAnimation != null);
+ });
// A curve sequence that is similar to the 'fastOutExtraSlowIn' curve used in
// the native transition.
@@ -207,6 +207,15 @@
/// [secondaryAnimation].
final Widget? child;
+ /// Whether to enable snapshotting on the entering route during the
+ /// transition animation.
+ ///
+ /// If not specified, defaults to true.
+ /// If false, the route snapshotting will not be applied to the route being
+ /// animating into, e.g. when transitioning from route A to route B, B will
+ /// not be snapshotted.
+ final bool allowEnterRouteSnapshotting;
+
@override
Widget build(BuildContext context) {
return DualTransitionBuilder(
@@ -218,7 +227,7 @@
) {
return _ZoomEnterTransition(
animation: animation,
- allowSnapshotting: allowSnapshotting,
+ allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting,
child: child,
);
},
@@ -243,7 +252,7 @@
) {
return _ZoomEnterTransition(
animation: animation,
- allowSnapshotting: allowSnapshotting,
+ allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting ,
reverse: true,
child: child,
);
@@ -271,8 +280,7 @@
this.reverse = false,
required this.allowSnapshotting,
this.child,
- }) : assert(animation != null),
- assert(reverse != null);
+ });
final Animation<double> animation;
final Widget? child;
@@ -381,8 +389,7 @@
this.reverse = false,
required this.allowSnapshotting,
this.child,
- }) : assert(animation != null),
- assert(reverse != null);
+ });
final Animation<double> animation;
final bool allowSnapshotting;
@@ -596,7 +603,18 @@
class ZoomPageTransitionsBuilder extends PageTransitionsBuilder {
/// Constructs a page transition animation that matches the transition used on
/// Android Q.
- const ZoomPageTransitionsBuilder();
+ const ZoomPageTransitionsBuilder({
+ this.allowEnterRouteSnapshotting = true,
+ });
+
+ /// Whether to enable snapshotting on the entering route during the
+ /// transition animation.
+ ///
+ /// If not specified, defaults to true.
+ /// If false, the route snapshotting will not be applied to the route being
+ /// animating into, e.g. when transitioning from route A to route B, B will
+ /// not be snapshotted.
+ final bool allowEnterRouteSnapshotting;
@override
Widget buildTransitions<T>(
@@ -610,6 +628,7 @@
animation: animation,
secondaryAnimation: secondaryAnimation,
allowSnapshotting: route?.allowSnapshotting ?? true,
+ allowEnterRouteSnapshotting: allowEnterRouteSnapshotting,
child: child,
);
}
diff --git a/framework/lib/src/material/paginated_data_table.dart b/framework/lib/src/material/paginated_data_table.dart
index 1677546..56ed3b4 100644
--- a/framework/lib/src/material/paginated_data_table.dart
+++ b/framework/lib/src/material/paginated_data_table.dart
@@ -88,27 +88,16 @@
this.checkboxHorizontalMargin,
this.controller,
this.primary,
- }) : assert(actions == null || (actions != null && header != null)),
- assert(columns != null),
- assert(dragStartBehavior != null),
+ }) : assert(actions == null || (header != null)),
assert(columns.isNotEmpty),
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
- assert(sortAscending != null),
- assert(dataRowHeight != null),
- assert(headingRowHeight != null),
- assert(horizontalMargin != null),
- assert(columnSpacing != null),
- assert(showCheckboxColumn != null),
- assert(showFirstLastButtons != null),
- assert(rowsPerPage != null),
assert(rowsPerPage > 0),
assert(() {
if (onRowsPerPageChanged != null) {
- assert(availableRowsPerPage != null && availableRowsPerPage.contains(rowsPerPage));
+ assert(availableRowsPerPage.contains(rowsPerPage));
}
return true;
}()),
- assert(source != null),
assert(!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
'You cannot both set primary to true and pass an explicit controller.',
diff --git a/framework/lib/src/material/popup_menu.dart b/framework/lib/src/material/popup_menu.dart
index c6861fd..44991de 100644
--- a/framework/lib/src/material/popup_menu.dart
+++ b/framework/lib/src/material/popup_menu.dart
@@ -131,7 +131,7 @@
const _MenuItem({
required this.onLayout,
required super.child,
- }) : assert(onLayout != null);
+ });
final ValueChanged<Size> onLayout;
@@ -147,7 +147,7 @@
}
class _RenderMenuItem extends RenderShiftedBox {
- _RenderMenuItem(this.onLayout, [RenderBox? child]) : assert(onLayout != null), super(child);
+ _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
ValueChanged<Size> onLayout;
@@ -229,8 +229,7 @@
this.labelTextStyle,
this.mouseCursor,
required this.child,
- }) : assert(enabled != null),
- assert(height != null);
+ });
/// The value that will be returned by [showMenu] if this entry is selected.
final T? value;
@@ -478,7 +477,7 @@
super.height,
super.mouseCursor,
super.child,
- }) : assert(checked != null);
+ });
/// Whether to display a checkmark next to the menu item.
///
@@ -574,7 +573,7 @@
);
Widget item = route.items[i];
if (route.initialValue != null && route.items[i].represents(route.initialValue)) {
- item = Container(
+ item = ColoredBox(
color: Theme.of(context).highlightColor,
child: item,
);
@@ -698,7 +697,7 @@
final double buttonHeight = size.height - position.top - position.bottom;
// Find the ideal vertical position.
double y = position.top;
- if (selectedItemIndex != null && itemSizes != null) {
+ if (selectedItemIndex != null) {
double selectedItemOffset = _kMenuVerticalPadding;
for (int index = 0; index < selectedItemIndex!; index += 1) {
selectedItemOffset += itemSizes[index]!.height;
@@ -717,7 +716,6 @@
x = position.left;
} else {
// Menu button is equidistant from both edges, so grow in reading direction.
- assert(textDirection != null);
switch (textDirection) {
case TextDirection.rtl:
x = size.width - position.right - childSize.width;
@@ -795,8 +793,8 @@
this.constraints,
required this.clipBehavior,
}) : itemSizes = List<Size?>.filled(items.length, null),
- // Menus always cycle focus through its items irrespective of the
- // focus traversal edge behavior set in the MediaQuery.
+ // Menus always cycle focus through their items irrespective of the
+ // focus traversal edge behavior set in the Navigator.
super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop);
final RelativeRect position;
@@ -955,10 +953,7 @@
BoxConstraints? constraints,
Clip clipBehavior = Clip.none,
}) {
- assert(context != null);
- assert(position != null);
- assert(useRootNavigator != null);
- assert(items != null && items.isNotEmpty);
+ assert(items.isNotEmpty);
assert(debugCheckHasMaterialLocalizations(context));
switch (Theme.of(context).platform) {
@@ -1067,9 +1062,7 @@
this.constraints,
this.position,
this.clipBehavior = Clip.none,
- }) : assert(itemBuilder != null),
- assert(enabled != null),
- assert(
+ }) : assert(
!(child != null && icon != null),
'You can only pass [child] or [icon], not both.',
);
@@ -1295,7 +1288,7 @@
}
bool get _canRequestFocus {
- final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
+ final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return widget.enabled;
@@ -1378,7 +1371,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _PopupMenuDefaultsM3 extends PopupMenuThemeData {
_PopupMenuDefaultsM3(this.context)
diff --git a/framework/lib/src/material/popup_menu_theme.dart b/framework/lib/src/material/popup_menu_theme.dart
index df7aa57..9b3d4c8 100644
--- a/framework/lib/src/material/popup_menu_theme.dart
+++ b/framework/lib/src/material/popup_menu_theme.dart
@@ -129,7 +129,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static PopupMenuThemeData? lerp(PopupMenuThemeData? a, PopupMenuThemeData? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -212,7 +211,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties for descendant popup menu widgets.
final PopupMenuThemeData data;
diff --git a/framework/lib/src/material/progress_indicator.dart b/framework/lib/src/material/progress_indicator.dart
index e2a65fe..85b7adf 100644
--- a/framework/lib/src/material/progress_indicator.dart
+++ b/framework/lib/src/material/progress_indicator.dart
@@ -145,7 +145,7 @@
this.value,
required this.animationValue,
required this.textDirection,
- }) : assert(textDirection != null);
+ });
final Color backgroundColor;
final Color valueColor;
@@ -515,8 +515,8 @@
/// [CupertinoActivityIndicator] in iOS and [CircularProgressIndicator] in
/// material theme/non-iOS.
///
- /// The [value], [backgroundColor], [valueColor], [strokeWidth],
- /// [semanticsLabel], and [semanticsValue] will be ignored in iOS.
+ /// The [value], [valueColor], [strokeWidth], [semanticsLabel], and
+ /// [semanticsValue] will be ignored in iOS.
///
/// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
const CircularProgressIndicator.adaptive({
@@ -654,7 +654,6 @@
return _buildAnimation();
case _ActivityIndicatorType.adaptive:
final ThemeData theme = Theme.of(context);
- assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
@@ -925,7 +924,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _CircularProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData {
_CircularProgressIndicatorDefaultsM3(this.context);
diff --git a/framework/lib/src/material/progress_indicator_theme.dart b/framework/lib/src/material/progress_indicator_theme.dart
index 7cef172..b2b5521 100644
--- a/framework/lib/src/material/progress_indicator_theme.dart
+++ b/framework/lib/src/material/progress_indicator_theme.dart
@@ -87,7 +87,6 @@
if (a == null && b == null) {
return null;
}
- assert(t != null);
return ProgressIndicatorThemeData(
color: Color.lerp(a?.color, b?.color, t),
linearTrackColor : Color.lerp(a?.linearTrackColor, b?.linearTrackColor, t),
@@ -160,7 +159,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties for descendant [ProgressIndicator] widgets.
final ProgressIndicatorThemeData data;
diff --git a/framework/lib/src/material/radio.dart b/framework/lib/src/material/radio.dart
index a6dcdb7..8fc68d5 100644
--- a/framework/lib/src/material/radio.dart
+++ b/framework/lib/src/material/radio.dart
@@ -93,8 +93,7 @@
this.visualDensity,
this.focusNode,
this.autofocus = false,
- }) : assert(autofocus != null),
- assert(toggleable != null);
+ });
/// The value represented by this radio button.
final T value;
@@ -537,7 +536,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _RadioDefaultsM3 extends RadioThemeData {
_RadioDefaultsM3(this.context);
diff --git a/framework/lib/src/material/radio_list_tile.dart b/framework/lib/src/material/radio_list_tile.dart
index b3a4237..754220c 100644
--- a/framework/lib/src/material/radio_list_tile.dart
+++ b/framework/lib/src/material/radio_list_tile.dart
@@ -53,7 +53,7 @@
///
/// {@tool snippet}
/// ```dart
-/// Container(
+/// ColoredBox(
/// color: Colors.green,
/// child: Material(
/// child: RadioListTile<Meridiem>(
@@ -86,6 +86,13 @@
/// ** See code in examples/api/lib/material/radio_list_tile/radio_list_tile.0.dart **
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This sample demonstrates how [RadioListTile] positions the radio widget
+/// relative to the text in different configurations.
+///
+/// ** See code in examples/api/lib/material/radio_list_tile/radio_list_tile.1.dart **
+/// {@end-tool}
+///
/// ## Semantics in RadioListTile
///
/// Since the entirety of the RadioListTile is interactive, it should represent
@@ -110,7 +117,7 @@
/// LinkedLabelRadio, that includes an interactive [RichText] widget that
/// handles tap gestures.
///
-/// ** See code in examples/api/lib/material/radio_list_tile/radio_list_tile.1.dart **
+/// ** See code in examples/api/lib/material/radio_list_tile/custom_labeled_radio.0.dart **
/// {@end-tool}
///
/// ## RadioListTile isn't exactly what I want
@@ -126,7 +133,7 @@
/// Here is an example of a custom LabeledRadio widget, but you can easily
/// make your own configurable widget.
///
-/// ** See code in examples/api/lib/material/radio_list_tile/radio_list_tile.2.dart **
+/// ** See code in examples/api/lib/material/radio_list_tile/custom_labeled_radio.1.dart **
/// {@end-tool}
///
/// See also:
@@ -173,12 +180,7 @@
this.focusNode,
this.onFocusChange,
this.enableFeedback,
- }) : assert(toggleable != null),
- assert(isThreeLine != null),
- assert(!isThreeLine || subtitle != null),
- assert(selected != null),
- assert(controlAffinity != null),
- assert(autofocus != null);
+ }) : assert(!isThreeLine || subtitle != null);
/// The value represented by this radio button.
final T value;
@@ -245,7 +247,7 @@
/// The color to use when this radio button is selected.
///
- /// Defaults to accent color of the current [Theme].
+ /// Defaults to [ColorScheme.secondary] of the current [Theme].
final Color? activeColor;
/// The primary content of the list tile.
diff --git a/framework/lib/src/material/range_slider.dart b/framework/lib/src/material/range_slider.dart
index 7396c96..bdbb5ff 100644
--- a/framework/lib/src/material/range_slider.dart
+++ b/framework/lib/src/material/range_slider.dart
@@ -14,6 +14,7 @@
import 'constants.dart';
import 'debug.dart';
+import 'material_state.dart';
import 'slider_theme.dart';
import 'theme.dart';
@@ -144,11 +145,10 @@
this.labels,
this.activeColor,
this.inactiveColor,
+ this.overlayColor,
+ this.mouseCursor,
this.semanticFormatterCallback,
- }) : assert(values != null),
- assert(min != null),
- assert(max != null),
- assert(min <= max),
+ }) : assert(min <= max),
assert(values.start <= values.end),
assert(values.start >= min && values.start <= max),
assert(values.end >= min && values.end <= max),
@@ -322,6 +322,26 @@
/// appearance of various components of the slider.
final Color? inactiveColor;
+ /// The highlight color that's typically used to indicate that
+ /// the range slider thumb is hovered or dragged.
+ ///
+ /// If this property is null, [RangeSlider] will use [activeColor] with
+ /// an opacity of 0.12. If null, [SliderThemeData.overlayColor]
+ /// will be used, otherwise defaults to [ColorScheme.primary] with
+ /// an opacity of 0.12.
+ final MaterialStateProperty<Color?>? overlayColor;
+
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If null, then the value of [SliderThemeData.mouseCursor] is used. If that
+ /// is also null, then [MaterialStateMouseCursor.clickable] is used.
+ ///
+ /// See also:
+ ///
+ /// * [MaterialStateMouseCursor], which can be used to create a [MouseCursor].
+ final MaterialStateProperty<MouseCursor?>? mouseCursor;
+
/// The callback used to create a semantic value from the slider's values.
///
/// Defaults to formatting values as a percentage.
@@ -400,6 +420,16 @@
PaintRangeValueIndicator? paintTopValueIndicator;
PaintRangeValueIndicator? paintBottomValueIndicator;
+ bool get _enabled => widget.onChanged != null;
+
+ bool _dragging = false;
+
+ bool _hovering = false;
+ void _handleHoverChanged(bool hovering) {
+ if (hovering != _hovering) {
+ setState(() { _hovering = hovering; });
+ }
+ }
@override
void initState() {
@@ -415,7 +445,7 @@
enableController = AnimationController(
duration: enableAnimationDuration,
vsync: this,
- value: widget.onChanged != null ? 1.0 : 0.0,
+ value: _enabled ? 1.0 : 0.0,
);
startPositionController = AnimationController(
duration: Duration.zero,
@@ -436,7 +466,7 @@
return;
}
final bool wasEnabled = oldWidget.onChanged != null;
- final bool isEnabled = widget.onChanged != null;
+ final bool isEnabled = _enabled;
if (wasEnabled != isEnabled) {
if (isEnabled) {
enableController.forward();
@@ -462,7 +492,7 @@
}
void _handleChanged(RangeValues values) {
- assert(widget.onChanged != null);
+ assert(_enabled);
final RangeValues lerpValues = _lerpRangeValues(values);
if (lerpValues != widget.values) {
widget.onChanged!(lerpValues);
@@ -471,11 +501,13 @@
void _handleDragStart(RangeValues values) {
assert(widget.onChangeStart != null);
+ _dragging = true;
widget.onChangeStart!(_lerpRangeValues(values));
}
void _handleDragEnd(RangeValues values) {
assert(widget.onChangeEnd != null);
+ _dragging = false;
widget.onChangeEnd!(_lerpRangeValues(values));
}
@@ -576,6 +608,12 @@
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
const double defaultMinThumbSeparation = 8;
+ final Set<MaterialState> states = <MaterialState>{
+ if (!_enabled) MaterialState.disabled,
+ if (_hovering) MaterialState.hovered,
+ if (_dragging) MaterialState.dragged,
+ };
+
// The value indicator's color is not the same as the thumb and active track
// (which can be defined by activeColor) if the
// RectangularSliderValueIndicatorShape is used. In all other cases, the
@@ -588,6 +626,13 @@
valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
}
+ Color? effectiveOverlayColor() {
+ return widget.overlayColor?.resolve(states)
+ ?? widget.activeColor?.withOpacity(0.12)
+ ?? MaterialStateProperty.resolveAs<Color?>(sliderTheme.overlayColor, states)
+ ?? theme.colorScheme.primary.withOpacity(0.12);
+ }
+
sliderTheme = sliderTheme.copyWith(
trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
@@ -601,7 +646,7 @@
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface,
disabledThumbColor: sliderTheme.disabledThumbColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(.38), theme.colorScheme.surface),
- overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12),
+ overlayColor: effectiveOverlayColor(),
valueIndicatorColor: valueIndicatorColor,
rangeTrackShape: sliderTheme.rangeTrackShape ?? defaultTrackShape,
rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? defaultTickMarkShape,
@@ -615,26 +660,36 @@
minThumbSeparation: sliderTheme.minThumbSeparation ?? defaultMinThumbSeparation,
thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector,
);
+ final MouseCursor effectiveMouseCursor = widget.mouseCursor?.resolve(states)
+ ?? sliderTheme.mouseCursor?.resolve(states)
+ ?? MaterialStateMouseCursor.clickable.resolve(states);
// This size is used as the max bounds for the painting of the value
// indicators. It must be kept in sync with the function with the same name
// in slider.dart.
- Size screenSize() => MediaQuery.of(context).size;
+ Size screenSize() => MediaQuery.sizeOf(context);
- return CompositedTransformTarget(
- link: _layerLink,
- child: _RangeSliderRenderObjectWidget(
- values: _unlerpRangeValues(widget.values),
- divisions: widget.divisions,
- labels: widget.labels,
- sliderTheme: sliderTheme,
- textScaleFactor: MediaQuery.of(context).textScaleFactor,
- screenSize: screenSize(),
- onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
- onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
- onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
- state: this,
- semanticFormatterCallback: widget.semanticFormatterCallback,
+ return FocusableActionDetector(
+ enabled: _enabled,
+ onShowHoverHighlight: _handleHoverChanged,
+ includeFocusSemantics: false,
+ mouseCursor: effectiveMouseCursor,
+ child: CompositedTransformTarget(
+ link: _layerLink,
+ child: _RangeSliderRenderObjectWidget(
+ values: _unlerpRangeValues(widget.values),
+ divisions: widget.divisions,
+ labels: widget.labels,
+ sliderTheme: sliderTheme,
+ textScaleFactor: MediaQuery.of(context).textScaleFactor,
+ screenSize: screenSize(),
+ onChanged: _enabled && (widget.max > widget.min) ? _handleChanged : null,
+ onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
+ onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
+ state: this,
+ semanticFormatterCallback: widget.semanticFormatterCallback,
+ hovering: _hovering,
+ ),
),
);
}
@@ -673,6 +728,7 @@
required this.onChangeEnd,
required this.state,
required this.semanticFormatterCallback,
+ required this.hovering,
});
final RangeValues values;
@@ -686,6 +742,7 @@
final ValueChanged<RangeValues>? onChangeEnd;
final SemanticFormatterCallback? semanticFormatterCallback;
final _RangeSliderState state;
+ final bool hovering;
@override
_RenderRangeSlider createRenderObject(BuildContext context) {
@@ -704,7 +761,8 @@
textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform,
- gestureSettings: MediaQuery.of(context).gestureSettings,
+ hovering: hovering,
+ gestureSettings: MediaQuery.gestureSettingsOf(context),
);
}
@@ -726,7 +784,8 @@
..textDirection = Directionality.of(context)
..semanticFormatterCallback = semanticFormatterCallback
..platform = Theme.of(context).platform
- ..gestureSettings = MediaQuery.of(context).gestureSettings;
+ ..hovering = hovering
+ ..gestureSettings = MediaQuery.gestureSettingsOf(context);
}
}
@@ -746,12 +805,10 @@
required this.onChangeEnd,
required _RangeSliderState state,
required TextDirection textDirection,
+ required bool hovering,
required DeviceGestureSettings gestureSettings,
- }) : assert(values != null),
- assert(values.start >= 0.0 && values.start <= 1.0),
+ }) : assert(values.start >= 0.0 && values.start <= 1.0),
assert(values.end >= 0.0 && values.end <= 1.0),
- assert(state != null),
- assert(textDirection != null),
_platform = platform,
_semanticFormatterCallback = semanticFormatterCallback,
_labels = labels,
@@ -763,7 +820,8 @@
_screenSize = screenSize,
_onChanged = onChanged,
_state = state,
- _textDirection = textDirection {
+ _textDirection = textDirection,
+ _hovering = hovering {
_updateLabelPainters();
final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer()
@@ -842,6 +900,8 @@
late RangeValues _newValues;
late Offset _startThumbCenter;
late Offset _endThumbCenter;
+ Rect? overlayStartRect;
+ Rect? overlayEndRect;
bool get isEnabled => onChanged != null;
@@ -852,9 +912,8 @@
RangeValues get values => _values;
RangeValues _values;
set values(RangeValues newValues) {
- assert(newValues != null);
- assert(newValues.start != null && newValues.start >= 0.0 && newValues.start <= 1.0);
- assert(newValues.end != null && newValues.end >= 0.0 && newValues.end <= 1.0);
+ assert(newValues.start >= 0.0 && newValues.start <= 1.0);
+ assert(newValues.end >= 0.0 && newValues.end <= 1.0);
assert(newValues.start <= newValues.end);
final RangeValues convertedValues = isDiscrete ? _discretizeRangeValues(newValues) : newValues;
if (convertedValues == _values) {
@@ -985,7 +1044,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (value == _textDirection) {
return;
}
@@ -993,6 +1051,50 @@
_updateLabelPainters();
}
+ /// True if this slider is being hovered over by a pointer.
+ bool get hovering => _hovering;
+ bool _hovering;
+ set hovering(bool value) {
+ if (value == _hovering) {
+ return;
+ }
+ _hovering = value;
+ _updateForHover(_hovering);
+ }
+
+ /// True if the slider is interactive and the start thumb is being
+ /// hovered over by a pointer.
+ bool _hoveringStartThumb = false;
+ bool get hoveringStartThumb => _hoveringStartThumb;
+ set hoveringStartThumb(bool value) {
+ if (value == _hoveringStartThumb) {
+ return;
+ }
+ _hoveringStartThumb = value;
+ _updateForHover(_hovering);
+ }
+
+ /// True if the slider is interactive and the end thumb is being
+ /// hovered over by a pointer.
+ bool _hoveringEndThumb = false;
+ bool get hoveringEndThumb => _hoveringEndThumb;
+ set hoveringEndThumb(bool value) {
+ if (value == _hoveringEndThumb) {
+ return;
+ }
+ _hoveringEndThumb = value;
+ _updateForHover(_hovering);
+ }
+
+ void _updateForHover(bool hovered) {
+ // Only show overlay when pointer is hovering the thumb.
+ if (hovered && (hoveringStartThumb || hoveringEndThumb)) {
+ _state.overlayController.forward();
+ } else {
+ _state.overlayController.reverse();
+ }
+ }
+
bool get showValueIndicator {
switch (_sliderTheme.showValueIndicator!) {
case ShowValueIndicator.onlyForDiscrete:
@@ -1253,6 +1355,14 @@
_drag.addPointer(event);
_tap.addPointer(event);
}
+ if (isEnabled) {
+ if (overlayStartRect != null) {
+ hoveringStartThumb = overlayStartRect!.contains(event.localPosition);
+ }
+ if (overlayEndRect != null) {
+ hoveringEndThumb = overlayEndRect!.contains(event.localPosition);
+ }
+ }
}
@override
@@ -1307,6 +1417,11 @@
);
_startThumbCenter = Offset(trackRect.left + startVisualPosition * trackRect.width, trackRect.center.dy);
_endThumbCenter = Offset(trackRect.left + endVisualPosition * trackRect.width, trackRect.center.dy);
+ if (isEnabled) {
+ final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isEnabled, false);
+ overlayStartRect = Rect.fromCircle(center: _startThumbCenter, radius: overlaySize.width / 2.0);
+ overlayEndRect = Rect.fromCircle(center: _endThumbCenter, radius: overlaySize.width / 2.0);
+ }
_sliderTheme.rangeTrackShape!.paint(
context,
@@ -1326,7 +1441,7 @@
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;
if (!_overlayAnimation.isDismissed) {
- if (startThumbSelected) {
+ if (startThumbSelected || hoveringStartThumb) {
_sliderTheme.overlayShape!.paint(
context,
_startThumbCenter,
@@ -1342,7 +1457,7 @@
sizeWithOverflow: resolvedscreenSize,
);
}
- if (endThumbSelected) {
+ if (endThumbSelected || hoveringEndThumb) {
_sliderTheme.overlayShape!.paint(
context,
_endThumbCenter,
diff --git a/framework/lib/src/material/refresh_indicator.dart b/framework/lib/src/material/refresh_indicator.dart
index a0d44c8..c04a70f 100644
--- a/framework/lib/src/material/refresh_indicator.dart
+++ b/framework/lib/src/material/refresh_indicator.dart
@@ -138,11 +138,7 @@
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
- }) : assert(child != null),
- assert(onRefresh != null),
- assert(notificationPredicate != null),
- assert(strokeWidth != null),
- assert(triggerMode != null);
+ });
/// The widget below this widget in the tree.
///
@@ -469,29 +465,12 @@
.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
.then<void>((void value) {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
- assert(widget.onRefresh != null);
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
});
final Future<void> refreshResult = widget.onRefresh();
- assert(() {
- if (refreshResult == null) {
- FlutterError.reportError(FlutterErrorDetails(
- exception: FlutterError(
- 'The onRefresh callback returned null.\n'
- 'The RefreshIndicator onRefresh callback must return a Future.',
- ),
- context: ErrorDescription('when calling onRefresh'),
- library: 'material library',
- ));
- }
- return true;
- }());
- if (refreshResult == null) {
- return;
- }
refreshResult.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete();
diff --git a/framework/lib/src/material/reorderable_list.dart b/framework/lib/src/material/reorderable_list.dart
index 8cf0d4a..ec985bd 100644
--- a/framework/lib/src/material/reorderable_list.dart
+++ b/framework/lib/src/material/reorderable_list.dart
@@ -85,10 +85,7 @@
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
- }) : assert(scrollDirection != null),
- assert(onReorder != null),
- assert(children != null),
- assert(
+ }) : assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
),
@@ -96,7 +93,6 @@
children.every((Widget w) => w.key != null),
'All children of this widget must have a key.',
),
- assert(buildDefaultDragHandles != null),
itemBuilder = ((BuildContext context, int index) => children[index]),
itemCount = children.length;
@@ -154,14 +150,11 @@
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
- }) : assert(scrollDirection != null),
- assert(itemCount >= 0),
- assert(onReorder != null),
+ }) : assert(itemCount >= 0),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
- ),
- assert(buildDefaultDragHandles != null);
+ );
/// {@macro flutter.widgets.reorderable_list.itemBuilder}
final IndexedWidgetBuilder itemBuilder;
diff --git a/framework/lib/src/material/scaffold.dart b/framework/lib/src/material/scaffold.dart
index 6a82925..a5a0885 100644
--- a/framework/lib/src/material/scaffold.dart
+++ b/framework/lib/src/material/scaffold.dart
@@ -100,7 +100,7 @@
const ScaffoldMessenger({
super.key,
required this.child,
- }) : assert(child != null);
+ });
/// The widget below this widget in the tree.
///
@@ -144,7 +144,6 @@
/// * [debugCheckHasScaffoldMessenger], which asserts that the given context
/// has a [ScaffoldMessenger] ancestor.
static ScaffoldMessengerState of(BuildContext context) {
- assert(context != null);
assert(debugCheckHasScaffoldMessenger(context));
final _ScaffoldMessengerScope scope = context.dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>()!;
@@ -161,7 +160,6 @@
/// * [of], which is a similar function, except that it will throw an
/// exception if a [ScaffoldMessenger] is not found in the given context.
static ScaffoldMessengerState? maybeOf(BuildContext context) {
- assert(context != null);
final _ScaffoldMessengerScope? scope = context.dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>();
return scope?._scaffoldMessengerState;
@@ -191,18 +189,18 @@
@override
void didChangeDependencies() {
- final MediaQueryData mediaQuery = MediaQuery.of(context);
+ final bool accessibleNavigation = MediaQuery.accessibleNavigationOf(context);
// If we transition from accessible navigation to non-accessible navigation
// and there is a SnackBar that would have timed out that has already
// completed its timer, dismiss that SnackBar. If the timer hasn't finished
// yet, let it timeout as normal.
if ((_accessibleNavigation ?? false)
- && !mediaQuery.accessibleNavigation
+ && !accessibleNavigation
&& _snackBarTimer != null
&& !_snackBarTimer!.isActive) {
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
}
- _accessibleNavigation = mediaQuery.accessibleNavigation;
+ _accessibleNavigation = accessibleNavigation;
super.didChangeDependencies();
}
@@ -383,7 +381,6 @@
/// The removed snack bar does not run its normal exit animation. If there are
/// any queued snack bars, they begin their entrance animation immediately.
void removeCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.remove }) {
- assert(reason != null);
if (_snackBars.isEmpty) {
return;
}
@@ -401,7 +398,6 @@
///
/// The closed completer is called after the animation is complete.
void hideCurrentSnackBar({ SnackBarClosedReason reason = SnackBarClosedReason.hide }) {
- assert(reason != null);
if (_snackBars.isEmpty || _snackBarController!.status == AnimationStatus.dismissed) {
return;
}
@@ -518,7 +514,6 @@
/// The removed material banner does not run its normal exit animation. If there are
/// any queued material banners, they begin their entrance animation immediately.
void removeCurrentMaterialBanner({ MaterialBannerClosedReason reason = MaterialBannerClosedReason.remove }) {
- assert(reason != null);
if (_materialBanners.isEmpty) {
return;
}
@@ -535,7 +530,6 @@
///
/// The closed completer is called after the animation is complete.
void hideCurrentMaterialBanner({ MaterialBannerClosedReason reason = MaterialBannerClosedReason.hide }) {
- assert(reason != null);
if (_materialBanners.isEmpty || _materialBannerController!.status == AnimationStatus.dismissed) {
return;
}
@@ -568,8 +562,7 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- _accessibleNavigation = mediaQuery.accessibleNavigation;
+ _accessibleNavigation = MediaQuery.accessibleNavigationOf(context);
if (_snackBars.isNotEmpty) {
final ModalRoute<dynamic>? route = ModalRoute.of(context);
@@ -582,8 +575,7 @@
_snackBarController!.status == AnimationStatus.completed,
);
// Look up MediaQuery again in case the setting changed.
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- if (mediaQuery.accessibleNavigation && snackBar.action != null) {
+ if (snackBar.action != null && MediaQuery.accessibleNavigationOf(context)) {
return;
}
hideCurrentSnackBar(reason: SnackBarClosedReason.timeout);
@@ -829,8 +821,7 @@
}
class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenable<ScaffoldGeometry> {
- _ScaffoldGeometryNotifier(this.geometry, this.context)
- : assert (context != null);
+ _ScaffoldGeometryNotifier(this.geometry, this.context);
final BuildContext context;
double? floatingActionButtonScale;
@@ -882,11 +873,8 @@
required this.bottomWidgetsHeight,
required this.appBarHeight,
required this.materialBannerHeight,
- }) : assert(bottomWidgetsHeight != null),
- assert(bottomWidgetsHeight >= 0),
- assert(appBarHeight != null),
+ }) : assert(bottomWidgetsHeight >= 0),
assert(appBarHeight >= 0),
- assert(materialBannerHeight != null),
assert(materialBannerHeight >= 0);
final double bottomWidgetsHeight;
@@ -923,9 +911,7 @@
required this.extendBody,
required this.extendBodyBehindAppBar,
required this.body,
- }) : assert(extendBody != null),
- assert(extendBodyBehindAppBar != null),
- assert(body != null);
+ });
final Widget body;
final bool extendBody;
@@ -981,13 +967,7 @@
required this.extendBody,
required this.extendBodyBehindAppBar,
required this.extendBodyBehindMaterialBanner,
- }) : assert(minInsets != null),
- assert(textDirection != null),
- assert(geometryNotifier != null),
- assert(previousFloatingActionButtonLocation != null),
- assert(currentFloatingActionButtonLocation != null),
- assert(extendBody != null),
- assert(extendBodyBehindAppBar != null);
+ });
final bool extendBody;
final bool extendBodyBehindAppBar;
@@ -1253,9 +1233,7 @@
required this.fabMotionAnimator,
required this.geometryNotifier,
required this.currentController,
- }) : assert(fabMoveAnimation != null),
- assert(fabMotionAnimator != null),
- assert(currentController != null);
+ });
final Widget? child;
final Animation<double> fabMoveAnimation;
@@ -1602,10 +1580,7 @@
this.drawerEnableOpenDragGesture = true,
this.endDrawerEnableOpenDragGesture = true,
this.restorationId,
- }) : assert(primary != null),
- assert(extendBody != null),
- assert(extendBodyBehindAppBar != null),
- assert(drawerDragStartBehavior != null);
+ });
/// If true, and [bottomNavigationBar] or [persistentFooterButtons]
/// is specified, then the [body] extends to the bottom of the Scaffold,
@@ -1824,11 +1799,11 @@
/// drawer.
///
/// By default, the value used is 20.0 added to the padding edge of
- /// `MediaQuery.of(context).padding` that corresponds to the surrounding
+ /// `MediaQuery.paddingOf(context)` that corresponds to the surrounding
/// [TextDirection]. This ensures that the drag area for notched devices is
/// not obscured. For example, if `TextDirection.of(context)` is set to
/// [TextDirection.ltr], 20.0 will be added to
- /// `MediaQuery.of(context).padding.left`.
+ /// `MediaQuery.paddingOf(context).left`.
final double? drawerEdgeDragWidth;
/// Determines if the [Scaffold.drawer] can be opened with a drag
@@ -1900,7 +1875,6 @@
/// If there is no [Scaffold] in scope, then this will throw an exception.
/// To return null if there is no [Scaffold], use [maybeOf] instead.
static ScaffoldState of(BuildContext context) {
- assert(context != null);
final ScaffoldState? result = context.findAncestorStateOfType<ScaffoldState>();
if (result != null) {
return result;
@@ -1947,7 +1921,6 @@
/// encloses the given context. Also includes some sample code in its
/// documentation.
static ScaffoldState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<ScaffoldState>();
}
@@ -2016,8 +1989,6 @@
/// * [Scaffold.of], which provides access to the [ScaffoldState] object as a
/// whole, from which you can show bottom sheets, and so forth.
static bool hasDrawer(BuildContext context, { bool registerForUpdates = true }) {
- assert(registerForUpdates != null);
- assert(context != null);
if (registerForUpdates) {
final _ScaffoldScope? scaffold = context.dependOnInheritedWidgetOfExactType<_ScaffoldScope>();
return scaffold?.hasDrawer ?? false;
@@ -2232,7 +2203,9 @@
child: DraggableScrollableActuator(
child: StatefulBuilder(
key: _currentBottomSheetKey,
- builder: (BuildContext context, StateSetter setState) => widget.bottomSheet!,
+ builder: (BuildContext context, StateSetter setState) {
+ return widget.bottomSheet ?? const SizedBox.shrink();
+ },
),
),
);
@@ -2509,7 +2482,6 @@
/// Sets the current value of the visibility animation for the
/// [Scaffold.floatingActionButton]. This value must not be null.
set _floatingActionButtonVisibilityValue(double newValue) {
- assert(newValue != null);
_floatingActionButtonVisibilityController.value = clampDouble(newValue,
_floatingActionButtonVisibilityController.lowerBound,
_floatingActionButtonVisibilityController.upperBound,
@@ -2747,7 +2719,6 @@
///
/// The `value` parameter must not be null.
void showBodyScrim(bool value, double opacity) {
- assert(value != null);
if (_showBodyScrim == value && _bodyScrimColor.opacity == opacity) {
return;
}
@@ -2761,7 +2732,6 @@
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasDirectionality(context));
- final MediaQueryData mediaQuery = MediaQuery.of(context);
final ThemeData themeData = Theme.of(context);
final TextDirection textDirection = Directionality.of(context);
@@ -2796,7 +2766,7 @@
}
if (widget.appBar != null) {
- final double topPadding = widget.primary ? mediaQuery.padding.top : 0.0;
+ final double topPadding = widget.primary ? MediaQuery.paddingOf(context).top : 0.0;
_appBarMaxHeight = AppBar.preferredHeightFor(context, widget.appBar!.preferredSize) + topPadding;
assert(_appBarMaxHeight! >= 0.0 && _appBarMaxHeight!.isFinite);
_addIfNonNull(
@@ -2973,14 +2943,14 @@
}
// The minimum insets for contents of the Scaffold to keep visible.
- final EdgeInsets minInsets = mediaQuery.padding.copyWith(
- bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
+ final EdgeInsets minInsets = MediaQuery.paddingOf(context).copyWith(
+ bottom: _resizeToAvoidBottomInset ? MediaQuery.viewInsetsOf(context).bottom : 0.0,
);
// The minimum viewPadding for interactive elements positioned by the
// Scaffold to keep within safe interactive areas.
- final EdgeInsets minViewPadding = mediaQuery.viewPadding.copyWith(
- bottom: _resizeToAvoidBottomInset && mediaQuery.viewInsets.bottom != 0.0 ? 0.0 : null,
+ final EdgeInsets minViewPadding = MediaQuery.viewPaddingOf(context).copyWith(
+ bottom: _resizeToAvoidBottomInset && MediaQuery.viewInsetsOf(context).bottom != 0.0 ? 0.0 : null,
);
// extendBody locked when keyboard is open
@@ -3082,8 +3052,7 @@
const _BottomSheetSuspendedCurve(
this.startingPoint, {
this.curve = Curves.easeOutCubic,
- }) : assert(startingPoint != null),
- assert(curve != null);
+ });
/// The progress value at which [curve] should begin.
///
@@ -3157,7 +3126,6 @@
@override
void initState() {
super.initState();
- assert(widget.animationController != null);
assert(
widget.animationController.status == AnimationStatus.forward
|| widget.animationController.status == AnimationStatus.completed,
@@ -3178,7 +3146,6 @@
}
void close() {
- assert(widget.animationController != null);
widget.animationController.reverse();
widget.onClosing?.call();
}
@@ -3282,7 +3249,7 @@
required this.hasDrawer,
required this.geometryNotifier,
required super.child,
- }) : assert(hasDrawer != null);
+ });
final bool hasDrawer;
final _ScaffoldGeometryNotifier geometryNotifier;
diff --git a/framework/lib/src/material/scrollbar.dart b/framework/lib/src/material/scrollbar.dart
index a2f0d1f..6342470 100644
--- a/framework/lib/src/material/scrollbar.dart
+++ b/framework/lib/src/material/scrollbar.dart
@@ -427,7 +427,7 @@
..crossAxisMargin = _scrollbarTheme.crossAxisMargin ?? (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin)
..mainAxisMargin = _scrollbarTheme.mainAxisMargin ?? 0.0
..minLength = _scrollbarTheme.minThumbLength ?? _kScrollbarMinLength
- ..padding = MediaQuery.of(context).padding
+ ..padding = MediaQuery.paddingOf(context)
..scrollbarOrientation = widget.scrollbarOrientation
..ignorePointer = !enableGestures;
}
diff --git a/framework/lib/src/material/scrollbar_theme.dart b/framework/lib/src/material/scrollbar_theme.dart
index ca32843..2257055 100644
--- a/framework/lib/src/material/scrollbar_theme.dart
+++ b/framework/lib/src/material/scrollbar_theme.dart
@@ -204,7 +204,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static ScrollbarThemeData lerp(ScrollbarThemeData? a, ScrollbarThemeData? b, double t) {
- assert(t != null);
return ScrollbarThemeData(
thumbVisibility: MaterialStateProperty.lerp<bool?>(a?.thumbVisibility, b?.thumbVisibility, t, _lerpBool),
thickness: MaterialStateProperty.lerp<double?>(a?.thickness, b?.thickness, t, lerpDouble),
diff --git a/framework/lib/src/material/search.dart b/framework/lib/src/material/search.dart
index 6537007..e5f656b 100644
--- a/framework/lib/src/material/search.dart
+++ b/framework/lib/src/material/search.dart
@@ -62,9 +62,6 @@
String? query = '',
bool useRootNavigator = false,
}) {
- assert(delegate != null);
- assert(context != null);
- assert(useRootNavigator != null);
delegate.query = query ?? delegate.query;
delegate._currentBody = _SearchBody.suggestions;
return Navigator.of(context, rootNavigator: useRootNavigator).push(_SearchPageRoute<T>(
@@ -230,16 +227,15 @@
/// * [InputDecorationTheme], which configures the appearance of the search
/// text field.
ThemeData appBarTheme(BuildContext context) {
- assert(context != null);
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
- assert(theme != null);
return theme.copyWith(
appBarTheme: AppBarTheme(
brightness: colorScheme.brightness,
backgroundColor: colorScheme.brightness == Brightness.dark ? Colors.grey[900] : Colors.white,
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
- textTheme: theme.textTheme,
+ titleTextStyle: theme.textTheme.titleLarge,
+ toolbarTextStyle: theme.textTheme.bodyMedium,
),
inputDecorationTheme: searchFieldDecorationTheme ??
InputDecorationTheme(
@@ -261,7 +257,6 @@
///
/// Setting the query string programmatically moves the cursor to the end of the text field.
set query(String value) {
- assert(query != null);
_queryTextController.text = value;
if (_queryTextController.text.isNotEmpty) {
_queryTextController.selection = TextSelection.fromPosition(TextPosition(offset: _queryTextController.text.length));
@@ -389,7 +384,7 @@
class _SearchPageRoute<T> extends PageRoute<T> {
_SearchPageRoute({
required this.delegate,
- }) : assert(delegate != null) {
+ }) {
assert(
delegate._route == null,
'The ${delegate.runtimeType} instance is currently used by another active '
diff --git a/framework/lib/src/material/segmented_button.dart b/framework/lib/src/material/segmented_button.dart
index 86b05c6..23e9706 100644
--- a/framework/lib/src/material/segmented_button.dart
+++ b/framework/lib/src/material/segmented_button.dart
@@ -116,9 +116,7 @@
this.style,
this.showSelectedIcon = true,
this.selectedIcon,
- }) : assert(segments != null),
- assert(segments.length > 0),
- assert(selected != null),
+ }) : assert(segments.length > 0),
assert(selected.length > 0 || emptySelectionAllowed),
assert(selected.length < 2 || multiSelectionEnabled);
@@ -374,7 +372,7 @@
}
}
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
- _SegmentedButtonRenderWidget({
+ const _SegmentedButtonRenderWidget({
super.key,
required this.segments,
required this.enabledBorder,
@@ -693,7 +691,6 @@
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
- assert(position != null);
RenderBox? child = lastChild;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
@@ -720,15 +717,13 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _SegmentedButtonDefaultsM3 extends SegmentedButtonThemeData {
_SegmentedButtonDefaultsM3(this.context);
-
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
-
@override ButtonStyle? get style {
return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge),
@@ -766,7 +761,7 @@
if (states.contains(MaterialState.focused)) {
return _colors.onSurface;
}
- return null;
+ return _colors.onSurface;
}
}),
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
@@ -803,9 +798,9 @@
return BorderSide(color: _colors.outline);
}),
shape: const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()),
+ minimumSize: const MaterialStatePropertyAll<Size?>(Size.fromHeight(40.0)),
);
}
-
@override
Widget? get selectedIcon => const Icon(Icons.check);
}
diff --git a/framework/lib/src/material/segmented_button_theme.dart b/framework/lib/src/material/segmented_button_theme.dart
index 10cc6c6..686455b 100644
--- a/framework/lib/src/material/segmented_button_theme.dart
+++ b/framework/lib/src/material/segmented_button_theme.dart
@@ -107,7 +107,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the visual properties used by descendant [SegmentedButton]
/// widgets.
@@ -158,7 +158,6 @@
/// * [of], which will return [ThemeData.segmentedButtonTheme] if it doesn't
/// find a [SegmentedButtonTheme] ancestor, instead of returning null.
static SegmentedButtonThemeData? maybeOf(BuildContext context) {
- assert(context != null);
return context.dependOnInheritedWidgetOfExactType<SegmentedButtonTheme>()?.data;
}
diff --git a/framework/lib/src/material/selectable_text.dart b/framework/lib/src/material/selectable_text.dart
index 844ddf3..e4d7071 100644
--- a/framework/lib/src/material/selectable_text.dart
+++ b/framework/lib/src/material/selectable_text.dart
@@ -30,7 +30,6 @@
class _TextSpanEditingController extends TextEditingController {
_TextSpanEditingController({required TextSpan textSpan}):
- assert(textSpan != null),
_textSpan = textSpan,
super(text: textSpan.toPlainText(includeSemanticsLabels: false));
@@ -85,7 +84,7 @@
}
@override
- void onSingleTapUp(TapUpDetails details) {
+ void onSingleTapUp(TapDragUpDetails details) {
editableText.hideToolbar();
if (delegate.selectionEnabled) {
switch (Theme.of(_state.context).platform) {
@@ -215,21 +214,12 @@
this.onSelectionChanged,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
- }) : assert(showCursor != null),
- assert(autofocus != null),
- assert(dragStartBehavior != null),
- assert(selectionHeightStyle != null),
- assert(selectionWidthStyle != null),
- assert(maxLines == null || maxLines > 0),
+ }) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(
- data != null,
- 'A non-null String must be provided to a SelectableText widget.',
- ),
textSpan = null;
/// Creates a selectable text widget with a [TextSpan].
@@ -273,19 +263,12 @@
this.onSelectionChanged,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
- }) : assert(showCursor != null),
- assert(autofocus != null),
- assert(dragStartBehavior != null),
- assert(maxLines == null || maxLines > 0),
+ }) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(
- textSpan != null,
- 'A non-null TextSpan must be provided to a SelectableText.rich widget.',
- ),
data = null;
/// The text to display.
@@ -558,8 +541,6 @@
});
}
- TextSelection? _lastSeenTextSelection;
-
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) {
@@ -567,12 +548,8 @@
_showSelectionHandles = willShowSelectionHandles;
});
}
- // TODO(chunhtai): The selection may be the same. We should remove this
- // check once this is fixed https://github.com/flutter/flutter/issues/76349.
- if (widget.onSelectionChanged != null && _lastSeenTextSelection != selection) {
- widget.onSelectionChanged!(selection, cause);
- }
- _lastSeenTextSelection = selection;
+
+ widget.onSelectionChanged?.call(selection, cause);
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
@@ -659,7 +636,7 @@
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
- cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
+ cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
break;
case TargetPlatform.macOS:
@@ -671,7 +648,7 @@
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
- cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
+ cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
break;
case TargetPlatform.android:
@@ -700,7 +677,7 @@
if (effectiveTextStyle == null || effectiveTextStyle.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(widget.style ?? _controller._textSpan.style);
}
- if (MediaQuery.boldTextOverride(context)) {
+ if (MediaQuery.boldTextOf(context)) {
effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold));
}
final Widget child = RepaintBoundary(
diff --git a/framework/lib/src/material/slider.dart b/framework/lib/src/material/slider.dart
index 72b0335..d64d736 100644
--- a/framework/lib/src/material/slider.dart
+++ b/framework/lib/src/material/slider.dart
@@ -159,9 +159,6 @@
this.focusNode,
this.autofocus = false,
}) : _sliderType = _SliderType.material,
- assert(value != null),
- assert(min != null),
- assert(max != null),
assert(min <= max),
assert(value >= min && value <= max,
'Value $value is not between minimum $min and maximum $max'),
@@ -202,9 +199,6 @@
this.focusNode,
this.autofocus = false,
}) : _sliderType = _SliderType.adaptive,
- assert(value != null),
- assert(min != null),
- assert(max != null),
assert(min <= max),
assert(value >= min && value <= max,
'Value $value is not between minimum $min and maximum $max'),
@@ -437,7 +431,7 @@
/// the slider thumb is focused, hovered, or dragged.
///
/// If this property is null, [Slider] will use [activeColor] with
- /// with an opacity of 0.12, If null, [SliderThemeData.overlayColor]
+ /// an opacity of 0.12, If null, [SliderThemeData.overlayColor]
/// will be used.
///
/// If that is also null, If [ThemeData.useMaterial3] is true,
@@ -735,7 +729,6 @@
case _SliderType.adaptive: {
final ThemeData theme = Theme.of(context);
- assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@@ -826,7 +819,7 @@
// This size is used as the max bounds for the painting of the value
// indicators It must be kept in sync with the function with the same name
// in range_slider.dart.
- Size screenSize() => MediaQuery.of(context).size;
+ Size screenSize() => MediaQuery.sizeOf(context);
VoidCallback? handleDidGainAccessibilityFocus;
switch (theme.platform) {
@@ -847,7 +840,7 @@
}
final Map<ShortcutActivator, Intent> shortcutMap;
- switch (MediaQuery.of(context).navigationMode) {
+ switch (MediaQuery.navigationModeOf(context)) {
case NavigationMode.directional:
shortcutMap = _directionalNavShortcutMap;
break;
@@ -861,8 +854,8 @@
// This needs to be updated when accessibility
// guidelines are available on the material specs page
// https://m3.material.io/components/sliders/accessibility.
- ? math.min(MediaQuery.of(context).textScaleFactor, 1.3)
- : MediaQuery.of(context).textScaleFactor;
+ ? math.min(MediaQuery.textScaleFactorOf(context), 1.3)
+ : MediaQuery.textScaleFactorOf(context);
return Semantics(
container: true,
@@ -994,7 +987,7 @@
platform: Theme.of(context).platform,
hasFocus: hasFocus,
hovering: hovering,
- gestureSettings: MediaQuery.of(context).gestureSettings,
+ gestureSettings: MediaQuery.gestureSettingsOf(context),
);
}
@@ -1018,7 +1011,7 @@
..platform = Theme.of(context).platform
..hasFocus = hasFocus
..hovering = hovering
- ..gestureSettings = MediaQuery.of(context).gestureSettings;
+ ..gestureSettings = MediaQuery.gestureSettingsOf(context);
// Ticker provider cannot change since there's a 1:1 relationship between
// the _SliderRenderObjectWidget object and the _SliderState object.
}
@@ -1043,10 +1036,8 @@
required bool hasFocus,
required bool hovering,
required DeviceGestureSettings gestureSettings,
- }) : assert(value != null && value >= 0.0 && value <= 1.0),
+ }) : assert(value >= 0.0 && value <= 1.0),
assert(secondaryTrackValue == null || (secondaryTrackValue >= 0.0 && secondaryTrackValue <= 1.0)),
- assert(state != null),
- assert(textDirection != null),
_platform = platform,
_semanticFormatterCallback = semanticFormatterCallback,
_label = label,
@@ -1138,7 +1129,7 @@
double get value => _value;
double _value;
set value(double newValue) {
- assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
+ assert(newValue >= 0.0 && newValue <= 1.0);
final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
if (convertedValue == _value) {
return;
@@ -1272,7 +1263,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (value == _textDirection) {
return;
}
@@ -1284,7 +1274,6 @@
bool get hasFocus => _hasFocus;
bool _hasFocus;
set hasFocus(bool value) {
- assert(value != null);
if (value == _hasFocus) {
return;
}
@@ -1297,7 +1286,6 @@
bool get hovering => _hovering;
bool _hovering;
set hovering(bool value) {
- assert(value != null);
if (value == _hovering) {
return;
}
@@ -1310,7 +1298,6 @@
bool _hoveringThumb = false;
bool get hoveringThumb => _hoveringThumb;
set hoveringThumb(bool value) {
- assert(value != null);
if (value == _hoveringThumb) {
return;
}
@@ -1528,6 +1515,9 @@
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
+ if (!_state.mounted) {
+ return;
+ }
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && isInteractive) {
// We need to add the drag first so that it has priority.
@@ -1888,7 +1878,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _SliderDefaultsM3 extends SliderThemeData {
_SliderDefaultsM3(this.context)
diff --git a/framework/lib/src/material/slider_theme.dart b/framework/lib/src/material/slider_theme.dart
index 025f665..c01c918 100644
--- a/framework/lib/src/material/slider_theme.dart
+++ b/framework/lib/src/material/slider_theme.dart
@@ -54,8 +54,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// Specifies the color and shape values for descendant slider widgets.
final SliderThemeData data;
@@ -310,10 +309,6 @@
required Color primaryColorLight,
required TextStyle valueIndicatorTextStyle,
}) {
- assert(primaryColor != null);
- assert(primaryColorDark != null);
- assert(primaryColorLight != null);
- assert(valueIndicatorTextStyle != null);
// These are Material Design defaults, and are used to derive
// component Colors (with opacity) from base colors.
@@ -655,9 +650,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
- assert(a != null);
- assert(b != null);
- assert(t != null);
return SliderThemeData(
trackHeight: lerpDouble(a.trackHeight, b.trackHeight, t),
activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t),
@@ -1508,10 +1500,6 @@
bool isEnabled = false,
bool isDiscrete = false,
}) {
- assert(isEnabled != null);
- assert(isDiscrete != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
final double thumbWidth = sliderTheme.thumbShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight!;
@@ -1570,20 +1558,11 @@
bool isDiscrete = false,
bool isEnabled = false,
}) {
- assert(context != null);
- assert(offset != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.thumbShape != null);
- assert(enableAnimation != null);
- assert(textDirection != null);
- assert(thumbCenter != null);
- assert(isEnabled != null);
- assert(isDiscrete != null);
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
@@ -1692,18 +1671,11 @@
bool isEnabled = false,
double additionalActiveTrackHeight = 2,
}) {
- assert(context != null);
- assert(offset != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.thumbShape != null);
- assert(enableAnimation != null);
- assert(textDirection != null);
- assert(thumbCenter != null);
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
@@ -1842,12 +1814,7 @@
bool isEnabled = false,
bool isDiscrete = false,
}) {
- assert(parentBox != null);
- assert(offset != null);
- assert(sliderTheme != null);
assert(sliderTheme.overlayShape != null);
- assert(isEnabled != null);
- assert(isDiscrete != null);
final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight!;
assert(overlayWidth >= 0);
@@ -1874,21 +1841,12 @@
bool isDiscrete = false,
required TextDirection textDirection,
}) {
- assert(context != null);
- assert(offset != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.rangeThumbShape != null);
assert(enableAnimation != null);
- assert(startThumbCenter != null);
- assert(endThumbCenter != null);
- assert(isEnabled != null);
- assert(isDiscrete != null);
- assert(textDirection != null);
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
@@ -1973,13 +1931,8 @@
bool isEnabled = false,
bool isDiscrete = false,
}) {
- assert(parentBox != null);
- assert(offset != null);
- assert(sliderTheme != null);
assert(sliderTheme.overlayShape != null);
assert(sliderTheme.trackHeight != null);
- assert(isEnabled != null);
- assert(isDiscrete != null);
final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight!;
assert(overlayWidth >= 0);
@@ -2007,21 +1960,11 @@
required TextDirection textDirection,
double additionalActiveTrackHeight = 2,
}) {
- assert(context != null);
- assert(offset != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.rangeThumbShape != null);
- assert(enableAnimation != null);
- assert(startThumbCenter != null);
- assert(endThumbCenter != null);
- assert(isEnabled != null);
- assert(isDiscrete != null);
- assert(textDirection != null);
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
return;
@@ -2139,9 +2082,7 @@
required SliderThemeData sliderTheme,
required bool isEnabled,
}) {
- assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null);
- assert(isEnabled != null);
// The tick marks are tiny circles. If no radius is provided, then the
// radius is defaulted to be a fraction of the
// [SliderThemeData.trackHeight]. The fraction is 1/4.
@@ -2159,18 +2100,10 @@
required Offset thumbCenter,
required bool isEnabled,
}) {
- assert(context != null);
- assert(center != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledActiveTickMarkColor != null);
assert(sliderTheme.disabledInactiveTickMarkColor != null);
assert(sliderTheme.activeTickMarkColor != null);
assert(sliderTheme.inactiveTickMarkColor != null);
- assert(enableAnimation != null);
- assert(textDirection != null);
- assert(thumbCenter != null);
- assert(isEnabled != null);
// The paint color of the tick mark depends on its position relative
// to the thumb and the text direction.
Color? begin;
@@ -2237,9 +2170,7 @@
required SliderThemeData sliderTheme,
bool isEnabled = false,
}) {
- assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null);
- assert(isEnabled != null);
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight! / 4);
}
@@ -2255,19 +2186,10 @@
bool isEnabled = false,
required TextDirection textDirection,
}) {
- assert(context != null);
- assert(center != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledActiveTickMarkColor != null);
assert(sliderTheme.disabledInactiveTickMarkColor != null);
assert(sliderTheme.activeTickMarkColor != null);
assert(sliderTheme.inactiveTickMarkColor != null);
- assert(enableAnimation != null);
- assert(startThumbCenter != null);
- assert(endThumbCenter != null);
- assert(isEnabled != null);
- assert(textDirection != null);
final bool isBetweenThumbs;
switch (textDirection) {
@@ -2426,10 +2348,6 @@
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
- assert(context != null);
- assert(center != null);
- assert(enableAnimation != null);
- assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
@@ -2495,7 +2413,7 @@
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
- }) : assert(enabledThumbRadius != null);
+ });
/// The preferred radius of the round thumb shape when the slider is enabled.
///
@@ -2538,13 +2456,8 @@
Thumb? thumb,
bool? isPressed,
}) {
- assert(context != null);
- assert(center != null);
- assert(activationAnimation != null);
- assert(sliderTheme != null);
assert(sliderTheme.showValueIndicator != null);
assert(sliderTheme.overlappingShapeStrokeColor != null);
- assert(enableAnimation != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
@@ -2642,15 +2555,6 @@
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
- assert(context != null);
- assert(center != null);
- assert(activationAnimation != null);
- assert(enableAnimation != null);
- assert(labelPainter != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
- assert(textDirection != null);
- assert(value != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
@@ -2748,8 +2652,7 @@
required TextPainter labelPainter,
required double textScaleFactor,
}) {
- assert(labelPainter != null);
- assert(textScaleFactor != null && textScaleFactor >= 0);
+ assert(textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter, textScaleFactor);
}
@@ -2820,7 +2723,6 @@
TextPainter labelPainter,
double textScaleFactor,
) {
- assert(labelPainter != null);
return Size(
_upperRectangleWidth(labelPainter, 1, textScaleFactor),
labelPainter.height + _labelPadding,
@@ -2976,13 +2878,6 @@
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
- assert(context != null);
- assert(center != null);
- assert(activationAnimation != null);
- assert(enableAnimation != null);
- assert(labelPainter != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(!sizeWithOverflow.isEmpty);
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
@@ -3025,8 +2920,7 @@
required TextPainter labelPainter,
required double textScaleFactor,
}) {
- assert(labelPainter != null);
- assert(textScaleFactor != null && textScaleFactor >= 0);
+ assert(textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter, textScaleFactor);
}
@@ -3065,13 +2959,6 @@
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
- assert(context != null);
- assert(center != null);
- assert(activationAnimation != null);
- assert(enableAnimation != null);
- assert(labelPainter != null);
- assert(parentBox != null);
- assert(sliderTheme != null);
assert(!sizeWithOverflow!.isEmpty);
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
@@ -3135,8 +3022,7 @@
TextPainter labelPainter,
double textScaleFactor,
) {
- assert(labelPainter != null);
- assert(textScaleFactor != null && textScaleFactor >= 0);
+ assert(textScaleFactor >= 0);
final double width = math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2 * textScaleFactor;
return Size(width, _preferredHeight * textScaleFactor);
}
@@ -3556,7 +3442,6 @@
TextPainter labelPainter,
double textScaleFactor,
) {
- assert(labelPainter != null);
final double width = math.max(_minLabelWidth, labelPainter.width) + _labelPadding * 2 * textScaleFactor;
return Size(width, _preferredHeight * textScaleFactor);
}
diff --git a/framework/lib/src/material/snack_bar.dart b/framework/lib/src/material/snack_bar.dart
index 0587d1b..5d45c47 100644
--- a/framework/lib/src/material/snack_bar.dart
+++ b/framework/lib/src/material/snack_bar.dart
@@ -71,8 +71,8 @@
/// A button for a [SnackBar], known as an "action".
///
-/// Snack bar actions are always enabled. If you want to disable a snack bar
-/// action, simply don't include it in the snack bar.
+/// Snack bar actions are always enabled. Instead of disabling a snack bar
+/// action, avoid including it in the snack bar in the first place.
///
/// Snack bar actions can only be pressed once. Subsequent presses are ignored.
///
@@ -90,14 +90,13 @@
this.disabledTextColor,
required this.label,
required this.onPressed,
- }) : assert(label != null),
- assert(onPressed != null);
+ });
/// The button label color. If not provided, defaults to
/// [SnackBarThemeData.actionTextColor].
///
/// If [textColor] is a [MaterialStateColor], then the text color will be
- /// be resolved against the set of [MaterialState]s that the action text
+ /// resolved against the set of [MaterialState]s that the action text
/// is in, thus allowing for different colors for states such as pressed,
/// hovered and others.
final Color? textColor;
@@ -243,13 +242,10 @@
this.dismissDirection = DismissDirection.down,
this.clipBehavior = Clip.hardEdge,
}) : assert(elevation == null || elevation >= 0.0),
- assert(content != null),
assert(
width == null || margin == null,
'Width and margin can not be used together',
- ),
- assert(duration != null),
- assert(clipBehavior != null);
+ );
/// The primary content of the snack bar.
///
@@ -370,7 +366,7 @@
/// inverse surface.
///
/// If [closeIconColor] is a [MaterialStateColor], then the icon color will be
- /// be resolved against the set of [MaterialState]s that the action text
+ /// resolved against the set of [MaterialState]s that the action text
/// is in, thus allowing for different colors for states such as pressed,
/// hovered and others.
final Color? closeIconColor;
@@ -485,7 +481,7 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData mediaQueryData = MediaQuery.of(context);
+ final bool accessibleNavigation = MediaQuery.accessibleNavigationOf(context);
assert(widget.animation != null);
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
@@ -599,7 +595,7 @@
final EdgeInsets margin = widget.margin?.resolve(TextDirection.ltr) ?? snackBarTheme.insetPadding ?? defaults.insetPadding!;
- final double snackBarWidth = widget.width ?? mediaQueryData.size.width - (margin.left + margin.right);
+ final double snackBarWidth = widget.width ?? MediaQuery.sizeOf(context).width - (margin.left + margin.right);
// Action and Icon will overflow to a new line if their width is greater
// than one quarter of the total Snack Bar width.
final bool actionLineOverflow =
@@ -675,7 +671,7 @@
color: backgroundColor,
child: Theme(
data: effectiveTheme,
- child: mediaQueryData.accessibleNavigation || theme.useMaterial3
+ child: accessibleNavigation || theme.useMaterial3
? snackBar
: FadeTransition(
opacity: fadeOutAnimation,
@@ -723,7 +719,7 @@
);
final Widget snackBarTransition;
- if (mediaQueryData.accessibleNavigation) {
+ if (accessibleNavigation) {
snackBarTransition = snackBar;
} else if (isFloatingSnackBar && !theme.useMaterial3) {
snackBarTransition = FadeTransition(
@@ -828,7 +824,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _SnackbarDefaultsM3 extends SnackBarThemeData {
_SnackbarDefaultsM3(this.context);
@@ -883,10 +879,6 @@
@override
bool get showCloseIcon => false;
-
- @override
- Color get closeIconColor => _colors.onInverseSurface;
- }
-
+}
// END GENERATED TOKEN PROPERTIES - Snackbar
diff --git a/framework/lib/src/material/snack_bar_theme.dart b/framework/lib/src/material/snack_bar_theme.dart
index c35c8d6..a4b4c88 100644
--- a/framework/lib/src/material/snack_bar_theme.dart
+++ b/framework/lib/src/material/snack_bar_theme.dart
@@ -67,7 +67,7 @@
}) : assert(elevation == null || elevation >= 0.0),
assert(
width == null ||
- (width != null && identical(behavior, SnackBarBehavior.floating)),
+ (identical(behavior, SnackBarBehavior.floating)),
'Width can only be set if behaviour is SnackBarBehavior.floating');
/// Overrides the default value for [SnackBar.backgroundColor].
///
@@ -169,7 +169,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static SnackBarThemeData lerp(SnackBarThemeData? a, SnackBarThemeData? b, double t) {
- assert(t != null);
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/spell_check_suggestions_toolbar.dart b/framework/lib/src/material/spell_check_suggestions_toolbar.dart
new file mode 100644
index 0000000..44a8a78
--- /dev/null
+++ b/framework/lib/src/material/spell_check_suggestions_toolbar.dart
@@ -0,0 +1,221 @@
+// 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/cupertino.dart';
+import 'package:flute/services.dart' show SuggestionSpan;
+
+import 'adaptive_text_selection_toolbar.dart';
+import 'colors.dart';
+import 'material.dart';
+import 'spell_check_suggestions_toolbar_layout_delegate.dart';
+import 'text_selection_toolbar.dart';
+import 'text_selection_toolbar_text_button.dart';
+
+// The default height of the SpellCheckSuggestionsToolbar, which
+// assumes there are the maximum number of spell check suggestions available, 3.
+// Size eyeballed on Pixel 4 emulator running Android API 31.
+const double _kDefaultToolbarHeight = 193.0;
+
+/// The default spell check suggestions toolbar for Android.
+///
+/// Tries to position itself below the [anchor], but if it doesn't fit, then it
+/// readjusts to fit above bottom view insets.
+class SpellCheckSuggestionsToolbar extends StatelessWidget {
+ /// Constructs a [SpellCheckSuggestionsToolbar].
+ const SpellCheckSuggestionsToolbar({
+ super.key,
+ required this.anchor,
+ required this.buttonItems,
+ });
+
+ /// {@template flutter.material.SpellCheckSuggestionsToolbar.anchor}
+ /// The focal point below which the toolbar attempts to position itself.
+ /// {@endtemplate}
+ final Offset anchor;
+
+ /// The [ContextMenuButtonItem]s that will be turned into the correct button
+ /// widgets and displayed in the spell check suggestions toolbar.
+ ///
+ /// See also:
+ ///
+ /// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
+ /// [ContextMenuButtonItem]s that are used to build the buttons of the
+ /// text selection toolbar.
+ final List<ContextMenuButtonItem> buttonItems;
+
+ /// Padding between the toolbar and the anchor. Eyeballed on Pixel 4 emulator
+ /// running Android API 31.
+ static const double kToolbarContentDistanceBelow = TextSelectionToolbar.kHandleSize - 3.0;
+
+ /// Builds the default Android Material spell check suggestions toolbar.
+ static Widget _spellCheckSuggestionsToolbarBuilder(BuildContext context, Widget child) {
+ return _SpellCheckSuggestionsToolbarContainer(
+ child: child,
+ );
+ }
+
+ /// Builds the button items for the toolbar based on the available
+ /// spell check suggestions.
+ static List<ContextMenuButtonItem>? buildButtonItems(
+ BuildContext context,
+ EditableTextState editableTextState,
+ ) {
+ // Determine if composing region is misspelled.
+ final SuggestionSpan? spanAtCursorIndex =
+ editableTextState.findSuggestionSpanAtCursorIndex(
+ editableTextState.currentTextEditingValue.selection.baseOffset,
+ );
+
+ if (spanAtCursorIndex == null) {
+ return null;
+ }
+
+ final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
+
+ // Build suggestion buttons.
+ for (final String suggestion in spanAtCursorIndex.suggestions) {
+ buttonItems.add(ContextMenuButtonItem(
+ onPressed: () {
+ editableTextState
+ .replaceComposingRegion(
+ SelectionChangedCause.toolbar,
+ suggestion,
+ );
+ },
+ label: suggestion,
+ ));
+ }
+
+ // Build delete button.
+ final ContextMenuButtonItem deleteButton =
+ ContextMenuButtonItem(
+ onPressed: () {
+ editableTextState.replaceComposingRegion(
+ SelectionChangedCause.toolbar,
+ '',
+ );
+ },
+ type: ContextMenuButtonType.delete,
+ );
+ buttonItems.add(deleteButton);
+
+ return buttonItems;
+ }
+
+ /// Determines the Offset that the toolbar will be anchored to.
+ static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
+ return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!;
+ }
+
+ /// Builds the toolbar buttons based on the [buttonItems].
+ List<Widget> _buildToolbarButtons(BuildContext context) {
+ return buttonItems.map((ContextMenuButtonItem buttonItem) {
+ final TextSelectionToolbarTextButton button =
+ TextSelectionToolbarTextButton(
+ padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
+ onPressed: buttonItem.onPressed,
+ alignment: Alignment.centerLeft,
+ child: Text(
+ AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem),
+ style: buttonItem.type == ContextMenuButtonType.delete ? const TextStyle(color: Colors.blue) : null,
+ ),
+ );
+
+ if (buttonItem.type != ContextMenuButtonType.delete) {
+ return button;
+ }
+ return DecoratedBox(
+ decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.grey))),
+ child: button,
+ );
+ }).toList();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // Adjust toolbar height if needed.
+ final double spellCheckSuggestionsToolbarHeight =
+ _kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length));
+ // Incorporate the padding distance between the content and toolbar.
+ final Offset anchorPadded =
+ anchor + const Offset(0.0, kToolbarContentDistanceBelow);
+ final MediaQueryData mediaQueryData = MediaQuery.of(context);
+ final double softKeyboardViewInsetsBottom = mediaQueryData.viewInsets.bottom;
+ final double paddingAbove = mediaQueryData.padding.top + CupertinoTextSelectionToolbar.kToolbarScreenPadding;
+ // Makes up for the Padding.
+ final Offset localAdjustment = Offset(CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove);
+
+ return Padding(
+ padding: EdgeInsets.fromLTRB(
+ CupertinoTextSelectionToolbar.kToolbarScreenPadding,
+ kToolbarContentDistanceBelow,
+ CupertinoTextSelectionToolbar.kToolbarScreenPadding,
+ CupertinoTextSelectionToolbar.kToolbarScreenPadding + softKeyboardViewInsetsBottom,
+ ),
+ child: CustomSingleChildLayout(
+ delegate: SpellCheckSuggestionsToolbarLayoutDelegate(
+ anchor: anchorPadded - localAdjustment,
+ ),
+ child: AnimatedSize(
+ // This duration was eyeballed on a Pixel 2 emulator running Android
+ // API 28 for the Material TextSelectionToolbar.
+ duration: const Duration(milliseconds: 140),
+ child: _spellCheckSuggestionsToolbarBuilder(context, _SpellCheckSuggestsionsToolbarItemsLayout(
+ height: spellCheckSuggestionsToolbarHeight,
+ children: <Widget>[..._buildToolbarButtons(context)],
+ )),
+ ),
+ ),
+ );
+ }
+}
+
+/// The Material-styled toolbar outline for the spell check suggestions
+/// toolbar.
+class _SpellCheckSuggestionsToolbarContainer extends StatelessWidget {
+ const _SpellCheckSuggestionsToolbarContainer({
+ required this.child,
+ });
+
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ // This elevation was eyeballed on a Pixel 4 emulator running Android
+ // API 31 for the SpellCheckSuggestionsToolbar.
+ elevation: 2.0,
+ type: MaterialType.card,
+ child: child,
+ );
+ }
+}
+
+/// Renders the spell check suggestions toolbar items in the correct positions
+/// in the menu.
+class _SpellCheckSuggestsionsToolbarItemsLayout extends StatelessWidget {
+ const _SpellCheckSuggestsionsToolbarItemsLayout({
+ required this.height,
+ required this.children,
+ });
+
+ final double height;
+
+ final List<Widget> children;
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ // This width was eyeballed on a Pixel 4 emulator running Android
+ // API 31 for the SpellCheckSuggestionsToolbar.
+ width: 165,
+ height: height,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: children,
+ ),
+ );
+ }
+}
diff --git a/framework/lib/src/material/spell_check_suggestions_toolbar_layout_delegate.dart b/framework/lib/src/material/spell_check_suggestions_toolbar_layout_delegate.dart
new file mode 100644
index 0000000..f3074e9
--- /dev/null
+++ b/framework/lib/src/material/spell_check_suggestions_toolbar_layout_delegate.dart
@@ -0,0 +1,50 @@
+// 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/rendering.dart';
+import 'package:flute/widgets.dart' show TextSelectionToolbarLayoutDelegate;
+
+/// Positions the toolbar below [anchor] or adjusts it higher to fit above
+/// the bottom view insets, if applicable.
+///
+/// See also:
+///
+/// * [SpellCheckSuggestionsToolbar], which uses this to position itself.
+class SpellCheckSuggestionsToolbarLayoutDelegate extends SingleChildLayoutDelegate {
+ /// Creates an instance of [SpellCheckSuggestionsToolbarLayoutDelegate].
+ SpellCheckSuggestionsToolbarLayoutDelegate({
+ required this.anchor,
+ });
+
+ /// {@macro flutter.material.SpellCheckSuggestionsToolbar.anchor}
+ ///
+ /// Should be provided in local coordinates.
+ final Offset anchor;
+
+ @override
+ BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
+ return constraints.loosen();
+ }
+
+ @override
+ Offset getPositionForChild(Size size, Size childSize) {
+ return Offset(
+ TextSelectionToolbarLayoutDelegate.centerOn(
+ anchor.dx,
+ childSize.width,
+ size.width,
+ ),
+ // Positions child (of childSize) just enough upwards to fit within size
+ // if it otherwise does not fit below the anchor.
+ anchor.dy + childSize.height > size.height
+ ? size.height - childSize.height
+ : anchor.dy,
+ );
+ }
+
+ @override
+ bool shouldRelayout(SpellCheckSuggestionsToolbarLayoutDelegate oldDelegate) {
+ return anchor != oldDelegate.anchor;
+ }
+}
diff --git a/framework/lib/src/material/stepper.dart b/framework/lib/src/material/stepper.dart
old mode 100755
new mode 100644
index e9aa608..4a7f5a2
--- a/framework/lib/src/material/stepper.dart
+++ b/framework/lib/src/material/stepper.dart
@@ -139,9 +139,7 @@
this.state = StepState.indexed,
this.isActive = false,
this.label,
- }) : assert(title != null),
- assert(content != null),
- assert(state != null);
+ });
/// The title of the step that typically describes it.
final Widget title;
@@ -209,10 +207,7 @@
this.controlsBuilder,
this.elevation,
this.margin,
- }) : assert(steps != null),
- assert(type != null),
- assert(currentStep != null),
- assert(0 <= currentStep && currentStep < steps.length);
+ }) : assert(0 <= currentStep && currentStep < steps.length);
/// The steps of the stepper whose titles, subtitles, icons always get shown.
///
@@ -378,7 +373,6 @@
Widget _buildCircleChild(int index, bool oldState) {
final StepState state = oldState ? _oldStates[index]! : widget.steps[index].state;
final bool isDarkActive = _isDark() && widget.steps[index].isActive;
- assert(state != null);
switch (state) {
case StepState.indexed:
case StepState.disabled:
@@ -557,7 +551,6 @@
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
- assert(widget.steps[index].state != null);
switch (widget.steps[index].state) {
case StepState.indexed:
case StepState.editing:
@@ -578,7 +571,6 @@
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
- assert(widget.steps[index].state != null);
switch (widget.steps[index].state) {
case StepState.indexed:
case StepState.editing:
@@ -599,7 +591,6 @@
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
- assert(widget.steps[index].state != null);
switch (widget.steps[index].state) {
case StepState.indexed:
case StepState.editing:
@@ -846,7 +837,6 @@
}
return true;
}());
- assert(widget.type != null);
switch (widget.type) {
case StepperType.vertical:
return _buildVertical();
diff --git a/framework/lib/src/material/switch.dart b/framework/lib/src/material/switch.dart
index 4511dac..0170814 100644
--- a/framework/lib/src/material/switch.dart
+++ b/framework/lib/src/material/switch.dart
@@ -117,7 +117,7 @@
this.onFocusChange,
this.autofocus = false,
}) : _switchType = _SwitchType.material,
- assert(dragStartBehavior != null),
+ applyCupertinoTheme = false,
assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null);
@@ -161,8 +161,8 @@
this.focusNode,
this.onFocusChange,
this.autofocus = false,
- }) : assert(autofocus != null),
- assert(activeThumbImage != null || onActiveThumbImageError == null),
+ this.applyCupertinoTheme,
+ }) : assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
_switchType = _SwitchType.adaptive;
@@ -381,6 +381,9 @@
final _SwitchType _switchType;
+ /// {@macro flutter.cupertino.CupertinoSwitch.applyTheme}
+ final bool? applyCupertinoTheme;
+
/// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
@@ -495,6 +498,7 @@
onChanged: onChanged,
activeColor: activeColor,
trackColor: inactiveTrackColor,
+ applyTheme: applyCupertinoTheme,
),
),
);
@@ -537,7 +541,6 @@
case _SwitchType.adaptive: {
final ThemeData theme = Theme.of(context);
- assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@@ -586,8 +589,7 @@
this.focusNode,
this.onFocusChange,
this.autofocus = false,
- }) : assert(dragStartBehavior != null),
- assert(activeThumbImage != null || onActiveThumbImageError == null),
+ }) : assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null);
final bool value;
@@ -928,7 +930,6 @@
Color get activeIconColor => _activeIconColor!;
Color? _activeIconColor;
set activeIconColor(Color value) {
- assert(value != null);
if (value == _activeIconColor) {
return;
}
@@ -939,7 +940,6 @@
Color get inactiveIconColor => _inactiveIconColor!;
Color? _inactiveIconColor;
set inactiveIconColor(Color value) {
- assert(value != null);
if (value == _inactiveIconColor) {
return;
}
@@ -972,7 +972,6 @@
double get activeThumbRadius => _activeThumbRadius!;
double? _activeThumbRadius;
set activeThumbRadius(double value) {
- assert(value != null);
if (value == _activeThumbRadius) {
return;
}
@@ -983,7 +982,6 @@
double get inactiveThumbRadius => _inactiveThumbRadius!;
double? _inactiveThumbRadius;
set inactiveThumbRadius(double value) {
- assert(value != null);
if (value == _inactiveThumbRadius) {
return;
}
@@ -994,7 +992,6 @@
double get pressedThumbRadius => _pressedThumbRadius!;
double? _pressedThumbRadius;
set pressedThumbRadius(double value) {
- assert(value != null);
if (value == _pressedThumbRadius) {
return;
}
@@ -1015,7 +1012,6 @@
Size get transitionalThumbSize => _transitionalThumbSize!;
Size? _transitionalThumbSize;
set transitionalThumbSize(Size value) {
- assert(value != null);
if (value == _transitionalThumbSize) {
return;
}
@@ -1026,7 +1022,6 @@
double get trackHeight => _trackHeight!;
double? _trackHeight;
set trackHeight(double value) {
- assert(value != null);
if (value == _trackHeight) {
return;
}
@@ -1037,7 +1032,6 @@
double get trackWidth => _trackWidth!;
double? _trackWidth;
set trackWidth(double value) {
- assert(value != null);
if (value == _trackWidth) {
return;
}
@@ -1088,7 +1082,6 @@
Color get activeTrackColor => _activeTrackColor!;
Color? _activeTrackColor;
set activeTrackColor(Color value) {
- assert(value != null);
if (value == _activeTrackColor) {
return;
}
@@ -1109,7 +1102,6 @@
Color get inactiveTrackColor => _inactiveTrackColor!;
Color? _inactiveTrackColor;
set inactiveTrackColor(Color value) {
- assert(value != null);
if (value == _inactiveTrackColor) {
return;
}
@@ -1120,7 +1112,6 @@
ImageConfiguration get configuration => _configuration!;
ImageConfiguration? _configuration;
set configuration(ImageConfiguration value) {
- assert(value != null);
if (value == _configuration) {
return;
}
@@ -1131,7 +1122,6 @@
TextDirection get textDirection => _textDirection!;
TextDirection? _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textDirection == value) {
return;
}
@@ -1142,7 +1132,6 @@
Color get surfaceColor => _surfaceColor!;
Color? _surfaceColor;
set surfaceColor(Color value) {
- assert(value != null);
if (value == _surfaceColor) {
return;
}
@@ -1635,7 +1624,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _SwitchDefaultsM3 extends SwitchThemeData {
_SwitchDefaultsM3(BuildContext context)
diff --git a/framework/lib/src/material/switch_list_tile.dart b/framework/lib/src/material/switch_list_tile.dart
index 43070ab..e017c2d 100644
--- a/framework/lib/src/material/switch_list_tile.dart
+++ b/framework/lib/src/material/switch_list_tile.dart
@@ -57,7 +57,7 @@
///
/// {@tool snippet}
/// ```dart
-/// Container(
+/// ColoredBox(
/// color: Colors.green,
/// child: Material(
/// child: SwitchListTile(
@@ -89,6 +89,13 @@
/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.0.dart **
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This sample demonstrates how [SwitchListTile] positions the switch widget
+/// relative to the text in different configurations.
+///
+/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart **
+/// {@end-tool}
+///
/// ## Semantics in SwitchListTile
///
/// Since the entirety of the SwitchListTile is interactive, it should represent
@@ -113,7 +120,7 @@
/// LinkedLabelRadio, that includes an interactive [RichText] widget that
/// handles tap gestures.
///
-/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart **
+/// ** See code in examples/api/lib/material/switch_list_tile/custom_labeled_switch.0.dart **
/// {@end-tool}
///
/// ## SwitchListTile isn't exactly what I want
@@ -129,7 +136,7 @@
/// Here is an example of a custom LabeledSwitch widget, but you can easily
/// make your own configurable widget.
///
-/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.2.dart **
+/// ** See code in examples/api/lib/material/switch_list_tile/custom_labeled_switch.1.dart **
/// {@end-tool}
///
/// See also:
@@ -180,11 +187,7 @@
this.enableFeedback,
this.hoverColor,
}) : _switchListTileType = _SwitchListTileType.material,
- assert(value != null),
- assert(isThreeLine != null),
- assert(!isThreeLine || subtitle != null),
- assert(selected != null),
- assert(autofocus != null);
+ assert(!isThreeLine || subtitle != null);
/// Creates a Material [ListTile] with an adaptive [Switch], following
/// Material design's
@@ -226,11 +229,7 @@
this.enableFeedback,
this.hoverColor,
}) : _switchListTileType = _SwitchListTileType.adaptive,
- assert(value != null),
- assert(isThreeLine != null),
- assert(!isThreeLine || subtitle != null),
- assert(selected != null),
- assert(autofocus != null);
+ assert(!isThreeLine || subtitle != null);
/// Whether this switch is checked.
///
@@ -266,7 +265,7 @@
/// The color to use when this switch is on.
///
- /// Defaults to accent color of the current [Theme].
+ /// Defaults to [ColorScheme.secondary] of the current [Theme].
final Color? activeColor;
/// The color to use on the track when this switch is on.
diff --git a/framework/lib/src/material/tab_bar_theme.dart b/framework/lib/src/material/tab_bar_theme.dart
index bfbc033..5e9f249 100644
--- a/framework/lib/src/material/tab_bar_theme.dart
+++ b/framework/lib/src/material/tab_bar_theme.dart
@@ -127,9 +127,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static TabBarTheme lerp(TabBarTheme a, TabBarTheme b, double t) {
- assert(a != null);
- assert(b != null);
- assert(t != null);
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 885f1fc..390b5cb 100644
--- a/framework/lib/src/material/tab_controller.dart
+++ b/framework/lib/src/material/tab_controller.dart
@@ -102,8 +102,8 @@
/// 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 != null && length >= 0),
- assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
+ : assert(length >= 0),
+ assert(initialIndex >= 0 && (length == 0 || initialIndex < length)),
_index = initialIndex,
_previousIndex = initialIndex,
_animationDuration = animationDuration ?? kTabScrollDuration,
@@ -177,7 +177,6 @@
final int length;
void _changeIndex(int value, { Duration? duration, Curve? curve }) {
- assert(value != null);
assert(value >= 0 && (value < length || length == 0));
assert(duration != null || curve == null);
assert(_indexIsChangingCount >= 0);
@@ -254,7 +253,6 @@
/// 0.0 and 1.0 implies that the TabBarView has been dragged to the right.
double get offset => _animationController!.value - _index.toDouble();
set offset(double value) {
- assert(value != null);
assert(value >= -1.0 && value <= 1.0);
assert(!indexIsChanging);
if (value == offset) {
@@ -348,8 +346,7 @@
this.initialIndex = 0,
required this.child,
this.animationDuration,
- }) : assert(initialIndex != null),
- assert(length >= 0),
+ }) : assert(length >= 0),
assert(length == 0 || (initialIndex >= 0 && initialIndex < length));
/// The total number of tabs.
diff --git a/framework/lib/src/material/tab_indicator.dart b/framework/lib/src/material/tab_indicator.dart
index d00c474..f9249e2 100644
--- a/framework/lib/src/material/tab_indicator.dart
+++ b/framework/lib/src/material/tab_indicator.dart
@@ -23,8 +23,7 @@
this.borderRadius,
this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
this.insets = EdgeInsets.zero,
- }) : assert(borderSide != null),
- assert(insets != null);
+ });
/// The radius of the indicator's corners.
///
@@ -71,8 +70,6 @@
}
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
- assert(rect != null);
- assert(textDirection != null);
final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
return Rect.fromLTWH(
indicator.left,
@@ -98,15 +95,13 @@
this.decoration,
this.borderRadius,
super.onChanged,
- )
- : assert(decoration != null);
+ );
final UnderlineTabIndicator decoration;
final BorderRadius? borderRadius;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
- assert(configuration != null);
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection textDirection = configuration.textDirection!;
diff --git a/framework/lib/src/material/tabs.dart b/framework/lib/src/material/tabs.dart
index 6541452..732009f 100644
--- a/framework/lib/src/material/tabs.dart
+++ b/framework/lib/src/material/tabs.dart
@@ -239,8 +239,7 @@
required TextDirection super.textDirection,
required super.verticalDirection,
required this.onPerformLayout,
- }) : assert(onPerformLayout != null),
- assert(textDirection != null);
+ });
_LayoutCallback onPerformLayout;
@@ -276,7 +275,7 @@
// upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
// or in response to input.
class _TabLabelBar extends Flex {
- _TabLabelBar({
+ const _TabLabelBar({
super.children,
required this.onPerformLayout,
}) : super(
@@ -332,10 +331,9 @@
required this.tabKeys,
required _IndicatorPainter? old,
required this.indicatorPadding,
+ required this.labelPaddings,
this.dividerColor,
- }) : assert(controller != null),
- assert(indicator != null),
- super(repaint: controller.animation) {
+ }) : super(repaint: controller.animation) {
if (old != null) {
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
}
@@ -347,6 +345,7 @@
final EdgeInsetsGeometry indicatorPadding;
final List<GlobalKey> tabKeys;
final Color? dividerColor;
+ final List<EdgeInsetsGeometry> labelPaddings;
// _currentTabOffsets and _currentTextDirection are set each time TabBar
// layout is completed. These values can be null when TabBar contains no
@@ -402,9 +401,11 @@
if (indicatorSize == TabBarIndicatorSize.label) {
final double tabWidth = tabKeys[tabIndex].currentContext!.size!.width;
- final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
- tabLeft += delta;
- tabRight -= delta;
+ final EdgeInsetsGeometry labelPadding = labelPaddings[tabIndex];
+ final EdgeInsets insets = labelPadding.resolve(_currentTextDirection);
+ final double delta = ((tabRight - tabLeft) - (tabWidth + insets.horizontal)) / 2.0;
+ tabLeft += delta + insets.left;
+ tabRight = tabLeft + tabWidth;
}
final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection);
@@ -655,11 +656,7 @@
this.physics,
this.splashFactory,
this.splashBorderRadius,
- }) : assert(tabs != null),
- assert(isScrollable != null),
- assert(dragStartBehavior != null),
- assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
- assert(indicator != null || (indicatorPadding != null));
+ }) : assert(indicator != null || (indicatorWeight > 0.0));
/// Typically a list of two or more [Tab] widgets.
///
@@ -952,6 +949,7 @@
int? _currentIndex;
late double _tabStripWidth;
late List<GlobalKey> _tabKeys;
+ late List<EdgeInsetsGeometry> _labelPaddings;
bool _debugHasScheduledValidTabsCountCheck = false;
@override
@@ -960,6 +958,7 @@
// If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
// the width of tab widget i. See _IndicatorPainter.indicatorRect().
_tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList();
+ _labelPaddings = List<EdgeInsetsGeometry>.filled(widget.tabs.length, EdgeInsets.zero, growable: true);
}
Decoration _getIndicator() {
@@ -1063,6 +1062,7 @@
tabKeys: _tabKeys,
old: _indicatorPainter,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? defaults.dividerColor : null,
+ labelPaddings: _labelPaddings,
);
}
@@ -1098,8 +1098,10 @@
if (widget.tabs.length > _tabKeys.length) {
final int delta = widget.tabs.length - _tabKeys.length;
_tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
+ _labelPaddings.addAll(List<EdgeInsetsGeometry>.filled(delta, EdgeInsets.zero));
} else if (widget.tabs.length < _tabKeys.length) {
_tabKeys.removeRange(widget.tabs.length, _tabKeys.length);
+ _labelPaddings.removeRange(widget.tabs.length, _tabKeys.length);
}
}
@@ -1274,10 +1276,12 @@
}
}
+ _labelPaddings[index] = adjustedPadding ?? widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding;
+
return Center(
heightFactor: 1.0,
child: Padding(
- padding: adjustedPadding ?? widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding,
+ padding: _labelPaddings[index],
child: KeyedSubtree(
key: _tabKeys[index],
child: widget.tabs[index],
@@ -1423,8 +1427,7 @@
this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge,
- }) : assert(children != null),
- assert(dragStartBehavior != null);
+ });
/// This widget's selection and animation state.
///
@@ -1711,9 +1714,7 @@
required this.borderColor,
required this.size,
this.borderStyle = BorderStyle.solid,
- }) : assert(backgroundColor != null),
- assert(borderColor != null),
- assert(size != null);
+ });
/// The indicator circle's background color.
final Color backgroundColor;
@@ -1763,7 +1764,7 @@
this.color,
this.selectedColor,
this.borderStyle,
- }) : assert(indicatorSize != null && indicatorSize > 0.0);
+ }) : assert(indicatorSize > 0.0);
/// This widget's selection and animation state.
///
@@ -1901,7 +1902,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _TabsDefaultsM3 extends TabBarTheme {
_TabsDefaultsM3(this.context)
diff --git a/framework/lib/src/material/text_button.dart b/framework/lib/src/material/text_button.dart
index ceac94a..cd9966a 100644
--- a/framework/lib/src/material/text_button.dart
+++ b/framework/lib/src/material/text_button.dart
@@ -249,7 +249,7 @@
/// each state and "others" means all other states.
///
/// The `textScaleFactor` is the value of
- /// `MediaQuery.of(context).textScaleFactor` and the names of the
+ /// `MediaQuery.textScaleFactorOf(context)` and the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
/// abbreviated for readability.
///
@@ -382,7 +382,7 @@
const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
}
@@ -481,9 +481,7 @@
super.statesController,
required Widget icon,
required Widget label,
- }) : assert(icon != null),
- assert(label != null),
- super(
+ }) : super(
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
child: _TextButtonWithIconChild(icon: icon, label: label),
@@ -495,7 +493,7 @@
const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 4),
const EdgeInsets.symmetric(horizontal: 4),
- MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+ MediaQuery.textScaleFactorOf(context),
);
return super.defaultStyleOf(context).copyWith(
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
@@ -514,7 +512,7 @@
@override
Widget build(BuildContext context) {
- final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
+ final double scale = MediaQuery.textScaleFactorOf(context);
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
return Row(
mainAxisSize: MainAxisSize.min,
@@ -530,7 +528,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _TextButtonDefaultsM3 extends ButtonStyle {
_TextButtonDefaultsM3(this.context)
diff --git a/framework/lib/src/material/text_button_theme.dart b/framework/lib/src/material/text_button_theme.dart
index fb47f8d..130817e 100644
--- a/framework/lib/src/material/text_button_theme.dart
+++ b/framework/lib/src/material/text_button_theme.dart
@@ -49,7 +49,6 @@
/// Linearly interpolate between two text button themes.
static TextButtonThemeData? lerp(TextButtonThemeData? a, TextButtonThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -98,7 +97,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The configuration of this theme.
final TextButtonThemeData data;
diff --git a/framework/lib/src/material/text_field.dart b/framework/lib/src/material/text_field.dart
index 5f09142..eeffe03 100644
--- a/framework/lib/src/material/text_field.dart
+++ b/framework/lib/src/material/text_field.dart
@@ -20,6 +20,7 @@
import 'material_localizations.dart';
import 'material_state.dart';
import 'selectable_text.dart' show iOSHorizontalOffset;
+import 'spell_check_suggestions_toolbar.dart';
import 'text_selection.dart';
import 'theme.dart';
@@ -64,7 +65,7 @@
}
@override
- void onSingleTapUp(TapUpDetails details) {
+ void onSingleTapUp(TapDragUpDetails details) {
super.onSingleTapUp(details);
_state._requestKeyboard();
_state.widget.onTap?.call();
@@ -306,33 +307,24 @@
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
+ this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
this.contextMenuBuilder = _defaultContextMenuBuilder,
+ this.canRequestFocus = true,
this.spellCheckConfiguration,
this.magnifierConfiguration,
- }) : assert(textAlign != null),
- assert(readOnly != null),
- assert(autofocus != null),
- assert(obscuringCharacter != null && obscuringCharacter.length == 1),
- assert(obscureText != null),
- assert(autocorrect != null),
+ }) : assert(obscuringCharacter.length == 1),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
- assert(enableSuggestions != null),
- assert(scrollPadding != null),
- assert(dragStartBehavior != null),
- assert(selectionHeightStyle != null),
- assert(selectionWidthStyle != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
@@ -346,8 +338,6 @@
!identical(keyboardType, TextInputType.text),
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
),
- assert(clipBehavior != null),
- assert(enableIMEPersonalizedLearning != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
@@ -765,6 +755,9 @@
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
+ /// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
+ final ContentInsertionConfiguration? contentInsertionConfiguration;
+
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, will build a default menu based on the platform.
@@ -774,6 +767,13 @@
/// * [AdaptiveTextSelectionToolbar], which is built by default.
final EditableTextContextMenuBuilder? contextMenuBuilder;
+ /// Determine whether this text field can request the primary focus.
+ ///
+ /// Defaults to true. If false, the text field will not request focus
+ /// when tapped, or when its context menu is displayed. If false it will not
+ /// be possible to move the focus to the text field with tab key.
+ final bool canRequestFocus;
+
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
@@ -800,6 +800,32 @@
decorationStyle: TextDecorationStyle.wavy,
);
+ /// Default builder for the spell check suggestions toolbar in the Material
+ /// style.
+ ///
+ /// See also:
+ /// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
+ // builder configured to show a spell check suggestions toolbar.
+ @visibleForTesting
+ static Widget defaultSpellCheckSuggestionsToolbarBuilder(
+ BuildContext context,
+ EditableTextState editableTextState,
+ ) {
+ final Offset anchor =
+ SpellCheckSuggestionsToolbar.getToolbarAnchor(editableTextState.contextMenuAnchors);
+ final List<ContextMenuButtonItem>? buttonItems =
+ SpellCheckSuggestionsToolbar.buildButtonItems(context, editableTextState);
+
+ if (buttonItems == null){
+ return const SizedBox.shrink();
+ }
+
+ return SpellCheckSuggestionsToolbar(
+ anchor: anchor,
+ buttonItems: buttonItems,
+ );
+ }
+
@override
State<TextField> createState() => _TextFieldState();
@@ -843,6 +869,7 @@
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
+ properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
}
}
@@ -962,15 +989,15 @@
if (widget.controller == null) {
_createLocalController();
}
- _effectiveFocusNode.canRequestFocus = _isEnabled;
+ _effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
_effectiveFocusNode.addListener(_handleFocusChanged);
}
bool get _canRequestFocus {
- final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
+ final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
- return _isEnabled;
+ return widget.canRequestFocus && _isEnabled;
case NavigationMode.directional:
return true;
}
@@ -1192,7 +1219,11 @@
widget.spellCheckConfiguration != const SpellCheckConfiguration.disabled()
? widget.spellCheckConfiguration!.copyWith(
misspelledTextStyle: widget.spellCheckConfiguration!.misspelledTextStyle
- ?? TextField.materialMisspelledTextStyle)
+ ?? TextField.materialMisspelledTextStyle,
+ spellCheckSuggestionsToolbarBuilder:
+ widget.spellCheckConfiguration!.spellCheckSuggestionsToolbarBuilder
+ ?? TextField.defaultSpellCheckSuggestionsToolbarBuilder
+ )
: const SpellCheckConfiguration.disabled();
TextSelectionControls? textSelectionControls = widget.selectionControls;
@@ -1215,7 +1246,7 @@
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
- cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
+ cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
autocorrectionTextRectColor = selectionColor;
break;
@@ -1228,7 +1259,7 @@
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
- cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
+ cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
handleDidGainAccessibilityFocus = () {
// Automatically activate the TextField when it receives accessibility focus.
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
@@ -1335,6 +1366,7 @@
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
+ contentInsertionConfiguration: widget.contentInsertionConfiguration,
contextMenuBuilder: widget.contextMenuBuilder,
spellCheckConfiguration: spellCheckConfiguration,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
@@ -1424,11 +1456,11 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
TextStyle _m3CounterErrorStyle(BuildContext context) =>
- Theme.of(context).textTheme.bodySmall!.copyWith(color:Theme.of(context).colorScheme.error);
+ Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
// END GENERATED TOKEN PROPERTIES - TextField
diff --git a/framework/lib/src/material/text_form_field.dart b/framework/lib/src/material/text_form_field.dart
index fc057c9..f3f5deb 100644
--- a/framework/lib/src/material/text_form_field.dart
+++ b/framework/lib/src/material/text_form_field.dart
@@ -17,7 +17,7 @@
/// This is a convenience widget that wraps a [TextField] widget in a
/// [FormField].
///
-/// A [Form] ancestor is not required. The [Form] simply makes it easier to
+/// A [Form] ancestor is not required. The [Form] allows one to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a `GlobalKey<FormFieldState>` (see [GlobalKey]) to the constructor and use
/// [GlobalKey.currentState] to save or reset the form field.
@@ -154,29 +154,21 @@
bool enableIMEPersonalizedLearning = true,
MouseCursor? mouseCursor,
EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder,
+ TextMagnifierConfiguration? magnifierConfiguration,
}) : assert(initialValue == null || controller == null),
- assert(textAlign != null),
- assert(autofocus != null),
- assert(readOnly != null),
- assert(obscuringCharacter != null && obscuringCharacter.length == 1),
- assert(obscureText != null),
- assert(autocorrect != null),
- assert(enableSuggestions != null),
- assert(scrollPadding != null),
+ assert(obscuringCharacter.length == 1),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
- assert(enableIMEPersonalizedLearning != null),
super(
initialValue: controller != null ? controller.text : (initialValue ?? ''),
enabled: enabled ?? decoration?.enabled ?? true,
@@ -243,6 +235,7 @@
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
mouseCursor: mouseCursor,
contextMenuBuilder: contextMenuBuilder,
+ magnifierConfiguration: magnifierConfiguration,
),
);
},
diff --git a/framework/lib/src/material/text_selection.dart b/framework/lib/src/material/text_selection.dart
index 9bd2f71..44f797d 100644
--- a/framework/lib/src/material/text_selection.dart
+++ b/framework/lib/src/material/text_selection.dart
@@ -55,7 +55,7 @@
ClipboardStatusNotifier? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
- return _TextSelectionControlsToolbar(
+ return _TextSelectionControlsToolbar(
globalEditableRegion: globalEditableRegion,
textLineHeight: textLineHeight,
selectionMidpoint: selectionMidpoint,
diff --git a/framework/lib/src/material/text_selection_theme.dart b/framework/lib/src/material/text_selection_theme.dart
index 134a9d6..592019b 100644
--- a/framework/lib/src/material/text_selection_theme.dart
+++ b/framework/lib/src/material/text_selection_theme.dart
@@ -76,7 +76,6 @@
if (a == null && b == null) {
return null;
}
- assert(t != null);
return TextSelectionThemeData(
cursorColor: Color.lerp(a?.cursorColor, b?.cursorColor, t),
selectionColor: Color.lerp(a?.selectionColor, b?.selectionColor, t),
@@ -147,8 +146,7 @@
super.key,
required this.data,
required Widget child,
- }) : assert(data != null),
- _child = child,
+ }) : _child = child,
// See `get child` override below.
super(child: const _NullWidget());
diff --git a/framework/lib/src/material/text_selection_toolbar.dart b/framework/lib/src/material/text_selection_toolbar.dart
index cc3701c..5e607f3 100644
--- a/framework/lib/src/material/text_selection_toolbar.dart
+++ b/framework/lib/src/material/text_selection_toolbar.dart
@@ -4,9 +4,9 @@
import 'dart:math' as math;
+import 'package:flute/cupertino.dart';
import 'package:flute/foundation.dart' show listEquals;
import 'package:flute/rendering.dart';
-import 'package:flute/widgets.dart';
import 'debug.dart';
import 'icon_button.dart';
@@ -14,15 +14,7 @@
import 'material.dart';
import 'material_localizations.dart';
-// Minimal padding from all edges of the selection toolbar to all edges of the
-// viewport.
-const double _kToolbarScreenPadding = 8.0;
const double _kToolbarHeight = 44.0;
-
-const double _kHandleSize = 22.0;
-
-// Padding between the toolbar and the anchor.
-const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
const double _kToolbarContentDistance = 8.0;
/// A fully-functional Material-style text selection toolbar.
@@ -84,6 +76,17 @@
/// {@endtemplate}
final ToolbarBuilder toolbarBuilder;
+ /// The size of the text selection handles.
+ ///
+ /// See also:
+ ///
+ /// * [SpellCheckSuggestionsToolbar], which references this value to calculate
+ /// the padding between the toolbar and anchor.
+ static const double kHandleSize = 22.0;
+
+ /// Padding between the toolbar and the anchor.
+ static const double kToolbarContentDistanceBelow = kHandleSize - 2.0;
+
// Build the default Android Material text selection menu toolbar.
static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
return _TextSelectionToolbarContainer(
@@ -97,21 +100,22 @@
final Offset anchorAbovePadded =
anchorAbove - const Offset(0.0, _kToolbarContentDistance);
final Offset anchorBelowPadded =
- anchorBelow + const Offset(0.0, _kToolbarContentDistanceBelow);
+ anchorBelow + const Offset(0.0, kToolbarContentDistanceBelow);
- final double paddingAbove = MediaQuery.of(context).padding.top
- + _kToolbarScreenPadding;
+ const double screenPadding = CupertinoTextSelectionToolbar.kToolbarScreenPadding;
+ final double paddingAbove = MediaQuery.paddingOf(context).top
+ + screenPadding;
final double availableHeight = anchorAbovePadded.dy - _kToolbarContentDistance - paddingAbove;
final bool fitsAbove = _kToolbarHeight <= availableHeight;
// Makes up for the Padding above the Stack.
- final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
+ final Offset localAdjustment = Offset(screenPadding, paddingAbove);
return Padding(
padding: EdgeInsets.fromLTRB(
- _kToolbarScreenPadding,
+ screenPadding,
paddingAbove,
- _kToolbarScreenPadding,
- _kToolbarScreenPadding,
+ screenPadding,
+ screenPadding,
),
child: CustomSingleChildLayout(
delegate: TextSelectionToolbarLayoutDelegate(
@@ -237,8 +241,7 @@
required Widget super.child,
required this.overflowOpen,
required this.textDirection,
- }) : assert(child != null),
- assert(overflowOpen != null);
+ });
final bool overflowOpen;
final TextDirection textDirection;
@@ -362,13 +365,11 @@
// Renders the menu items in the correct positions in the menu and its overflow
// submenu based on calculating which item would first overflow.
class _TextSelectionToolbarItemsLayout extends MultiChildRenderObjectWidget {
- _TextSelectionToolbarItemsLayout({
+ const _TextSelectionToolbarItemsLayout({
required this.isAbove,
required this.overflowOpen,
required super.children,
- }) : assert(children != null),
- assert(isAbove != null),
- assert(overflowOpen != null);
+ });
final bool isAbove;
final bool overflowOpen;
@@ -411,9 +412,7 @@
_RenderTextSelectionToolbarItemsLayout({
required bool isAbove,
required bool overflowOpen,
- }) : assert(overflowOpen != null),
- assert(isAbove != null),
- _isAbove = isAbove,
+ }) : _isAbove = isAbove,
_overflowOpen = overflowOpen,
super();
diff --git a/framework/lib/src/material/text_selection_toolbar_text_button.dart b/framework/lib/src/material/text_selection_toolbar_text_button.dart
index ca6fae2..959ca8e 100644
--- a/framework/lib/src/material/text_selection_toolbar_text_button.dart
+++ b/framework/lib/src/material/text_selection_toolbar_text_button.dart
@@ -31,6 +31,7 @@
required this.child,
required this.padding,
this.onPressed,
+ this.alignment,
});
// These values were eyeballed to match the native text selection menu on a
@@ -62,6 +63,15 @@
/// * [ButtonStyle.padding], which is where this padding is applied.
final EdgeInsets padding;
+ /// The alignment of the button's child.
+ ///
+ /// By default, this will be [Alignment.center].
+ ///
+ /// See also:
+ ///
+ /// * [ButtonStyle.alignment], which is where this alignment is applied.
+ final AlignmentGeometry? alignment;
+
/// Returns the standard padding for a button at index out of a total number
/// of buttons.
///
@@ -104,6 +114,22 @@
return _TextSelectionToolbarItemPosition.middle;
}
+ /// Returns a copy of the current [TextSelectionToolbarTextButton] instance
+ /// with specific overrides.
+ TextSelectionToolbarTextButton copyWith({
+ Widget? child,
+ VoidCallback? onPressed,
+ EdgeInsets? padding,
+ AlignmentGeometry? alignment,
+ }) {
+ return TextSelectionToolbarTextButton(
+ onPressed: onPressed ?? this.onPressed,
+ padding: padding ?? this.padding,
+ alignment: alignment ?? this.alignment,
+ child: child ?? this.child,
+ );
+ }
+
@override
Widget build(BuildContext context) {
// TODO(hansmuller): Should be colorScheme.onSurface
@@ -117,6 +143,7 @@
shape: const RoundedRectangleBorder(),
minimumSize: const Size(kMinInteractiveDimension, kMinInteractiveDimension),
padding: padding,
+ alignment: alignment,
),
onPressed: onPressed,
child: child,
diff --git a/framework/lib/src/material/text_theme.dart b/framework/lib/src/material/text_theme.dart
index 351fdf8..42aefb8 100644
--- a/framework/lib/src/material/text_theme.dart
+++ b/framework/lib/src/material/text_theme.dart
@@ -799,7 +799,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static TextTheme lerp(TextTheme? a, TextTheme? b, double t) {
- assert(t != null);
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.dart b/framework/lib/src/material/theme.dart
index b00e085..bb736d5 100644
--- a/framework/lib/src/material/theme.dart
+++ b/framework/lib/src/material/theme.dart
@@ -42,8 +42,7 @@
super.key,
required this.data,
required this.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// Specifies the color and typography values for descendant widgets.
final ThemeData data;
@@ -154,7 +153,7 @@
const _InheritedTheme({
required this.theme,
required super.child,
- }) : assert(theme != null);
+ });
final Theme theme;
@@ -211,8 +210,7 @@
super.duration = kThemeAnimationDuration,
super.onEnd,
required this.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// Specifies the color and typography values for descendant widgets.
final ThemeData data;
diff --git a/framework/lib/src/material/theme_data.dart b/framework/lib/src/material/theme_data.dart
index 4019219..cdaf4c0 100644
--- a/framework/lib/src/material/theme_data.dart
+++ b/framework/lib/src/material/theme_data.dart
@@ -22,6 +22,7 @@
import 'colors.dart';
import 'constants.dart';
import 'data_table_theme.dart';
+import 'date_picker_theme.dart';
import 'dialog_theme.dart';
import 'divider_theme.dart';
import 'drawer_theme.dart';
@@ -70,6 +71,8 @@
/// An interface that defines custom additions to a [ThemeData] object.
///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=8-szcYzFVao}
+///
/// Typically used for custom colors. To use, subclass [ThemeExtension],
/// define a number of fields (e.g. [Color]s), and implement the [copyWith] and
/// [lerp] methods. The latter will ensure smooth transitions of properties when
@@ -346,6 +349,7 @@
CheckboxThemeData? checkboxTheme,
ChipThemeData? chipTheme,
DataTableThemeData? dataTableTheme,
+ DatePickerThemeData? datePickerTheme,
DialogTheme? dialogTheme,
DividerThemeData? dividerTheme,
DrawerThemeData? drawerTheme,
@@ -392,25 +396,6 @@
)
Brightness? accentColorBrightness,
@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.',
- )
- TextTheme? accentTextTheme,
- @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.',
- )
- IconThemeData? accentIconTheme,
- @Deprecated(
- 'No longer used by the framework, please remove any reference to it. '
- 'This feature was deprecated after v2.3.0-0.2.pre.',
- )
- Color? buttonColor,
- @Deprecated(
'This "fix" is now enabled by default. '
'This feature was deprecated after v2.5.0-1.0.pre.',
)
@@ -522,7 +507,6 @@
toggleableActiveColor ??= isDark ? Colors.tealAccent[200]! : (accentColor ?? primarySwatch[600]!);
accentColor ??= isDark ? Colors.tealAccent[200]! : primarySwatch[500]!;
accentColorBrightness ??= estimateBrightnessForColor(accentColor);
- final bool accentIsDark = accentColorBrightness == Brightness.dark;
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;
@@ -552,7 +536,7 @@
// [disabledColor], [highlightColor], and [splashColor].
buttonTheme ??= ButtonThemeData(
colorScheme: colorScheme,
- buttonColor: buttonColor ?? (isDark ? primarySwatch[600]! : Colors.grey[300]!),
+ buttonColor: isDark ? primarySwatch[600]! : Colors.grey[300]!,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
@@ -570,21 +554,17 @@
: Typography.material2014(platform: platform);
TextTheme defaultTextTheme = isDark ? typography.white : typography.black;
TextTheme defaultPrimaryTextTheme = primaryIsDark ? typography.white : typography.black;
- TextTheme defaultAccentTextTheme = accentIsDark ? typography.white : typography.black;
if (fontFamily != null) {
defaultTextTheme = defaultTextTheme.apply(fontFamily: fontFamily);
defaultPrimaryTextTheme = defaultPrimaryTextTheme.apply(fontFamily: fontFamily);
- defaultAccentTextTheme = defaultAccentTextTheme.apply(fontFamily: fontFamily);
}
if (fontFamilyFallback != null) {
defaultTextTheme = defaultTextTheme.apply(fontFamilyFallback: fontFamilyFallback);
defaultPrimaryTextTheme = defaultPrimaryTextTheme.apply(fontFamilyFallback: fontFamilyFallback);
- defaultAccentTextTheme = defaultAccentTextTheme.apply(fontFamilyFallback: fontFamilyFallback);
}
if (package != null) {
defaultTextTheme = defaultTextTheme.apply(package: package);
defaultPrimaryTextTheme = defaultPrimaryTextTheme.apply(package: package);
- defaultAccentTextTheme = defaultAccentTextTheme.apply(package: package);
}
textTheme = defaultTextTheme.merge(textTheme);
primaryTextTheme = defaultPrimaryTextTheme.merge(primaryTextTheme);
@@ -603,6 +583,7 @@
checkboxTheme ??= const CheckboxThemeData();
chipTheme ??= const ChipThemeData();
dataTableTheme ??= const DataTableThemeData();
+ datePickerTheme ??= const DatePickerThemeData();
dialogTheme ??= const DialogTheme();
dividerTheme ??= const DividerThemeData();
drawerTheme ??= const DrawerThemeData();
@@ -635,9 +616,6 @@
tooltipTheme ??= const TooltipThemeData();
// DEPRECATED (newest deprecations at the bottom)
- accentTextTheme = defaultAccentTextTheme.merge(accentTextTheme);
- accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
- buttonColor ??= isDark ? primarySwatch[600]! : Colors.grey[300]!;
fixTextFieldOutlineLabel ??= true;
primaryColorBrightness = estimatedPrimaryColorBrightness;
errorColor ??= Colors.red[700]!;
@@ -701,6 +679,7 @@
checkboxTheme: checkboxTheme,
chipTheme: chipTheme,
dataTableTheme: dataTableTheme,
+ datePickerTheme: datePickerTheme,
dialogTheme: dialogTheme,
dividerTheme: dividerTheme,
drawerTheme: drawerTheme,
@@ -734,9 +713,6 @@
// DEPRECATED (newest deprecations at the bottom)
accentColor: accentColor,
accentColorBrightness: accentColorBrightness,
- accentTextTheme: accentTextTheme,
- accentIconTheme: accentIconTheme,
- buttonColor: buttonColor,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
primaryColorBrightness: primaryColorBrightness,
androidOverscrollIndicator: androidOverscrollIndicator,
@@ -815,6 +791,7 @@
required this.checkboxTheme,
required this.chipTheme,
required this.dataTableTheme,
+ required this.datePickerTheme,
required this.dialogTheme,
required this.dividerTheme,
required this.drawerTheme,
@@ -861,25 +838,6 @@
)
Brightness? accentColorBrightness,
@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.',
- )
- TextTheme? accentTextTheme,
- @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.',
- )
- IconThemeData? accentIconTheme,
- @Deprecated(
- 'No longer used by the framework, please remove any reference to it. '
- 'This feature was deprecated after v2.3.0-0.2.pre.',
- )
- Color? buttonColor,
- @Deprecated(
'This "fix" is now enabled by default. '
'This feature was deprecated after v2.5.0-1.0.pre.',
)
@@ -926,9 +884,6 @@
// should not be `required`, use getter pattern to avoid breakages.
_accentColor = accentColor,
_accentColorBrightness = accentColorBrightness,
- _accentTextTheme = accentTextTheme,
- _accentIconTheme = accentIconTheme,
- _buttonColor = buttonColor,
_fixTextFieldOutlineLabel = fixTextFieldOutlineLabel,
_primaryColorBrightness = primaryColorBrightness,
_toggleableActiveColor = toggleableActiveColor,
@@ -936,93 +891,10 @@
_errorColor = errorColor,
_backgroundColor = backgroundColor,
_bottomAppBarColor = bottomAppBarColor,
- // GENERAL CONFIGURATION
- assert(applyElevationOverlayColor != null),
- assert(extensions != null),
- assert(inputDecorationTheme != null),
- assert(materialTapTargetSize != null),
- assert(pageTransitionsTheme != null),
- assert(platform != null),
- assert(scrollbarTheme != null),
- assert(splashFactory != null),
- assert(useMaterial3 != null),
- assert(visualDensity != null),
- // COLOR
- assert(canvasColor != null),
- assert(cardColor != null),
- assert(colorScheme != null),
- assert(dialogBackgroundColor != null),
- assert(disabledColor != null),
- assert(dividerColor != null),
- assert(focusColor != null),
- assert(highlightColor != null),
- assert(hintColor != null),
- assert(hoverColor != null),
- assert(indicatorColor != null),
- assert(primaryColor != null),
- assert(primaryColorDark != null),
- assert(primaryColorLight != null),
- assert(scaffoldBackgroundColor != null),
- assert(secondaryHeaderColor != null),
- assert(shadowColor != null),
- assert(splashColor != null),
assert(toggleableActiveColor != null),
- assert(unselectedWidgetColor != null),
- // TYPOGRAPHY & ICONOGRAPHY
- assert(iconTheme != null),
- assert(primaryIconTheme != null),
- assert(primaryTextTheme != null),
- assert(textTheme != null),
- assert(typography != null),
- // COMPONENT THEMES
- assert(appBarTheme != null),
- assert(badgeTheme != null),
- assert(bannerTheme != null),
- assert(bottomAppBarTheme != null),
- assert(bottomNavigationBarTheme != null),
- assert(bottomSheetTheme != null),
- assert(buttonBarTheme != null),
- assert(buttonTheme != null),
- assert(cardTheme != null),
- assert(checkboxTheme != null),
- assert(chipTheme != null),
- assert(dataTableTheme != null),
- assert(dialogTheme != null),
- assert(dividerTheme != null),
- assert(drawerTheme != null),
- assert(dropdownMenuTheme != null),
- assert(elevatedButtonTheme != null),
- assert(expansionTileTheme != null),
- assert(filledButtonTheme != null),
- assert(floatingActionButtonTheme != null),
- assert(iconButtonTheme != null),
- assert(listTileTheme != null),
- assert(menuBarTheme != null),
- assert(menuButtonTheme != null),
- assert(menuTheme != null),
- assert(navigationBarTheme != null),
- assert(navigationDrawerTheme != null),
- assert(navigationRailTheme != null),
- assert(outlinedButtonTheme != null),
- assert(popupMenuTheme != null),
- assert(progressIndicatorTheme != null),
- assert(radioTheme != null),
- assert(segmentedButtonTheme != null),
- assert(sliderTheme != null),
- assert(snackBarTheme != null),
- assert(switchTheme != null),
- assert(tabBarTheme != null),
- assert(textButtonTheme != null),
- assert(textSelectionTheme != null),
- assert(timePickerTheme != null),
- assert(toggleButtonsTheme != null),
- assert(tooltipTheme != null),
// DEPRECATED (newest deprecations at the bottom)
assert(accentColor != null),
assert(accentColorBrightness != null),
- assert(accentTextTheme != null),
- assert(accentIconTheme != null),
- assert(buttonColor != null),
assert(fixTextFieldOutlineLabel != null),
assert(primaryColorBrightness != null),
assert(errorColor != null),
@@ -1237,7 +1109,7 @@
/// to determine the current platform for the purpose of emulating the
/// platform behavior (e.g. scrolling or haptic effects). Widgets and render
/// objects at lower layers that try to emulate the underlying platform
- /// platform can depend on [defaultTargetPlatform] directly, or may require
+ /// can depend on [defaultTargetPlatform] directly, or may require
/// that the target platform be provided as an argument. The
/// [dart:io.Platform] object should only be used directly when it's critical
/// to actually know the current platform, without any overrides possible (for
@@ -1316,35 +1188,37 @@
/// * Typography: [Typography] (see table above)
///
/// ### Components
- /// * Badges: [Badge]
+ /// * Badges: [Badge] (*new*)
/// * Bottom app bar: [BottomAppBar]
/// * Bottom sheets: [BottomSheet]
/// * Buttons
- /// - Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton]
+ /// - Common buttons: [ElevatedButton], [FilledButton] (*new*), FilledButton.tonal] (*new*), [OutlinedButton], [TextButton]
/// - FAB: [FloatingActionButton], [FloatingActionButton.extended]
/// - Icon buttons: [IconButton]
- /// - Segmented buttons: [SegmentedButton]
+ /// - Segmented buttons: [SegmentedButton] (*new*, replacing [ToggleButtons])
/// * Cards: [Card]
- /// * Checkbox: [Checkbox]
+ /// * Checkbox: [Checkbox], [CheckboxListTile]
/// * Chips:
/// - [ActionChip] (used for Assist and Suggestion chips),
/// - [FilterChip], [ChoiceChip] (used for single selection filter chips),
/// - [InputChip]
- /// * Dialogs: [Dialog], [AlertDialog]
- /// * Divider: [Divider]
+ /// * Date pickers: [showDatePicker], [showDateRangePicker], [DatePickerDialog], [DateRangePickerDialog], [InputDatePickerFormField]
+ /// * Dialogs: [AlertDialog], [Dialog.fullscreen] (*new*)
+ /// * Divider: [Divider], [VerticalDivider]
/// * Lists: [ListTile]
- /// * Menus: [MenuBar], [DropdownMenu]
- /// * Navigation bar: [NavigationBar] (new, replacing [BottomNavigationBar])
- /// * Navigation drawer: [NavigationDrawer]
+ /// * Menus: [MenuAnchor] (*new*), [DropdownMenu] (*new*), [MenuBar] (*new*)
+ /// * Navigation bar: [NavigationBar] (*new*, replacing [BottomNavigationBar])
+ /// * Navigation drawer: [NavigationDrawer] (*new*, replacing [Drawer])
/// * Navigation rail: [NavigationRail]
/// * Progress indicators: [CircularProgressIndicator], [LinearProgressIndicator]
- /// * Radio button: [Radio]
+ /// * Radio button: [Radio], [RadioListTile]
/// * Snack bar: [SnackBar]
- /// * Slider: [Slider]
- /// * Switch: [Switch]
+ /// * Slider: [Slider], [RangeSlider]
+ /// * Switch: [Switch], [SwitchListTile]
/// * Tabs: [TabBar]
/// * TextFields: [TextField] together with its [InputDecoration]
- /// * Top app bar: [AppBar]
+ /// * Time pickers: [showTimePicker], [TimePickerDialog]
+ /// * Top app bar: [AppBar], [SliverAppBar], [SliverAppBar.medium] (*new*), [SliverAppBar.large] (*new*)
///
/// In addition, this flag enables features introduced in Android 12.
/// * Stretch overscroll: [MaterialScrollBehavior]
@@ -1405,14 +1279,13 @@
/// The color of [Material] when it is used as a [Card].
final Color cardColor;
- /// A set of twelve colors that can be used to configure the
- /// color properties of most components.
+ /// {@macro flutter.material.color_scheme.ColorScheme}
///
- /// This property was added much later than the theme's set of highly
- /// specific colors, like [cardColor], [buttonColor], [canvasColor] etc.
- /// New components can be defined exclusively in terms of [colorScheme].
- /// Existing components will gradually migrate to it, to the extent
- /// that is possible without significant backwards compatibility breaks.
+ /// This property was added much later than the theme's set of highly specific
+ /// colors, like [cardColor], [canvasColor] etc. New components can be defined
+ /// exclusively in terms of [colorScheme]. Existing components will gradually
+ /// migrate to it, to the extent that is possible without significant
+ /// backwards compatibility breaks.
final ColorScheme colorScheme;
/// The background color of [Dialog] elements.
@@ -1566,6 +1439,10 @@
/// widgets.
final DataTableThemeData dataTableTheme;
+ /// A theme for customizing the appearance and layout of [DatePickerDialog]
+ /// widgets.
+ final DatePickerThemeData datePickerTheme;
+
/// A theme for customizing the shape of a dialog.
final DialogTheme dialogTheme;
@@ -1684,7 +1561,7 @@
///
/// Apps should migrate uses of this property to the theme's [colorScheme]
/// [ColorScheme.secondary] color. In cases where a color is needed that
- /// that contrasts well with the secondary color [ColorScheme.onSecondary]
+ /// contrasts well with the secondary color [ColorScheme.onSecondary]
/// can be used.
@Deprecated(
'Use colorScheme.secondary instead. '
@@ -1712,54 +1589,6 @@
Brightness get accentColorBrightness => _accentColorBrightness!;
final Brightness? _accentColorBrightness;
- /// Obsolete property that was originally used when a [TextTheme]
- /// that contrasted well with the [accentColor] was needed.
- ///
- /// The material library no longer uses this property and most uses
- /// of [accentColor] have been replaced with
- /// the theme's [colorScheme] [ColorScheme.secondary].
- /// You can configure the color of a [textTheme] [TextStyle] so that it
- /// contrasts well with the [ColorScheme.secondary] like this:
- ///
- /// ```dart
- /// final ThemeData theme = Theme.of(context);
- /// final TextStyle style = theme.textTheme.displayLarge!.copyWith(
- /// color: theme.colorScheme.onSecondary,
- /// );
- /// // ...use style...
- /// ```
- @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.',
- )
- TextTheme get accentTextTheme => _accentTextTheme!;
- final TextTheme? _accentTextTheme;
-
- /// Obsolete property that was originally used when an [IconTheme]
- /// that contrasted well with the [accentColor] was needed.
- ///
- /// The material library no longer uses this property and most uses
- /// of [accentColor] have been replaced with
- /// the theme's [colorScheme] [ColorScheme.secondary].
- @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.',
- )
- IconThemeData get accentIconTheme => _accentIconTheme!;
- final IconThemeData? _accentIconTheme;
-
- /// The default fill color of the [Material].
- @Deprecated(
- 'No longer used by the framework, please remove any reference to it. '
- 'This feature was deprecated after v2.3.0-0.2.pre.',
- )
- Color get buttonColor => _buttonColor!;
- final Color? _buttonColor;
-
/// 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].
@@ -1912,6 +1741,7 @@
CheckboxThemeData? checkboxTheme,
ChipThemeData? chipTheme,
DataTableThemeData? dataTableTheme,
+ DatePickerThemeData? datePickerTheme,
DialogTheme? dialogTheme,
DividerThemeData? dividerTheme,
DrawerThemeData? drawerTheme,
@@ -1958,25 +1788,6 @@
)
Brightness? accentColorBrightness,
@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.',
- )
- TextTheme? accentTextTheme,
- @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.',
- )
- IconThemeData? accentIconTheme,
- @Deprecated(
- 'No longer used by the framework, please remove any reference to it. '
- 'This feature was deprecated after v2.3.0-0.2.pre.',
- )
- Color? buttonColor,
- @Deprecated(
'This "fix" is now enabled by default. '
'This feature was deprecated after v2.5.0-1.0.pre.',
)
@@ -2077,6 +1888,7 @@
checkboxTheme: checkboxTheme ?? this.checkboxTheme,
chipTheme: chipTheme ?? this.chipTheme,
dataTableTheme: dataTableTheme ?? this.dataTableTheme,
+ datePickerTheme: datePickerTheme ?? this.datePickerTheme,
dialogTheme: dialogTheme ?? this.dialogTheme,
dividerTheme: dividerTheme ?? this.dividerTheme,
drawerTheme: drawerTheme ?? this.drawerTheme,
@@ -2110,9 +1922,6 @@
// DEPRECATED (newest deprecations at the bottom)
accentColor: accentColor ?? _accentColor,
accentColorBrightness: accentColorBrightness ?? _accentColorBrightness,
- accentTextTheme: accentTextTheme ?? _accentTextTheme,
- accentIconTheme: accentIconTheme ?? _accentIconTheme,
- buttonColor: buttonColor ?? _buttonColor,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? _fixTextFieldOutlineLabel,
primaryColorBrightness: primaryColorBrightness ?? _primaryColorBrightness,
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
@@ -2147,15 +1956,12 @@
//
// There are only two hard things in Computer Science: cache invalidation
// and naming things. -- Phil Karlton
- assert(baseTheme != null);
- assert(localTextGeometry != null);
return _localizedThemeDataCache.putIfAbsent(
_IdentityThemeDataCacheKey(baseTheme, localTextGeometry),
() {
return baseTheme.copyWith(
primaryTextTheme: localTextGeometry.merge(baseTheme.primaryTextTheme),
- accentTextTheme: localTextGeometry.merge(baseTheme.accentTextTheme),
textTheme: localTextGeometry.merge(baseTheme.textTheme),
);
},
@@ -2217,9 +2023,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static ThemeData lerp(ThemeData a, ThemeData b, double t) {
- assert(a != null);
- assert(b != null);
- assert(t != null);
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.
@@ -2277,6 +2080,7 @@
checkboxTheme: CheckboxThemeData.lerp(a.checkboxTheme, b.checkboxTheme, t),
chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t)!,
dataTableTheme: DataTableThemeData.lerp(a.dataTableTheme, b.dataTableTheme, t),
+ datePickerTheme: DatePickerThemeData.lerp(a.datePickerTheme, b.datePickerTheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
drawerTheme: DrawerThemeData.lerp(a.drawerTheme, b.drawerTheme, t)!,
@@ -2310,9 +2114,6 @@
// DEPRECATED (newest deprecations at the bottom)
accentColor: Color.lerp(a.accentColor, b.accentColor, t),
accentColorBrightness: t < 0.5 ? a.accentColorBrightness : b.accentColorBrightness,
- accentTextTheme: TextTheme.lerp(a.accentTextTheme, b.accentTextTheme, t),
- accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
- buttonColor: Color.lerp(a.buttonColor, b.buttonColor, 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,
@@ -2386,6 +2187,7 @@
other.checkboxTheme == checkboxTheme &&
other.chipTheme == chipTheme &&
other.dataTableTheme == dataTableTheme &&
+ other.datePickerTheme == datePickerTheme &&
other.dialogTheme == dialogTheme &&
other.dividerTheme == dividerTheme &&
other.drawerTheme == drawerTheme &&
@@ -2419,9 +2221,6 @@
// DEPRECATED (newest deprecations at the bottom)
other.accentColor == accentColor &&
other.accentColorBrightness == accentColorBrightness &&
- other.accentTextTheme == accentTextTheme &&
- other.accentIconTheme == accentIconTheme &&
- other.buttonColor == buttonColor &&
other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel &&
other.primaryColorBrightness == primaryColorBrightness &&
other.androidOverscrollIndicator == androidOverscrollIndicator &&
@@ -2492,6 +2291,7 @@
checkboxTheme,
chipTheme,
dataTableTheme,
+ datePickerTheme,
dialogTheme,
dividerTheme,
drawerTheme,
@@ -2525,9 +2325,6 @@
// DEPRECATED (newest deprecations at the bottom)
accentColor,
accentColorBrightness,
- accentTextTheme,
- accentIconTheme,
- buttonColor,
fixTextFieldOutlineLabel,
primaryColorBrightness,
androidOverscrollIndicator,
@@ -2600,6 +2397,7 @@
properties.add(DiagnosticsProperty<CheckboxThemeData>('checkboxTheme', checkboxTheme, defaultValue: defaultData.checkboxTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DataTableThemeData>('dataTableTheme', dataTableTheme, defaultValue: defaultData.dataTableTheme, level: DiagnosticLevel.debug));
+ properties.add(DiagnosticsProperty<DatePickerThemeData>('datePickerTheme', datePickerTheme, defaultValue: defaultData.datePickerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DividerThemeData>('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DrawerThemeData>('drawerTheme', drawerTheme, defaultValue: defaultData.drawerTheme, level: DiagnosticLevel.debug));
@@ -2633,9 +2431,6 @@
// DEPRECATED (newest deprecations at the bottom)
properties.add(ColorProperty('accentColor', accentColor, defaultValue: defaultData.accentColor, level: DiagnosticLevel.debug));
properties.add(EnumProperty<Brightness>('accentColorBrightness', accentColorBrightness, defaultValue: defaultData.accentColorBrightness, level: DiagnosticLevel.debug));
- properties.add(DiagnosticsProperty<TextTheme>('accentTextTheme', accentTextTheme, level: DiagnosticLevel.debug));
- properties.add(DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme, level: DiagnosticLevel.debug));
- properties.add(ColorProperty('buttonColor', buttonColor, defaultValue: defaultData.buttonColor, 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));
@@ -2655,7 +2450,7 @@
/// styles.
///
/// In the most basic case, [ThemeData]'s `cupertinoOverrideTheme` is null and
-/// and descendant Cupertino widgets' styling is derived from the Material theme.
+/// descendant Cupertino widgets' styling is derived from the Material theme.
///
/// To override individual parts of the Material-derived Cupertino styling,
/// `cupertinoOverrideTheme`'s construction parameters can be used.
@@ -2691,9 +2486,7 @@
MaterialBasedCupertinoThemeData._(
this._materialTheme,
this._cupertinoOverrideTheme,
- ) : assert(_materialTheme != null),
- assert(_cupertinoOverrideTheme != null),
- // Pass all values to the superclass so Material-agnostic properties
+ ) : // Pass all values to the superclass so Material-agnostic properties
// like barBackgroundColor can still behave like a normal
// CupertinoThemeData.
super.raw(
@@ -2703,6 +2496,7 @@
_cupertinoOverrideTheme.textTheme,
_cupertinoOverrideTheme.barBackgroundColor,
_cupertinoOverrideTheme.scaffoldBackgroundColor,
+ _cupertinoOverrideTheme.applyThemeToAll,
);
final ThemeData _materialTheme;
@@ -2738,6 +2532,7 @@
CupertinoTextThemeData? textTheme,
Color? barBackgroundColor,
Color? scaffoldBackgroundColor,
+ bool? applyThemeToAll,
}) {
return MaterialBasedCupertinoThemeData._(
_materialTheme,
@@ -2748,6 +2543,7 @@
textTheme: textTheme,
barBackgroundColor: barBackgroundColor,
scaffoldBackgroundColor: scaffoldBackgroundColor,
+ applyThemeToAll: applyThemeToAll,
),
);
}
@@ -2791,7 +2587,7 @@
/// The key that was inserted before all other keys is evicted first, i.e. the
/// one inserted least recently.
class _FifoCache<K, V> {
- _FifoCache(this._maximumSize) : assert(_maximumSize != null && _maximumSize > 0);
+ _FifoCache(this._maximumSize) : assert(_maximumSize > 0);
/// In Dart the map literal uses a linked hash-map implementation, whose keys
/// are stored such that [Map.keys] returns them in the order they were
@@ -2810,7 +2606,6 @@
/// The arguments must not be null.
V putIfAbsent(K key, V Function() loader) {
assert(key != null);
- assert(loader != null);
final V? result = _cache[key];
if (result != null) {
return result;
@@ -2867,9 +2662,7 @@
const VisualDensity({
this.horizontal = 0.0,
this.vertical = 0.0,
- }) : assert(horizontal != null),
- assert(vertical != null),
- assert(vertical <= maximumDensity),
+ }) : assert(vertical <= maximumDensity),
assert(vertical <= maximumDensity),
assert(vertical >= minimumDensity),
assert(horizontal <= maximumDensity),
@@ -3000,7 +2793,7 @@
/// The resulting minWidth and minHeight values are clamped to not exceed the
/// maxWidth and maxHeight values, respectively.
BoxConstraints effectiveConstraints(BoxConstraints constraints) {
- assert(constraints != null && constraints.debugAssertIsValid());
+ assert(constraints.debugAssertIsValid());
return constraints.copyWith(
minWidth: clampDouble(constraints.minWidth + baseSizeAdjustment.dx, 0.0, constraints.maxWidth),
minHeight: clampDouble(constraints.minHeight + baseSizeAdjustment.dy, 0.0, constraints.maxHeight),
@@ -3040,7 +2833,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
const ColorScheme _colorSchemeLightM3 = ColorScheme(
brightness: Brightness.light,
diff --git a/framework/lib/src/material/time.dart b/framework/lib/src/material/time.dart
index 2e6351c..02e64ac 100644
--- a/framework/lib/src/material/time.dart
+++ b/framework/lib/src/material/time.dart
@@ -108,7 +108,7 @@
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return localizations.formatTimeOfDay(
this,
- alwaysUse24HourFormat: MediaQuery.of(context).alwaysUse24HourFormat,
+ alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context),
);
}
diff --git a/framework/lib/src/material/time_picker.dart b/framework/lib/src/material/time_picker.dart
index 6651366..e7f8c90 100644
--- a/framework/lib/src/material/time_picker.dart
+++ b/framework/lib/src/material/time_picker.dart
@@ -4,14 +4,15 @@
import 'dart:async';
import 'dart:math' as math;
+import 'package:engine/ui.dart' hide TextStyle;
import 'package:flute/rendering.dart';
import 'package:flute/services.dart';
import 'package:flute/widgets.dart';
+import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
-import 'constants.dart';
import 'curves.dart';
import 'debug.dart';
import 'dialog.dart';
@@ -40,30 +41,22 @@
const double _kTwoPi = 2 * math.pi;
const Duration _kVibrateCommitDelay = Duration(milliseconds: 100);
-enum _TimePickerMode { hour, minute }
-
-const double _kTimePickerHeaderLandscapeWidth = 264.0;
-const double _kTimePickerHeaderControlHeight = 80.0;
-
-const double _kTimePickerWidthPortrait = 328.0;
-const double _kTimePickerWidthLandscape = 528.0;
-
-const double _kTimePickerHeightInput = 226.0;
-const double _kTimePickerHeightPortrait = 496.0;
-const double _kTimePickerHeightLandscape = 316.0;
-
-const double _kTimePickerHeightPortraitCollapsed = 484.0;
-const double _kTimePickerHeightLandscapeCollapsed = 304.0;
-
-const BorderRadius _kDefaultBorderRadius = BorderRadius.all(Radius.circular(4.0));
-const ShapeBorder _kDefaultShape = RoundedRectangleBorder(borderRadius: _kDefaultBorderRadius);
+const double _kTimePickerHeaderLandscapeWidth = 216;
+const double _kTimePickerInnerDialOffset = 28;
+const double _kTimePickerDialMinRadius = 50;
+const double _kTimePickerDialPadding = 28;
/// Interactive input mode of the time picker dialog.
///
-/// In [TimePickerEntryMode.dial] mode, a clock dial is displayed and
-/// the user taps or drags the time they wish to select. In
-/// TimePickerEntryMode.input] mode, [TextField]s are displayed and the user
-/// types in the time they wish to select.
+/// In [TimePickerEntryMode.dial] mode, a clock dial is displayed and the user
+/// taps or drags the time they wish to select. In TimePickerEntryMode.input]
+/// mode, [TextField]s are displayed and the user types in the time they wish to
+/// select.
+///
+/// See also:
+///
+/// * [showTimePicker], a function that shows a [TimePickerDialog] and returns
+/// the selected time as a [Future].
enum TimePickerEntryMode {
/// User picks time from a clock dial.
///
@@ -86,186 +79,217 @@
inputOnly
}
-/// Provides properties for rendering time picker header fragments.
-@immutable
-class _TimePickerFragmentContext {
- const _TimePickerFragmentContext({
- required this.selectedTime,
- required this.mode,
- required this.onTimeChange,
- required this.onModeChange,
- required this.onHourDoubleTapped,
- required this.onMinuteDoubleTapped,
- required this.use24HourDials,
- }) : assert(selectedTime != null),
- assert(mode != null),
- assert(onTimeChange != null),
- assert(onModeChange != null),
- assert(use24HourDials != null);
+// Whether the dial-mode time picker is currently selecting the hour or the
+// minute.
+enum _HourMinuteMode { hour, minute }
- final TimeOfDay selectedTime;
- final _TimePickerMode mode;
- final ValueChanged<TimeOfDay> onTimeChange;
- final ValueChanged<_TimePickerMode> onModeChange;
- final GestureTapCallback onHourDoubleTapped;
- final GestureTapCallback onMinuteDoubleTapped;
- final bool use24HourDials;
+// Aspects of _TimePickerModel that can be depended upon.
+enum _TimePickerAspect {
+ use24HourFormat,
+ useMaterial3,
+ entryMode,
+ hourMinuteMode,
+ onHourMinuteModeChanged,
+ onHourDoubleTapped,
+ onMinuteDoubleTapped,
+ hourDialType,
+ selectedTime,
+ onSelectedTimeChanged,
+ orientation,
+ theme,
+ defaultTheme,
}
-class _TimePickerHeader extends StatelessWidget {
- const _TimePickerHeader({
- required this.selectedTime,
- required this.mode,
- required this.orientation,
- required this.onModeChanged,
- required this.onChanged,
+class _TimePickerModel extends InheritedModel<_TimePickerAspect> {
+ const _TimePickerModel({
+ required this.entryMode,
+ required this.hourMinuteMode,
+ required this.onHourMinuteModeChanged,
required this.onHourDoubleTapped,
required this.onMinuteDoubleTapped,
- required this.use24HourDials,
- required this.helpText,
- }) : assert(selectedTime != null),
- assert(mode != null),
- assert(orientation != null),
- assert(use24HourDials != null);
+ required this.selectedTime,
+ required this.onSelectedTimeChanged,
+ required this.use24HourFormat,
+ required this.useMaterial3,
+ required this.hourDialType,
+ required this.orientation,
+ required this.theme,
+ required this.defaultTheme,
+ required super.child,
+ });
- final TimeOfDay selectedTime;
- final _TimePickerMode mode;
- final Orientation orientation;
- final ValueChanged<_TimePickerMode> onModeChanged;
- final ValueChanged<TimeOfDay> onChanged;
+ final TimePickerEntryMode entryMode;
+ final _HourMinuteMode hourMinuteMode;
+ final ValueChanged<_HourMinuteMode> onHourMinuteModeChanged;
final GestureTapCallback onHourDoubleTapped;
final GestureTapCallback onMinuteDoubleTapped;
- final bool use24HourDials;
- final String? helpText;
+ final TimeOfDay selectedTime;
+ final ValueChanged<TimeOfDay> onSelectedTimeChanged;
+ final bool use24HourFormat;
+ final bool useMaterial3;
+ final _HourDialType hourDialType;
+ final Orientation orientation;
+ final TimePickerThemeData theme;
+ final _TimePickerDefaults defaultTheme;
- void _handleChangeMode(_TimePickerMode value) {
- if (value != mode) {
- onModeChanged(value);
+ static _TimePickerModel of(BuildContext context, [_TimePickerAspect? aspect]) => InheritedModel.inheritFrom<_TimePickerModel>(context, aspect: aspect)!;
+ static TimePickerEntryMode entryModeOf(BuildContext context) => of(context, _TimePickerAspect.entryMode).entryMode;
+ static _HourMinuteMode hourMinuteModeOf(BuildContext context) => of(context, _TimePickerAspect.hourMinuteMode).hourMinuteMode;
+ static TimeOfDay selectedTimeOf(BuildContext context) => of(context, _TimePickerAspect.selectedTime).selectedTime;
+ static bool use24HourFormatOf(BuildContext context) => of(context, _TimePickerAspect.use24HourFormat).use24HourFormat;
+ static bool useMaterial3Of(BuildContext context) => of(context, _TimePickerAspect.useMaterial3).useMaterial3;
+ static _HourDialType hourDialTypeOf(BuildContext context) => of(context, _TimePickerAspect.hourDialType).hourDialType;
+ static Orientation orientationOf(BuildContext context) => of(context, _TimePickerAspect.orientation).orientation;
+ static TimePickerThemeData themeOf(BuildContext context) => of(context, _TimePickerAspect.theme).theme;
+ static _TimePickerDefaults defaultThemeOf(BuildContext context) => of(context, _TimePickerAspect.defaultTheme).defaultTheme;
+
+ static void setSelectedTime(BuildContext context, TimeOfDay value) => of(context, _TimePickerAspect.onSelectedTimeChanged).onSelectedTimeChanged(value);
+ static void setHourMinuteMode(BuildContext context, _HourMinuteMode value) => of(context, _TimePickerAspect.onHourMinuteModeChanged).onHourMinuteModeChanged(value);
+
+ @override
+ bool updateShouldNotifyDependent(_TimePickerModel oldWidget, Set<_TimePickerAspect> dependencies) {
+ if (use24HourFormat != oldWidget.use24HourFormat && dependencies.contains(_TimePickerAspect.use24HourFormat)) {
+ return true;
}
+ if (useMaterial3 != oldWidget.useMaterial3 && dependencies.contains(_TimePickerAspect.useMaterial3)) {
+ return true;
+ }
+ if (entryMode != oldWidget.entryMode && dependencies.contains(_TimePickerAspect.entryMode)) {
+ return true;
+ }
+ if (hourMinuteMode != oldWidget.hourMinuteMode && dependencies.contains(_TimePickerAspect.hourMinuteMode)) {
+ return true;
+ }
+ if (onHourMinuteModeChanged != oldWidget.onHourMinuteModeChanged && dependencies.contains(_TimePickerAspect.onHourMinuteModeChanged)) {
+ return true;
+ }
+ if (onHourMinuteModeChanged != oldWidget.onHourDoubleTapped && dependencies.contains(_TimePickerAspect.onHourDoubleTapped)) {
+ return true;
+ }
+ if (onHourMinuteModeChanged != oldWidget.onMinuteDoubleTapped && dependencies.contains(_TimePickerAspect.onMinuteDoubleTapped)) {
+ return true;
+ }
+ if (hourDialType != oldWidget.hourDialType && dependencies.contains(_TimePickerAspect.hourDialType)) {
+ return true;
+ }
+ if (selectedTime != oldWidget.selectedTime && dependencies.contains(_TimePickerAspect.selectedTime)) {
+ return true;
+ }
+ if (onSelectedTimeChanged != oldWidget.onSelectedTimeChanged && dependencies.contains(_TimePickerAspect.onSelectedTimeChanged)) {
+ return true;
+ }
+ if (orientation != oldWidget.orientation && dependencies.contains(_TimePickerAspect.orientation)) {
+ return true;
+ }
+ if (theme != oldWidget.theme && dependencies.contains(_TimePickerAspect.theme)) {
+ return true;
+ }
+ if (defaultTheme != oldWidget.defaultTheme && dependencies.contains(_TimePickerAspect.defaultTheme)) {
+ return true;
+ }
+ return false;
}
@override
+ bool updateShouldNotify(_TimePickerModel oldWidget) {
+ return use24HourFormat != oldWidget.use24HourFormat
+ || useMaterial3 != oldWidget.useMaterial3
+ || entryMode != oldWidget.entryMode
+ || hourMinuteMode != oldWidget.hourMinuteMode
+ || onHourMinuteModeChanged != oldWidget.onHourMinuteModeChanged
+ || onHourDoubleTapped != oldWidget.onHourDoubleTapped
+ || onMinuteDoubleTapped != oldWidget.onMinuteDoubleTapped
+ || hourDialType != oldWidget.hourDialType
+ || selectedTime != oldWidget.selectedTime
+ || onSelectedTimeChanged != oldWidget.onSelectedTimeChanged
+ || orientation != oldWidget.orientation
+ || theme != oldWidget.theme
+ || defaultTheme != oldWidget.defaultTheme;
+ }
+}
+
+class _TimePickerHeader extends StatelessWidget {
+ const _TimePickerHeader({ required this.helpText });
+
+ final String helpText;
+
+ @override
Widget build(BuildContext context) {
- assert(debugCheckHasMediaQuery(context));
- final ThemeData themeData = Theme.of(context);
final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat(
- alwaysUse24HourFormat: MediaQuery.of(context).alwaysUse24HourFormat,
- );
- final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final String timePickerDialHelpText = themeData.useMaterial3
- ? localizations.timePickerDialHelpText
- : localizations.timePickerDialHelpText.toUpperCase();
-
- final _TimePickerFragmentContext fragmentContext = _TimePickerFragmentContext(
- selectedTime: selectedTime,
- mode: mode,
- onTimeChange: onChanged,
- onModeChange: _handleChangeMode,
- onHourDoubleTapped: onHourDoubleTapped,
- onMinuteDoubleTapped: onMinuteDoubleTapped,
- use24HourDials: use24HourDials,
+ alwaysUse24HourFormat: _TimePickerModel.use24HourFormatOf(context),
);
- final EdgeInsets padding;
- double? width;
- final Widget controls;
-
- switch (orientation) {
+ final _HourDialType hourDialType = _TimePickerModel.hourDialTypeOf(context);
+ switch (_TimePickerModel.orientationOf(context)) {
case Orientation.portrait:
- // Keep width null because in portrait we don't cap the width.
- padding = const EdgeInsets.symmetric(horizontal: 24.0);
- controls = Column(
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- const SizedBox(height: 16.0),
- SizedBox(
- height: kMinInteractiveDimension * 2,
- child: Row(
- children: <Widget>[
- if (!use24HourDials && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
- _DayPeriodControl(
- selectedTime: selectedTime,
- orientation: orientation,
- onChanged: onChanged,
- ),
- const SizedBox(width: 12.0),
+ Padding(padding: EdgeInsetsDirectional.only(bottom: _TimePickerModel.useMaterial3Of(context) ? 20 : 24),
+ child: Text(
+ helpText,
+ style: _TimePickerModel.themeOf(context).helpTextStyle ?? _TimePickerModel.defaultThemeOf(context).helpTextStyle,
+ ),
+ ),
+ Row(
+ children: <Widget>[
+ if (hourDialType == _HourDialType.twelveHour && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm)
+ const _DayPeriodControl(),
+ Expanded(
+ child: Row(
+ // Hour/minutes should not change positions in RTL locales.
+ textDirection: TextDirection.ltr,
+ children: <Widget>[
+ const Expanded(child: _HourControl()),
+ _StringFragment(timeOfDayFormat: timeOfDayFormat),
+ const Expanded(child: _MinuteControl()),
+ ],
+ ),
+ ),
+ if (hourDialType == _HourDialType.twelveHour && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm)
+ ...<Widget>[
+ const SizedBox(width: 12),
+ const _DayPeriodControl(),
],
- Expanded(
+ ],
+ ),
+ ],
+ );
+ case Orientation.landscape:
+ return SizedBox(
+ width: _kTimePickerHeaderLandscapeWidth,
+ child: Stack(
+ children: <Widget>[
+ Text(
+ helpText,
+ style: _TimePickerModel.themeOf(context).helpTextStyle ?? _TimePickerModel.defaultThemeOf(context).helpTextStyle,
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ if (hourDialType == _HourDialType.twelveHour && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm)
+ const _DayPeriodControl(),
+ Padding(
+ padding: EdgeInsets.only(bottom: hourDialType == _HourDialType.twelveHour ? 12 : 0),
child: Row(
// Hour/minutes should not change positions in RTL locales.
textDirection: TextDirection.ltr,
children: <Widget>[
- Expanded(child: _HourControl(fragmentContext: fragmentContext)),
+ const Expanded(child: _HourControl()),
_StringFragment(timeOfDayFormat: timeOfDayFormat),
- Expanded(child: _MinuteControl(fragmentContext: fragmentContext)),
+ const Expanded(child: _MinuteControl()),
],
),
),
- if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
- const SizedBox(width: 12.0),
- _DayPeriodControl(
- selectedTime: selectedTime,
- orientation: orientation,
- onChanged: onChanged,
- ),
- ],
+ if (hourDialType == _HourDialType.twelveHour && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm)
+ const _DayPeriodControl(),
],
),
- ),
- ],
- );
- break;
- case Orientation.landscape:
- width = _kTimePickerHeaderLandscapeWidth;
- padding = const EdgeInsets.symmetric(horizontal: 24.0);
- controls = Expanded(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- if (!use24HourDials && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm)
- _DayPeriodControl(
- selectedTime: selectedTime,
- orientation: orientation,
- onChanged: onChanged,
- ),
- SizedBox(
- height: kMinInteractiveDimension * 2,
- child: Row(
- // Hour/minutes should not change positions in RTL locales.
- textDirection: TextDirection.ltr,
- children: <Widget>[
- Expanded(child: _HourControl(fragmentContext: fragmentContext)),
- _StringFragment(timeOfDayFormat: timeOfDayFormat),
- Expanded(child: _MinuteControl(fragmentContext: fragmentContext)),
- ],
- ),
- ),
- if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm)
- _DayPeriodControl(
- selectedTime: selectedTime,
- orientation: orientation,
- onChanged: onChanged,
- ),
],
),
);
- break;
}
-
- return Container(
- width: width,
- padding: padding,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: <Widget>[
- const SizedBox(height: 16.0),
- Text(
- helpText ?? timePickerDialHelpText,
- style: TimePickerTheme.of(context).helpTextStyle ?? themeData.textTheme.labelSmall,
- ),
- controls,
- ],
- ),
- );
}
}
@@ -275,9 +299,7 @@
required this.onTap,
required this.onDoubleTap,
required this.isSelected,
- }) : assert(text != null),
- assert(onTap != null),
- assert(isSelected != null);
+ });
final String text;
final GestureTapCallback onTap;
@@ -286,27 +308,37 @@
@override
Widget build(BuildContext context) {
- final ThemeData themeData = Theme.of(context);
- final TimePickerThemeData timePickerTheme = TimePickerTheme.of(context);
- final bool isDark = themeData.colorScheme.brightness == Brightness.dark;
- final Color textColor = timePickerTheme.hourMinuteTextColor
- ?? MaterialStateColor.resolveWith((Set<MaterialState> states) {
- return states.contains(MaterialState.selected)
- ? themeData.colorScheme.primary
- : themeData.colorScheme.onSurface;
- });
- final Color backgroundColor = timePickerTheme.hourMinuteColor
- ?? MaterialStateColor.resolveWith((Set<MaterialState> states) {
- return states.contains(MaterialState.selected)
- ? themeData.colorScheme.primary.withOpacity(isDark ? 0.24 : 0.12)
- : themeData.colorScheme.onSurface.withOpacity(0.12);
- });
- final TextStyle style = timePickerTheme.hourMinuteTextStyle ?? themeData.textTheme.displayMedium!;
- final ShapeBorder shape = timePickerTheme.hourMinuteShape ?? _kDefaultShape;
+ final TimePickerThemeData timePickerTheme = _TimePickerModel.themeOf(context);
+ final _TimePickerDefaults defaultTheme = _TimePickerModel.defaultThemeOf(context);
+ final Color backgroundColor = timePickerTheme.hourMinuteColor ?? defaultTheme.hourMinuteColor;
+ final ShapeBorder shape = timePickerTheme.hourMinuteShape ?? defaultTheme.hourMinuteShape;
- final Set<MaterialState> states = isSelected ? <MaterialState>{MaterialState.selected} : <MaterialState>{};
+ final Set<MaterialState> states = <MaterialState>{
+ if (isSelected) MaterialState.selected,
+ };
+ final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(
+ _TimePickerModel.themeOf(context).hourMinuteTextColor ?? _TimePickerModel.defaultThemeOf(context).hourMinuteTextColor,
+ states,
+ );
+ final TextStyle effectiveStyle = MaterialStateProperty.resolveAs<TextStyle>(
+ timePickerTheme.hourMinuteTextStyle ?? defaultTheme.hourMinuteTextStyle,
+ states,
+ ).copyWith(color: effectiveTextColor);
+
+ final double height;
+ switch (_TimePickerModel.entryModeOf(context)) {
+ case TimePickerEntryMode.dial:
+ case TimePickerEntryMode.dialOnly:
+ height = defaultTheme.hourMinuteSize.height;
+ break;
+ case TimePickerEntryMode.input:
+ case TimePickerEntryMode.inputOnly:
+ height = defaultTheme.hourMinuteInputSize.height;
+ break;
+ }
+
return SizedBox(
- height: _kTimePickerHeaderControlHeight,
+ height: height,
child: Material(
color: MaterialStateProperty.resolveAs(backgroundColor, states),
clipBehavior: Clip.antiAlias,
@@ -317,8 +349,8 @@
child: Center(
child: Text(
text,
- style: style.copyWith(color: MaterialStateProperty.resolveAs(textColor, states)),
- textScaleFactor: 1.0,
+ style: effectiveStyle,
+ textScaleFactor: 1,
),
),
),
@@ -326,39 +358,39 @@
);
}
}
+
/// Displays the hour fragment.
///
-/// When tapped changes time picker dial mode to [_TimePickerMode.hour].
+/// When tapped changes time picker dial mode to [_HourMinuteMode.hour].
class _HourControl extends StatelessWidget {
- const _HourControl({
- required this.fragmentContext,
- });
-
- final _TimePickerFragmentContext fragmentContext;
+ const _HourControl();
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final bool alwaysUse24HourFormat = MediaQuery.of(context).alwaysUse24HourFormat;
+ final bool alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(context);
+ final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final String formattedHour = localizations.formatHour(
- fragmentContext.selectedTime,
- alwaysUse24HourFormat: alwaysUse24HourFormat,
+ selectedTime,
+ alwaysUse24HourFormat: _TimePickerModel.use24HourFormatOf(context),
);
TimeOfDay hoursFromSelected(int hoursToAdd) {
- if (fragmentContext.use24HourDials) {
- final int selectedHour = fragmentContext.selectedTime.hour;
- return fragmentContext.selectedTime.replacing(
- hour: (selectedHour + hoursToAdd) % TimeOfDay.hoursPerDay,
- );
- } else {
- // Cycle 1 through 12 without changing day period.
- final int periodOffset = fragmentContext.selectedTime.periodOffset;
- final int hours = fragmentContext.selectedTime.hourOfPeriod;
- return fragmentContext.selectedTime.replacing(
- hour: periodOffset + (hours + hoursToAdd) % TimeOfDay.hoursPerPeriod,
- );
+ switch (_TimePickerModel.hourDialTypeOf(context)) {
+ case _HourDialType.twentyFourHour:
+ case _HourDialType.twentyFourHourDoubleRing:
+ final int selectedHour = selectedTime.hour;
+ return selectedTime.replacing(
+ hour: (selectedHour + hoursToAdd) % TimeOfDay.hoursPerDay,
+ );
+ case _HourDialType.twelveHour:
+ // Cycle 1 through 12 without changing day period.
+ final int periodOffset = selectedTime.periodOffset;
+ final int hours = selectedTime.hourOfPeriod;
+ return selectedTime.replacing(
+ hour: periodOffset + (hours + hoursToAdd) % TimeOfDay.hoursPerPeriod,
+ );
}
}
@@ -378,27 +410,27 @@
excludeSemantics: true,
increasedValue: formattedNextHour,
onIncrease: () {
- fragmentContext.onTimeChange(nextHour);
+ _TimePickerModel.setSelectedTime(context, nextHour);
},
decreasedValue: formattedPreviousHour,
onDecrease: () {
- fragmentContext.onTimeChange(previousHour);
+ _TimePickerModel.setSelectedTime(context, previousHour);
},
child: _HourMinuteControl(
- isSelected: fragmentContext.mode == _TimePickerMode.hour,
+ isSelected: _TimePickerModel.hourMinuteModeOf(context) == _HourMinuteMode.hour,
text: formattedHour,
- onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context)!,
- onDoubleTap: fragmentContext.onHourDoubleTapped,
+ onTap: Feedback.wrapForTap(() => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.hour), context)!,
+ onDoubleTap: _TimePickerModel.of(context, _TimePickerAspect.onHourDoubleTapped).onHourDoubleTapped,
),
);
}
}
/// A passive fragment showing a string value.
+///
+/// Used to display the appropriate separator between the input fields.
class _StringFragment extends StatelessWidget {
- const _StringFragment({
- required this.timeOfDayFormat,
- });
+ const _StringFragment({ required this.timeOfDayFormat });
final TimeOfDayFormat timeOfDayFormat;
@@ -420,18 +452,39 @@
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TimePickerThemeData timePickerTheme = TimePickerTheme.of(context);
- final TextStyle hourMinuteStyle = timePickerTheme.hourMinuteTextStyle ?? theme.textTheme.displayMedium!;
- final Color textColor = timePickerTheme.hourMinuteTextColor ?? theme.colorScheme.onSurface;
+ final _TimePickerDefaults defaultTheme = theme.useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ final Set<MaterialState> states = <MaterialState>{};
+
+ final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(
+ timePickerTheme.hourMinuteTextColor ?? defaultTheme.hourMinuteTextColor,
+ states,
+ );
+ final TextStyle effectiveStyle = MaterialStateProperty.resolveAs<TextStyle>(
+ timePickerTheme.hourMinuteTextStyle ?? defaultTheme.hourMinuteTextStyle,
+ states,
+ ).copyWith(color: effectiveTextColor);
+
+ final double height;
+ switch (_TimePickerModel.entryModeOf(context)) {
+ case TimePickerEntryMode.dial:
+ case TimePickerEntryMode.dialOnly:
+ height = defaultTheme.hourMinuteSize.height;
+ break;
+ case TimePickerEntryMode.input:
+ case TimePickerEntryMode.inputOnly:
+ height = defaultTheme.hourMinuteInputSize.height;
+ break;
+ }
return ExcludeSemantics(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 6.0),
- child: Center(
- child: Text(
- _stringFragmentValue(timeOfDayFormat),
- style: hourMinuteStyle.apply(color: MaterialStateProperty.resolveAs(textColor, <MaterialState>{})),
- textScaleFactor: 1.0,
- ),
+ child: SizedBox(
+ width: timeOfDayFormat == TimeOfDayFormat.frenchCanadian ? 36 : 24,
+ height: height,
+ child: Text(
+ _stringFragmentValue(timeOfDayFormat),
+ style: effectiveStyle,
+ textScaleFactor: 1,
+ textAlign: TextAlign.center,
),
),
);
@@ -440,24 +493,21 @@
/// Displays the minute fragment.
///
-/// When tapped changes time picker dial mode to [_TimePickerMode.minute].
+/// When tapped changes time picker dial mode to [_HourMinuteMode.minute].
class _MinuteControl extends StatelessWidget {
- const _MinuteControl({
- required this.fragmentContext,
- });
-
- final _TimePickerFragmentContext fragmentContext;
+ const _MinuteControl();
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final String formattedMinute = localizations.formatMinute(fragmentContext.selectedTime);
- final TimeOfDay nextMinute = fragmentContext.selectedTime.replacing(
- minute: (fragmentContext.selectedTime.minute + 1) % TimeOfDay.minutesPerHour,
+ final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
+ final String formattedMinute = localizations.formatMinute(selectedTime);
+ final TimeOfDay nextMinute = selectedTime.replacing(
+ minute: (selectedTime.minute + 1) % TimeOfDay.minutesPerHour,
);
final String formattedNextMinute = localizations.formatMinute(nextMinute);
- final TimeOfDay previousMinute = fragmentContext.selectedTime.replacing(
- minute: (fragmentContext.selectedTime.minute - 1) % TimeOfDay.minutesPerHour,
+ final TimeOfDay previousMinute = selectedTime.replacing(
+ minute: (selectedTime.minute - 1) % TimeOfDay.minutesPerHour,
);
final String formattedPreviousMinute = localizations.formatMinute(previousMinute);
@@ -466,43 +516,42 @@
value: '${localizations.timePickerMinuteModeAnnouncement} $formattedMinute',
increasedValue: formattedNextMinute,
onIncrease: () {
- fragmentContext.onTimeChange(nextMinute);
+ _TimePickerModel.setSelectedTime(context, nextMinute);
},
decreasedValue: formattedPreviousMinute,
onDecrease: () {
- fragmentContext.onTimeChange(previousMinute);
+ _TimePickerModel.setSelectedTime(context, previousMinute);
},
child: _HourMinuteControl(
- isSelected: fragmentContext.mode == _TimePickerMode.minute,
+ isSelected: _TimePickerModel.hourMinuteModeOf(context) == _HourMinuteMode.minute,
text: formattedMinute,
- onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.minute), context)!,
- onDoubleTap: fragmentContext.onMinuteDoubleTapped,
+ onTap: Feedback.wrapForTap(() => _TimePickerModel.setHourMinuteMode(context, _HourMinuteMode.minute), context)!,
+ onDoubleTap: _TimePickerModel.of(context, _TimePickerAspect.onMinuteDoubleTapped).onMinuteDoubleTapped,
),
);
}
}
-
/// Displays the am/pm fragment and provides controls for switching between am
/// and pm.
class _DayPeriodControl extends StatelessWidget {
- const _DayPeriodControl({
- required this.selectedTime,
- required this.onChanged,
- required this.orientation,
- });
+ const _DayPeriodControl({ this.onPeriodChanged });
- final TimeOfDay selectedTime;
- final Orientation orientation;
- final ValueChanged<TimeOfDay> onChanged;
+ final ValueChanged<TimeOfDay>? onPeriodChanged;
- void _togglePeriod() {
+ void _togglePeriod(BuildContext context) {
+ final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
final int newHour = (selectedTime.hour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
final TimeOfDay newTime = selectedTime.replacing(hour: newHour);
- onChanged(newTime);
+ if (onPeriodChanged != null) {
+ onPeriodChanged!.call(newTime);
+ } else {
+ _TimePickerModel.setSelectedTime(context, newTime);
+ }
}
void _setAm(BuildContext context) {
+ final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
if (selectedTime.period == DayPeriod.am) {
return;
}
@@ -517,10 +566,11 @@
case TargetPlatform.macOS:
break;
}
- _togglePeriod();
+ _togglePeriod(context);
}
void _setPm(BuildContext context) {
+ final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
if (selectedTime.period == DayPeriod.pm) {
return;
}
@@ -535,113 +585,72 @@
case TargetPlatform.macOS:
break;
}
- _togglePeriod();
+ _togglePeriod(context);
}
@override
Widget build(BuildContext context) {
final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(context);
- final ColorScheme colorScheme = Theme.of(context).colorScheme;
- final TimePickerThemeData timePickerTheme = TimePickerTheme.of(context);
- final bool isDark = colorScheme.brightness == Brightness.dark;
- final Color textColor = timePickerTheme.dayPeriodTextColor
- ?? MaterialStateColor.resolveWith((Set<MaterialState> states) {
- return states.contains(MaterialState.selected)
- ? colorScheme.primary
- : colorScheme.onSurface.withOpacity(0.60);
- });
- final Color backgroundColor = timePickerTheme.dayPeriodColor
- ?? MaterialStateColor.resolveWith((Set<MaterialState> states) {
- // The unselected day period should match the overall picker dialog
- // color. Making it transparent enables that without being redundant
- // and allows the optional elevation overlay for dark mode to be
- // visible.
- return states.contains(MaterialState.selected)
- ? colorScheme.primary.withOpacity(isDark ? 0.24 : 0.12)
- : Colors.transparent;
- });
+ final TimePickerThemeData timePickerTheme = _TimePickerModel.themeOf(context);
+ final _TimePickerDefaults defaultTheme = _TimePickerModel.defaultThemeOf(context);
+ final TimeOfDay selectedTime = _TimePickerModel.selectedTimeOf(context);
final bool amSelected = selectedTime.period == DayPeriod.am;
- final Set<MaterialState> amStates = amSelected ? <MaterialState>{MaterialState.selected} : <MaterialState>{};
final bool pmSelected = !amSelected;
- final Set<MaterialState> pmStates = pmSelected ? <MaterialState>{MaterialState.selected} : <MaterialState>{};
- final TextStyle textStyle = timePickerTheme.dayPeriodTextStyle ?? Theme.of(context).textTheme.titleMedium!;
- final TextStyle amStyle = textStyle.copyWith(
- color: MaterialStateProperty.resolveAs(textColor, amStates),
- );
- final TextStyle pmStyle = textStyle.copyWith(
- color: MaterialStateProperty.resolveAs(textColor, pmStates),
- );
- OutlinedBorder shape = timePickerTheme.dayPeriodShape ??
- const RoundedRectangleBorder(borderRadius: _kDefaultBorderRadius);
- final BorderSide borderSide = timePickerTheme.dayPeriodBorderSide ?? BorderSide(
- color: Color.alphaBlend(colorScheme.onBackground.withOpacity(0.38), colorScheme.surface),
- );
- // Apply the custom borderSide.
- shape = shape.copyWith(
- side: borderSide,
+ final BorderSide resolvedSide = timePickerTheme.dayPeriodBorderSide ?? defaultTheme.dayPeriodBorderSide;
+ final OutlinedBorder resolvedShape = (timePickerTheme.dayPeriodShape ?? defaultTheme.dayPeriodShape)
+ .copyWith(side: resolvedSide);
+
+ final Widget amButton = _AmPmButton(
+ selected: amSelected,
+ onPressed: () => _setAm(context),
+ label: materialLocalizations.anteMeridiemAbbreviation,
);
- final double buttonTextScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 2.0);
-
- final Widget amButton = Material(
- color: MaterialStateProperty.resolveAs(backgroundColor, amStates),
- child: InkWell(
- onTap: Feedback.wrapForTap(() => _setAm(context), context),
- child: Semantics(
- checked: amSelected,
- inMutuallyExclusiveGroup: true,
- button: true,
- child: Center(
- child: Text(
- materialLocalizations.anteMeridiemAbbreviation,
- style: amStyle,
- textScaleFactor: buttonTextScaleFactor,
- ),
- ),
- ),
- ),
+ final Widget pmButton = _AmPmButton(
+ selected: pmSelected,
+ onPressed: () => _setPm(context),
+ label: materialLocalizations.postMeridiemAbbreviation,
);
- final Widget pmButton = Material(
- color: MaterialStateProperty.resolveAs(backgroundColor, pmStates),
- child: InkWell(
- onTap: Feedback.wrapForTap(() => _setPm(context), context),
- child: Semantics(
- checked: pmSelected,
- inMutuallyExclusiveGroup: true,
- button: true,
- child: Center(
- child: Text(
- materialLocalizations.postMeridiemAbbreviation,
- style: pmStyle,
- textScaleFactor: buttonTextScaleFactor,
- ),
- ),
- ),
- ),
- );
+ Size dayPeriodSize;
+ final Orientation orientation;
+ switch (_TimePickerModel.entryModeOf(context)) {
+ case TimePickerEntryMode.dial:
+ case TimePickerEntryMode.dialOnly:
+ orientation = _TimePickerModel.orientationOf(context);
+ switch (orientation) {
+ case Orientation.portrait:
+ dayPeriodSize = defaultTheme.dayPeriodPortraitSize;
+ break;
+ case Orientation.landscape:
+ dayPeriodSize = defaultTheme.dayPeriodLandscapeSize;
+ break;
+ }
+ break;
+ case TimePickerEntryMode.input:
+ case TimePickerEntryMode.inputOnly:
+ orientation = Orientation.portrait;
+ dayPeriodSize = defaultTheme.dayPeriodInputSize;
+ break;
+ }
final Widget result;
switch (orientation) {
case Orientation.portrait:
- const double width = 52.0;
result = _DayPeriodInputPadding(
- minSize: const Size(width, kMinInteractiveDimension * 2),
+ minSize: dayPeriodSize,
orientation: orientation,
- child: SizedBox(
- width: width,
- height: _kTimePickerHeaderControlHeight,
+ child: SizedBox.fromSize(
+ size: dayPeriodSize,
child: Material(
clipBehavior: Clip.antiAlias,
color: Colors.transparent,
- shape: shape,
+ shape: resolvedShape,
child: Column(
children: <Widget>[
Expanded(child: amButton),
Container(
- decoration: BoxDecoration(
- border: Border(top: borderSide),
- ),
+ decoration: BoxDecoration(border: Border(top: resolvedSide)),
height: 1,
),
Expanded(child: pmButton),
@@ -653,21 +662,19 @@
break;
case Orientation.landscape:
result = _DayPeriodInputPadding(
- minSize: const Size(0.0, kMinInteractiveDimension),
+ minSize: dayPeriodSize,
orientation: orientation,
child: SizedBox(
- height: 40.0,
+ height: dayPeriodSize.height,
child: Material(
clipBehavior: Clip.antiAlias,
color: Colors.transparent,
- shape: shape,
+ shape: resolvedShape,
child: Row(
children: <Widget>[
Expanded(child: amButton),
Container(
- decoration: BoxDecoration(
- border: Border(left: borderSide),
- ),
+ decoration: BoxDecoration(border: Border(left: resolvedSide)),
width: 1,
),
Expanded(child: pmButton),
@@ -682,6 +689,48 @@
}
}
+class _AmPmButton extends StatelessWidget {
+ const _AmPmButton({
+ required this.onPressed,
+ required this.selected,
+ required this.label,
+ });
+
+ final bool selected;
+ final VoidCallback onPressed;
+ final String label;
+
+ @override
+ Widget build(BuildContext context) {
+ final Set<MaterialState> states = <MaterialState>{ if (selected) MaterialState.selected };
+ final TimePickerThemeData timePickerTheme = _TimePickerModel.themeOf(context);
+ final _TimePickerDefaults defaultTheme = _TimePickerModel.defaultThemeOf(context);
+ final Color resolvedBackgroundColor = MaterialStateProperty.resolveAs<Color>(timePickerTheme.dayPeriodColor ?? defaultTheme.dayPeriodColor, states);
+ final Color resolvedTextColor = MaterialStateProperty.resolveAs<Color>(timePickerTheme.dayPeriodTextColor ?? defaultTheme.dayPeriodTextColor, states);
+ final TextStyle? resolvedTextStyle = MaterialStateProperty.resolveAs<TextStyle?>(timePickerTheme.dayPeriodTextStyle ?? defaultTheme.dayPeriodTextStyle, states)?.copyWith(color: resolvedTextColor);
+ final double buttonTextScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 2);
+
+ return Material(
+ color: resolvedBackgroundColor,
+ child: InkWell(
+ onTap: Feedback.wrapForTap(onPressed, context),
+ child: Semantics(
+ checked: selected,
+ inMutuallyExclusiveGroup: true,
+ button: true,
+ child: Center(
+ child: Text(
+ label,
+ style: resolvedTextStyle,
+ textScaleFactor: buttonTextScaleFactor,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
/// A widget to pad the area around the [_DayPeriodControl]'s inner [Material].
class _DayPeriodInputPadding extends SingleChildRenderObjectWidget {
const _DayPeriodInputPadding({
@@ -734,7 +783,7 @@
if (child != null) {
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
}
- return 0.0;
+ return 0;
}
@override
@@ -742,7 +791,7 @@
if (child != null) {
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
}
- return 0.0;
+ return 0;
}
@override
@@ -750,7 +799,7 @@
if (child != null) {
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
}
- return 0.0;
+ return 0;
}
@override
@@ -758,7 +807,7 @@
if (child != null) {
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
}
- return 0.0;
+ return 0;
}
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
@@ -797,9 +846,9 @@
return true;
}
- if (position.dx < 0.0 ||
+ if (position.dx < 0 ||
position.dx > math.max(child!.size.width, minSize.width) ||
- position.dy < 0.0 ||
+ position.dy < 0 ||
position.dy > math.max(child!.size.height, minSize.height)) {
return false;
}
@@ -808,21 +857,20 @@
switch (orientation) {
case Orientation.portrait:
if (position.dy > newPosition.dy) {
- newPosition += const Offset(0.0, 1.0);
+ newPosition += const Offset(0, 1);
} else {
- newPosition += const Offset(0.0, -1.0);
+ newPosition += const Offset(0, -1);
}
break;
case Orientation.landscape:
if (position.dx > newPosition.dx) {
- newPosition += const Offset(1.0, 0.0);
+ newPosition += const Offset(1, 0);
} else {
- newPosition += const Offset(-1.0, 0.0);
+ newPosition += const Offset(-1, 0);
}
break;
}
-
return result.addWithRawTransform(
transform: MatrixUtils.forceToPoint(newPosition),
position: newPosition,
@@ -837,6 +885,7 @@
class _TappableLabel {
_TappableLabel({
required this.value,
+ required this.inner,
required this.painter,
required this.onTap,
});
@@ -844,6 +893,10 @@
/// The value this label is displaying.
final int value;
+ /// This value is part of the "inner" ring of values on the dial, used for 24
+ /// hour input.
+ final bool inner;
+
/// Paints the text of the label.
final TextPainter painter;
@@ -854,73 +907,88 @@
class _DialPainter extends CustomPainter {
_DialPainter({
required this.primaryLabels,
- required this.secondaryLabels,
+ required this.selectedLabels,
required this.backgroundColor,
- required this.accentColor,
+ required this.handColor,
+ required this.handWidth,
required this.dotColor,
+ required this.dotRadius,
+ required this.centerRadius,
required this.theta,
+ required this.radius,
required this.textDirection,
required this.selectedValue,
}) : super(repaint: PaintingBinding.instance.systemFonts);
final List<_TappableLabel> primaryLabels;
- final List<_TappableLabel> secondaryLabels;
+ final List<_TappableLabel> selectedLabels;
final Color backgroundColor;
- final Color accentColor;
+ final Color handColor;
+ final double handWidth;
final Color dotColor;
+ final double dotRadius;
+ final double centerRadius;
final double theta;
+ final double radius;
final TextDirection textDirection;
final int selectedValue;
- static const double _labelPadding = 28.0;
-
void dispose() {
for (final _TappableLabel label in primaryLabels) {
label.painter.dispose();
}
- for (final _TappableLabel label in secondaryLabels) {
+ for (final _TappableLabel label in selectedLabels) {
label.painter.dispose();
}
primaryLabels.clear();
- secondaryLabels.clear();
+ selectedLabels.clear();
}
@override
void paint(Canvas canvas, Size size) {
- final double radius = size.shortestSide / 2.0;
- final Offset center = Offset(size.width / 2.0, size.height / 2.0);
+ final double dialRadius = clampDouble(size.shortestSide / 2, _kTimePickerDialMinRadius + dotRadius, double.infinity);
+ final double labelRadius = clampDouble(dialRadius - _kTimePickerDialPadding, _kTimePickerDialMinRadius, double.infinity);
+ final double innerLabelRadius = clampDouble(labelRadius - _kTimePickerInnerDialOffset, 0, double.infinity);
+ final double handleRadius = clampDouble(labelRadius - (radius < 0.5 ? 1 : 0) * (labelRadius - innerLabelRadius), _kTimePickerDialMinRadius, double.infinity);
+ final Offset center = Offset(size.width / 2, size.height / 2);
final Offset centerPoint = center;
- canvas.drawCircle(centerPoint, radius, Paint()..color = backgroundColor);
+ canvas.drawCircle(centerPoint, dialRadius, Paint()..color = backgroundColor);
- final double labelRadius = radius - _labelPadding;
- Offset getOffsetForTheta(double theta) {
- return center + Offset(labelRadius * math.cos(theta), -labelRadius * math.sin(theta));
+ Offset getOffsetForTheta(double theta, double radius) {
+ return center + Offset(radius * math.cos(theta), -radius * math.sin(theta));
}
- void paintLabels(List<_TappableLabel>? labels) {
- if (labels == null) {
+ void paintLabels(List<_TappableLabel> labels, double radius) {
+ if (labels.isEmpty) {
return;
}
final double labelThetaIncrement = -_kTwoPi / labels.length;
- double labelTheta = math.pi / 2.0;
+ double labelTheta = math.pi / 2;
for (final _TappableLabel label in labels) {
final TextPainter labelPainter = label.painter;
- final Offset labelOffset = Offset(-labelPainter.width / 2.0, -labelPainter.height / 2.0);
- labelPainter.paint(canvas, getOffsetForTheta(labelTheta) + labelOffset);
+ final Offset labelOffset = Offset(-labelPainter.width / 2, -labelPainter.height / 2);
+ labelPainter.paint(canvas, getOffsetForTheta(labelTheta, radius) + labelOffset);
labelTheta += labelThetaIncrement;
}
}
- paintLabels(primaryLabels);
+ void paintInnerOuterLabels(List<_TappableLabel>? labels) {
+ if (labels == null) {
+ return;
+ }
- final Paint selectorPaint = Paint()
- ..color = accentColor;
- final Offset focusedPoint = getOffsetForTheta(theta);
- const double focusedRadius = _labelPadding - 4.0;
- canvas.drawCircle(centerPoint, 4.0, selectorPaint);
- canvas.drawCircle(focusedPoint, focusedRadius, selectorPaint);
- selectorPaint.strokeWidth = 2.0;
+ paintLabels(labels.where((_TappableLabel label) => !label.inner).toList(), labelRadius);
+ paintLabels(labels.where((_TappableLabel label) => label.inner).toList(), innerLabelRadius);
+ }
+
+ paintInnerOuterLabels(primaryLabels);
+
+ final Paint selectorPaint = Paint()..color = handColor;
+ final Offset focusedPoint = getOffsetForTheta(theta, handleRadius);
+ canvas.drawCircle(centerPoint, centerRadius, selectorPaint);
+ canvas.drawCircle(focusedPoint, dotRadius, selectorPaint);
+ selectorPaint.strokeWidth = handWidth;
canvas.drawLine(centerPoint, focusedPoint, selectorPaint);
// Add a dot inside the selector but only when it isn't over the labels.
@@ -929,43 +997,49 @@
// labels. The values were derived by manually testing the dial.
final double labelThetaIncrement = -_kTwoPi / primaryLabels.length;
if (theta % labelThetaIncrement > 0.1 && theta % labelThetaIncrement < 0.45) {
- canvas.drawCircle(focusedPoint, 2.0, selectorPaint..color = dotColor);
+ canvas.drawCircle(focusedPoint, 2, selectorPaint..color = dotColor);
}
final Rect focusedRect = Rect.fromCircle(
- center: focusedPoint, radius: focusedRadius,
+ center: focusedPoint,
+ radius: dotRadius,
);
canvas
..save()
..clipPath(Path()..addOval(focusedRect));
- paintLabels(secondaryLabels);
+ paintInnerOuterLabels(selectedLabels);
canvas.restore();
}
@override
bool shouldRepaint(_DialPainter oldPainter) {
return oldPainter.primaryLabels != primaryLabels
- || oldPainter.secondaryLabels != secondaryLabels
+ || oldPainter.selectedLabels != selectedLabels
|| oldPainter.backgroundColor != backgroundColor
- || oldPainter.accentColor != accentColor
+ || oldPainter.handColor != handColor
|| oldPainter.theta != theta;
}
}
+// Which kind of hour dial being presented.
+enum _HourDialType {
+ twentyFourHour,
+ twentyFourHourDoubleRing,
+ twelveHour,
+}
+
class _Dial extends StatefulWidget {
const _Dial({
required this.selectedTime,
- required this.mode,
- required this.use24HourDials,
+ required this.hourMinuteMode,
+ required this.hourDialType,
required this.onChanged,
required this.onHourSelected,
- }) : assert(selectedTime != null),
- assert(mode != null),
- assert(use24HourDials != null);
+ });
final TimeOfDay selectedTime;
- final _TimePickerMode mode;
- final bool use24HourDials;
+ final _HourMinuteMode hourMinuteMode;
+ final _HourDialType hourDialType;
final ValueChanged<TimeOfDay>? onChanged;
final VoidCallback? onHourSelected;
@@ -974,103 +1048,174 @@
}
class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
+ late ThemeData themeData;
+ late MaterialLocalizations localizations;
+ _DialPainter? painter;
+ late AnimationController _animationController;
+ late Tween<double> _thetaTween;
+ late Animation<double> _theta;
+ late Tween<double> _radiusTween;
+ late Animation<double> _radius;
+ bool _dragging = false;
+
@override
void initState() {
super.initState();
- _thetaController = AnimationController(
+ _animationController = AnimationController(
duration: _kDialAnimateDuration,
vsync: this,
);
_thetaTween = Tween<double>(begin: _getThetaForTime(widget.selectedTime));
- _theta = _thetaController
+ _radiusTween = Tween<double>(begin: _getRadiusForTime(widget.selectedTime));
+ _theta = _animationController
.drive(CurveTween(curve: standardEasing))
.drive(_thetaTween)
..addListener(() => setState(() { /* _theta.value has changed */ }));
+ _radius = _animationController
+ .drive(CurveTween(curve: standardEasing))
+ .drive(_radiusTween)
+ ..addListener(() => setState(() { /* _radius.value has changed */ }));
}
- late ThemeData themeData;
- late MaterialLocalizations localizations;
- late MediaQueryData media;
- _DialPainter? painter;
-
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(debugCheckHasMediaQuery(context));
themeData = Theme.of(context);
localizations = MaterialLocalizations.of(context);
- media = MediaQuery.of(context);
}
@override
void didUpdateWidget(_Dial oldWidget) {
super.didUpdateWidget(oldWidget);
- if (widget.mode != oldWidget.mode || widget.selectedTime != oldWidget.selectedTime) {
+ if (widget.hourMinuteMode != oldWidget.hourMinuteMode || widget.selectedTime != oldWidget.selectedTime) {
if (!_dragging) {
- _animateTo(_getThetaForTime(widget.selectedTime));
+ _animateTo(_getThetaForTime(widget.selectedTime), _getRadiusForTime(widget.selectedTime));
}
}
}
@override
void dispose() {
- _thetaController.dispose();
+ _animationController.dispose();
painter?.dispose();
super.dispose();
}
- late Tween<double> _thetaTween;
- late Animation<double> _theta;
- late AnimationController _thetaController;
- bool _dragging = false;
-
static double _nearest(double target, double a, double b) {
return ((target - a).abs() < (target - b).abs()) ? a : b;
}
- void _animateTo(double targetTheta) {
- final double currentTheta = _theta.value;
- double beginTheta = _nearest(targetTheta, currentTheta, currentTheta + _kTwoPi);
- beginTheta = _nearest(targetTheta, beginTheta, currentTheta - _kTwoPi);
- _thetaTween
- ..begin = beginTheta
- ..end = targetTheta;
- _thetaController
- ..value = 0.0
- ..forward();
+ void _animateTo(double targetTheta, double targetRadius) {
+ void animateToValue({
+ required double target,
+ required Animation<double> animation,
+ required Tween<double> tween,
+ required AnimationController controller,
+ required double min,
+ required double max,
+ }) {
+ double beginValue = _nearest(target, animation.value, max);
+ beginValue = _nearest(target, beginValue, min);
+ tween
+ ..begin = beginValue
+ ..end = target;
+ controller
+ ..value = 0
+ ..forward();
+ }
+
+ animateToValue(
+ target: targetTheta,
+ animation: _theta,
+ tween: _thetaTween,
+ controller: _animationController,
+ min: _theta.value - _kTwoPi,
+ max: _theta.value + _kTwoPi,
+ );
+ animateToValue(
+ target: targetRadius,
+ animation: _radius,
+ tween: _radiusTween,
+ controller: _animationController,
+ min: 0,
+ max: 1,
+ );
+ }
+
+ double _getRadiusForTime(TimeOfDay time) {
+ switch (widget.hourMinuteMode) {
+ case _HourMinuteMode.hour:
+ switch (widget.hourDialType) {
+ case _HourDialType.twentyFourHourDoubleRing:
+ return time.hour >= 12 ? 0 : 1;
+ case _HourDialType.twentyFourHour:
+ case _HourDialType.twelveHour:
+ return 1;
+ }
+ case _HourMinuteMode.minute:
+ return 1;
+ }
}
double _getThetaForTime(TimeOfDay time) {
- final int hoursFactor = widget.use24HourDials ? TimeOfDay.hoursPerDay : TimeOfDay.hoursPerPeriod;
- final double fraction = widget.mode == _TimePickerMode.hour
- ? (time.hour / hoursFactor) % hoursFactor
- : (time.minute / TimeOfDay.minutesPerHour) % TimeOfDay.minutesPerHour;
- return (math.pi / 2.0 - fraction * _kTwoPi) % _kTwoPi;
+ final int hoursFactor;
+ switch (widget.hourDialType) {
+ case _HourDialType.twentyFourHour:
+ hoursFactor = TimeOfDay.hoursPerDay;
+ break;
+ case _HourDialType.twentyFourHourDoubleRing:
+ hoursFactor = TimeOfDay.hoursPerPeriod;
+ break;
+ case _HourDialType.twelveHour:
+ hoursFactor = TimeOfDay.hoursPerPeriod;
+ break;
+ }
+ final double fraction;
+ switch (widget.hourMinuteMode) {
+ case _HourMinuteMode.hour:
+ fraction = (time.hour / hoursFactor) % hoursFactor;
+ break;
+ case _HourMinuteMode.minute:
+ fraction = (time.minute / TimeOfDay.minutesPerHour) % TimeOfDay.minutesPerHour;
+ break;
+ }
+ return (math.pi / 2 - fraction * _kTwoPi) % _kTwoPi;
}
- TimeOfDay _getTimeForTheta(double theta, {bool roundMinutes = false}) {
- final double fraction = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;
- if (widget.mode == _TimePickerMode.hour) {
- int newHour;
- if (widget.use24HourDials) {
- newHour = (fraction * TimeOfDay.hoursPerDay).round() % TimeOfDay.hoursPerDay;
- } else {
- newHour = (fraction * TimeOfDay.hoursPerPeriod).round() % TimeOfDay.hoursPerPeriod;
- newHour = newHour + widget.selectedTime.periodOffset;
- }
- return widget.selectedTime.replacing(hour: newHour);
- } else {
- int minute = (fraction * TimeOfDay.minutesPerHour).round() % TimeOfDay.minutesPerHour;
- if (roundMinutes) {
- // Round the minutes to nearest 5 minute interval.
- minute = ((minute + 2) ~/ 5) * 5 % TimeOfDay.minutesPerHour;
- }
- return widget.selectedTime.replacing(minute: minute);
+ TimeOfDay _getTimeForTheta(double theta, {bool roundMinutes = false, required double radius}) {
+ final double fraction = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1;
+ switch (widget.hourMinuteMode) {
+ case _HourMinuteMode.hour:
+ int newHour;
+ switch (widget.hourDialType) {
+ case _HourDialType.twentyFourHour:
+ newHour = (fraction * TimeOfDay.hoursPerDay).round() % TimeOfDay.hoursPerDay;
+ break;
+ case _HourDialType.twentyFourHourDoubleRing:
+ newHour = (fraction * TimeOfDay.hoursPerPeriod).round() % TimeOfDay.hoursPerPeriod;
+ if (radius < 0.5) {
+ newHour = newHour + TimeOfDay.hoursPerPeriod;
+ }
+ break;
+ case _HourDialType.twelveHour:
+ newHour = (fraction * TimeOfDay.hoursPerPeriod).round() % TimeOfDay.hoursPerPeriod;
+ newHour = newHour + widget.selectedTime.periodOffset;
+ break;
+ }
+ return widget.selectedTime.replacing(hour: newHour);
+ case _HourMinuteMode.minute:
+ int minute = (fraction * TimeOfDay.minutesPerHour).round() % TimeOfDay.minutesPerHour;
+ if (roundMinutes) {
+ // Round the minutes to nearest 5 minute interval.
+ minute = ((minute + 2) ~/ 5) * 5 % TimeOfDay.minutesPerHour;
+ }
+ return widget.selectedTime.replacing(minute: minute);
}
}
TimeOfDay _notifyOnChangedIfNeeded({ bool roundMinutes = false }) {
- final TimeOfDay current = _getTimeForTheta(_theta.value, roundMinutes: roundMinutes);
+ final TimeOfDay current = _getTimeForTheta(_theta.value, roundMinutes: roundMinutes, radius: _radius.value);
if (widget.onChanged == null) {
return current;
}
@@ -1083,25 +1228,34 @@
void _updateThetaForPan({ bool roundMinutes = false }) {
setState(() {
final Offset offset = _position! - _center!;
- double angle = (math.atan2(offset.dx, offset.dy) - math.pi / 2.0) % _kTwoPi;
+ final double labelRadius = _dialSize!.shortestSide / 2 - _kTimePickerDialPadding;
+ final double innerRadius = labelRadius - _kTimePickerInnerDialOffset;
+ double angle = (math.atan2(offset.dx, offset.dy) - math.pi / 2) % _kTwoPi;
+ final double radius = clampDouble((offset.distance - innerRadius) / _kTimePickerInnerDialOffset, 0, 1);
if (roundMinutes) {
- angle = _getThetaForTime(_getTimeForTheta(angle, roundMinutes: roundMinutes));
+ angle = _getThetaForTime(_getTimeForTheta(angle, roundMinutes: roundMinutes, radius: radius));
}
+ // The controller doesn't animate during the pan gesture.
_thetaTween
..begin = angle
- ..end = angle; // The controller doesn't animate during the pan gesture.
+ ..end = angle;
+ _radiusTween
+ ..begin = radius
+ ..end = radius;
});
}
Offset? _position;
Offset? _center;
+ Size? _dialSize;
void _handlePanStart(DragStartDetails details) {
assert(!_dragging);
_dragging = true;
final RenderBox box = context.findRenderObject()! as RenderBox;
_position = box.globalToLocal(details.globalPosition);
- _center = box.size.center(Offset.zero);
+ _dialSize = box.size;
+ _center = _dialSize!.center(Offset.zero);
_updateThetaForPan();
_notifyOnChangedIfNeeded();
}
@@ -1117,8 +1271,9 @@
_dragging = false;
_position = null;
_center = null;
- _animateTo(_getThetaForTime(widget.selectedTime));
- if (widget.mode == _TimePickerMode.hour) {
+ _dialSize = null;
+ _animateTo(_getThetaForTime(widget.selectedTime), _getRadiusForTime(widget.selectedTime));
+ if (widget.hourMinuteMode == _HourMinuteMode.hour) {
widget.onHourSelected?.call();
}
}
@@ -1127,36 +1282,60 @@
final RenderBox box = context.findRenderObject()! as RenderBox;
_position = box.globalToLocal(details.globalPosition);
_center = box.size.center(Offset.zero);
+ _dialSize = box.size;
_updateThetaForPan(roundMinutes: true);
final TimeOfDay newTime = _notifyOnChangedIfNeeded(roundMinutes: true);
- if (widget.mode == _TimePickerMode.hour) {
- if (widget.use24HourDials) {
- _announceToAccessibility(context, localizations.formatDecimal(newTime.hour));
- } else {
- _announceToAccessibility(context, localizations.formatDecimal(newTime.hourOfPeriod));
+ if (widget.hourMinuteMode == _HourMinuteMode.hour) {
+ switch (widget.hourDialType) {
+ case _HourDialType.twentyFourHour:
+ case _HourDialType.twentyFourHourDoubleRing:
+ _announceToAccessibility(context, localizations.formatDecimal(newTime.hour));
+ break;
+ case _HourDialType.twelveHour:
+ _announceToAccessibility(context, localizations.formatDecimal(newTime.hourOfPeriod));
+ break;
}
widget.onHourSelected?.call();
} else {
_announceToAccessibility(context, localizations.formatDecimal(newTime.minute));
}
- _animateTo(_getThetaForTime(_getTimeForTheta(_theta.value, roundMinutes: true)));
+ final TimeOfDay time = _getTimeForTheta(_theta.value, roundMinutes: true, radius: _radius.value);
+ _animateTo(_getThetaForTime(time), _getRadiusForTime(time));
_dragging = false;
_position = null;
_center = null;
+ _dialSize = null;
}
void _selectHour(int hour) {
_announceToAccessibility(context, localizations.formatDecimal(hour));
final TimeOfDay time;
- if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) {
- time = TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
- } else {
- if (widget.selectedTime.period == DayPeriod.am) {
- time = TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
- } else {
- time = TimeOfDay(hour: hour + TimeOfDay.hoursPerPeriod, minute: widget.selectedTime.minute);
+
+ TimeOfDay getAmPmTime() {
+ switch (widget.selectedTime.period) {
+ case DayPeriod.am:
+ return TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
+ case DayPeriod.pm:
+ return TimeOfDay(hour: hour + TimeOfDay.hoursPerPeriod, minute: widget.selectedTime.minute);
}
}
+
+ switch (widget.hourMinuteMode) {
+ case _HourMinuteMode.hour:
+ switch (widget.hourDialType) {
+ case _HourDialType.twentyFourHour:
+ case _HourDialType.twentyFourHourDoubleRing:
+ time = TimeOfDay(hour: hour, minute: widget.selectedTime.minute);
+ break;
+ case _HourDialType.twelveHour:
+ time = getAmPmTime();
+ break;
+ }
+ break;
+ case _HourMinuteMode.minute:
+ time = getAmPmTime();
+ break;
+ }
final double angle = _getThetaForTime(time);
_thetaTween
..begin = angle
@@ -1192,7 +1371,8 @@
TimeOfDay(hour: 11, minute: 0),
];
- static const List<TimeOfDay> _twentyFourHours = <TimeOfDay>[
+ // On M2, there's no inner ring of numbers.
+ static const List<TimeOfDay> _twentyFourHoursM2 = <TimeOfDay>[
TimeOfDay(hour: 0, minute: 0),
TimeOfDay(hour: 2, minute: 0),
TimeOfDay(hour: 4, minute: 0),
@@ -1207,13 +1387,47 @@
TimeOfDay(hour: 22, minute: 0),
];
- _TappableLabel _buildTappableLabel(TextTheme textTheme, Color color, int value, String label, VoidCallback onTap) {
- final TextStyle style = textTheme.bodyLarge!.copyWith(color: color);
- final double labelScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 2.0);
+ static const List<TimeOfDay> _twentyFourHours = <TimeOfDay>[
+ TimeOfDay(hour: 0, minute: 0),
+ TimeOfDay(hour: 1, minute: 0),
+ TimeOfDay(hour: 2, minute: 0),
+ TimeOfDay(hour: 3, minute: 0),
+ TimeOfDay(hour: 4, minute: 0),
+ TimeOfDay(hour: 5, minute: 0),
+ TimeOfDay(hour: 6, minute: 0),
+ TimeOfDay(hour: 7, minute: 0),
+ TimeOfDay(hour: 8, minute: 0),
+ TimeOfDay(hour: 9, minute: 0),
+ TimeOfDay(hour: 10, minute: 0),
+ TimeOfDay(hour: 11, minute: 0),
+ TimeOfDay(hour: 12, minute: 0),
+ TimeOfDay(hour: 13, minute: 0),
+ TimeOfDay(hour: 14, minute: 0),
+ TimeOfDay(hour: 15, minute: 0),
+ TimeOfDay(hour: 16, minute: 0),
+ TimeOfDay(hour: 17, minute: 0),
+ TimeOfDay(hour: 18, minute: 0),
+ TimeOfDay(hour: 19, minute: 0),
+ TimeOfDay(hour: 20, minute: 0),
+ TimeOfDay(hour: 21, minute: 0),
+ TimeOfDay(hour: 22, minute: 0),
+ TimeOfDay(hour: 23, minute: 0),
+ ];
+
+ _TappableLabel _buildTappableLabel({
+ required TextStyle? textStyle,
+ required int selectedValue,
+ required int value,
+ required bool inner,
+ required String label,
+ required VoidCallback onTap,
+ }) {
+ final double labelScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 2);
return _TappableLabel(
value: value,
+ inner: inner,
painter: TextPainter(
- text: TextSpan(style: style, text: label),
+ text: TextSpan(style: textStyle, text: label),
textDirection: TextDirection.ltr,
textScaleFactor: labelScaleFactor,
)..layout(),
@@ -1221,33 +1435,63 @@
);
}
- List<_TappableLabel> _build24HourRing(TextTheme textTheme, Color color) => <_TappableLabel>[
- for (final TimeOfDay timeOfDay in _twentyFourHours)
- _buildTappableLabel(
- textTheme,
- color,
- timeOfDay.hour,
- localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
- () {
- _selectHour(timeOfDay.hour);
- },
- ),
- ];
+ List<_TappableLabel> _build24HourRing({
+ required TextStyle? textStyle,
+ required int selectedValue,
+ }) {
+ return <_TappableLabel>[
+ if (themeData.useMaterial3)
+ for (final TimeOfDay timeOfDay in _twentyFourHours)
+ _buildTappableLabel(
+ textStyle: textStyle,
+ selectedValue: selectedValue,
+ inner: timeOfDay.hour >= 12,
+ value: timeOfDay.hour,
+ label: timeOfDay.hour != 0
+ ? '${timeOfDay.hour}'
+ : localizations.formatHour(timeOfDay, alwaysUse24HourFormat: true),
+ onTap: () {
+ _selectHour(timeOfDay.hour);
+ },
+ ),
+ if (!themeData.useMaterial3)
+ for (final TimeOfDay timeOfDay in _twentyFourHoursM2)
+ _buildTappableLabel(
+ textStyle: textStyle,
+ selectedValue: selectedValue,
+ inner: false,
+ value: timeOfDay.hour,
+ label: localizations.formatHour(timeOfDay, alwaysUse24HourFormat: true),
+ onTap: () {
+ _selectHour(timeOfDay.hour);
+ },
+ ),
+ ];
+ }
- List<_TappableLabel> _build12HourRing(TextTheme textTheme, Color color) => <_TappableLabel>[
- for (final TimeOfDay timeOfDay in _amHours)
- _buildTappableLabel(
- textTheme,
- color,
- timeOfDay.hour,
- localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
- () {
- _selectHour(timeOfDay.hour);
- },
- ),
- ];
+ List<_TappableLabel> _build12HourRing({
+ required TextStyle? textStyle,
+ required int selectedValue,
+ }) {
+ return <_TappableLabel>[
+ for (final TimeOfDay timeOfDay in _amHours)
+ _buildTappableLabel(
+ textStyle: textStyle,
+ selectedValue: selectedValue,
+ inner: false,
+ value: timeOfDay.hour,
+ label: localizations.formatHour(timeOfDay, alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context)),
+ onTap: () {
+ _selectHour(timeOfDay.hour);
+ },
+ ),
+ ];
+ }
- List<_TappableLabel> _buildMinutes(TextTheme textTheme, Color color) {
+ List<_TappableLabel> _buildMinutes({
+ required TextStyle? textStyle,
+ required int selectedValue,
+ }) {
const List<TimeOfDay> minuteMarkerValues = <TimeOfDay>[
TimeOfDay(hour: 0, minute: 0),
TimeOfDay(hour: 0, minute: 5),
@@ -1266,11 +1510,12 @@
return <_TappableLabel>[
for (final TimeOfDay timeOfDay in minuteMarkerValues)
_buildTappableLabel(
- textTheme,
- color,
- timeOfDay.minute,
- localizations.formatMinute(timeOfDay),
- () {
+ textStyle: textStyle,
+ selectedValue: selectedValue,
+ inner: false,
+ value: timeOfDay.minute,
+ label: localizations.formatMinute(timeOfDay),
+ onTap: () {
_selectMinute(timeOfDay.minute);
},
),
@@ -1280,42 +1525,79 @@
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
- final TimePickerThemeData pickerTheme = TimePickerTheme.of(context);
- final Color backgroundColor = pickerTheme.dialBackgroundColor ?? themeData.colorScheme.onBackground.withOpacity(0.12);
- final Color accentColor = pickerTheme.dialHandColor ?? themeData.colorScheme.primary;
- final Color primaryLabelColor = MaterialStateProperty.resolveAs(pickerTheme.dialTextColor, <MaterialState>{}) ?? themeData.colorScheme.onSurface;
- final Color secondaryLabelColor = MaterialStateProperty.resolveAs(pickerTheme.dialTextColor, <MaterialState>{MaterialState.selected}) ?? themeData.colorScheme.onPrimary;
+ final TimePickerThemeData timePickerTheme = TimePickerTheme.of(context);
+ final _TimePickerDefaults defaultTheme = theme.useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ final Color backgroundColor = timePickerTheme.dialBackgroundColor ?? defaultTheme.dialBackgroundColor;
+ final Color dialHandColor = timePickerTheme.dialHandColor ?? defaultTheme.dialHandColor;
+ final TextStyle labelStyle = timePickerTheme.dialTextStyle ?? defaultTheme.dialTextStyle;
+ final Color dialTextUnselectedColor = MaterialStateProperty
+ .resolveAs<Color>(timePickerTheme.dialTextColor ?? defaultTheme.dialTextColor, <MaterialState>{ });
+ final Color dialTextSelectedColor = MaterialStateProperty
+ .resolveAs<Color>(timePickerTheme.dialTextColor ?? defaultTheme.dialTextColor, <MaterialState>{ MaterialState.selected });
+ final TextStyle resolvedUnselectedLabelStyle = labelStyle.copyWith(color: dialTextUnselectedColor);
+ final TextStyle resolvedSelectedLabelStyle = labelStyle.copyWith(color: dialTextSelectedColor);
+ final Color dotColor = dialTextSelectedColor;
+
List<_TappableLabel> primaryLabels;
- List<_TappableLabel> secondaryLabels;
+ List<_TappableLabel> selectedLabels;
final int selectedDialValue;
- switch (widget.mode) {
- case _TimePickerMode.hour:
- if (widget.use24HourDials) {
- selectedDialValue = widget.selectedTime.hour;
- primaryLabels = _build24HourRing(theme.textTheme, primaryLabelColor);
- secondaryLabels = _build24HourRing(theme.textTheme, secondaryLabelColor);
- } else {
- selectedDialValue = widget.selectedTime.hourOfPeriod;
- primaryLabels = _build12HourRing(theme.textTheme, primaryLabelColor);
- secondaryLabels = _build12HourRing(theme.textTheme, secondaryLabelColor);
+ final double radiusValue;
+ switch (widget.hourMinuteMode) {
+ case _HourMinuteMode.hour:
+ switch (widget.hourDialType) {
+ case _HourDialType.twentyFourHour:
+ case _HourDialType.twentyFourHourDoubleRing:
+ selectedDialValue = widget.selectedTime.hour;
+ primaryLabels = _build24HourRing(
+ textStyle: resolvedUnselectedLabelStyle,
+ selectedValue: selectedDialValue,
+ );
+ selectedLabels = _build24HourRing(
+ textStyle: resolvedSelectedLabelStyle,
+ selectedValue: selectedDialValue,
+ );
+ radiusValue = theme.useMaterial3 ? _radius.value : 1;
+ break;
+ case _HourDialType.twelveHour:
+ selectedDialValue = widget.selectedTime.hourOfPeriod;
+ primaryLabels = _build12HourRing(
+ textStyle: resolvedUnselectedLabelStyle,
+ selectedValue: selectedDialValue,
+ );
+ selectedLabels = _build12HourRing(
+ textStyle: resolvedSelectedLabelStyle,
+ selectedValue: selectedDialValue,
+ );
+ radiusValue = 1;
+ break;
}
break;
- case _TimePickerMode.minute:
+ case _HourMinuteMode.minute:
selectedDialValue = widget.selectedTime.minute;
- primaryLabels = _buildMinutes(theme.textTheme, primaryLabelColor);
- secondaryLabels = _buildMinutes(theme.textTheme, secondaryLabelColor);
+ primaryLabels = _buildMinutes(
+ textStyle: resolvedUnselectedLabelStyle,
+ selectedValue: selectedDialValue,
+ );
+ selectedLabels = _buildMinutes(
+ textStyle: resolvedSelectedLabelStyle,
+ selectedValue: selectedDialValue,
+ );
+ radiusValue = 1;
break;
}
-
painter?.dispose();
painter = _DialPainter(
selectedValue: selectedDialValue,
primaryLabels: primaryLabels,
- secondaryLabels: secondaryLabels,
+ selectedLabels: selectedLabels,
backgroundColor: backgroundColor,
- accentColor: accentColor,
- dotColor: theme.colorScheme.surface,
+ handColor: dialHandColor,
+ handWidth: defaultTheme.handWidth,
+ dotColor: dotColor,
+ dotRadius: defaultTheme.dotRadius,
+ centerRadius: defaultTheme.centerRadius,
theta: _theta.value,
+ radius: radiusValue,
textDirection: Directionality.of(context),
);
@@ -1336,23 +1618,18 @@
class _TimePickerInput extends StatefulWidget {
const _TimePickerInput({
required this.initialSelectedTime,
- required this.helpText,
required this.errorInvalidText,
required this.hourLabelText,
required this.minuteLabelText,
+ required this.helpText,
required this.autofocusHour,
required this.autofocusMinute,
- required this.onChanged,
this.restorationId,
- }) : assert(initialSelectedTime != null),
- assert(onChanged != null);
+ });
/// The time initially selected when the dialog is shown.
final TimeOfDay initialSelectedTime;
- /// Optionally provide your own help text to the time picker.
- final String? helpText;
-
/// Optionally provide your own validation error text.
final String? errorInvalidText;
@@ -1362,12 +1639,12 @@
/// Optionally provide your own minute label text.
final String? minuteLabelText;
+ final String helpText;
+
final bool? autofocusHour;
final bool? autofocusMinute;
- final ValueChanged<TimeOfDay> onChanged;
-
/// Restoration ID to save and restore the state of the time picker input
/// widget.
///
@@ -1406,14 +1683,14 @@
return null;
}
- if (MediaQuery.of(context).alwaysUse24HourFormat) {
+ if (MediaQuery.alwaysUse24HourFormatOf(context)) {
if (newHour >= 0 && newHour < 24) {
return newHour;
}
} else {
if (newHour > 0 && newHour < 13) {
- if ((_selectedTime.value.period == DayPeriod.pm && newHour != 12)
- || (_selectedTime.value.period == DayPeriod.am && newHour == 12)) {
+ if ((_selectedTime.value.period == DayPeriod.pm && newHour != 12) ||
+ (_selectedTime.value.period == DayPeriod.am && newHour == 12)) {
newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay;
}
return newHour;
@@ -1442,7 +1719,7 @@
final int? newHour = _parseHour(value);
if (newHour != null) {
_selectedTime.value = TimeOfDay(hour: newHour, minute: _selectedTime.value.minute);
- widget.onChanged(_selectedTime.value);
+ _TimePickerModel.setSelectedTime(context, _selectedTime.value);
FocusScope.of(context).requestFocus();
}
}
@@ -1459,14 +1736,14 @@
final int? newMinute = _parseMinute(value);
if (newMinute != null) {
_selectedTime.value = TimeOfDay(hour: _selectedTime.value.hour, minute: int.parse(value!));
- widget.onChanged(_selectedTime.value);
+ _TimePickerModel.setSelectedTime(context, _selectedTime.value);
FocusScope.of(context).unfocus();
}
}
void _handleDayPeriodChanged(TimeOfDay value) {
_selectedTime.value = value;
- widget.onChanged(_selectedTime.value);
+ _TimePickerModel.setSelectedTime(context, _selectedTime.value);
}
String? _validateHour(String? value) {
@@ -1494,36 +1771,32 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData media = MediaQuery.of(context);
- final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat);
+ final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat(alwaysUse24HourFormat: _TimePickerModel.use24HourFormatOf(context));
final bool use24HourDials = hourFormat(of: timeOfDayFormat) != HourFormat.h;
final ThemeData theme = Theme.of(context);
- final TextStyle hourMinuteStyle = TimePickerTheme.of(context).hourMinuteTextStyle ?? theme.textTheme.displayMedium!;
- final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- final String timePickerInputHelpText = theme.useMaterial3
- ? localizations.timePickerInputHelpText
- : localizations.timePickerInputHelpText.toUpperCase();
+ final TimePickerThemeData timePickerTheme = _TimePickerModel.themeOf(context);
+ final _TimePickerDefaults defaultTheme = _TimePickerModel.defaultThemeOf(context);
+ final TextStyle hourMinuteStyle = timePickerTheme.hourMinuteTextStyle ?? defaultTheme.hourMinuteTextStyle;
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
+ padding: _TimePickerModel.useMaterial3Of(context) ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- Text(
- widget.helpText ?? timePickerInputHelpText,
- style: TimePickerTheme.of(context).helpTextStyle ?? theme.textTheme.labelSmall,
+ Padding(padding: EdgeInsetsDirectional.only(bottom: _TimePickerModel.useMaterial3Of(context) ? 20 : 24),
+ child: Text(
+ widget.helpText,
+ style: _TimePickerModel.themeOf(context).helpTextStyle ?? _TimePickerModel.defaultThemeOf(context).helpTextStyle,
+ ),
),
- const SizedBox(height: 16.0),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (!use24HourDials && timeOfDayFormat == TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
- _DayPeriodControl(
- selectedTime: _selectedTime.value,
- orientation: Orientation.portrait,
- onChanged: _handleDayPeriodChanged,
+ Padding(
+ padding: const EdgeInsetsDirectional.only(end: 12),
+ child: _DayPeriodControl(onPeriodChanged: _handleDayPeriodChanged),
),
- const SizedBox(width: 12.0),
],
Expanded(
child: Row(
@@ -1535,19 +1808,20 @@
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- const SizedBox(height: 8.0),
- _HourTextField(
- restorationId: 'hour_text_field',
- selectedTime: _selectedTime.value,
- style: hourMinuteStyle,
- autofocus: widget.autofocusHour,
- inputAction: TextInputAction.next,
- validator: _validateHour,
- onSavedSubmitted: _handleHourSavedSubmitted,
- onChanged: _handleHourChanged,
- hourLabelText: widget.hourLabelText,
+ Padding(
+ padding: const EdgeInsets.only(bottom: 10),
+ child: _HourTextField(
+ restorationId: 'hour_text_field',
+ selectedTime: _selectedTime.value,
+ style: hourMinuteStyle,
+ autofocus: widget.autofocusHour,
+ inputAction: TextInputAction.next,
+ validator: _validateHour,
+ onSavedSubmitted: _handleHourSavedSubmitted,
+ onChanged: _handleHourChanged,
+ hourLabelText: widget.hourLabelText,
+ ),
),
- const SizedBox(height: 8.0),
if (!hourHasError.value && !minuteHasError.value)
ExcludeSemantics(
child: Text(
@@ -1560,27 +1834,24 @@
],
),
),
- Container(
- margin: const EdgeInsets.only(top: 8.0),
- height: _kTimePickerHeaderControlHeight,
- child: _StringFragment(timeOfDayFormat: timeOfDayFormat),
- ),
+ _StringFragment(timeOfDayFormat: timeOfDayFormat),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- const SizedBox(height: 8.0),
- _MinuteTextField(
- restorationId: 'minute_text_field',
- selectedTime: _selectedTime.value,
- style: hourMinuteStyle,
- autofocus: widget.autofocusMinute,
- inputAction: TextInputAction.done,
- validator: _validateMinute,
- onSavedSubmitted: _handleMinuteSavedSubmitted,
- minuteLabelText: widget.minuteLabelText,
+ Padding(
+ padding: const EdgeInsets.only(bottom: 10),
+ child: _MinuteTextField(
+ restorationId: 'minute_text_field',
+ selectedTime: _selectedTime.value,
+ style: hourMinuteStyle,
+ autofocus: widget.autofocusMinute,
+ inputAction: TextInputAction.done,
+ validator: _validateMinute,
+ onSavedSubmitted: _handleMinuteSavedSubmitted,
+ minuteLabelText: widget.minuteLabelText,
+ ),
),
- const SizedBox(height: 8.0),
if (!hourHasError.value && !minuteHasError.value)
ExcludeSemantics(
child: Text(
@@ -1597,11 +1868,9 @@
),
),
if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...<Widget>[
- const SizedBox(width: 12.0),
- _DayPeriodControl(
- selectedTime: _selectedTime.value,
- orientation: Orientation.portrait,
- onChanged: _handleDayPeriodChanged,
+ Padding(
+ padding: const EdgeInsetsDirectional.only(start: 12),
+ child: _DayPeriodControl(onPeriodChanged: _handleDayPeriodChanged),
),
],
],
@@ -1612,7 +1881,7 @@
style: theme.textTheme.bodyMedium!.copyWith(color: theme.colorScheme.error),
)
else
- const SizedBox(height: 2.0),
+ const SizedBox(height: 2),
],
),
);
@@ -1651,7 +1920,7 @@
autofocus: autofocus,
inputAction: inputAction,
style: style,
- semanticHintText: hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel,
+ semanticHintText: hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel,
validator: validator,
onSavedSubmitted: onSavedSubmitted,
onChanged: onChanged,
@@ -1733,9 +2002,12 @@
@override
void initState() {
super.initState();
- focusNode = FocusNode()..addListener(() {
- setState(() { }); // Rebuild.
- });
+ focusNode = FocusNode()
+ ..addListener(() {
+ setState(() {
+ // Rebuild when focus changes.
+ });
+ });
}
@override
@@ -1759,86 +2031,97 @@
}
String get _formattedValue {
- final bool alwaysUse24HourFormat = MediaQuery.of(context).alwaysUse24HourFormat;
+ final bool alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
- return !widget.isHour ? localizations.formatMinute(widget.selectedTime) : localizations.formatHour(
- widget.selectedTime,
- alwaysUse24HourFormat: alwaysUse24HourFormat,
- );
+ return !widget.isHour
+ ? localizations.formatMinute(widget.selectedTime)
+ : localizations.formatHour(
+ widget.selectedTime,
+ alwaysUse24HourFormat: alwaysUse24HourFormat,
+ );
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TimePickerThemeData timePickerTheme = TimePickerTheme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
+ final _TimePickerDefaults defaultTheme = theme.useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ final bool alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(context);
- final InputDecorationTheme? inputDecorationTheme = timePickerTheme.inputDecorationTheme;
- InputDecoration inputDecoration;
- if (inputDecorationTheme != null) {
- inputDecoration = const InputDecoration().applyDefaults(inputDecorationTheme);
- } else {
- inputDecoration = InputDecoration(
- contentPadding: EdgeInsets.zero,
- filled: true,
- enabledBorder: const OutlineInputBorder(
- borderSide: BorderSide(color: Colors.transparent),
- ),
- errorBorder: OutlineInputBorder(
- borderSide: BorderSide(color: colorScheme.error, width: 2.0),
- ),
- focusedBorder: OutlineInputBorder(
- borderSide: BorderSide(color: colorScheme.primary, width: 2.0),
- ),
- focusedErrorBorder: OutlineInputBorder(
- borderSide: BorderSide(color: colorScheme.error, width: 2.0),
- ),
- hintStyle: widget.style.copyWith(color: colorScheme.onSurface.withOpacity(0.36)),
- // TODO(rami-a): Remove this logic once https://github.com/flutter/flutter/issues/54104 is fixed.
- errorStyle: const TextStyle(fontSize: 0.0, height: 0.0), // Prevent the error text from appearing.
- );
- }
- final Color unfocusedFillColor = timePickerTheme.hourMinuteColor ?? colorScheme.onSurface.withOpacity(0.12);
- // If screen reader is in use, make the hint text say hours/minutes.
- // Otherwise, remove the hint text when focused because the centered cursor
+ final InputDecorationTheme inputDecorationTheme = timePickerTheme.inputDecorationTheme ?? defaultTheme.inputDecorationTheme;
+ InputDecoration inputDecoration = const InputDecoration().applyDefaults(inputDecorationTheme);
+ // Remove the hint text when focused because the centered cursor
// appears odd above the hint text.
- //
- // TODO(rami-a): Once https://github.com/flutter/flutter/issues/67571 is
- // resolved, remove the window check for semantics being enabled on web.
- final String? hintText = MediaQuery.of(context).accessibleNavigation || WidgetsBinding.instance.window.semanticsEnabled
- ? widget.semanticHintText
- : (focusNode.hasFocus ? null : _formattedValue);
+ final String? hintText = focusNode.hasFocus ? null : _formattedValue;
+
+ // Because the fill color is specified in both the inputDecorationTheme and
+ // the TimePickerTheme, if there's one in the user's input decoration theme,
+ // use that. If not, but there's one in the user's
+ // timePickerTheme.hourMinuteColor, use that, and otherwise use the default.
+ // We ignore the value in the fillColor of the input decoration in the
+ // default theme here, but it's the same as the hourMinuteColor.
+ final Color startingFillColor =
+ timePickerTheme.inputDecorationTheme?.fillColor ??
+ timePickerTheme.hourMinuteColor ??
+ defaultTheme.hourMinuteColor;
+ final Color fillColor;
+ if (theme.useMaterial3) {
+ fillColor = MaterialStateProperty.resolveAs<Color>(
+ startingFillColor,
+ <MaterialState>{
+ if (focusNode.hasFocus) MaterialState.focused,
+ if (focusNode.hasFocus) MaterialState.selected,
+ },
+ );
+ } else {
+ fillColor = focusNode.hasFocus ? Colors.transparent : startingFillColor;
+ }
+
inputDecoration = inputDecoration.copyWith(
hintText: hintText,
- fillColor: focusNode.hasFocus ? Colors.transparent : inputDecorationTheme?.fillColor ?? unfocusedFillColor,
+ fillColor: fillColor,
);
- return SizedBox(
- height: _kTimePickerHeaderControlHeight,
+ final Set<MaterialState> states = <MaterialState>{
+ if (focusNode.hasFocus) MaterialState.focused,
+ if (focusNode.hasFocus) MaterialState.selected,
+ };
+ final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(
+ timePickerTheme.hourMinuteTextColor ?? defaultTheme.hourMinuteTextColor,
+ states,
+ );
+ final TextStyle effectiveStyle = MaterialStateProperty.resolveAs<TextStyle>(widget.style, states)
+ .copyWith(color: effectiveTextColor);
+
+ return SizedBox.fromSize(
+ size: alwaysUse24HourFormat ? defaultTheme.hourMinuteInputSize24Hour : defaultTheme.hourMinuteInputSize,
child: MediaQuery(
- data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+ data: MediaQuery.of(context).copyWith(textScaleFactor: 1),
child: UnmanagedRestorationScope(
bucket: bucket,
- child: TextFormField(
- restorationId: 'hour_minute_text_form_field',
- autofocus: widget.autofocus ?? false,
- expands: true,
- maxLines: null,
- inputFormatters: <TextInputFormatter>[
- LengthLimitingTextInputFormatter(2),
- ],
- focusNode: focusNode,
- textAlign: TextAlign.center,
- textInputAction: widget.inputAction,
- keyboardType: TextInputType.number,
- style: widget.style.copyWith(color: timePickerTheme.hourMinuteTextColor ?? colorScheme.onSurface),
- controller: controller.value,
- decoration: inputDecoration,
- validator: widget.validator,
- onEditingComplete: () => widget.onSavedSubmitted(controller.value.text),
- onSaved: widget.onSavedSubmitted,
- onFieldSubmitted: widget.onSavedSubmitted,
- onChanged: widget.onChanged,
+ child: Semantics(
+ label: widget.semanticHintText,
+ child: TextFormField(
+ restorationId: 'hour_minute_text_form_field',
+ autofocus: widget.autofocus ?? false,
+ expands: true,
+ maxLines: null,
+ inputFormatters: <TextInputFormatter>[
+ LengthLimitingTextInputFormatter(2),
+ ],
+ focusNode: focusNode,
+ textAlign: TextAlign.center,
+ textInputAction: widget.inputAction,
+ keyboardType: TextInputType.number,
+ style: effectiveStyle,
+ controller: controller.value,
+ decoration: inputDecoration,
+ validator: widget.validator,
+ onEditingComplete: () => widget.onSavedSubmitted(controller.value.text),
+ onSaved: widget.onSavedSubmitted,
+ onFieldSubmitted: widget.onSavedSubmitted,
+ onChanged: widget.onChanged,
+ ),
),
),
),
@@ -1870,15 +2153,13 @@
this.minuteLabelText,
this.restorationId,
this.initialEntryMode = TimePickerEntryMode.dial,
+ this.orientation,
this.onEntryModeChanged,
- }) : assert(initialTime != null);
+ });
/// The time initially selected when the dialog is shown.
final TimeOfDay initialTime;
- /// The entry mode for the picker. Whether it's text input or a dial.
- final TimePickerEntryMode initialEntryMode;
-
/// Optionally provide your own text for the cancel button.
///
/// If null, the button uses [MaterialLocalizations.cancelButtonLabel].
@@ -1915,6 +2196,22 @@
/// Flutter.
final String? restorationId;
+ /// The entry mode for the picker. Whether it's text input or a dial.
+ final TimePickerEntryMode initialEntryMode;
+
+ /// The optional [orientation] parameter sets the [Orientation] to use when
+ /// displaying the dialog.
+ ///
+ /// By default, the orientation is derived from the [MediaQueryData.size] of
+ /// the ambient [MediaQuery]. If the aspect of the size is tall, then
+ /// [Orientation.portrait] is used, if the size is wide, then
+ /// [Orientation.landscape] is used.
+ ///
+ /// Use this parameter to override the default and force the dialog to appear
+ /// in either portrait or landscape mode regardless of the aspect of the
+ /// [MediaQueryData.size].
+ final Orientation? orientation;
+
/// Callback called when the selected entry mode is changed.
final EntryModeChangeCallback? onEntryModeChanged;
@@ -1922,124 +2219,421 @@
State<TimePickerDialog> createState() => _TimePickerDialogState();
}
-// A restorable [TimePickerEntryMode] value.
-//
-// This serializes each entry as a unique `int` value.
-class _RestorableTimePickerEntryMode extends RestorableValue<TimePickerEntryMode> {
- _RestorableTimePickerEntryMode(
- TimePickerEntryMode defaultValue,
- ) : _defaultValue = defaultValue;
-
- final TimePickerEntryMode _defaultValue;
-
- @override
- TimePickerEntryMode createDefaultValue() => _defaultValue;
-
- @override
- void didUpdateValue(TimePickerEntryMode? oldValue) {
- assert(debugIsSerializableForRestoration(value.index));
- notifyListeners();
- }
-
- @override
- TimePickerEntryMode fromPrimitives(Object? data) => TimePickerEntryMode.values[data! as int];
-
- @override
- Object? toPrimitives() => value.index;
-}
-
-// A restorable [_RestorableTimePickerEntryMode] value.
-//
-// This serializes each entry as a unique `int` value.
-class _RestorableTimePickerMode extends RestorableValue<_TimePickerMode> {
- _RestorableTimePickerMode(
- _TimePickerMode defaultValue,
- ) : _defaultValue = defaultValue;
-
- final _TimePickerMode _defaultValue;
-
- @override
- _TimePickerMode createDefaultValue() => _defaultValue;
-
- @override
- void didUpdateValue(_TimePickerMode? oldValue) {
- assert(debugIsSerializableForRestoration(value.index));
- notifyListeners();
- }
-
- @override
- _TimePickerMode fromPrimitives(Object? data) => _TimePickerMode.values[data! as int];
-
- @override
- Object? toPrimitives() => value.index;
-}
-
-// A restorable [AutovalidateMode] value.
-//
-// This serializes each entry as a unique `int` value.
-class _RestorableAutovalidateMode extends RestorableValue<AutovalidateMode> {
- _RestorableAutovalidateMode(
- AutovalidateMode defaultValue,
- ) : _defaultValue = defaultValue;
-
- final AutovalidateMode _defaultValue;
-
- @override
- AutovalidateMode createDefaultValue() => _defaultValue;
-
- @override
- void didUpdateValue(AutovalidateMode? oldValue) {
- assert(debugIsSerializableForRestoration(value.index));
- notifyListeners();
- }
-
- @override
- AutovalidateMode fromPrimitives(Object? data) => AutovalidateMode.values[data! as int];
-
- @override
- Object? toPrimitives() => value.index;
-}
-
-// A restorable [_RestorableTimePickerEntryMode] value.
-//
-// This serializes each entry as a unique `int` value.
-//
-// This value can be null.
-class _RestorableTimePickerModeN extends RestorableValue<_TimePickerMode?> {
- _RestorableTimePickerModeN(
- _TimePickerMode? defaultValue,
- ) : _defaultValue = defaultValue;
-
- final _TimePickerMode? _defaultValue;
-
- @override
- _TimePickerMode? createDefaultValue() => _defaultValue;
-
- @override
- void didUpdateValue(_TimePickerMode? oldValue) {
- assert(debugIsSerializableForRestoration(value?.index));
- notifyListeners();
- }
-
- @override
- _TimePickerMode fromPrimitives(Object? data) => _TimePickerMode.values[data! as int];
-
- @override
- Object? toPrimitives() => value?.index;
-}
-
class _TimePickerDialogState extends State<TimePickerDialog> with RestorationMixin {
+ late final RestorableEnum<TimePickerEntryMode> _entryMode = RestorableEnum<TimePickerEntryMode>(widget.initialEntryMode, values: TimePickerEntryMode.values);
+ late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.initialTime);
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+ final RestorableEnum<AutovalidateMode> _autovalidateMode = RestorableEnum<AutovalidateMode>(AutovalidateMode.disabled, values: AutovalidateMode.values);
+ late final RestorableEnumN<Orientation> _orientation = RestorableEnumN<Orientation>(widget.orientation, values: Orientation.values);
- late final _RestorableTimePickerEntryMode _entryMode = _RestorableTimePickerEntryMode(widget.initialEntryMode);
- final _RestorableTimePickerMode _mode = _RestorableTimePickerMode(_TimePickerMode.hour);
- final _RestorableTimePickerModeN _lastModeAnnounced = _RestorableTimePickerModeN(null);
- final _RestorableAutovalidateMode _autovalidateMode = _RestorableAutovalidateMode(AutovalidateMode.disabled);
+ // Base sizes
+ static const Size _kTimePickerPortraitSize = Size(310, 468);
+ static const Size _kTimePickerLandscapeSize = Size(524, 342);
+ static const Size _kTimePickerLandscapeSizeM2 = Size(508, 300);
+ static const Size _kTimePickerInputSize = Size(312, 216);
+
+ // Absolute minimum dialog sizes, which is the point at which it begins
+ // scrolling to fit everything in.
+ static const Size _kTimePickerMinPortraitSize = Size(238, 326);
+ static const Size _kTimePickerMinLandscapeSize = Size(416, 248);
+ static const Size _kTimePickerMinInputSize = Size(312, 196);
+
+ @override
+ String? get restorationId => widget.restorationId;
+
+ @override
+ void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
+ registerForRestoration(_selectedTime, 'selected_time');
+ registerForRestoration(_entryMode, 'entry_mode');
+ registerForRestoration(_autovalidateMode, 'autovalidate_mode');
+ registerForRestoration(_orientation, 'orientation');
+ }
+
+ void _handleTimeChanged(TimeOfDay value) {
+ if (value != _selectedTime.value) {
+ setState(() {
+ _selectedTime.value = value;
+ });
+ }
+ }
+
+ void _handleEntryModeChanged(TimePickerEntryMode value) {
+ if (value != _entryMode.value) {
+ setState(() {
+ switch (_entryMode.value) {
+ case TimePickerEntryMode.dial:
+ _autovalidateMode.value = AutovalidateMode.disabled;
+ break;
+ case TimePickerEntryMode.input:
+ _formKey.currentState!.save();
+ break;
+ case TimePickerEntryMode.dialOnly:
+ break;
+ case TimePickerEntryMode.inputOnly:
+ break;
+ }
+ _entryMode.value = value;
+ widget.onEntryModeChanged?.call(value);
+ });
+ }
+ }
+
+ void _toggleEntryMode() {
+ switch (_entryMode.value) {
+ case TimePickerEntryMode.dial:
+ _handleEntryModeChanged(TimePickerEntryMode.input);
+ break;
+ case TimePickerEntryMode.input:
+ _handleEntryModeChanged(TimePickerEntryMode.dial);
+ break;
+ case TimePickerEntryMode.dialOnly:
+ case TimePickerEntryMode.inputOnly:
+ FlutterError('Can not change entry mode from $_entryMode');
+ break;
+ }
+ }
+
+ void _handleCancel() {
+ Navigator.pop(context);
+ }
+
+ void _handleOk() {
+ if (_entryMode.value == TimePickerEntryMode.input || _entryMode.value == TimePickerEntryMode.inputOnly) {
+ final FormState form = _formKey.currentState!;
+ if (!form.validate()) {
+ setState(() {
+ _autovalidateMode.value = AutovalidateMode.always;
+ });
+ return;
+ }
+ form.save();
+ }
+ Navigator.pop(context, _selectedTime.value);
+ }
+
+ Size _minDialogSize(BuildContext context, {required bool useMaterial3}) {
+ final Orientation orientation = _orientation.value ?? MediaQuery.orientationOf(context);
+
+ switch (_entryMode.value) {
+ case TimePickerEntryMode.dial:
+ case TimePickerEntryMode.dialOnly:
+ switch (orientation) {
+ case Orientation.portrait:
+ return _kTimePickerMinPortraitSize;
+ case Orientation.landscape:
+ return _kTimePickerMinLandscapeSize;
+ }
+ case TimePickerEntryMode.input:
+ case TimePickerEntryMode.inputOnly:
+ final MaterialLocalizations localizations = MaterialLocalizations.of(context);
+ final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context));
+ final double timePickerWidth;
+ switch(timeOfDayFormat) {
+ case TimeOfDayFormat.HH_colon_mm:
+ case TimeOfDayFormat.HH_dot_mm:
+ case TimeOfDayFormat.frenchCanadian:
+ case TimeOfDayFormat.H_colon_mm:
+ final _TimePickerDefaults defaultTheme = useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ timePickerWidth = _kTimePickerMinInputSize.width - defaultTheme.dayPeriodPortraitSize.width - 12;
+ break;
+ case TimeOfDayFormat.a_space_h_colon_mm:
+ case TimeOfDayFormat.h_colon_mm_space_a:
+ timePickerWidth = _kTimePickerMinInputSize.width;
+ break;
+ }
+ return Size(timePickerWidth, _kTimePickerMinInputSize.height);
+ }
+ }
+
+ Size _dialogSize(BuildContext context, {required bool useMaterial3}) {
+ final Orientation orientation = _orientation.value ?? MediaQuery.orientationOf(context);
+ // Constrain the textScaleFactor to prevent layout issues. Since only some
+ // parts of the time picker scale up with textScaleFactor, we cap the factor
+ // to 1.1 as that provides enough space to reasonably fit all the content.
+ final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 1.1);
+
+ final Size timePickerSize;
+ switch (_entryMode.value) {
+ case TimePickerEntryMode.dial:
+ case TimePickerEntryMode.dialOnly:
+ switch (orientation) {
+ case Orientation.portrait:
+ timePickerSize = _kTimePickerPortraitSize;
+ break;
+ case Orientation.landscape:
+ timePickerSize = Size(
+ _kTimePickerLandscapeSize.width * textScaleFactor,
+ useMaterial3 ? _kTimePickerLandscapeSize.height : _kTimePickerLandscapeSizeM2.height
+ );
+ break;
+ }
+ break;
+ case TimePickerEntryMode.input:
+ case TimePickerEntryMode.inputOnly:
+ final MaterialLocalizations localizations = MaterialLocalizations.of(context);
+ final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context));
+ final double timePickerWidth;
+ switch(timeOfDayFormat) {
+ case TimeOfDayFormat.HH_colon_mm:
+ case TimeOfDayFormat.HH_dot_mm:
+ case TimeOfDayFormat.frenchCanadian:
+ case TimeOfDayFormat.H_colon_mm:
+ final _TimePickerDefaults defaultTheme = useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ timePickerWidth = _kTimePickerInputSize.width - defaultTheme.dayPeriodPortraitSize.width - 12;
+ break;
+ case TimeOfDayFormat.a_space_h_colon_mm:
+ case TimeOfDayFormat.h_colon_mm_space_a:
+ timePickerWidth = _kTimePickerInputSize.width;
+ break;
+ }
+ timePickerSize = Size(timePickerWidth, _kTimePickerInputSize.height);
+ break;
+ }
+ return Size(timePickerSize.width, timePickerSize.height * textScaleFactor);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ assert(debugCheckHasMediaQuery(context));
+ final ThemeData theme = Theme.of(context);
+ final TimePickerThemeData pickerTheme = TimePickerTheme.of(context);
+ final _TimePickerDefaults defaultTheme = theme.useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ final ShapeBorder shape = pickerTheme.shape ?? defaultTheme.shape;
+ final Color entryModeIconColor = pickerTheme.entryModeIconColor ?? defaultTheme.entryModeIconColor;
+ final MaterialLocalizations localizations = MaterialLocalizations.of(context);
+
+ final Widget actions = Padding(
+ padding: EdgeInsetsDirectional.only(start: theme.useMaterial3 ? 0 : 4),
+ child: Row(
+ children: <Widget>[
+ if (_entryMode.value == TimePickerEntryMode.dial || _entryMode.value == TimePickerEntryMode.input)
+ IconButton(
+ // In material3 mode, we want to use the color as part of the
+ // button style which applies its own opacity. In material2 mode,
+ // we want to use the color as the color, which already includes
+ // the opacity.
+ color: theme.useMaterial3 ? null : entryModeIconColor,
+ style: theme.useMaterial3 ? IconButton.styleFrom(foregroundColor: entryModeIconColor) : null,
+ onPressed: _toggleEntryMode,
+ icon: Icon(_entryMode.value == TimePickerEntryMode.dial ? Icons.keyboard_outlined : Icons.access_time),
+ tooltip: _entryMode.value == TimePickerEntryMode.dial
+ ? MaterialLocalizations.of(context).inputTimeModeButtonLabel
+ : MaterialLocalizations.of(context).dialModeButtonLabel,
+ ),
+ Expanded(
+ child: Container(
+ alignment: AlignmentDirectional.centerEnd,
+ constraints: const BoxConstraints(minHeight: 36),
+ child: OverflowBar(
+ spacing: 8,
+ overflowAlignment: OverflowBarAlignment.end,
+ children: <Widget>[
+ TextButton(
+ onPressed: _handleCancel,
+ child: Text(widget.cancelText ??
+ (theme.useMaterial3
+ ? localizations.cancelButtonLabel
+ : localizations.cancelButtonLabel.toUpperCase())),
+ ),
+ TextButton(
+ onPressed: _handleOk,
+ child: Text(widget.confirmText ?? localizations.okButtonLabel),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ final Offset tapTargetSizeOffset;
+ switch (theme.materialTapTargetSize) {
+ case MaterialTapTargetSize.padded:
+ tapTargetSizeOffset = Offset.zero;
+ break;
+ case MaterialTapTargetSize.shrinkWrap:
+ // _dialogSize returns "padded" sizes.
+ tapTargetSizeOffset = const Offset(0, -12);
+ break;
+ }
+
+ final Size dialogSize = _dialogSize(context, useMaterial3: theme.useMaterial3) + tapTargetSizeOffset;
+ final Size minDialogSize = _minDialogSize(context, useMaterial3: theme.useMaterial3) + tapTargetSizeOffset;
+ return Dialog(
+ shape: shape,
+ elevation: pickerTheme.elevation ?? defaultTheme.elevation,
+ backgroundColor: pickerTheme.backgroundColor ?? defaultTheme.backgroundColor,
+ insetPadding: EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: (_entryMode.value == TimePickerEntryMode.input || _entryMode.value == TimePickerEntryMode.inputOnly) ? 0 : 24,
+ ),
+ child: Padding(
+ padding: pickerTheme.padding ?? defaultTheme.padding,
+ child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
+ final Size constrainedSize = constraints.constrain(dialogSize);
+ final Size allowedSize = Size(
+ constrainedSize.width < minDialogSize.width ? minDialogSize.width : constrainedSize.width,
+ constrainedSize.height < minDialogSize.height ? minDialogSize.height : constrainedSize.height,
+ );
+ return SingleChildScrollView(
+ restorationId: 'time_picker_scroll_view_horizontal',
+ scrollDirection: Axis.horizontal,
+ child: SingleChildScrollView(
+ restorationId: 'time_picker_scroll_view_vertical',
+ child: AnimatedContainer(
+ width: allowedSize.width,
+ height: allowedSize.height,
+ duration: _kDialogSizeAnimationDuration,
+ curve: Curves.easeIn,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Expanded(
+ child: Form(
+ key: _formKey,
+ autovalidateMode: _autovalidateMode.value,
+ child: _TimePicker(
+ time: widget.initialTime,
+ onTimeChanged: _handleTimeChanged,
+ helpText: widget.helpText,
+ cancelText: widget.cancelText,
+ confirmText: widget.confirmText,
+ errorInvalidText: widget.errorInvalidText,
+ hourLabelText: widget.hourLabelText,
+ minuteLabelText: widget.minuteLabelText,
+ restorationId: 'time_picker',
+ entryMode: _entryMode.value,
+ orientation: widget.orientation,
+ onEntryModeChanged: _handleEntryModeChanged,
+ ),
+ ),
+ ),
+ actions,
+ ],
+ ),
+ ),
+ ),
+ );
+ }),
+ ),
+ );
+ }
+}
+
+// The _TimePicker widget is constructed so that in the future we could expose
+// this as a public API for embedding time pickers into other non-dialog
+// widgets, once we're sure we want to support that.
+
+/// A Time Picker widget that can be embedded into another widget.
+class _TimePicker extends StatefulWidget {
+ /// Creates a const Material Design time picker.
+ const _TimePicker({
+ required this.time,
+ required this.onTimeChanged,
+ this.helpText,
+ this.cancelText,
+ this.confirmText,
+ this.errorInvalidText,
+ this.hourLabelText,
+ this.minuteLabelText,
+ this.restorationId,
+ this.entryMode = TimePickerEntryMode.dial,
+ this.orientation,
+ this.onEntryModeChanged,
+ });
+
+ /// Optionally provide your own text for the help text at the top of the
+ /// control.
+ ///
+ /// If null, the widget uses [MaterialLocalizations.timePickerDialHelpText]
+ /// when the [entryMode] is [TimePickerEntryMode.dial], and
+ /// [MaterialLocalizations.timePickerInputHelpText] when the [entryMode] is
+ /// [TimePickerEntryMode.input].
+ final String? helpText;
+
+ /// Optionally provide your own text for the cancel button.
+ ///
+ /// If null, the button uses [MaterialLocalizations.cancelButtonLabel].
+ final String? cancelText;
+
+ /// Optionally provide your own text for the confirm button.
+ ///
+ /// If null, the button uses [MaterialLocalizations.okButtonLabel].
+ final String? confirmText;
+
+ /// Optionally provide your own validation error text.
+ final String? errorInvalidText;
+
+ /// Optionally provide your own hour label text.
+ final String? hourLabelText;
+
+ /// Optionally provide your own minute label text.
+ final String? minuteLabelText;
+
+ /// Restoration ID to save and restore the state of the [TimePickerDialog].
+ ///
+ /// If it is non-null, the time picker will persist and restore the
+ /// dialog's state.
+ ///
+ /// The state of this widget is persisted in a [RestorationBucket] claimed
+ /// from the surrounding [RestorationScope] using the provided restoration ID.
+ ///
+ /// See also:
+ ///
+ /// * [RestorationManager], which explains how state restoration works in
+ /// Flutter.
+ final String? restorationId;
+
+ /// The initial entry mode for the picker. Whether it's text input or a dial.
+ final TimePickerEntryMode entryMode;
+
+ /// The currently selected time of day.
+ final TimeOfDay time;
+
+ final ValueChanged<TimeOfDay>? onTimeChanged;
+
+ /// The optional [orientation] parameter sets the [Orientation] to use when
+ /// displaying the dialog.
+ ///
+ /// By default, the orientation is derived from the [MediaQueryData.size] of
+ /// the ambient [MediaQuery]. If the aspect of the size is tall, then
+ /// [Orientation.portrait] is used, if the size is wide, then
+ /// [Orientation.landscape] is used.
+ ///
+ /// Use this parameter to override the default and force the dialog to appear
+ /// in either portrait or landscape mode regardless of the aspect of the
+ /// [MediaQueryData.size].
+ final Orientation? orientation;
+
+ /// Callback called when the selected entry mode is changed.
+ final EntryModeChangeCallback? onEntryModeChanged;
+
+ @override
+ State<_TimePicker> createState() => _TimePickerState();
+}
+
+class _TimePickerState extends State<_TimePicker> with RestorationMixin {
+ Timer? _vibrateTimer;
+ late MaterialLocalizations localizations;
+ final RestorableEnum<_HourMinuteMode> _hourMinuteMode =
+ RestorableEnum<_HourMinuteMode>(_HourMinuteMode.hour, values: _HourMinuteMode.values);
+ final RestorableEnumN<_HourMinuteMode> _lastModeAnnounced =
+ RestorableEnumN<_HourMinuteMode>(null, values: _HourMinuteMode.values);
final RestorableBoolN _autofocusHour = RestorableBoolN(null);
final RestorableBoolN _autofocusMinute = RestorableBoolN(null);
final RestorableBool _announcedInitialTime = RestorableBool(false);
+ late final RestorableEnumN<Orientation> _orientation =
+ RestorableEnumN<Orientation>(widget.orientation, values: Orientation.values);
+ RestorableTimeOfDay get selectedTime => _selectedTime;
+ late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.time);
- late final VoidCallback _entryModeListener;
+ @override
+ void dispose() {
+ _vibrateTimer?.cancel();
+ _vibrateTimer = null;
+ super.dispose();
+ }
@override
void didChangeDependencies() {
@@ -2050,10 +2644,18 @@
}
@override
- void initState() {
- super.initState();
- _entryModeListener = () => widget.onEntryModeChanged?.call(_entryMode.value);
- _entryMode.addListener(_entryModeListener);
+ void didUpdateWidget (_TimePicker oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.orientation != widget.orientation) {
+ _orientation.value = widget.orientation;
+ }
+ if (oldWidget.time != widget.time) {
+ _selectedTime.value = widget.time;
+ }
+ }
+
+ void _setEntryMode(TimePickerEntryMode mode){
+ widget.onEntryModeChanged?.call(mode);
}
@override
@@ -2061,22 +2663,15 @@
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
- registerForRestoration(_entryMode, 'entry_mode');
- registerForRestoration(_mode, 'mode');
+ registerForRestoration(_hourMinuteMode, 'hour_minute_mode');
registerForRestoration(_lastModeAnnounced, 'last_mode_announced');
- registerForRestoration(_autovalidateMode, 'autovalidateMode');
registerForRestoration(_autofocusHour, 'autofocus_hour');
registerForRestoration(_autofocusMinute, 'autofocus_minute');
registerForRestoration(_announcedInitialTime, 'announced_initial_time');
registerForRestoration(_selectedTime, 'selected_time');
+ registerForRestoration(_orientation, 'orientation');
}
- RestorableTimeOfDay get selectedTime => _selectedTime;
- late final RestorableTimeOfDay _selectedTime = RestorableTimeOfDay(widget.initialTime);
-
- Timer? _vibrateTimer;
- late MaterialLocalizations localizations;
-
void _vibrate() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
@@ -2095,50 +2690,50 @@
}
}
- void _handleModeChanged(_TimePickerMode mode) {
+ void _handleHourMinuteModeChanged(_HourMinuteMode mode) {
_vibrate();
setState(() {
- _mode.value = mode;
+ _hourMinuteMode.value = mode;
_announceModeOnce();
});
}
void _handleEntryModeToggle() {
setState(() {
- switch (_entryMode.value) {
+ TimePickerEntryMode newMode = widget.entryMode;
+ switch (widget.entryMode) {
case TimePickerEntryMode.dial:
- _autovalidateMode.value = AutovalidateMode.disabled;
- _entryMode.value = TimePickerEntryMode.input;
+ newMode = TimePickerEntryMode.input;
break;
case TimePickerEntryMode.input:
- _formKey.currentState!.save();
_autofocusHour.value = false;
_autofocusMinute.value = false;
- _entryMode.value = TimePickerEntryMode.dial;
+ newMode = TimePickerEntryMode.dial;
break;
case TimePickerEntryMode.dialOnly:
case TimePickerEntryMode.inputOnly:
- FlutterError('Can not change entry mode from $_entryMode');
+ FlutterError('Can not change entry mode from ${widget.entryMode}');
break;
}
+ _setEntryMode(newMode);
});
}
void _announceModeOnce() {
- if (_lastModeAnnounced.value == _mode.value) {
+ if (_lastModeAnnounced.value == _hourMinuteMode.value) {
// Already announced it.
return;
}
- switch (_mode.value) {
- case _TimePickerMode.hour:
+ switch (_hourMinuteMode.value) {
+ case _HourMinuteMode.hour:
_announceToAccessibility(context, localizations.timePickerHourModeAnnouncement);
break;
- case _TimePickerMode.minute:
+ case _HourMinuteMode.minute:
_announceToAccessibility(context, localizations.timePickerMinuteModeAnnouncement);
break;
}
- _lastModeAnnounced.value = _mode.value;
+ _lastModeAnnounced.value = _hourMinuteMode.value;
}
void _announceInitialTimeOnce() {
@@ -2146,11 +2741,10 @@
return;
}
- final MediaQueryData media = MediaQuery.of(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
_announceToAccessibility(
context,
- localizations.formatTimeOfDay(widget.initialTime, alwaysUse24HourFormat: media.alwaysUse24HourFormat),
+ localizations.formatTimeOfDay(_selectedTime.value, alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context)),
);
_announcedInitialTime.value = true;
}
@@ -2159,6 +2753,7 @@
_vibrate();
setState(() {
_selectedTime.value = value;
+ widget.onTimeChanged?.call(value);
});
}
@@ -2174,161 +2769,94 @@
void _handleHourSelected() {
setState(() {
- _mode.value = _TimePickerMode.minute;
+ _hourMinuteMode.value = _HourMinuteMode.minute;
});
}
- void _handleCancel() {
- Navigator.pop(context);
- }
-
- void _handleOk() {
- if (_entryMode.value == TimePickerEntryMode.input || _entryMode.value == TimePickerEntryMode.inputOnly) {
- final FormState form = _formKey.currentState!;
- if (!form.validate()) {
- setState(() { _autovalidateMode.value = AutovalidateMode.always; });
- return;
- }
- form.save();
- }
- Navigator.pop(context, _selectedTime.value);
- }
-
- Size _dialogSize(BuildContext context) {
- final Orientation orientation = MediaQuery.of(context).orientation;
- final ThemeData theme = Theme.of(context);
- // Constrain the textScaleFactor to prevent layout issues. Since only some
- // parts of the time picker scale up with textScaleFactor, we cap the factor
- // to 1.1 as that provides enough space to reasonably fit all the content.
- final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.1);
-
- final double timePickerWidth;
- final double timePickerHeight;
- switch (_entryMode.value) {
- case TimePickerEntryMode.dial:
- case TimePickerEntryMode.dialOnly:
- switch (orientation) {
- case Orientation.portrait:
- timePickerWidth = _kTimePickerWidthPortrait;
- timePickerHeight = theme.materialTapTargetSize == MaterialTapTargetSize.padded
- ? _kTimePickerHeightPortrait
- : _kTimePickerHeightPortraitCollapsed;
- break;
- case Orientation.landscape:
- timePickerWidth = _kTimePickerWidthLandscape * textScaleFactor;
- timePickerHeight = theme.materialTapTargetSize == MaterialTapTargetSize.padded
- ? _kTimePickerHeightLandscape
- : _kTimePickerHeightLandscapeCollapsed;
- break;
- }
- break;
- case TimePickerEntryMode.input:
- case TimePickerEntryMode.inputOnly:
- timePickerWidth = _kTimePickerWidthPortrait;
- timePickerHeight = _kTimePickerHeightInput;
- break;
- }
- return Size(timePickerWidth, timePickerHeight * textScaleFactor);
- }
-
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData media = MediaQuery.of(context);
- final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat);
- final bool use24HourDials = hourFormat(of: timeOfDayFormat) != HourFormat.h;
+ final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context));
final ThemeData theme = Theme.of(context);
- final ShapeBorder shape = TimePickerTheme.of(context).shape ?? _kDefaultShape;
- final Orientation orientation = media.orientation;
+ final _TimePickerDefaults defaultTheme = theme.useMaterial3 ? _TimePickerDefaultsM3(context) : _TimePickerDefaultsM2(context);
+ final Orientation orientation = _orientation.value ?? MediaQuery.orientationOf(context);
+ final HourFormat timeOfDayHour = hourFormat(of: timeOfDayFormat);
+ final _HourDialType hourMode;
+ switch (timeOfDayHour) {
+ case HourFormat.HH:
+ case HourFormat.H:
+ hourMode = theme.useMaterial3 ? _HourDialType.twentyFourHourDoubleRing : _HourDialType.twentyFourHour;
+ break;
+ case HourFormat.h:
+ hourMode = _HourDialType.twelveHour;
+ break;
+ }
- final Widget actions = Row(
- children: <Widget>[
- const SizedBox(width: 10.0),
- if (_entryMode.value == TimePickerEntryMode.dial || _entryMode.value == TimePickerEntryMode.input)
- IconButton(
- color: TimePickerTheme.of(context).entryModeIconColor ?? theme.colorScheme.onSurface.withOpacity(
- theme.colorScheme.brightness == Brightness.dark ? 1.0 : 0.6,
- ),
- onPressed: _handleEntryModeToggle,
- icon: Icon(_entryMode.value == TimePickerEntryMode.dial ? Icons.keyboard : Icons.access_time),
- tooltip: _entryMode.value == TimePickerEntryMode.dial
- ? MaterialLocalizations.of(context).inputTimeModeButtonLabel
- : MaterialLocalizations.of(context).dialModeButtonLabel,
- ),
- Expanded(
- child: Container(
- alignment: AlignmentDirectional.centerEnd,
- constraints: const BoxConstraints(minHeight: 52.0),
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: OverflowBar(
- spacing: 8,
- overflowAlignment: OverflowBarAlignment.end,
- children: <Widget>[
- TextButton(
- onPressed: _handleCancel,
- child: Text(widget.cancelText ?? (
- theme.useMaterial3
- ? localizations.cancelButtonLabel
- : localizations.cancelButtonLabel.toUpperCase()
- )),
- ),
- TextButton(
- onPressed: _handleOk,
- child: Text(widget.confirmText ?? localizations.okButtonLabel),
- ),
- ],
- ),
- ),
- ),
- ],
- );
-
+ final String helpText;
final Widget picker;
- switch (_entryMode.value) {
+ switch (widget.entryMode) {
case TimePickerEntryMode.dial:
case TimePickerEntryMode.dialOnly:
+ helpText = widget.helpText ?? (theme.useMaterial3
+ ? localizations.timePickerDialHelpText
+ : localizations.timePickerDialHelpText.toUpperCase());
+
+ final EdgeInsetsGeometry dialPadding;
+ switch (orientation) {
+ case Orientation.portrait:
+ dialPadding = const EdgeInsets.only(left: 12, right: 12, top: 36);
+ break;
+ case Orientation.landscape:
+ switch (theme.materialTapTargetSize) {
+ case MaterialTapTargetSize.padded:
+ dialPadding = const EdgeInsetsDirectional.only(start: 64);
+ break;
+ case MaterialTapTargetSize.shrinkWrap:
+ dialPadding = const EdgeInsetsDirectional.only(start: 64);
+ break;
+ }
+ break;
+ }
final Widget dial = Padding(
- padding: orientation == Orientation.portrait ? const EdgeInsets.symmetric(horizontal: 36, vertical: 24) : const EdgeInsets.all(24),
+ padding: dialPadding,
child: ExcludeSemantics(
- child: AspectRatio(
- aspectRatio: 1.0,
- child: _Dial(
- mode: _mode.value,
- use24HourDials: use24HourDials,
- selectedTime: _selectedTime.value,
- onChanged: _handleTimeChanged,
- onHourSelected: _handleHourSelected,
+ child: SizedBox.fromSize(
+ size: defaultTheme.dialSize,
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: _Dial(
+ hourMinuteMode: _hourMinuteMode.value,
+ hourDialType: hourMode,
+ selectedTime: _selectedTime.value,
+ onChanged: _handleTimeChanged,
+ onHourSelected: _handleHourSelected,
+ ),
),
),
),
);
- final Widget header = _TimePickerHeader(
- selectedTime: _selectedTime.value,
- mode: _mode.value,
- orientation: orientation,
- onModeChanged: _handleModeChanged,
- onChanged: _handleTimeChanged,
- onHourDoubleTapped: _handleHourDoubleTapped,
- onMinuteDoubleTapped: _handleMinuteDoubleTapped,
- use24HourDials: use24HourDials,
- helpText: widget.helpText,
- );
-
switch (orientation) {
case Orientation.portrait:
picker = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
- header,
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: theme.useMaterial3 ? 0 : 16),
+ child: _TimePickerHeader(helpText: helpText),
+ ),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// Dial grows and shrinks with the available space.
- Expanded(child: dial),
- actions,
+ Expanded(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: theme.useMaterial3 ? 0 : 16),
+ child: dial,
+ ),
+ ),
],
),
),
@@ -2339,14 +2867,17 @@
picker = Column(
children: <Widget>[
Expanded(
- child: Row(
- children: <Widget>[
- header,
- Expanded(child: dial),
- ],
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: theme.useMaterial3 ? 0 : 16),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ _TimePickerHeader(helpText: helpText),
+ Expanded(child: dial),
+ ],
+ ),
),
),
- actions,
],
);
break;
@@ -2354,58 +2885,43 @@
break;
case TimePickerEntryMode.input:
case TimePickerEntryMode.inputOnly:
- picker = Form(
- key: _formKey,
- autovalidateMode: _autovalidateMode.value,
- child: SingleChildScrollView(
- restorationId: 'time_picker_scroll_view',
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: <Widget>[
- _TimePickerInput(
- initialSelectedTime: _selectedTime.value,
- helpText: widget.helpText,
- errorInvalidText: widget.errorInvalidText,
- hourLabelText: widget.hourLabelText,
- minuteLabelText: widget.minuteLabelText,
- autofocusHour: _autofocusHour.value,
- autofocusMinute: _autofocusMinute.value,
- onChanged: _handleTimeChanged,
- restorationId: 'time_picker_input',
- ),
- actions,
- ],
+ final String helpText = widget.helpText ?? (theme.useMaterial3
+ ? localizations.timePickerInputHelpText
+ : localizations.timePickerInputHelpText.toUpperCase());
+
+ picker = Column(
+ mainAxisSize: MainAxisSize.min,
+ children: <Widget>[
+ _TimePickerInput(
+ initialSelectedTime: _selectedTime.value,
+ errorInvalidText: widget.errorInvalidText,
+ hourLabelText: widget.hourLabelText,
+ minuteLabelText: widget.minuteLabelText,
+ helpText: helpText,
+ autofocusHour: _autofocusHour.value,
+ autofocusMinute: _autofocusMinute.value,
+ restorationId: 'time_picker_input',
),
- ),
+ ],
);
- break;
}
-
- final Size dialogSize = _dialogSize(context);
- return Dialog(
- shape: shape,
- backgroundColor: TimePickerTheme.of(context).backgroundColor ?? theme.colorScheme.surface,
- insetPadding: EdgeInsets.symmetric(
- horizontal: 16.0,
- vertical: (_entryMode.value == TimePickerEntryMode.input || _entryMode.value == TimePickerEntryMode.inputOnly) ? 0.0 : 24.0,
- ),
- child: AnimatedContainer(
- width: dialogSize.width,
- height: dialogSize.height,
- duration: _kDialogSizeAnimationDuration,
- curve: Curves.easeIn,
- child: picker,
- ),
+ return _TimePickerModel(
+ entryMode: widget.entryMode,
+ selectedTime: _selectedTime.value,
+ hourMinuteMode: _hourMinuteMode.value,
+ orientation: orientation,
+ onHourMinuteModeChanged: _handleHourMinuteModeChanged,
+ onHourDoubleTapped: _handleHourDoubleTapped,
+ onMinuteDoubleTapped: _handleMinuteDoubleTapped,
+ hourDialType: hourMode,
+ onSelectedTimeChanged: _handleTimeChanged,
+ useMaterial3: theme.useMaterial3,
+ use24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context),
+ theme: TimePickerTheme.of(context),
+ defaultTheme: defaultTheme,
+ child: picker,
);
}
-
- @override
- void dispose() {
- _vibrateTimer?.cancel();
- _vibrateTimer = null;
- _entryMode.removeListener(_entryModeListener);
- super.dispose();
- }
}
/// Shows a dialog containing a Material Design time picker.
@@ -2413,8 +2929,7 @@
/// The returned Future resolves to the time selected by the user when the user
/// closes the dialog. If the user cancels the dialog, null is returned.
///
-/// {@tool snippet}
-/// Show a dialog with [initialTime] equal to the current time.
+/// {@tool snippet} Show a dialog with [initialTime] equal to the current time.
///
/// ```dart
/// Future<TimeOfDay?> selectedTime = showTimePicker(
@@ -2424,29 +2939,35 @@
/// ```
/// {@end-tool}
///
-/// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
-/// [showDialog], the documentation for which discusses how it is used.
+/// The [context], [useRootNavigator] and [routeSettings] arguments are passed
+/// to [showDialog], the documentation for which discusses how it is used.
///
-/// The [builder] parameter can be used to wrap the dialog widget
-/// to add inherited widgets like [Localizations.override],
-/// [Directionality], or [MediaQuery].
+/// The [builder] parameter can be used to wrap the dialog widget to add
+/// inherited widgets like [Localizations.override], [Directionality], or
+/// [MediaQuery].
///
-/// The `initialEntryMode` parameter can be used to
-/// determine the initial time entry selection of the picker (either a clock
-/// dial or text input).
+/// The `initialEntryMode` parameter can be used to determine the initial time
+/// entry selection of the picker (either a clock dial or text input).
///
/// Optional strings for the [helpText], [cancelText], [errorInvalidText],
/// [hourLabelText], [minuteLabelText] and [confirmText] can be provided to
/// override the default values.
///
+/// The optional [orientation] parameter sets the [Orientation] to use when
+/// displaying the dialog. By default, the orientation is derived from the
+/// [MediaQueryData.size] of the ambient [MediaQuery]: wide sizes use the
+/// landscape orientation, and tall sizes use the portrait orientation. Use this
+/// parameter to override the default and force the dialog to appear in either
+/// portrait or landscape mode.
+///
/// {@macro flutter.widgets.RawDialogRoute}
///
/// By default, the time picker gets its colors from the overall theme's
/// [ColorScheme]. The time picker can be further customized by providing a
/// [TimePickerThemeData] to the overall theme.
///
-/// {@tool snippet}
-/// Show a dialog with the text direction overridden to be [TextDirection.rtl].
+/// {@tool snippet} Show a dialog with the text direction overridden to be
+/// [TextDirection.rtl].
///
/// ```dart
/// Future<TimeOfDay?> selectedTimeRTL = showTimePicker(
@@ -2462,8 +2983,8 @@
/// ```
/// {@end-tool}
///
-/// {@tool snippet}
-/// Show a dialog with time unconditionally displayed in 24 hour format.
+/// {@tool snippet} Show a dialog with time unconditionally displayed in 24 hour
+/// format.
///
/// ```dart
/// Future<TimeOfDay?> selectedTime24Hour = showTimePicker(
@@ -2479,14 +3000,21 @@
/// ```
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This example illustrates how to open a time picker, and allows exploring
+/// some of the variations in the types of time pickers that may be shown.
+///
+/// ** See code in examples/api/lib/material/time_picker/show_time_picker.0.dart **
+/// {@end-tool}
+///
/// See also:
///
-/// * [showDatePicker], which shows a dialog that contains a Material Design
-/// date picker.
-/// * [TimePickerThemeData], which allows you to customize the colors,
-/// typography, and shape of the time picker.
-/// * [DisplayFeatureSubScreen], which documents the specifics of how
-/// [DisplayFeature]s can split the screen into sub-screens.
+/// * [showDatePicker], which shows a dialog that contains a Material Design
+/// date picker.
+/// * [TimePickerThemeData], which allows you to customize the colors,
+/// typography, and shape of the time picker.
+/// * [DisplayFeatureSubScreen], which documents the specifics of how
+/// [DisplayFeature]s can split the screen into sub-screens.
Future<TimeOfDay?> showTimePicker({
required BuildContext context,
required TimeOfDay initialTime,
@@ -2502,11 +3030,8 @@
RouteSettings? routeSettings,
EntryModeChangeCallback? onEntryModeChanged,
Offset? anchorPoint,
+ Orientation? orientation,
}) async {
- assert(context != null);
- assert(initialTime != null);
- assert(useRootNavigator != null);
- assert(initialEntryMode != null);
assert(debugCheckHasMaterialLocalizations(context));
final Widget dialog = TimePickerDialog(
@@ -2518,6 +3043,7 @@
errorInvalidText: errorInvalidText,
hourLabelText: hourLabelText,
minuteLabelText: minuteLabelText,
+ orientation: orientation,
onEntryModeChanged: onEntryModeChanged,
);
return showDialog<TimeOfDay>(
@@ -2534,3 +3060,661 @@
void _announceToAccessibility(BuildContext context, String message) {
SemanticsService.announce(message, Directionality.of(context));
}
+
+// An abstract base class for the M2 and M3 defaults below, so that their return
+// types can be non-nullable.
+abstract class _TimePickerDefaults extends TimePickerThemeData {
+ @override
+ Color get backgroundColor;
+
+ @override
+ ButtonStyle get cancelButtonStyle;
+
+ @override
+ ButtonStyle get confirmButtonStyle;
+
+ @override
+ BorderSide get dayPeriodBorderSide;
+
+ @override
+ Color get dayPeriodColor;
+
+ @override
+ OutlinedBorder get dayPeriodShape;
+
+ Size get dayPeriodInputSize;
+ Size get dayPeriodLandscapeSize;
+ Size get dayPeriodPortraitSize;
+
+ @override
+ Color get dayPeriodTextColor;
+
+ @override
+ TextStyle get dayPeriodTextStyle;
+
+ @override
+ Color get dialBackgroundColor;
+
+ @override
+ Color get dialHandColor;
+
+ // Sizes that are generated from the tokens, but these aren't ones we're ready
+ // to expose in the theme.
+ Size get dialSize;
+ double get handWidth;
+ double get dotRadius;
+ double get centerRadius;
+
+ @override
+ Color get dialTextColor;
+
+ @override
+ TextStyle get dialTextStyle;
+
+ @override
+ double get elevation;
+
+ @override
+ Color get entryModeIconColor;
+
+ @override
+ TextStyle get helpTextStyle;
+
+ @override
+ Color get hourMinuteColor;
+
+ @override
+ ShapeBorder get hourMinuteShape;
+
+ Size get hourMinuteSize;
+ Size get hourMinuteSize24Hour;
+ Size get hourMinuteInputSize;
+ Size get hourMinuteInputSize24Hour;
+
+ @override
+ Color get hourMinuteTextColor;
+
+ @override
+ TextStyle get hourMinuteTextStyle;
+
+ @override
+ InputDecorationTheme get inputDecorationTheme;
+
+ @override
+ EdgeInsetsGeometry get padding;
+
+ @override
+ ShapeBorder get shape;
+}
+
+// These theme defaults are not auto-generated: they match the values for the
+// Material 2 spec, which are not expected to change.
+class _TimePickerDefaultsM2 extends _TimePickerDefaults {
+ _TimePickerDefaultsM2(this.context) : super();
+
+ final BuildContext context;
+
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+ late final TextTheme _textTheme = Theme.of(context).textTheme;
+ static const OutlinedBorder _kDefaultShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4)));
+
+ @override
+ Color get backgroundColor {
+ return _colors.surface;
+ }
+
+ @override
+ ButtonStyle get cancelButtonStyle {
+ return TextButton.styleFrom();
+ }
+
+ @override
+ ButtonStyle get confirmButtonStyle {
+ return TextButton.styleFrom();
+ }
+
+ @override
+ BorderSide get dayPeriodBorderSide {
+ return BorderSide(
+ color: Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface),
+ );
+ }
+
+ @override
+ Color get dayPeriodColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.primary.withOpacity(_colors.brightness == Brightness.dark ? 0.24 : 0.12);
+ }
+ // The unselected day period should match the overall picker dialog color.
+ // Making it transparent enables that without being redundant and allows
+ // the optional elevation overlay for dark mode to be visible.
+ return Colors.transparent;
+ });
+ }
+
+ @override
+ OutlinedBorder get dayPeriodShape {
+ return _kDefaultShape;
+ }
+
+ @override
+ Size get dayPeriodPortraitSize {
+ return const Size(52, 80);
+ }
+
+ @override
+ Size get dayPeriodLandscapeSize {
+ return const Size(0, 40);
+ }
+
+ @override
+ Size get dayPeriodInputSize {
+ return const Size(52, 70);
+ }
+
+ @override
+ Color get dayPeriodTextColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ return states.contains(MaterialState.selected) ? _colors.primary : _colors.onSurface.withOpacity(0.60);
+ });
+ }
+
+ @override
+ TextStyle get dayPeriodTextStyle {
+ return _textTheme.titleMedium!.copyWith(color: dayPeriodTextColor);
+ }
+
+ @override
+ Color get dialBackgroundColor {
+ return _colors.onSurface.withOpacity(_colors.brightness == Brightness.dark ? 0.12 : 0.08);
+ }
+
+ @override
+ Color get dialHandColor {
+ return _colors.primary;
+ }
+
+ @override
+ Size get dialSize {
+ return const Size.square(280);
+ }
+
+ @override
+ double get handWidth {
+ return 2;
+ }
+
+ @override
+ double get dotRadius {
+ return 22;
+ }
+
+ @override
+ double get centerRadius {
+ return 4;
+ }
+
+ @override
+ Color get dialTextColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.surface;
+ }
+ return _colors.onSurface;
+ });
+ }
+
+ @override
+ TextStyle get dialTextStyle {
+ return _textTheme.bodyLarge!;
+ }
+
+ @override
+ double get elevation {
+ return 6;
+ }
+
+ @override
+ Color get entryModeIconColor {
+ return _colors.onSurface.withOpacity(_colors.brightness == Brightness.dark ? 1.0 : 0.6);
+ }
+
+ @override
+ TextStyle get helpTextStyle {
+ return _textTheme.labelSmall!;
+ }
+
+ @override
+ Color get hourMinuteColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ return states.contains(MaterialState.selected)
+ ? _colors.primary.withOpacity(_colors.brightness == Brightness.dark ? 0.24 : 0.12)
+ : _colors.onSurface.withOpacity(0.12);
+ });
+ }
+
+ @override
+ ShapeBorder get hourMinuteShape {
+ return _kDefaultShape;
+ }
+
+ @override
+ Size get hourMinuteSize {
+ return const Size(96, 80);
+ }
+
+ @override
+ Size get hourMinuteSize24Hour {
+ return const Size(114, 80);
+ }
+
+ @override
+ Size get hourMinuteInputSize {
+ return const Size(96, 70);
+ }
+
+ @override
+ Size get hourMinuteInputSize24Hour {
+ return const Size(114, 70);
+ }
+
+ @override
+ Color get hourMinuteTextColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ return states.contains(MaterialState.selected) ? _colors.primary : _colors.onSurface;
+ });
+ }
+
+ @override
+ TextStyle get hourMinuteTextStyle {
+ return _textTheme.displayMedium!;
+ }
+
+ Color get _hourMinuteInputColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ return states.contains(MaterialState.selected)
+ ? Colors.transparent
+ : _colors.onSurface.withOpacity(0.12);
+ });
+ }
+
+ @override
+ InputDecorationTheme get inputDecorationTheme {
+ return InputDecorationTheme(
+ contentPadding: EdgeInsets.zero,
+ filled: true,
+ fillColor: _hourMinuteInputColor,
+ focusColor: Colors.transparent,
+ enabledBorder: const OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.transparent),
+ ),
+ errorBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: _colors.error, width: 2),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: _colors.primary, width: 2),
+ ),
+ focusedErrorBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: _colors.error, width: 2),
+ ),
+ hintStyle: hourMinuteTextStyle.copyWith(color: _colors.onSurface.withOpacity(0.36)),
+ // Prevent the error text from appearing.
+ // TODO(rami-a): Remove this workaround once
+ // https://github.com/flutter/flutter/issues/54104
+ // is fixed.
+ errorStyle: const TextStyle(fontSize: 0, height: 0),
+ );
+ }
+
+ @override
+ EdgeInsetsGeometry get padding {
+ return const EdgeInsets.fromLTRB(8, 18, 8, 8);
+ }
+
+ @override
+ ShapeBorder get shape {
+ return _kDefaultShape;
+ }
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES - TimePicker
+
+// 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 _TimePickerDefaultsM3 extends _TimePickerDefaults {
+ _TimePickerDefaultsM3(this.context);
+
+ final BuildContext context;
+
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+ late final TextTheme _textTheme = Theme.of(context).textTheme;
+
+ @override
+ Color get backgroundColor {
+ return _colors.surface;
+ }
+
+ @override
+ ButtonStyle get cancelButtonStyle {
+ return TextButton.styleFrom();
+ }
+
+ @override
+ ButtonStyle get confirmButtonStyle {
+ return TextButton.styleFrom();
+ }
+
+ @override
+ BorderSide get dayPeriodBorderSide {
+ return BorderSide(color: _colors.outline);
+ }
+
+ @override
+ Color get dayPeriodColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.tertiaryContainer;
+ }
+ // The unselected day period should match the overall picker dialog color.
+ // Making it transparent enables that without being redundant and allows
+ // the optional elevation overlay for dark mode to be visible.
+ return Colors.transparent;
+ });
+ }
+
+ @override
+ OutlinedBorder get dayPeriodShape {
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))).copyWith(side: dayPeriodBorderSide);
+ }
+
+ @override
+ Size get dayPeriodPortraitSize {
+ return const Size(52, 80);
+ }
+
+ @override
+ Size get dayPeriodLandscapeSize {
+ return const Size(216, 38);
+ }
+
+ @override
+ Size get dayPeriodInputSize {
+ // Input size is eight pixels smaller than the portrait size in the spec,
+ // but there's not token for it yet.
+ return Size(dayPeriodPortraitSize.width, dayPeriodPortraitSize.height - 8);
+ }
+
+ @override
+ Color get dayPeriodTextColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ return _dayPeriodForegroundColor.resolve(states);
+ });
+ }
+
+ MaterialStateProperty<Color> get _dayPeriodForegroundColor {
+ return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ Color? textColor;
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.pressed)) {
+ textColor = _colors.onTertiaryContainer;
+ } else {
+ // not pressed
+ if (states.contains(MaterialState.focused)) {
+ textColor = _colors.onTertiaryContainer;
+ } else {
+ // not focused
+ if (states.contains(MaterialState.hovered)) {
+ textColor = _colors.onTertiaryContainer;
+ }
+ }
+ }
+ } else {
+ // unselected
+ if (states.contains(MaterialState.pressed)) {
+ textColor = _colors.onSurfaceVariant;
+ } else {
+ // not pressed
+ if (states.contains(MaterialState.focused)) {
+ textColor = _colors.onSurfaceVariant;
+ } else {
+ // not focused
+ if (states.contains(MaterialState.hovered)) {
+ textColor = _colors.onSurfaceVariant;
+ }
+ }
+ }
+ }
+ return textColor ?? _colors.onTertiaryContainer;
+ });
+ }
+
+ @override
+ TextStyle get dayPeriodTextStyle {
+ return _textTheme.titleMedium!.copyWith(color: dayPeriodTextColor);
+ }
+
+ @override
+ Color get dialBackgroundColor {
+ return _colors.surfaceVariant.withOpacity(_colors.brightness == Brightness.dark ? 0.12 : 0.08);
+ }
+
+ @override
+ Color get dialHandColor {
+ return _colors.primary;
+ }
+
+ @override
+ Size get dialSize {
+ return const Size.square(256.0);
+ }
+
+ @override
+ double get handWidth {
+ return const Size(2, double.infinity).width;
+ }
+
+ @override
+ double get dotRadius {
+ return const Size.square(48.0).width / 2;
+ }
+
+ @override
+ double get centerRadius {
+ return const Size.square(8.0).width / 2;
+ }
+
+ @override
+ Color get dialTextColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return _colors.onPrimary;
+ }
+ return _colors.onSurface;
+ });
+ }
+
+ @override
+ TextStyle get dialTextStyle {
+ return _textTheme.bodyLarge!;
+ }
+
+ @override
+ double get elevation {
+ return 6.0;
+ }
+
+ @override
+ Color get entryModeIconColor {
+ return _colors.onSurface;
+ }
+
+ @override
+ TextStyle get helpTextStyle {
+ return MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
+ final TextStyle textStyle = _textTheme.labelMedium!;
+ return textStyle.copyWith(color: _colors.onSurfaceVariant);
+ });
+ }
+
+ @override
+ EdgeInsetsGeometry get padding {
+ return const EdgeInsets.all(24);
+ }
+
+ @override
+ Color get hourMinuteColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ Color overlayColor = _colors.primaryContainer;
+ if (states.contains(MaterialState.pressed)) {
+ overlayColor = _colors.onPrimaryContainer;
+ } else if (states.contains(MaterialState.focused)) {
+ const double focusOpacity = 0.12;
+ overlayColor = _colors.onPrimaryContainer.withOpacity(focusOpacity);
+ } else if (states.contains(MaterialState.hovered)) {
+ const double hoverOpacity = 0.08;
+ overlayColor = _colors.onPrimaryContainer.withOpacity(hoverOpacity);
+ }
+ return Color.alphaBlend(overlayColor, _colors.primaryContainer);
+ } else {
+ Color overlayColor = _colors.surfaceVariant;
+ if (states.contains(MaterialState.pressed)) {
+ overlayColor = _colors.onSurface;
+ } else if (states.contains(MaterialState.focused)) {
+ const double focusOpacity = 0.12;
+ overlayColor = _colors.onSurface.withOpacity(focusOpacity);
+ } else if (states.contains(MaterialState.hovered)) {
+ const double hoverOpacity = 0.08;
+ overlayColor = _colors.onSurface.withOpacity(hoverOpacity);
+ }
+ return Color.alphaBlend(overlayColor, _colors.surfaceVariant);
+ }
+ });
+ }
+
+ @override
+ ShapeBorder get hourMinuteShape {
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0)));
+ }
+
+ @override
+ Size get hourMinuteSize {
+ return const Size(96, 80);
+ }
+
+ @override
+ Size get hourMinuteSize24Hour {
+ return Size(const Size(114, double.infinity).width, hourMinuteSize.height);
+ }
+
+ @override
+ Size get hourMinuteInputSize {
+ // Input size is eight pixels smaller than the regular size in the spec, but
+ // there's not token for it yet.
+ return Size(hourMinuteSize.width, hourMinuteSize.height - 8);
+ }
+
+ @override
+ Size get hourMinuteInputSize24Hour {
+ // Input size is eight pixels smaller than the regular size in the spec, but
+ // there's not token for it yet.
+ return Size(hourMinuteSize24Hour.width, hourMinuteSize24Hour.height - 8);
+ }
+
+ @override
+ Color get hourMinuteTextColor {
+ return MaterialStateColor.resolveWith((Set<MaterialState> states) {
+ return _hourMinuteTextColor.resolve(states);
+ });
+ }
+
+ MaterialStateProperty<Color> get _hourMinuteTextColor {
+ return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onPrimaryContainer;
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onPrimaryContainer;
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onPrimaryContainer;
+ }
+ return _colors.onPrimaryContainer;
+ } else {
+ // unselected
+ if (states.contains(MaterialState.pressed)) {
+ return _colors.onSurface;
+ }
+ if (states.contains(MaterialState.focused)) {
+ return _colors.onSurface;
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return _colors.onSurface;
+ }
+ return _colors.onSurface;
+ }
+ });
+ }
+
+ @override
+ TextStyle get hourMinuteTextStyle {
+ return MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
+ return _textTheme.displayLarge!.copyWith(color: _hourMinuteTextColor.resolve(states));
+ });
+ }
+
+ @override
+ InputDecorationTheme get inputDecorationTheme {
+ // This is NOT correct, but there's no token for
+ // 'time-input.container.shape', so this is using the radius from the shape
+ // for the hour/minute selector. It's a BorderRadiusGeometry, so we have to
+ // resolve it before we can use it.
+ final BorderRadius selectorRadius = const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0)))
+ .borderRadius
+ .resolve(Directionality.of(context));
+ return InputDecorationTheme(
+ contentPadding: EdgeInsets.zero,
+ filled: true,
+ // This should be derived from a token, but there isn't one for 'time-input'.
+ fillColor: hourMinuteColor,
+ // This should be derived from a token, but there isn't one for 'time-input'.
+ focusColor: _colors.primaryContainer,
+ enabledBorder: OutlineInputBorder(
+ borderRadius: selectorRadius,
+ borderSide: const BorderSide(color: Colors.transparent),
+ ),
+ errorBorder: OutlineInputBorder(
+ borderRadius: selectorRadius,
+ borderSide: BorderSide(color: _colors.error, width: 2),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: selectorRadius,
+ borderSide: BorderSide(color: _colors.primary, width: 2),
+ ),
+ focusedErrorBorder: OutlineInputBorder(
+ borderRadius: selectorRadius,
+ borderSide: BorderSide(color: _colors.error, width: 2),
+ ),
+ hintStyle: hourMinuteTextStyle.copyWith(color: _colors.onSurface.withOpacity(0.36)),
+ // Prevent the error text from appearing.
+ // TODO(rami-a): Remove this workaround once
+ // https://github.com/flutter/flutter/issues/54104
+ // is fixed.
+ errorStyle: const TextStyle(fontSize: 0, height: 0),
+ );
+ }
+
+ @override
+ ShapeBorder get shape {
+ return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0)));
+ }
+}
+
+// END GENERATED TOKEN PROPERTIES - TimePicker
diff --git a/framework/lib/src/material/time_picker_theme.dart b/framework/lib/src/material/time_picker_theme.dart
index 743d68c..e2312b8 100644
--- a/framework/lib/src/material/time_picker_theme.dart
+++ b/framework/lib/src/material/time_picker_theme.dart
@@ -2,10 +2,14 @@
// 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' hide TextStyle;
+
import 'package:flute/foundation.dart';
import 'package:flute/widgets.dart';
+import 'button_style.dart';
import 'input_decorator.dart';
+import 'material_state.dart';
import 'theme.dart';
// Examples can assume:
@@ -36,22 +40,27 @@
/// [ThemeData.timePickerTheme].
const TimePickerThemeData({
this.backgroundColor,
- this.hourMinuteTextColor,
- this.hourMinuteColor,
- this.dayPeriodTextColor,
- this.dayPeriodColor,
- this.dialHandColor,
- this.dialBackgroundColor,
- this.dialTextColor,
- this.entryModeIconColor,
- this.hourMinuteTextStyle,
- this.dayPeriodTextStyle,
- this.helpTextStyle,
- this.shape,
- this.hourMinuteShape,
- this.dayPeriodShape,
+ this.cancelButtonStyle,
+ this.confirmButtonStyle,
this.dayPeriodBorderSide,
+ this.dayPeriodColor,
+ this.dayPeriodShape,
+ this.dayPeriodTextColor,
+ this.dayPeriodTextStyle,
+ this.dialBackgroundColor,
+ this.dialHandColor,
+ this.dialTextColor,
+ this.dialTextStyle,
+ this.elevation,
+ this.entryModeIconColor,
+ this.helpTextStyle,
+ this.hourMinuteColor,
+ this.hourMinuteShape,
+ this.hourMinuteTextColor,
+ this.hourMinuteTextStyle,
this.inputDecorationTheme,
+ this.padding,
+ this.shape,
});
/// The background color of a time picker.
@@ -60,41 +69,25 @@
/// [ColorScheme.background].
final Color? backgroundColor;
- /// The color of the header text that represents hours and minutes.
- ///
- /// If [hourMinuteTextColor] is a [MaterialStateColor], then the effective
- /// text color can depend on the [MaterialState.selected] state, i.e. if the
- /// text is selected or not.
- ///
- /// By default the overall theme's [ColorScheme.primary] color is used when
- /// the text is selected and [ColorScheme.onSurface] when it's not selected.
- final Color? hourMinuteTextColor;
+ /// The style of the cancel button of a [TimePickerDialog].
+ final ButtonStyle? cancelButtonStyle;
- /// The background color of the hour and minutes header segments.
- ///
- /// If [hourMinuteColor] is a [MaterialStateColor], then the effective
- /// background color can depend on the [MaterialState.selected] state, i.e.
- /// if the segment is selected or not.
- ///
- /// By default, if the segment is selected, the overall theme's
- /// `ColorScheme.primary.withOpacity(0.12)` is used when the overall theme's
- /// brightness is [Brightness.light] and
- /// `ColorScheme.primary.withOpacity(0.24)` is used when the overall theme's
- /// brightness is [Brightness.dark].
- /// If the segment is not selected, the overall theme's
- /// `ColorScheme.onSurface.withOpacity(0.12)` is used.
- final Color? hourMinuteColor;
+ /// The style of the conform (OK) button of a [TimePickerDialog].
+ final ButtonStyle? confirmButtonStyle;
- /// The color of the day period text that represents AM/PM.
+ /// The color and weight of the day period's outline.
///
- /// If [dayPeriodTextColor] is a [MaterialStateColor], then the effective
- /// text color can depend on the [MaterialState.selected] state, i.e. if the
- /// text is selected or not.
+ /// If this is null, the time picker defaults to:
///
- /// By default the overall theme's [ColorScheme.primary] color is used when
- /// the text is selected and `ColorScheme.onSurface.withOpacity(0.60)` when
- /// it's not selected.
- final Color? dayPeriodTextColor;
+ /// ```dart
+ /// BorderSide(
+ /// color: Color.alphaBlend(
+ /// Theme.of(context).colorScheme.onBackground.withOpacity(0.38),
+ /// Theme.of(context).colorScheme.surface,
+ /// ),
+ /// ),
+ /// ```
+ final BorderSide? dayPeriodBorderSide;
/// The background color of the AM/PM toggle.
///
@@ -111,69 +104,6 @@
/// [Dialog]'s color to be used.
final Color? dayPeriodColor;
- /// The color of the time picker dial's hand.
- ///
- /// If this is null, the time picker defaults to the overall theme's
- /// [ColorScheme.primary].
- final Color? dialHandColor;
-
- /// The background color of the time picker dial.
- ///
- /// If this is null, the time picker defaults to the overall theme's
- /// [ColorScheme.primary].
- final Color? dialBackgroundColor;
-
- /// The color of the dial text that represents specific hours and minutes.
- ///
- /// If [dialTextColor] is a [MaterialStateColor], then the effective
- /// text color can depend on the [MaterialState.selected] state, i.e. if the
- /// text is selected or not.
- ///
- /// If this color is null then the dial's text colors are based on the
- /// theme's [ThemeData.colorScheme].
- final Color? dialTextColor;
-
- /// The color of the entry mode [IconButton].
- ///
- /// If this is null, the time picker defaults to:
- ///
- /// ```dart
- /// Theme.of(context).colorScheme.onSurface.withOpacity(
- /// Theme.of(context).colorScheme.brightness == Brightness.dark ? 1.0 : 0.6,
- /// )
- /// ```
- final Color? entryModeIconColor;
-
- /// Used to configure the [TextStyle]s for the hour/minute controls.
- ///
- /// If this is null, the time picker defaults to the overall theme's
- /// [TextTheme.headline3].
- final TextStyle? hourMinuteTextStyle;
-
- /// Used to configure the [TextStyle]s for the day period control.
- ///
- /// If this is null, the time picker defaults to the overall theme's
- /// [TextTheme.titleMedium].
- final TextStyle? dayPeriodTextStyle;
-
- /// Used to configure the [TextStyle]s for the helper text in the header.
- ///
- /// If this is null, the time picker defaults to the overall theme's
- /// [TextTheme.labelSmall].
- final TextStyle? helpTextStyle;
-
- /// The shape of the [Dialog] that the time picker is presented in.
- ///
- /// If this is null, the time picker defaults to
- /// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
- final ShapeBorder? shape;
-
- /// The shape of the hour and minute controls that the time picker uses.
- ///
- /// If this is null, the time picker defaults to
- /// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
- final ShapeBorder? hourMinuteShape;
-
/// The shape of the day period that the time picker uses.
///
/// If this is null, the time picker defaults to:
@@ -186,64 +116,180 @@
/// ```
final OutlinedBorder? dayPeriodShape;
- /// The color and weight of the day period's outline.
+ /// The color of the day period text that represents AM/PM.
+ ///
+ /// If [dayPeriodTextColor] is a [MaterialStateColor], then the effective
+ /// text color can depend on the [MaterialState.selected] state, i.e. if the
+ /// text is selected or not.
+ ///
+ /// By default the overall theme's [ColorScheme.primary] color is used when
+ /// the text is selected and `ColorScheme.onSurface.withOpacity(0.60)` when
+ /// it's not selected.
+ final Color? dayPeriodTextColor;
+
+ /// Used to configure the [TextStyle]s for the day period control.
+ ///
+ /// If this is null, the time picker defaults to the overall theme's
+ /// [TextTheme.titleMedium].
+ final TextStyle? dayPeriodTextStyle;
+
+ /// The background color of the time picker dial when the entry mode is
+ /// [TimePickerEntryMode.dial] or [TimePickerEntryMode.dialOnly].
+ ///
+ /// If this is null, the time picker defaults to the overall theme's
+ /// [ColorScheme.primary].
+ final Color? dialBackgroundColor;
+
+ /// The color of the time picker dial's hand when the entry mode is
+ /// [TimePickerEntryMode.dial] or [TimePickerEntryMode.dialOnly].
+ ///
+ /// If this is null, the time picker defaults to the overall theme's
+ /// [ColorScheme.primary].
+ final Color? dialHandColor;
+
+ /// The color of the dial text that represents specific hours and minutes.
+ ///
+ /// If [dialTextColor] is a [MaterialStateColor], then the effective
+ /// text color can depend on the [MaterialState.selected] state, i.e. if the
+ /// text is selected or not.
+ ///
+ /// If this color is null then the dial's text colors are based on the
+ /// theme's [ThemeData.colorScheme].
+ final Color? dialTextColor;
+
+ /// The [TextStyle] for the numbers on the time selection dial.
+ ///
+ /// If [dialTextStyle]'s [TextStyle.color] is a [MaterialStateColor], then the
+ /// effective text color can depend on the [MaterialState.selected] state,
+ /// i.e. if the text is selected or not.
+ ///
+ /// If this style is null then the dial's text style is based on the theme's
+ /// [ThemeData.textTheme].
+ final TextStyle? dialTextStyle;
+
+ /// The Material elevation for the time picker dialog.
+ final double? elevation;
+
+ /// The color of the entry mode [IconButton].
///
/// If this is null, the time picker defaults to:
///
+ ///
/// ```dart
- /// BorderSide(
- /// color: Color.alphaBlend(
- /// Theme.of(context).colorScheme.onBackground.withOpacity(0.38),
- /// Theme.of(context).colorScheme.surface,
- /// ),
- /// ),
+ /// Theme.of(context).colorScheme.onSurface.withOpacity(
+ /// Theme.of(context).colorScheme.brightness == Brightness.dark ? 1.0 : 0.6,
+ /// )
/// ```
- final BorderSide? dayPeriodBorderSide;
+ final Color? entryModeIconColor;
+
+ /// Used to configure the [TextStyle]s for the helper text in the header.
+ ///
+ /// If this is null, the time picker defaults to the overall theme's
+ /// [TextTheme.labelSmall].
+ final TextStyle? helpTextStyle;
+
+ /// The background color of the hour and minute header segments.
+ ///
+ /// If [hourMinuteColor] is a [MaterialStateColor], then the effective
+ /// background color can depend on the [MaterialState.selected] state, i.e.
+ /// if the segment is selected or not.
+ ///
+ /// By default, if the segment is selected, the overall theme's
+ /// `ColorScheme.primary.withOpacity(0.12)` is used when the overall theme's
+ /// brightness is [Brightness.light] and
+ /// `ColorScheme.primary.withOpacity(0.24)` is used when the overall theme's
+ /// brightness is [Brightness.dark].
+ /// If the segment is not selected, the overall theme's
+ /// `ColorScheme.onSurface.withOpacity(0.12)` is used.
+ final Color? hourMinuteColor;
+
+ /// The shape of the hour and minute controls that the time picker uses.
+ ///
+ /// If this is null, the time picker defaults to
+ /// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
+ final ShapeBorder? hourMinuteShape;
+
+ /// The color of the header text that represents hours and minutes.
+ ///
+ /// If [hourMinuteTextColor] is a [MaterialStateColor], then the effective
+ /// text color can depend on the [MaterialState.selected] state, i.e. if the
+ /// text is selected or not.
+ ///
+ /// By default the overall theme's [ColorScheme.primary] color is used when
+ /// the text is selected and [ColorScheme.onSurface] when it's not selected.
+ final Color? hourMinuteTextColor;
+
+ /// Used to configure the [TextStyle]s for the hour/minute controls.
+ ///
+ /// If this is null, the time picker defaults to the overall theme's
+ /// [TextTheme.headline3].
+ final TextStyle? hourMinuteTextStyle;
/// The input decoration theme for the [TextField]s in the time picker.
///
/// If this is null, the time picker provides its own defaults.
final InputDecorationTheme? inputDecorationTheme;
+ /// The padding around the time picker dialog when the entry mode is
+ /// [TimePickerEntryMode.dial] or [TimePickerEntryMode.dialOnly].
+ final EdgeInsetsGeometry? padding;
+
+ /// The shape of the [Dialog] that the time picker is presented in.
+ ///
+ /// If this is null, the time picker defaults to
+ /// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
+ final ShapeBorder? shape;
+
/// Creates a copy of this object with the given fields replaced with the
/// new values.
TimePickerThemeData copyWith({
Color? backgroundColor,
- Color? hourMinuteTextColor,
- Color? hourMinuteColor,
- Color? dayPeriodTextColor,
- Color? dayPeriodColor,
- Color? dialHandColor,
- Color? dialBackgroundColor,
- Color? dialTextColor,
- Color? entryModeIconColor,
- TextStyle? hourMinuteTextStyle,
- TextStyle? dayPeriodTextStyle,
- TextStyle? helpTextStyle,
- ShapeBorder? shape,
- ShapeBorder? hourMinuteShape,
- OutlinedBorder? dayPeriodShape,
+ ButtonStyle? cancelButtonStyle,
+ ButtonStyle? confirmButtonStyle,
+ ButtonStyle? dayPeriodButtonStyle,
BorderSide? dayPeriodBorderSide,
+ Color? dayPeriodColor,
+ OutlinedBorder? dayPeriodShape,
+ Color? dayPeriodTextColor,
+ TextStyle? dayPeriodTextStyle,
+ Color? dialBackgroundColor,
+ Color? dialHandColor,
+ Color? dialTextColor,
+ TextStyle? dialTextStyle,
+ double? elevation,
+ Color? entryModeIconColor,
+ TextStyle? helpTextStyle,
+ Color? hourMinuteColor,
+ ShapeBorder? hourMinuteShape,
+ Color? hourMinuteTextColor,
+ TextStyle? hourMinuteTextStyle,
InputDecorationTheme? inputDecorationTheme,
+ EdgeInsetsGeometry? padding,
+ ShapeBorder? shape,
}) {
return TimePickerThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
- hourMinuteTextColor: hourMinuteTextColor ?? this.hourMinuteTextColor,
- hourMinuteColor: hourMinuteColor ?? this.hourMinuteColor,
- dayPeriodTextColor: dayPeriodTextColor ?? this.dayPeriodTextColor,
- dayPeriodColor: dayPeriodColor ?? this.dayPeriodColor,
- dialHandColor: dialHandColor ?? this.dialHandColor,
- dialBackgroundColor: dialBackgroundColor ?? this.dialBackgroundColor,
- dialTextColor: dialTextColor ?? this.dialTextColor,
- entryModeIconColor: entryModeIconColor ?? this.entryModeIconColor,
- hourMinuteTextStyle: hourMinuteTextStyle ?? this.hourMinuteTextStyle,
- dayPeriodTextStyle: dayPeriodTextStyle ?? this.dayPeriodTextStyle,
- helpTextStyle: helpTextStyle ?? this.helpTextStyle,
- shape: shape ?? this.shape,
- hourMinuteShape: hourMinuteShape ?? this.hourMinuteShape,
- dayPeriodShape: dayPeriodShape ?? this.dayPeriodShape,
+ cancelButtonStyle: cancelButtonStyle ?? this.cancelButtonStyle,
+ confirmButtonStyle: confirmButtonStyle ?? this.confirmButtonStyle,
dayPeriodBorderSide: dayPeriodBorderSide ?? this.dayPeriodBorderSide,
+ dayPeriodColor: dayPeriodColor ?? this.dayPeriodColor,
+ dayPeriodShape: dayPeriodShape ?? this.dayPeriodShape,
+ dayPeriodTextColor: dayPeriodTextColor ?? this.dayPeriodTextColor,
+ dayPeriodTextStyle: dayPeriodTextStyle ?? this.dayPeriodTextStyle,
+ dialBackgroundColor: dialBackgroundColor ?? this.dialBackgroundColor,
+ dialHandColor: dialHandColor ?? this.dialHandColor,
+ dialTextColor: dialTextColor ?? this.dialTextColor,
+ dialTextStyle: dialTextStyle ?? this.dialTextStyle,
+ elevation: elevation ?? this.elevation,
+ entryModeIconColor: entryModeIconColor ?? this.entryModeIconColor,
+ helpTextStyle: helpTextStyle ?? this.helpTextStyle,
+ hourMinuteColor: hourMinuteColor ?? this.hourMinuteColor,
+ hourMinuteShape: hourMinuteShape ?? this.hourMinuteShape,
+ hourMinuteTextColor: hourMinuteTextColor ?? this.hourMinuteTextColor,
+ hourMinuteTextStyle: hourMinuteTextStyle ?? this.hourMinuteTextStyle,
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
+ padding: padding ?? this.padding,
+ shape: shape ?? this.shape,
);
}
@@ -253,7 +299,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static TimePickerThemeData lerp(TimePickerThemeData? a, TimePickerThemeData? b, double t) {
- assert(t != null);
// Workaround since BorderSide's lerp does not allow for null arguments.
BorderSide? lerpedBorderSide;
@@ -268,45 +313,55 @@
}
return TimePickerThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
- hourMinuteTextColor: Color.lerp(a?.hourMinuteTextColor, b?.hourMinuteTextColor, t),
- hourMinuteColor: Color.lerp(a?.hourMinuteColor, b?.hourMinuteColor, t),
- dayPeriodTextColor: Color.lerp(a?.dayPeriodTextColor, b?.dayPeriodTextColor, t),
- dayPeriodColor: Color.lerp(a?.dayPeriodColor, b?.dayPeriodColor, t),
- dialHandColor: Color.lerp(a?.dialHandColor, b?.dialHandColor, t),
- dialBackgroundColor: Color.lerp(a?.dialBackgroundColor, b?.dialBackgroundColor, t),
- dialTextColor: Color.lerp(a?.dialTextColor, b?.dialTextColor, t),
- entryModeIconColor: Color.lerp(a?.entryModeIconColor, b?.entryModeIconColor, t),
- hourMinuteTextStyle: TextStyle.lerp(a?.hourMinuteTextStyle, b?.hourMinuteTextStyle, t),
- dayPeriodTextStyle: TextStyle.lerp(a?.dayPeriodTextStyle, b?.dayPeriodTextStyle, t),
- helpTextStyle: TextStyle.lerp(a?.helpTextStyle, b?.helpTextStyle, t),
- shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
- hourMinuteShape: ShapeBorder.lerp(a?.hourMinuteShape, b?.hourMinuteShape, t),
- dayPeriodShape: ShapeBorder.lerp(a?.dayPeriodShape, b?.dayPeriodShape, t) as OutlinedBorder?,
+ cancelButtonStyle: ButtonStyle.lerp(a?.cancelButtonStyle, b?.cancelButtonStyle, t),
+ confirmButtonStyle: ButtonStyle.lerp(a?.confirmButtonStyle, b?.confirmButtonStyle, t),
dayPeriodBorderSide: lerpedBorderSide,
+ dayPeriodColor: Color.lerp(a?.dayPeriodColor, b?.dayPeriodColor, t),
+ dayPeriodShape: ShapeBorder.lerp(a?.dayPeriodShape, b?.dayPeriodShape, t) as OutlinedBorder?,
+ dayPeriodTextColor: Color.lerp(a?.dayPeriodTextColor, b?.dayPeriodTextColor, t),
+ dayPeriodTextStyle: TextStyle.lerp(a?.dayPeriodTextStyle, b?.dayPeriodTextStyle, t),
+ dialBackgroundColor: Color.lerp(a?.dialBackgroundColor, b?.dialBackgroundColor, t),
+ dialHandColor: Color.lerp(a?.dialHandColor, b?.dialHandColor, t),
+ dialTextColor: Color.lerp(a?.dialTextColor, b?.dialTextColor, t),
+ dialTextStyle: TextStyle.lerp(a?.dialTextStyle, b?.dialTextStyle, t),
+ elevation: lerpDouble(a?.elevation, b?.elevation, t),
+ entryModeIconColor: Color.lerp(a?.entryModeIconColor, b?.entryModeIconColor, t),
+ helpTextStyle: TextStyle.lerp(a?.helpTextStyle, b?.helpTextStyle, t),
+ hourMinuteColor: Color.lerp(a?.hourMinuteColor, b?.hourMinuteColor, t),
+ hourMinuteShape: ShapeBorder.lerp(a?.hourMinuteShape, b?.hourMinuteShape, t),
+ hourMinuteTextColor: Color.lerp(a?.hourMinuteTextColor, b?.hourMinuteTextColor, t),
+ hourMinuteTextStyle: TextStyle.lerp(a?.hourMinuteTextStyle, b?.hourMinuteTextStyle, t),
inputDecorationTheme: t < 0.5 ? a?.inputDecorationTheme : b?.inputDecorationTheme,
+ padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
+ shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
);
}
@override
- int get hashCode => Object.hash(
+ int get hashCode => Object.hashAll(<Object?>[
backgroundColor,
- hourMinuteTextColor,
- hourMinuteColor,
- dayPeriodTextColor,
- dayPeriodColor,
- dialHandColor,
- dialBackgroundColor,
- dialTextColor,
- entryModeIconColor,
- hourMinuteTextStyle,
- dayPeriodTextStyle,
- helpTextStyle,
- shape,
- hourMinuteShape,
- dayPeriodShape,
+ cancelButtonStyle,
+ confirmButtonStyle,
dayPeriodBorderSide,
+ dayPeriodColor,
+ dayPeriodShape,
+ dayPeriodTextColor,
+ dayPeriodTextStyle,
+ dialBackgroundColor,
+ dialHandColor,
+ dialTextColor,
+ dialTextStyle,
+ elevation,
+ entryModeIconColor,
+ helpTextStyle,
+ hourMinuteColor,
+ hourMinuteShape,
+ hourMinuteTextColor,
+ hourMinuteTextStyle,
inputDecorationTheme,
- );
+ padding,
+ shape,
+ ]);
@override
bool operator ==(Object other) {
@@ -318,44 +373,54 @@
}
return other is TimePickerThemeData
&& other.backgroundColor == backgroundColor
- && other.hourMinuteTextColor == hourMinuteTextColor
- && other.hourMinuteColor == hourMinuteColor
- && other.dayPeriodTextColor == dayPeriodTextColor
- && other.dayPeriodColor == dayPeriodColor
- && other.dialHandColor == dialHandColor
- && other.dialBackgroundColor == dialBackgroundColor
- && other.dialTextColor == dialTextColor
- && other.entryModeIconColor == entryModeIconColor
- && other.hourMinuteTextStyle == hourMinuteTextStyle
- && other.dayPeriodTextStyle == dayPeriodTextStyle
- && other.helpTextStyle == helpTextStyle
- && other.shape == shape
- && other.hourMinuteShape == hourMinuteShape
- && other.dayPeriodShape == dayPeriodShape
+ && other.cancelButtonStyle == cancelButtonStyle
+ && other.confirmButtonStyle == confirmButtonStyle
&& other.dayPeriodBorderSide == dayPeriodBorderSide
- && other.inputDecorationTheme == inputDecorationTheme;
+ && other.dayPeriodColor == dayPeriodColor
+ && other.dayPeriodShape == dayPeriodShape
+ && other.dayPeriodTextColor == dayPeriodTextColor
+ && other.dayPeriodTextStyle == dayPeriodTextStyle
+ && other.dialBackgroundColor == dialBackgroundColor
+ && other.dialHandColor == dialHandColor
+ && other.dialTextColor == dialTextColor
+ && other.dialTextStyle == dialTextStyle
+ && other.elevation == elevation
+ && other.entryModeIconColor == entryModeIconColor
+ && other.helpTextStyle == helpTextStyle
+ && other.hourMinuteColor == hourMinuteColor
+ && other.hourMinuteShape == hourMinuteShape
+ && other.hourMinuteTextColor == hourMinuteTextColor
+ && other.hourMinuteTextStyle == hourMinuteTextStyle
+ && other.inputDecorationTheme == inputDecorationTheme
+ && other.padding == padding
+ && other.shape == shape;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
- properties.add(ColorProperty('hourMinuteTextColor', hourMinuteTextColor, defaultValue: null));
- properties.add(ColorProperty('hourMinuteColor', hourMinuteColor, defaultValue: null));
- properties.add(ColorProperty('dayPeriodTextColor', dayPeriodTextColor, defaultValue: null));
- properties.add(ColorProperty('dayPeriodColor', dayPeriodColor, defaultValue: null));
- properties.add(ColorProperty('dialHandColor', dialHandColor, defaultValue: null));
- properties.add(ColorProperty('dialBackgroundColor', dialBackgroundColor, defaultValue: null));
- properties.add(ColorProperty('dialTextColor', dialTextColor, defaultValue: null));
- properties.add(ColorProperty('entryModeIconColor', entryModeIconColor, defaultValue: null));
- properties.add(DiagnosticsProperty<TextStyle>('hourMinuteTextStyle', hourMinuteTextStyle, defaultValue: null));
- properties.add(DiagnosticsProperty<TextStyle>('dayPeriodTextStyle', dayPeriodTextStyle, defaultValue: null));
- properties.add(DiagnosticsProperty<TextStyle>('helpTextStyle', helpTextStyle, defaultValue: null));
- properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
- properties.add(DiagnosticsProperty<ShapeBorder>('hourMinuteShape', hourMinuteShape, defaultValue: null));
- properties.add(DiagnosticsProperty<ShapeBorder>('dayPeriodShape', dayPeriodShape, defaultValue: null));
+ properties.add(DiagnosticsProperty<ButtonStyle>('cancelButtonStyle', cancelButtonStyle, defaultValue: null));
+ properties.add(DiagnosticsProperty<ButtonStyle>('confirmButtonStyle', confirmButtonStyle, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide>('dayPeriodBorderSide', dayPeriodBorderSide, defaultValue: null));
+ properties.add(ColorProperty('dayPeriodColor', dayPeriodColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<ShapeBorder>('dayPeriodShape', dayPeriodShape, defaultValue: null));
+ properties.add(ColorProperty('dayPeriodTextColor', dayPeriodTextColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('dayPeriodTextStyle', dayPeriodTextStyle, defaultValue: null));
+ properties.add(ColorProperty('dialBackgroundColor', dialBackgroundColor, defaultValue: null));
+ properties.add(ColorProperty('dialHandColor', dialHandColor, defaultValue: null));
+ properties.add(ColorProperty('dialTextColor', dialTextColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle?>('dialTextStyle', dialTextStyle, defaultValue: null));
+ properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
+ properties.add(ColorProperty('entryModeIconColor', entryModeIconColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('helpTextStyle', helpTextStyle, defaultValue: null));
+ properties.add(ColorProperty('hourMinuteColor', hourMinuteColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<ShapeBorder>('hourMinuteShape', hourMinuteShape, defaultValue: null));
+ properties.add(ColorProperty('hourMinuteTextColor', hourMinuteTextColor, defaultValue: null));
+ properties.add(DiagnosticsProperty<TextStyle>('hourMinuteTextStyle', hourMinuteTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme, defaultValue: null));
+ properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
+ properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
}
}
@@ -371,7 +436,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties for descendant time picker widgets.
final TimePickerThemeData data;
diff --git a/framework/lib/src/material/toggle_buttons.dart b/framework/lib/src/material/toggle_buttons.dart
index 31ef55f..99692c9 100644
--- a/framework/lib/src/material/toggle_buttons.dart
+++ b/framework/lib/src/material/toggle_buttons.dart
@@ -207,12 +207,7 @@
this.borderWidth,
this.direction = Axis.horizontal,
this.verticalDirection = VerticalDirection.down,
- }) :
- assert(children != null),
- assert(isSelected != null),
- assert(children.length == isSelected.length),
- assert(direction != null),
- assert(direction == Axis.horizontal || verticalDirection != null);
+ }) : assert(children.length == isSelected.length);
static const double _defaultBorderWidth = 1.0;
@@ -653,18 +648,6 @@
@override
Widget build(BuildContext context) {
assert(
- !isSelected.any((bool val) => val == null),
- 'All elements of isSelected must be non-null.\n'
- 'The current list of isSelected values is as follows:\n'
- '$isSelected',
- );
- assert(
- focusNodes == null || !focusNodes!.any((FocusNode val) => val == null),
- 'All elements of focusNodes must be non-null.\n'
- 'The current list of focus node values is as follows:\n'
- '$focusNodes',
- );
- assert(
() {
if (focusNodes != null) {
return focusNodes!.length == children.length;
diff --git a/framework/lib/src/material/toggle_buttons_theme.dart b/framework/lib/src/material/toggle_buttons_theme.dart
index e2279f0..d4c9bd5 100644
--- a/framework/lib/src/material/toggle_buttons_theme.dart
+++ b/framework/lib/src/material/toggle_buttons_theme.dart
@@ -150,7 +150,6 @@
/// Linearly interpolate between two toggle buttons themes.
static ToggleButtonsThemeData? lerp(ToggleButtonsThemeData? a, ToggleButtonsThemeData? b, double t) {
- assert (t != null);
if (a == null && b == null) {
return null;
}
@@ -253,7 +252,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// Specifies the color and border values for descendant [ToggleButtons] widgets.
final ToggleButtonsThemeData data;
diff --git a/framework/lib/src/material/tooltip.dart b/framework/lib/src/material/tooltip.dart
index 28138c1..07517be 100644
--- a/framework/lib/src/material/tooltip.dart
+++ b/framework/lib/src/material/tooltip.dart
@@ -630,6 +630,7 @@
_entry?.remove();
}
_isConcealed = false;
+ _entry?.dispose();
_entry = null;
if (_mouseIsConnected) {
Tooltip._revealLastTooltip();
@@ -778,9 +779,7 @@
required this.target,
required this.verticalOffset,
required this.preferBelow,
- }) : assert(target != null),
- assert(verticalOffset != null),
- assert(preferBelow != null);
+ });
/// The offset of the target the tooltip is positioned near in the global
/// coordinate system.
@@ -884,7 +883,7 @@
);
}
return Positioned.fill(
- bottom: MediaQuery.maybeOf(context)?.viewInsets.bottom ?? 0.0,
+ bottom: MediaQuery.maybeViewInsetsOf(context)?.bottom ?? 0.0,
child: CustomSingleChildLayout(
delegate: _TooltipPositionDelegate(
target: target,
diff --git a/framework/lib/src/material/tooltip_theme.dart b/framework/lib/src/material/tooltip_theme.dart
index 7a4c7a4..53cde10 100644
--- a/framework/lib/src/material/tooltip_theme.dart
+++ b/framework/lib/src/material/tooltip_theme.dart
@@ -152,7 +152,6 @@
if (a == null && b == null) {
return null;
}
- assert(t != null);
return TooltipThemeData(
height: lerpDouble(a?.height, b?.height, t),
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
@@ -269,7 +268,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null);
+ });
/// The properties for descendant [Tooltip] widgets.
final TooltipThemeData data;
diff --git a/framework/lib/src/material/typography.dart b/framework/lib/src/material/typography.dart
index 0d846ba..0a08ffc 100644
--- a/framework/lib/src/material/typography.dart
+++ b/framework/lib/src/material/typography.dart
@@ -211,9 +211,6 @@
TextTheme tall,
) {
assert(platform != null || (black != null && white != null));
- assert(englishLike != null);
- assert(dense != null);
- assert(tall != null);
switch (platform) {
case TargetPlatform.iOS:
black ??= blackCupertino;
@@ -242,12 +239,7 @@
return Typography._(black!, white!, englishLike, dense, tall);
}
- const Typography._(this.black, this.white, this.englishLike, this.dense, this.tall)
- : assert(black != null),
- assert(white != null),
- assert(englishLike != null),
- assert(dense != null),
- assert(tall != null);
+ const Typography._(this.black, this.white, this.englishLike, this.dense, this.tall);
/// A Material Design text theme with dark glyphs.
///
@@ -305,7 +297,6 @@
/// Returns one of [englishLike], [dense], or [tall].
TextTheme geometryThemeFor(ScriptCategory category) {
- assert(category != null);
switch (category) {
case ScriptCategory.englishLike:
return englishLike;
@@ -759,7 +750,7 @@
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
-// Token database version: v0_143
+// Token database version: v0_158
class _M3Typography {
_M3Typography._();
diff --git a/framework/lib/src/painting/_network_image_io.dart b/framework/lib/src/painting/_network_image_io.dart
index 13ab17e..e8f5e87 100644
--- a/framework/lib/src/painting/_network_image_io.dart
+++ b/framework/lib/src/painting/_network_image_io.dart
@@ -19,9 +19,7 @@
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
- const NetworkImage(this.url, { this.scale = 1.0, this.headers })
- : assert(url != null),
- assert(scale != null);
+ const NetworkImage(this.url, { this.scale = 1.0, this.headers });
@override
final String url;
@@ -45,7 +43,7 @@
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key as NetworkImage, chunkEvents, null, decode),
+ codec: _loadAsync(key as NetworkImage, chunkEvents, decodeDeprecated: decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
@@ -64,7 +62,26 @@
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key as NetworkImage, chunkEvents, decode, null),
+ codec: _loadAsync(key as NetworkImage, chunkEvents, decodeBufferDeprecated: decode),
+ chunkEvents: chunkEvents.stream,
+ scale: key.scale,
+ debugLabel: key.url,
+ informationCollector: () => <DiagnosticsNode>[
+ DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
+ DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
+ ],
+ );
+ }
+
+ @override
+ ImageStreamCompleter loadImage(image_provider.NetworkImage key, image_provider.ImageDecoderCallback decode) {
+ // Ownership of this controller is handed off to [_loadAsync]; it is that
+ // method's responsibility to close the controller's stream when the image
+ // has been loaded or an error is thrown.
+ final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
+
+ return MultiFrameImageStreamCompleter(
+ codec: _loadAsync(key as NetworkImage, chunkEvents, decode: decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
@@ -94,10 +111,11 @@
Future<ui.Codec> _loadAsync(
NetworkImage key,
- StreamController<ImageChunkEvent> chunkEvents,
- image_provider.DecoderBufferCallback? decode,
- image_provider.DecoderCallback? decodeDepreacted,
- ) async {
+ StreamController<ImageChunkEvent> chunkEvents, {
+ image_provider.ImageDecoderCallback? decode,
+ image_provider.DecoderBufferCallback? decodeBufferDeprecated,
+ image_provider.DecoderCallback? decodeDeprecated,
+ }) async {
try {
assert(key == this);
@@ -133,9 +151,12 @@
if (decode != null) {
final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return decode(buffer);
+ } else if (decodeBufferDeprecated != null) {
+ final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
+ return decodeBufferDeprecated(buffer);
} else {
- assert(decodeDepreacted != null);
- return decodeDepreacted!(bytes);
+ assert(decodeDeprecated != null);
+ return decodeDeprecated!(bytes);
}
} catch (e) {
// Depending on where the exception was thrown, the image cache may not
diff --git a/framework/lib/src/painting/_network_image_web.dart b/framework/lib/src/painting/_network_image_web.dart
index b5fa5b8..4821c6d 100644
--- a/framework/lib/src/painting/_network_image_web.dart
+++ b/framework/lib/src/painting/_network_image_web.dart
@@ -39,9 +39,7 @@
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
- const NetworkImage(this.url, {this.scale = 1.0, this.headers})
- : assert(url != null),
- assert(scale != null);
+ const NetworkImage(this.url, {this.scale = 1.0, this.headers});
@override
final String url;
@@ -67,7 +65,7 @@
return MultiFrameImageStreamCompleter(
chunkEvents: chunkEvents.stream,
- codec: _loadAsync(key as NetworkImage, null, decode, chunkEvents),
+ codec: _loadAsync(key as NetworkImage, null, null, decode, chunkEvents),
scale: key.scale,
debugLabel: key.url,
informationCollector: _imageStreamInformationCollector(key),
@@ -84,7 +82,23 @@
return MultiFrameImageStreamCompleter(
chunkEvents: chunkEvents.stream,
- codec: _loadAsync(key as NetworkImage, decode, null, chunkEvents),
+ codec: _loadAsync(key as NetworkImage, null, decode, null, chunkEvents),
+ scale: key.scale,
+ debugLabel: key.url,
+ informationCollector: _imageStreamInformationCollector(key),
+ );
+ }
+
+ @override
+ ImageStreamCompleter loadImage(image_provider.NetworkImage key, image_provider.ImageDecoderCallback decode) {
+ // Ownership of this controller is handed off to [_loadAsync]; it is that
+ // method's responsibility to close the controller's stream when the image
+ // has been loaded or an error is thrown.
+ final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
+
+ return MultiFrameImageStreamCompleter(
+ chunkEvents: chunkEvents.stream,
+ codec: _loadAsync(key as NetworkImage, decode, null, null, chunkEvents),
scale: key.scale,
debugLabel: key.url,
informationCollector: _imageStreamInformationCollector(key),
@@ -103,34 +117,36 @@
return collector;
}
- // TODO(garyq): We should eventually support custom decoding of network images on Web as
- // well, see https://github.com/flutter/flutter/issues/42789.
- //
- // Web does not support decoding network images to a specified size. The decode parameter
+ // Html renderer does not support decoding network images to a specified size. The decode parameter
// here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used
// directly in place of the typical `instantiateImageCodec` method.
Future<ui.Codec> _loadAsync(
NetworkImage key,
- image_provider.DecoderBufferCallback? decode,
- image_provider.DecoderCallback? decodeDepreacted,
+ image_provider.ImageDecoderCallback? decode,
+ image_provider.DecoderBufferCallback? decodeBufferDeprecated,
+ image_provider.DecoderCallback? decodeDeprecated,
StreamController<ImageChunkEvent> chunkEvents,
) async {
assert(key == this);
final Uri resolved = Uri.base.resolve(key.url);
+ final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;
+
// We use a different method when headers are set because the
// `ui.webOnlyInstantiateImageCodecFromUrl` method is not capable of handling headers.
- if (key.headers?.isNotEmpty ?? false) {
+ if (isCanvasKit || containsNetworkImageHeaders) {
final Completer<DomXMLHttpRequest> completer =
Completer<DomXMLHttpRequest>();
final DomXMLHttpRequest request = httpRequestFactory();
request.open('GET', key.url, true);
request.responseType = 'arraybuffer';
- key.headers!.forEach((String header, String value) {
- request.setRequestHeader(header, value);
- });
+ if (containsNetworkImageHeaders) {
+ key.headers!.forEach((String header, String value) {
+ request.setRequestHeader(header, value);
+ });
+ }
request.addEventListener('load', allowInterop((DomEvent e) {
final int? status = request.status;
@@ -166,9 +182,12 @@
if (decode != null) {
final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return decode(buffer);
+ } else if (decodeBufferDeprecated != null) {
+ final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
+ return decodeBufferDeprecated(buffer);
} else {
- assert(decodeDepreacted != null);
- return decodeDepreacted!(bytes);
+ assert(decodeDeprecated != null);
+ return decodeDeprecated!(bytes);
}
} else {
// This API only exists in the web engine implementation and is not
diff --git a/framework/lib/src/painting/alignment.dart b/framework/lib/src/painting/alignment.dart
index cbb2cc1..4149485 100644
--- a/framework/lib/src/painting/alignment.dart
+++ b/framework/lib/src/painting/alignment.dart
@@ -87,7 +87,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static AlignmentGeometry? lerp(AlignmentGeometry? a, AlignmentGeometry? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -188,9 +187,7 @@
/// Creates an alignment.
///
/// The [x] and [y] arguments must not be null.
- const Alignment(this.x, this.y)
- : assert(x != null),
- assert(y != null);
+ const Alignment(this.x, this.y);
/// The distance fraction in the horizontal direction.
///
@@ -340,7 +337,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static Alignment? lerp(Alignment? a, Alignment? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -407,9 +403,7 @@
/// Creates a directional alignment.
///
/// The [start] and [y] arguments must not be null.
- const AlignmentDirectional(this.start, this.y)
- : assert(start != null),
- assert(y != null);
+ const AlignmentDirectional(this.start, this.y);
/// The distance fraction in the horizontal direction.
///
@@ -534,7 +528,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static AlignmentDirectional? lerp(AlignmentDirectional? a, AlignmentDirectional? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -682,8 +675,7 @@
/// Creates a TextAlignVertical from any y value between -1.0 and 1.0.
const TextAlignVertical({
required this.y,
- }) : assert(y != null),
- assert(y >= -1.0 && y <= 1.0);
+ }) : assert(y >= -1.0 && y <= 1.0);
/// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost
/// locations of the top and bottom of the input box.
diff --git a/framework/lib/src/painting/basic_types.dart b/framework/lib/src/painting/basic_types.dart
index c48b20f..6675055 100644
--- a/framework/lib/src/painting/basic_types.dart
+++ b/framework/lib/src/painting/basic_types.dart
@@ -136,7 +136,6 @@
///
/// * [flipAxisDirection], which does the same thing for [AxisDirection] values.
Axis flipAxis(Axis direction) {
- assert(direction != null);
switch (direction) {
case Axis.horizontal:
return Axis.vertical;
@@ -204,7 +203,6 @@
/// [AxisDirection.down] and returns [Axis.horizontal] for [AxisDirection.left]
/// and [AxisDirection.right].
Axis axisDirectionToAxis(AxisDirection axisDirection) {
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.down:
@@ -220,7 +218,6 @@
/// Specifically, returns [AxisDirection.left] for [TextDirection.rtl] and
/// [AxisDirection.right] for [TextDirection.ltr].
AxisDirection textDirectionToAxisDirection(TextDirection textDirection) {
- assert(textDirection != null);
switch (textDirection) {
case TextDirection.rtl:
return AxisDirection.left;
@@ -239,7 +236,6 @@
///
/// * [flipAxis], which does the same thing for [Axis] values.
AxisDirection flipAxisDirection(AxisDirection axisDirection) {
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
return AxisDirection.down;
@@ -258,7 +254,6 @@
/// Specifically, returns true for [AxisDirection.up] and [AxisDirection.left]
/// and false for [AxisDirection.down] and [AxisDirection.right].
bool axisDirectionIsReversed(AxisDirection axisDirection) {
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
diff --git a/framework/lib/src/painting/beveled_rectangle_border.dart b/framework/lib/src/painting/beveled_rectangle_border.dart
index 1480e5b..6205814 100644
--- a/framework/lib/src/painting/beveled_rectangle_border.dart
+++ b/framework/lib/src/painting/beveled_rectangle_border.dart
@@ -25,8 +25,7 @@
const BeveledRectangleBorder({
super.side,
this.borderRadius = BorderRadius.zero,
- }) : assert(side != null),
- assert(borderRadius != null);
+ });
/// The radii for each corner.
///
@@ -49,7 +48,6 @@
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is BeveledRectangleBorder) {
return BeveledRectangleBorder(
side: BorderSide.lerp(a.side, side, t),
@@ -61,7 +59,6 @@
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
- assert(t != null);
if (b is BeveledRectangleBorder) {
return BeveledRectangleBorder(
side: BorderSide.lerp(side, b.side, t),
diff --git a/framework/lib/src/painting/binding.dart b/framework/lib/src/painting/binding.dart
index aa81bcd..6eae7c3 100644
--- a/framework/lib/src/painting/binding.dart
+++ b/framework/lib/src/painting/binding.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 Codec, ImmutableBuffer, instantiateImageCodec, instantiateImageCodecFromBuffer;
+import 'package:engine/ui.dart' as ui;
import 'package:flute/foundation.dart';
import 'package:flute/services.dart' show ServicesBinding;
@@ -100,7 +100,7 @@
/// above its native resolution should prefer scaling the canvas the image is
/// drawn into.
@Deprecated(
- 'Use instantiateImageCodecFromBuffer with an ImmutableBuffer instance instead. '
+ 'Use instantiateImageCodecWithSize with an ImmutableBuffer instance instead. '
'This feature was deprecated after v2.13.0-1.0.pre.',
)
Future<ui.Codec> instantiateImageCodec(
@@ -111,7 +111,6 @@
}) {
assert(cacheWidth == null || cacheWidth > 0);
assert(cacheHeight == null || cacheHeight > 0);
- assert(allowUpscaling != null);
return ui.instantiateImageCodec(
bytes,
targetWidth: cacheWidth,
@@ -141,6 +140,10 @@
/// unnecessary memory usage for images. Callers that wish to display an image
/// above its native resolution should prefer scaling the canvas the image is
/// drawn into.
+ @Deprecated(
+ 'Use instantiateImageCodecWithSize instead. '
+ 'This feature was deprecated after v3.7.0-1.4.pre.',
+ )
Future<ui.Codec> instantiateImageCodecFromBuffer(
ui.ImmutableBuffer buffer, {
int? cacheWidth,
@@ -149,7 +152,6 @@
}) {
assert(cacheWidth == null || cacheWidth > 0);
assert(cacheHeight == null || cacheHeight > 0);
- assert(allowUpscaling != null);
return ui.instantiateImageCodecFromBuffer(
buffer,
targetWidth: cacheWidth,
@@ -158,6 +160,28 @@
);
}
+ /// Calls through to [dart:ui.instantiateImageCodecWithSize] from [ImageCache].
+ ///
+ /// The [buffer] parameter should be an [ui.ImmutableBuffer] instance which can
+ /// be acquired from [ui.ImmutableBuffer.fromUint8List] or
+ /// [ui.ImmutableBuffer.fromAsset].
+ ///
+ /// The [getTargetSize] parameter, when specified, will be invoked and passed
+ /// the image's intrinsic size to determine the size to decode the image to.
+ /// The width and the height of the size it returns must be positive values
+ /// greater than or equal to 1, or null. It is valid to return a [TargetImageSize]
+ /// that specifies only one of `width` and `height` with the other remaining
+ /// null, in which case the omitted dimension will be scaled to maintain the
+ /// aspect ratio of the original dimensions. When both are null or omitted,
+ /// the image will be decoded at its native resolution (as will be the case if
+ /// the [getTargetSize] parameter is omitted).
+ Future<ui.Codec> instantiateImageCodecWithSize(
+ ui.ImmutableBuffer buffer, {
+ ui.TargetImageSizeCallback? getTargetSize,
+ }) {
+ return ui.instantiateImageCodecWithSize(buffer, getTargetSize: getTargetSize);
+ }
+
@override
void evict(String asset) {
super.evict(asset);
diff --git a/framework/lib/src/painting/border_radius.dart b/framework/lib/src/painting/border_radius.dart
index 7b4761f..b1bdfaf 100644
--- a/framework/lib/src/painting/border_radius.dart
+++ b/framework/lib/src/painting/border_radius.dart
@@ -129,7 +129,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderRadiusGeometry? lerp(BorderRadiusGeometry? a, BorderRadiusGeometry? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -507,7 +506,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderRadius? lerp(BorderRadius? a, BorderRadius? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -590,7 +588,7 @@
/// A border radius with all zero radii.
///
- /// Consider using [EdgeInsets.zero] instead, since that object has the same
+ /// Consider using [BorderRadius.zero] instead, since that object has the same
/// effect, but will be cheaper to [resolve].
static const BorderRadiusDirectional zero = BorderRadiusDirectional.all(Radius.zero);
@@ -729,7 +727,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderRadiusDirectional? lerp(BorderRadiusDirectional? a, BorderRadiusDirectional? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/borders.dart b/framework/lib/src/painting/borders.dart
index 01604ea..4316028 100644
--- a/framework/lib/src/painting/borders.dart
+++ b/framework/lib/src/painting/borders.dart
@@ -66,11 +66,7 @@
this.width = 1.0,
this.style = BorderStyle.solid,
this.strokeAlign = strokeAlignInside,
- }) : assert(color != null),
- assert(width != null),
- assert(width >= 0.0),
- assert(style != null),
- assert(strokeAlign != null);
+ }) : assert(width >= 0.0);
/// Creates a [BorderSide] that represents the addition of the two given
/// [BorderSide]s.
@@ -84,8 +80,6 @@
///
/// The arguments must not be null.
static BorderSide merge(BorderSide a, BorderSide b) {
- assert(a != null);
- assert(b != null);
assert(canMerge(a, b));
final bool aIsNone = a.style == BorderStyle.none && a.width == 0.0;
final bool bIsNone = b.style == BorderStyle.none && b.width == 0.0;
@@ -136,7 +130,7 @@
///
/// Values typically range from -1.0 ([strokeAlignInside], inside border,
/// default) to 1.0 ([strokeAlignOutside], outside border), without any
- /// bound constraints (e.g., a value of -2.0 is is not typical, but allowed).
+ /// bound constraints (e.g., a value of -2.0 is not typical, but allowed).
/// A value of 0 ([strokeAlignCenter]) will center the border on the edge
/// of the widget.
///
@@ -202,7 +196,6 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
BorderSide scale(double t) {
- assert(t != null);
return BorderSide(
color: color,
width: math.max(0.0, width * t),
@@ -239,8 +232,6 @@
///
/// The arguments must not be null.
static bool canMerge(BorderSide a, BorderSide b) {
- assert(a != null);
- assert(b != null);
if ((a.style == BorderStyle.none && a.width == 0.0) ||
(b.style == BorderStyle.none && b.width == 0.0)) {
return true;
@@ -255,9 +246,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderSide lerp(BorderSide a, BorderSide b, double t) {
- assert(a != null);
- assert(b != null);
- assert(t != null);
if (t == 0.0) {
return a;
}
@@ -517,7 +505,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static ShapeBorder? lerp(ShapeBorder? a, ShapeBorder? b, double t) {
- assert(t != null);
ShapeBorder? result;
if (b != null) {
result = b.lerpFrom(a, t);
@@ -664,7 +651,7 @@
/// const constructors so that they can be used in const expressions.
///
/// The value of [side] must not be null.
- const OutlinedBorder({ this.side = BorderSide.none }) : assert(side != null);
+ const OutlinedBorder({ this.side = BorderSide.none });
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.all(math.max(side.strokeInset, 0));
@@ -707,7 +694,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static OutlinedBorder? lerp(OutlinedBorder? a, OutlinedBorder? b, double t) {
- assert(t != null);
ShapeBorder? result;
if (b != null) {
result = b.lerpFrom(a, t);
@@ -724,8 +710,7 @@
/// The borders are listed from the outside to the inside.
class _CompoundBorder extends ShapeBorder {
_CompoundBorder(this.borders)
- : assert(borders != null),
- assert(borders.length >= 2),
+ : assert(borders.length >= 2),
assert(!borders.any((ShapeBorder border) => border is _CompoundBorder));
final List<ShapeBorder> borders;
@@ -788,7 +773,6 @@
}
static _CompoundBorder lerp(ShapeBorder? a, ShapeBorder? b, double t) {
- assert(t != null);
assert(a is _CompoundBorder || b is _CompoundBorder); // Not really necessary, but all call sites currently intend this.
final List<ShapeBorder?> aList = a is _CompoundBorder ? a.borders : <ShapeBorder?>[a];
final List<ShapeBorder?> bList = b is _CompoundBorder ? b.borders : <ShapeBorder?>[b];
@@ -897,12 +881,6 @@
BorderSide bottom = BorderSide.none,
BorderSide left = BorderSide.none,
}) {
- assert(canvas != null);
- assert(rect != null);
- assert(top != null);
- assert(right != null);
- assert(bottom != null);
- assert(left != null);
// We draw the borders as filled shapes, unless the borders are hairline
// borders, in which case we use PaintingStyle.stroke, with the stroke width
diff --git a/framework/lib/src/painting/box_border.dart b/framework/lib/src/painting/box_border.dart
index faf1b7b..e5a4ffc 100644
--- a/framework/lib/src/painting/box_border.dart
+++ b/framework/lib/src/painting/box_border.dart
@@ -103,7 +103,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BoxBorder? lerp(BoxBorder? a, BoxBorder? b, double t) {
- assert(t != null);
if ((a is Border?) && (b is Border?)) {
return Border.lerp(a, b, t);
}
@@ -322,17 +321,13 @@
this.right = BorderSide.none,
this.bottom = BorderSide.none,
this.left = BorderSide.none,
- }) : assert(top != null),
- assert(right != null),
- assert(bottom != null),
- assert(left != null);
+ });
/// Creates a border whose sides are all the same.
///
/// The `side` argument must not be null.
const Border.fromBorderSide(BorderSide side)
- : assert(side != null),
- top = side,
+ : top = side,
right = side,
bottom = side,
left = side;
@@ -346,9 +341,7 @@
const Border.symmetric({
BorderSide vertical = BorderSide.none,
BorderSide horizontal = BorderSide.none,
- }) : assert(vertical != null),
- assert(horizontal != null),
- left = vertical,
+ }) : left = vertical,
top = horizontal,
right = vertical,
bottom = horizontal;
@@ -374,8 +367,6 @@
///
/// The arguments must not be null.
static Border merge(Border a, Border b) {
- assert(a != null);
- assert(b != null);
assert(BorderSide.canMerge(a.top, b.top));
assert(BorderSide.canMerge(a.right, b.right));
assert(BorderSide.canMerge(a.bottom, b.bottom));
@@ -478,7 +469,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static Border? lerp(Border? a, Border? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -650,10 +640,7 @@
this.start = BorderSide.none,
this.end = BorderSide.none,
this.bottom = BorderSide.none,
- }) : assert(top != null),
- assert(start != null),
- assert(end != null),
- assert(bottom != null);
+ });
/// Creates a [BorderDirectional] that represents the addition of the two
/// given [BorderDirectional]s.
@@ -663,8 +650,6 @@
///
/// The arguments must not be null.
static BorderDirectional merge(BorderDirectional a, BorderDirectional b) {
- assert(a != null);
- assert(b != null);
assert(BorderSide.canMerge(a.top, b.top));
assert(BorderSide.canMerge(a.start, b.start));
assert(BorderSide.canMerge(a.end, b.end));
@@ -826,7 +811,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BorderDirectional? lerp(BorderDirectional? a, BorderDirectional? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/box_decoration.dart b/framework/lib/src/painting/box_decoration.dart
index 9c49d0c..a81eab4 100644
--- a/framework/lib/src/painting/box_decoration.dart
+++ b/framework/lib/src/painting/box_decoration.dart
@@ -93,8 +93,7 @@
this.gradient,
this.backgroundBlendMode,
this.shape = BoxShape.rectangle,
- }) : assert(shape != null),
- assert(
+ }) : assert(
backgroundBlendMode == null || color != null || gradient != null,
"backgroundBlendMode applies to BoxDecoration's background color or "
'gradient, but no color or gradient was provided.',
@@ -209,7 +208,7 @@
final BoxShape shape;
@override
- EdgeInsetsGeometry? get padding => border?.dimensions;
+ EdgeInsetsGeometry get padding => border?.dimensions ?? EdgeInsets.zero;
@override
Path getClipPath(Rect rect, TextDirection textDirection) {
@@ -289,7 +288,6 @@
/// 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) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -365,7 +363,6 @@
@override
bool hitTest(Size size, Offset position, { TextDirection? textDirection }) {
- assert(shape != null);
assert((Offset.zero & size).contains(position));
switch (shape) {
case BoxShape.rectangle:
@@ -391,15 +388,13 @@
/// An object that paints a [BoxDecoration] into a canvas.
class _BoxDecorationPainter extends BoxPainter {
- _BoxDecorationPainter(this._decoration, super.onChanged)
- : assert(_decoration != null);
+ _BoxDecorationPainter(this._decoration, super.onChanged);
final BoxDecoration _decoration;
Paint? _cachedBackgroundPaint;
Rect? _rectForCachedBackgroundPaint;
Paint _getBackgroundPaint(Rect rect, TextDirection? textDirection) {
- assert(rect != null);
assert(_decoration.gradient != null || _rectForCachedBackgroundPaint == null);
if (_cachedBackgroundPaint == null ||
@@ -489,7 +484,6 @@
/// Paint the box decoration into the given location on the given canvas.
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
- assert(configuration != null);
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection? textDirection = configuration.textDirection;
diff --git a/framework/lib/src/painting/box_fit.dart b/framework/lib/src/painting/box_fit.dart
index 38d14e0..ccfdc81 100644
--- a/framework/lib/src/painting/box_fit.dart
+++ b/framework/lib/src/painting/box_fit.dart
@@ -166,12 +166,26 @@
destinationSize = outputSize;
break;
case BoxFit.fitWidth:
- sourceSize = Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
- destinationSize = Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
+ if (outputSize.width / outputSize.height > inputSize.width / inputSize.height) {
+ // Like "cover"
+ sourceSize = Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
+ destinationSize = outputSize;
+ } else {
+ // Like "contain"
+ sourceSize = inputSize;
+ destinationSize = Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
+ }
break;
case BoxFit.fitHeight:
- sourceSize = Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
- destinationSize = Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
+ if (outputSize.width / outputSize.height > inputSize.width / inputSize.height) {
+ // Like "contain"
+ sourceSize = inputSize;
+ destinationSize = Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
+ } else {
+ // Like "cover"
+ sourceSize = Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
+ destinationSize = outputSize;
+ }
break;
case BoxFit.none:
sourceSize = Size(math.min(inputSize.width, outputSize.width), math.min(inputSize.height, outputSize.height));
diff --git a/framework/lib/src/painting/box_shadow.dart b/framework/lib/src/painting/box_shadow.dart
index 513589c..f050709 100644
--- a/framework/lib/src/painting/box_shadow.dart
+++ b/framework/lib/src/painting/box_shadow.dart
@@ -80,13 +80,12 @@
/// Linearly interpolate between two box shadows.
///
- /// If either box shadow is null, this function linearly interpolates from a
+ /// If either box shadow is null, this function linearly interpolates from
/// a box shadow that matches the other box shadow in color but has a zero
/// offset and a zero blurRadius.
///
/// {@macro dart.ui.shadow.lerp}
static BoxShadow? lerp(BoxShadow? a, BoxShadow? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -111,7 +110,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static List<BoxShadow>? lerpList(List<BoxShadow>? a, List<BoxShadow>? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/circle_border.dart b/framework/lib/src/painting/circle_border.dart
index dd255a3..c39d06f 100644
--- a/framework/lib/src/painting/circle_border.dart
+++ b/framework/lib/src/painting/circle_border.dart
@@ -33,9 +33,7 @@
///
/// The [side] argument must not be null.
const CircleBorder({ super.side, this.eccentricity = 0.0 })
- : assert(side != null),
- assert(eccentricity != null),
- assert(eccentricity >= 0.0, 'The eccentricity argument $eccentricity is not greater than or equal to zero.'),
+ : assert(eccentricity >= 0.0, 'The eccentricity argument $eccentricity is not greater than or equal to zero.'),
assert(eccentricity <= 1.0, 'The eccentricity argument $eccentricity is not less than or equal to one.');
/// Defines the ratio (0.0-1.0) from which the border will deform
diff --git a/framework/lib/src/painting/clip.dart b/framework/lib/src/painting/clip.dart
index 233f257..f6e46b5 100644
--- a/framework/lib/src/painting/clip.dart
+++ b/framework/lib/src/painting/clip.dart
@@ -10,7 +10,6 @@
Canvas get canvas;
void _clipAndPaint(void Function(bool doAntiAlias) canvasClipCall, Clip clipBehavior, Rect bounds, VoidCallback painter) {
- assert(canvasClipCall != null);
canvas.save();
switch (clipBehavior) {
case Clip.none:
diff --git a/framework/lib/src/painting/colors.dart b/framework/lib/src/painting/colors.dart
index b73e7d4..45a87fe 100644
--- a/framework/lib/src/painting/colors.dart
+++ b/framework/lib/src/painting/colors.dart
@@ -89,11 +89,7 @@
/// All the arguments must not be null and be in their respective ranges. See
/// the fields for each parameter for a description of their ranges.
const HSVColor.fromAHSV(this.alpha, this.hue, this.saturation, this.value)
- : assert(alpha != null),
- assert(hue != null),
- assert(saturation != null),
- assert(value != null),
- assert(alpha >= 0.0),
+ : assert(alpha >= 0.0),
assert(alpha <= 1.0),
assert(hue >= 0.0),
assert(hue <= 360.0),
@@ -198,7 +194,6 @@
///
/// Values outside of the valid range for each channel will be clamped.
static HSVColor? lerp(HSVColor? a, HSVColor? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -262,11 +257,7 @@
/// All the arguments must not be null and be in their respective ranges. See
/// the fields for each parameter for a description of their ranges.
const HSLColor.fromAHSL(this.alpha, this.hue, this.saturation, this.lightness)
- : assert(alpha != null),
- assert(hue != null),
- assert(saturation != null),
- assert(lightness != null),
- assert(alpha >= 0.0),
+ : assert(alpha >= 0.0),
assert(alpha <= 1.0),
assert(hue >= 0.0),
assert(hue <= 360.0),
@@ -386,7 +377,6 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static HSLColor? lerp(HSLColor? a, HSLColor? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -519,9 +509,7 @@
super.defaultValue,
super.style,
super.level,
- }) : assert(showName != null),
- assert(style != null),
- assert(level != null);
+ });
@override
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
diff --git a/framework/lib/src/painting/continuous_rectangle_border.dart b/framework/lib/src/painting/continuous_rectangle_border.dart
index ac6ad27..05302b1 100644
--- a/framework/lib/src/painting/continuous_rectangle_border.dart
+++ b/framework/lib/src/painting/continuous_rectangle_border.dart
@@ -37,8 +37,7 @@
const ContinuousRectangleBorder({
super.side,
this.borderRadius = BorderRadius.zero,
- }) : assert(side != null),
- assert(borderRadius != null);
+ });
/// The radius for each corner.
///
@@ -59,7 +58,6 @@
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is ContinuousRectangleBorder) {
return ContinuousRectangleBorder(
side: BorderSide.lerp(a.side, side, t),
@@ -71,7 +69,6 @@
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
- assert(t != null);
if (b is ContinuousRectangleBorder) {
return ContinuousRectangleBorder(
side: BorderSide.lerp(side, b.side, t),
diff --git a/framework/lib/src/painting/decoration.dart b/framework/lib/src/painting/decoration.dart
index 285e4d5..85ef326 100644
--- a/framework/lib/src/painting/decoration.dart
+++ b/framework/lib/src/painting/decoration.dart
@@ -59,7 +59,7 @@
/// [EdgeInsetsGeometry.resolve] to obtain an absolute [EdgeInsets]. (For
/// example, [BorderDirectional] will return an [EdgeInsetsDirectional] for
/// its [padding].)
- EdgeInsetsGeometry? get padding => EdgeInsets.zero;
+ EdgeInsetsGeometry get padding => EdgeInsets.zero;
/// Whether this decoration is complex enough to benefit from caching its painting.
bool get isComplex => false;
@@ -129,7 +129,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static Decoration? lerp(Decoration? a, Decoration? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/decoration_image.dart b/framework/lib/src/painting/decoration_image.dart
index 041fd8b..b6e8c7e 100644
--- a/framework/lib/src/painting/decoration_image.dart
+++ b/framework/lib/src/painting/decoration_image.dart
@@ -3,10 +3,12 @@
// found in the LICENSE file.
import 'dart:developer' as developer;
-import 'package:engine/ui.dart' as ui show Image;
+import 'dart:math' as math;
+import 'package:engine/ui.dart' as ui show FlutterView, Image;
import 'package:flute/foundation.dart';
import 'package:flute/scheduler.dart';
+import 'package:vector_math/vector_math_64.dart';
import 'alignment.dart';
import 'basic_types.dart';
@@ -56,11 +58,7 @@
this.filterQuality = FilterQuality.low,
this.invertColors = false,
this.isAntiAlias = false,
- }) : assert(image != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
- assert(scale != null);
+ });
/// The image to be painted into the decoration.
///
@@ -180,7 +178,6 @@
/// image needs to be repainted, e.g. because it is loading incrementally or
/// because it is animated.
DecorationImagePainter createPainter(VoidCallback onChanged) {
- assert(onChanged != null);
return DecorationImagePainter._(this, onChanged);
}
@@ -264,7 +261,7 @@
/// This object should be disposed using the [dispose] method when it is no
/// longer needed.
class DecorationImagePainter {
- DecorationImagePainter._(this._details, this._onChanged) : assert(_details != null);
+ DecorationImagePainter._(this._details, this._onChanged);
final DecorationImage _details;
final VoidCallback _onChanged;
@@ -287,9 +284,6 @@
/// then the `onChanged` callback passed to [DecorationImage.createPainter]
/// will be called.
void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration) {
- assert(canvas != null);
- assert(rect != null);
- assert(configuration != null);
bool flipHorizontally = false;
if (_details.matchTextDirection) {
@@ -366,7 +360,6 @@
}
_image?.dispose();
_image = value;
- assert(_onChanged != null);
if (!synchronousCall) {
_onChanged();
}
@@ -412,6 +405,61 @@
}());
}
+/// Information that describes how to tile an image for a given [ImageRepeat]
+/// enum.
+///
+/// Used with [createTilingInfo].
+@visibleForTesting
+@immutable
+class ImageTilingInfo {
+ /// Create a new [ImageTilingInfo] object.
+ const ImageTilingInfo({
+ required this.tmx,
+ required this.tmy,
+ required this.transform,
+ });
+
+ /// The tile mode for the x-axis.
+ final TileMode tmx;
+
+ /// The tile mode for the y-axis.
+ final TileMode tmy;
+
+ /// The transform to apply to the image shader.
+ final Matrix4 transform;
+
+ @override
+ String toString() {
+ if (!kDebugMode) {
+ return 'ImageTilingInfo';
+ }
+ return 'ImageTilingInfo($tmx, $tmy, $transform)';
+ }
+}
+
+/// Create the [ImageTilingInfo] for a given [ImageRepeat], canvas [rect],
+/// [destinationRect], and [sourceRect].
+@visibleForTesting
+ImageTilingInfo createTilingInfo(ImageRepeat repeat, Rect rect, Rect destinationRect, Rect sourceRect) {
+ assert(repeat != ImageRepeat.noRepeat);
+ final TileMode tmx = (repeat == ImageRepeat.repeatX || repeat == ImageRepeat.repeat)
+ ? TileMode.repeated
+ : TileMode.decal;
+ final TileMode tmy = (repeat == ImageRepeat.repeatY || repeat == ImageRepeat.repeat)
+ ? TileMode.repeated
+ : TileMode.decal;
+ final Rect data = _generateImageTileRects(rect, destinationRect, repeat).first;
+ final Matrix4 transform = Matrix4.identity()
+ ..scale(data.width / sourceRect.width, data.height / sourceRect.height)
+ ..setTranslationRaw(data.topLeft.dx, data.topLeft.dy, 0);
+
+ return ImageTilingInfo(
+ tmx: tmx,
+ tmy: tmy,
+ transform: transform,
+ );
+}
+
/// Paints an image into the given rectangle on the canvas.
///
/// The arguments have the following meanings:
@@ -499,12 +547,6 @@
FilterQuality filterQuality = FilterQuality.low,
bool isAntiAlias = false,
}) {
- assert(canvas != null);
- assert(image != null);
- assert(alignment != null);
- assert(repeat != null);
- assert(flipHorizontally != null);
- assert(isAntiAlias != null);
assert(
image.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true,
'Cannot paint an image that is disposed.\n'
@@ -558,13 +600,22 @@
bool invertedCanvas = false;
// Output size and destination rect are fully calculated.
if (!kReleaseMode) {
+ // We can use the devicePixelRatio of the views directly here (instead of
+ // going through a MediaQuery) because if it changes, whatever is aware of
+ // the MediaQuery will be repainting the image anyways.
+ // Furthermore, for the memory check below we just assume that all images
+ // are decoded for the view with the highest device pixel ratio and use that
+ // as an upper bound for the display size of the image.
+ final double maxDevicePixelRatio = PaintingBinding.instance.platformDispatcher.views.fold(
+ 0.0,
+ (double previousValue, ui.FlutterView view) => math.max(previousValue, view.devicePixelRatio),
+ );
+
final ImageSizeInfo sizeInfo = ImageSizeInfo(
// Some ImageProvider implementations may not have given this.
source: debugImageLabel ?? '<Unknown Image(${image.width}×${image.height})>',
imageSize: Size(image.width.toDouble(), image.height.toDouble()),
- // It's ok to use this instead of a MediaQuery because if this changes,
- // whatever is aware of the MediaQuery will be repainting the image anyway.
- displaySize: outputSize * PaintingBinding.instance.window.devicePixelRatio,
+ displaySize: outputSize * maxDevicePixelRatio,
);
assert(() {
if (debugInvertOversizedImages &&
@@ -576,7 +627,8 @@
exception: 'Image $debugImageLabel has a display size of '
'$outputWidth×$outputHeight but a decode size of '
'${image.width}×${image.height}, which uses an additional '
- '${overheadInKilobytes}KB.\n\n'
+ '${overheadInKilobytes}KB (assuming a device pixel ratio of '
+ '$maxDevicePixelRatio).\n\n'
'Consider resizing the asset ahead of time, supplying a cacheWidth '
'parameter of $outputWidth, a cacheHeight parameter of '
'$outputHeight, or using a ResizeImage.',
@@ -603,7 +655,7 @@
return true;
}());
// Avoid emitting events that are the same as those emitted in the last frame.
- if (!_lastFrameImageSizeInfo.contains(sizeInfo)) {
+ if (!kReleaseMode && !_lastFrameImageSizeInfo.contains(sizeInfo)) {
final ImageSizeInfo? existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source];
if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) {
_pendingImageSizeInfo[sizeInfo.source!] = sizeInfo;
@@ -630,7 +682,8 @@
if (needSave) {
canvas.save();
}
- if (repeat != ImageRepeat.noRepeat) {
+ if (repeat != ImageRepeat.noRepeat && centerSlice != null) {
+ // Don't clip if an image shader is used.
canvas.clipRect(rect);
}
if (flipHorizontally) {
@@ -646,9 +699,12 @@
if (repeat == ImageRepeat.noRepeat) {
canvas.drawImageRect(image, sourceRect, destinationRect, paint);
} else {
- for (final Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) {
- canvas.drawImageRect(image, sourceRect, tileRect, paint);
- }
+ final ImageTilingInfo info = createTilingInfo(repeat, rect, destinationRect, sourceRect);
+ final ImageShader shader = ImageShader(image, info.tmx, info.tmy, info.transform.storage, filterQuality: filterQuality);
+ canvas.drawRect(
+ rect,
+ paint..shader = shader
+ );
}
} else {
canvas.scale(1 / scale);
@@ -669,7 +725,7 @@
}
}
-Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) {
+List<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) {
int startX = 0;
int startY = 0;
int stopX = 0;
diff --git a/framework/lib/src/painting/edge_insets.dart b/framework/lib/src/painting/edge_insets.dart
index 4515508..26e9c2d 100644
--- a/framework/lib/src/painting/edge_insets.dart
+++ b/framework/lib/src/painting/edge_insets.dart
@@ -64,7 +64,6 @@
/// The total offset in the given direction.
double along(Axis axis) {
- assert(axis != null);
switch (axis) {
case Axis.horizontal:
return horizontal;
@@ -216,7 +215,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static EdgeInsetsGeometry? lerp(EdgeInsetsGeometry? a, EdgeInsetsGeometry? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -604,7 +602,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static EdgeInsets? lerp(EdgeInsets? a, EdgeInsets? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -871,7 +868,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static EdgeInsetsDirectional? lerp(EdgeInsetsDirectional? a, EdgeInsetsDirectional? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/flutter_logo.dart b/framework/lib/src/painting/flutter_logo.dart
index 42eb084..c3c31ee 100644
--- a/framework/lib/src/painting/flutter_logo.dart
+++ b/framework/lib/src/painting/flutter_logo.dart
@@ -44,10 +44,7 @@
this.textColor = const Color(0xFF757575),
this.style = FlutterLogoStyle.markOnly,
this.margin = EdgeInsets.zero,
- }) : assert(textColor != null),
- assert(style != null),
- assert(margin != null),
- _position = identical(style, FlutterLogoStyle.markOnly) ? 0.0 : identical(style, FlutterLogoStyle.horizontal) ? 1.0 : -1.0,
+ }) : _position = identical(style, FlutterLogoStyle.markOnly) ? 0.0 : identical(style, FlutterLogoStyle.horizontal) ? 1.0 : -1.0,
_opacity = 1.0;
const FlutterLogoDecoration._(this.textColor, this.style, this.margin, this._position, this._opacity);
@@ -78,12 +75,7 @@
@override
bool debugAssertIsValid() {
assert(
- textColor != null
- && style != null
- && margin != null
- && _position != null
- && _position.isFinite
- && _opacity != null
+ _position.isFinite
&& _opacity >= 0.0
&& _opacity <= 1.0,
);
@@ -107,7 +99,6 @@
///
/// * [Decoration.lerp], which interpolates between arbitrary decorations.
static FlutterLogoDecoration? lerp(FlutterLogoDecoration? a, FlutterLogoDecoration? b, double t) {
- assert(t != null);
assert(a == null || a.debugAssertIsValid());
assert(b == null || b.debugAssertIsValid());
if (a == null && b == null) {
@@ -218,8 +209,7 @@
/// An object that paints a [BoxDecoration] into a canvas.
class _FlutterLogoPainter extends BoxPainter {
_FlutterLogoPainter(this._config)
- : assert(_config != null),
- assert(_config.debugAssertIsValid()),
+ : assert(_config.debugAssertIsValid()),
super(null) {
_prepareText();
}
diff --git a/framework/lib/src/painting/fractional_offset.dart b/framework/lib/src/painting/fractional_offset.dart
index 130c9d1..d32957c 100644
--- a/framework/lib/src/painting/fractional_offset.dart
+++ b/framework/lib/src/painting/fractional_offset.dart
@@ -56,17 +56,13 @@
///
/// The [dx] and [dy] arguments must not be null.
const FractionalOffset(double dx, double dy)
- : assert(dx != null),
- assert(dy != null),
- super(dx * 2.0 - 1.0, dy * 2.0 - 1.0);
+ : super(dx * 2.0 - 1.0, dy * 2.0 - 1.0);
/// Creates a fractional offset from a specific offset and size.
///
/// The returned [FractionalOffset] describes the position of the
/// [Offset] in the [Size], as a fraction of the [Size].
factory FractionalOffset.fromOffsetAndSize(Offset offset, Size size) {
- assert(size != null);
- assert(offset != null);
return FractionalOffset(
offset.dx / size.width,
offset.dy / size.height,
@@ -180,7 +176,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static FractionalOffset? lerp(FractionalOffset? a, FractionalOffset? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/geometry.dart b/framework/lib/src/painting/geometry.dart
index a662de3..a6b7fc0 100644
--- a/framework/lib/src/painting/geometry.dart
+++ b/framework/lib/src/painting/geometry.dart
@@ -45,12 +45,6 @@
double verticalOffset = 0.0,
double margin = 10.0,
}) {
- assert(size != null);
- assert(childSize != null);
- assert(target != null);
- assert(verticalOffset != null);
- assert(preferBelow != null);
- assert(margin != null);
// VERTICAL DIRECTION
final bool fitsBelow = target.dy + verticalOffset + childSize.height <= size.height - margin;
final bool fitsAbove = target.dy - verticalOffset - childSize.height >= margin;
diff --git a/framework/lib/src/painting/gradient.dart b/framework/lib/src/painting/gradient.dart
index 7e9fb21..810eca6 100644
--- a/framework/lib/src/painting/gradient.dart
+++ b/framework/lib/src/painting/gradient.dart
@@ -20,11 +20,8 @@
/// Calculate the color at position [t] of the gradient defined by [colors] and [stops].
Color _sample(List<Color> colors, List<double> stops, double t) {
- assert(colors != null);
assert(colors.isNotEmpty);
- assert(stops != null);
assert(stops.isNotEmpty);
- assert(t != null);
if (t <= stops.first) {
return colors.first;
}
@@ -107,7 +104,6 @@
@override
Matrix4 transform(Rect bounds, {TextDirection? textDirection}) {
- assert(bounds != null);
final double sinRadians = math.sin(radians);
final double oneMinusCosRadians = 1 - math.cos(radians);
final Offset center = bounds.center;
@@ -170,7 +166,7 @@
required this.colors,
this.stops,
this.transform,
- }) : assert(colors != null);
+ });
/// The colors the gradient should obtain at each of the stops.
///
@@ -311,7 +307,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static Gradient? lerp(Gradient? a, Gradient? b, double t) {
- assert(t != null);
Gradient? result;
if (b != null) {
result = b.lerpFrom(a, t); // if a is null, this must return non-null
@@ -388,9 +383,7 @@
super.stops,
this.tileMode = TileMode.clamp,
super.transform,
- }) : assert(begin != null),
- assert(end != null),
- assert(tileMode != null);
+ });
/// The offset at which stop 0.0 of the gradient is placed.
///
@@ -475,7 +468,7 @@
/// Linearly interpolate between two [LinearGradient]s.
///
- /// If either gradient is null, this function linearly interpolates from a
+ /// If either gradient is null, this function linearly interpolates from
/// a gradient that matches the other gradient in [begin], [end], [stops] and
/// [tileMode] and with the same [colors] but transparent (using [scale]).
///
@@ -493,7 +486,6 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static LinearGradient? lerp(LinearGradient? a, LinearGradient? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -644,10 +636,7 @@
this.focal,
this.focalRadius = 0.0,
super.transform,
- }) : assert(center != null),
- assert(radius != null),
- assert(tileMode != null),
- assert(focalRadius != null);
+ });
/// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
/// square describing the gradient which will be mapped onto the paint box.
@@ -758,7 +747,7 @@
/// Linearly interpolate between two [RadialGradient]s.
///
- /// If either gradient is null, this function linearly interpolates from a
+ /// If either gradient is null, this function linearly interpolates from
/// a gradient that matches the other gradient in [center], [radius], [stops] and
/// [tileMode] and with the same [colors] but transparent (using [scale]).
///
@@ -776,7 +765,6 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static RadialGradient? lerp(RadialGradient? a, RadialGradient? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -943,10 +931,7 @@
super.stops,
this.tileMode = TileMode.clamp,
super.transform,
- }) : assert(center != null),
- assert(startAngle != null),
- assert(endAngle != null),
- assert(tileMode != null);
+ });
/// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
/// square describing the gradient which will be mapped onto the paint box.
@@ -1047,7 +1032,6 @@
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static SweepGradient? lerp(SweepGradient? a, SweepGradient? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
diff --git a/framework/lib/src/painting/image_cache.dart b/framework/lib/src/painting/image_cache.dart
index e86eaa5..648ceea 100644
--- a/framework/lib/src/painting/image_cache.dart
+++ b/framework/lib/src/painting/image_cache.dart
@@ -99,7 +99,6 @@
/// returning it to its original value will therefore immediately clear the
/// cache.
set maximumSize(int value) {
- assert(value != null);
assert(value >= 0);
if (value == maximumSize) {
return;
@@ -139,7 +138,6 @@
/// returning it to its original value will therefore immediately clear the
/// cache.
set maximumSizeBytes(int value) {
- assert(value != null);
assert(value >= 0);
if (value == _maximumSizeBytes) {
return;
@@ -240,7 +238,6 @@
///
/// * [ImageProvider], for providing images to the [Image] widget.
bool evict(Object key, { bool includeLive = true }) {
- assert(includeLive != null);
if (includeLive) {
// Remove from live images - the cache will not be able to mark
// it as complete, and it might be getting evicted because it
@@ -324,8 +321,6 @@
/// no completers are cached and `null` is returned instead of a new
/// completer.
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
- assert(key != null);
- assert(loader != null);
TimelineTask? timelineTask;
TimelineTask? listenerTask;
if (!kReleaseMode) {
@@ -613,8 +608,7 @@
_CachedImageBase(
this.completer, {
this.sizeBytes,
- }) : assert(completer != null),
- handle = completer.keepAlive();
+ }) : handle = completer.keepAlive();
final ImageStreamCompleter completer;
int? sizeBytes;
diff --git a/framework/lib/src/painting/image_provider.dart b/framework/lib/src/painting/image_provider.dart
index da46c3b..fbc9ccc 100644
--- a/framework/lib/src/painting/image_provider.dart
+++ b/framework/lib/src/painting/image_provider.dart
@@ -4,7 +4,7 @@
import 'dart:async';
import 'dart:io';
-import 'package:engine/ui.dart' as ui show Codec, ImmutableBuffer;
+import 'package:engine/ui.dart' as ui;
import 'package:engine/ui.dart' show Locale, Size, TextDirection;
import 'package:flute/foundation.dart';
@@ -175,12 +175,11 @@
/// * [ResizeImage], which uses this to override the `cacheWidth`,
/// `cacheHeight`, and `allowUpscaling` parameters.
@Deprecated(
- 'Use DecoderBufferCallback with ImageProvider.loadBuffer instead. '
+ 'Use ImageDecoderCallback with ImageProvider.loadImage instead. '
'This feature was deprecated after v2.13.0-1.0.pre.',
)
typedef DecoderCallback = Future<ui.Codec> Function(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling});
-
/// Performs the decode process for use in [ImageProvider.loadBuffer].
///
/// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and
@@ -191,8 +190,25 @@
///
/// * [ResizeImage], which uses this to override the `cacheWidth`,
/// `cacheHeight`, and `allowUpscaling` parameters.
+@Deprecated(
+ 'Use ImageDecoderCallback with ImageProvider.loadImage instead. '
+ 'This feature was deprecated after v3.7.0-1.4.pre.',
+)
typedef DecoderBufferCallback = Future<ui.Codec> Function(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling});
+/// Performs the decode process for use in [ImageProvider.loadImage].
+///
+/// This callback allows decoupling of the `getTargetSize` parameter from
+/// implementations of [ImageProvider] that do not expose it.
+///
+/// See also:
+///
+/// * [ResizeImage], which uses this to load images at specific sizes.
+typedef ImageDecoderCallback = Future<ui.Codec> Function(
+ ui.ImmutableBuffer buffer, {
+ ui.TargetImageSizeCallback? getTargetSize,
+});
+
/// Identifies an image without committing to the precise final asset. This
/// allows a set of images to be identified and for the precise image to later
/// be resolved based on the environment, e.g. the device pixel ratio.
@@ -348,7 +364,6 @@
/// See the Lifecycle documentation on [ImageProvider] for more information.
@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
- assert(configuration != null);
final ImageStream stream = createStream(configuration);
// Load the key (potentially asynchronously), set up an error handling zone,
// and call resolveStreamForKey.
@@ -407,7 +422,6 @@
required ImageConfiguration configuration,
ImageErrorListener? handleError,
}) {
- assert(configuration != null);
final Completer<ImageCacheStatus?> completer = Completer<ImageCacheStatus?>();
_createErrorHandlerAndKey(
configuration,
@@ -455,9 +469,9 @@
return;
}
if (!didError) {
+ didError = true;
errorCallback(obtainedKey, exception, stack);
}
- didError = true;
}
Future<T> key;
@@ -510,7 +524,22 @@
}
final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
- () => loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer),
+ () {
+ ImageStreamCompleter result = loadImage(key, PaintingBinding.instance.instantiateImageCodecWithSize);
+ // This check exists as a fallback for backwards compatibility until the
+ // deprecated `loadBuffer()` method is removed. Until then, ImageProvider
+ // subclasses may have only overridden `loadBuffer()`, in which case the
+ // base implementation of `loadWithSize()` will return a sentinel value
+ // of type `_AbstractImageStreamCompleter`.
+ if (result is _AbstractImageStreamCompleter) {
+ result = loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer);
+ if (result is _AbstractImageStreamCompleter) {
+ // Same fallback as above but for the deprecated `load()` method.
+ result = load(key, PaintingBinding.instance.instantiateImageCodec);
+ }
+ }
+ return result;
+ },
onError: handleError,
);
if (completer != null) {
@@ -594,7 +623,7 @@
/// * [ResizeImage], for modifying the key to account for cache dimensions.
@protected
@Deprecated(
- 'Implement loadBuffer for faster image loading. '
+ 'Implement loadImage for faster image loading. '
'This feature was deprecated after v2.13.0-1.0.pre.',
)
ImageStreamCompleter load(T key, DecoderCallback decode) {
@@ -604,9 +633,10 @@
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
/// image.
///
- /// For backwards-compatibility the default implementation of this method calls
- /// through to [ImageProvider.load]. However, implementors of this interface should
- /// only override this method and not [ImageProvider.load], which is deprecated.
+ /// For backwards-compatibility the default implementation of this method returns
+ /// an object that will cause [resolveStreamForKey] to consult [load]. However,
+ /// implementors of this interface should only override this method and not
+ /// [load], which is deprecated.
///
/// The [decode] callback provides the logic to obtain the codec for the
/// image.
@@ -615,14 +645,42 @@
///
/// * [ResizeImage], for modifying the key to account for cache dimensions.
@protected
+ @Deprecated(
+ 'Implement loadImage for image loading. '
+ 'This feature was deprecated after v3.7.0-1.4.pre.',
+ )
ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) {
- return load(key, PaintingBinding.instance.instantiateImageCodec);
+ return _AbstractImageStreamCompleter();
+ }
+
+ /// Converts a key into an [ImageStreamCompleter], and begins fetching the
+ /// image.
+ ///
+ /// For backwards-compatibility the default implementation of this method returns
+ /// an object that will cause [resolveStreamForKey] to consult [loadBuffer].
+ /// However, implementors of this interface should only override this method
+ /// and not [loadBuffer], which is deprecated.
+ ///
+ /// The [decode] callback provides the logic to obtain the codec for the
+ /// image.
+ ///
+ /// See also:
+ ///
+ /// * [ResizeImage], for modifying the key to account for cache dimensions.
+ // TODO(tvolkert): make abstract (https://github.com/flutter/flutter/issues/119209)
+ @protected
+ ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
+ return _AbstractImageStreamCompleter();
}
@override
String toString() => '${objectRuntimeType(this, 'ImageConfiguration')}()';
}
+/// A class that exists to facilitate backwards compatibility in the transition
+/// from [ImageProvider.load] to [ImageProvider.loadBuffer] to [ImageProvider.loadImage]
+class _AbstractImageStreamCompleter extends ImageStreamCompleter {}
+
/// Key for the image obtained by an [AssetImage] or [ExactAssetImage].
///
/// This is used to identify the precise resource in the [imageCache].
@@ -635,9 +693,7 @@
required this.bundle,
required this.name,
required this.scale,
- }) : assert(bundle != null),
- assert(name != null),
- assert(scale != null);
+ });
/// The bundle from which the image will be obtained.
///
@@ -679,6 +735,24 @@
/// const constructors so that they can be used in const expressions.
const AssetBundleImageProvider();
+ @override
+ ImageStreamCompleter loadImage(AssetBundleImageKey key, ImageDecoderCallback decode) {
+ InformationCollector? collector;
+ assert(() {
+ collector = () => <DiagnosticsNode>[
+ DiagnosticsProperty<ImageProvider>('Image provider', this),
+ DiagnosticsProperty<AssetBundleImageKey>('Image key', key),
+ ];
+ return true;
+ }());
+ return MultiFrameImageStreamCompleter(
+ codec: _loadAsync(key, decode: decode),
+ scale: key.scale,
+ debugLabel: key.name,
+ informationCollector: collector,
+ );
+ }
+
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
/// image.
@override
@@ -692,7 +766,7 @@
return true;
}());
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key, decode, null),
+ codec: _loadAsync(key, decodeBufferDeprecated: decode),
scale: key.scale,
debugLabel: key.name,
informationCollector: collector,
@@ -710,7 +784,7 @@
return true;
}());
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key, null, decode),
+ codec: _loadAsync(key, decodeDeprecated: decode),
scale: key.scale,
debugLabel: key.name,
informationCollector: collector,
@@ -722,9 +796,14 @@
///
/// This function is used by [load].
@protected
- Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderBufferCallback? decode, DecoderCallback? decodeDepreacted) async {
+ Future<ui.Codec> _loadAsync(
+ AssetBundleImageKey key, {
+ ImageDecoderCallback? decode,
+ DecoderBufferCallback? decodeBufferDeprecated,
+ DecoderCallback? decodeDeprecated,
+ }) async {
if (decode != null) {
- ui.ImmutableBuffer? buffer;
+ ui.ImmutableBuffer buffer;
// Hot reload/restart could change whether an asset bundle or key in a
// bundle are available, or if it is a network backed bundle.
try {
@@ -733,12 +812,20 @@
PaintingBinding.instance.imageCache.evict(key);
rethrow;
}
- if (buffer == null) {
- PaintingBinding.instance.imageCache.evict(key);
- throw StateError('Unable to read data');
- }
return decode(buffer);
}
+ if (decodeBufferDeprecated != null) {
+ ui.ImmutableBuffer buffer;
+ // Hot reload/restart could change whether an asset bundle or key in a
+ // bundle are available, or if it is a network backed bundle.
+ try {
+ buffer = await key.bundle.loadBuffer(key.name);
+ } on FlutterError {
+ PaintingBinding.instance.imageCache.evict(key);
+ rethrow;
+ }
+ return decodeBufferDeprecated(buffer);
+ }
ByteData data;
// Hot reload/restart could change whether an asset bundle or key in a
// bundle are available, or if it is a network backed bundle.
@@ -748,11 +835,7 @@
PaintingBinding.instance.imageCache.evict(key);
rethrow;
}
- if (data == null) {
- PaintingBinding.instance.imageCache.evict(key);
- throw StateError('Unable to read data');
- }
- return decodeDepreacted!(data.buffer.asUint8List());
+ return decodeDeprecated!(data.buffer.asUint8List());
}
}
@@ -803,8 +886,7 @@
this.width,
this.height,
this.allowUpscaling = false,
- }) : assert(width != null || height != null),
- assert(allowUpscaling != null);
+ }) : assert(width != null || height != null);
/// The [ImageProvider] that this class wraps.
final ImageProvider imageProvider;
@@ -863,6 +945,7 @@
);
return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
}
+
final ImageStreamCompleter completer = imageProvider.loadBuffer(key._providerCacheKey, decodeResize);
if (!kReleaseMode) {
completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
@@ -871,6 +954,36 @@
}
@override
+ ImageStreamCompleter loadImage(ResizeImageKey key, ImageDecoderCallback decode) {
+ Future<ui.Codec> decodeResize(ui.ImmutableBuffer buffer, {ui.TargetImageSizeCallback? getTargetSize}) {
+ assert(
+ getTargetSize == null,
+ 'ResizeImage cannot be composed with another ImageProvider that applies '
+ '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;
+ }
+ }
+ return ui.TargetImageSize(width: targetWidth, height: targetHeight);
+ });
+ }
+
+ final ImageStreamCompleter completer = imageProvider.loadImage(key._providerCacheKey, decodeResize);
+ if (!kReleaseMode) {
+ completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
+ }
+ return completer;
+ }
+
+ @override
Future<ResizeImageKey> obtainKey(ImageConfiguration configuration) {
Completer<ResizeImageKey>? completer;
// If the imageProvider.obtainKey future is synchronous, then we will be able to fill in result with
@@ -901,9 +1014,10 @@
/// The image will be cached regardless of cache headers from the server.
///
/// When a network image is used on the Web platform, the `cacheWidth` and
-/// `cacheHeight` parameters of the [DecoderCallback] are ignored as the Web
-/// engine delegates image decoding of network images to the Web, which does
-/// not support custom decode sizes.
+/// `cacheHeight` parameters of the [DecoderCallback] are only supported when the
+/// application is running with the CanvasKit renderer. When the application is using
+/// the HTML renderer, the web engine delegates image decoding of network images to the Web,
+/// which does not support custom decode sizes.
///
/// See also:
///
@@ -933,6 +1047,9 @@
@override
ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode);
+
+ @override
+ ImageStreamCompleter loadImage(NetworkImage key, ImageDecoderCallback decode);
}
/// Decodes the given [File] object as an image, associating it with the given
@@ -949,9 +1066,7 @@
/// Creates an object that decodes a [File] as an image.
///
/// The arguments must not be null.
- const FileImage(this.file, { this.scale = 1.0 })
- : assert(file != null),
- assert(scale != null);
+ const FileImage(this.file, { this.scale = 1.0 });
/// The file to decode into an image.
final File file;
@@ -967,7 +1082,7 @@
@override
ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key, null, decode),
+ codec: _loadAsync(key, decodeDeprecated: decode),
scale: key.scale,
debugLabel: key.file.path,
informationCollector: () => <DiagnosticsNode>[
@@ -979,7 +1094,7 @@
@override
ImageStreamCompleter loadBuffer(FileImage key, DecoderBufferCallback decode) {
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key, decode, null),
+ codec: _loadAsync(key, decodeBufferDeprecated: decode),
scale: key.scale,
debugLabel: key.file.path,
informationCollector: () => <DiagnosticsNode>[
@@ -988,7 +1103,25 @@
);
}
- Future<ui.Codec> _loadAsync(FileImage key, DecoderBufferCallback? decode, DecoderCallback? decodeDeprecated) async {
+ @override
+ @protected
+ ImageStreamCompleter loadImage(FileImage key, ImageDecoderCallback decode) {
+ return MultiFrameImageStreamCompleter(
+ codec: _loadAsync(key, decode: decode),
+ scale: key.scale,
+ debugLabel: key.file.path,
+ informationCollector: () => <DiagnosticsNode>[
+ ErrorDescription('Path: ${file.path}'),
+ ],
+ );
+ }
+
+ Future<ui.Codec> _loadAsync(
+ FileImage key, {
+ ImageDecoderCallback? decode,
+ DecoderBufferCallback? decodeBufferDeprecated,
+ DecoderCallback? decodeDeprecated,
+ }) async {
assert(key == this);
// TODO(jonahwilliams): making this sync caused test failures that seem to
@@ -1007,6 +1140,12 @@
}
return decode(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes()));
}
+ if (decodeBufferDeprecated != null) {
+ if (file.runtimeType == File) {
+ return decodeBufferDeprecated(await ui.ImmutableBuffer.fromFilePath(file.path));
+ }
+ return decodeBufferDeprecated(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes()));
+ }
return decodeDeprecated!(await file.readAsBytes());
}
@@ -1044,9 +1183,7 @@
/// Creates an object that decodes a [Uint8List] buffer as an image.
///
/// The arguments must not be null.
- const MemoryImage(this.bytes, { this.scale = 1.0 })
- : assert(bytes != null),
- assert(scale != null);
+ const MemoryImage(this.bytes, { this.scale = 1.0 });
/// The bytes to decode into an image.
///
@@ -1074,7 +1211,7 @@
@override
ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key, null, decode),
+ codec: _loadAsync(key, decodeDeprecated: decode),
scale: key.scale,
debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
);
@@ -1083,19 +1220,37 @@
@override
ImageStreamCompleter loadBuffer(MemoryImage key, DecoderBufferCallback decode) {
return MultiFrameImageStreamCompleter(
- codec: _loadAsync(key, decode, null),
+ codec: _loadAsync(key, decodeBufferDeprecated: decode),
scale: key.scale,
debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
);
}
- Future<ui.Codec> _loadAsync(MemoryImage key, DecoderBufferCallback? decode, DecoderCallback? decodeDepreacted) async {
+ @override
+ ImageStreamCompleter loadImage(MemoryImage key, ImageDecoderCallback decode) {
+ return MultiFrameImageStreamCompleter(
+ codec: _loadAsync(key, decode: decode),
+ scale: key.scale,
+ debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
+ );
+ }
+
+ Future<ui.Codec> _loadAsync(
+ MemoryImage key, {
+ ImageDecoderCallback? decode,
+ DecoderBufferCallback? decodeBufferDeprecated,
+ DecoderCallback? decodeDeprecated,
+ }) async {
assert(key == this);
if (decode != null) {
final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return decode(buffer);
}
- return decodeDepreacted!(bytes);
+ if (decodeBufferDeprecated != null) {
+ final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
+ return decodeBufferDeprecated(buffer);
+ }
+ return decodeDeprecated!(bytes);
}
@override
@@ -1203,8 +1358,7 @@
this.scale = 1.0,
this.bundle,
this.package,
- }) : assert(assetName != null),
- assert(scale != null);
+ });
/// The name of the asset.
final String assetName;
@@ -1265,9 +1419,7 @@
/// Creates a [NetworkImageLoadException] with the specified http [statusCode]
/// and [uri].
NetworkImageLoadException({required this.statusCode, required this.uri})
- : assert(uri != null),
- assert(statusCode != null),
- _message = 'HTTP request failed, statusCode: $statusCode, $uri';
+ : _message = 'HTTP request failed, statusCode: $statusCode, $uri';
/// The HTTP status code from the server.
final int statusCode;
diff --git a/framework/lib/src/painting/image_resolution.dart b/framework/lib/src/painting/image_resolution.dart
index 4895c6b..b7e88ee 100644
--- a/framework/lib/src/painting/image_resolution.dart
+++ b/framework/lib/src/painting/image_resolution.dart
@@ -244,7 +244,7 @@
this.assetName, {
this.bundle,
this.package,
- }) : assert(assetName != null);
+ });
/// The name of the main asset from the set of images to choose from. See the
/// documentation for the [AssetImage] class itself for details.
diff --git a/framework/lib/src/painting/image_stream.dart b/framework/lib/src/painting/image_stream.dart
index d151696..272af2c 100644
--- a/framework/lib/src/painting/image_stream.dart
+++ b/framework/lib/src/painting/image_stream.dart
@@ -23,9 +23,7 @@
/// Both the [image] and the [scale] must not be null.
///
/// The [debugLabel] may be used to identify the source of this image.
- const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel })
- : assert(image != null),
- assert(scale != null);
+ const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel });
/// Creates an [ImageInfo] with a cloned [image].
///
@@ -167,7 +165,7 @@
this.onImage, {
this.onChunk,
this.onError,
- }) : assert(onImage != null);
+ });
/// Callback for getting notified that an image is available.
///
@@ -623,7 +621,6 @@
///
/// This callback will never fire if [removeListener] is never called.
void addOnLastListenerRemovedCallback(VoidCallback callback) {
- assert(callback != null);
_checkDisposed();
_onLastListenerRemovedCallbacks.add(callback);
}
@@ -631,7 +628,6 @@
/// Removes a callback previously supplied to
/// [addOnLastListenerRemovedCallback].
void removeOnLastListenerRemovedCallback(VoidCallback callback) {
- assert(callback != null);
_checkDisposed();
_onLastListenerRemovedCallbacks.remove(callback);
}
@@ -789,8 +785,7 @@
/// argument on [FlutterErrorDetails] set to true, meaning that by default the
/// message is only dumped to the console in debug mode (see [
/// FlutterErrorDetails]).
- OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector? informationCollector })
- : assert(image != null) {
+ OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector? informationCollector }) {
image.then<void>(setImage, onError: (Object error, StackTrace stack) {
reportError(
context: ErrorDescription('resolving a single-frame image stream'),
@@ -859,8 +854,7 @@
String? debugLabel,
Stream<ImageChunkEvent>? chunkEvents,
InformationCollector? informationCollector,
- }) : assert(codec != null),
- _informationCollector = informationCollector,
+ }) : _informationCollector = informationCollector,
_scale = scale {
this.debugLabel = debugLabel;
codec.then<void>(_handleCodecReady, onError: (Object error, StackTrace stack) {
diff --git a/framework/lib/src/painting/inline_span.dart b/framework/lib/src/painting/inline_span.dart
index e9e9cc1..1182338 100644
--- a/framework/lib/src/painting/inline_span.dart
+++ b/framework/lib/src/painting/inline_span.dart
@@ -60,9 +60,7 @@
this.semanticsLabel,
this.stringAttributes = const <ui.StringAttribute>[],
this.recognizer,
- }) : assert(text != null),
- assert(isPlaceholder != null),
- assert(isPlaceholder == false || (text == '\uFFFC' && semanticsLabel == null && recognizer == null)),
+ }) : assert(isPlaceholder == false || (text == '\uFFFC' && semanticsLabel == null && recognizer == null)),
requiresOwnNode = isPlaceholder || recognizer != null;
/// The text info for a [PlaceholderSpan].
diff --git a/framework/lib/src/painting/linear_border.dart b/framework/lib/src/painting/linear_border.dart
new file mode 100644
index 0000000..6fbd234
--- /dev/null
+++ b/framework/lib/src/painting/linear_border.dart
@@ -0,0 +1,389 @@
+// 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:engine/ui.dart' show lerpDouble;
+
+import 'package:flute/foundation.dart';
+
+import 'basic_types.dart';
+import 'borders.dart';
+import 'edge_insets.dart';
+
+/// Defines the relative size and alignment of one <LinearBorder> edge.
+///
+/// A [LinearBorder] defines a box outline as zero to four edges, each
+/// of which is rendered as a single line. The width and color of the
+/// lines is defined by [LinearBorder.side].
+///
+/// Each line's length is defined by [size], a value between 0.0 and 1.0
+/// (the default) which defines the length as a percentage of the
+/// length of a box edge.
+///
+/// When [size] is less than 1.0, the line is aligned within the
+/// available space according to [alignment], a value between -1.0 and
+/// 1.0. The default is 0.0, which means centered, -1.0 means align on the
+/// "start" side, and 1.0 means align on the "end" side. The meaning of
+/// start and end depend on the current [TextDirection], see
+/// [Directionality].
+@immutable
+class LinearBorderEdge {
+ /// Defines one side of a [LinearBorder].
+ ///
+ /// The values of [size] and [alignment] must be between
+ /// 0.0 and 1.0, and -1.0 and 1.0 respectively.
+ const LinearBorderEdge({
+ this.size = 1.0,
+ this.alignment = 0.0,
+ }) : assert(size >= 0.0 && size <= 1.0);
+
+ /// A value between 0.0 and 1.0 that defines the length of the edge as a
+ /// percentage of the length of the corresponding box
+ /// edge. Default is 1.0.
+ final double size;
+
+ /// A value between -1.0 and 1.0 that defines how edges for which [size]
+ /// is less than 1.0 are aligned relative to the corresponding box edge.
+ ///
+ /// * -1.0, aligned in the "start" direction. That's left
+ /// for [TextDirection.ltr] and right for [TextDirection.rtl].
+ /// * 0.0, centered.
+ /// * 1.0, aligned in the "end" direction. That's right
+ /// for [TextDirection.ltr] and left for [TextDirection.rtl].
+ final double alignment;
+
+ /// Linearly interpolates between two [LinearBorder]s.
+ ///
+ /// If both `a` and `b` are null then null is returned. If `a` is null
+ /// then we interpolate to `b` varying [size] from 0.0 to `b.size`. If `b`
+ /// 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;
+ }
+
+ a ??= LinearBorderEdge(alignment: b!.alignment, size: 0);
+ b ??= LinearBorderEdge(alignment: a.alignment, size: 0);
+
+ return LinearBorderEdge(
+ size: lerpDouble(a.size, b.size, t)!,
+ alignment: lerpDouble(a.alignment, b.alignment, t)!,
+ );
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is LinearBorderEdge
+ && other.size == size
+ && other.alignment == alignment;
+ }
+
+ @override
+ int get hashCode => Object.hash(size, alignment);
+
+ @override
+ String toString() {
+ final StringBuffer s = StringBuffer('${objectRuntimeType(this, 'LinearBorderEdge')}(');
+ if (size != 1.0 ) {
+ s.write('size: $size');
+ }
+ if (alignment != 0) {
+ final String comma = size != 1.0 ? ', ' : '';
+ s.write('${comma}alignment: $alignment');
+ }
+ s.write(')');
+ return s.toString();
+ }
+}
+
+/// An [OutlinedBorder] like [BoxBorder] that allows one to define a rectangular (box) border
+/// in terms of zero to four [LinearBorderEdge]s, each of which is rendered as a single line.
+///
+/// The color and width of each line are defined by [side]. When [LinearBorder] is used
+/// with a class whose border sides and shape are defined by a [ButtonStyle], then a non-null
+/// [ButtonStyle.side] will override the one specified here. For example the [LinearBorder]
+/// in the [TextButton] example below adds a red underline to the button. This is because
+/// TextButton's `side` parameter overrides the `side` property of its [ButtonStyle.shape].
+///
+/// ```dart
+/// TextButton(
+/// style: TextButton.styleFrom(
+/// side: const BorderSide(color: Colors.red),
+/// shape: const LinearBorder(
+/// side: BorderSide(color: Colors.blue),
+/// bottom: LinearBorderEdge(),
+/// ),
+/// ),
+/// onPressed: () { },
+/// child: const Text('Red LinearBorder'),
+/// )
+///```
+///
+/// This class resolves itself against the current [TextDirection] (see [Directionality]).
+/// Start and end values resolve to left and right for [TextDirection.ltr] and to
+/// right and left for [TextDirection.rtl].
+///
+/// Convenience constructors are included for the common case where just one edge is specified:
+/// [LinearBorder.start], [LinearBorder.end], [LinearBorder.top], [LinearBorder.bottom].
+class LinearBorder extends OutlinedBorder {
+ /// Creates a rectangular box border that's rendered as zero to four lines.
+ const LinearBorder({
+ super.side,
+ this.start,
+ this.end,
+ this.top,
+ this.bottom,
+ });
+
+ /// Creates a rectangular box border with an edge on the left for [TextDirection.ltr]
+ /// or on the right for [TextDirection.rtl].
+ LinearBorder.start({
+ super.side,
+ double alignment = 0.0,
+ double size = 1.0
+ }) : start = LinearBorderEdge(alignment: alignment, size: size),
+ end = null,
+ top = null,
+ bottom = null;
+
+ /// Creates a rectangular box border with an edge on the right for [TextDirection.ltr]
+ /// or on the left for [TextDirection.rtl].
+ LinearBorder.end({
+ super.side,
+ double alignment = 0.0,
+ double size = 1.0
+ }) : start = null,
+ end = LinearBorderEdge(alignment: alignment, size: size),
+ top = null,
+ bottom = null;
+
+ /// Creates a rectangular box border with an edge on the top.
+ LinearBorder.top({
+ super.side,
+ double alignment = 0.0,
+ double size = 1.0
+ }) : start = null,
+ end = null,
+ top = LinearBorderEdge(alignment: alignment, size: size),
+ bottom = null;
+
+ /// Creates a rectangular box border with an edge on the bottom.
+ LinearBorder.bottom({
+ super.side,
+ double alignment = 0.0,
+ double size = 1.0
+ }) : start = null,
+ end = null,
+ top = null,
+ bottom = LinearBorderEdge(alignment: alignment, size: size);
+
+ /// No border.
+ static const LinearBorder none = LinearBorder();
+
+ /// Defines the left edge for [TextDirection.ltr] or the right
+ /// for [TextDirection.rtl].
+ final LinearBorderEdge? start;
+
+ /// Defines the right edge for [TextDirection.ltr] or the left
+ /// for [TextDirection.rtl].
+ final LinearBorderEdge? end;
+
+ /// Defines the top edge.
+ final LinearBorderEdge? top;
+
+ /// Defines the bottom edge.
+ final LinearBorderEdge? bottom;
+
+ @override
+ LinearBorder scale(double t) {
+ return LinearBorder(
+ side: side.scale(t),
+ );
+ }
+
+ @override
+ EdgeInsetsGeometry get dimensions {
+ final double width = side.width;
+ return EdgeInsetsDirectional.fromSTEB(
+ start == null ? 0.0 : width,
+ top == null ? 0.0 : width,
+ end == null ? 0.0 : width,
+ bottom == null ? 0.0 : width,
+ );
+ }
+
+ @override
+ ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
+ if (a is LinearBorder) {
+ return LinearBorder(
+ side: BorderSide.lerp(a.side, side, t),
+ start: LinearBorderEdge.lerp(a.start, start, t),
+ end: LinearBorderEdge.lerp(a.end, end, t),
+ top: LinearBorderEdge.lerp(a.top, top, t),
+ bottom: LinearBorderEdge.lerp(a.bottom, bottom, t),
+ );
+ }
+ return super.lerpFrom(a, t);
+ }
+
+ @override
+ ShapeBorder? lerpTo(ShapeBorder? b, double t) {
+ if (b is LinearBorder) {
+ return LinearBorder(
+ side: BorderSide.lerp(side, b.side, t),
+ start: LinearBorderEdge.lerp(start, b.start, t),
+ end: LinearBorderEdge.lerp(end, b.end, t),
+ top: LinearBorderEdge.lerp(top, b.top, t),
+ bottom: LinearBorderEdge.lerp(bottom, b.bottom, t),
+ );
+ }
+ return super.lerpTo(b, t);
+ }
+
+ /// Returns a copy of this LinearBorder with the given fields replaced with
+ /// the new values.
+ @override
+ LinearBorder copyWith({
+ BorderSide? side,
+ LinearBorderEdge? start,
+ LinearBorderEdge? end,
+ LinearBorderEdge? top,
+ LinearBorderEdge? bottom,
+ }) {
+ return LinearBorder(
+ side: side ?? this.side,
+ start: start ?? this.start,
+ end: end ?? this.end,
+ top: top ?? this.top,
+ bottom: bottom ?? this.bottom,
+ );
+ }
+
+ @override
+ Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
+ final Rect adjustedRect = dimensions.resolve(textDirection).deflateRect(rect);
+ return Path()
+ ..addRect(adjustedRect);
+ }
+
+ @override
+ Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
+ return Path()
+ ..addRect(rect);
+ }
+
+ @override
+ void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
+ final EdgeInsets insets = dimensions.resolve(textDirection);
+ final bool rtl = textDirection == TextDirection.rtl;
+
+ final Path path = Path();
+ final Paint paint = Paint()
+ ..strokeWidth = 0.0;
+
+ void drawEdge(Rect rect, Color color) {
+ paint.color = color;
+ path.reset();
+ path.moveTo(rect.left, rect.top);
+ if (rect.width == 0.0) {
+ paint.style = PaintingStyle.stroke;
+ path.lineTo(rect.left, rect.bottom);
+ } else if (rect.height == 0.0) {
+ paint.style = PaintingStyle.stroke;
+ path.lineTo(rect.right, rect.top);
+ } else {
+ paint.style = PaintingStyle.fill;
+ path.lineTo(rect.right, rect.top);
+ path.lineTo(rect.right, rect.bottom);
+ path.lineTo(rect.left, rect.bottom);
+ }
+ canvas.drawPath(path, paint);
+ }
+
+ if (start != null && start!.size != 0.0 && side.style != BorderStyle.none) {
+ final Rect insetRect = Rect.fromLTWH(rect.left, rect.top + insets.top, rect.width, rect.height - insets.vertical);
+ final double x = rtl ? rect.right - insets.right : rect.left;
+ final double width = rtl ? insets.right : insets.left;
+ final double height = insetRect.height * start!.size;
+ final double y = (insetRect.height - height) * ((start!.alignment + 1.0) / 2.0);
+ final Rect r = Rect.fromLTWH(x, y, width, height);
+ drawEdge(r, side.color);
+ }
+
+ if (end != null && end!.size != 0.0 && side.style != BorderStyle.none) {
+ final Rect insetRect = Rect.fromLTWH(rect.left, rect.top + insets.top, rect.width, rect.height - insets.vertical);
+ final double x = rtl ? rect.left : rect.right - insets.right;
+ final double width = rtl ? insets.left : insets.right;
+ final double height = insetRect.height * end!.size;
+ final double y = (insetRect.height - height) * ((end!.alignment + 1.0) / 2.0);
+ final Rect r = Rect.fromLTWH(x, y, width, height);
+ drawEdge(r, side.color);
+ }
+
+ if (top != null && top!.size != 0.0 && side.style != BorderStyle.none) {
+ final double width = rect.width * top!.size;
+ final double startX = (rect.width - width) * ((top!.alignment + 1.0) / 2.0);
+ final double x = rtl ? rect.width - startX - width : startX;
+ final Rect r = Rect.fromLTWH(x, rect.top, width, insets.top);
+ drawEdge(r, side.color);
+ }
+
+ if (bottom != null && bottom!.size != 0.0 && side.style != BorderStyle.none) {
+ final double width = rect.width * bottom!.size;
+ final double startX = (rect.width - width) * ((bottom!.alignment + 1.0) / 2.0);
+ final double x = rtl ? rect.width - startX - width: startX;
+ final Rect r = Rect.fromLTWH(x, rect.bottom - insets.bottom, width, side.width);
+ drawEdge(r, side.color);
+ }
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is LinearBorder
+ && other.side == side
+ && other.start == start
+ && other.end == end
+ && other.top == top
+ && other.bottom == bottom;
+ }
+
+ @override
+ int get hashCode => Object.hash(side, start, end, top, bottom);
+
+ @override
+ String toString() {
+ if (this == LinearBorder.none) {
+ return 'LinearBorder.none';
+ }
+
+ final StringBuffer s = StringBuffer('${objectRuntimeType(this, 'LinearBorder')}(side: $side');
+
+ if (start != null ) {
+ s.write(', start: $start');
+ }
+ if (end != null ) {
+ s.write(', end: $end');
+ }
+ if (top != null ) {
+ s.write(', top: $top');
+ }
+ if (bottom != null ) {
+ s.write(', bottom: $bottom');
+ }
+ s.write(')');
+ return s.toString();
+ }
+}
diff --git a/framework/lib/src/painting/matrix_utils.dart b/framework/lib/src/painting/matrix_utils.dart
index 04beff2..13b11e3 100644
--- a/framework/lib/src/painting/matrix_utils.dart
+++ b/framework/lib/src/painting/matrix_utils.dart
@@ -18,7 +18,6 @@
///
/// Otherwise, returns null.
static Offset? getAsTranslation(Matrix4 transform) {
- assert(transform != null);
final Float64List values = transform.storage;
// Values are stored in column-major order.
if (values[0] == 1.0 && // col 1
@@ -45,7 +44,6 @@
///
/// Otherwise, returns null.
static double? getAsScale(Matrix4 transform) {
- assert(transform != null);
final Float64List values = transform.storage;
// Values are stored in column-major order.
if (values[1] == 0.0 && // col 1 (value 0 is the scale)
@@ -81,7 +79,6 @@
if (b == null) {
return isIdentity(a);
}
- assert(a != null && b != null);
return a.storage[0] == b.storage[0]
&& a.storage[1] == b.storage[1]
&& a.storage[2] == b.storage[2]
@@ -102,7 +99,6 @@
/// Whether the given matrix is the identity matrix.
static bool isIdentity(Matrix4 a) {
- assert(a != null);
return a.storage[0] == 1.0 // col 1
&& a.storage[1] == 0.0
&& a.storage[2] == 0.0
@@ -437,7 +433,6 @@
/// The transformed rect is then projected back into the plane with z equals
/// 0.0 before computing its bounding rect.
static Rect inverseTransformRect(Matrix4 transform, Rect rect) {
- assert(rect != null);
// As exposed by `unrelated_type_equality_checks`, this assert was a no-op.
// Fixing it introduces a bunch of runtime failures; for more context see:
// https://github.com/flutter/flutter/pull/31568
@@ -455,8 +450,8 @@
///
/// The `radius` simulates the radius of the cylinder the plane is being
/// wrapped onto. If the transformation is applied to a 0-dimensional dot
- /// instead of a plane, the dot would simply translate by +/- `radius` pixels
- /// along the `orientation` [Axis] when rotating from 0 to +/- 90 degrees.
+ /// instead of a plane, the dot would translate by ± `radius` pixels
+ /// along the `orientation` [Axis] when rotating from 0 to ±90 degrees.
///
/// A positive radius means the object is closest at 0 `angle` and a negative
/// radius means the object is closest at π `angle` or 180 degrees.
@@ -478,7 +473,7 @@
/// The `orientation` is the direction of the rotation axis.
///
/// Because the viewing position is a point, it's never possible to see the
- /// outer side of the cylinder at or past +/- π / 2 or 90 degrees and it's
+ /// outer side of the cylinder at or past ±π/2 or 90 degrees and it's
/// almost always possible to end up seeing the inner side of the cylinder
/// or the back side of the transformed plane before π / 2 when perspective > 0.
static Matrix4 createCylindricalProjectionTransform({
@@ -487,10 +482,7 @@
double perspective = 0.001,
Axis orientation = Axis.vertical,
}) {
- assert(radius != null);
- assert(angle != null);
assert(perspective >= 0 && perspective <= 1.0);
- assert(orientation != null);
// Pre-multiplied matrix of a projection matrix and a view matrix.
//
@@ -561,8 +553,7 @@
super.showName,
super.defaultValue,
super.level,
- }) : assert(showName != null),
- assert(level != null);
+ });
@override
String valueToString({ TextTreeConfiguration? parentConfiguration }) {
diff --git a/framework/lib/src/painting/rounded_rectangle_border.dart b/framework/lib/src/painting/rounded_rectangle_border.dart
index baf1d41..36e4e5c 100644
--- a/framework/lib/src/painting/rounded_rectangle_border.dart
+++ b/framework/lib/src/painting/rounded_rectangle_border.dart
@@ -30,8 +30,7 @@
const RoundedRectangleBorder({
super.side,
this.borderRadius = BorderRadius.zero,
- }) : assert(side != null),
- assert(borderRadius != null);
+ });
/// The radii for each corner.
final BorderRadiusGeometry borderRadius;
@@ -46,7 +45,6 @@
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is RoundedRectangleBorder) {
return RoundedRectangleBorder(
side: BorderSide.lerp(a.side, side, t),
@@ -66,7 +64,6 @@
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
- assert(t != null);
if (b is RoundedRectangleBorder) {
return RoundedRectangleBorder(
side: BorderSide.lerp(side, b.side, t),
@@ -165,9 +162,7 @@
this.borderRadius = BorderRadius.zero,
required this.circularity,
required this.eccentricity,
- }) : assert(side != null),
- assert(borderRadius != null),
- assert(circularity != null);
+ });
final BorderRadiusGeometry borderRadius;
final double circularity;
@@ -185,7 +180,6 @@
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is RoundedRectangleBorder) {
return _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(a.side, side, t),
diff --git a/framework/lib/src/painting/shape_decoration.dart b/framework/lib/src/painting/shape_decoration.dart
index de32872..43fc655 100644
--- a/framework/lib/src/painting/shape_decoration.dart
+++ b/framework/lib/src/painting/shape_decoration.dart
@@ -76,8 +76,7 @@
this.gradient,
this.shadows,
required this.shape,
- }) : assert(!(color != null && gradient != null)),
- assert(shape != null);
+ }) : assert(!(color != null && gradient != null));
/// Creates a shape decoration configured to match a [BoxDecoration].
///
@@ -91,7 +90,6 @@
/// transition from a [BoxShape.circle] to [BoxShape.rectangle]).
factory ShapeDecoration.fromBoxDecoration(BoxDecoration source) {
final ShapeBorder shape;
- assert(source.shape != null);
switch (source.shape) {
case BoxShape.circle:
if (source.border != null) {
@@ -227,7 +225,6 @@
/// 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) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -299,8 +296,7 @@
/// An object that paints a [ShapeDecoration] into a canvas.
class _ShapeDecorationPainter extends BoxPainter {
_ShapeDecorationPainter(this._decoration, VoidCallback onChanged)
- : assert(_decoration != null),
- super(onChanged);
+ : super(onChanged);
final ShapeDecoration _decoration;
@@ -318,7 +314,6 @@
VoidCallback get onChanged => super.onChanged!;
void _precache(Rect rect, TextDirection? textDirection) {
- assert(rect != null);
if (rect == _lastRect && textDirection == _lastTextDirection) {
return;
}
@@ -409,7 +404,6 @@
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
- assert(configuration != null);
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection? textDirection = configuration.textDirection;
diff --git a/framework/lib/src/painting/stadium_border.dart b/framework/lib/src/painting/stadium_border.dart
index 0866aa5..48a181f 100644
--- a/framework/lib/src/painting/stadium_border.dart
+++ b/framework/lib/src/painting/stadium_border.dart
@@ -27,14 +27,13 @@
/// Create a stadium border.
///
/// The [side] argument must not be null.
- const StadiumBorder({ super.side }) : assert(side != null);
+ const StadiumBorder({ super.side });
@override
ShapeBorder scale(double t) => StadiumBorder(side: side.scale(t));
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is StadiumBorder) {
return StadiumBorder(side: BorderSide.lerp(a.side, side, t));
}
@@ -57,7 +56,6 @@
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
- assert(t != null);
if (b is StadiumBorder) {
return StadiumBorder(side: BorderSide.lerp(side, b.side, t));
}
@@ -144,8 +142,7 @@
super.side,
this.circularity = 0.0,
required this.eccentricity,
- }) : assert(side != null),
- assert(circularity != null);
+ });
final double circularity;
final double eccentricity;
@@ -161,7 +158,6 @@
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is StadiumBorder) {
return _StadiumToCircleBorder(
side: BorderSide.lerp(a.side, side, t),
@@ -188,7 +184,6 @@
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
- assert(t != null);
if (b is StadiumBorder) {
return _StadiumToCircleBorder(
side: BorderSide.lerp(side, b.side, t),
@@ -326,9 +321,7 @@
super.side,
this.borderRadius = BorderRadius.zero,
this.rectilinearity = 0.0,
- }) : assert(side != null),
- assert(borderRadius != null),
- assert(rectilinearity != null);
+ });
final BorderRadiusGeometry borderRadius;
@@ -345,7 +338,6 @@
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
- assert(t != null);
if (a is StadiumBorder) {
return _StadiumToRoundedRectangleBorder(
side: BorderSide.lerp(a.side, side, t),
@@ -372,7 +364,6 @@
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
- assert(t != null);
if (b is StadiumBorder) {
return _StadiumToRoundedRectangleBorder(
side: BorderSide.lerp(side, b.side, t),
diff --git a/framework/lib/src/painting/star_border.dart b/framework/lib/src/painting/star_border.dart
index 18b9735..eba09ee 100644
--- a/framework/lib/src/painting/star_border.dart
+++ b/framework/lib/src/painting/star_border.dart
@@ -468,8 +468,8 @@
required this.rotation,
required this.squash,
}) : assert(points > 1),
- assert(innerRadiusRatio == null || innerRadiusRatio <= 1),
- assert(innerRadiusRatio == null || innerRadiusRatio >= 0),
+ assert(innerRadiusRatio <= 1),
+ assert(innerRadiusRatio >= 0),
assert(squash >= 0),
assert(squash <= 1),
assert(pointRounding >= 0),
@@ -484,7 +484,6 @@
final double valleyRounding;
final double rotation;
final double squash;
- bool get isStar => innerRadiusRatio != null;
Path generate(Rect rect) {
final double radius = rect.shortestSide / 2;
diff --git a/framework/lib/src/painting/strut_style.dart b/framework/lib/src/painting/strut_style.dart
index c9ec5c1..b90c49a 100644
--- a/framework/lib/src/painting/strut_style.dart
+++ b/framework/lib/src/painting/strut_style.dart
@@ -347,8 +347,7 @@
this.forceStrutHeight,
String? debugLabel,
String? package,
- }) : assert(textStyle != null),
- assert(fontSize == null || fontSize > 0),
+ }) : assert(fontSize == null || fontSize > 0),
assert(leading == null || leading >= 0),
assert(package == null || fontFamily != null || fontFamilyFallback != null),
fontFamily = fontFamily != null ? (package == null ? fontFamily : 'packages/$package/$fontFamily') : textStyle.fontFamily,
diff --git a/framework/lib/src/painting/text_painter.dart b/framework/lib/src/painting/text_painter.dart
index d418868..fdd064d 100644
--- a/framework/lib/src/painting/text_painter.dart
+++ b/framework/lib/src/painting/text_painter.dart
@@ -74,8 +74,7 @@
required this.alignment,
this.baseline,
this.baselineOffset,
- }) : assert(size != null),
- assert(alignment != null);
+ });
/// A constant representing an empty placeholder.
static const PlaceholderDimensions empty = PlaceholderDimensions(size: Size.zero, alignment: ui.PlaceholderAlignment.bottom);
@@ -144,6 +143,133 @@
longestLine,
}
+/// A [TextBoundary] subclass for locating word breaks.
+///
+/// The underlying implementation uses [UAX #29](https://unicode.org/reports/tr29/)
+/// defined default word boundaries.
+///
+/// The default word break rules can be tailored to meet the requirements of
+/// different use cases. For instance, the default rule set keeps horizontal
+/// whitespaces together as a single word, which may not make sense in a
+/// word-counting context -- "hello world" counts as 3 words instead of 2.
+/// An example is the [moveByWordBoundary] variant, which is a tailored
+/// word-break locator that more closely matches the default behavior of most
+/// platforms and editors when it comes to handling text editing keyboard
+/// shortcuts that move or delete word by word.
+class WordBoundary extends TextBoundary {
+ /// Creates a [WordBoundary] with the text and layout information.
+ WordBoundary._(this._text, this._paragraph);
+
+ final InlineSpan _text;
+ final ui.Paragraph _paragraph;
+
+ @override
+ TextRange getTextBoundaryAt(int position) => _paragraph.getWordBoundary(TextPosition(offset: max(position, 0)));
+
+ // Combines two UTF-16 code units (high surrogate + low surrogate) into a
+ // single code point that represents a supplementary character.
+ static int _codePointFromSurrogates(int highSurrogate, int lowSurrogate) {
+ assert(
+ TextPainter._isHighSurrogate(highSurrogate),
+ 'U+${highSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a high surrogate.',
+ );
+ assert(
+ TextPainter._isLowSurrogate(lowSurrogate),
+ 'U+${lowSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a low surrogate.',
+ );
+ const int base = 0x010000 - (0xD800 << 10) - 0xDC00;
+ return (highSurrogate << 10) + lowSurrogate + base;
+ }
+
+ // The Runes class does not provide random access with a code unit offset.
+ int? _codePointAt(int index) {
+ final int? codeUnitAtIndex = _text.codeUnitAt(index);
+ if (codeUnitAtIndex == null) {
+ return null;
+ }
+ switch (codeUnitAtIndex & 0xFC00) {
+ case 0xD800:
+ return _codePointFromSurrogates(codeUnitAtIndex, _text.codeUnitAt(index + 1)!);
+ case 0xDC00:
+ return _codePointFromSurrogates(_text.codeUnitAt(index - 1)!, codeUnitAtIndex);
+ default:
+ return codeUnitAtIndex;
+ }
+ }
+
+ static bool _isNewline(int codePoint) {
+ switch (codePoint) {
+ case 0x000A:
+ case 0x0085:
+ case 0x000B:
+ case 0x000C:
+ case 0x2028:
+ case 0x2029:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ bool _skipSpacesAndPunctuations(int offset, bool forward) {
+ // Use code point since some punctuations are supplementary characters.
+ // "inner" here refers to the code unit that's before the break in the
+ // search direction (`forward`).
+ final int? innerCodePoint = _codePointAt(forward ? offset - 1 : offset);
+ final int? outerCodeUnit = _text.codeUnitAt(forward ? offset : offset - 1);
+
+ // Make sure the hard break rules in UAX#29 take precedence over the ones we
+ // add below. Luckily there're only 4 hard break rules for word breaks, and
+ // dictionary based breaking does not introduce new hard breaks:
+ // https://unicode-org.github.io/icu/userguide/boundaryanalysis/break-rules.html#word-dictionaries
+ //
+ // WB1 & WB2: always break at the start or the end of the text.
+ final bool hardBreakRulesApply = innerCodePoint == null || outerCodeUnit == null
+ // WB3a & WB3b: always break before and after newlines.
+ || _isNewline(innerCodePoint) || _isNewline(outerCodeUnit);
+ return hardBreakRulesApply || !RegExp(r'[\p{Space_Separator}\p{Punctuation}]', unicode: true).hasMatch(String.fromCharCode(innerCodePoint));
+ }
+
+ /// Returns a [TextBoundary] suitable for handling keyboard navigation
+ /// commands that change the current selection word by word.
+ ///
+ /// This [TextBoundary] is used by text widgets in the flutter framework to
+ /// provide default implementation for text editing shortcuts, for example,
+ /// "delete to the previous word".
+ ///
+ /// The implementation applies the same set of rules [WordBoundary] uses,
+ /// except that word breaks end on a space separator or a punctuation will be
+ /// skipped, to match the behavior of most platforms. Additional rules may be
+ /// added in the future to better match platform behaviors.
+ late final TextBoundary moveByWordBoundary = _UntilTextBoundary(this, _skipSpacesAndPunctuations);
+}
+
+class _UntilTextBoundary extends TextBoundary {
+ const _UntilTextBoundary(this._textBoundary, this._predicate);
+
+ final UntilPredicate _predicate;
+ final TextBoundary _textBoundary;
+
+ @override
+ int? getLeadingTextBoundaryAt(int position) {
+ if (position < 0) {
+ return null;
+ }
+ final int? offset = _textBoundary.getLeadingTextBoundaryAt(position);
+ return offset == null || _predicate(offset, false)
+ ? offset
+ : getLeadingTextBoundaryAt(offset - 1);
+ }
+
+ @override
+ int? getTrailingTextBoundaryAt(int position) {
+ final int? offset = _textBoundary.getTrailingTextBoundaryAt(max(position, 0));
+ return offset == null || _predicate(offset, true)
+ ? offset
+ : getTrailingTextBoundaryAt(offset);
+ }
+}
+
/// This is used to cache and pass the computed metrics regarding the
/// caret's size and position. This is preferred due to the expensive
/// nature of the calculation.
@@ -202,10 +328,7 @@
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
ui.TextHeightBehavior? textHeightBehavior,
}) : assert(text == null || text.debugAssertIsValid()),
- assert(textAlign != null),
- assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0),
- assert(textWidthBasis != null),
_text = text,
_textAlign = textAlign,
_textDirection = textDirection,
@@ -400,7 +523,6 @@
TextAlign get textAlign => _textAlign;
TextAlign _textAlign;
set textAlign(TextAlign value) {
- assert(value != null);
if (_textAlign == value) {
return;
}
@@ -444,7 +566,6 @@
double get textScaleFactor => _textScaleFactor;
double _textScaleFactor;
set textScaleFactor(double value) {
- assert(value != null);
if (_textScaleFactor == value) {
return;
}
@@ -539,7 +660,6 @@
TextWidthBasis get textWidthBasis => _textWidthBasis;
TextWidthBasis _textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
- assert(value != null);
if (_textWidthBasis == value) {
return;
}
@@ -607,7 +727,6 @@
ui.ParagraphStyle _createParagraphStyle([ TextDirection? defaultTextDirection ]) {
// The defaultTextDirection argument is used for preferredLineHeight in case
// textDirection hasn't yet been set.
- assert(textAlign != null);
assert(textDirection != null || defaultTextDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
return _text!.style?.getParagraphStyle(
textAlign: textAlign,
@@ -720,7 +839,6 @@
/// Valid only after [layout] has been called.
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugAssertTextLayoutIsValid);
- assert(baseline != null);
switch (baseline) {
case TextBaseline.alphabetic:
return _paragraph!.alphabeticBaseline;
@@ -750,7 +868,7 @@
// Creates a ui.Paragraph using the current configurations in this class and
// assign it to _paragraph.
- void _createParagraph() {
+ ui.Paragraph _createParagraph() {
assert(_paragraph == null || _rebuildParagraphForPaint);
final InlineSpan? text = this.text;
if (text == null) {
@@ -763,8 +881,9 @@
_debugMarkNeedsLayoutCallStack = null;
return true;
}());
- _paragraph = builder.build();
+ final ui.Paragraph paragraph = _paragraph = builder.build();
_rebuildParagraphForPaint = false;
+ return paragraph;
}
void _layoutParagraph(double minWidth, double maxWidth) {
@@ -861,13 +980,18 @@
canvas.drawParagraph(_paragraph!, offset);
}
- // Returns true iff the given value is a valid UTF-16 surrogate. The value
+ // Returns true iff the given value is a valid UTF-16 high surrogate. The value
// must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF.
//
// See also:
// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
- static bool _isUtf16Surrogate(int value) {
- return value & 0xF800 == 0xD800;
+ static bool _isHighSurrogate(int value) {
+ return value & 0xFC00 == 0xD800;
+ }
+
+ // Whether the given UTF-16 code unit is a low (second) surrogate.
+ static bool _isLowSurrogate(int value) {
+ return value & 0xFC00 == 0xDC00;
}
// Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
@@ -886,7 +1010,7 @@
return null;
}
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
- return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
+ return _isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1;
}
/// Returns the closest offset before `offset` at which the input cursor can
@@ -897,7 +1021,7 @@
return null;
}
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
- return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
+ return _isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1;
}
// Unicode value for a zero width joiner character.
@@ -916,7 +1040,7 @@
const int NEWLINE_CODE_UNIT = 10;
// Check for multi-code-unit glyphs such as emojis or zero width joiner.
- final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
+ final bool needsSearch = _isHighSurrogate(prevCodeUnit) || _isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) {
@@ -966,7 +1090,7 @@
final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1));
// Check for multi-code-unit glyphs such as emojis or zero width joiner
- final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
+ final bool needsSearch = _isHighSurrogate(nextCodeUnit) || _isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) {
@@ -1001,7 +1125,6 @@
Offset get _emptyOffset {
assert(_debugAssertTextLayoutIsValid); // implies textDirection is non-null
- assert(textAlign != null);
switch (textAlign) {
case TextAlign.left:
return Offset.zero;
@@ -1066,7 +1189,6 @@
return;
}
final int offset = position.offset;
- assert(position.affinity != null);
Rect? rect;
switch (position.affinity) {
case TextAffinity.upstream: {
@@ -1111,8 +1233,6 @@
ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
}) {
assert(_debugAssertTextLayoutIsValid);
- assert(boxHeightStyle != null);
- assert(boxWidthStyle != null);
return _paragraph!.getBoxesForRange(
selection.start,
selection.end,
@@ -1141,6 +1261,18 @@
return _paragraph!.getWordBoundary(position);
}
+ /// {@template flutter.painting.TextPainter.wordBoundaries}
+ /// Returns a [TextBoundary] that can be used to perform word boundary analysis
+ /// on the current [text].
+ ///
+ /// This [TextBoundary] uses word boundary rules defined in [Unicode Standard
+ /// Annex #29](http://www.unicode.org/reports/tr29/#Word_Boundaries).
+ /// {@endtemplate}
+ ///
+ /// Currently word boundary analysis can only be performed after [layout]
+ /// has been called.
+ WordBoundary get wordBoundaries => WordBoundary._(text!, _paragraph!);
+
/// Returns the text range of the line at the given offset.
///
/// The newline (if any) is not returned as part of the range.
diff --git a/framework/lib/src/painting/text_span.dart b/framework/lib/src/painting/text_span.dart
index 9dfab07..846d426 100644
--- a/framework/lib/src/painting/text_span.dart
+++ b/framework/lib/src/painting/text_span.dart
@@ -291,7 +291,6 @@
}
if (children != null) {
for (final InlineSpan child in children!) {
- assert(child != null);
child.build(
builder,
textScaleFactor: textScaleFactor,
@@ -452,18 +451,6 @@
assert(() {
if (children != null) {
for (final InlineSpan child in children!) {
- if (child == null) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('TextSpan contains a null child.'),
- ErrorDescription(
- 'A TextSpan object with a non-null child list should not have any nulls in its child list.',
- ),
- toDiagnosticsNode(
- name: 'The full text in question was',
- style: DiagnosticsTreeStyle.errorProperty,
- ),
- ]);
- }
assert(child.debugAssertIsValid());
}
}
@@ -590,14 +577,7 @@
return const <DiagnosticsNode>[];
}
return children!.map<DiagnosticsNode>((InlineSpan child) {
- // `child` has a non-nullable return type, but might be null when running
- // with weak checking, so we need to null check it anyway (and ignore the
- // warning that the null-handling logic is dead code).
- if (child != null) {
- return child.toDiagnosticsNode();
- } else {
- return DiagnosticsNode.message('<null child>');
- }
+ return child.toDiagnosticsNode();
}).toList();
}
}
diff --git a/framework/lib/src/painting/text_style.dart b/framework/lib/src/painting/text_style.dart
index 2cdc7bc..2b390a5 100644
--- a/framework/lib/src/painting/text_style.dart
+++ b/framework/lib/src/painting/text_style.dart
@@ -504,7 +504,6 @@
}) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily',
_fontFamilyFallback = fontFamilyFallback,
_package = package,
- assert(inherit != null),
assert(color == null || foreground == null, _kColorForegroundWarning),
assert(backgroundColor == null || background == null, _kColorBackgroundWarning);
@@ -897,7 +896,7 @@
decorationThickness: decorationThickness ?? this.decorationThickness,
debugLabel: newDebugLabel,
fontFamily: fontFamily ?? _fontFamily,
- fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback,
+ fontFamilyFallback: fontFamilyFallback ?? _fontFamilyFallback,
package: package ?? _package,
overflow: overflow ?? this.overflow,
);
@@ -961,21 +960,10 @@
String? package,
TextOverflow? overflow,
}) {
- assert(fontSizeFactor != null);
- assert(fontSizeDelta != null);
assert(fontSize != null || (fontSizeFactor == 1.0 && fontSizeDelta == 0.0));
- assert(fontWeightDelta != null);
assert(fontWeight != null || fontWeightDelta == 0.0);
- assert(letterSpacingFactor != null);
- assert(letterSpacingDelta != null);
assert(letterSpacing != null || (letterSpacingFactor == 1.0 && letterSpacingDelta == 0.0));
- assert(wordSpacingFactor != null);
- assert(wordSpacingDelta != null);
assert(wordSpacing != null || (wordSpacingFactor == 1.0 && wordSpacingDelta == 0.0));
- assert(heightFactor != null);
- assert(heightDelta != null);
- assert(decorationThicknessFactor != null);
- assert(decorationThicknessDelta != null);
assert(decorationThickness != null || (decorationThicknessFactor == 1.0 && decorationThicknessDelta == 0.0));
String? modifiedDebugLabel;
@@ -991,7 +979,7 @@
color: foreground == null ? color ?? this.color : null,
backgroundColor: background == null ? backgroundColor ?? this.backgroundColor : null,
fontFamily: fontFamily ?? _fontFamily,
- fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback,
+ fontFamilyFallback: fontFamilyFallback ?? _fontFamilyFallback,
fontSize: fontSize == null ? null : fontSize! * fontSizeFactor + fontSizeDelta,
fontWeight: fontWeight == null ? null : FontWeight.values[(fontWeight!.index + fontWeightDelta).clamp(0, FontWeight.values.length - 1)], // ignore_clamp_double_lint
fontStyle: fontStyle ?? this.fontStyle,
@@ -1106,7 +1094,6 @@
/// 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) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -1332,7 +1319,6 @@
double? height,
StrutStyle? strutStyle,
}) {
- assert(textScaleFactor != null);
assert(maxLines == null || maxLines > 0);
final ui.TextLeadingDistribution? leadingDistribution = this.leadingDistribution;
final ui.TextHeightBehavior? effectiveTextHeightBehavior = textHeightBehavior
diff --git a/framework/lib/src/physics/clamped_simulation.dart b/framework/lib/src/physics/clamped_simulation.dart
index f5e4116..a7b23e1 100644
--- a/framework/lib/src/physics/clamped_simulation.dart
+++ b/framework/lib/src/physics/clamped_simulation.dart
@@ -36,8 +36,7 @@
this.xMax = double.infinity,
this.dxMin = double.negativeInfinity,
this.dxMax = double.infinity,
- }) : assert(simulation != null),
- assert(xMax >= xMin),
+ }) : assert(xMax >= xMin),
assert(dxMax >= dxMin);
/// The simulation being clamped. Calls to [x], [dx], and [isDone] are
diff --git a/framework/lib/src/physics/gravity_simulation.dart b/framework/lib/src/physics/gravity_simulation.dart
index 0a1204b..7655a0a 100644
--- a/framework/lib/src/physics/gravity_simulation.dart
+++ b/framework/lib/src/physics/gravity_simulation.dart
@@ -70,11 +70,7 @@
double distance,
double endDistance,
double velocity,
- ) : assert(acceleration != null),
- assert(distance != null),
- assert(velocity != null),
- assert(endDistance != null),
- assert(endDistance >= 0),
+ ) : assert(endDistance >= 0),
_a = acceleration,
_x = distance,
_v = velocity,
diff --git a/framework/lib/src/physics/spring_simulation.dart b/framework/lib/src/physics/spring_simulation.dart
index 0722f26..dbd8153 100644
--- a/framework/lib/src/physics/spring_simulation.dart
+++ b/framework/lib/src/physics/spring_simulation.dart
@@ -153,12 +153,6 @@
double initialPosition,
double initialVelocity,
) {
- assert(spring != null);
- assert(spring.mass != null);
- assert(spring.stiffness != null);
- assert(spring.damping != null);
- assert(initialPosition != null);
- assert(initialVelocity != null);
final double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.stiffness;
if (cmk == 0.0) {
return _CriticalSolution(spring, initialPosition, initialVelocity);
diff --git a/framework/lib/src/physics/utils.dart b/framework/lib/src/physics/utils.dart
index 7b9501b..7a8f82e 100644
--- a/framework/lib/src/physics/utils.dart
+++ b/framework/lib/src/physics/utils.dart
@@ -8,7 +8,6 @@
/// The `a` and `b` arguments may be null. A null value is only considered
/// near-equal to another null value.
bool nearEqual(double? a, double? b, double epsilon) {
- assert(epsilon != null);
assert(epsilon >= 0.0);
if (a == null || b == null) {
return a == b;
diff --git a/framework/lib/src/rendering/animated_size.dart b/framework/lib/src/rendering/animated_size.dart
index cc8a677..f72992a 100644
--- a/framework/lib/src/rendering/animated_size.dart
+++ b/framework/lib/src/rendering/animated_size.dart
@@ -82,11 +82,7 @@
super.textDirection,
super.child,
Clip clipBehavior = Clip.hardEdge,
- }) : assert(vsync != null),
- assert(duration != null),
- assert(curve != null),
- assert(clipBehavior != null),
- _vsync = vsync,
+ }) : _vsync = vsync,
_clipBehavior = clipBehavior {
_controller = AnimationController(
vsync: vsync,
@@ -119,7 +115,6 @@
/// The duration of the animation.
Duration get duration => _controller.duration!;
set duration(Duration value) {
- assert(value != null);
if (value == _controller.duration) {
return;
}
@@ -138,7 +133,6 @@
/// The curve of the animation.
Curve get curve => _animation.curve;
set curve(Curve value) {
- assert(value != null);
if (value == _animation.curve) {
return;
}
@@ -151,7 +145,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -169,7 +162,6 @@
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
- assert(value != null);
if (value == _vsync) {
return;
}
@@ -218,7 +210,6 @@
child!.layout(constraints, parentUsesSize: true);
- assert(_state != null);
switch (_state) {
case RenderAnimatedSizeState.start:
_layoutStart();
@@ -253,7 +244,6 @@
// size without modifying global state. See performLayout for comments
// explaining the rational behind the implementation.
final Size childSize = child!.getDryLayout(constraints);
- assert(_state != null);
switch (_state) {
case RenderAnimatedSizeState.start:
return constraints.constrain(childSize);
diff --git a/framework/lib/src/rendering/binding.dart b/framework/lib/src/rendering/binding.dart
index efb0407..da8c459 100644
--- a/framework/lib/src/rendering/binding.dart
+++ b/framework/lib/src/rendering/binding.dart
@@ -43,7 +43,6 @@
..onSemanticsAction = _handleSemanticsAction;
initRenderView();
_handleSemanticsEnabledChanged();
- assert(renderView != null);
addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker();
if (kIsWeb) {
@@ -234,7 +233,6 @@
/// Sets the given [RenderView] object (which must not be null), and its tree, to
/// be the new render tree to display. The previous tree, if any, is detached.
set renderView(RenderView value) {
- assert(value != null);
_pipelineOwner.rootNode = value;
}
@@ -244,7 +242,6 @@
@protected
@visibleForTesting
void handleMetricsChanged() {
- assert(renderView != null);
renderView.configuration = createViewConfiguration();
if (renderView.child != null) {
scheduleForcedFrame();
@@ -512,7 +509,6 @@
// When editing the above, also update widgets/binding.dart's copy.
@protected
void drawFrame() {
- assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
@@ -544,9 +540,6 @@
@override
void hitTest(HitTestResult result, Offset position) {
- assert(renderView != null);
- assert(result != null);
- assert(position != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
@@ -620,7 +613,6 @@
/// This binding does not automatically schedule any frames. Callers are
/// responsible for deciding when to first call [scheduleFrame].
RenderingFlutterBinding({ RenderBox? root }) {
- assert(renderView != null);
renderView.child = root;
}
diff --git a/framework/lib/src/rendering/box.dart b/framework/lib/src/rendering/box.dart
index 9528cbb..057a831 100644
--- a/framework/lib/src/rendering/box.dart
+++ b/framework/lib/src/rendering/box.dart
@@ -96,10 +96,7 @@
this.maxWidth = double.infinity,
this.minHeight = 0.0,
this.maxHeight = double.infinity,
- }) : assert(minWidth != null),
- assert(maxWidth != null),
- assert(minHeight != null),
- assert(maxHeight != null);
+ });
/// Creates box constraints that is respected only by the given size.
BoxConstraints.tight(Size size)
@@ -190,7 +187,6 @@
/// Returns new box constraints that are smaller by the given edge dimensions.
BoxConstraints deflate(EdgeInsets edges) {
- assert(edges != null);
assert(debugAssertIsValid());
final double horizontal = edges.horizontal;
final double vertical = edges.vertical;
@@ -472,7 +468,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static BoxConstraints? lerp(BoxConstraints? a, BoxConstraints? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -762,8 +757,6 @@
required Offset position,
required BoxHitTest hitTest,
}) {
- assert(position != null);
- assert(hitTest != null);
if (transform != null) {
transform = Matrix4.tryInvert(PointerEvent.removePerspectiveTransform(transform));
if (transform == null) {
@@ -801,8 +794,6 @@
required Offset position,
required BoxHitTest hitTest,
}) {
- assert(position != null);
- assert(hitTest != null);
final Offset transformedPosition = offset == null ? position : position - offset;
if (offset != null) {
pushOffset(-offset);
@@ -838,9 +829,6 @@
required Offset position,
required BoxHitTest hitTest,
}) {
- assert(position != null);
- assert(hitTest != null);
- assert(position != null);
final Offset transformedPosition = transform == null ?
position : MatrixUtils.transformPoint(transform, position);
if (transform != null) {
@@ -887,7 +875,6 @@
Matrix4? rawTransform,
required BoxHitTestWithOutOfBandPosition hitTest,
}) {
- assert(hitTest != null);
assert(
(paintOffset == null && paintTransform == null && rawTransform != null) ||
(paintOffset == null && paintTransform != null && rawTransform == null) ||
@@ -915,8 +902,7 @@
/// Creates a box hit test entry.
///
/// The [localPosition] argument must not be null.
- BoxHitTestEntry(super.target, this.localPosition)
- : assert(localPosition != null);
+ BoxHitTestEntry(super.target, this.localPosition);
/// The position of the hit test in the local coordinates of [target].
final Offset localPosition;
@@ -1001,7 +987,6 @@
/// AxisDirection get axis => _axis;
/// AxisDirection _axis = AxisDirection.down; // or initialized in constructor
/// set axis(AxisDirection value) {
-/// assert(value != null); // same checks as in the constructor
/// if (value == _axis) {
/// return;
/// }
@@ -1452,13 +1437,6 @@
@mustCallSuper
double getMinIntrinsicWidth(double height) {
assert(() {
- if (height == null) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('The height argument to getMinIntrinsicWidth was null.'),
- ErrorDescription('The argument to getMinIntrinsicWidth must not be negative or null.'),
- ErrorHint('If you do not have a specific height in mind, then pass double.infinity instead.'),
- ]);
- }
if (height < 0.0) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The height argument to getMinIntrinsicWidth was negative.'),
@@ -1601,13 +1579,6 @@
@mustCallSuper
double getMaxIntrinsicWidth(double height) {
assert(() {
- if (height == null) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('The height argument to getMaxIntrinsicWidth was null.'),
- ErrorDescription('The argument to getMaxIntrinsicWidth must not be negative or null.'),
- ErrorHint('If you do not have a specific height in mind, then pass double.infinity instead.'),
- ]);
- }
if (height < 0.0) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The height argument to getMaxIntrinsicWidth was negative.'),
@@ -1684,13 +1655,6 @@
@mustCallSuper
double getMinIntrinsicHeight(double width) {
assert(() {
- if (width == null) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('The width argument to getMinIntrinsicHeight was null.'),
- ErrorDescription('The argument to getMinIntrinsicHeight must not be negative or null.'),
- ErrorHint('If you do not have a specific width in mind, then pass double.infinity instead.'),
- ]);
- }
if (width < 0.0) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The width argument to getMinIntrinsicHeight was negative.'),
@@ -1766,13 +1730,6 @@
@mustCallSuper
double getMaxIntrinsicHeight(double width) {
assert(() {
- if (width == null) {
- throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('The width argument to getMaxIntrinsicHeight was null.'),
- ErrorDescription('The argument to getMaxIntrinsicHeight must not be negative or null.'),
- ErrorHint('If you do not have a specific width in mind, then pass double.infinity instead.'),
- ]);
- }
if (width < 0.0) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('The width argument to getMaxIntrinsicHeight was negative.'),
@@ -1936,7 +1893,7 @@
/// may depend on the baseline of a child (which is not available without
/// doing a full layout), it may be computed by a callback about which the
/// render object cannot reason, or the layout is so complex that it
- /// is simply impractical to calculate the size in an efficient way.
+ /// is impractical to calculate the size in an efficient way.
///
/// In such cases, it may be impossible (or at least impractical) to actually
/// return a valid answer. In such cases, the function should call
@@ -1964,7 +1921,7 @@
/// When asserts are enabled and [debugCheckingIntrinsics] is not true, this
/// method will either throw the provided [FlutterError] or it will create and
/// throw a [FlutterError] with the provided `reason`. Otherwise, it will
- /// simply return true.
+ /// return true.
///
/// One of the arguments has to be provided.
///
@@ -2253,7 +2210,6 @@
@override
void debugAssertDoesMeetConstraints() {
- assert(constraints != null);
assert(() {
if (!hasSize) {
final DiagnosticsNode contract;
@@ -2582,7 +2538,6 @@
/// child's [parentData] in the [BoxParentData.offset] field.
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- assert(child != null);
assert(child.parent == this);
assert(() {
if (child.parentData is! BoxParentData) {
diff --git a/framework/lib/src/rendering/custom_layout.dart b/framework/lib/src/rendering/custom_layout.dart
index e029bff..7a7c126 100644
--- a/framework/lib/src/rendering/custom_layout.dart
+++ b/framework/lib/src/rendering/custom_layout.dart
@@ -187,11 +187,6 @@
'There is no child with the id "$childId".',
);
}
- if (offset == null) {
- throw FlutterError(
- 'The $this custom multichild layout delegate provided a null position for the child with id "$childId".',
- );
- }
return true;
}());
final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
@@ -311,8 +306,7 @@
RenderCustomMultiChildLayoutBox({
List<RenderBox>? children,
required MultiChildLayoutDelegate delegate,
- }) : assert(delegate != null),
- _delegate = delegate {
+ }) : _delegate = delegate {
addAll(children);
}
@@ -327,7 +321,6 @@
MultiChildLayoutDelegate get delegate => _delegate;
MultiChildLayoutDelegate _delegate;
set delegate(MultiChildLayoutDelegate newDelegate) {
- assert(newDelegate != null);
if (_delegate == newDelegate) {
return;
}
diff --git a/framework/lib/src/rendering/custom_paint.dart b/framework/lib/src/rendering/custom_paint.dart
index 32a79be..43d68bb 100644
--- a/framework/lib/src/rendering/custom_paint.dart
+++ b/framework/lib/src/rendering/custom_paint.dart
@@ -294,8 +294,7 @@
required this.properties,
this.transform,
this.tags,
- }) : assert(rect != null),
- assert(properties != null);
+ });
/// Identifies this object in a list of siblings.
///
@@ -371,8 +370,7 @@
this.isComplex = false,
this.willChange = false,
RenderBox? child,
- }) : assert(preferredSize != null),
- _painter = painter,
+ }) : _painter = painter,
_foregroundPainter = foregroundPainter,
_preferredSize = preferredSize,
super(child);
@@ -467,7 +465,6 @@
Size get preferredSize => _preferredSize;
Size _preferredSize;
set preferredSize(Size value) {
- assert(value != null);
if (preferredSize == value) {
return;
}
diff --git a/framework/lib/src/rendering/editable.dart b/framework/lib/src/rendering/editable.dart
index 1a1c6e3..407b88c 100644
--- a/framework/lib/src/rendering/editable.dart
+++ b/framework/lib/src/rendering/editable.dart
@@ -42,8 +42,7 @@
/// Creates a description of a point in a text selection.
///
/// The [point] argument must not be null.
- const TextSelectionPoint(this.point, this.direction)
- : assert(point != null);
+ const TextSelectionPoint(this.point, this.direction);
/// Coordinates of the lower left or lower right corner of the selection,
/// relative to the top left of the [RenderEditable] object.
@@ -325,37 +324,19 @@
RenderEditablePainter? painter,
RenderEditablePainter? foregroundPainter,
List<RenderBox>? children,
- }) : assert(textAlign != null),
- assert(textDirection != null, 'RenderEditable created without a textDirection.'),
- assert(maxLines == null || maxLines > 0),
+ }) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
- assert(startHandleLayerLink != null),
- assert(endHandleLayerLink != null),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
- assert(textScaleFactor != null),
- assert(offset != null),
- assert(ignorePointer != null),
- assert(textWidthBasis != null),
- assert(paintCursorAboveText != null),
- assert(obscuringCharacter != null && obscuringCharacter.characters.length == 1),
- assert(obscureText != null),
- assert(textSelectionDelegate != null),
- assert(cursorWidth != null && cursorWidth >= 0.0),
+ assert(obscuringCharacter.characters.length == 1),
+ assert(cursorWidth >= 0.0),
assert(cursorHeight == null || cursorHeight >= 0.0),
- assert(readOnly != null),
- assert(forceLine != null),
- assert(devicePixelRatio != null),
- assert(selectionHeightStyle != null),
- assert(selectionWidthStyle != null),
- assert(clipBehavior != null),
_textPainter = TextPainter(
text: text,
textAlign: textAlign,
@@ -386,7 +367,6 @@
_forceLine = forceLine,
_clipBehavior = clipBehavior,
_hasFocus = hasFocus ?? false {
- assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null);
_selectionPainter.highlightColor = selectionColor;
@@ -592,7 +572,6 @@
/// {@macro flutter.painting.textPainter.textWidthBasis}
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
- assert(value != null);
if (_textPainter.textWidthBasis == value) {
return;
}
@@ -622,7 +601,7 @@
if (_obscuringCharacter == value) {
return;
}
- assert(value != null && value.characters.length == 1);
+ assert(value.characters.length == 1);
_obscuringCharacter = value;
markNeedsLayout();
}
@@ -852,7 +831,6 @@
/// This must not be null.
TextAlign get textAlign => _textPainter.textAlign;
set textAlign(TextAlign value) {
- assert(value != null);
if (_textPainter.textAlign == value) {
return;
}
@@ -878,7 +856,6 @@
// set it to null here, so _textPainter.textDirection cannot be null.
TextDirection get textDirection => _textPainter.textDirection!;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textPainter.textDirection == value) {
return;
}
@@ -936,7 +913,6 @@
ValueNotifier<bool> get showCursor => _showCursor;
ValueNotifier<bool> _showCursor;
set showCursor(ValueNotifier<bool> value) {
- assert(value != null);
if (_showCursor == value) {
return;
}
@@ -958,7 +934,6 @@
bool get hasFocus => _hasFocus;
bool _hasFocus = false;
set hasFocus(bool value) {
- assert(value != null);
if (_hasFocus == value) {
return;
}
@@ -970,7 +945,6 @@
bool get forceLine => _forceLine;
bool _forceLine = false;
set forceLine(bool value) {
- assert(value != null);
if (_forceLine == value) {
return;
}
@@ -982,7 +956,6 @@
bool get readOnly => _readOnly;
bool _readOnly = false;
set readOnly(bool value) {
- assert(value != null);
if (_readOnly == value) {
return;
}
@@ -1034,7 +1007,6 @@
bool get expands => _expands;
bool _expands;
set expands(bool value) {
- assert(value != null);
if (expands == value) {
return;
}
@@ -1054,7 +1026,6 @@
/// the specified font size.
double get textScaleFactor => _textPainter.textScaleFactor;
set textScaleFactor(double value) {
- assert(value != null);
if (_textPainter.textScaleFactor == value) {
return;
}
@@ -1088,7 +1059,6 @@
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
- assert(value != null);
if (_offset == value) {
return;
}
@@ -1295,7 +1265,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -1316,11 +1285,16 @@
/// Returns a list of rects that bound the given selection.
///
/// See [TextPainter.getBoxesForSelection] for more details.
- List<Rect> getBoxesForSelection(TextSelection selection) {
+ List<TextBox> getBoxesForSelection(TextSelection selection) {
_computeTextMetricsIfNeeded();
return _textPainter.getBoxesForSelection(selection)
- .map((TextBox textBox) => textBox.toRect().shift(_paintOffset))
- .toList();
+ .map((TextBox textBox) => TextBox.fromLTRBD(
+ textBox.left + _paintOffset.dx,
+ textBox.top + _paintOffset.dy,
+ textBox.right + _paintOffset.dx,
+ textBox.bottom + _paintOffset.dy,
+ textBox.direction
+ )).toList();
}
@override
@@ -1600,7 +1574,7 @@
TextRange? _getNextWord(int offset) {
while (true) {
final TextRange range = _textPainter.getWordBoundary(TextPosition(offset: offset));
- if (range == null || !range.isValid || range.isCollapsed) {
+ if (!range.isValid || range.isCollapsed) {
return null;
}
if (!_onlyWhitespace(range)) {
@@ -1613,7 +1587,7 @@
TextRange? _getPreviousWord(int offset) {
while (offset >= 0) {
final TextRange range = _textPainter.getWordBoundary(TextPosition(offset: offset));
- if (range == null || !range.isValid || range.isCollapsed) {
+ if (!range.isValid || range.isCollapsed) {
return null;
}
if (!_onlyWhitespace(range)) {
@@ -1878,21 +1852,10 @@
return math.max(estimatedHeight, minHeight);
}
- // TODO(LongCatIsLooong): this is a workaround for
- // https://github.com/flutter/flutter/issues/112123.
- // Use preferredLineHeight since SkParagraph currently returns an incorrect
- // height.
- final TextHeightBehavior? textHeightBehavior = this.textHeightBehavior;
- final bool usePreferredLineHeightHack = maxLines == 1
- && text?.codeUnitAt(0) == null
- && strutStyle != null && strutStyle != StrutStyle.disabled
- && textHeightBehavior != null
- && (!textHeightBehavior.applyHeightToFirstAscent || !textHeightBehavior.applyHeightToLastDescent);
-
// Special case maxLines == 1 since it forces the scrollable direction
// to be horizontal. Report the real height to prevent the text from being
// clipped.
- if (maxLines == 1 && !usePreferredLineHeightHack) {
+ if (maxLines == 1) {
// The _layoutText call lays out the paragraph using infinite width when
// maxLines == 1. Also _textPainter.maxLines will be set to 1 so should
// there be any line breaks only the first line is shown.
@@ -2084,8 +2047,6 @@
/// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds
/// to the [TextSelection.extentOffset].
void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
- assert(cause != null);
- assert(from != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextPosition? toPosition = to == null
@@ -2104,6 +2065,9 @@
_setSelection(newSelection, cause);
}
+ /// {@macro flutter.painting.TextPainter.wordBoundaries}
+ WordBoundary get wordBoundaries => _textPainter.wordBoundaries;
+
/// Select a word around the location of the last tap down.
///
/// {@macro flutter.rendering.RenderEditable.selectPosition}
@@ -2120,8 +2084,6 @@
///
/// {@macro flutter.rendering.RenderEditable.selectPosition}
void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
- assert(cause != null);
- assert(from != null);
_computeTextMetricsIfNeeded();
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextSelection fromWord = _getWordAtOffset(fromPosition);
@@ -2143,7 +2105,6 @@
///
/// {@macro flutter.rendering.RenderEditable.selectPosition}
void selectWordEdge({ required SelectionChangedCause cause }) {
- assert(cause != null);
_computeTextMetricsIfNeeded();
assert(_lastTapDownPosition != null);
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset));
@@ -2190,7 +2151,6 @@
// the position.
if (TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset))
&& effectiveOffset > 0) {
- assert(defaultTargetPlatform != null);
final TextRange? previousWord = _getPreviousWord(word.start);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
@@ -2314,7 +2274,6 @@
}
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
- assert(maxWidth != null && minWidth != null);
final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin);
final double availableMinWidth = math.min(minWidth, availableMaxWidth);
final double textMaxWidth = _isMultiline ? availableMaxWidth : double.infinity;
@@ -2347,7 +2306,6 @@
// the constraints used to layout the `_textPainter` is different. See
// `TextPainter.layout`.
void _computeTextMetricsIfNeeded() {
- assert(constraints != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
}
@@ -2365,7 +2323,6 @@
/// of the cursor for iOS is approximate and obtained through an eyeball
/// comparison.
void _computeCaretPrototype() {
- assert(defaultTargetPlatform != null);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
@@ -2527,9 +2484,6 @@
/// Sets the screen position of the floating cursor and the text position
/// closest to the cursor.
void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) {
- assert(state != null);
- assert(boundedOffset != null);
- assert(lastTextPosition != null);
if (state == FloatingCursorDragState.Start) {
_relativeOrigin = Offset.zero;
_previousOffset = null;
@@ -2880,7 +2834,6 @@
ui.BoxHeightStyle get selectionHeightStyle => _selectionHeightStyle;
ui.BoxHeightStyle _selectionHeightStyle = ui.BoxHeightStyle.tight;
set selectionHeightStyle(ui.BoxHeightStyle value) {
- assert(value != null);
if (_selectionHeightStyle == value) {
return;
}
@@ -2894,7 +2847,6 @@
ui.BoxWidthStyle get selectionWidthStyle => _selectionWidthStyle;
ui.BoxWidthStyle _selectionWidthStyle = ui.BoxWidthStyle.tight;
set selectionWidthStyle(ui.BoxWidthStyle value) {
- assert(value != null);
if (_selectionWidthStyle == value) {
return;
}
@@ -3073,7 +3025,6 @@
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
// Compute the caret location even when `shouldPaint` is false.
- assert(renderEditable != null);
final TextSelection? selection = renderEditable.selection;
// TODO(LongCatIsLooong): skip painting the caret when the selection is
@@ -3102,7 +3053,7 @@
}
canvas.drawRRect(
- RRect.fromRectAndRadius(floatingCursorRect.shift(renderEditable._paintOffset), _kFloatingCaretRadius),
+ RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius),
floatingCursorPaint..color = floatingCursorColor,
);
}
diff --git a/framework/lib/src/rendering/flex.dart b/framework/lib/src/rendering/flex.dart
index 7dab129..e66c006 100644
--- a/framework/lib/src/rendering/flex.dart
+++ b/framework/lib/src/rendering/flex.dart
@@ -199,7 +199,6 @@
}
bool? _startIsTopLeft(Axis direction, TextDirection? textDirection, VerticalDirection? verticalDirection) {
- assert(direction != null);
// If the relevant value of textDirection or verticalDirection is null, this returns null too.
switch (direction) {
case Axis.horizontal:
@@ -235,8 +234,8 @@
///
/// Layout for a [RenderFlex] proceeds in six steps:
///
-/// 1. Layout each child a null or zero flex factor with unbounded main axis
-/// constraints and the incoming cross axis constraints. If the
+/// 1. Layout each child with a null or zero flex factor with unbounded main
+/// axis constraints and the incoming cross axis constraints. If the
/// [crossAxisAlignment] is [CrossAxisAlignment.stretch], instead use tight
/// cross axis constraints that match the incoming max extent in the cross
/// axis.
@@ -289,12 +288,7 @@
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
Clip clipBehavior = Clip.none,
- }) : assert(direction != null),
- assert(mainAxisAlignment != null),
- assert(mainAxisSize != null),
- assert(crossAxisAlignment != null),
- assert(clipBehavior != null),
- _direction = direction,
+ }) : _direction = direction,
_mainAxisAlignment = mainAxisAlignment,
_mainAxisSize = mainAxisSize,
_crossAxisAlignment = crossAxisAlignment,
@@ -309,7 +303,6 @@
Axis get direction => _direction;
Axis _direction;
set direction(Axis value) {
- assert(value != null);
if (_direction != value) {
_direction = value;
markNeedsLayout();
@@ -328,7 +321,6 @@
MainAxisAlignment get mainAxisAlignment => _mainAxisAlignment;
MainAxisAlignment _mainAxisAlignment;
set mainAxisAlignment(MainAxisAlignment value) {
- assert(value != null);
if (_mainAxisAlignment != value) {
_mainAxisAlignment = value;
markNeedsLayout();
@@ -348,7 +340,6 @@
MainAxisSize get mainAxisSize => _mainAxisSize;
MainAxisSize _mainAxisSize;
set mainAxisSize(MainAxisSize value) {
- assert(value != null);
if (_mainAxisSize != value) {
_mainAxisSize = value;
markNeedsLayout();
@@ -367,7 +358,6 @@
CrossAxisAlignment get crossAxisAlignment => _crossAxisAlignment;
CrossAxisAlignment _crossAxisAlignment;
set crossAxisAlignment(CrossAxisAlignment value) {
- assert(value != null);
if (_crossAxisAlignment != value) {
_crossAxisAlignment = value;
markNeedsLayout();
@@ -444,8 +434,6 @@
}
bool get _debugHasNecessaryDirections {
- assert(direction != null);
- assert(crossAxisAlignment != null);
if (firstChild != null && lastChild != firstChild) {
// i.e. there's more than one child
switch (direction) {
@@ -453,7 +441,6 @@
assert(textDirection != null, 'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.');
break;
case Axis.vertical:
- assert(verticalDirection != null, 'Vertical $runtimeType with multiple children has a null verticalDirection, so the layout order is undefined.');
break;
}
}
@@ -464,7 +451,6 @@
assert(textDirection != null, 'Horizontal $runtimeType with $mainAxisAlignment has a null textDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
- assert(verticalDirection != null, 'Vertical $runtimeType with $mainAxisAlignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
}
}
@@ -472,7 +458,6 @@
crossAxisAlignment == CrossAxisAlignment.end) {
switch (direction) {
case Axis.horizontal:
- assert(verticalDirection != null, 'Horizontal $runtimeType with $crossAxisAlignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
assert(textDirection != null, 'Vertical $runtimeType with $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.');
@@ -494,7 +479,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -795,7 +779,6 @@
_LayoutSizes _computeSizes({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
assert(_debugHasNecessaryDirections);
- assert(constraints != null);
// Determine used flex factor, size inflexible items, calculate free space.
int totalFlex = 0;
@@ -861,7 +844,6 @@
minChildExtent = 0.0;
break;
}
- assert(minChildExtent != null);
final BoxConstraints innerConstraints;
if (crossAxisAlignment == CrossAxisAlignment.stretch) {
switch (_direction) {
diff --git a/framework/lib/src/rendering/flow.dart b/framework/lib/src/rendering/flow.dart
index 3f6d8b9..f762740 100644
--- a/framework/lib/src/rendering/flow.dart
+++ b/framework/lib/src/rendering/flow.dart
@@ -159,8 +159,8 @@
///
/// Rather than positioning the children during layout, the children are
/// positioned using transformation matrices during the paint phase using the
-/// matrices from the [FlowDelegate.paintChildren] function. The children can be
-/// repositioned efficiently by simply repainting the flow.
+/// matrices from the [FlowDelegate.paintChildren] function. The children are thus
+/// repositioned efficiently by repainting the flow, skipping layout.
///
/// The most efficient way to trigger a repaint of the flow is to supply a
/// repaint argument to the constructor of the [FlowDelegate]. The flow will
@@ -183,9 +183,7 @@
List<RenderBox>? children,
required FlowDelegate delegate,
Clip clipBehavior = Clip.hardEdge,
- }) : assert(delegate != null),
- assert(clipBehavior != null),
- _delegate = delegate,
+ }) : _delegate = delegate,
_clipBehavior = clipBehavior {
addAll(children);
}
@@ -209,7 +207,6 @@
/// to determine whether the new delegate requires this object to update its
/// layout or painting.
set delegate(FlowDelegate newDelegate) {
- assert(newDelegate != null);
if (_delegate == newDelegate) {
return;
}
@@ -234,7 +231,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
diff --git a/framework/lib/src/rendering/image.dart b/framework/lib/src/rendering/image.dart
index 8430b66..99d0877 100644
--- a/framework/lib/src/rendering/image.dart
+++ b/framework/lib/src/rendering/image.dart
@@ -44,13 +44,7 @@
bool invertColors = false,
bool isAntiAlias = false,
FilterQuality filterQuality = FilterQuality.low,
- }) : assert(scale != null),
- assert(repeat != null),
- assert(alignment != null),
- assert(filterQuality != null),
- assert(matchTextDirection != null),
- assert(isAntiAlias != null),
- _image = image,
+ }) : _image = image,
_width = width,
_height = height,
_scale = scale,
@@ -144,7 +138,6 @@
double get scale => _scale;
double _scale;
set scale(double value) {
- assert(value != null);
if (value == _scale) {
return;
}
@@ -200,7 +193,6 @@
FilterQuality get filterQuality => _filterQuality;
FilterQuality _filterQuality;
set filterQuality(FilterQuality value) {
- assert(value != null);
if (value == _filterQuality) {
return;
}
@@ -249,7 +241,6 @@
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
set alignment(AlignmentGeometry value) {
- assert(value != null);
if (value == _alignment) {
return;
}
@@ -261,7 +252,6 @@
ImageRepeat get repeat => _repeat;
ImageRepeat _repeat;
set repeat(ImageRepeat value) {
- assert(value != null);
if (value == _repeat) {
return;
}
@@ -318,7 +308,6 @@
bool get matchTextDirection => _matchTextDirection;
bool _matchTextDirection;
set matchTextDirection(bool value) {
- assert(value != null);
if (value == _matchTextDirection) {
return;
}
@@ -350,7 +339,6 @@
if (_isAntiAlias == value) {
return;
}
- assert(value != null);
_isAntiAlias = value;
markNeedsPaint();
}
diff --git a/framework/lib/src/rendering/layer.dart b/framework/lib/src/rendering/layer.dart
index 6995c8c..935c183 100644
--- a/framework/lib/src/rendering/layer.dart
+++ b/framework/lib/src/rendering/layer.dart
@@ -24,7 +24,7 @@
const AnnotationEntry({
required this.annotation,
required this.localPosition,
- }) : assert(localPosition != null);
+ });
/// The annotation object that is found.
final T annotation;
@@ -168,9 +168,7 @@
assert(delta != 0);
_compositionCallbackCount += delta;
assert(_compositionCallbackCount >= 0);
- if (parent != null) {
- parent!._updateSubtreeCompositionObserverCount(delta);
- }
+ parent?._updateSubtreeCompositionObserverCount(delta);
}
void _fireCompositionCallbacks({required bool includeChildren}) {
@@ -546,7 +544,7 @@
/// should search for annotations, or if the layer has its own annotations to
/// add.
///
- /// The default implementation simply returns `false`, which means neither
+ /// The default implementation always returns `false`, which means neither
/// the layer nor its children has annotations, and the annotation search
/// is not absorbed either (see below for explanation).
///
@@ -602,7 +600,7 @@
///
/// Returns null if no matching annotations are found.
///
- /// By default this method simply calls [findAnnotations] with `onlyFirst:
+ /// By default this method calls [findAnnotations] with `onlyFirst:
/// true` and returns the annotation of the first result. Prefer overriding
/// [findAnnotations] instead of this method, because during an annotation
/// search, only [findAnnotations] is recursively called, while custom
@@ -628,7 +626,7 @@
///
/// Returns a result with empty entries if no matching annotations are found.
///
- /// By default this method simply calls [findAnnotations] with `onlyFirst:
+ /// By default this method calls [findAnnotations] with `onlyFirst:
/// false` and returns the annotations of its result. Prefer overriding
/// [findAnnotations] instead of this method, because during an annotation
/// search, only [findAnnotations] is recursively called, while custom
@@ -891,8 +889,7 @@
required this.textureId,
this.freeze = false,
this.filterQuality = ui.FilterQuality.low,
- }) : assert(rect != null),
- assert(textureId != null);
+ });
/// Bounding rectangle of this layer.
final Rect rect;
@@ -939,8 +936,7 @@
PlatformViewLayer({
required this.rect,
required this.viewId,
- }) : assert(rect != null),
- assert(viewId != null);
+ });
/// Bounding rectangle of this layer in the global coordinate space.
final Rect rect;
@@ -1027,7 +1023,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
- assert(optionsMask != null);
builder.addPerformanceOverlay(optionsMask, overlayRect);
builder.setRasterizerTracingThreshold(rasterizerThreshold);
builder.setCheckerboardRasterCacheImages(checkerboardRasterCacheImages);
@@ -1255,7 +1250,6 @@
child._nextSibling = null;
assert(child.attached == attached);
dropChild(child);
- assert(child._parentHandle != null);
child._parentHandle.layer = null;
child = next;
}
@@ -1318,7 +1312,6 @@
/// position.
void applyTransform(Layer? child, Matrix4 transform) {
assert(child != null);
- assert(transform != null);
}
/// Returns the descendants of this layer in depth first order.
@@ -1399,7 +1392,6 @@
@override
void applyTransform(Layer? child, Matrix4 transform) {
assert(child != null);
- assert(transform != null);
transform.translate(offset.dx, offset.dy);
}
@@ -1426,8 +1418,6 @@
}
ui.Scene _createSceneForImage(Rect bounds, { double pixelRatio = 1.0 }) {
- assert(bounds != null);
- assert(pixelRatio != null);
final ui.SceneBuilder builder = ui.SceneBuilder();
final Matrix4 transform = Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1);
transform.translate(-(bounds.left + offset.dx), -(bounds.top + offset.dy));
@@ -1521,7 +1511,6 @@
Clip clipBehavior = Clip.hardEdge,
}) : _clipRect = clipRect,
_clipBehavior = clipBehavior,
- assert(clipBehavior != null),
assert(clipBehavior != Clip.none);
/// The rectangle to clip in the parent's coordinate system.
@@ -1550,7 +1539,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
- assert(value != null);
assert(value != Clip.none);
if (value != _clipBehavior) {
_clipBehavior = value;
@@ -1569,7 +1557,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
assert(clipRect != null);
- assert(clipBehavior != null);
bool enabled = true;
assert(() {
enabled = !debugDisableClipLayers;
@@ -1613,7 +1600,6 @@
Clip clipBehavior = Clip.antiAlias,
}) : _clipRRect = clipRRect,
_clipBehavior = clipBehavior,
- assert(clipBehavior != null),
assert(clipBehavior != Clip.none);
/// The rounded-rect to clip in the parent's coordinate system.
@@ -1638,7 +1624,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
- assert(value != null);
assert(value != Clip.none);
if (value != _clipBehavior) {
_clipBehavior = value;
@@ -1657,7 +1642,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
assert(clipRRect != null);
- assert(clipBehavior != null);
bool enabled = true;
assert(() {
enabled = !debugDisableClipLayers;
@@ -1701,7 +1685,6 @@
Clip clipBehavior = Clip.antiAlias,
}) : _clipPath = clipPath,
_clipBehavior = clipBehavior,
- assert(clipBehavior != null),
assert(clipBehavior != Clip.none);
/// The path to clip in the parent's coordinate system.
@@ -1726,7 +1709,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
- assert(value != null);
assert(value != Clip.none);
if (value != _clipBehavior) {
_clipBehavior = value;
@@ -1745,7 +1727,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
assert(clipPath != null);
- assert(clipBehavior != null);
bool enabled = true;
assert(() {
enabled = !debugDisableClipLayers;
@@ -1940,7 +1921,6 @@
@override
void applyTransform(Layer? child, Matrix4 transform) {
assert(child != null);
- assert(transform != null);
assert(_lastEffectiveTransform != null || this.transform != null);
if (_lastEffectiveTransform == null) {
transform.multiply(this.transform!);
@@ -2250,7 +2230,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsAddToScene();
@@ -2310,7 +2289,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
assert(clipPath != null);
- assert(clipBehavior != null);
assert(elevation != null);
assert(color != null);
assert(shadowColor != null);
@@ -2437,7 +2415,7 @@
///
/// The [offset] property must be non-null before the compositing phase of the
/// pipeline.
- LeaderLayer({ required LayerLink link, Offset offset = Offset.zero }) : assert(link != null), _link = link, _offset = offset;
+ LeaderLayer({ required LayerLink link, Offset offset = Offset.zero }) : _link = link, _offset = offset;
/// The object with which this layer should register.
///
@@ -2446,7 +2424,6 @@
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
- assert(value != null);
if (_link == value) {
return;
}
@@ -2467,7 +2444,6 @@
Offset get offset => _offset;
Offset _offset;
set offset(Offset value) {
- assert(value != null);
if (value == _offset) {
return;
}
@@ -2496,7 +2472,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
- assert(offset != null);
if (offset != Offset.zero) {
engineLayer = builder.pushTransform(
Matrix4.translationValues(offset.dx, offset.dy, 0.0).storage,
@@ -2551,23 +2526,18 @@
/// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties
/// must be non-null before the compositing phase of the pipeline.
FollowerLayer({
- required LayerLink link,
+ required this.link,
this.showWhenUnlinked = true,
this.unlinkedOffset = Offset.zero,
this.linkedOffset = Offset.zero,
- }) : assert(link != null), _link = link;
+ });
/// The link to the [LeaderLayer].
///
/// The same object should be provided to a [LeaderLayer] that is earlier in
/// the layer tree. When this layer is composited, it will apply a transform
/// that moves its children to match the position of the [LeaderLayer].
- LayerLink get link => _link;
- set link(LayerLink value) {
- assert(value != null);
- _link = value;
- }
- LayerLink _link;
+ LayerLink link;
/// Whether to show the layer's contents when the [link] does not point to a
/// [LeaderLayer].
@@ -2631,7 +2601,7 @@
@override
bool findAnnotations<S extends Object>(AnnotationResult<S> result, Offset localPosition, { required bool onlyFirst }) {
- if (_link.leader == null) {
+ if (link.leader == null) {
if (showWhenUnlinked!) {
return super.findAnnotations(result, localPosition - unlinkedOffset!, onlyFirst: onlyFirst);
}
@@ -2740,9 +2710,8 @@
/// Populate [_lastTransform] given the current state of the tree.
void _establishTransform() {
- assert(link != null);
_lastTransform = null;
- final LeaderLayer? leader = _link.leader;
+ final LeaderLayer? leader = link.leader;
// Check to see if we are linked.
if (leader == null) {
return;
@@ -2806,9 +2775,8 @@
@override
void addToScene(ui.SceneBuilder builder) {
- assert(link != null);
assert(showWhenUnlinked != null);
- if (_link.leader == null && !showWhenUnlinked!) {
+ if (link.leader == null && !showWhenUnlinked!) {
_lastTransform = null;
_lastOffset = null;
_inverseDirty = true;
@@ -2840,7 +2808,6 @@
@override
void applyTransform(Layer? child, Matrix4 transform) {
assert(child != null);
- assert(transform != null);
if (_lastTransform != null) {
transform.multiply(_lastTransform!);
} else {
@@ -2886,9 +2853,7 @@
this.size,
Offset? offset,
this.opaque = false,
- }) : assert(value != null),
- assert(opaque != null),
- offset = offset ?? Offset.zero;
+ }) : offset = offset ?? Offset.zero;
/// The annotated object, which is added to the result if all restrictions are
/// met.
diff --git a/framework/lib/src/rendering/list_body.dart b/framework/lib/src/rendering/list_body.dart
index a4ca501..bc504cd 100644
--- a/framework/lib/src/rendering/list_body.dart
+++ b/framework/lib/src/rendering/list_body.dart
@@ -32,8 +32,7 @@
RenderListBody({
List<RenderBox>? children,
AxisDirection axisDirection = AxisDirection.down,
- }) : assert(axisDirection != null),
- _axisDirection = axisDirection {
+ }) : _axisDirection = axisDirection {
addAll(children);
}
@@ -51,7 +50,6 @@
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
- assert(value != null);
if (_axisDirection == value) {
return;
}
@@ -259,7 +257,6 @@
@override
double computeMinIntrinsicWidth(double height) {
- assert(mainAxis != null);
switch (mainAxis) {
case Axis.horizontal:
return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
@@ -270,7 +267,6 @@
@override
double computeMaxIntrinsicWidth(double height) {
- assert(mainAxis != null);
switch (mainAxis) {
case Axis.horizontal:
return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
@@ -281,7 +277,6 @@
@override
double computeMinIntrinsicHeight(double width) {
- assert(mainAxis != null);
switch (mainAxis) {
case Axis.horizontal:
return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
@@ -292,7 +287,6 @@
@override
double computeMaxIntrinsicHeight(double width) {
- assert(mainAxis != null);
switch (mainAxis) {
case Axis.horizontal:
return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
diff --git a/framework/lib/src/rendering/list_wheel_viewport.dart b/framework/lib/src/rendering/list_wheel_viewport.dart
index e2f2264..a6dbebe 100644
--- a/framework/lib/src/rendering/list_wheel_viewport.dart
+++ b/framework/lib/src/rendering/list_wheel_viewport.dart
@@ -149,25 +149,13 @@
bool renderChildrenOutsideViewport = false,
Clip clipBehavior = Clip.none,
List<RenderBox>? children,
- }) : assert(childManager != null),
- assert(offset != null),
- assert(diameterRatio != null),
- assert(diameterRatio > 0, diameterRatioZeroMessage),
- assert(perspective != null),
+ }) : assert(diameterRatio > 0, diameterRatioZeroMessage),
assert(perspective > 0),
assert(perspective <= 0.01, perspectiveTooHighMessage),
- assert(offAxisFraction != null),
- assert(useMagnifier != null),
- assert(magnification != null),
assert(magnification > 0),
- assert(overAndUnderCenterOpacity != null),
assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1),
- assert(itemExtent != null),
- assert(squeeze != null),
assert(squeeze > 0),
assert(itemExtent > 0),
- assert(renderChildrenOutsideViewport != null),
- assert(clipBehavior != null),
assert(
!renderChildrenOutsideViewport || clipBehavior == Clip.none,
clipBehaviorAndRenderChildrenOutsideViewportConflict,
@@ -225,7 +213,6 @@
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
- assert(value != null);
if (value == _offset) {
return;
}
@@ -270,7 +257,6 @@
double get diameterRatio => _diameterRatio;
double _diameterRatio;
set diameterRatio(double value) {
- assert(value != null);
assert(
value > 0,
diameterRatioZeroMessage,
@@ -300,7 +286,6 @@
double get perspective => _perspective;
double _perspective;
set perspective(double value) {
- assert(value != null);
assert(value > 0);
assert(
value <= 0.01,
@@ -342,7 +327,6 @@
double get offAxisFraction => _offAxisFraction;
double _offAxisFraction = 0.0;
set offAxisFraction(double value) {
- assert(value != null);
if (value == _offAxisFraction) {
return;
}
@@ -356,7 +340,6 @@
bool get useMagnifier => _useMagnifier;
bool _useMagnifier = false;
set useMagnifier(bool value) {
- assert(value != null);
if (value == _useMagnifier) {
return;
}
@@ -376,7 +359,6 @@
double get magnification => _magnification;
double _magnification = 1.0;
set magnification(double value) {
- assert(value != null);
assert(value > 0);
if (value == _magnification) {
return;
@@ -396,7 +378,6 @@
double get overAndUnderCenterOpacity => _overAndUnderCenterOpacity;
double _overAndUnderCenterOpacity = 1.0;
set overAndUnderCenterOpacity(double value) {
- assert(value != null);
assert(value >= 0 && value <= 1);
if (value == _overAndUnderCenterOpacity) {
return;
@@ -414,7 +395,6 @@
double get itemExtent => _itemExtent;
double _itemExtent;
set itemExtent(double value) {
- assert(value != null);
assert(value > 0);
if (value == _itemExtent) {
return;
@@ -447,7 +427,6 @@
double get squeeze => _squeeze;
double _squeeze;
set squeeze(double value) {
- assert(value != null);
assert(value > 0);
if (value == _squeeze) {
return;
@@ -470,7 +449,6 @@
bool get renderChildrenOutsideViewport => _renderChildrenOutsideViewport;
bool _renderChildrenOutsideViewport;
set renderChildrenOutsideViewport(bool value) {
- assert(value != null);
assert(
!renderChildrenOutsideViewport || clipBehavior == Clip.none,
clipBehaviorAndRenderChildrenOutsideViewportConflict,
@@ -489,7 +467,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -636,7 +613,6 @@
///
/// This relies on the [childManager] maintaining [ListWheelParentData.index].
int indexOf(RenderBox child) {
- assert(child != null);
final ListWheelParentData childParentData = child.parentData! as ListWheelParentData;
assert(childParentData.index != null);
return childParentData.index!;
diff --git a/framework/lib/src/rendering/mouse_tracker.dart b/framework/lib/src/rendering/mouse_tracker.dart
index 8a02c9b..022c71a 100644
--- a/framework/lib/src/rendering/mouse_tracker.dart
+++ b/framework/lib/src/rendering/mouse_tracker.dart
@@ -30,8 +30,7 @@
class _MouseState {
_MouseState({
required PointerEvent initialEvent,
- }) : assert(initialEvent != null),
- _latestEvent = initialEvent;
+ }) : _latestEvent = initialEvent;
// The list of annotations that contains this device.
//
@@ -40,7 +39,6 @@
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
LinkedHashMap<MouseTrackerAnnotation, Matrix4> replaceAnnotations(LinkedHashMap<MouseTrackerAnnotation, Matrix4> value) {
- assert(value != null);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> previous = _annotations;
_annotations = value;
return previous;
@@ -51,7 +49,6 @@
PointerEvent _latestEvent;
PointerEvent replaceLatestEvent(PointerEvent value) {
- assert(value != null);
assert(value.device == _latestEvent.device);
final PointerEvent previous = _latestEvent;
_latestEvent = value;
@@ -83,10 +80,7 @@
required this.lastAnnotations,
required this.nextAnnotations,
required PointerEvent this.previousEvent,
- }) : assert(previousEvent != null),
- assert(lastAnnotations != null),
- assert(nextAnnotations != null),
- triggeringEvent = null;
+ }) : triggeringEvent = null;
/// When device update is triggered by a pointer event.
///
@@ -97,9 +91,7 @@
required this.nextAnnotations,
this.previousEvent,
required PointerEvent this.triggeringEvent,
- }) : assert(triggeringEvent != null),
- assert(lastAnnotations != null),
- assert(nextAnnotations != null);
+ });
/// The annotations that the device is hovering before the update.
///
@@ -129,7 +121,6 @@
/// The pointing device of this update.
int get device {
final int result = (previousEvent ?? triggeringEvent)!.device;
- assert(result != null);
return result;
}
@@ -138,7 +129,6 @@
/// The [latestEvent] is never null.
PointerEvent get latestEvent {
final PointerEvent result = triggeringEvent ?? previousEvent!;
- assert(result != null);
return result;
}
@@ -219,7 +209,6 @@
if (state == null) {
return true;
}
- assert(event != null);
final PointerEvent lastEvent = state.latestEvent;
assert(event.device == lastEvent.device);
// An Added can only follow a Removed, and a Removed can only be followed
@@ -236,7 +225,6 @@
}
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _hitTestResultToAnnotations(HitTestResult result) {
- assert(result != null);
final LinkedHashMap<MouseTrackerAnnotation, Matrix4> annotations = LinkedHashMap<MouseTrackerAnnotation, Matrix4>();
for (final HitTestEntry entry in result.path) {
final Object target = entry.target;
@@ -253,8 +241,6 @@
// If the device is not connected or not a mouse, an empty map is returned
// without calling `hitTest`.
LinkedHashMap<MouseTrackerAnnotation, Matrix4> _findAnnotations(_MouseState state, MouseDetectorAnnotationFinder hitTest) {
- assert(state != null);
- assert(hitTest != null);
final Offset globalPosition = state.latestEvent.position;
final int device = state.device;
if (!_mouseStates.containsKey(device)) {
@@ -302,8 +288,8 @@
/// [MouseTracker] filter which to react to.
///
/// The `getResult` is a function to return the hit test result at the
- /// position of the event. It should not simply return cached hit test
- /// result, because the cache does not change throughout a tap sequence.
+ /// position of the event. It should not return a cached hit test
+ /// result, because the cache would not change during a tap sequence.
void updateWithEvent(PointerEvent event, ValueGetter<HitTestResult> getResult) {
if (event.kind != PointerDeviceKind.mouse) {
return;
diff --git a/framework/lib/src/rendering/object.dart b/framework/lib/src/rendering/object.dart
index 40ced32..c1f1159 100644
--- a/framework/lib/src/rendering/object.dart
+++ b/framework/lib/src/rendering/object.dart
@@ -10,6 +10,7 @@
import 'package:flute/foundation.dart';
import 'package:flute/gestures.dart';
import 'package:flute/painting.dart';
+import 'package:flute/scheduler.dart';
import 'package:flute/semantics.dart';
import 'debug.dart';
@@ -82,9 +83,7 @@
/// Typically only called by [PaintingContext.repaintCompositedChild]
/// and [pushLayer].
@protected
- PaintingContext(this._containerLayer, this.estimatedBounds)
- : assert(_containerLayer != null),
- assert(estimatedBounds != null);
+ PaintingContext(this._containerLayer, this.estimatedBounds);
final ContainerLayer _containerLayer;
@@ -461,7 +460,6 @@
/// * [addLayer], for pushing a layer without painting further contents
/// within it.
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
- assert(painter != null);
// If a layer is being reused, it may already contain children. We remove
// them so that `painter` can add children that are relevant for this frame.
if (childLayer.hasChildren) {
@@ -559,7 +557,6 @@
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
ClipRRectLayer? pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipRRectLayer? oldLayer }) {
- assert(clipBehavior != null);
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
@@ -599,7 +596,6 @@
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
ClipPathLayer? pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipPathLayer? oldLayer }) {
- assert(clipBehavior != null);
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
@@ -636,7 +632,6 @@
/// ancestor render objects that this render object will include a composited
/// layer, which, for example, causes them to use composited clips.
ColorFilterLayer pushColorFilter(Offset offset, ColorFilter colorFilter, PaintingContextCallback painter, { ColorFilterLayer? oldLayer }) {
- assert(colorFilter != null);
final ColorFilterLayer layer = oldLayer ?? ColorFilterLayer();
layer.colorFilter = colorFilter;
pushLayer(layer, painter, offset);
@@ -830,8 +825,7 @@
/// You can obtain the [PipelineOwner] using the [RenderObject.owner] property.
class SemanticsHandle {
SemanticsHandle._(PipelineOwner owner, this.listener)
- : assert(owner != null),
- _owner = owner {
+ : _owner = owner {
if (listener != null) {
_owner.semanticsOwner!.addListener(listener!);
}
@@ -1509,7 +1503,6 @@
@override
void adoptChild(RenderObject child) {
assert(_debugCanPerformMutations);
- assert(child != null);
setupParentData(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
@@ -1524,7 +1517,6 @@
@override
void dropChild(RenderObject child) {
assert(_debugCanPerformMutations);
- assert(child != null);
assert(child.parentData != null);
child._cleanRelayoutBoundary();
child.parentData!.detach();
@@ -2078,7 +2070,6 @@
arguments: debugTimelineArguments,
);
}
- assert(constraints != null);
assert(constraints.debugAssertIsValid(
isAppliedConstraint: true,
informationCollector: () {
@@ -3100,6 +3091,10 @@
if (_cachedSemanticsConfiguration == null) {
_cachedSemanticsConfiguration = SemanticsConfiguration();
describeSemanticsConfiguration(_cachedSemanticsConfiguration!);
+ assert(
+ !_cachedSemanticsConfiguration!.explicitChildNodes || _cachedSemanticsConfiguration!.childConfigurationsDelegate == null,
+ 'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.',
+ );
}
return _cachedSemanticsConfiguration!;
}
@@ -3160,15 +3155,30 @@
// the semantics subtree starting at the identified semantics boundary.
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
+
+ bool mayProduceSiblingNodes =
+ _cachedSemanticsConfiguration?.childConfigurationsDelegate != null ||
+ _semanticsConfiguration.childConfigurationsDelegate != null;
_cachedSemanticsConfiguration = null;
+
bool isEffectiveSemanticsBoundary = _semanticsConfiguration.isSemanticBoundary && wasSemanticsBoundary;
RenderObject node = this;
- while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
+ // The sibling nodes will be attached to the parent of immediate semantics
+ // node, thus marking this semantics boundary dirty is not enough, it needs
+ // to find the first parent semantics boundary that does not have any
+ // possible sibling node.
+ while (node.parent is RenderObject && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
if (node != this && node._needsSemanticsUpdate) {
break;
}
node._needsSemanticsUpdate = true;
+ // Since this node is a semantics boundary, the produced sibling nodes will
+ // be attached to the parent semantics boundary. Thus, these sibling nodes
+ // will not be carried to the next loop.
+ if (isEffectiveSemanticsBoundary) {
+ mayProduceSiblingNodes = false;
+ }
node = node.parent! as RenderObject;
isEffectiveSemanticsBoundary = node._semanticsConfiguration.isSemanticBoundary;
@@ -3213,92 +3223,116 @@
assert(fragment is _InterestingSemanticsFragment);
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
final List<SemanticsNode> result = <SemanticsNode>[];
+ final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
interestingFragment.compileChildren(
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
parentPaintClipRect: _semantics?.parentPaintClipRect,
elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0,
result: result,
+ siblingNodes: siblingNodes,
);
- final SemanticsNode node = result.single;
- // Fragment only wants to add this node's SemanticsNode to the parent.
- assert(interestingFragment.config == null && node == _semantics);
+ // Result may contain sibling nodes that are irrelevant for this update.
+ assert(interestingFragment.config == null && result.any((SemanticsNode node) => node == _semantics));
}
/// Returns the semantics that this node would like to add to its parent.
_SemanticsFragment _getSemanticsForParent({
required bool mergeIntoParent,
}) {
- assert(mergeIntoParent != null);
assert(!_needsLayout, 'Updated layout information required for $this to calculate semantics.');
final SemanticsConfiguration config = _semanticsConfiguration;
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
- final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[];
- final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = <_InterestingSemanticsFragment>{};
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
-
+ final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
+ final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
+ final bool hasChildConfigurationsDelegate = config.childConfigurationsDelegate != null;
+ final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
+ final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
+ final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
visitChildrenForSemantics((RenderObject renderChild) {
assert(!_needsLayout);
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
mergeIntoParent: childrenMergeIntoParent,
);
if (parentFragment.dropsSemanticsOfPreviousSiblings) {
- fragments.clear();
- toBeMarkedExplicit.clear();
+ childConfigurations.clear();
+ mergeUpFragments.clear();
+ siblingMergeFragmentGroups.clear();
if (!config.isSemanticBoundary) {
dropSemanticsOfPreviousSiblings = true;
}
}
- // Figure out which child fragments are to be made explicit.
- for (final _InterestingSemanticsFragment fragment in parentFragment.interestingFragments) {
- fragments.add(fragment);
+ for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
fragment.addAncestor(this);
fragment.addTags(config.tagsForChildren);
- if (config.explicitChildNodes || parent is! RenderObject) {
- fragment.markAsExplicit();
- continue;
+ if (hasChildConfigurationsDelegate && fragment.config != null) {
+ // This fragment need to go through delegate to determine whether it
+ // merge up or not.
+ childConfigurations.add(fragment.config!);
+ configToFragment[fragment.config!] = fragment;
+ } else {
+ mergeUpFragments.add(fragment);
}
- if (!fragment.hasConfigForParent || producesForkingFragment) {
- continue;
- }
- if (!config.isCompatibleWith(fragment.config)) {
- toBeMarkedExplicit.add(fragment);
- }
- final int siblingLength = fragments.length - 1;
- for (int i = 0; i < siblingLength; i += 1) {
- final _InterestingSemanticsFragment siblingFragment = fragments[i];
- if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
- toBeMarkedExplicit.add(fragment);
- toBeMarkedExplicit.add(siblingFragment);
+ }
+ if (parentFragment is _ContainerSemanticsFragment) {
+ // Container fragments needs to propagate sibling merge group to be
+ // compiled by _SwitchableSemanticsFragment.
+ for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
+ for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
+ siblingMergingFragment.addAncestor(this);
+ siblingMergingFragment.addTags(config.tagsForChildren);
}
+ siblingMergeFragmentGroups.add(siblingMergeGroup);
}
}
});
- for (final _InterestingSemanticsFragment fragment in toBeMarkedExplicit) {
- fragment.markAsExplicit();
+ assert(hasChildConfigurationsDelegate || configToFragment.isEmpty);
+
+ if (explicitChildNode) {
+ for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
+ fragment.markAsExplicit();
+ }
+ } else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
+ final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
+ mergeUpFragments.addAll(
+ result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
+ );
+ for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
+ siblingMergeFragmentGroups.add(
+ group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
+ );
+ }
}
_needsSemanticsUpdate = false;
- _SemanticsFragment result;
+ final _SemanticsFragment result;
if (parent is! RenderObject) {
assert(!config.hasBeenAnnotated);
assert(!mergeIntoParent);
+ assert(siblingMergeFragmentGroups.isEmpty);
+ _marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
+ siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _RootSemanticsFragment(
owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
} else if (producesForkingFragment) {
result = _ContainerSemanticsFragment(
+ siblingMergeGroups: siblingMergeFragmentGroups,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
} else {
+ _marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
+ siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _SwitchableSemanticsFragment(
config: config,
mergeIntoParent: mergeIntoParent,
+ siblingMergeGroups: siblingMergeFragmentGroups,
owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
@@ -3307,12 +3341,34 @@
fragment.markAsExplicit();
}
}
-
- result.addAll(fragments);
-
+ result.addAll(mergeUpFragments);
return result;
}
+ void _marksExplicitInMergeGroup(List<_InterestingSemanticsFragment> mergeGroup, {bool isMergeUp = false}) {
+ final Set<_InterestingSemanticsFragment> toBeExplicit = <_InterestingSemanticsFragment>{};
+ for (int i = 0; i < mergeGroup.length; i += 1) {
+ final _InterestingSemanticsFragment fragment = mergeGroup[i];
+ if (!fragment.hasConfigForParent) {
+ continue;
+ }
+ if (isMergeUp && !_semanticsConfiguration.isCompatibleWith(fragment.config)) {
+ toBeExplicit.add(fragment);
+ }
+ final int siblingLength = i;
+ for (int j = 0; j < siblingLength; j += 1) {
+ final _InterestingSemanticsFragment siblingFragment = mergeGroup[j];
+ if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
+ toBeExplicit.add(fragment);
+ toBeExplicit.add(siblingFragment);
+ }
+ }
+ }
+ for (final _InterestingSemanticsFragment fragment in toBeExplicit) {
+ fragment.markAsExplicit();
+ }
+ }
+
/// Called when collecting the semantics of this node.
///
/// The implementation has to return the children in paint order skipping all
@@ -3908,7 +3964,6 @@
/// The previous child before the given child in the child list.
ChildType? childBefore(ChildType child) {
- assert(child != null);
assert(child.parent == this);
final ParentDataType childParentData = child.parentData! as ParentDataType;
return childParentData.previousSibling;
@@ -3916,7 +3971,6 @@
/// The next child after the given child in the child list.
ChildType? childAfter(ChildType child) {
- assert(child != null);
assert(child.parent == this);
final ParentDataType childParentData = child.parentData! as ParentDataType;
return childParentData.nextSibling;
@@ -3945,13 +3999,18 @@
/// Mixin for [RenderObject] that will call [systemFontsDidChange] whenever the
/// system fonts change.
///
-/// System fonts can change when the OS install or remove a font. Use this mixin if
-/// the [RenderObject] uses [TextPainter] or [Paragraph] to correctly update the
-/// text when it happens.
+/// System fonts can change when the OS installs or removes a font. Use this
+/// mixin if the [RenderObject] uses [TextPainter] or [Paragraph] to correctly
+/// update the text when it happens.
mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
/// A callback that is called when system fonts have changed.
///
+ /// The framework defers the invocation of the callback to the
+ /// [SchedulerPhase.transientCallbacks] phase to ensure that the
+ /// [RenderObject]'s text layout is still valid when user interactions are in
+ /// progress (which usually take place during the [SchedulerPhase.idle] phase).
+ ///
/// By default, [markNeedsLayout] is called on the [RenderObject]
/// implementing this mixin.
///
@@ -3963,15 +4022,44 @@
markNeedsLayout();
}
+ bool _hasPendingSystemFontsDidChangeCallBack = false;
+ void _scheduleSystemFontsUpdate() {
+ assert(
+ SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle,
+ '${objectRuntimeType(this, "RelayoutWhenSystemFontsChangeMixin")}._scheduleSystemFontsUpdate() '
+ 'called during ${SchedulerBinding.instance.schedulerPhase}.',
+ );
+ if (_hasPendingSystemFontsDidChangeCallBack) {
+ return;
+ }
+ _hasPendingSystemFontsDidChangeCallBack = true;
+ SchedulerBinding.instance.scheduleFrameCallback((Duration timeStamp) {
+ assert(_hasPendingSystemFontsDidChangeCallBack);
+ _hasPendingSystemFontsDidChangeCallBack = false;
+ assert(
+ attached || (debugDisposed ?? true),
+ '$this is detached during ${SchedulerBinding.instance.schedulerPhase} but is not disposed.',
+ );
+ if (attached) {
+ systemFontsDidChange();
+ }
+ });
+ }
+
@override
- void attach(covariant PipelineOwner owner) {
+ void attach(PipelineOwner owner) {
super.attach(owner);
- PaintingBinding.instance.systemFonts.addListener(systemFontsDidChange);
+ // If there's a pending callback that would imply this node was detached
+ // between the idle phase and the next transientCallbacks phase. The tree
+ // can not be mutated between those two phases so that should never happen.
+ assert(!_hasPendingSystemFontsDidChangeCallBack);
+ PaintingBinding.instance.systemFonts.addListener(_scheduleSystemFontsUpdate);
}
@override
void detach() {
- PaintingBinding.instance.systemFonts.removeListener(systemFontsDidChange);
+ assert(!_hasPendingSystemFontsDidChangeCallBack);
+ PaintingBinding.instance.systemFonts.removeListener(_scheduleSystemFontsUpdate);
super.detach();
}
}
@@ -3985,8 +4073,9 @@
/// * [_ContainerSemanticsFragment]: a container class to transport the semantic
/// information of multiple [_InterestingSemanticsFragment] to a parent.
abstract class _SemanticsFragment {
- _SemanticsFragment({ required this.dropsSemanticsOfPreviousSiblings })
- : assert (dropsSemanticsOfPreviousSiblings != null);
+ _SemanticsFragment({
+ required this.dropsSemanticsOfPreviousSiblings,
+ });
/// Incorporate the fragments of children into this fragment.
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
@@ -4002,25 +4091,29 @@
/// Returns [_InterestingSemanticsFragment] describing the actual semantic
/// information that this fragment wants to add to the parent.
- List<_InterestingSemanticsFragment> get interestingFragments;
+ List<_InterestingSemanticsFragment> get mergeUpFragments;
}
/// A container used when a [RenderObject] wants to add multiple independent
/// [_InterestingSemanticsFragment] to its parent.
///
/// The [_InterestingSemanticsFragment] to be added to the parent can be
-/// obtained via [interestingFragments].
+/// obtained via [mergeUpFragments].
class _ContainerSemanticsFragment extends _SemanticsFragment {
+ _ContainerSemanticsFragment({
+ required super.dropsSemanticsOfPreviousSiblings,
+ required this.siblingMergeGroups,
+ });
- _ContainerSemanticsFragment({ required super.dropsSemanticsOfPreviousSiblings });
+ final List<List<_InterestingSemanticsFragment>> siblingMergeGroups;
@override
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
- interestingFragments.addAll(fragments);
+ mergeUpFragments.addAll(fragments);
}
@override
- final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[];
+ final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
}
/// A [_SemanticsFragment] that describes which concrete semantic information
@@ -4033,8 +4126,7 @@
_InterestingSemanticsFragment({
required RenderObject owner,
required super.dropsSemanticsOfPreviousSiblings,
- }) : assert(owner != null),
- _ancestorChain = <RenderObject>[owner];
+ }) : _ancestorChain = <RenderObject>[owner];
/// The [RenderObject] that owns this fragment (and any new [SemanticsNode]
/// introduced by it).
@@ -4057,6 +4149,7 @@
required Rect? parentPaintClipRect,
required double elevationAdjustment,
required List<SemanticsNode> result,
+ required List<SemanticsNode> siblingNodes,
});
/// The [SemanticsConfiguration] the child wants to merge into the parent's
@@ -4086,7 +4179,7 @@
bool get hasConfigForParent => config != null;
@override
- List<_InterestingSemanticsFragment> get interestingFragments => <_InterestingSemanticsFragment>[this];
+ List<_InterestingSemanticsFragment> get mergeUpFragments => <_InterestingSemanticsFragment>[this];
Set<SemanticsTag>? _tagsForChildren;
@@ -4124,7 +4217,13 @@
});
@override
- void compileChildren({ Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, required double elevationAdjustment, required List<SemanticsNode> result }) {
+ void compileChildren({
+ Rect? parentSemanticsClipRect,
+ Rect? parentPaintClipRect,
+ required double elevationAdjustment,
+ required List<SemanticsNode> result,
+ required List<SemanticsNode> siblingNodes,
+ }) {
assert(_tagsForChildren == null || _tagsForChildren!.isEmpty);
assert(parentSemanticsClipRect == null);
assert(parentPaintClipRect == null);
@@ -4150,8 +4249,11 @@
parentPaintClipRect: parentPaintClipRect,
elevationAdjustment: 0.0,
result: children,
+ siblingNodes: siblingNodes,
);
}
+ // Root node does not have a parent and thus can't attach sibling nodes.
+ assert(siblingNodes.isEmpty);
node.updateWith(config: null, childrenInInversePaintOrder: children);
// The root node is the only semantics node allowed to be invisible. This
@@ -4201,24 +4303,136 @@
_SwitchableSemanticsFragment({
required bool mergeIntoParent,
required SemanticsConfiguration config,
+ required List<List<_InterestingSemanticsFragment>> siblingMergeGroups,
required super.owner,
required super.dropsSemanticsOfPreviousSiblings,
- }) : _mergeIntoParent = mergeIntoParent,
- _config = config,
- assert(mergeIntoParent != null),
- assert(config != null);
+ }) : _siblingMergeGroups = siblingMergeGroups,
+ _mergeIntoParent = mergeIntoParent,
+ _config = config;
final bool _mergeIntoParent;
SemanticsConfiguration _config;
bool _isConfigWritable = false;
+ bool _mergesToSibling = false;
+
+ final List<List<_InterestingSemanticsFragment>> _siblingMergeGroups;
+
+ void _mergeSiblingGroup(Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, List<SemanticsNode> result, Set<int> usedSemanticsIds) {
+ for (final List<_InterestingSemanticsFragment> group in _siblingMergeGroups) {
+ Rect? rect;
+ Rect? semanticsClipRect;
+ Rect? paintClipRect;
+ SemanticsConfiguration? configuration;
+ // Use empty set because the _tagsForChildren may not contains all of the
+ // tags if this fragment is not explicit. The _tagsForChildren are added
+ // to sibling nodes at the end of compileChildren if this fragment is
+ // explicit.
+ final Set<SemanticsTag> tags = <SemanticsTag>{};
+ SemanticsNode? node;
+ for (final _InterestingSemanticsFragment fragment in group) {
+ if (fragment.config != null) {
+ final _SwitchableSemanticsFragment switchableFragment = fragment as _SwitchableSemanticsFragment;
+ switchableFragment._mergesToSibling = true;
+ node ??= fragment.owner._semantics;
+ if (configuration == null) {
+ switchableFragment._ensureConfigIsWritable();
+ configuration = switchableFragment.config;
+ } else {
+ configuration.absorb(switchableFragment.config!);
+ }
+ // It is a child fragment of a _SwitchableFragment, it must have a
+ // geometry.
+ final _SemanticsGeometry geometry = switchableFragment._computeSemanticsGeometry(
+ parentSemanticsClipRect: parentSemanticsClipRect,
+ parentPaintClipRect: parentPaintClipRect,
+ )!;
+ final Rect fragmentRect = MatrixUtils.transformRect(geometry.transform, geometry.rect);
+ if (rect == null) {
+ rect = fragmentRect;
+ } else {
+ rect = rect.expandToInclude(fragmentRect);
+ }
+ if (geometry.semanticsClipRect != null) {
+ final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.semanticsClipRect!);
+ if (semanticsClipRect == null) {
+ semanticsClipRect = rect;
+ } else {
+ semanticsClipRect = semanticsClipRect.intersect(rect);
+ }
+ }
+ if (geometry.paintClipRect != null) {
+ final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.paintClipRect!);
+ if (paintClipRect == null) {
+ paintClipRect = rect;
+ } else {
+ paintClipRect = paintClipRect.intersect(rect);
+ }
+ }
+ if (switchableFragment._tagsForChildren != null) {
+ tags.addAll(switchableFragment._tagsForChildren!);
+ }
+ }
+ }
+ // Can be null if all fragments in group are marked as explicit.
+ if (configuration != null && !rect!.isEmpty) {
+ if (node == null || usedSemanticsIds.contains(node.id)) {
+ node = SemanticsNode(showOnScreen: owner.showOnScreen);
+ }
+ usedSemanticsIds.add(node.id);
+ node
+ ..tags = tags
+ ..rect = rect
+ ..transform = null // Will be set when compiling immediate parent node.
+ ..parentSemanticsClipRect = semanticsClipRect
+ ..parentPaintClipRect = paintClipRect;
+ for (final _InterestingSemanticsFragment fragment in group) {
+ if (fragment.config != null) {
+ fragment.owner._semantics = node;
+ }
+ }
+ node.updateWith(config: configuration);
+ result.add(node);
+ }
+ }
+ }
+
final List<_InterestingSemanticsFragment> _children = <_InterestingSemanticsFragment>[];
@override
- void compileChildren({ Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, required double elevationAdjustment, required List<SemanticsNode> result }) {
+ void compileChildren({
+ Rect? parentSemanticsClipRect,
+ Rect? parentPaintClipRect,
+ required double elevationAdjustment,
+ required List<SemanticsNode> result,
+ required List<SemanticsNode> siblingNodes,
+ }) {
+ final Set<int> usedSemanticsIds = <int>{};
+ Iterable<_InterestingSemanticsFragment> compilingFragments = _children;
+ for (final List<_InterestingSemanticsFragment> siblingGroup in _siblingMergeGroups) {
+ compilingFragments = compilingFragments.followedBy(siblingGroup);
+ }
if (!_isExplicit) {
- owner._semantics = null;
- for (final _InterestingSemanticsFragment fragment in _children) {
+ if (!_mergesToSibling) {
+ owner._semantics = null;
+ }
+ _mergeSiblingGroup(
+ parentSemanticsClipRect,
+ parentPaintClipRect,
+ siblingNodes,
+ usedSemanticsIds,
+ );
+ for (final _InterestingSemanticsFragment fragment in compilingFragments) {
assert(_ancestorChain.first == fragment._ancestorChain.last);
+ if (fragment is _SwitchableSemanticsFragment) {
+ // Cached semantics node may be part of sibling merging group prior
+ // to this update. In this case, the semantics node may continue to
+ // be reused in that sibling merging group.
+ if (fragment._isExplicit &&
+ fragment.owner._semantics != null &&
+ usedSemanticsIds.contains(fragment.owner._semantics!.id)) {
+ fragment.owner._semantics = null;
+ }
+ }
fragment._ancestorChain.addAll(_ancestorChain.skip(1));
fragment.compileChildren(
parentSemanticsClipRect: parentSemanticsClipRect,
@@ -4228,14 +4442,16 @@
// its children are placed at the elevation dictated by this config.
elevationAdjustment: elevationAdjustment + _config.elevation,
result: result,
+ siblingNodes: siblingNodes,
);
}
return;
}
- final _SemanticsGeometry? geometry = _needsGeometryUpdate
- ? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
- : null;
+ final _SemanticsGeometry? geometry = _computeSemanticsGeometry(
+ parentSemanticsClipRect: parentSemanticsClipRect,
+ parentPaintClipRect: parentPaintClipRect,
+ );
if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) {
return; // Drop the node, it's not going to be visible.
@@ -4264,22 +4480,66 @@
_config.isHidden = true;
}
}
-
final List<SemanticsNode> children = <SemanticsNode>[];
- for (final _InterestingSemanticsFragment fragment in _children) {
+ _mergeSiblingGroup(
+ node.parentSemanticsClipRect,
+ node.parentPaintClipRect,
+ siblingNodes,
+ usedSemanticsIds,
+ );
+ for (final _InterestingSemanticsFragment fragment in compilingFragments) {
+ if (fragment is _SwitchableSemanticsFragment) {
+ // Cached semantics node may be part of sibling merging group prior
+ // to this update. In this case, the semantics node may continue to
+ // be reused in that sibling merging group.
+ if (fragment._isExplicit &&
+ fragment.owner._semantics != null &&
+ usedSemanticsIds.contains(fragment.owner._semantics!.id)) {
+ fragment.owner._semantics = null;
+ }
+ }
+ final List<SemanticsNode> childSiblingNodes = <SemanticsNode>[];
fragment.compileChildren(
parentSemanticsClipRect: node.parentSemanticsClipRect,
parentPaintClipRect: node.parentPaintClipRect,
elevationAdjustment: 0.0,
result: children,
+ siblingNodes: childSiblingNodes,
);
+ siblingNodes.addAll(childSiblingNodes);
}
+
if (_config.isSemanticBoundary) {
owner.assembleSemanticsNode(node, _config, children);
} else {
node.updateWith(config: _config, childrenInInversePaintOrder: children);
}
result.add(node);
+ // Sibling node needs to attach to the parent of an explicit node.
+ for (final SemanticsNode siblingNode in siblingNodes) {
+ // sibling nodes are in the same coordinate of the immediate explicit node.
+ // They need to share the same transform if they are going to attach to the
+ // parent of the immediate explicit node.
+ assert(siblingNode.transform == null);
+ siblingNode
+ ..transform = node.transform
+ ..isMergedIntoParent = node.isMergedIntoParent;
+ if (_tagsForChildren != null) {
+ siblingNode.tags ??= <SemanticsTag>{};
+ siblingNode.tags!.addAll(_tagsForChildren!);
+ }
+ }
+ result.addAll(siblingNodes);
+ siblingNodes.clear();
+ }
+
+ _SemanticsGeometry? _computeSemanticsGeometry({
+ required Rect? parentSemanticsClipRect,
+ required Rect? parentPaintClipRect,
+ }) {
+ return _needsGeometryUpdate
+ ? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
+ : null;
}
@override
@@ -4403,7 +4663,6 @@
/// From parent to child coordinate system.
static Rect? _transformRect(Rect? rect, Matrix4 transform) {
- assert(transform != null);
if (rect == null) {
return null;
}
@@ -4424,18 +4683,12 @@
Matrix4 transform,
Matrix4 clipRectTransform,
) {
- assert(ancestor != null);
- assert(child != null);
- assert(transform != null);
- assert(clipRectTransform != null);
assert(clipRectTransform.isIdentity());
RenderObject intermediateParent = child.parent! as RenderObject;
- assert(intermediateParent != null);
while (intermediateParent != ancestor) {
intermediateParent.applyPaintTransform(child, transform);
intermediateParent = intermediateParent.parent! as RenderObject;
child = child.parent! as RenderObject;
- assert(intermediateParent != null);
}
ancestor.applyPaintTransform(child, transform);
ancestor.applyPaintTransform(child, clipRectTransform);
@@ -4481,8 +4734,7 @@
/// Create a [DiagnosticsProperty] with its [value] initialized to input
/// [RenderObject.debugCreator].
DiagnosticsDebugCreator(Object value)
- : assert(value != null),
- super(
+ : super(
'debugCreator',
value,
level: DiagnosticLevel.hidden,
diff --git a/framework/lib/src/rendering/paragraph.dart b/framework/lib/src/rendering/paragraph.dart
index d58ea02..5b44dc1 100644
--- a/framework/lib/src/rendering/paragraph.dart
+++ b/framework/lib/src/rendering/paragraph.dart
@@ -8,7 +8,6 @@
import 'package:flute/foundation.dart';
import 'package:flute/gestures.dart';
-import 'package:flute/scheduler.dart';
import 'package:flute/semantics.dart';
import 'package:flute/services.dart';
@@ -89,15 +88,8 @@
List<RenderBox>? children,
Color? selectionColor,
SelectionRegistrar? registrar,
- }) : assert(text != null),
- assert(text.debugAssertIsValid()),
- assert(textAlign != null),
- assert(textDirection != null),
- assert(softWrap != null),
- assert(overflow != null),
- assert(textScaleFactor != null),
+ }) : assert(text.debugAssertIsValid()),
assert(maxLines == null || maxLines > 0),
- assert(textWidthBasis != null),
_softWrap = softWrap,
_overflow = overflow,
_selectionColor = selectionColor,
@@ -133,11 +125,14 @@
/// The text to display.
InlineSpan get text => _textPainter.text!;
set text(InlineSpan value) {
- assert(value != null);
switch (_textPainter.text!.compareTo(value)) {
case RenderComparison.identical:
- case RenderComparison.metadata:
return;
+ case RenderComparison.metadata:
+ _textPainter.text = value;
+ _cachedCombinedSemanticsInfos = null;
+ markNeedsSemanticsUpdate();
+ break;
case RenderComparison.paint:
_textPainter.text = value;
_cachedAttributedLabel = null;
@@ -276,7 +271,6 @@
/// How the text should be aligned horizontally.
TextAlign get textAlign => _textPainter.textAlign;
set textAlign(TextAlign value) {
- assert(value != null);
if (_textPainter.textAlign == value) {
return;
}
@@ -299,7 +293,6 @@
/// This must not be null.
TextDirection get textDirection => _textPainter.textDirection!;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textPainter.textDirection == value) {
return;
}
@@ -317,7 +310,6 @@
bool get softWrap => _softWrap;
bool _softWrap;
set softWrap(bool value) {
- assert(value != null);
if (_softWrap == value) {
return;
}
@@ -329,7 +321,6 @@
TextOverflow get overflow => _overflow;
TextOverflow _overflow;
set overflow(TextOverflow value) {
- assert(value != null);
if (_overflow == value) {
return;
}
@@ -344,7 +335,6 @@
/// the specified font size.
double get textScaleFactor => _textPainter.textScaleFactor;
set textScaleFactor(double value) {
- assert(value != null);
if (_textPainter.textScaleFactor == value) {
return;
}
@@ -402,7 +392,6 @@
/// {@macro flutter.painting.textPainter.textWidthBasis}
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
- assert(value != null);
if (_textPainter.textWidthBasis == value) {
return;
}
@@ -487,7 +476,6 @@
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!debugNeedsLayout);
- assert(constraints != null);
assert(constraints.debugAssertIsValid());
_layoutTextWithConstraints(constraints);
// TODO(garyq): Since our metric for ideographic baseline is currently
@@ -650,37 +638,10 @@
);
}
- bool _systemFontsChangeScheduled = false;
@override
void systemFontsDidChange() {
- final SchedulerPhase phase = SchedulerBinding.instance.schedulerPhase;
- switch (phase) {
- case SchedulerPhase.idle:
- case SchedulerPhase.postFrameCallbacks:
- if (_systemFontsChangeScheduled) {
- return;
- }
- _systemFontsChangeScheduled = true;
- SchedulerBinding.instance.scheduleFrameCallback((Duration timeStamp) {
- assert(_systemFontsChangeScheduled);
- _systemFontsChangeScheduled = false;
- assert(
- attached || (debugDisposed ?? true),
- '$this is detached during $phase but not disposed.',
- );
- if (attached) {
- super.systemFontsDidChange();
- _textPainter.markNeedsLayout();
- }
- });
- break;
- case SchedulerPhase.transientCallbacks:
- case SchedulerPhase.midFrameMicrotasks:
- case SchedulerPhase.persistentCallbacks:
- super.systemFontsDidChange();
- _textPainter.markNeedsLayout();
- break;
- }
+ super.systemFontsDidChange();
+ _textPainter.markNeedsLayout();
}
// Placeholder dimensions representing the sizes of child inline widgets.
@@ -837,7 +798,6 @@
_overflowShader = null;
break;
case TextOverflow.fade:
- assert(textDirection != null);
_needsClipping = true;
final TextPainter fadeSizePainter = TextPainter(
text: TextSpan(style: _textPainter.text!.style, text: '\u2026'),
@@ -999,8 +959,6 @@
ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
}) {
assert(!debugNeedsLayout);
- assert(boxHeightStyle != null);
- assert(boxWidthStyle != null);
_layoutTextWithConstraints(constraints);
return _textPainter.getBoxesForSelection(
selection,
@@ -1583,21 +1541,21 @@
switch (granularity) {
case TextGranularity.character:
final String text = range.textInside(fullText);
- newPosition = _getNextPosition(CharacterBoundary(text), targetedEdge, forward);
+ newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, CharacterBoundary(text));
result = SelectionResult.end;
break;
case TextGranularity.word:
- final String text = range.textInside(fullText);
- newPosition = _getNextPosition(WhitespaceBoundary(text) + WordBoundary(this), targetedEdge, forward);
+ final TextBoundary textBoundary = paragraph._textPainter.wordBoundaries.moveByWordBoundary;
+ newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, textBoundary);
result = SelectionResult.end;
break;
case TextGranularity.line:
- newPosition = _getNextPosition(LineBreak(this), targetedEdge, forward);
+ newPosition = _moveToTextBoundaryAtDirection(targetedEdge, forward, LineBoundary(this));
result = SelectionResult.end;
break;
case TextGranularity.document:
final String text = range.textInside(fullText);
- newPosition = _getNextPosition(DocumentBoundary(text), targetedEdge, forward);
+ newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, DocumentBoundary(text));
if (forward && newPosition.offset == range.end) {
result = SelectionResult.next;
} else if (!forward && newPosition.offset == range.start) {
@@ -1616,15 +1574,43 @@
return result;
}
- TextPosition _getNextPosition(TextBoundary boundary, TextPosition position, bool forward) {
- if (forward) {
- return _clampTextPosition(
- (PushTextPosition.forward + boundary).getTrailingTextBoundaryAt(position)
- );
+ // Move **beyond** the local boundary of the given type (unless range.start or
+ // range.end is reached). Used for most TextGranularity types except for
+ // TextGranularity.line, to ensure the selection movement doesn't get stuck at
+ // a local fixed point.
+ TextPosition _moveBeyondTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
+ final int newOffset = forward
+ ? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end
+ : textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start;
+ return TextPosition(offset: newOffset);
+ }
+
+ // Move **to** the local boundary of the given type. Typically used for line
+ // boundaries, such that performing "move to line start" more than once never
+ // moves the selection to the previous line.
+ TextPosition _moveToTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
+ assert(end.offset >= 0);
+ final int caretOffset;
+ switch (end.affinity) {
+ case TextAffinity.upstream:
+ if (end.offset < 1 && !forward) {
+ assert (end.offset == 0);
+ return const TextPosition(offset: 0);
+ }
+ final CharacterBoundary characterBoundary = CharacterBoundary(fullText);
+ caretOffset = math.max(
+ 0,
+ characterBoundary.getLeadingTextBoundaryAt(range.start + end.offset) ?? range.start,
+ ) - 1;
+ break;
+ case TextAffinity.downstream:
+ caretOffset = end.offset;
+ break;
}
- return _clampTextPosition(
- (PushTextPosition.backward + boundary).getLeadingTextBoundaryAt(position),
- );
+ final int offset = forward
+ ? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end
+ : textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start;
+ return TextPosition(offset: offset);
}
MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
diff --git a/framework/lib/src/rendering/performance_overlay.dart b/framework/lib/src/rendering/performance_overlay.dart
index 773f2eb..d48e213 100644
--- a/framework/lib/src/rendering/performance_overlay.dart
+++ b/framework/lib/src/rendering/performance_overlay.dart
@@ -67,11 +67,7 @@
int rasterizerThreshold = 0,
bool checkerboardRasterCacheImages = false,
bool checkerboardOffscreenLayers = false,
- }) : assert(optionsMask != null),
- assert(rasterizerThreshold != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- _optionsMask = optionsMask,
+ }) : _optionsMask = optionsMask,
_rasterizerThreshold = rasterizerThreshold,
_checkerboardRasterCacheImages = checkerboardRasterCacheImages,
_checkerboardOffscreenLayers = checkerboardOffscreenLayers;
@@ -81,7 +77,6 @@
int get optionsMask => _optionsMask;
int _optionsMask;
set optionsMask(int value) {
- assert(value != null);
if (value == _optionsMask) {
return;
}
@@ -95,7 +90,6 @@
int get rasterizerThreshold => _rasterizerThreshold;
int _rasterizerThreshold;
set rasterizerThreshold(int value) {
- assert(value != null);
if (value == _rasterizerThreshold) {
return;
}
@@ -107,7 +101,6 @@
bool get checkerboardRasterCacheImages => _checkerboardRasterCacheImages;
bool _checkerboardRasterCacheImages;
set checkerboardRasterCacheImages(bool value) {
- assert(value != null);
if (value == _checkerboardRasterCacheImages) {
return;
}
@@ -119,7 +112,6 @@
bool get checkerboardOffscreenLayers => _checkerboardOffscreenLayers;
bool _checkerboardOffscreenLayers;
set checkerboardOffscreenLayers(bool value) {
- assert(value != null);
if (value == _checkerboardOffscreenLayers) {
return;
}
diff --git a/framework/lib/src/rendering/platform_view.dart b/framework/lib/src/rendering/platform_view.dart
index 0f4136e..2c3c90d 100644
--- a/framework/lib/src/rendering/platform_view.dart
+++ b/framework/lib/src/rendering/platform_view.dart
@@ -80,11 +80,7 @@
required PlatformViewHitTestBehavior hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
Clip clipBehavior = Clip.hardEdge,
- }) : assert(viewController != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null),
- assert(clipBehavior != null),
- _viewController = viewController,
+ }) : _viewController = viewController,
_clipBehavior = clipBehavior,
super(controller: viewController, hitTestBehavior: hitTestBehavior, gestureRecognizers: gestureRecognizers) {
_viewController.pointTransformer = (Offset offset) => globalToLocal(offset);
@@ -110,8 +106,6 @@
@override
set controller(AndroidViewController controller) {
assert(!_isDisposed);
- assert(_viewController != null);
- assert(controller != null);
if (_viewController == controller) {
return;
}
@@ -132,7 +126,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -278,13 +271,6 @@
/// A render object for an iOS UIKit UIView.
///
-/// {@template flutter.rendering.RenderUiKitView}
-/// Embedding UIViews is still preview-quality. To enable the preview for an iOS app add a boolean
-/// field with the key 'io.flutter.embedded_views_preview' and the value set to 'YES' to the
-/// application's Info.plist file. A list of open issued with embedding UIViews is available on
-/// [Github](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3A%22a%3A+platform-views%22+label%3Aplatform-ios+sort%3Acreated-asc)
-/// {@endtemplate}
-///
/// [RenderUiKitView] is responsible for sizing and displaying an iOS
/// [UIView](https://developer.apple.com/documentation/uikit/uiview).
///
@@ -306,10 +292,7 @@
required UiKitViewController viewController,
required this.hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
- }) : assert(viewController != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null),
- _viewController = viewController {
+ }) : _viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
@@ -321,7 +304,6 @@
UiKitViewController get viewController => _viewController;
UiKitViewController _viewController;
set viewController(UiKitViewController value) {
- assert(value != null);
if (_viewController == value) {
return;
}
@@ -340,7 +322,6 @@
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
- assert(gestureRecognizers != null);
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
'There were multiple gesture recognizer factories for the same type, there must only be a single '
@@ -639,9 +620,7 @@
required PlatformViewController controller,
required PlatformViewHitTestBehavior hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
- }) : assert(controller != null && controller.viewId != null && controller.viewId > -1),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null),
+ }) : assert(controller.viewId > -1),
_controller = controller {
this.hitTestBehavior = hitTestBehavior;
updateGestureRecognizers(gestureRecognizers);
@@ -652,8 +631,7 @@
PlatformViewController _controller;
/// This value must not be null, and setting it to a new value will result in a repaint.
set controller(covariant PlatformViewController controller) {
- assert(controller != null);
- assert(controller.viewId != null && controller.viewId > -1);
+ assert(controller.viewId > -1);
if (_controller == controller) {
return;
@@ -702,7 +680,6 @@
@override
void paint(PaintingContext context, Offset offset) {
- assert(_controller.viewId != null);
context.addLayer(PlatformViewLayer(
rect: offset & size,
viewId: _controller.viewId,
@@ -712,7 +689,6 @@
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
- assert(_controller.viewId != null);
config.isSemanticBoundary = true;
config.platformViewId = _controller.viewId;
}
@@ -740,7 +716,6 @@
/// Any active gesture arena the `PlatformView` participates in is rejected when the
/// set of gesture recognizers is changed.
void _updateGestureRecognizersWithCallBack(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, _HandlePointerEvent handlePointerEvent) {
- assert(gestureRecognizers != null);
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
'There were multiple gesture recognizer factories for the same type, there must only be a single '
diff --git a/framework/lib/src/rendering/proxy_box.dart b/framework/lib/src/rendering/proxy_box.dart
index af37616..8f54447 100644
--- a/framework/lib/src/rendering/proxy_box.dart
+++ b/framework/lib/src/rendering/proxy_box.dart
@@ -24,7 +24,7 @@
/// A base class for render boxes that resemble their children.
///
-/// A proxy box has a single child and simply mimics all the properties of that
+/// A proxy box has a single child and mimics all the properties of that
/// child by calling through to the child for each function in the render box
/// protocol. For example, a proxy box determines its size by asking its child
/// to layout with the same constraints and then matching the size.
@@ -41,7 +41,7 @@
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {
/// Creates a proxy render box.
///
- /// Proxy render boxes are rarely created directly because they simply proxy
+ /// Proxy render boxes are rarely created directly because they proxy
/// the render box protocol to [child]. Instead, consider using one of the
/// subclasses.
RenderProxyBox([RenderBox? child]) {
@@ -220,8 +220,7 @@
RenderConstrainedBox({
RenderBox? child,
required BoxConstraints additionalConstraints,
- }) : assert(additionalConstraints != null),
- assert(additionalConstraints.debugAssertIsValid()),
+ }) : assert(additionalConstraints.debugAssertIsValid()),
_additionalConstraints = additionalConstraints,
super(child);
@@ -229,7 +228,6 @@
BoxConstraints get additionalConstraints => _additionalConstraints;
BoxConstraints _additionalConstraints;
set additionalConstraints(BoxConstraints value) {
- assert(value != null);
assert(value.debugAssertIsValid());
if (_additionalConstraints == value) {
return;
@@ -353,8 +351,8 @@
RenderBox? child,
double maxWidth = double.infinity,
double maxHeight = double.infinity,
- }) : assert(maxWidth != null && maxWidth >= 0.0),
- assert(maxHeight != null && maxHeight >= 0.0),
+ }) : assert(maxWidth >= 0.0),
+ assert(maxHeight >= 0.0),
_maxWidth = maxWidth,
_maxHeight = maxHeight,
super(child);
@@ -363,7 +361,7 @@
double get maxWidth => _maxWidth;
double _maxWidth;
set maxWidth(double value) {
- assert(value != null && value >= 0.0);
+ assert(value >= 0.0);
if (_maxWidth == value) {
return;
}
@@ -375,7 +373,7 @@
double get maxHeight => _maxHeight;
double _maxHeight;
set maxHeight(double value) {
- assert(value != null && value >= 0.0);
+ assert(value >= 0.0);
if (_maxHeight == value) {
return;
}
@@ -457,8 +455,7 @@
RenderAspectRatio({
RenderBox? child,
required double aspectRatio,
- }) : assert(aspectRatio != null),
- assert(aspectRatio > 0.0),
+ }) : assert(aspectRatio > 0.0),
assert(aspectRatio.isFinite),
_aspectRatio = aspectRatio,
super(child);
@@ -470,7 +467,6 @@
double get aspectRatio => _aspectRatio;
double _aspectRatio;
set aspectRatio(double value) {
- assert(value != null);
assert(value > 0.0);
assert(value.isFinite);
if (_aspectRatio == value) {
@@ -869,7 +865,7 @@
///
/// For values of opacity other than 0.0 and 1.0, this class is relatively
/// expensive because it requires painting the child into an intermediate
-/// buffer. For the value 0.0, the child is simply not painted at all. For the
+/// buffer. For the value 0.0, the child is not painted at all. For the
/// value 1.0, the child is painted immediately without an intermediate buffer.
class RenderOpacity extends RenderProxyBox {
/// Creates a partially transparent render object.
@@ -879,9 +875,7 @@
double opacity = 1.0,
bool alwaysIncludeSemantics = false,
RenderBox? child,
- }) : assert(opacity != null),
- assert(opacity >= 0.0 && opacity <= 1.0),
- assert(alwaysIncludeSemantics != null),
+ }) : assert(opacity >= 0.0 && opacity <= 1.0),
_opacity = opacity,
_alwaysIncludeSemantics = alwaysIncludeSemantics,
_alpha = ui.Color.getAlphaFromOpacity(opacity),
@@ -890,6 +884,9 @@
@override
bool get alwaysNeedsCompositing => child != null && _alpha > 0;
+ @override
+ bool get isRepaintBoundary => alwaysNeedsCompositing;
+
int _alpha;
/// The fraction to scale the child's alpha value.
@@ -905,7 +902,6 @@
double get opacity => _opacity;
double _opacity;
set opacity(double value) {
- assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (_opacity == value) {
return;
@@ -917,7 +913,7 @@
if (didNeedCompositing != alwaysNeedsCompositing) {
markNeedsCompositingBitsUpdate();
}
- markNeedsPaint();
+ markNeedsCompositedLayerUpdate();
if (wasVisible != (_alpha != 0) && !alwaysIncludeSemantics) {
markNeedsSemanticsUpdate();
}
@@ -945,22 +941,18 @@
}
@override
- void paint(PaintingContext context, Offset offset) {
- if (child == null) {
- return;
- }
- if (_alpha == 0) {
- // No need to keep the layer. We'll create a new one if necessary.
- layer = null;
- return;
- }
+ OffsetLayer updateCompositedLayer({required covariant OpacityLayer? oldLayer}) {
+ final OpacityLayer layer = oldLayer ?? OpacityLayer();
+ layer.alpha = _alpha;
+ return layer;
+ }
- assert(needsCompositing);
- layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
- assert(() {
- layer!.debugCreator = debugCreator;
- return true;
- }());
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ if (child == null || _alpha == 0) {
+ return;
+ }
+ super.paint(context, offset);
}
@override
@@ -1010,7 +1002,6 @@
Animation<double> get opacity => _opacity!;
Animation<double>? _opacity;
set opacity(Animation<double> value) {
- assert(value != null);
if (_opacity == value) {
return;
}
@@ -1112,9 +1103,7 @@
required Animation<double> opacity,
bool alwaysIncludeSemantics = false,
RenderBox? child,
- }) : assert(opacity != null),
- assert(alwaysIncludeSemantics != null),
- super(child) {
+ }) : super(child) {
this.opacity = opacity;
this.alwaysIncludeSemantics = alwaysIncludeSemantics;
}
@@ -1137,9 +1126,7 @@
RenderBox? child,
required ShaderCallback shaderCallback,
BlendMode blendMode = BlendMode.modulate,
- }) : assert(shaderCallback != null),
- assert(blendMode != null),
- _shaderCallback = shaderCallback,
+ }) : _shaderCallback = shaderCallback,
_blendMode = blendMode,
super(child);
@@ -1158,7 +1145,6 @@
ShaderCallback get shaderCallback => _shaderCallback;
ShaderCallback _shaderCallback;
set shaderCallback(ShaderCallback value) {
- assert(value != null);
if (_shaderCallback == value) {
return;
}
@@ -1173,7 +1159,6 @@
BlendMode get blendMode => _blendMode;
BlendMode _blendMode;
set blendMode(BlendMode value) {
- assert(value != null);
if (_blendMode == value) {
return;
}
@@ -1215,9 +1200,7 @@
/// The [blendMode] argument, if provided, must not be null
/// and will default to [BlendMode.srcOver].
RenderBackdropFilter({ RenderBox? child, required ui.ImageFilter filter, BlendMode blendMode = BlendMode.srcOver })
- : assert(filter != null),
- assert(blendMode != null),
- _filter = filter,
+ : _filter = filter,
_blendMode = blendMode,
super(child);
@@ -1232,7 +1215,6 @@
ui.ImageFilter get filter => _filter;
ui.ImageFilter _filter;
set filter(ui.ImageFilter value) {
- assert(value != null);
if (_filter == value) {
return;
}
@@ -1247,7 +1229,6 @@
BlendMode get blendMode => _blendMode;
BlendMode _blendMode;
set blendMode(BlendMode value) {
- assert(value != null);
if (_blendMode == value) {
return;
}
@@ -1372,7 +1353,7 @@
const ShapeBorderClipper({
required this.shape,
this.textDirection,
- }) : assert(shape != null);
+ });
/// The shape border whose outer path this clipper clips to.
final ShapeBorder shape;
@@ -1405,8 +1386,7 @@
RenderBox? child,
CustomClipper<T>? clipper,
Clip clipBehavior = Clip.antiAlias,
- }) : assert(clipBehavior != null),
- _clipper = clipper,
+ }) : _clipper = clipper,
_clipBehavior = clipBehavior,
super(child);
@@ -1541,7 +1521,7 @@
super.child,
super.clipper,
super.clipBehavior,
- }) : assert(clipBehavior != null);
+ });
@override
Rect get _defaultClip => Offset.zero & size;
@@ -1613,15 +1593,11 @@
RenderClipRRect({
super.child,
BorderRadiusGeometry borderRadius = BorderRadius.zero,
- CustomClipper<RRect>? clipper,
+ super.clipper,
super.clipBehavior,
TextDirection? textDirection,
- }) : assert(clipBehavior != null),
- _borderRadius = borderRadius,
- _textDirection = textDirection,
- super(clipper: clipper) {
- assert(_borderRadius != null || clipper != null);
- }
+ }) : _borderRadius = borderRadius,
+ _textDirection = textDirection;
/// The border radius of the rounded corners.
///
@@ -1632,7 +1608,6 @@
BorderRadiusGeometry get borderRadius => _borderRadius;
BorderRadiusGeometry _borderRadius;
set borderRadius(BorderRadiusGeometry value) {
- assert(value != null);
if (_borderRadius == value) {
return;
}
@@ -1721,7 +1696,7 @@
super.child,
super.clipper,
super.clipBehavior,
- }) : assert(clipBehavior != null);
+ });
Rect? _cachedRect;
late Path _cachedPath;
@@ -1817,7 +1792,7 @@
super.child,
super.clipper,
super.clipBehavior,
- }) : assert(clipBehavior != null);
+ });
@override
Path get _defaultClip => Path()..addRect(Offset.zero & size);
@@ -1886,10 +1861,7 @@
required Color shadowColor,
super.clipBehavior = Clip.none,
super.clipper,
- }) : assert(elevation != null && elevation >= 0.0),
- assert(color != null),
- assert(shadowColor != null),
- assert(clipBehavior != null),
+ }) : assert(elevation >= 0.0),
_elevation = elevation,
_color = color,
_shadowColor = shadowColor;
@@ -1903,7 +1875,7 @@
double get elevation => _elevation;
double _elevation;
set elevation(double value) {
- assert(value != null && value >= 0.0);
+ assert(value >= 0.0);
if (elevation == value) {
return;
}
@@ -1919,7 +1891,6 @@
Color get shadowColor => _shadowColor;
Color _shadowColor;
set shadowColor(Color value) {
- assert(value != null);
if (shadowColor == value) {
return;
}
@@ -1931,7 +1902,6 @@
Color get color => _color;
Color _color;
set color(Color value) {
- assert(value != null);
if (color == value) {
return;
}
@@ -1976,11 +1946,7 @@
super.elevation = 0.0,
required super.color,
super.shadowColor = const Color(0xFF000000),
- }) : assert(shape != null),
- assert(clipBehavior != null),
- assert(elevation != null && elevation >= 0.0),
- assert(color != null),
- assert(shadowColor != null),
+ }) : assert(elevation >= 0.0),
_shape = shape,
_borderRadius = borderRadius;
@@ -1991,7 +1957,6 @@
BoxShape get shape => _shape;
BoxShape _shape;
set shape(BoxShape value) {
- assert(value != null);
if (shape == value) {
return;
}
@@ -2020,7 +1985,6 @@
@override
RRect get _defaultClip {
assert(hasSize);
- assert(_shape != null);
final Rect rect = Offset.zero & size;
switch (_shape) {
case BoxShape.rectangle:
@@ -2149,10 +2113,7 @@
super.elevation = 0.0,
required super.color,
super.shadowColor = const Color(0xFF000000),
- }) : assert(clipper != null),
- assert(elevation != null && elevation >= 0.0),
- assert(color != null),
- assert(shadowColor != null);
+ }) : assert(elevation >= 0.0);
@override
Path get _defaultClip => Path()..addRect(Offset.zero & size);
@@ -2275,10 +2236,7 @@
DecorationPosition position = DecorationPosition.background,
ImageConfiguration configuration = ImageConfiguration.empty,
RenderBox? child,
- }) : assert(decoration != null),
- assert(position != null),
- assert(configuration != null),
- _decoration = decoration,
+ }) : _decoration = decoration,
_position = position,
_configuration = configuration,
super(child);
@@ -2291,7 +2249,6 @@
Decoration get decoration => _decoration;
Decoration _decoration;
set decoration(Decoration value) {
- assert(value != null);
if (value == _decoration) {
return;
}
@@ -2305,7 +2262,6 @@
DecorationPosition get position => _position;
DecorationPosition _position;
set position(DecorationPosition value) {
- assert(value != null);
if (value == _position) {
return;
}
@@ -2322,7 +2278,6 @@
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
- assert(value != null);
if (value == _configuration) {
return;
}
@@ -2350,8 +2305,6 @@
@override
void paint(PaintingContext context, Offset offset) {
- assert(size.width != null);
- assert(size.height != null);
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {
@@ -2410,8 +2363,7 @@
this.transformHitTests = true,
FilterQuality? filterQuality,
RenderBox? child,
- }) : assert(transform != null),
- super(child) {
+ }) : super(child) {
this.transform = transform;
this.alignment = alignment;
this.textDirection = textDirection;
@@ -2491,7 +2443,6 @@
/// mutations outside of the control of the render object could not reliably
/// be reflected in the rendering.
set transform(Matrix4 value) { // ignore: avoid_setters_without_getters
- assert(value != null);
if (_transform == value) {
return;
}
@@ -2678,10 +2629,7 @@
TextDirection? textDirection,
RenderBox? child,
Clip clipBehavior = Clip.none,
- }) : assert(fit != null),
- assert(alignment != null),
- assert(clipBehavior != null),
- _fit = fit,
+ }) : _fit = fit,
_alignment = alignment,
_textDirection = textDirection,
_clipBehavior = clipBehavior,
@@ -2719,7 +2667,6 @@
BoxFit get fit => _fit;
BoxFit _fit;
set fit(BoxFit value) {
- assert(value != null);
if (_fit == value) {
return;
}
@@ -2744,7 +2691,6 @@
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
set alignment(AlignmentGeometry value) {
- assert(value != null);
if (_alignment == value) {
return;
}
@@ -2845,7 +2791,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -2977,8 +2922,7 @@
required Offset translation,
this.transformHitTests = true,
RenderBox? child,
- }) : assert(translation != null),
- _translation = translation,
+ }) : _translation = translation,
super(child);
/// The translation to apply to the child, scaled to the child's size.
@@ -2988,7 +2932,6 @@
Offset get translation => _translation;
Offset _translation;
set translation(Offset value) {
- assert(value != null);
if (_translation == value) {
return;
}
@@ -3247,9 +3190,7 @@
bool opaque = true,
super.child,
HitTestBehavior? hitTestBehavior = HitTestBehavior.opaque,
- }) : assert(opaque != null),
- assert(cursor != null),
- _cursor = cursor,
+ }) : _cursor = cursor,
_validForMouseTracker = validForMouseTracker,
_opaque = opaque,
super(behavior: hitTestBehavior ?? HitTestBehavior.opaque);
@@ -3650,9 +3591,7 @@
bool? ignoringSemantics,
}) : _ignoring = ignoring,
_ignoringSemantics = ignoringSemantics,
- super(child) {
- assert(_ignoring != null);
- }
+ super(child);
/// Whether this render object is ignored during hit testing.
///
@@ -3661,7 +3600,6 @@
bool get ignoring => _ignoring;
bool _ignoring;
set ignoring(bool value) {
- assert(value != null);
if (value == _ignoring) {
return;
}
@@ -3728,8 +3666,7 @@
RenderOffstage({
bool offstage = true,
RenderBox? child,
- }) : assert(offstage != null),
- _offstage = offstage,
+ }) : _offstage = offstage,
super(child);
/// Whether the child is hidden from the rest of the tree.
@@ -3742,7 +3679,6 @@
bool get offstage => _offstage;
bool _offstage;
set offstage(bool value) {
- assert(value != null);
if (value == _offstage) {
return;
}
@@ -3883,8 +3819,7 @@
RenderBox? child,
bool absorbing = true,
bool? ignoringSemantics,
- }) : assert(absorbing != null),
- _absorbing = absorbing,
+ }) : _absorbing = absorbing,
_ignoringSemantics = ignoringSemantics,
super(child);
@@ -3993,8 +3928,7 @@
GestureDragUpdateCallback? onVerticalDragUpdate,
this.scrollFactor = 0.8,
super.behavior,
- }) : assert(scrollFactor != null),
- _onTap = onTap,
+ }) : _onTap = onTap,
_onLongPress = onLongPress,
_onHorizontalDragUpdate = onHorizontalDragUpdate,
_onVerticalDragUpdate = onVerticalDragUpdate;
@@ -4187,8 +4121,7 @@
bool explicitChildNodes = false,
bool excludeSemantics = false,
TextDirection? textDirection,
- }) : assert(container != null),
- _container = container,
+ }) : _container = container,
_explicitChildNodes = explicitChildNodes,
_excludeSemantics = excludeSemantics,
_textDirection = textDirection,
@@ -4201,7 +4134,6 @@
SemanticsProperties get properties => _properties;
SemanticsProperties _properties;
set properties(SemanticsProperties value) {
- assert(value != null);
if (_properties == value) {
return;
}
@@ -4220,7 +4152,6 @@
bool get container => _container;
bool _container;
set container(bool value) {
- assert(value != null);
if (container == value) {
return;
}
@@ -4244,7 +4175,6 @@
bool get explicitChildNodes => _explicitChildNodes;
bool _explicitChildNodes;
set explicitChildNodes(bool value) {
- assert(value != null);
if (_explicitChildNodes == value) {
return;
}
@@ -4261,7 +4191,6 @@
bool get excludeSemantics => _excludeSemantics;
bool _excludeSemantics;
set excludeSemantics(bool value) {
- assert(value != null);
if (_excludeSemantics == value) {
return;
}
@@ -4624,7 +4553,6 @@
bool get blocking => _blocking;
bool _blocking;
set blocking(bool value) {
- assert(value != null);
if (value == _blocking) {
return;
}
@@ -4678,15 +4606,12 @@
RenderBox? child,
bool excluding = true,
}) : _excluding = excluding,
- super(child) {
- assert(_excluding != null);
- }
+ super(child);
/// Whether this render object is excluded from the semantic tree.
bool get excluding => _excluding;
bool _excluding;
set excluding(bool value) {
- assert(value != null);
if (value == _excluding) {
return;
}
@@ -4724,8 +4649,7 @@
RenderIndexedSemantics({
RenderBox? child,
required int index,
- }) : assert(index != null),
- _index = index,
+ }) : _index = index,
super(child);
/// The index used to annotated child semantics.
@@ -4765,8 +4689,7 @@
RenderLeaderLayer({
required LayerLink link,
RenderBox? child,
- }) : assert(link != null),
- _link = link,
+ }) : _link = link,
super(child);
/// The link object that connects this [RenderLeaderLayer] with one or more
@@ -4777,7 +4700,6 @@
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
- assert(value != null);
if (_link == value) {
return;
}
@@ -4852,10 +4774,7 @@
Alignment leaderAnchor = Alignment.topLeft,
Alignment followerAnchor = Alignment.topLeft,
RenderBox? child,
- }) : assert(link != null),
- assert(showWhenUnlinked != null),
- assert(offset != null),
- _link = link,
+ }) : _link = link,
_showWhenUnlinked = showWhenUnlinked,
_offset = offset,
_leaderAnchor = leaderAnchor,
@@ -4867,7 +4786,6 @@
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
- assert(value != null);
if (_link == value) {
return;
}
@@ -4887,7 +4805,6 @@
bool get showWhenUnlinked => _showWhenUnlinked;
bool _showWhenUnlinked;
set showWhenUnlinked(bool value) {
- assert(value != null);
if (_showWhenUnlinked == value) {
return;
}
@@ -4900,7 +4817,6 @@
Offset get offset => _offset;
Offset _offset;
set offset(Offset value) {
- assert(value != null);
if (_offset == value) {
return;
}
@@ -4925,7 +4841,6 @@
Alignment get leaderAnchor => _leaderAnchor;
Alignment _leaderAnchor;
set leaderAnchor(Alignment value) {
- assert(value != null);
if (_leaderAnchor == value) {
return;
}
@@ -4942,7 +4857,6 @@
Alignment get followerAnchor => _followerAnchor;
Alignment _followerAnchor;
set followerAnchor(Alignment value) {
- assert(value != null);
if (_followerAnchor == value) {
return;
}
@@ -5009,7 +4923,6 @@
final Offset effectiveLinkedOffset = leaderSize == null
? this.offset
: leaderAnchor.alongSize(leaderSize) - followerAnchor.alongSize(size) + this.offset;
- assert(showWhenUnlinked != null);
if (layer == null) {
layer = FollowerLayer(
link: link,
@@ -5076,9 +4989,7 @@
required T value,
required bool sized,
RenderBox? child,
- }) : assert(value != null),
- assert(sized != null),
- _value = value,
+ }) : _value = value,
_sized = sized,
super(child);
diff --git a/framework/lib/src/rendering/proxy_sliver.dart b/framework/lib/src/rendering/proxy_sliver.dart
index 2a1f151..0e23813 100644
--- a/framework/lib/src/rendering/proxy_sliver.dart
+++ b/framework/lib/src/rendering/proxy_sliver.dart
@@ -15,7 +15,7 @@
/// A base class for sliver render objects that resemble their children.
///
-/// A proxy sliver has a single child and simply mimics all the properties of
+/// A proxy sliver has a single child and mimics all the properties of
/// that child by calling through to the child for each function in the render
/// sliver protocol. For example, a proxy sliver determines its geometry by
/// asking its sliver child to layout with the same constraints and then
@@ -33,7 +33,7 @@
abstract class RenderProxySliver extends RenderSliver with RenderObjectWithChildMixin<RenderSliver> {
/// Creates a proxy render sliver.
///
- /// Proxy render slivers aren't created directly because they simply proxy
+ /// Proxy render slivers aren't created directly because they proxy
/// the render sliver protocol to their sliver [child]. Instead, use one of
/// the subclasses.
RenderProxySliver([RenderSliver? child]) {
@@ -74,14 +74,12 @@
@override
double childMainAxisPosition(RenderSliver child) {
- assert(child != null);
assert(child == this.child);
return 0.0;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- assert(child != null);
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
}
@@ -94,7 +92,7 @@
///
/// For values of opacity other than 0.0 and 1.0, this class is relatively
/// expensive, because it requires painting the sliver child into an intermediate
-/// buffer. For the value 0.0, the sliver child is simply not painted at all.
+/// buffer. For the value 0.0, the sliver child is not painted at all.
/// For the value 1.0, the sliver child is painted immediately without an
/// intermediate buffer.
class RenderSliverOpacity extends RenderProxySliver {
@@ -105,8 +103,7 @@
double opacity = 1.0,
bool alwaysIncludeSemantics = false,
RenderSliver? sliver,
- }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
- assert(alwaysIncludeSemantics != null),
+ }) : assert(opacity >= 0.0 && opacity <= 1.0),
_opacity = opacity,
_alwaysIncludeSemantics = alwaysIncludeSemantics,
_alpha = ui.Color.getAlphaFromOpacity(opacity) {
@@ -131,7 +128,6 @@
double get opacity => _opacity;
double _opacity;
set opacity(double value) {
- assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (_opacity == value) {
return;
@@ -220,8 +216,7 @@
RenderSliver? sliver,
bool ignoring = true,
bool? ignoringSemantics,
- }) : assert(ignoring != null),
- _ignoring = ignoring,
+ }) : _ignoring = ignoring,
_ignoringSemantics = ignoringSemantics {
child = sliver;
}
@@ -233,7 +228,6 @@
bool get ignoring => _ignoring;
bool _ignoring;
set ignoring(bool value) {
- assert(value != null);
if (value == _ignoring) {
return;
}
@@ -297,8 +291,7 @@
RenderSliverOffstage({
bool offstage = true,
RenderSliver? sliver,
- }) : assert(offstage != null),
- _offstage = offstage {
+ }) : _offstage = offstage {
child = sliver;
}
@@ -313,7 +306,6 @@
bool _offstage;
set offstage(bool value) {
- assert(value != null);
if (value == _offstage) {
return;
}
@@ -401,8 +393,7 @@
required Animation<double> opacity,
bool alwaysIncludeSemantics = false,
RenderSliver? sliver,
- }) : assert(opacity != null),
- assert(alwaysIncludeSemantics != null) {
+ }) {
this.opacity = opacity;
this.alwaysIncludeSemantics = alwaysIncludeSemantics;
child = sliver;
diff --git a/framework/lib/src/rendering/rotated_box.dart b/framework/lib/src/rendering/rotated_box.dart
index 8d01084..03cb105 100644
--- a/framework/lib/src/rendering/rotated_box.dart
+++ b/framework/lib/src/rendering/rotated_box.dart
@@ -24,8 +24,7 @@
RenderRotatedBox({
required int quarterTurns,
RenderBox? child,
- }) : assert(quarterTurns != null),
- _quarterTurns = quarterTurns {
+ }) : _quarterTurns = quarterTurns {
this.child = child;
}
@@ -33,7 +32,6 @@
int get quarterTurns => _quarterTurns;
int _quarterTurns;
set quarterTurns(int value) {
- assert(value != null);
if (_quarterTurns == value) {
return;
}
diff --git a/framework/lib/src/rendering/selection.dart b/framework/lib/src/rendering/selection.dart
index c3b2c9d..d4bd7ed 100644
--- a/framework/lib/src/rendering/selection.dart
+++ b/framework/lib/src/rendering/selection.dart
@@ -687,9 +687,7 @@
required this.localPosition,
required this.lineHeight,
required this.handleType,
- }) : assert(localPosition != null),
- assert(lineHeight != null),
- assert(handleType != null);
+ });
/// The position of the selection point in the local coordinates of the
/// containing [Selectable].
diff --git a/framework/lib/src/rendering/shifted_box.dart b/framework/lib/src/rendering/shifted_box.dart
index 9c4d1c5..ca04288 100644
--- a/framework/lib/src/rendering/shifted_box.dart
+++ b/framework/lib/src/rendering/shifted_box.dart
@@ -107,8 +107,7 @@
required EdgeInsetsGeometry padding,
TextDirection? textDirection,
RenderBox? child,
- }) : assert(padding != null),
- assert(padding.isNonNegative),
+ }) : assert(padding.isNonNegative),
_textDirection = textDirection,
_padding = padding,
super(child);
@@ -135,7 +134,6 @@
EdgeInsetsGeometry get padding => _padding;
EdgeInsetsGeometry _padding;
set padding(EdgeInsetsGeometry value) {
- assert(value != null);
assert(value.isNonNegative);
if (_padding == value) {
return;
@@ -277,8 +275,7 @@
AlignmentGeometry alignment = Alignment.center,
required TextDirection? textDirection,
RenderBox? child,
- }) : assert(alignment != null),
- _alignment = alignment,
+ }) : _alignment = alignment,
_textDirection = textDirection,
super(child);
@@ -314,7 +311,6 @@
///
/// The new alignment must not be null.
set alignment(AlignmentGeometry value) {
- assert(value != null);
if (_alignment == value) {
return;
}
@@ -697,10 +693,7 @@
required BoxConstraintsTransform constraintsTransform,
super.child,
Clip clipBehavior = Clip.none,
- }) : assert(alignment != null),
- assert(clipBehavior != null),
- assert(constraintsTransform != null),
- _constraintsTransform = constraintsTransform,
+ }) : _constraintsTransform = constraintsTransform,
_clipBehavior = clipBehavior;
/// {@macro flutter.widgets.constraintsTransform}
@@ -729,7 +722,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -783,7 +775,6 @@
final RenderBox? child = this.child;
if (child != null) {
final BoxConstraints childConstraints = constraintsTransform(constraints);
- assert(childConstraints != null);
assert(childConstraints.isNormalized, '$childConstraints is not normalized');
_childConstraints = childConstraints;
child.layout(childConstraints, parentUsesSize: true);
@@ -896,14 +887,12 @@
required Size requestedSize,
super.alignment,
super.textDirection,
- }) : assert(requestedSize != null),
- _requestedSize = requestedSize;
+ }) : _requestedSize = requestedSize;
/// The size this render box should attempt to be.
Size get requestedSize => _requestedSize;
Size _requestedSize;
set requestedSize(Size value) {
- assert(value != null);
if (_requestedSize == value) {
return;
}
@@ -1212,15 +1201,13 @@
RenderCustomSingleChildLayoutBox({
RenderBox? child,
required SingleChildLayoutDelegate delegate,
- }) : assert(delegate != null),
- _delegate = delegate,
+ }) : _delegate = delegate,
super(child);
/// A delegate that controls this object's layout.
SingleChildLayoutDelegate get delegate => _delegate;
SingleChildLayoutDelegate _delegate;
set delegate(SingleChildLayoutDelegate newDelegate) {
- assert(newDelegate != null);
if (_delegate == newDelegate) {
return;
}
@@ -1333,9 +1320,7 @@
RenderBox? child,
required double baseline,
required TextBaseline baselineType,
- }) : assert(baseline != null),
- assert(baselineType != null),
- _baseline = baseline,
+ }) : _baseline = baseline,
_baselineType = baselineType,
super(child);
@@ -1344,7 +1329,6 @@
double get baseline => _baseline;
double _baseline;
set baseline(double value) {
- assert(value != null);
if (_baseline == value) {
return;
}
@@ -1356,7 +1340,6 @@
TextBaseline get baselineType => _baselineType;
TextBaseline _baselineType;
set baselineType(TextBaseline value) {
- assert(value != null);
if (_baselineType == value) {
return;
}
diff --git a/framework/lib/src/rendering/sliver.dart b/framework/lib/src/rendering/sliver.dart
index b58b019..1910cd0 100644
--- a/framework/lib/src/rendering/sliver.dart
+++ b/framework/lib/src/rendering/sliver.dart
@@ -49,8 +49,6 @@
/// [AxisDirection] and a [GrowthDirection] and wish to compute the
/// [AxisDirection] in which growth will occur.
AxisDirection applyGrowthDirectionToAxisDirection(AxisDirection axisDirection, GrowthDirection growthDirection) {
- assert(axisDirection != null);
- assert(growthDirection != null);
switch (growthDirection) {
case GrowthDirection.forward:
return axisDirection;
@@ -69,8 +67,6 @@
/// [ScrollDirection] and a [GrowthDirection] and wish to compute the
/// [ScrollDirection] in which growth will occur.
ScrollDirection applyGrowthDirectionToScrollDirection(ScrollDirection scrollDirection, GrowthDirection growthDirection) {
- assert(scrollDirection != null);
- assert(growthDirection != null);
switch (growthDirection) {
case GrowthDirection.forward:
return scrollDirection;
@@ -103,18 +99,7 @@
required this.viewportMainAxisExtent,
required this.remainingCacheExtent,
required this.cacheOrigin,
- }) : assert(axisDirection != null),
- assert(growthDirection != null),
- assert(userScrollDirection != null),
- assert(scrollOffset != null),
- assert(precedingScrollExtent != null),
- assert(overlap != null),
- assert(remainingPaintExtent != null),
- assert(crossAxisExtent != null),
- assert(crossAxisDirection != null),
- assert(viewportMainAxisExtent != null),
- assert(remainingCacheExtent != null),
- assert(cacheOrigin != null);
+ });
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
@@ -341,7 +326,6 @@
/// This can be useful in combination with [axis] to view the [axisDirection]
/// and [growthDirection] in different terms.
GrowthDirection get normalizedGrowthDirection {
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.down:
case AxisDirection.right:
@@ -417,7 +401,6 @@
errorMessage.writeln(' $message');
}
void verifyDouble(double property, String name, {bool mustBePositive = false, bool mustBeNegative = false}) {
- verify(property != null, 'The "$name" is null.');
if (property.isNaN) {
String additional = '.';
if (mustBePositive) {
@@ -432,13 +415,10 @@
verify(property <= 0.0, 'The "$name" is positive.');
}
}
- verify(axis != null, 'The "axis" is null.');
- verify(growthDirection != null, 'The "growthDirection" is null.');
verifyDouble(scrollOffset, 'scrollOffset');
verifyDouble(overlap, 'overlap');
verifyDouble(crossAxisExtent, 'crossAxisExtent');
verifyDouble(scrollOffset, 'scrollOffset', mustBePositive: true);
- verify(crossAxisDirection != null, 'The "crossAxisDirection" is null.');
verify(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection), 'The "axisDirection" and the "crossAxisDirection" are along the same axis.');
verifyDouble(viewportMainAxisExtent, 'viewportMainAxisExtent', mustBePositive: true);
verifyDouble(remainingPaintExtent, 'remainingPaintExtent', mustBePositive: true);
@@ -539,12 +519,7 @@
this.hasVisualOverflow = false,
this.scrollOffsetCorrection,
double? cacheExtent,
- }) : assert(scrollExtent != null),
- assert(paintExtent != null),
- assert(paintOrigin != null),
- assert(maxPaintExtent != null),
- assert(hasVisualOverflow != null),
- assert(scrollOffsetCorrection != 0.0),
+ }) : assert(scrollOffsetCorrection != 0.0),
layoutExtent = layoutExtent ?? paintExtent,
hitTestExtent = hitTestExtent ?? paintExtent,
cacheExtent = cacheExtent ?? layoutExtent ?? paintExtent,
@@ -717,12 +692,8 @@
]);
}
- verify(scrollExtent != null, 'The "scrollExtent" is null.');
verify(scrollExtent >= 0.0, 'The "scrollExtent" is negative.');
- verify(paintExtent != null, 'The "paintExtent" is null.');
verify(paintExtent >= 0.0, 'The "paintExtent" is negative.');
- verify(paintOrigin != null, 'The "paintOrigin" is null.');
- verify(layoutExtent != null, 'The "layoutExtent" is null.');
verify(layoutExtent >= 0.0, 'The "layoutExtent" is negative.');
verify(cacheExtent >= 0.0, 'The "cacheExtent" is negative.');
if (layoutExtent > paintExtent) {
@@ -731,7 +702,6 @@
details: _debugCompareFloats('paintExtent', paintExtent, 'layoutExtent', layoutExtent),
);
}
- verify(maxPaintExtent != null, 'The "maxPaintExtent" is null.');
// If the paintExtent is slightly more than the maxPaintExtent, but the difference is still less
// than precisionErrorTolerance, we will not throw the assert below.
if (paintExtent - maxPaintExtent > precisionErrorTolerance) {
@@ -742,10 +712,7 @@
..add(ErrorDescription("By definition, a sliver can't paint more than the maximum that it can paint!")),
);
}
- verify(hitTestExtent != null, 'The "hitTestExtent" is null.');
verify(hitTestExtent >= 0.0, 'The "hitTestExtent" is negative.');
- verify(visible != null, 'The "visible" property is null.');
- verify(hasVisualOverflow != null, 'The "hasVisualOverflow" is null.');
verify(scrollOffsetCorrection != 0.0, 'The "scrollOffsetCorrection" is zero.');
return true;
}());
@@ -847,11 +814,6 @@
required double crossAxisPosition,
required SliverHitTest hitTest,
}) {
- assert(mainAxisOffset != null);
- assert(crossAxisOffset != null);
- assert(mainAxisPosition != null);
- assert(crossAxisPosition != null);
- assert(hitTest != null);
if (paintOffset != null) {
pushOffset(-paintOffset);
}
@@ -879,8 +841,7 @@
super.target, {
required this.mainAxisPosition,
required this.crossAxisPosition,
- }) : assert(mainAxisPosition != null),
- assert(crossAxisPosition != null);
+ });
/// The distance in the [AxisDirection] from the edge of the sliver's painted
/// area (as given by the [SliverConstraints.scrollOffset]) to the hit point.
@@ -1180,7 +1141,6 @@
@override
Rect get paintBounds {
- assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.horizontal:
return Rect.fromLTWH(
@@ -1599,8 +1559,6 @@
/// Mixin for [RenderSliver] subclasses that provides some utility functions.
mixin RenderSliverHelpers implements RenderSliver {
bool _getRightWayUp(SliverConstraints constraints) {
- assert(constraints != null);
- assert(constraints.axisDirection != null);
bool rightWayUp;
switch (constraints.axisDirection) {
case AxisDirection.up:
@@ -1612,7 +1570,6 @@
rightWayUp = true;
break;
}
- assert(constraints.growthDirection != null);
switch (constraints.growthDirection) {
case GrowthDirection.forward:
break;
@@ -1620,7 +1577,6 @@
rightWayUp = !rightWayUp;
break;
}
- assert(rightWayUp != null);
return rightWayUp;
}
@@ -1642,7 +1598,6 @@
double absolutePosition = mainAxisPosition - delta;
final double absoluteCrossAxisPosition = crossAxisPosition - crossAxisDelta;
Offset paintOffset, transformedPosition;
- assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.horizontal:
if (!rightWayUp) {
@@ -1661,8 +1616,6 @@
transformedPosition = Offset(absoluteCrossAxisPosition, absolutePosition);
break;
}
- assert(paintOffset != null);
- assert(transformedPosition != null);
return result.addWithOutOfBandPosition(
paintOffset: paintOffset,
hitTest: (BoxHitTestResult result) {
@@ -1684,7 +1637,6 @@
final bool rightWayUp = _getRightWayUp(constraints);
double delta = childMainAxisPosition(child);
final double crossAxisDelta = childCrossAxisPosition(child);
- assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.horizontal:
if (!rightWayUp) {
@@ -1736,8 +1688,6 @@
@protected
void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) {
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
- assert(constraints.axisDirection != null);
- assert(constraints.growthDirection != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
childParentData.paintOffset = Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)));
@@ -1752,7 +1702,6 @@
childParentData.paintOffset = Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0);
break;
}
- assert(childParentData.paintOffset != null);
}
@override
@@ -1771,7 +1720,6 @@
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- assert(child != null);
assert(child == this.child);
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
@@ -1821,7 +1769,6 @@
childExtent = child!.size.height;
break;
}
- assert(childExtent != null);
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
diff --git a/framework/lib/src/rendering/sliver_fill.dart b/framework/lib/src/rendering/sliver_fill.dart
index 9608f70..75d63c3 100644
--- a/framework/lib/src/rendering/sliver_fill.dart
+++ b/framework/lib/src/rendering/sliver_fill.dart
@@ -32,8 +32,7 @@
RenderSliverFillViewport({
required super.childManager,
double viewportFraction = 1.0,
- }) : assert(viewportFraction != null),
- assert(viewportFraction > 0.0),
+ }) : assert(viewportFraction > 0.0),
_viewportFraction = viewportFraction;
@override
@@ -47,7 +46,6 @@
double get viewportFraction => _viewportFraction;
double _viewportFraction;
set viewportFraction(double value) {
- assert(value != null);
if (_viewportFraction == value) {
return;
}
diff --git a/framework/lib/src/rendering/sliver_fixed_extent_list.dart b/framework/lib/src/rendering/sliver_fixed_extent_list.dart
index 388c60d..efadf2c 100644
--- a/framework/lib/src/rendering/sliver_fixed_extent_list.dart
+++ b/framework/lib/src/rendering/sliver_fixed_extent_list.dart
@@ -257,7 +257,6 @@
child.layout(childConstraints);
}
trailingChildWithLayout = child;
- assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
assert(childParentData.index == index);
childParentData.layoutOffset = indexToLayoutOffset(itemExtent, childParentData.index!);
@@ -351,7 +350,6 @@
double get itemExtent => _itemExtent;
double _itemExtent;
set itemExtent(double value) {
- assert(value != null);
if (_itemExtent == value) {
return;
}
diff --git a/framework/lib/src/rendering/sliver_grid.dart b/framework/lib/src/rendering/sliver_grid.dart
index 956b050..5944019 100644
--- a/framework/lib/src/rendering/sliver_grid.dart
+++ b/framework/lib/src/rendering/sliver_grid.dart
@@ -155,12 +155,11 @@
required this.childMainAxisExtent,
required this.childCrossAxisExtent,
required this.reverseCrossAxis,
- }) : assert(crossAxisCount != null && crossAxisCount > 0),
- assert(mainAxisStride != null && mainAxisStride >= 0),
- assert(crossAxisStride != null && crossAxisStride >= 0),
- assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
- assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
- assert(reverseCrossAxis != null);
+ }) : assert(crossAxisCount > 0),
+ assert(mainAxisStride >= 0),
+ assert(crossAxisStride >= 0),
+ assert(childMainAxisExtent >= 0),
+ assert(childCrossAxisExtent >= 0);
/// The number of children in the cross axis.
final int crossAxisCount;
@@ -226,7 +225,6 @@
@override
double computeMaxScrollOffset(int childCount) {
- assert(childCount != null);
final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
return mainAxisStride * mainAxisCount - mainAxisSpacing;
@@ -317,10 +315,10 @@
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
this.mainAxisExtent,
- }) : assert(crossAxisCount != null && crossAxisCount > 0),
- assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
- assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
- assert(childAspectRatio != null && childAspectRatio > 0);
+ }) : assert(crossAxisCount > 0),
+ assert(mainAxisSpacing >= 0),
+ assert(crossAxisSpacing >= 0),
+ assert(childAspectRatio > 0);
/// The number of children in the cross axis.
final int crossAxisCount;
@@ -416,10 +414,10 @@
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
this.mainAxisExtent,
- }) : assert(maxCrossAxisExtent != null && maxCrossAxisExtent > 0),
- assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
- assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
- assert(childAspectRatio != null && childAspectRatio > 0);
+ }) : assert(maxCrossAxisExtent > 0),
+ assert(mainAxisSpacing >= 0),
+ assert(crossAxisSpacing >= 0),
+ assert(childAspectRatio > 0);
/// The maximum extent of tiles in the cross axis.
///
@@ -525,8 +523,7 @@
RenderSliverGrid({
required super.childManager,
required SliverGridDelegate gridDelegate,
- }) : assert(gridDelegate != null),
- _gridDelegate = gridDelegate;
+ }) : _gridDelegate = gridDelegate;
@override
void setupParentData(RenderObject child) {
@@ -539,7 +536,6 @@
SliverGridDelegate get gridDelegate => _gridDelegate;
SliverGridDelegate _gridDelegate;
set gridDelegate(SliverGridDelegate value) {
- assert(value != null);
if (_gridDelegate == value) {
return;
}
@@ -640,7 +636,6 @@
child.layout(childConstraints);
}
trailingChildWithLayout = child;
- assert(child != null);
final SliverGridParentData childParentData = child.parentData! as SliverGridParentData;
childParentData.layoutOffset = gridGeometry.scrollOffset;
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
diff --git a/framework/lib/src/rendering/sliver_multi_box_adaptor.dart b/framework/lib/src/rendering/sliver_multi_box_adaptor.dart
index a90af66..c14f5d5 100644
--- a/framework/lib/src/rendering/sliver_multi_box_adaptor.dart
+++ b/framework/lib/src/rendering/sliver_multi_box_adaptor.dart
@@ -113,7 +113,7 @@
/// This function always returns true.
///
/// The manager is not required to track whether it is expecting modifications
- /// to the [RenderSliverMultiBoxAdaptor]'s child list and can simply return
+ /// to the [RenderSliverMultiBoxAdaptor]'s child list and can return
/// true without making any assertions.
bool debugAssertChildListLocked() => true;
}
@@ -169,7 +169,7 @@
/// been laid out during that layout pass.
/// * Children cannot be added except during a call to [childManager], and
/// then only if there is no child corresponding to that index (or the child
-/// child corresponding to that index was first removed).
+/// corresponding to that index was first removed).
///
/// See also:
///
@@ -188,8 +188,7 @@
/// The [childManager] argument must not be null.
RenderSliverMultiBoxAdaptor({
required RenderSliverBoxChildManager childManager,
- }) : assert(childManager != null),
- _childManager = childManager {
+ }) : _childManager = childManager {
assert(() {
_debugDanglingKeepAlives = <RenderBox>[];
return true;
@@ -229,7 +228,6 @@
bool get debugChildIntegrityEnabled => _debugChildIntegrityEnabled;
bool _debugChildIntegrityEnabled = true;
set debugChildIntegrityEnabled(bool enabled) {
- assert(enabled != null);
assert(() {
_debugChildIntegrityEnabled = enabled;
return _debugVerifyChildOrder() &&
@@ -536,7 +534,6 @@
/// Returns the index of the given child, as given by the
/// [SliverMultiBoxAdaptorParentData.index] field of the child's [parentData].
int indexOf(RenderBox child) {
- assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
assert(childParentData.index != null);
return childParentData.index!;
@@ -546,7 +543,6 @@
/// child's [RenderBox.size] property. This is only valid after layout.
@protected
double paintExtentOf(RenderBox child) {
- assert(child != null);
assert(child.hasSize);
switch (constraints.axis) {
case Axis.horizontal:
@@ -576,7 +572,6 @@
@override
double? childScrollOffset(RenderObject child) {
- assert(child != null);
assert(child.parent == this);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
return childParentData.layoutOffset;
@@ -640,8 +635,6 @@
addExtent = true;
break;
}
- assert(mainAxisUnit != null);
- assert(addExtent != null);
RenderBox? child = firstChild;
while (child != null) {
final double mainAxisDelta = childMainAxisPosition(child);
diff --git a/framework/lib/src/rendering/sliver_padding.dart b/framework/lib/src/rendering/sliver_padding.dart
index 34fadde..15f173c 100644
--- a/framework/lib/src/rendering/sliver_padding.dart
+++ b/framework/lib/src/rendering/sliver_padding.dart
@@ -35,9 +35,6 @@
/// Only valid after layout has started, since before layout the render object
/// doesn't know what direction it will be laid out in.
double get beforePadding {
- assert(constraints != null);
- assert(constraints.axisDirection != null);
- assert(constraints.growthDirection != null);
assert(resolvedPadding != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
@@ -56,9 +53,6 @@
/// Only valid after layout has started, since before layout the render object
/// doesn't know what direction it will be laid out in.
double get afterPadding {
- assert(constraints != null);
- assert(constraints.axisDirection != null);
- assert(constraints.growthDirection != null);
assert(resolvedPadding != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
@@ -79,8 +73,6 @@
/// Only valid after layout has started, since before layout the render object
/// doesn't know what direction it will be laid out in.
double get mainAxisPadding {
- assert(constraints != null);
- assert(constraints.axis != null);
assert(resolvedPadding != null);
return resolvedPadding!.along(constraints.axis);
}
@@ -92,8 +84,6 @@
/// Only valid after layout has started, since before layout the render object
/// doesn't know what direction it will be laid out in.
double get crossAxisPadding {
- assert(constraints != null);
- assert(constraints.axis != null);
assert(resolvedPadding != null);
switch (constraints.axis) {
case Axis.horizontal:
@@ -201,8 +191,6 @@
);
final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
- assert(constraints.axisDirection != null);
- assert(constraints.growthDirection != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
childParentData.paintOffset = Offset(resolvedPadding!.left, calculatePaintOffset(constraints, from: resolvedPadding!.bottom + childLayoutGeometry.scrollExtent, to: resolvedPadding!.bottom + childLayoutGeometry.scrollExtent + resolvedPadding!.top));
@@ -217,7 +205,6 @@
childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: resolvedPadding!.right + childLayoutGeometry.scrollExtent, to: resolvedPadding!.right + childLayoutGeometry.scrollExtent + resolvedPadding!.left), resolvedPadding!.top);
break;
}
- assert(childParentData.paintOffset != null);
assert(beforePadding == this.beforePadding);
assert(afterPadding == this.afterPadding);
assert(mainAxisPadding == this.mainAxisPadding);
@@ -242,18 +229,13 @@
@override
double childMainAxisPosition(RenderSliver child) {
- assert(child != null);
assert(child == this.child);
return calculatePaintOffset(constraints, from: 0.0, to: beforePadding);
}
@override
double childCrossAxisPosition(RenderSliver child) {
- assert(child != null);
assert(child == this.child);
- assert(constraints != null);
- assert(constraints.axisDirection != null);
- assert(constraints.growthDirection != null);
assert(resolvedPadding != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
@@ -273,7 +255,6 @@
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- assert(child != null);
assert(child == this.child);
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
@@ -326,8 +307,7 @@
required EdgeInsetsGeometry padding,
TextDirection? textDirection,
RenderSliver? child,
- }) : assert(padding != null),
- assert(padding.isNonNegative),
+ }) : assert(padding.isNonNegative),
_padding = padding,
_textDirection = textDirection {
this.child = child;
@@ -357,7 +337,6 @@
EdgeInsetsGeometry get padding => _padding;
EdgeInsetsGeometry _padding;
set padding(EdgeInsetsGeometry value) {
- assert(value != null);
assert(padding.isNonNegative);
if (_padding == value) {
return;
diff --git a/framework/lib/src/rendering/sliver_persistent_header.dart b/framework/lib/src/rendering/sliver_persistent_header.dart
index 68257cf..904429f 100644
--- a/framework/lib/src/rendering/sliver_persistent_header.dart
+++ b/framework/lib/src/rendering/sliver_persistent_header.dart
@@ -37,7 +37,7 @@
OverScrollHeaderStretchConfiguration({
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
- }) : assert(stretchTriggerOffset != null);
+ });
/// The offset of overscroll required to trigger the [onStretchTrigger].
final double stretchTriggerOffset;
@@ -152,7 +152,6 @@
return 0.0;
}
assert(child!.hasSize);
- assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.vertical:
return child!.size.height;
@@ -217,7 +216,6 @@
/// The `overlapsContent` argument is passed to [updateChild].
@protected
void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) {
- assert(maxExtent != null);
final double shrinkOffset = math.min(scrollOffset, maxExtent);
if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
@@ -228,7 +226,6 @@
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
}
- assert(minExtent != null);
assert(() {
if (minExtent <= maxExtent) {
return true;
@@ -294,7 +291,6 @@
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- assert(child != null);
assert(child == this.child);
applyPaintTransformForBoxChild(child as RenderBox, transform);
}
@@ -302,7 +298,6 @@
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
- assert(constraints.axisDirection != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
offset += Offset(0.0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent);
@@ -496,8 +491,7 @@
FloatingHeaderSnapConfiguration({
this.curve = Curves.ease,
this.duration = const Duration(milliseconds: 300),
- }) : assert(curve != null),
- assert(duration != null);
+ });
/// The snap animation curve.
final Curve curve;
@@ -607,9 +601,6 @@
}
void _updateAnimation(Duration duration, double endValue, Curve curve) {
- assert(duration != null);
- assert(endValue != null);
- assert(curve != null);
assert(
vsync != null,
'vsync must not be null if the floating header changes size animatedly.',
diff --git a/framework/lib/src/rendering/stack.dart b/framework/lib/src/rendering/stack.dart
index 6027b65..d490d6b 100644
--- a/framework/lib/src/rendering/stack.dart
+++ b/framework/lib/src/rendering/stack.dart
@@ -25,8 +25,7 @@
/// Creates a RelativeRect with the given values.
///
/// The arguments must not be null.
- const RelativeRect.fromLTRB(this.left, this.top, this.right, this.bottom)
- : assert(left != null && top != null && right != null && bottom != null);
+ const RelativeRect.fromLTRB(this.left, this.top, this.right, this.bottom);
/// Creates a RelativeRect from a Rect and a Size. The Rect (first argument)
/// and the RelativeRect (the output) are in the coordinate space of the
@@ -166,7 +165,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static RelativeRect? lerp(RelativeRect? a, RelativeRect? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -354,10 +352,7 @@
TextDirection? textDirection,
StackFit fit = StackFit.loose,
Clip clipBehavior = Clip.hardEdge,
- }) : assert(alignment != null),
- assert(fit != null),
- assert(clipBehavior != null),
- _alignment = alignment,
+ }) : _alignment = alignment,
_textDirection = textDirection,
_fit = fit,
_clipBehavior = clipBehavior {
@@ -405,7 +400,6 @@
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
set alignment(AlignmentGeometry value) {
- assert(value != null);
if (_alignment == value) {
return;
}
@@ -435,7 +429,6 @@
StackFit get fit => _fit;
StackFit _fit;
set fit(StackFit value) {
- assert(value != null);
if (_fit != value) {
_fit = value;
markNeedsLayout();
@@ -448,7 +441,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -571,7 +563,6 @@
double height = constraints.minHeight;
final BoxConstraints nonPositionedConstraints;
- assert(fit != null);
switch (fit) {
case StackFit.loose:
nonPositionedConstraints = constraints.loosen();
@@ -583,7 +574,6 @@
nonPositionedConstraints = constraints;
break;
}
- assert(nonPositionedConstraints != null);
RenderBox? child = firstChild;
while (child != null) {
@@ -755,7 +745,6 @@
if (firstChild == null || index == null) {
return false;
}
- assert(position != null);
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData! as StackParentData;
return result.addWithPaintOffset(
diff --git a/framework/lib/src/rendering/table.dart b/framework/lib/src/rendering/table.dart
index 311477c..21e6bd7 100644
--- a/framework/lib/src/rendering/table.dart
+++ b/framework/lib/src/rendering/table.dart
@@ -134,7 +134,7 @@
/// Creates a column width based on a fixed number of logical pixels.
///
/// The [value] argument must not be null.
- const FixedColumnWidth(this.value) : assert(value != null);
+ const FixedColumnWidth(this.value);
/// The width the column should occupy in logical pixels.
final double value;
@@ -161,7 +161,7 @@
/// maxWidth.
///
/// The [value] argument must not be null.
- const FractionColumnWidth(this.value) : assert(value != null);
+ const FractionColumnWidth(this.value);
/// The fraction of the table's constraints' maxWidth that this column should
/// occupy.
@@ -199,7 +199,7 @@
/// the other columns have been laid out.
///
/// The [value] argument must not be null.
- const FlexColumnWidth([this.value = 1.0]) : assert(value != null);
+ const FlexColumnWidth([this.value = 1.0]);
/// The fraction of the remaining space once all the other columns have
/// been laid out that this column should occupy.
@@ -388,9 +388,6 @@
}) : assert(columns == null || columns >= 0),
assert(rows == null || rows >= 0),
assert(rows == null || children == null),
- assert(defaultColumnWidth != null),
- assert(textDirection != null),
- assert(configuration != null),
_textDirection = textDirection,
_columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0),
_rows = rows ?? 0,
@@ -419,7 +416,6 @@
int get columns => _columns;
int _columns;
set columns(int value) {
- assert(value != null);
assert(value >= 0);
if (value == columns) {
return;
@@ -454,7 +450,6 @@
int get rows => _rows;
int _rows;
set rows(int value) {
- assert(value != null);
assert(value >= 0);
if (value == rows) {
return;
@@ -513,7 +508,6 @@
TableColumnWidth get defaultColumnWidth => _defaultColumnWidth;
TableColumnWidth _defaultColumnWidth;
set defaultColumnWidth(TableColumnWidth value) {
- assert(value != null);
if (defaultColumnWidth == value) {
return;
}
@@ -525,7 +519,6 @@
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
- assert(value != null);
if (_textDirection == value) {
return;
}
@@ -573,7 +566,6 @@
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
- assert(value != null);
if (value == _configuration) {
return;
}
@@ -585,7 +577,6 @@
TableCellVerticalAlignment get defaultVerticalAlignment => _defaultVerticalAlignment;
TableCellVerticalAlignment _defaultVerticalAlignment;
set defaultVerticalAlignment(TableCellVerticalAlignment value) {
- assert(value != null);
if (_defaultVerticalAlignment == value) {
return;
}
@@ -617,7 +608,7 @@
/// replacing the existing children.
///
/// If the new cells contain any existing children of the table, those
- /// children are simply moved to their new location in the table rather than
+ /// children are moved to their new location in the table rather than
/// removed from the table and re-added.
void setFlatChildren(int columns, List<RenderBox?> cells) {
if (cells == _children && columns == _columns) {
@@ -626,7 +617,7 @@
assert(columns >= 0);
// consider the case of a newly empty table
if (columns == 0 || cells.isEmpty) {
- assert(cells == null || cells.isEmpty);
+ assert(cells.isEmpty);
_columns = columns;
if (_children.isEmpty) {
assert(_rows == 0);
@@ -642,7 +633,6 @@
markNeedsLayout();
return;
}
- assert(cells != null);
assert(cells.length % columns == 0);
// fill a set with the cells that are moving (it's important not
// to dropChild a child that's remaining with us, because that
@@ -722,8 +712,6 @@
/// does not modify the table. Otherwise, the given child must not already
/// have a parent.
void setChild(int x, int y, RenderBox? value) {
- assert(x != null);
- assert(y != null);
assert(x >= 0 && x < columns && y >= 0 && y < rows);
assert(_children.length == rows * columns);
final int xy = x + y * columns;
@@ -864,7 +852,6 @@
}
List<double> _computeColumnWidths(BoxConstraints constraints) {
- assert(constraints != null);
assert(_children.length == rows * columns);
// We apply the constraints to the column widths in the order of
// least important to most important:
@@ -1061,7 +1048,6 @@
final RenderBox? child = _children[xy];
if (child != null) {
final TableCellParentData childParentData = child.parentData! as TableCellParentData;
- assert(childParentData != null);
switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
case TableCellVerticalAlignment.baseline:
assert(debugCannotComputeDryLayout(
@@ -1133,7 +1119,6 @@
final RenderBox? child = _children[xy];
if (child != null) {
final TableCellParentData childParentData = child.parentData! as TableCellParentData;
- assert(childParentData != null);
childParentData.x = x;
childParentData.y = y;
switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
diff --git a/framework/lib/src/rendering/table_border.dart b/framework/lib/src/rendering/table_border.dart
index b4bcfb8..c4fecb7 100644
--- a/framework/lib/src/rendering/table_border.dart
+++ b/framework/lib/src/rendering/table_border.dart
@@ -89,12 +89,6 @@
/// Whether all the sides of the border (outside and inside) are identical.
/// Uniform borders are typically more efficient to paint.
bool get isUniform {
- assert(top != null);
- assert(right != null);
- assert(bottom != null);
- assert(left != null);
- assert(horizontalInside != null);
- assert(verticalInside != null);
final Color topColor = top.color;
if (right.color != topColor ||
@@ -159,7 +153,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static TableBorder? lerp(TableBorder? a, TableBorder? b, double t) {
- assert(t != null);
if (a == null && b == null) {
return null;
}
@@ -212,19 +205,9 @@
required Iterable<double> columns,
}) {
// properties can't be null
- assert(top != null);
- assert(right != null);
- assert(bottom != null);
- assert(left != null);
- assert(horizontalInside != null);
- assert(verticalInside != null);
// arguments can't be null
- assert(canvas != null);
- assert(rect != null);
- assert(rows != null);
assert(rows.isEmpty || (rows.first >= 0.0 && rows.last <= rect.height));
- assert(columns != null);
assert(columns.isEmpty || (columns.first >= 0.0 && columns.last <= rect.width));
if (columns.isNotEmpty || rows.isNotEmpty) {
diff --git a/framework/lib/src/rendering/texture.dart b/framework/lib/src/rendering/texture.dart
index c28a8b2..0485ab1 100644
--- a/framework/lib/src/rendering/texture.dart
+++ b/framework/lib/src/rendering/texture.dart
@@ -40,8 +40,7 @@
required int textureId,
bool freeze = false,
FilterQuality filterQuality = FilterQuality.low,
- }) : assert(textureId != null),
- _textureId = textureId,
+ }) : _textureId = textureId,
_freeze = freeze,
_filterQuality = filterQuality;
@@ -49,7 +48,6 @@
int get textureId => _textureId;
int _textureId;
set textureId(int value) {
- assert(value != null);
if (value != _textureId) {
_textureId = value;
markNeedsPaint();
@@ -60,7 +58,6 @@
bool get freeze => _freeze;
bool _freeze;
set freeze(bool value) {
- assert(value != null);
if (value != _freeze) {
_freeze = value;
markNeedsPaint();
@@ -71,7 +68,6 @@
FilterQuality get filterQuality => _filterQuality;
FilterQuality _filterQuality;
set filterQuality(FilterQuality value) {
- assert(value != null);
if (value != _filterQuality) {
_filterQuality = value;
markNeedsPaint();
diff --git a/framework/lib/src/rendering/view.dart b/framework/lib/src/rendering/view.dart
index fbe3097..31dcf88 100644
--- a/framework/lib/src/rendering/view.dart
+++ b/framework/lib/src/rendering/view.dart
@@ -69,8 +69,7 @@
RenderBox? child,
required ViewConfiguration configuration,
required ui.FlutterView window,
- }) : assert(configuration != null),
- _configuration = configuration,
+ }) : _configuration = configuration,
_window = window {
this.child = child;
}
@@ -88,7 +87,6 @@
///
/// Always call [prepareInitialFrame] before changing the configuration.
set configuration(ViewConfiguration value) {
- assert(value != null);
if (configuration == value) {
return;
}
@@ -201,7 +199,6 @@
/// * [Layer.findAllAnnotations], which is used by this method to find all
/// [AnnotatedRegionLayer]s annotated for mouse tracking.
HitTestResult hitTestMouseTrackers(Offset position) {
- assert(position != null);
final BoxHitTestResult result = BoxHitTestResult();
hitTest(result, position: position);
return result;
diff --git a/framework/lib/src/rendering/viewport.dart b/framework/lib/src/rendering/viewport.dart
index 042ffdc..f714a8a 100644
--- a/framework/lib/src/rendering/viewport.dart
+++ b/framework/lib/src/rendering/viewport.dart
@@ -136,8 +136,7 @@
const RevealedOffset({
required this.offset,
required this.rect,
- }) : assert(offset != null),
- assert(rect != null);
+ });
/// Offset for the viewport to reveal a specific element in the viewport.
///
@@ -213,13 +212,8 @@
double? cacheExtent,
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
Clip clipBehavior = Clip.hardEdge,
- }) : assert(axisDirection != null),
- assert(crossAxisDirection != null),
- assert(offset != null),
- assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)),
- assert(cacheExtentStyle != null),
+ }) : assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)),
assert(cacheExtent != null || cacheExtentStyle == CacheExtentStyle.pixel),
- assert(clipBehavior != null),
_axisDirection = axisDirection,
_crossAxisDirection = crossAxisDirection,
_offset = offset,
@@ -264,7 +258,6 @@
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
- assert(value != null);
if (value == _axisDirection) {
return;
}
@@ -281,7 +274,6 @@
AxisDirection get crossAxisDirection => _crossAxisDirection;
AxisDirection _crossAxisDirection;
set crossAxisDirection(AxisDirection value) {
- assert(value != null);
if (value == _crossAxisDirection) {
return;
}
@@ -304,7 +296,6 @@
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
- assert(value != null);
if (value == _offset) {
return;
}
@@ -357,7 +348,6 @@
double _cacheExtent;
set cacheExtent(double? value) {
value ??= RenderAbstractViewport.defaultCacheExtent;
- assert(value != null);
if (value == _cacheExtent) {
return;
}
@@ -390,7 +380,6 @@
CacheExtentStyle get cacheExtentStyle => _cacheExtentStyle;
CacheExtentStyle _cacheExtentStyle;
set cacheExtentStyle(CacheExtentStyle value) {
- assert(value != null);
if (value == _cacheExtentStyle) {
return;
}
@@ -404,7 +393,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -530,7 +518,6 @@
final double initialLayoutOffset = layoutOffset;
final ScrollDirection adjustedUserScrollDirection =
applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
- assert(adjustedUserScrollDirection != null);
double maxPaintOffset = layoutOffset + overlap;
double precedingScrollExtent = 0.0;
@@ -653,7 +640,6 @@
@override
Rect describeSemanticsClip(RenderSliver? child) {
- assert(axis != null);
if (_calculatedCacheExtent == null) {
return semanticBounds;
@@ -733,7 +719,6 @@
size = Size(child.geometry!.layoutExtent, child.constraints.crossAxisExtent);
break;
}
- assert(size != null);
canvas.drawRect(((offset + paintOffsetOf(child)) & size).deflate(0.5), paint);
child = childAfter(child);
}
@@ -754,8 +739,6 @@
crossAxisPosition = position.dy;
break;
}
- assert(mainAxisPosition != null);
- assert(crossAxisPosition != null);
final SliverHitTestResult sliverResult = SliverHitTestResult.wrap(result);
for (final RenderSliver child in childrenInHitTestOrder) {
if (!child.geometry!.visible) {
@@ -808,7 +791,6 @@
bool onlySlivers = target is RenderSliver; // ... between viewport and `target` (`target` included).
while (child.parent != this) {
final RenderObject parent = child.parent! as RenderObject;
- assert(parent != null, '$target must be a descendant of $this');
if (child is RenderBox) {
pivot = child;
}
@@ -878,10 +860,6 @@
return RevealedOffset(offset: offset.pixels, rect: rect!);
}
- assert(pivotExtent != null);
- assert(rect != null);
- assert(rectLocal != null);
- assert(growthDirection != null);
assert(child.parent == this);
assert(child is RenderSliver);
final RenderSliver sliver = child as RenderSliver;
@@ -994,9 +972,6 @@
@protected
Offset computeAbsolutePaintOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
assert(hasSize); // this is only usable once we have a size
- assert(axisDirection != null);
- assert(growthDirection != null);
- assert(child != null);
assert(child.geometry != null);
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up:
@@ -1213,8 +1188,6 @@
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
- assert(viewport != null);
- assert(offset != null);
if (descendant == null) {
return rect;
}
@@ -1265,7 +1238,6 @@
return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds);
}
- assert(targetOffset != null);
offset.moveTo(targetOffset.offset, duration: duration, curve: curve);
return targetOffset.rect;
@@ -1321,10 +1293,8 @@
super.cacheExtent,
super.cacheExtentStyle,
super.clipBehavior,
- }) : assert(anchor != null),
- assert(anchor >= 0.0 && anchor <= 1.0),
+ }) : assert(anchor >= 0.0 && anchor <= 1.0),
assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
- assert(clipBehavior != null),
_anchor = anchor,
_center = center {
addAll(children);
@@ -1385,7 +1355,6 @@
double get anchor => _anchor;
double _anchor;
set anchor(double value) {
- assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (value == _anchor) {
return;
@@ -1537,7 +1506,6 @@
double correction;
int count = 0;
do {
- assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
offset.correctBy(correction);
@@ -1680,7 +1648,6 @@
double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
assert(child.parent == this);
final GrowthDirection growthDirection = child.constraints.growthDirection;
- assert(growthDirection != null);
switch (growthDirection) {
case GrowthDirection.forward:
double scrollOffsetToChild = 0.0;
@@ -1705,7 +1672,6 @@
double maxScrollObstructionExtentBefore(RenderSliver child) {
assert(child.parent == this);
final GrowthDirection growthDirection = child.constraints.growthDirection;
- assert(growthDirection != null);
switch (growthDirection) {
case GrowthDirection.forward:
double pinnedExtent = 0.0;
@@ -1729,15 +1695,12 @@
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
// Hit test logic relies on this always providing an invertible matrix.
- assert(child != null);
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
}
@override
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
- assert(child != null);
- assert(child.constraints != null);
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
case AxisDirection.down:
@@ -1935,7 +1898,6 @@
double correction;
double effectiveExtent;
do {
- assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
if (correction != 0.0) {
offset.correctBy(correction);
@@ -2061,15 +2023,12 @@
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
// Hit test logic relies on this always providing an invertible matrix.
- assert(child != null);
final Offset offset = paintOffsetOf(child as RenderSliver);
transform.translate(offset.dx, offset.dy);
}
@override
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
- assert(child != null);
- assert(child.constraints != null);
assert(hasSize);
final SliverLogicalParentData childParentData = child.parentData! as SliverLogicalParentData;
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
diff --git a/framework/lib/src/rendering/viewport_offset.dart b/framework/lib/src/rendering/viewport_offset.dart
index 89f991c..59b178c 100644
--- a/framework/lib/src/rendering/viewport_offset.dart
+++ b/framework/lib/src/rendering/viewport_offset.dart
@@ -196,7 +196,6 @@
Curve? curve,
bool? clamp,
}) {
- assert(to != null);
if (duration == null || duration == Duration.zero) {
jumpTo(to);
return Future<void>.value();
diff --git a/framework/lib/src/rendering/wrap.dart b/framework/lib/src/rendering/wrap.dart
index 6857f87..4d8bd50 100644
--- a/framework/lib/src/rendering/wrap.dart
+++ b/framework/lib/src/rendering/wrap.dart
@@ -121,14 +121,7 @@
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
Clip clipBehavior = Clip.none,
- }) : assert(direction != null),
- assert(alignment != null),
- assert(spacing != null),
- assert(runAlignment != null),
- assert(runSpacing != null),
- assert(crossAxisAlignment != null),
- assert(clipBehavior != null),
- _direction = direction,
+ }) : _direction = direction,
_alignment = alignment,
_spacing = spacing,
_runAlignment = runAlignment,
@@ -149,7 +142,6 @@
Axis get direction => _direction;
Axis _direction;
set direction (Axis value) {
- assert(value != null);
if (_direction == value) {
return;
}
@@ -173,7 +165,6 @@
WrapAlignment get alignment => _alignment;
WrapAlignment _alignment;
set alignment (WrapAlignment value) {
- assert(value != null);
if (_alignment == value) {
return;
}
@@ -195,7 +186,6 @@
double get spacing => _spacing;
double _spacing;
set spacing (double value) {
- assert(value != null);
if (_spacing == value) {
return;
}
@@ -220,7 +210,6 @@
WrapAlignment get runAlignment => _runAlignment;
WrapAlignment _runAlignment;
set runAlignment (WrapAlignment value) {
- assert(value != null);
if (_runAlignment == value) {
return;
}
@@ -241,7 +230,6 @@
double get runSpacing => _runSpacing;
double _runSpacing;
set runSpacing (double value) {
- assert(value != null);
if (_runSpacing == value) {
return;
}
@@ -267,7 +255,6 @@
WrapCrossAlignment get crossAxisAlignment => _crossAxisAlignment;
WrapCrossAlignment _crossAxisAlignment;
set crossAxisAlignment (WrapCrossAlignment value) {
- assert(value != null);
if (_crossAxisAlignment == value) {
return;
}
@@ -344,7 +331,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -353,10 +339,6 @@
}
bool get _debugHasNecessaryDirections {
- assert(direction != null);
- assert(alignment != null);
- assert(runAlignment != null);
- assert(crossAxisAlignment != null);
if (firstChild != null && lastChild != firstChild) {
// i.e. there's more than one child
switch (direction) {
@@ -364,7 +346,6 @@
assert(textDirection != null, 'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.');
break;
case Axis.vertical:
- assert(verticalDirection != null, 'Vertical $runtimeType with multiple children has a null verticalDirection, so the layout order is undefined.');
break;
}
}
@@ -374,14 +355,12 @@
assert(textDirection != null, 'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
- assert(verticalDirection != null, 'Vertical $runtimeType with alignment $alignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
}
}
if (runAlignment == WrapAlignment.start || runAlignment == WrapAlignment.end) {
switch (direction) {
case Axis.horizontal:
- assert(verticalDirection != null, 'Horizontal $runtimeType with runAlignment $runAlignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
assert(textDirection != null, 'Vertical $runtimeType with runAlignment $runAlignment has a null textDirection, so the alignment cannot be resolved.');
@@ -391,7 +370,6 @@
if (crossAxisAlignment == WrapCrossAlignment.start || crossAxisAlignment == WrapCrossAlignment.end) {
switch (direction) {
case Axis.horizontal:
- assert(verticalDirection != null, 'Horizontal $runtimeType with crossAxisAlignment $crossAxisAlignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
assert(textDirection != null, 'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.');
@@ -610,8 +588,6 @@
}
break;
}
- assert(childConstraints != null);
- assert(mainAxisLimit != null);
final double spacing = this.spacing;
final double runSpacing = this.runSpacing;
final List<_RunMetrics> runMetrics = <_RunMetrics>[];
diff --git a/framework/lib/src/scheduler/binding.dart b/framework/lib/src/scheduler/binding.dart
index f0392cb..8af3176 100644
--- a/framework/lib/src/scheduler/binding.dart
+++ b/framework/lib/src/scheduler/binding.dart
@@ -385,7 +385,6 @@
@protected
@mustCallSuper
void handleAppLifecycleStateChanged(AppLifecycleState state) {
- assert(state != null);
_lifecycleState = state;
switch (state) {
case AppLifecycleState.resumed:
@@ -1026,7 +1025,6 @@
/// presentation time, and can be used to ensure that animations running in
/// different processes are synchronized.
Duration get currentSystemFrameTimeStamp {
- assert(_lastRawTimeStamp != null);
return _lastRawTimeStamp;
}
@@ -1275,11 +1273,10 @@
// Calls the given [callback] with [timestamp] as argument.
//
// Wraps the callback in a try/catch and forwards any error to
- // [debugSchedulerExceptionHandler], if set. If not set, then simply prints
+ // [debugSchedulerExceptionHandler], if set. If not set, prints
// the error.
@pragma('vm:notify-debugger-on-exception')
void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace? callbackStack ]) {
- assert(callback != null);
assert(_FrameCallbackEntry.debugCurrentCallbackStack == null);
assert(() {
_FrameCallbackEntry.debugCurrentCallbackStack = callbackStack;
diff --git a/framework/lib/src/semantics/semantics.dart b/framework/lib/src/semantics/semantics.dart
index c36f471..32fb160 100644
--- a/framework/lib/src/semantics/semantics.dart
+++ b/framework/lib/src/semantics/semantics.dart
@@ -6,6 +6,7 @@
import 'package:engine/ui.dart' as ui;
import 'package:engine/ui.dart' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
+import 'package:collection/collection.dart';
import 'package:flute/foundation.dart';
import 'package:flute/painting.dart' show MatrixUtils, TransformProperty;
import 'package:flute/services.dart';
@@ -53,6 +54,20 @@
/// Used by [SemanticsOwner.onSemanticsUpdate].
typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
+/// Signature for the [SemanticsConfiguration.childConfigurationsDelegate].
+///
+/// The input list contains all [SemanticsConfiguration]s that rendering
+/// children want to merge upward. One can tag a render child with a
+/// [SemanticsTag] and look up its [SemanticsConfiguration]s through
+/// [SemanticsConfiguration.tagsChildrenWith].
+///
+/// The return value is the arrangement of these configs, including which
+/// configs continue to merge upward and which configs form sibling merge group.
+///
+/// Use [ChildSemanticsConfigurationsResultBuilder] to generate the return
+/// value.
+typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>);
+
/// A tag for a [SemanticsNode].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
@@ -85,6 +100,89 @@
String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)';
}
+/// The result that contains the arrangement for the child
+/// [SemanticsConfiguration]s.
+///
+/// When the [PipelineOwner] builds the semantics tree, it uses the returned
+/// [ChildSemanticsConfigurationsResult] from
+/// [SemanticsConfiguration.childConfigurationsDelegate] to decide how semantics nodes
+/// should form.
+///
+/// Use [ChildSemanticsConfigurationsResultBuilder] to build the result.
+class ChildSemanticsConfigurationsResult {
+ ChildSemanticsConfigurationsResult._(this.mergeUp, this.siblingMergeGroups);
+
+ /// Returns the [SemanticsConfiguration]s that are supposed to be merged into
+ /// the parent semantics node.
+ ///
+ /// [SemanticsConfiguration]s that are either semantics boundaries or are
+ /// conflicting with other [SemanticsConfiguration]s will form explicit
+ /// semantics nodes. All others will be merged into the parent.
+ final List<SemanticsConfiguration> mergeUp;
+
+ /// The groups of child semantics configurations that want to merge together
+ /// and form a sibling [SemanticsNode].
+ ///
+ /// All the [SemanticsConfiguration]s in a given group that are either
+ /// semantics boundaries or are conflicting with other
+ /// [SemanticsConfiguration]s of the same group will be excluded from the
+ /// sibling merge group and form independent semantics nodes as usual.
+ ///
+ /// The result [SemanticsNode]s from the merges are attached as the sibling
+ /// nodes of the immediate parent semantics node. For example, a `RenderObjectA`
+ /// has a rendering child, `RenderObjectB`. If both of them form their own
+ /// semantics nodes, `SemanticsNodeA` and `SemanticsNodeB`, any semantics node
+ /// created from sibling merge groups of `RenderObjectB` will be attach to
+ /// `SemanticsNodeA` as a sibling of `SemanticsNodeB`.
+ final List<List<SemanticsConfiguration>> siblingMergeGroups;
+}
+
+/// The builder to build a [ChildSemanticsConfigurationsResult] based on its
+/// annotations.
+///
+/// To use this builder, one can use [markAsMergeUp] and
+/// [markAsSiblingMergeGroup] to annotate the arrangement of
+/// [SemanticsConfiguration]s. Once all the configs are annotated, use [build]
+/// to generate the [ChildSemanticsConfigurationsResult].
+class ChildSemanticsConfigurationsResultBuilder {
+ /// Creates a [ChildSemanticsConfigurationsResultBuilder].
+ ChildSemanticsConfigurationsResultBuilder();
+
+ final List<SemanticsConfiguration> _mergeUp = <SemanticsConfiguration>[];
+ final List<List<SemanticsConfiguration>> _siblingMergeGroups = <List<SemanticsConfiguration>>[];
+
+ /// Marks the [SemanticsConfiguration] to be merged into the parent semantics
+ /// node.
+ ///
+ /// The [SemanticsConfiguration] will be added to the
+ /// [ChildSemanticsConfigurationsResult.mergeUp] that this builder builds.
+ void markAsMergeUp(SemanticsConfiguration config) => _mergeUp.add(config);
+
+ /// Marks a group of [SemanticsConfiguration]s to merge together
+ /// and form a sibling [SemanticsNode].
+ ///
+ /// The group of [SemanticsConfiguration]s will be added to the
+ /// [ChildSemanticsConfigurationsResult.siblingMergeGroups] that this builder builds.
+ void markAsSiblingMergeGroup(List<SemanticsConfiguration> configs) => _siblingMergeGroups.add(configs);
+
+ /// Builds a [ChildSemanticsConfigurationsResult] contains the arrangement.
+ ChildSemanticsConfigurationsResult build() {
+ assert((){
+ final Set<SemanticsConfiguration> seenConfigs = <SemanticsConfiguration>{};
+ for (final SemanticsConfiguration config in <SemanticsConfiguration>[..._mergeUp, ..._siblingMergeGroups.flattened]) {
+ assert(
+ seenConfigs.add(config),
+ 'Duplicated SemanticsConfigurations. This can happen if the same '
+ 'SemanticsConfiguration was marked twice in markAsMergeUp and/or '
+ 'markAsSiblingMergeGroup'
+ );
+ }
+ return true;
+ }());
+ return ChildSemanticsConfigurationsResult._(_mergeUp, _siblingMergeGroups);
+ }
+}
+
/// An identifier of a custom semantics action.
///
/// Custom semantics actions can be provided to make complex user
@@ -113,8 +211,7 @@
///
/// The [label] must not be null or the empty string.
const CustomSemanticsAction({required String this.label})
- : assert(label != null),
- assert(label != ''),
+ : assert(label != ''),
hint = null,
action = null;
@@ -123,9 +220,7 @@
///
/// The [hint] must not be null or the empty string.
const CustomSemanticsAction.overridingAction({required String this.hint, required SemanticsAction this.action})
- : assert(hint != null),
- assert(hint != ''),
- assert(action != null),
+ : assert(hint != ''),
label = null;
/// The user readable name of this custom semantics action.
@@ -273,8 +368,7 @@
super.defaultValue,
super.level,
super.description,
- }) : assert(showName != null),
- assert(level != null);
+ });
/// Whether to show the property when the [value] is an [AttributedString]
/// whose [AttributedString.string] is the empty string.
@@ -343,20 +437,12 @@
this.tags,
this.transform,
this.customSemanticsActionIds,
- }) : assert(flags != null),
- assert(actions != null),
- assert(attributedLabel != null),
- assert(attributedValue != null),
- assert(attributedDecreasedValue != null),
- assert(attributedIncreasedValue != null),
- assert(attributedHint != null),
- assert(tooltip == '' || textDirection != null, 'A SemanticsData object with tooltip "$tooltip" had a null textDirection.'),
+ }) : assert(tooltip == '' || textDirection != null, 'A SemanticsData object with tooltip "$tooltip" had a null textDirection.'),
assert(attributedLabel.string == '' || textDirection != null, 'A SemanticsData object with label "${attributedLabel.string}" had a null textDirection.'),
assert(attributedValue.string == '' || textDirection != null, 'A SemanticsData object with value "${attributedValue.string}" had a null textDirection.'),
assert(attributedDecreasedValue.string == '' || textDirection != null, 'A SemanticsData object with decreasedValue "${attributedDecreasedValue.string}" had a null textDirection.'),
assert(attributedIncreasedValue.string == '' || textDirection != null, 'A SemanticsData object with increasedValue "${attributedIncreasedValue.string}" had a null textDirection.'),
- assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.'),
- assert(rect != null);
+ assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.');
/// A bit field of [SemanticsFlag]s that apply to this node.
final int flags;
@@ -1623,7 +1709,6 @@
Rect get rect => _rect;
Rect _rect = Rect.zero;
set rect(Rect value) {
- assert(value != null);
assert(value.isFinite, '$this (with $owner) tried to set a non-finite rect.');
if (_rect != value) {
_rect = value;
@@ -1708,7 +1793,6 @@
bool get isMergedIntoParent => _isMergedIntoParent;
bool _isMergedIntoParent = false;
set isMergedIntoParent(bool value) {
- assert(value != null);
if (_isMergedIntoParent == value) {
return;
}
@@ -1831,7 +1915,6 @@
}
}
if (!sawChange && _children != null) {
- assert(newChildren != null);
assert(newChildren.length == _children!.length);
// Did the order change?
for (int i = 0; i < _children!.length; i++) {
@@ -2406,13 +2489,13 @@
platformViewId ??= node._platformViewId;
maxValueLength ??= node._maxValueLength;
currentValueLength ??= node._currentValueLength;
- if (attributedValue == null || attributedValue.string == '') {
+ if (attributedValue.string == '') {
attributedValue = node._attributedValue;
}
- if (attributedIncreasedValue == null || attributedIncreasedValue.string == '') {
+ if (attributedIncreasedValue.string == '') {
attributedIncreasedValue = node._attributedIncreasedValue;
}
- if (attributedDecreasedValue == null || attributedDecreasedValue.string == '') {
+ if (attributedDecreasedValue.string == '') {
attributedDecreasedValue = node._attributedDecreasedValue;
}
if (tooltip == '') {
@@ -2710,7 +2793,6 @@
DiagnosticLevel minLevel = DiagnosticLevel.debug,
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
}) {
- assert(childOrder != null);
return toDiagnosticsNode(childOrder: childOrder).toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
}
@@ -2737,7 +2819,6 @@
/// Returns the list of direct children of this node in the specified order.
List<SemanticsNode> debugListChildrenInOrder(DebugSemanticsDumpOrder childOrder) {
- assert(childOrder != null);
if (_children == null) {
return const <SemanticsNode>[];
}
@@ -2764,10 +2845,7 @@
required this.isLeadingEdge,
required this.offset,
required this.node,
- }) : assert(isLeadingEdge != null),
- assert(offset != null),
- assert(offset.isFinite),
- assert(node != null);
+ }) : assert(offset.isFinite);
/// True if the edge comes before the seconds edge along the traversal
/// direction, and false otherwise.
@@ -2801,7 +2879,7 @@
_SemanticsSortGroup({
required this.startOffset,
required this.textDirection,
- }) : assert(startOffset != null);
+ });
/// The offset from the start edge of the parent [SemanticsNode] in the
/// direction of the traversal.
@@ -3024,9 +3102,7 @@
required this.node,
this.sortKey,
required this.position,
- })
- : assert(node != null),
- assert(position != null);
+ });
/// The node whose position this sort node determines.
final SemanticsNode node;
@@ -3165,7 +3241,6 @@
/// If the given `action` requires arguments they need to be passed in via
/// the `args` parameter.
void performAction(int id, SemanticsAction action, [ Object? args ]) {
- assert(action != null);
final SemanticsActionHandler? handler = _getSemanticsActionHandlerForId(id, action);
if (handler != null) {
handler(args);
@@ -3219,7 +3294,6 @@
/// If the given `action` requires arguments they need to be passed in via
/// the `args` parameter.
void performActionAt(Offset position, SemanticsAction action, [ Object? args ]) {
- assert(action != null);
final SemanticsNode? node = rootSemanticsNode;
if (node == null) {
return;
@@ -3321,7 +3395,6 @@
/// The provided `handler` is called to respond to the user triggered
/// `action`.
void _addAction(SemanticsAction action, SemanticsActionHandler handler) {
- assert(handler != null);
_actions[action] = handler;
_actionsAsBits |= action.index;
_hasBeenAnnotated = true;
@@ -3333,7 +3406,6 @@
/// The provided `handler` is called to respond to the user triggered
/// `action`.
void _addArgumentlessAction(SemanticsAction action, VoidCallback handler) {
- assert(handler != null);
_addAction(action, (Object? args) {
assert(args == null);
handler();
@@ -3648,7 +3720,7 @@
_addAction(SemanticsAction.setSelection, (Object? args) {
assert(args != null && args is Map);
final Map<String, int> selection = (args! as Map<dynamic, dynamic>).cast<String, int>();
- assert(selection != null && selection['base'] != null && selection['extent'] != null);
+ assert(selection['base'] != null && selection['extent'] != null);
value!(TextSelection(
baseOffset: selection['base']!,
extentOffset: selection['extent']!,
@@ -3724,6 +3796,25 @@
_onDidLoseAccessibilityFocus = value;
}
+ /// A delegate that decides how to handle [SemanticsConfiguration]s produced
+ /// in the widget subtree.
+ ///
+ /// The [SemanticsConfiguration]s are produced by rendering objects in the
+ /// subtree and want to merge up to their parent. This delegate can decide
+ /// which of these should be merged together to form sibling SemanticsNodes and
+ /// 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.
+ ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
+ ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
+ set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {
+ assert(value != null);
+ _childConfigurationsDelegate = value;
+ // Setting the childConfigsDelegate does not annotate any meaningful
+ // semantics information of the config.
+ }
+
/// Returns the action handler registered for [action] or null if none was
/// registered.
SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action];
@@ -3887,7 +3978,6 @@
/// * [attributedLabel], which is the [AttributedString] of this property.
String get label => _attributedLabel.string;
set label(String label) {
- assert(label != null);
_attributedLabel = AttributedString(label);
_hasBeenAnnotated = true;
}
@@ -3928,7 +4018,6 @@
/// [value] will be after performing [SemanticsAction.decrease].
String get value => _attributedValue.string;
set value(String value) {
- assert(value != null);
_attributedValue = AttributedString(value);
_hasBeenAnnotated = true;
}
@@ -3974,7 +4063,6 @@
/// * [attributedIncreasedValue], which is the [AttributedString] of this property.
String get increasedValue => _attributedIncreasedValue.string;
set increasedValue(String increasedValue) {
- assert(increasedValue != null);
_attributedIncreasedValue = AttributedString(increasedValue);
_hasBeenAnnotated = true;
}
@@ -4012,7 +4100,6 @@
/// * [attributedDecreasedValue], which is the [AttributedString] of this property.
String get decreasedValue => _attributedDecreasedValue.string;
set decreasedValue(String decreasedValue) {
- assert(decreasedValue != null);
_attributedDecreasedValue = AttributedString(decreasedValue);
_hasBeenAnnotated = true;
}
@@ -4047,7 +4134,6 @@
/// * [attributedHint], which is the [AttributedString] of this property.
String get hint => _attributedHint.string;
set hint(String hint) {
- assert(hint != null);
_attributedHint = AttributedString(hint);
_hasBeenAnnotated = true;
}
@@ -4100,7 +4186,7 @@
double get elevation => _elevation;
double _elevation = 0.0;
set elevation(double value) {
- assert(value != null && value >= 0.0);
+ assert(value >= 0.0);
if (value == _elevation) {
return;
}
@@ -4117,7 +4203,7 @@
double get thickness => _thickness;
double _thickness = 0.0;
set thickness(double value) {
- assert(value != null && value >= 0.0);
+ assert(value >= 0.0);
if (value == _thickness) {
return;
}
@@ -4448,6 +4534,11 @@
/// * [addTagForChildren] to add a tag and for more information about their
/// usage.
Iterable<SemanticsTag>? get tagsForChildren => _tagsForChildren;
+
+ /// Whether this configuration will tag the child semantics nodes with a
+ /// given [SemanticsTag].
+ bool tagsChildrenWith(SemanticsTag tag) => _tagsForChildren?.contains(tag) ?? false;
+
Set<SemanticsTag>? _tagsForChildren;
/// Specifies a [SemanticsTag] that this configuration wants to apply to all
@@ -4510,7 +4601,7 @@
if (_currentValueLength != null && other._currentValueLength != null) {
return false;
}
- if (_attributedValue != null && _attributedValue.string.isNotEmpty && other._attributedValue != null && other._attributedValue.string.isNotEmpty) {
+ if (_attributedValue.string.isNotEmpty && other._attributedValue.string.isNotEmpty) {
return false;
}
return true;
@@ -4558,13 +4649,13 @@
otherAttributedString: child._attributedLabel,
otherTextDirection: child.textDirection,
);
- if (_attributedValue == null || _attributedValue.string == '') {
+ if (_attributedValue.string == '') {
_attributedValue = child._attributedValue;
}
- if (_attributedIncreasedValue == null || _attributedIncreasedValue.string == '') {
+ if (_attributedIncreasedValue.string == '') {
_attributedIncreasedValue = child._attributedIncreasedValue;
}
- if (_attributedDecreasedValue == null || _attributedDecreasedValue.string == '') {
+ if (_attributedDecreasedValue.string == '') {
_attributedDecreasedValue = child._attributedDecreasedValue;
}
_attributedHint = _concatAttributedString(
@@ -4733,7 +4824,7 @@
}
}
-/// A [SemanticsSortKey] that sorts simply based on the `double` value it is
+/// A [SemanticsSortKey] that sorts based on the `double` value it is
/// given.
///
/// The [OrdinalSortKey] compares itself with other [OrdinalSortKey]s
@@ -4758,8 +4849,7 @@
const OrdinalSortKey(
this.order, {
super.name,
- }) : assert(order != null),
- assert(order > double.negativeInfinity),
+ }) : assert(order > double.negativeInfinity),
assert(order < double.infinity);
/// Determines the placement of this key in a sequence of keys that defines
@@ -4772,7 +4862,7 @@
@override
int doCompare(OrdinalSortKey other) {
- if (other.order == null || order == null || other.order == order) {
+ if (other.order == order) {
return 0;
}
return order.compareTo(other.order);
diff --git a/framework/lib/src/semantics/semantics_event.dart b/framework/lib/src/semantics/semantics_event.dart
index c59b0b1..63d3cbb 100644
--- a/framework/lib/src/semantics/semantics_event.dart
+++ b/framework/lib/src/semantics/semantics_event.dart
@@ -87,9 +87,7 @@
/// Constructs an event that triggers an announcement by the platform.
const AnnounceSemanticsEvent(this.message, this.textDirection, {this.assertiveness = Assertiveness.polite})
- : assert(message != null),
- assert(textDirection != null),
- super('announce');
+ : super('announce');
/// The message to announce.
///
diff --git a/framework/lib/src/services/asset_bundle.dart b/framework/lib/src/services/asset_bundle.dart
index fab80de..b90ad57 100644
--- a/framework/lib/src/services/asset_bundle.dart
+++ b/framework/lib/src/services/asset_bundle.dart
@@ -96,12 +96,22 @@
}
/// Retrieve a string from the asset bundle, parse it with the given function,
- /// and return the function's result.
+ /// and return that function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser);
+ /// Retrieve [ByteData] from the asset bundle, parse it with the given function,
+ /// and return that function's result.
+ ///
+ /// Implementations may cache the result, so a particular key should only be
+ /// used with one parser for the lifetime of the asset bundle.
+ Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
+ final ByteData data = await load(key);
+ return parser(data);
+ }
+
/// If this is a caching asset bundle, and the given key describes a cached
/// asset, then evict the asset from the cache so that the next time it is
/// loaded, the cache will be reread from the asset bundle.
@@ -151,11 +161,19 @@
/// fetched.
@override
Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser) async {
- assert(key != null);
- assert(parser != null);
return parser(await loadString(key));
}
+ /// Retrieve [ByteData] from the asset bundle, parse it with the given function,
+ /// and return the function's result.
+ ///
+ /// The result is not cached. The parser is run each time the resource is
+ /// fetched.
+ @override
+ Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
+ return parser(await load(key));
+ }
+
// TODO(ianh): Once the underlying network logic learns about caching, we
// should implement evict().
@@ -175,6 +193,7 @@
// TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
+ final Map<String, Future<dynamic>> _structuredBinaryDataCache = <String, Future<dynamic>>{};
@override
Future<String> loadString(String key, { bool cache = true }) {
@@ -196,8 +215,6 @@
/// callback synchronously.
@override
Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser) {
- assert(key != null);
- assert(parser != null);
if (_structuredDataCache.containsKey(key)) {
return _structuredDataCache[key]! as Future<T>;
}
@@ -225,16 +242,66 @@
return completer.future;
}
+ /// Retrieve bytedata from the asset bundle, parse it with the given function,
+ /// and return the function's result.
+ ///
+ /// The result of parsing the bytedata is cached (the bytedata itself is not).
+ /// For any given `key`, the `parser` is only run the first time.
+ ///
+ /// Once the value has been parsed, the future returned by this function for
+ /// subsequent calls will be a [SynchronousFuture], which resolves its
+ /// callback synchronously.
+ @override
+ Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) {
+ if (_structuredBinaryDataCache.containsKey(key)) {
+ return _structuredBinaryDataCache[key]! as Future<T>;
+ }
+
+ // load can return a SynchronousFuture in certain cases, like in the
+ // flutter_test framework. So, we need to support both async and sync flows.
+ Completer<T>? completer; // For async flow.
+ SynchronousFuture<T>? result; // For sync flow.
+
+ load(key)
+ .then<T>(parser)
+ .then<void>((T value) {
+ result = SynchronousFuture<T>(value);
+ if (completer != null) {
+ // The load and parse operation ran asynchronously. We already returned
+ // from the loadStructuredBinaryData function and therefore the caller
+ // was given the future of the completer.
+ completer.complete(value);
+ }
+ }, onError: (Object error, StackTrace stack) {
+ completer!.completeError(error, stack);
+ });
+
+ if (result != null) {
+ // The above code ran synchronously. We can synchronously return the result.
+ _structuredBinaryDataCache[key] = result!;
+ return result!;
+ }
+
+ // Since the above code is being run asynchronously and thus hasn't run its
+ // `then` handler yet, we'll return a completer that will be completed
+ // when the handler does run.
+ completer = Completer<T>();
+ _structuredBinaryDataCache[key] = completer.future;
+ return completer.future;
+ }
+
@override
void evict(String key) {
_stringCache.remove(key);
_structuredDataCache.remove(key);
+ _structuredBinaryDataCache.remove(key);
}
@override
void clear() {
_stringCache.clear();
_structuredDataCache.clear();
+ _structuredBinaryDataCache.clear();
}
@override
@@ -276,7 +343,7 @@
bool debugUsePlatformChannel = false;
assert(() {
// dart:io is safe to use here since we early return for web
- // above. If that code is changed, this needs to be gaurded on
+ // above. If that code is changed, this needs to be guarded on
// web presence. Override how assets are loaded in tests so that
// the old loader behavior that allows tests to load assets from
// the current package using the package prefix.
diff --git a/framework/lib/src/services/asset_manifest.dart b/framework/lib/src/services/asset_manifest.dart
new file mode 100644
index 0000000..879a17c
--- /dev/null
+++ b/framework/lib/src/services/asset_manifest.dart
@@ -0,0 +1,134 @@
+// 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 'asset_bundle.dart';
+import 'message_codecs.dart';
+
+const String _kAssetManifestFilename = 'AssetManifest.bin';
+
+/// Contains details about available assets and their variants.
+/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
+/// to learn about asset variants and how to declare them.
+abstract class AssetManifest {
+ /// Loads asset manifest data from an [AssetBundle] object and creates an
+ /// [AssetManifest] object from that data.
+ static Future<AssetManifest> loadFromAssetBundle(AssetBundle bundle) {
+ return bundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
+ }
+
+ /// Lists the keys of all main assets. This does not include assets
+ /// that are variants of other assets.
+ ///
+ /// The logical key maps to the path of an asset specified in the pubspec.yaml
+ /// file at build time.
+ ///
+ /// See [Specifying assets](https://docs.flutter.dev/development/ui/assets-and-images#specifying-assets)
+ /// and [Loading assets](https://docs.flutter.dev/development/ui/assets-and-images#loading-assets) for more
+ /// information.
+ List<String> listAssets();
+
+ /// Retrieves metadata about an asset and its variants.
+ ///
+ /// Note that 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);
+}
+
+// Lazily parses the binary asset manifest into a data structure that's easier to work
+// with.
+//
+// The binary asset manifest is a map of asset keys to a list of objects
+// representing the asset's variants.
+//
+// The entries with each variant object are:
+// - "asset": the location of this variant to load it from.
+// - "dpr": The device-pixel-ratio that the asset is best-suited for.
+//
+// New fields could be added to this object schema to support new asset variation
+// features, such as themes, locale/region support, reading directions, and so on.
+class _AssetManifestBin implements AssetManifest {
+ _AssetManifestBin(Map<Object?, Object?> standardMessageData): _data = standardMessageData;
+
+ factory _AssetManifestBin.fromStandardMessageCodecMessage(ByteData message) {
+ final dynamic data = const StandardMessageCodec().decodeMessage(message);
+ return _AssetManifestBin(data as Map<Object?, Object?>);
+ }
+
+ final Map<Object?, Object?> _data;
+ final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
+
+ @override
+ 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.');
+ }
+ _typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
+ .cast<Map<Object?, Object?>>()
+ .map((Map<Object?, Object?> data) => AssetMetadata(
+ key: data['asset']! as String,
+ targetDevicePixelRatio: data['dpr']! as double,
+ main: false,
+ ))
+ .toList();
+
+ _data.remove(key);
+ }
+
+ final AssetMetadata mainAsset = AssetMetadata(key: key,
+ targetDevicePixelRatio: null,
+ main: true
+ );
+
+ return <AssetMetadata>[mainAsset, ..._typeCastedData[key]!];
+ }
+
+ @override
+ List<String> listAssets() {
+ return <String>[..._data.keys.cast<String>(), ..._typeCastedData.keys];
+ }
+}
+
+/// Contains information about an asset.
+@immutable
+class AssetMetadata {
+ /// Creates an object containing information about an asset.
+ const AssetMetadata({
+ required this.key,
+ required this.targetDevicePixelRatio,
+ required this.main,
+ });
+
+ /// The device pixel ratio that this asset is most ideal for. This is determined
+ /// by the name of the parent folder of the asset file. For example, if the
+ /// parent folder is named "3.0x", the target device pixel ratio of that
+ /// asset will be interpreted as 3.
+ ///
+ /// This will be null if the parent folder name is not a ratio value followed
+ /// by an "x".
+ ///
+ /// See [Declaring resolution-aware image assets](https://docs.flutter.dev/development/ui/assets-and-images#resolution-aware)
+ /// for more information.
+ final double? targetDevicePixelRatio;
+
+ /// The asset's key, which is the path to the asset specified in the pubspec.yaml
+ /// file at build time.
+ final String key;
+
+ /// Whether or not this is a main asset. In other words, this is true if
+ /// this asset is not a variant of another asset.
+ ///
+ /// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
+ /// for more about asset variants.
+ final bool main;
+}
diff --git a/framework/lib/src/services/autofill.dart b/framework/lib/src/services/autofill.dart
index b7e9a86..54010cf 100644
--- a/framework/lib/src/services/autofill.dart
+++ b/framework/lib/src/services/autofill.dart
@@ -651,8 +651,7 @@
this.autofillHints = const <String>[],
this.hintText,
required this.currentEditingValue,
- }) : assert(uniqueIdentifier != null),
- assert(autofillHints != null);
+ });
/// An [AutofillConfiguration] that indicates the [AutofillClient] does not
/// wish to be autofilled.
@@ -810,9 +809,7 @@
_AutofillScopeTextInputConfiguration({
required this.allConfigurations,
required TextInputConfiguration currentClientConfiguration,
- }) : assert(allConfigurations != null),
- assert(currentClientConfiguration != null),
- super(inputType: currentClientConfiguration.inputType,
+ }) : super(inputType: currentClientConfiguration.inputType,
obscureText: currentClientConfiguration.obscureText,
autocorrect: currentClientConfiguration.autocorrect,
smartDashesType: currentClientConfiguration.smartDashesType,
@@ -843,7 +840,6 @@
mixin AutofillScopeMixin implements AutofillScope {
@override
TextInputConnection attach(TextInputClient trigger, TextInputConfiguration configuration) {
- assert(trigger != null);
assert(
!autofillClients.any((AutofillClient client) => !client.textInputConfiguration.autofillConfiguration.enabled),
'Every client in AutofillScope.autofillClients must enable autofill',
diff --git a/framework/lib/src/services/browser_context_menu.dart b/framework/lib/src/services/browser_context_menu.dart
new file mode 100644
index 0000000..0f5c667
--- /dev/null
+++ b/framework/lib/src/services/browser_context_menu.dart
@@ -0,0 +1,83 @@
+// 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 'system_channels.dart';
+
+/// Controls the browser's context menu on the web platform.
+///
+/// The context menu is the menu that appears on right clicking or selecting
+/// text in the browser, for example.
+///
+/// On web, by default, the browser's context menu is enabled and Flutter's
+/// context menus are hidden.
+///
+/// On all non-web platforms, this does nothing.
+class BrowserContextMenu {
+ BrowserContextMenu._();
+
+ static final BrowserContextMenu _instance = BrowserContextMenu._();
+
+ /// Whether showing the browser's context menu is enabled.
+ ///
+ /// When true, any event that the browser typically uses to trigger its
+ /// context menu (e.g. right click) will do so. When false, the browser's
+ /// context menu will not show.
+ ///
+ /// It's possible for this to be true but for the browser's context menu to
+ /// not show due to direct manipulation of the DOM. For example, handlers for
+ /// the browser's `contextmenu` event could be added/removed in the browser's
+ /// JavaScript console, and this boolean wouldn't know about it. This boolean
+ /// only indicates the results of calling [disableContextMenu] and
+ /// [enableContextMenu] here.
+ ///
+ /// Defaults to true.
+ static bool get enabled => _instance._enabled;
+
+ bool _enabled = true;
+
+ final MethodChannel _channel = SystemChannels.contextMenu;
+
+ /// Disable the browser's context menu.
+ ///
+ /// By default, when the app starts, the browser's context menu is already
+ /// enabled.
+ ///
+ /// This is an asynchronous action. The context menu can be considered to be
+ /// disabled at the time that the Future resolves. [enabled] won't reflect the
+ /// change until that time.
+ ///
+ /// See also:
+ /// * [enableContextMenu], which performs the opposite operation.
+ static Future<void> disableContextMenu() {
+ assert(kIsWeb, 'This has no effect on platforms other than web.');
+ return _instance._channel.invokeMethod<void>(
+ 'disableContextMenu',
+ ).then((_) {
+ _instance._enabled = false;
+ });
+ }
+
+ /// Enable the browser's context menu.
+ ///
+ /// By default, when the app starts, the browser's context menu is already
+ /// enabled. Typically this method would be called after first calling
+ /// [disableContextMenu].
+ ///
+ /// This is an asynchronous action. The context menu can be considered to be
+ /// enabled at the time that the Future resolves. [enabled] won't reflect the
+ /// change until that time.
+ ///
+ /// See also:
+ /// * [disableContextMenu], which performs the opposite operation.
+ static Future<void> enableContextMenu() {
+ assert(kIsWeb, 'This has no effect on platforms other than web.');
+ return _instance._channel.invokeMethod<void>(
+ 'enableContextMenu',
+ ).then((_) {
+ _instance._enabled = true;
+ });
+ }
+}
diff --git a/framework/lib/src/services/debug.dart b/framework/lib/src/services/debug.dart
index 610a1ff..ade9404 100644
--- a/framework/lib/src/services/debug.dart
+++ b/framework/lib/src/services/debug.dart
@@ -17,7 +17,7 @@
/// Profile and print statistics on Platform Channel usage.
///
-/// When this is is true statistics about the usage of Platform Channels will be
+/// When this is true statistics about the usage of Platform Channels will be
/// printed out periodically to the console and Timeline events will show the
/// time between sending and receiving a message (encoding and decoding time
/// excluded).
diff --git a/framework/lib/src/services/hardware_keyboard.dart b/framework/lib/src/services/hardware_keyboard.dart
index 5cc2f5e..7fcbe1a 100644
--- a/framework/lib/src/services/hardware_keyboard.dart
+++ b/framework/lib/src/services/hardware_keyboard.dart
@@ -26,34 +26,33 @@
/// Only a limited number of modes are supported, which are enumerated as
/// static members of this class. Manual constructing of this class is
/// prohibited.
-@immutable
-class KeyboardLockMode {
- // KeyboardLockMode has a fixed pool of supported keys, enumerated as static
- // members of this class, therefore constructing is prohibited.
- const KeyboardLockMode._(this.logicalKey);
-
- /// The logical key that triggers this lock mode.
- final LogicalKeyboardKey logicalKey;
-
+enum KeyboardLockMode {
/// Represents the number lock mode on the keyboard.
///
/// On supporting systems, enabling number lock mode usually allows key
/// presses of the number pad to input numbers, instead of acting as up, down,
/// left, right, page up, end, etc.
- static const KeyboardLockMode numLock = KeyboardLockMode._(LogicalKeyboardKey.numLock);
+ numLock._(LogicalKeyboardKey.numLock),
/// Represents the scrolling lock mode on the keyboard.
///
/// On supporting systems and applications (such as a spreadsheet), enabling
/// scrolling lock mode usually allows key presses of the cursor keys to
/// scroll the document instead of the cursor.
- static const KeyboardLockMode scrollLock = KeyboardLockMode._(LogicalKeyboardKey.scrollLock);
+ scrollLock._(LogicalKeyboardKey.scrollLock),
/// Represents the capital letters lock mode on the keyboard.
///
/// On supporting systems, enabling capital lock mode allows key presses of
/// the letter keys to input uppercase letters instead of lowercase.
- static const KeyboardLockMode capsLock = KeyboardLockMode._(LogicalKeyboardKey.capsLock);
+ capsLock._(LogicalKeyboardKey.capsLock);
+
+ // KeyboardLockMode has a fixed pool of supported keys, enumerated as static
+ // members of this class, therefore constructing is prohibited.
+ const KeyboardLockMode._(this.logicalKey);
+
+ /// The logical key that triggers this lock mode.
+ final LogicalKeyboardKey logicalKey;
static final Map<int, KeyboardLockMode> _knownLockModes = <int, KeyboardLockMode>{
numLock.logicalKey.keyId: numLock,
@@ -101,7 +100,7 @@
/// is a mnemonic ("keyA" is easier to remember than 0x70004), derived from the
/// key's effect on a QWERTY keyboard. The name does not represent the key's
/// effect whatsoever (a physical "keyA" can be the Q key on an AZERTY
- /// keyboard.)
+ /// keyboard).
///
/// For instance, if you wanted to make a game where the key to the right of
/// the CAPS LOCK key made the player move left, you would be comparing a
@@ -115,10 +114,10 @@
///
/// Also, even though physical keys are defined with USB HID codes, their
/// values are not necessarily the same HID codes produced by the hardware and
- /// presented to the driver, because on most platforms Flutter has to map the
- /// platform representation back to an HID code since the original HID
- /// code is not provided. USB HID is simply a conveniently well-defined
- /// standard to list possible keys that a Flutter app can encounter.
+ /// presented to the driver. On most platforms, Flutter has to map the
+ /// platform representation back to a HID code because the original HID
+ /// code is not provided. USB HID was chosen because it is a well-defined
+ /// standard for referring to keys such as those a Flutter app may encounter.
///
/// See also:
///
@@ -242,6 +241,9 @@
/// An event indicating that the user has been holding a key on the keyboard
/// and causing repeated events.
///
+/// Repeat events are not guaranteed and are provided only if supported by the
+/// underlying platform.
+///
/// See also:
///
/// * [KeyDownEvent], a key event representing the user
@@ -259,7 +261,7 @@
});
}
-/// The signature for [HardwareKeyboard.addHandler], a callback to to decide whether
+/// The signature for [HardwareKeyboard.addHandler], a callback to decide whether
/// the entire framework handles a key event.
typedef KeyEventCallback = bool Function(KeyEvent event);
@@ -738,7 +740,7 @@
/// The global entrance which handles all key events sent to Flutter.
///
/// Typical applications use [WidgetsBinding], where this field is
- /// set by the focus system (see `FocusManger`) on startup to a function that
+ /// set by the focus system (see `FocusManager`) on startup to a function that
/// dispatches incoming events to the focus system, including
/// `FocusNode.onKey`, `FocusNode.onKeyEvent`, and `Shortcuts`. In this case,
/// the application does not need to read, assign, or invoke this value.
diff --git a/framework/lib/src/services/keyboard_inserted_content.dart b/framework/lib/src/services/keyboard_inserted_content.dart
new file mode 100644
index 0000000..4276539
--- /dev/null
+++ b/framework/lib/src/services/keyboard_inserted_content.dart
@@ -0,0 +1,58 @@
+// 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';
+
+/// A class representing rich content (such as a PNG image) inserted via the
+/// system input method.
+///
+/// The following data is represented in this class:
+/// - MIME Type
+/// - Bytes
+/// - URI
+@immutable
+class KeyboardInsertedContent {
+ /// Creates an object to represent content that is inserted from the virtual
+ /// keyboard.
+ ///
+ /// The mime type and URI will always be provided, but the bytedata may be null.
+ const KeyboardInsertedContent({required this.mimeType, required this.uri, this.data});
+
+ /// Converts JSON received from the Flutter Engine into the Dart class.
+ KeyboardInsertedContent.fromJson(Map<String, dynamic> metadata):
+ mimeType = metadata['mimeType'] as String,
+ uri = metadata['uri'] as String,
+ data = metadata['data'] != null
+ ? Uint8List.fromList(List<int>.from(metadata['data'] as Iterable<dynamic>))
+ : null;
+
+ /// The mime type of the inserted content.
+ final String mimeType;
+
+ /// The URI (location) of the inserted content, usually a "content://" URI.
+ final String uri;
+
+ /// The bytedata of the inserted content.
+ final Uint8List? data;
+
+ /// Convenience getter to check if bytedata is available for the inserted content.
+ bool get hasData => data?.isNotEmpty ?? false;
+
+ @override
+ String toString() => '${objectRuntimeType(this, 'KeyboardInsertedContent')}($mimeType, $uri, $data)';
+
+ @override
+ bool operator ==(Object other) {
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is KeyboardInsertedContent
+ && other.mimeType == mimeType
+ && other.uri == uri
+ && other.data == data;
+ }
+
+ @override
+ int get hashCode => Object.hash(mimeType, uri, data);
+}
diff --git a/framework/lib/src/services/message_codec.dart b/framework/lib/src/services/message_codec.dart
index ede7d76..1816667 100644
--- a/framework/lib/src/services/message_codec.dart
+++ b/framework/lib/src/services/message_codec.dart
@@ -29,13 +29,12 @@
T? decodeMessage(ByteData? message);
}
-/// An command object representing the invocation of a named method.
+/// A command object representing the invocation of a named method.
@immutable
class MethodCall {
/// Creates a [MethodCall] representing the invocation of [method] with the
/// specified [arguments].
- const MethodCall(this.method, [this.arguments])
- : assert(method != null);
+ const MethodCall(this.method, [this.arguments]);
/// The name of the method to be called.
final String method;
@@ -114,7 +113,7 @@
this.message,
this.details,
this.stacktrace,
- }) : assert(code != null);
+ });
/// An error code.
final String code;
diff --git a/framework/lib/src/services/message_codecs.dart b/framework/lib/src/services/message_codecs.dart
index 1b09f28..0aa6d32 100644
--- a/framework/lib/src/services/message_codecs.dart
+++ b/framework/lib/src/services/message_codecs.dart
@@ -195,7 +195,6 @@
@override
ByteData encodeErrorEnvelope({ required String code, String? message, Object? details}) {
- assert(code != null);
return const JSONMessageCodec().encodeMessage(<Object?>[code, message, details])!;
}
}
diff --git a/framework/lib/src/services/mouse_cursor.dart b/framework/lib/src/services/mouse_cursor.dart
index 57ac614..6df02a0 100644
--- a/framework/lib/src/services/mouse_cursor.dart
+++ b/framework/lib/src/services/mouse_cursor.dart
@@ -104,9 +104,7 @@
/// Create a session.
///
/// All arguments must be non-null.
- MouseCursorSession(this.cursor, this.device)
- : assert(cursor != null),
- assert(device != null);
+ MouseCursorSession(this.cursor, this.device);
/// The cursor that created this session.
final MouseCursor cursor;
@@ -215,7 +213,7 @@
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
final String debugDescription = this.debugDescription;
- if (minLevel.index >= DiagnosticLevel.info.index && debugDescription != null) {
+ if (minLevel.index >= DiagnosticLevel.info.index) {
return debugDescription;
}
return super.toString(minLevel: minLevel);
@@ -262,7 +260,6 @@
/// Returns the first cursor that is not a [MouseCursor.defer].
static MouseCursor? firstNonDeferred(Iterable<MouseCursor> cursors) {
for (final MouseCursor cursor in cursors) {
- assert(cursor != null);
if (cursor != MouseCursor.defer) {
return cursor;
}
@@ -358,7 +355,7 @@
// the supported system cursors are enumerated in [SystemMouseCursors].
const SystemMouseCursor._({
required this.kind,
- }) : assert(kind != null);
+ });
/// A string that identifies the kind of the cursor.
///
diff --git a/framework/lib/src/services/mouse_tracking.dart b/framework/lib/src/services/mouse_tracking.dart
index 93ec4ff..e354e3e 100644
--- a/framework/lib/src/services/mouse_tracking.dart
+++ b/framework/lib/src/services/mouse_tracking.dart
@@ -51,7 +51,7 @@
this.onExit,
this.cursor = MouseCursor.defer,
this.validForMouseTracker = true,
- }) : assert(cursor != null);
+ });
/// Triggered when a mouse pointer, with or without buttons pressed, has
/// entered the region and [validForMouseTracker] is true.
diff --git a/framework/lib/src/services/platform_channel.dart b/framework/lib/src/services/platform_channel.dart
index 053fc7b..c556691 100644
--- a/framework/lib/src/services/platform_channel.dart
+++ b/framework/lib/src/services/platform_channel.dart
@@ -163,9 +163,7 @@
/// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger]
/// instance is used if [binaryMessenger] is null.
const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger })
- : assert(name != null),
- assert(codec != null),
- _binaryMessenger = binaryMessenger;
+ : _binaryMessenger = binaryMessenger;
/// The logical channel on which communication happens, not null.
final String name;
@@ -253,9 +251,7 @@
/// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger]
/// instance is used if [binaryMessenger] is null.
const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ])
- : assert(name != null),
- assert(codec != null),
- _binaryMessenger = binaryMessenger;
+ : _binaryMessenger = binaryMessenger;
/// The logical channel on which communication happens, not null.
final String name;
@@ -300,7 +296,6 @@
/// nullable.
@optionalTypeArgs
Future<T?> _invokeMethod<T>(String method, { required bool missingOk, dynamic arguments }) async {
- assert(method != null);
final ByteData input = codec.encodeMethodCall(MethodCall(method, arguments));
final ByteData? result =
!kReleaseMode && debugProfilePlatformChannels ?
@@ -535,7 +530,7 @@
/// Any other exception results in an error envelope being sent.
void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
assert(
- _binaryMessenger != null || ServicesBinding.instance != null,
+ _binaryMessenger != null || BindingBase.debugBindingType() != null,
'Cannot set the method call handler before the binary messenger has been initialized. '
'This happens when you call setMethodCallHandler() before the WidgetsFlutterBinding '
'has been initialized. You can fix this by either calling WidgetsFlutterBinding.ensureInitialized() '
@@ -609,9 +604,7 @@
/// Neither [name] nor [codec] may be null. The default [ServicesBinding.defaultBinaryMessenger]
/// instance is used if [binaryMessenger] is null.
const EventChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger])
- : assert(name != null),
- assert(codec != null),
- _binaryMessenger = binaryMessenger;
+ : _binaryMessenger = binaryMessenger;
/// The logical channel on which communication happens, not null.
final String name;
diff --git a/framework/lib/src/services/platform_views.dart b/framework/lib/src/services/platform_views.dart
index bb919ab..6229559 100644
--- a/framework/lib/src/services/platform_views.dart
+++ b/framework/lib/src/services/platform_views.dart
@@ -119,9 +119,6 @@
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) {
- assert(id != null);
- assert(viewType != null);
- assert(layoutDirection != null);
assert(creationParams == null || creationParamsCodec != null);
final TextureAndroidViewController controller = TextureAndroidViewController._(
@@ -150,9 +147,6 @@
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) {
- assert(id != null);
- assert(viewType != null);
- assert(layoutDirection != null);
assert(creationParams == null || creationParamsCodec != null);
final SurfaceAndroidViewController controller = SurfaceAndroidViewController._(
@@ -222,9 +216,6 @@
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) async {
- assert(id != null);
- assert(viewType != null);
- assert(layoutDirection != null);
assert(creationParams == null || creationParamsCodec != null);
// TODO(amirh): pass layoutDirection once the system channel supports it.
@@ -258,8 +249,7 @@
const AndroidPointerProperties({
required this.id,
required this.toolType,
- }) : assert(id != null),
- assert(toolType != null);
+ });
/// See Android's [MotionEvent.PointerProperties#id](https://developer.android.com/reference/android/view/MotionEvent.PointerProperties.html#id).
final int id;
@@ -308,15 +298,7 @@
required this.touchMinor,
required this.x,
required this.y,
- }) : assert(orientation != null),
- assert(pressure != null),
- assert(size != null),
- assert(toolMajor != null),
- assert(toolMinor != null),
- assert(touchMajor != null),
- assert(touchMinor != null),
- assert(x != null),
- assert(y != null);
+ });
/// The orientation of the touch area and tool area in radians clockwise from vertical.
///
@@ -404,21 +386,7 @@
required this.source,
required this.flags,
required this.motionEventId,
- }) : assert(downTime != null),
- assert(eventTime != null),
- assert(action != null),
- assert(pointerCount != null),
- assert(pointerProperties != null),
- assert(pointerCoords != null),
- assert(metaState != null),
- assert(buttonState != null),
- assert(xPrecision != null),
- assert(yPrecision != null),
- assert(deviceId != null),
- assert(edgeFlags != null),
- assert(source != null),
- assert(flags != null),
- assert(pointerProperties.length == pointerCount),
+ }) : assert(pointerProperties.length == pointerCount),
assert(pointerCoords.length == pointerCount);
/// The time (in ms) when the user originally pressed down to start a stream of position events,
@@ -532,12 +500,7 @@
<int, AndroidPointerProperties>{};
final Set<int> usedAndroidPointerIds = <int>{};
- PointTransformer get pointTransformer => _pointTransformer;
- late PointTransformer _pointTransformer;
- set pointTransformer(PointTransformer transformer) {
- assert(transformer != null);
- _pointTransformer = transformer;
- }
+ late PointTransformer pointTransformer;
int? downTimeMillis;
@@ -554,7 +517,7 @@
}
void updatePointerPositions(PointerEvent event) {
- final Offset position = _pointTransformer(event.position);
+ final Offset position = pointTransformer(event.position);
pointerPositions[event.pointer] = AndroidPointerCoords(
orientation: event.orientation,
pressure: event.pressure,
@@ -691,10 +654,7 @@
required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic>? creationParamsCodec,
- }) : assert(viewId != null),
- assert(viewType != null),
- assert(layoutDirection != null),
- assert(creationParams == null || creationParamsCodec != null),
+ }) : assert(creationParams == null || creationParamsCodec != null),
_viewType = viewType,
_layoutDirection = layoutDirection,
_creationParams = creationParams == null ? null : _CreationParams(creationParams, creationParamsCodec!);
@@ -755,7 +715,6 @@
<PlatformViewCreatedCallback>[];
static int _getAndroidDirection(TextDirection direction) {
- assert(direction != null);
switch (direction) {
case TextDirection.ltr:
return kAndroidLayoutDirectionLtr;
@@ -874,10 +833,9 @@
///
/// This is required to convert a [PointerEvent] to an [AndroidMotionEvent].
/// It is typically provided by using [RenderBox.globalToLocal].
- PointTransformer get pointTransformer => _motionEventConverter._pointTransformer;
+ PointTransformer get pointTransformer => _motionEventConverter.pointTransformer;
set pointTransformer(PointTransformer transformer) {
- assert(transformer != null);
- _motionEventConverter._pointTransformer = transformer;
+ _motionEventConverter.pointTransformer = transformer;
}
/// Whether the platform view has already been created.
@@ -886,14 +844,12 @@
/// Adds a callback that will get invoke after the platform view has been
/// created.
void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
- assert(listener != null);
assert(_state != _AndroidViewState.disposed);
_platformViewCreatedCallbacks.add(listener);
}
/// Removes a callback added with [addOnPlatformViewCreatedListener].
void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
- assert(listener != null);
assert(_state != _AndroidViewState.disposed);
_platformViewCreatedCallbacks.remove(listener);
}
@@ -914,7 +870,6 @@
return;
}
- assert(layoutDirection != null);
_layoutDirection = layoutDirection;
// If the view was not yet created we just update _layoutDirection and return, as the new
@@ -1361,9 +1316,7 @@
UiKitViewController._(
this.id,
TextDirection layoutDirection,
- ) : assert(id != null),
- assert(layoutDirection != null),
- _layoutDirection = layoutDirection;
+ ) : _layoutDirection = layoutDirection;
/// The unique identifier of the iOS view controlled by this controller.
@@ -1384,7 +1337,6 @@
return;
}
- assert(layoutDirection != null);
_layoutDirection = layoutDirection;
// TODO(amirh): invoke the iOS platform views channel direction method once available.
diff --git a/framework/lib/src/services/raw_keyboard.dart b/framework/lib/src/services/raw_keyboard.dart
index 833b9f0..842af59 100644
--- a/framework/lib/src/services/raw_keyboard.dart
+++ b/framework/lib/src/services/raw_keyboard.dart
@@ -585,6 +585,24 @@
/// These key events are typically only key events generated by a hardware
/// keyboard, and not those from software keyboards or input method editors.
///
+/// ## Compared to [HardwareKeyboard]
+///
+/// [RawKeyboard] is the legacy API, and will be deprecated and removed in the
+/// future. It is recommended to always use [HardwareKeyboard] and [KeyEvent]
+/// APIs (such as [FocusNode.onKeyEvent]) to handle key events.
+///
+/// Behavior-wise, [RawKeyboard] provides a less unified, less regular
+/// event model than [HardwareKeyboard]. For example:
+///
+/// * Down events might not be matched with an up event, and vice versa (the
+/// set of pressed keys is silently updated).
+/// * The logical key of the down event might not be the same as that of the up
+/// event.
+/// * Down events and repeat events are not easily distinguishable (must be
+/// tracked manually).
+/// * Lock modes (such as CapsLock) only have their "enabled" state recorded.
+/// There's no way to acquire their pressing state.
+///
/// See also:
///
/// * [RawKeyDownEvent] and [RawKeyUpEvent], the classes used to describe
@@ -592,6 +610,7 @@
/// * [RawKeyboardListener], a widget that listens for raw key events.
/// * [SystemChannels.keyEvent], the low-level channel used for receiving
/// events from the system.
+/// * [HardwareKeyboard], the recommended replacement.
class RawKeyboard {
RawKeyboard._();
diff --git a/framework/lib/src/services/raw_keyboard_android.dart b/framework/lib/src/services/raw_keyboard_android.dart
index 0be9ada..11877c5 100644
--- a/framework/lib/src/services/raw_keyboard_android.dart
+++ b/framework/lib/src/services/raw_keyboard_android.dart
@@ -42,11 +42,7 @@
this.productId = 0,
this.deviceId = 0,
this.repeatCount = 0,
- }) : assert(flags != null),
- assert(codePoint != null),
- assert(keyCode != null),
- assert(scanCode != null),
- assert(metaState != null);
+ });
/// The current set of additional flags for this event.
///
@@ -227,7 +223,6 @@
@override
bool isModifierPressed(ModifierKey key, { KeyboardSide side = KeyboardSide.any }) {
- assert(side != null);
switch (key) {
case ModifierKey.controlModifier:
return _isLeftRightModifierPressed(side, modifierControl, modifierLeftControl, modifierRightControl);
diff --git a/framework/lib/src/services/raw_keyboard_fuchsia.dart b/framework/lib/src/services/raw_keyboard_fuchsia.dart
index f4feeba..4dd715b 100644
--- a/framework/lib/src/services/raw_keyboard_fuchsia.dart
+++ b/framework/lib/src/services/raw_keyboard_fuchsia.dart
@@ -28,9 +28,7 @@
this.hidUsage = 0,
this.codePoint = 0,
this.modifiers = 0,
- }) : assert(hidUsage != null),
- assert(codePoint != null),
- assert(modifiers != null);
+ });
/// The USB HID usage.
///
@@ -107,7 +105,6 @@
@override
bool isModifierPressed(ModifierKey key, { KeyboardSide side = KeyboardSide.any }) {
- assert(side != null);
switch (key) {
case ModifierKey.controlModifier:
return _isLeftRightModifierPressed(side, modifierControl, modifierLeftControl, modifierRightControl);
diff --git a/framework/lib/src/services/raw_keyboard_ios.dart b/framework/lib/src/services/raw_keyboard_ios.dart
index 426f077..e351df3 100644
--- a/framework/lib/src/services/raw_keyboard_ios.dart
+++ b/framework/lib/src/services/raw_keyboard_ios.dart
@@ -30,10 +30,7 @@
this.charactersIgnoringModifiers = '',
this.keyCode = 0,
this.modifiers = 0,
- }) : assert(characters != null),
- assert(charactersIgnoringModifiers != null),
- assert(keyCode != null),
- assert(modifiers != null);
+ });
/// The Unicode characters associated with a key-up or key-down event.
///
diff --git a/framework/lib/src/services/raw_keyboard_linux.dart b/framework/lib/src/services/raw_keyboard_linux.dart
index 009728d..46ec9fb 100644
--- a/framework/lib/src/services/raw_keyboard_linux.dart
+++ b/framework/lib/src/services/raw_keyboard_linux.dart
@@ -33,12 +33,7 @@
this.modifiers = 0,
required this.isDown,
this.specifiedLogicalKey,
- }) : assert(scanCode != null),
- assert(unicodeScalarValues != null),
- assert((unicodeScalarValues & ~LogicalKeyboardKey.valueMask) == 0),
- assert(keyCode != null),
- assert(modifiers != null),
- assert(keyHelper != null);
+ }) : assert((unicodeScalarValues & ~LogicalKeyboardKey.valueMask) == 0);
/// A helper class that abstracts the fetching of the toolkit-specific mappings.
///
diff --git a/framework/lib/src/services/raw_keyboard_macos.dart b/framework/lib/src/services/raw_keyboard_macos.dart
index 4dd0ab2..becbac4 100644
--- a/framework/lib/src/services/raw_keyboard_macos.dart
+++ b/framework/lib/src/services/raw_keyboard_macos.dart
@@ -42,10 +42,7 @@
this.keyCode = 0,
this.modifiers = 0,
this.specifiedLogicalKey,
- }) : assert(characters != null),
- assert(charactersIgnoringModifiers != null),
- assert(keyCode != null),
- assert(modifiers != null);
+ });
/// The Unicode characters associated with a key-up or key-down event.
///
diff --git a/framework/lib/src/services/raw_keyboard_web.dart b/framework/lib/src/services/raw_keyboard_web.dart
index cd3a8a2..9d313a3 100644
--- a/framework/lib/src/services/raw_keyboard_web.dart
+++ b/framework/lib/src/services/raw_keyboard_web.dart
@@ -35,8 +35,7 @@
this.location = 0,
this.metaState = modifierNone,
this.keyCode = 0,
- }) : assert(code != null),
- assert(metaState != null);
+ });
/// The `KeyboardEvent.code` corresponding to this event.
///
diff --git a/framework/lib/src/services/raw_keyboard_windows.dart b/framework/lib/src/services/raw_keyboard_windows.dart
index 5a6c477..4d2ef1f 100644
--- a/framework/lib/src/services/raw_keyboard_windows.dart
+++ b/framework/lib/src/services/raw_keyboard_windows.dart
@@ -35,10 +35,7 @@
this.scanCode = 0,
this.characterCodePoint = 0,
this.modifiers = 0,
- }) : assert(keyCode != null),
- assert(scanCode != null),
- assert(characterCodePoint != null),
- assert(modifiers != null);
+ });
/// The hardware key code corresponding to this key event.
///
diff --git a/framework/lib/src/services/restoration.dart b/framework/lib/src/services/restoration.dart
index 24f5074..073026b 100644
--- a/framework/lib/src/services/restoration.dart
+++ b/framework/lib/src/services/restoration.dart
@@ -259,7 +259,6 @@
/// called.
@protected
void handleRestorationUpdateFromEngine({required bool enabled, required Uint8List? data}) {
- assert(enabled != null);
assert(enabled || data == null);
_isReplacing = _rootBucketIsValid && enabled;
@@ -297,7 +296,6 @@
/// by the data.
@protected
Future<void> sendToEngine(Uint8List encodedData) {
- assert(encodedData != null);
return SystemChannels.restoration.invokeMethod<void>(
'put',
encodedData,
@@ -344,7 +342,6 @@
@protected
@visibleForTesting
void scheduleSerializationFor(RestorationBucket bucket) {
- assert(bucket != null);
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.add(bucket);
@@ -366,7 +363,6 @@
@protected
@visibleForTesting
void unscheduleSerializationFor(RestorationBucket bucket) {
- assert(bucket != null);
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.remove(bucket);
@@ -502,8 +498,7 @@
RestorationBucket.empty({
required String restorationId,
required Object? debugOwner,
- }) : assert(restorationId != null),
- _restorationId = restorationId,
+ }) : _restorationId = restorationId,
_rawData = <String, Object?>{} {
assert(() {
_debugOwner = debugOwner;
@@ -537,8 +532,7 @@
RestorationBucket.root({
required RestorationManager manager,
required Map<Object?, Object?>? rawData,
- }) : assert(manager != null),
- _manager = manager,
+ }) : _manager = manager,
_rawData = rawData ?? <Object?, Object?>{},
_restorationId = 'root' {
assert(() {
@@ -561,9 +555,7 @@
required String restorationId,
required RestorationBucket parent,
required Object? debugOwner,
- }) : assert(restorationId != null),
- assert(parent != null),
- assert(parent._rawChildren[restorationId] != null),
+ }) : assert(parent._rawChildren[restorationId] != null),
_manager = parent._manager,
_parent = parent,
_rawData = parent._rawChildren[restorationId]! as Map<Object?, Object?>,
@@ -635,7 +627,6 @@
/// restoration ID.
P? read<P>(String restorationId) {
assert(_debugAssertNotDisposed());
- assert(restorationId != null);
return _rawValues[restorationId] as P?;
}
@@ -657,7 +648,6 @@
/// restoration ID.
void write<P>(String restorationId, P value) {
assert(_debugAssertNotDisposed());
- assert(restorationId != null);
assert(debugIsSerializableForRestoration(value));
if (_rawValues[restorationId] != value || !_rawValues.containsKey(restorationId)) {
_rawValues[restorationId] = value;
@@ -679,7 +669,6 @@
/// restoration ID.
P? remove<P>(String restorationId) {
assert(_debugAssertNotDisposed());
- assert(restorationId != null);
final bool needsUpdate = _rawValues.containsKey(restorationId);
final P? result = _rawValues.remove(restorationId) as P?;
if (_rawValues.isEmpty) {
@@ -701,7 +690,6 @@
/// * [remove], which removes a value from the bucket.
bool contains(String restorationId) {
assert(_debugAssertNotDisposed());
- assert(restorationId != null);
return _rawValues.containsKey(restorationId);
}
@@ -737,7 +725,6 @@
/// delete the information stored in it from the app's restoration data.
RestorationBucket claimChild(String restorationId, {required Object? debugOwner}) {
assert(_debugAssertNotDisposed());
- assert(restorationId != null);
// There are three cases to consider:
// 1. Claiming an ID that has already been claimed.
// 2. Claiming an ID that doesn't yet exist in [_rawChildren].
@@ -787,7 +774,6 @@
/// No-op if the provided bucket is already a child of this bucket.
void adoptChild(RestorationBucket child) {
assert(_debugAssertNotDisposed());
- assert(child != null);
if (child._parent != this) {
child._parent?._removeChildData(child);
child._parent = this;
@@ -801,7 +787,6 @@
}
void _dropChild(RestorationBucket child) {
- assert(child != null);
assert(child._parent == this);
_removeChildData(child);
child._parent = null;
@@ -876,7 +861,6 @@
}
void _removeChildData(RestorationBucket child) {
- assert(child != null);
assert(child._parent == this);
if (_claimedChildren.remove(child.restorationId) == child) {
_rawChildren.remove(child.restorationId);
@@ -901,7 +885,6 @@
}
void _addChildData(RestorationBucket child) {
- assert(child != null);
assert(child._parent == this);
if (_claimedChildren.containsKey(child.restorationId)) {
// Delay addition until the end of the frame in the hopes that the current
@@ -944,7 +927,6 @@
/// another ID, or has moved it to a new parent via [adoptChild].
void rename(String newRestorationId) {
assert(_debugAssertNotDisposed());
- assert(newRestorationId != null);
if (newRestorationId == restorationId) {
return;
}
diff --git a/framework/lib/src/services/spell_check.dart b/framework/lib/src/services/spell_check.dart
index 1de1c34..da7055c 100644
--- a/framework/lib/src/services/spell_check.dart
+++ b/framework/lib/src/services/spell_check.dart
@@ -27,9 +27,7 @@
///
/// The [range] and replacement [suggestions] must all not
/// be null.
- const SuggestionSpan(this.range, this.suggestions)
- : assert(range != null),
- assert(suggestions != null);
+ const SuggestionSpan(this.range, this.suggestions);
/// The misspelled range of text.
final TextRange range;
@@ -58,9 +56,7 @@
@immutable
class SpellCheckResults {
/// Creates results based off those received by spell checking some text input.
- const SpellCheckResults(this.spellCheckedText, this.suggestionSpans)
- : assert(spellCheckedText != null),
- assert(suggestionSpans != null);
+ const SpellCheckResults(this.spellCheckedText, this.suggestionSpans);
/// The text that the [suggestionSpans] correspond to.
final String spellCheckedText;
@@ -170,8 +166,6 @@
@override
Future<List<SuggestionSpan>?> fetchSpellCheckSuggestions(
Locale locale, String text) async {
- assert(locale != null);
- assert(text != null);
final List<dynamic> rawResults;
final String languageTag = locale.toLanguageTag();
diff --git a/framework/lib/src/services/system_channels.dart b/framework/lib/src/services/system_channels.dart
index 77d3daf..c6e4b18 100644
--- a/framework/lib/src/services/system_channels.dart
+++ b/framework/lib/src/services/system_channels.dart
@@ -465,4 +465,17 @@
///
/// * [DefaultPlatformMenuDelegate], which uses this channel.
static const MethodChannel menu = OptionalMethodChannel('flutter/menu');
+
+ /// A [MethodChannel] for configuring the browser's context menu on web.
+ ///
+ /// The following outgoing methods are defined for this channel (invoked using
+ /// [OptionalMethodChannel.invokeMethod]):
+ ///
+ /// * `enableContextMenu`: enables the browser's context menu. When a Flutter
+ /// app starts, the browser's context menu is already enabled.
+ /// * `disableContextMenu`: disables the browser's context menu.
+ static const MethodChannel contextMenu = OptionalMethodChannel(
+ 'flutter/contextmenu',
+ JSONMethodCodec(),
+ );
}
diff --git a/framework/lib/src/services/system_chrome.dart b/framework/lib/src/services/system_chrome.dart
index c213d72..e37f78b 100644
--- a/framework/lib/src/services/system_chrome.dart
+++ b/framework/lib/src/services/system_chrome.dart
@@ -577,7 +577,6 @@
///
/// * [AnnotatedRegion], the widget used to place data into the layer tree.
static void setSystemUIOverlayStyle(SystemUiOverlayStyle style) {
- assert(style != null);
if (_pendingStyle != null) {
// The microtask has already been queued; just update the pending value.
_pendingStyle = style;
diff --git a/framework/lib/src/services/system_navigator.dart b/framework/lib/src/services/system_navigator.dart
index 1c3d05d..3d1a5b5 100644
--- a/framework/lib/src/services/system_navigator.dart
+++ b/framework/lib/src/services/system_navigator.dart
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-
import 'system_channels.dart';
/// Controls specific aspects of the system navigation stack.
@@ -98,28 +97,4 @@
},
);
}
-
- /// Notifies the platform of a route change, and selects single-entry history
- /// mode.
- ///
- /// This is equivalent to calling [selectSingleEntryHistory] and
- /// [routeInformationUpdated] together.
- ///
- /// The `previousRouteName` argument is ignored.
- @Deprecated(
- 'Use routeInformationUpdated instead. '
- 'This feature was deprecated after v2.3.0-1.0.pre.'
- )
- static Future<void> routeUpdated({
- String? routeName,
- String? previousRouteName,
- }) {
- return SystemChannels.navigation.invokeMethod<void>(
- 'routeUpdated',
- <String, dynamic>{
- 'previousRouteName': previousRouteName,
- 'routeName': routeName,
- },
- );
- }
}
diff --git a/framework/lib/src/services/text_boundary.dart b/framework/lib/src/services/text_boundary.dart
index c35b071..efbea24 100644
--- a/framework/lib/src/services/text_boundary.dart
+++ b/framework/lib/src/services/text_boundary.dart
@@ -2,60 +2,68 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:math' as math;
-import 'package:engine/ui.dart';
+import 'dart:math';
import 'package:characters/characters.dart' show CharacterRange;
import 'text_layout_metrics.dart';
-/// An interface for retrieving the logical text boundary (left-closed-right-open)
-/// at a given location in a document.
+// Examples can assume:
+// late TextLayoutMetrics textLayout;
+// late TextSpan text;
+// bool isWhitespace(int? codeUnit) => true;
+
+/// Signature for a predicate that takes an offset into a UTF-16 string, and a
+/// boolean that indicates the search direction.
+typedef UntilPredicate = bool Function(int offset, bool forward);
+
+/// An interface for retrieving the logical text boundary (as opposed to the
+/// visual boundary) at a given code unit offset in a document.
///
-/// The input [TextPosition] points to a position between 2 code units (which
-/// can be visually represented by the caret if the selection were to collapse
-/// to that position). The [TextPosition.affinity] is used to determine which
-/// code unit it points. For example, `TextPosition(i, upstream)` points to
-/// code unit `i - 1` and `TextPosition(i, downstream)` points to code unit `i`.
+/// Either the [getTextBoundaryAt] method, or both the
+/// [getLeadingTextBoundaryAt] method and the [getTrailingTextBoundaryAt] method
+/// must be implemented.
abstract class TextBoundary {
/// A constant constructor to enable subclass override.
const TextBoundary();
- /// Returns the leading text boundary at the given location.
+ /// Returns the offset of the closest text boundary before or at the given
+ /// `position`, or null if no boundaries can be found.
///
- /// The return value must be less or equal to the input position.
- TextPosition getLeadingTextBoundaryAt(TextPosition position);
-
- /// Returns the trailing text boundary at the given location, exclusive.
- ///
- /// The return value must be greater or equal to the input position.
- TextPosition getTrailingTextBoundaryAt(TextPosition position);
-
- /// Gets the text boundary range that encloses the input position.
- TextRange getTextBoundaryAt(TextPosition position) {
- return TextRange(
- start: getLeadingTextBoundaryAt(position).offset,
- end: getTrailingTextBoundaryAt(position).offset,
- );
+ /// The return value, if not null, is usually less than or equal to `position`.
+ int? getLeadingTextBoundaryAt(int position) {
+ if (position < 0) {
+ return null;
+ }
+ final int start = getTextBoundaryAt(position).start;
+ return start >= 0 ? start : null;
}
- /// Gets the boundary by calling the left-hand side and pipe the result to
- /// right-hand side.
+ /// Returns the offset of the closest text boundaries after the given `position`,
+ /// or null if there is no boundaries can be found after `position`.
///
- /// Combining two text boundaries can be useful if one wants to ignore certain
- /// text before finding the text boundary. For example, use
- /// [WhitespaceBoundary] + [WordBoundary] to ignores any white space before
- /// finding word boundary if the input position happens to be a whitespace
- /// character.
- TextBoundary operator +(TextBoundary other) {
- return _ExpandedTextBoundary(inner: other, outer: this);
+ /// The return value, if not null, is usually greater than `position`.
+ int? getTrailingTextBoundaryAt(int position) {
+ final int end = getTextBoundaryAt(max(0, position)).end;
+ return end >= 0 ? end : null;
+ }
+
+ /// Returns the text boundary range that encloses the input position.
+ ///
+ /// The returned [TextRange] may contain `-1`, which indicates no boundaries
+ /// can be found in that direction.
+ TextRange getTextBoundaryAt(int position) {
+ final int start = getLeadingTextBoundaryAt(position) ?? -1;
+ final int end = getTrailingTextBoundaryAt(position) ?? -1;
+ return TextRange(start: start, end: end);
}
}
-/// A text boundary that uses characters as logical boundaries.
+/// A [TextBoundary] subclass for retriving the range of the grapheme the given
+/// `position` is in.
///
-/// This class takes grapheme clusters into account and avoid creating
-/// boundaries that generate malformed utf-16 characters.
+/// The class is implemented using the
+/// [characters](https://pub.dev/packages/characters) package.
class CharacterBoundary extends TextBoundary {
/// Creates a [CharacterBoundary] with the text.
const CharacterBoundary(this._text);
@@ -63,286 +71,145 @@
final String _text;
@override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) {
- if (position.offset <= 0) {
- return const TextPosition(offset: 0);
+ int? getLeadingTextBoundaryAt(int position) {
+ if (position < 0) {
+ return null;
}
- if (position.offset > _text.length ||
- (position.offset == _text.length && position.affinity == TextAffinity.downstream)) {
- return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
- }
- final int endOffset;
- final int startOffset;
- switch (position.affinity) {
- case TextAffinity.upstream:
- startOffset = math.min(position.offset - 1, _text.length);
- endOffset = math.min(position.offset, _text.length);
- break;
- case TextAffinity.downstream:
- startOffset = math.min(position.offset, _text.length);
- endOffset = math.min(position.offset + 1, _text.length);
- break;
- }
- return TextPosition(
- offset: CharacterRange.at(_text, startOffset, endOffset).stringBeforeLength,
- );
+ final int graphemeStart = CharacterRange.at(_text, min(position, _text.length)).stringBeforeLength;
+ assert(CharacterRange.at(_text, graphemeStart).isEmpty);
+ return graphemeStart;
}
@override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- if (position.offset < 0 ||
- (position.offset == 0 && position.affinity == TextAffinity.upstream)) {
- return const TextPosition(offset: 0);
+ int? getTrailingTextBoundaryAt(int position) {
+ if (position >= _text.length) {
+ return null;
}
- if (position.offset >= _text.length) {
- return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
+ final CharacterRange rangeAtPosition = CharacterRange.at(_text, max(0, position + 1));
+ final int nextBoundary = rangeAtPosition.stringBeforeLength + rangeAtPosition.current.length;
+ assert(nextBoundary == _text.length || CharacterRange.at(_text, nextBoundary).isEmpty);
+ return nextBoundary;
+ }
+
+ @override
+ TextRange getTextBoundaryAt(int position) {
+ if (position < 0) {
+ return TextRange(start: -1, end: getTrailingTextBoundaryAt(position) ?? -1);
+ } else if (position >= _text.length) {
+ return TextRange(start: getLeadingTextBoundaryAt(position) ?? -1, end: -1);
}
- final int endOffset;
- final int startOffset;
- switch (position.affinity) {
- case TextAffinity.upstream:
- startOffset = math.min(position.offset - 1, _text.length);
- endOffset = math.min(position.offset, _text.length);
- break;
- case TextAffinity.downstream:
- startOffset = math.min(position.offset, _text.length);
- endOffset = math.min(position.offset + 1, _text.length);
- break;
- }
- final CharacterRange range = CharacterRange.at(_text, startOffset, endOffset);
- return TextPosition(
- offset: _text.length - range.stringAfterLength,
- affinity: TextAffinity.upstream,
- );
+ final CharacterRange rangeAtPosition = CharacterRange.at(_text, position);
+ return rangeAtPosition.isNotEmpty
+ ? TextRange(start: rangeAtPosition.stringBeforeLength, end: rangeAtPosition.stringBeforeLength + rangeAtPosition.current.length)
+ // rangeAtPosition is empty means `position` is a grapheme boundary.
+ : TextRange(start: rangeAtPosition.stringBeforeLength, end: getTrailingTextBoundaryAt(position) ?? -1);
}
}
-/// A text boundary that uses words as logical boundaries.
+/// A [TextBoundary] subclass for locating closest line breaks to a given
+/// `position`.
///
-/// This class uses [UAX #29](https://unicode.org/reports/tr29/) defined word
-/// boundaries to calculate its logical boundaries.
-class WordBoundary extends TextBoundary {
- /// Creates a [WordBoundary] with the text and layout information.
- const WordBoundary(this._textLayout);
+/// When the given `position` points to a hard line break, the returned range
+/// is the line's content range before the hard line break, and does not contain
+/// the given `position`. For instance, the line breaks at `position = 1` for
+/// "a\nb" is `[0, 1)`, which does not contain the position `1`.
+class LineBoundary extends TextBoundary {
+ /// Creates a [LineBoundary] with the text and layout information.
+ const LineBoundary(this._textLayout);
final TextLayoutMetrics _textLayout;
@override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) {
- return TextPosition(
- offset: _textLayout.getWordBoundary(position).start,
- affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
- );
- }
-
- @override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- return TextPosition(
- offset: _textLayout.getWordBoundary(position).end,
- affinity: TextAffinity.upstream,
- );
- }
-
- @override
- TextRange getTextBoundaryAt(TextPosition position) {
- return _textLayout.getWordBoundary(position);
- }
+ TextRange getTextBoundaryAt(int position) => _textLayout.getLineAtOffset(TextPosition(offset: max(position, 0)));
}
-/// A text boundary that uses line breaks as logical boundaries.
+/// A text boundary that uses paragraphs as logical boundaries.
///
-/// The input [TextPosition]s will be interpreted as caret locations if
-/// [TextLayoutMetrics.getLineAtOffset] is text-affinity-aware.
-class LineBreak extends TextBoundary {
- /// Creates a [LineBreak] with the text and layout information.
- const LineBreak(this._textLayout);
+/// A paragraph is defined as the range between line terminators. If no
+/// line terminators exist then the paragraph boundary is the entire document.
+class ParagraphBoundary extends TextBoundary {
+ /// Creates a [ParagraphBoundary] with the text.
+ const ParagraphBoundary(this._text);
- final TextLayoutMetrics _textLayout;
+ final String _text;
+ /// Returns the [int] representing the start position of the paragraph that
+ /// bounds the given `position`. The returned [int] is the position of the code unit
+ /// that follows the line terminator that encloses the desired paragraph.
@override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) {
- return TextPosition(
- offset: _textLayout.getLineAtOffset(position).start,
- );
+ int? getLeadingTextBoundaryAt(int position) {
+ if (position < 0 || _text.isEmpty) {
+ return null;
+ }
+
+ if (position >= _text.length) {
+ return _text.length;
+ }
+
+ if (position == 0) {
+ return 0;
+ }
+
+ final List<int> codeUnits = _text.codeUnits;
+ int index = position;
+
+ if (index > 1 && codeUnits[index] == 0xA && codeUnits[index - 1] == 0xD) {
+ index -= 2;
+ } else if (TextLayoutMetrics.isLineTerminator(codeUnits[index])) {
+ index -= 1;
+ }
+
+ while (index > 0) {
+ if (TextLayoutMetrics.isLineTerminator(codeUnits[index])) {
+ return index + 1;
+ }
+ index -= 1;
+ }
+
+ return max(index, 0);
}
+ /// Returns the [int] representing the end position of the paragraph that
+ /// bounds the given `position`. The returned [int] is the position of the
+ /// code unit representing the trailing line terminator that encloses the
+ /// desired paragraph.
@override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- return TextPosition(
- offset: _textLayout.getLineAtOffset(position).end,
- affinity: TextAffinity.upstream,
- );
- }
+ int? getTrailingTextBoundaryAt(int position) {
+ if (position >= _text.length || _text.isEmpty) {
+ return null;
+ }
- @override
- TextRange getTextBoundaryAt(TextPosition position) {
- return _textLayout.getLineAtOffset(position);
+ if (position < 0) {
+ return 0;
+ }
+
+ final List<int> codeUnits = _text.codeUnits;
+ int index = position;
+
+ while (!TextLayoutMetrics.isLineTerminator(codeUnits[index])) {
+ index += 1;
+ if (index == codeUnits.length) {
+ return index;
+ }
+ }
+
+ return index < codeUnits.length - 1
+ && codeUnits[index] == 0xD
+ && codeUnits[index + 1] == 0xA
+ ? index + 2
+ : index + 1;
}
}
/// A text boundary that uses the entire document as logical boundary.
-///
-/// The document boundary is unique and is a constant function of the input
-/// position.
class DocumentBoundary extends TextBoundary {
- /// Creates a [DocumentBoundary] with the text
+ /// Creates a [DocumentBoundary] with the text.
const DocumentBoundary(this._text);
final String _text;
@override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) => const TextPosition(offset: 0);
+ int? getLeadingTextBoundaryAt(int position) => position < 0 ? null : 0;
@override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- return TextPosition(
- offset: _text.length,
- affinity: TextAffinity.upstream,
- );
- }
-}
-
-/// A text boundary that uses the first non-whitespace character as the logical
-/// boundary.
-///
-/// This text boundary uses [TextLayoutMetrics.isWhitespace] to identify white
-/// spaces, this includes newline characters from ASCII and separators from the
-/// [unicode separator category](https://en.wikipedia.org/wiki/Whitespace_character).
-class WhitespaceBoundary extends TextBoundary {
- /// Creates a [WhitespaceBoundary] with the text.
- const WhitespaceBoundary(this._text);
-
- final String _text;
-
- @override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) {
- // Handles outside of string end.
- if (position.offset > _text.length || (position.offset == _text.length && position.affinity == TextAffinity.downstream)) {
- position = TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
- }
- // Handles outside of string start.
- if (position.offset <= 0) {
- return const TextPosition(offset: 0);
- }
- int index = position.offset;
- if (position.affinity == TextAffinity.downstream && !TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
- return position;
- }
-
- while ((index -= 1) >= 0) {
- if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
- return TextPosition(offset: index + 1, affinity: TextAffinity.upstream);
- }
- }
- return const TextPosition(offset: 0);
- }
-
- @override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- // Handles outside of right bound.
- if (position.offset >= _text.length) {
- return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
- }
- // Handles outside of left bound.
- if (position.offset < 0 || (position.offset == 0 && position.affinity == TextAffinity.upstream)) {
- position = const TextPosition(offset: 0);
- }
-
- int index = position.offset;
- if (position.affinity == TextAffinity.upstream && !TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index - 1))) {
- return position;
- }
-
- for (; index < _text.length; index += 1) {
- if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
- return TextPosition(offset: index);
- }
- }
- return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
- }
-}
-
-/// Gets the boundary by calling the [outer] and pipe the result to
-/// [inner].
-class _ExpandedTextBoundary extends TextBoundary {
- /// Creates a [_ExpandedTextBoundary] with inner and outter boundaries
- const _ExpandedTextBoundary({required this.inner, required this.outer});
-
- /// The inner boundary to call with the result from [outer].
- final TextBoundary inner;
-
- /// The outer boundary to call with the input position.
- ///
- /// The result is piped to the [inner] before returning to the caller.
- final TextBoundary outer;
-
- @override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) {
- return inner.getLeadingTextBoundaryAt(
- outer.getLeadingTextBoundaryAt(position),
- );
- }
-
- @override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- return inner.getTrailingTextBoundaryAt(
- outer.getTrailingTextBoundaryAt(position),
- );
- }
-}
-
-/// A text boundary that will push input text position forward or backward
-/// one affinity
-///
-/// To push a text position forward one affinity unit, this proxy converts
-/// affinity to downstream if it is upstream; otherwise it increase the offset
-/// by one with its affinity sets to upstream. For example,
-/// `TextPosition(1, upstream)` becomes `TextPosition(1, downstream)`,
-/// `TextPosition(4, downstream)` becomes `TextPosition(5, upstream)`.
-///
-/// See also:
-/// * [PushTextPosition.forward], a text boundary to push the input position
-/// forward.
-/// * [PushTextPosition.backward], a text boundary to push the input position
-/// backward.
-class PushTextPosition extends TextBoundary {
- const PushTextPosition._(this._forward);
-
- /// A text boundary that pushes the input position forward.
- static const TextBoundary forward = PushTextPosition._(true);
-
- /// A text boundary that pushes the input position backward.
- static const TextBoundary backward = PushTextPosition._(false);
-
- /// Whether to push the input position forward or backward.
- final bool _forward;
-
- TextPosition _calculateTargetPosition(TextPosition position) {
- if (_forward) {
- switch(position.affinity) {
- case TextAffinity.upstream:
- return TextPosition(offset: position.offset);
- case TextAffinity.downstream:
- return position = TextPosition(
- offset: position.offset + 1,
- affinity: TextAffinity.upstream,
- );
- }
- } else {
- switch(position.affinity) {
- case TextAffinity.upstream:
- return position = TextPosition(offset: position.offset - 1);
- case TextAffinity.downstream:
- return TextPosition(
- offset: position.offset,
- affinity: TextAffinity.upstream,
- );
- }
- }
- }
-
- @override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) => _calculateTargetPosition(position);
-
- @override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) => _calculateTargetPosition(position);
+ int? getTrailingTextBoundaryAt(int position) => position >= _text.length ? null : _text.length;
}
diff --git a/framework/lib/src/services/text_editing_delta.dart b/framework/lib/src/services/text_editing_delta.dart
index a9d40ea..91872c6 100644
--- a/framework/lib/src/services/text_editing_delta.dart
+++ b/framework/lib/src/services/text_editing_delta.dart
@@ -64,9 +64,7 @@
required this.oldText,
required this.selection,
required this.composing,
- }) : assert(oldText != null),
- assert(selection != null),
- assert(composing != null);
+ });
/// Creates an instance of this class from a JSON object by inferring the
/// type of delta based on values sent from the engine.
@@ -95,7 +93,7 @@
// 'world'{replacementDestinationEnd, replacementDestinationStart + replacementSourceEnd}
// can be considered an insertion. In this case we inserted 'd'.
//
- // Similarly for a a deletion, say we are currently composing the word: 'worl'.
+ // Similarly for a deletion, say we are currently composing the word: 'worl'.
// Our current state is 'world|' with the cursor at the end of 'd'. If we
// press backspace to delete the character 'd', the platform will tell us 'world'
// was replaced with 'worl' at range (0,5). Here we can check if the text found
diff --git a/framework/lib/src/services/text_formatter.dart b/framework/lib/src/services/text_formatter.dart
index 8e5c647..c22cebb 100644
--- a/framework/lib/src/services/text_formatter.dart
+++ b/framework/lib/src/services/text_formatter.dart
@@ -88,6 +88,9 @@
/// * [FilteringTextInputFormatter], a provided formatter for filtering
/// characters.
abstract class TextInputFormatter {
+ /// This constructor enables subclasses to provide const constructors so that they can be used in const expressions.
+ const TextInputFormatter();
+
/// Called when text is being typed or cut/copy/pasted in the [EditableText].
///
/// You can override the resulting text based on the previous text value and
@@ -118,8 +121,7 @@
/// Wiring for [TextInputFormatter.withFunction].
class _SimpleTextInputFormatter extends TextInputFormatter {
- _SimpleTextInputFormatter(this.formatFunction)
- : assert(formatFunction != null);
+ _SimpleTextInputFormatter(this.formatFunction);
final TextInputFormatFunction formatFunction;
@@ -268,9 +270,7 @@
this.filterPattern, {
required this.allow,
this.replacementString = '',
- }) : assert(filterPattern != null),
- assert(allow != null),
- assert(replacementString != null);
+ });
/// Creates a formatter that only allows characters matching a pattern.
///
diff --git a/framework/lib/src/services/text_input.dart b/framework/lib/src/services/text_input.dart
index cf7b494..d244c2a 100644
--- a/framework/lib/src/services/text_input.dart
+++ b/framework/lib/src/services/text_input.dart
@@ -17,6 +17,7 @@
import 'autofill.dart';
import 'clipboard.dart' show Clipboard;
+import 'keyboard_inserted_content.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
@@ -477,18 +478,10 @@
this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration = AutofillConfiguration.disabled,
this.enableIMEPersonalizedLearning = true,
+ this.allowedMimeTypes = const <String>[],
this.enableDeltaModel = false,
- }) : assert(inputType != null),
- assert(obscureText != null),
- smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
- smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
- assert(autocorrect != null),
- assert(enableSuggestions != null),
- assert(keyboardAppearance != null),
- assert(inputAction != null),
- assert(textCapitalization != null),
- assert(enableIMEPersonalizedLearning != null),
- assert(enableDeltaModel != null);
+ }) : smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
+ smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled);
/// The type of information for which to optimize the text input control.
final TextInputType inputType;
@@ -627,6 +620,9 @@
/// {@endtemplate}
final bool enableIMEPersonalizedLearning;
+ /// {@macro flutter.widgets.contentInsertionConfiguration.allowedMimeTypes}
+ final List<String> allowedMimeTypes;
+
/// Creates a copy of this [TextInputConfiguration] with the given fields
/// replaced with new values.
TextInputConfiguration copyWith({
@@ -643,6 +639,7 @@
Brightness? keyboardAppearance,
TextCapitalization? textCapitalization,
bool? enableIMEPersonalizedLearning,
+ List<String>? allowedMimeTypes,
AutofillConfiguration? autofillConfiguration,
bool? enableDeltaModel,
}) {
@@ -659,6 +656,7 @@
textCapitalization: textCapitalization ?? this.textCapitalization,
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
+ allowedMimeTypes: allowedMimeTypes ?? this.allowedMimeTypes,
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
enableDeltaModel: enableDeltaModel ?? this.enableDeltaModel,
);
@@ -706,6 +704,7 @@
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
+ 'contentCommitMimeTypes': allowedMimeTypes,
if (autofill != null) 'autofill': autofill,
'enableDeltaModel' : enableDeltaModel,
};
@@ -746,8 +745,7 @@
RawFloatingCursorPoint({
this.offset,
required this.state,
- }) : assert(state != null),
- assert(state != FloatingCursorDragState.Update || offset != null);
+ }) : assert(state != FloatingCursorDragState.Update || offset != null);
/// The raw position of the floating cursor as determined by the iOS sdk.
final Offset? offset;
@@ -773,13 +771,7 @@
this.text = '',
this.selection = const TextSelection.collapsed(offset: -1),
this.composing = TextRange.empty,
- }) : assert(text != null),
- // The constructor does not verify that `selection` and `composing` are
- // valid ranges within `text`, and is unable to do so due to limitation
- // of const constructors. Some checks are performed by assertion in
- // other occasions. See `_textRangeIsValid`.
- assert(selection != null),
- assert(composing != null);
+ });
/// Creates an instance of this class from a JSON object.
factory TextEditingValue.fromJSON(Map<String, dynamic> encoded) {
@@ -1121,6 +1113,9 @@
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
+ /// Notify client about new content insertion from Android keyboard.
+ void insertContent(KeyboardInsertedContent content) {}
+
/// Request from the input method that this client perform the given private
/// command.
///
@@ -1211,7 +1206,11 @@
class SelectionRect {
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
- const SelectionRect({required this.position, required this.bounds});
+ const SelectionRect({
+ required this.position,
+ required this.bounds,
+ this.direction = TextDirection.ltr,
+ });
/// The position of this selection rect within the text String.
final int position;
@@ -1220,6 +1219,9 @@
/// currently focused [RenderEditable]'s coordinate space.
final Rect bounds;
+ /// The direction text flows within this selection rect.
+ final TextDirection direction;
+
@override
bool operator ==(Object other) {
if (identical(this, other)) {
@@ -1230,7 +1232,8 @@
}
return other is SelectionRect
&& other.position == position
- && other.bounds == bounds;
+ && other.bounds == bounds
+ && other.direction == direction;
}
@override
@@ -1292,8 +1295,7 @@
/// the system's text input using a [TextInputConnection].
class TextInputConnection {
TextInputConnection._(this._client)
- : assert(_client != null),
- _id = _nextId++;
+ : _id = _nextId++;
Size? _cachedSize;
Matrix4? _cachedTransform;
@@ -1310,7 +1312,6 @@
/// application code will likely break text input for the application.
@visibleForTesting
static void debugResetId({int to = 1}) {
- assert(to != null);
assert(() {
_nextId = to;
return true;
@@ -1387,7 +1388,6 @@
/// This information is used for positioning the IME candidates menu on each
/// platform.
void setComposingRect(Rect rect) {
- assert(rect != null);
if (rect == _cachedRect) {
return;
}
@@ -1399,7 +1399,6 @@
/// Sends the coordinates of caret rect. This is used on macOS for positioning
/// the accent selection menu.
void setCaretRect(Rect rect) {
- assert(rect != null);
if (rect == _cachedCaretRect) {
return;
}
@@ -1507,7 +1506,6 @@
}
RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) {
- assert(state != null, 'You must provide a state to set a new editing point.');
assert(encoded['X'] != null, 'You must provide a value for the horizontal location of the floating cursor.');
assert(encoded['Y'] != null, 'You must provide a value for the vertical location of the floating cursor.');
final Offset offset = state == FloatingCursorDragState.Update
@@ -1691,8 +1689,6 @@
/// should call [TextInputConnection.close] on the returned
/// [TextInputConnection].
static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
- assert(client != null);
- assert(configuration != null);
final TextInputConnection connection = TextInputConnection._(client);
_instance._attach(connection, configuration);
return connection;
@@ -1702,9 +1698,6 @@
// by [attach] and by [_handleTextInputInvocation] for the
// `TextInputClient.requestExistingInputState` method.
void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
- assert(connection != null);
- assert(connection._client != null);
- assert(configuration != null);
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
_currentConnection = connection;
_currentConfiguration = configuration;
@@ -1798,7 +1791,6 @@
// The requestExistingInputState request needs to be handled regardless of
// the client ID, as long as we have a _currentConnection.
if (method == 'TextInputClient.requestExistingInputState') {
- assert(_currentConnection!._client != null);
_attach(_currentConnection!, _currentConfiguration);
final TextEditingValue? editingValue = _currentConnection!._client.currentTextEditingValue;
if (editingValue != null) {
@@ -1812,7 +1804,6 @@
// The updateEditingStateWithTag request (autofill) can come up even to a
// text field that doesn't have a connection.
if (method == 'TextInputClient.updateEditingStateWithTag') {
- assert(_currentConnection!._client != null);
final TextInputClient client = _currentConnection!._client;
final AutofillScope? scope = client.currentAutofillScope;
final Map<String, dynamic> editingValue = args[1] as Map<String, dynamic>;
@@ -1867,7 +1858,12 @@
(_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
break;
case 'TextInputClient.performAction':
- _currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
+ if (args[1] as String == 'TextInputAction.commitContent') {
+ final KeyboardInsertedContent content = KeyboardInsertedContent.fromJson(args[2] as Map<String, dynamic>);
+ _currentConnection!._client.insertContent(content);
+ } else {
+ _currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
+ }
break;
case 'TextInputClient.performSelectors':
final List<String> selectors = (args[1] as List<dynamic>).cast<String>();
@@ -1943,14 +1939,12 @@
}
void _updateConfig(TextInputConfiguration configuration) {
- assert(configuration != null);
for (final TextInputControl control in _inputControls) {
control.updateConfig(configuration);
}
}
void _setEditingState(TextEditingValue value) {
- assert(value != null);
for (final TextInputControl control in _inputControls) {
control.setEditingState(value);
}
@@ -2086,7 +2080,6 @@
/// * [AutofillGroup.onDisposeAction], a configurable action that runs when a
/// topmost [AutofillGroup] is getting disposed.
static void finishAutofillContext({ bool shouldSave = true }) {
- assert(shouldSave != null);
for (final TextInputControl control in TextInput._instance._inputControls) {
control.finishAutofillContext(shouldSave: shouldSave);
}
@@ -2321,7 +2314,14 @@
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
selectionRects.map((SelectionRect rect) {
- return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
+ return <num>[
+ rect.bounds.left,
+ rect.bounds.top,
+ rect.bounds.width,
+ rect.bounds.height,
+ rect.position,
+ rect.direction.index,
+ ];
}).toList(),
);
}
diff --git a/framework/lib/src/services/text_layout_metrics.dart b/framework/lib/src/services/text_layout_metrics.dart
index fdaf5a6..6ae774a 100644
--- a/framework/lib/src/services/text_layout_metrics.dart
+++ b/framework/lib/src/services/text_layout_metrics.dart
@@ -54,6 +54,25 @@
return true;
}
+ /// Check if the given code unit is a line terminator character.
+ ///
+ /// Includes newline characters from ASCII
+ /// (https://www.unicode.org/standard/reports/tr13/tr13-5.html).
+ static bool isLineTerminator(int codeUnit) {
+ switch (codeUnit) {
+ case 0xA: // line feed
+ case 0xB: // vertical feed
+ case 0xC: // form feed
+ case 0xD: // carriage return
+ case 0x85: // new line
+ case 0x2028: // line separator
+ case 0x2029: // paragraph separator
+ return true;
+ default:
+ return false;
+ }
+ }
+
/// {@template flutter.services.TextLayoutMetrics.getLineAtOffset}
/// Return a [TextSelection] containing the line of the given [TextPosition].
/// {@endtemplate}
diff --git a/framework/lib/src/widgets/actions.dart b/framework/lib/src/widgets/actions.dart
index b7b699c..4987c41 100644
--- a/framework/lib/src/widgets/actions.dart
+++ b/framework/lib/src/widgets/actions.dart
@@ -256,13 +256,15 @@
/// This is typically used when the action is invoked in response to a keyboard
/// shortcut.
///
+ /// The [invokeResult] argument is the value returned by the [invoke] method.
+ ///
/// By default, calls [consumesKey] and converts the returned boolean to
/// [KeyEventResult.handled] if it's true, and [KeyEventResult.skipRemainingHandlers]
/// if it's false.
///
- /// Concrete implementations may refine the type of [actionResult], since
- /// they know type returned by [invoke].
- KeyEventResult toKeyEventResult(T intent, covariant Object? actionResult) {
+ /// Concrete implementations may refine the type of [invokeResult], since
+ /// they know the type returned by [invoke].
+ KeyEventResult toKeyEventResult(T intent, covariant Object? invokeResult) {
return consumesKey(intent)
? KeyEventResult.handled
: KeyEventResult.skipRemainingHandlers;
@@ -429,9 +431,7 @@
required this.listener,
required this.action,
required this.child,
- }) : assert(listener != null),
- assert(action != null),
- assert(child != null);
+ });
/// The [ActionListenerCallback] callback to register with the [action].
///
@@ -546,7 +546,7 @@
///
/// The `intentKey` and [onInvoke] parameters must not be null.
/// The [onInvoke] parameter is required.
- CallbackAction({required this.onInvoke}) : assert(onInvoke != null);
+ CallbackAction({required this.onInvoke});
/// The callback to be called when invoked.
///
@@ -558,7 +558,13 @@
Object? invoke(T intent) => onInvoke(intent);
}
-/// An action dispatcher that simply invokes the actions given to it.
+/// An action dispatcher that invokes the actions given to it.
+///
+/// The [invokeAction] method on this class directly calls the [Action.invoke]
+/// method on the [Action] object.
+///
+/// For [ContextAction] actions, if no `context` is provided, the
+/// [BuildContext] of the [primaryFocus] is used instead.
///
/// See also:
///
@@ -587,8 +593,6 @@
covariant Intent intent, [
BuildContext? context,
]) {
- assert(action != null);
- assert(intent != null);
assert(action.isEnabled(intent), 'Action must be enabled when calling invokeAction');
if (action is ContextAction) {
context ??= primaryFocus?.context;
@@ -643,8 +647,7 @@
this.dispatcher,
required this.actions,
required this.child,
- }) : assert(actions != null),
- assert(child != null);
+ });
/// The [ActionDispatcher] object that invokes actions.
///
@@ -673,7 +676,7 @@
// getElementForInheritedWidgetOfExactType. Returns true if the visitor found
// what it was looking for.
static bool _visitActionsAncestors(BuildContext context, bool Function(InheritedElement element) visitor) {
- InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
+ InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsScope>();
while (actionsElement != null) {
if (visitor(actionsElement) == true) {
break;
@@ -682,7 +685,7 @@
// context.getElementForInheritedWidgetOfExactType will return itself if it
// happens to be of the correct type.
final BuildContext parent = _getParent(actionsElement);
- actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
+ actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsScope>();
}
return actionsElement != null;
}
@@ -692,7 +695,7 @@
static ActionDispatcher _findDispatcher(BuildContext context) {
ActionDispatcher? dispatcher;
_visitActionsAncestors(context, (InheritedElement element) {
- final ActionDispatcher? found = (element.widget as _ActionsMarker).dispatcher;
+ final ActionDispatcher? found = (element.widget as _ActionsScope).dispatcher;
if (found != null) {
dispatcher = found;
return true;
@@ -799,7 +802,7 @@
);
_visitActionsAncestors(context, (InheritedElement element) {
- final _ActionsMarker actions = element.widget as _ActionsMarker;
+ final _ActionsScope actions = element.widget as _ActionsScope;
final Action<T>? result = _castAction(actions, intent: intent);
if (result != null) {
context.dependOnInheritedElement(element);
@@ -828,7 +831,7 @@
);
_visitActionsAncestors(context, (InheritedElement element) {
- final _ActionsMarker actions = element.widget as _ActionsMarker;
+ final _ActionsScope actions = element.widget as _ActionsScope;
final Action<T>? result = _castAction(actions, intent: intent);
if (result != null) {
action = result;
@@ -841,8 +844,8 @@
}
// Find the [Action] that handles the given `intent` in the given
- // `_ActionsMarker`, and verify it has the right type parameter.
- static Action<T>? _castAction<T extends Intent>(_ActionsMarker actionsMarker, { T? intent }) {
+ // `_ActionsScope`, and verify it has the right type parameter.
+ static Action<T>? _castAction<T extends Intent>(_ActionsScope actionsMarker, { T? intent }) {
final Action<Intent>? mappedAction = actionsMarker.actions[intent?.runtimeType ?? T];
if (mappedAction is Action<T>?) {
return mappedAction;
@@ -861,8 +864,7 @@
/// Will return a newly created [ActionDispatcher] if no ambient [Actions]
/// widget is found.
static ActionDispatcher of(BuildContext context) {
- assert(context != null);
- final _ActionsMarker? marker = context.dependOnInheritedWidgetOfExactType<_ActionsMarker>();
+ final _ActionsScope? marker = context.dependOnInheritedWidgetOfExactType<_ActionsScope>();
return marker?.dispatcher ?? _findDispatcher(context);
}
@@ -884,12 +886,10 @@
BuildContext context,
T intent,
) {
- assert(intent != null);
- assert(context != null);
Object? returnValue;
final bool actionFound = _visitActionsAncestors(context, (InheritedElement element) {
- final _ActionsMarker actions = element.widget as _ActionsMarker;
+ final _ActionsScope actions = element.widget as _ActionsScope;
final Action<T>? result = _castAction(actions, intent: intent);
if (result != null && result.isEnabled(intent)) {
// Invoke the action we found using the relevant dispatcher from the Actions
@@ -937,16 +937,14 @@
BuildContext context,
T intent,
) {
- assert(intent != null);
- assert(context != null);
Object? returnValue;
_visitActionsAncestors(context, (InheritedElement element) {
- final _ActionsMarker actions = element.widget as _ActionsMarker;
+ final _ActionsScope actions = element.widget as _ActionsScope;
final Action<T>? result = _castAction(actions, intent: intent);
if (result != null && result.isEnabled(intent)) {
// Invoke the action we found using the relevant dispatcher from the Actions
- // Element we found.
+ // element we found.
returnValue = _findDispatcher(element).invokeAction(result, intent, context);
}
return result != null;
@@ -1016,7 +1014,7 @@
@override
Widget build(BuildContext context) {
- return _ActionsMarker(
+ return _ActionsScope(
actions: widget.actions,
dispatcher: widget.dispatcher,
rebuildKey: rebuildKey,
@@ -1027,21 +1025,20 @@
// An inherited widget used by Actions widget for fast lookup of the Actions
// widget information.
-class _ActionsMarker extends InheritedWidget {
- const _ActionsMarker({
+class _ActionsScope extends InheritedWidget {
+ const _ActionsScope({
required this.dispatcher,
required this.actions,
required this.rebuildKey,
required super.child,
- }) : assert(child != null),
- assert(actions != null);
+ });
final ActionDispatcher? dispatcher;
final Map<Type, Action<Intent>> actions;
final Object rebuildKey;
@override
- bool updateShouldNotify(_ActionsMarker oldWidget) {
+ bool updateShouldNotify(_ActionsScope oldWidget) {
return rebuildKey != oldWidget.rebuildKey
|| oldWidget.dispatcher != dispatcher
|| !mapEquals<Type, Action<Intent>>(oldWidget.actions, actions);
@@ -1098,10 +1095,7 @@
this.mouseCursor = MouseCursor.defer,
this.includeFocusSemantics = true,
required this.child,
- }) : assert(enabled != null),
- assert(autofocus != null),
- assert(mouseCursor != null),
- assert(child != null);
+ });
/// Is this widget enabled or not.
///
@@ -1247,7 +1241,7 @@
}
bool canRequestFocus(FocusableActionDetector target) {
- final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
+ final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return target.enabled;
@@ -1288,7 +1282,7 @@
}
bool get _canRequestFocus {
- final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
+ final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
switch (mode) {
case NavigationMode.traditional:
return widget.enabled;
@@ -1470,7 +1464,8 @@
/// * [WidgetsApp.shortcuts], which defines the shortcuts to use in an
/// application (and defaults to [WidgetsApp.defaultShortcuts]).
class ButtonActivateIntent extends Intent {
- /// Creates an intent that the currently focused control, if it's a button.
+ /// Creates an intent that activates the currently focused control,
+ /// if it's a button.
const ButtonActivateIntent();
}
@@ -1521,7 +1516,7 @@
/// of intents, the first available of which will be used.
const PrioritizedIntents({
required this.orderedIntents,
- }) : assert(orderedIntents != null);
+ });
/// List of intents to be evaluated in order for execution. When an
/// [Action.isEnabled] returns true, that action will be invoked and
@@ -1557,8 +1552,6 @@
@override
void invoke(PrioritizedIntents intent) {
- assert(_selectedAction != null);
- assert(_selectedIntent != null);
_selectedAction.invoke(_selectedIntent);
}
}
@@ -1573,7 +1566,7 @@
bool debugAssertConsumeKeyMutuallyRecursive = false;
// The default action to invoke if an enabled override Action can't be found
- // using [lookupContext];
+ // using [lookupContext].
Action<T> get defaultAction;
// The [BuildContext] used to find the override of this [Action].
diff --git a/framework/lib/src/widgets/animated_cross_fade.dart b/framework/lib/src/widgets/animated_cross_fade.dart
index 2d6b8af..543d9a8 100644
--- a/framework/lib/src/widgets/animated_cross_fade.dart
+++ b/framework/lib/src/widgets/animated_cross_fade.dart
@@ -130,16 +130,7 @@
this.reverseDuration,
this.layoutBuilder = defaultLayoutBuilder,
this.excludeBottomFocus = true,
- }) : assert(firstChild != null),
- assert(secondChild != null),
- assert(firstCurve != null),
- assert(secondCurve != null),
- assert(sizeCurve != null),
- assert(alignment != null),
- assert(crossFadeState != null),
- assert(duration != null),
- assert(layoutBuilder != null),
- assert(excludeBottomFocus != null);
+ });
/// The child that is visible when [crossFadeState] is
/// [CrossFadeState.showFirst]. It fades out when transitioning
diff --git a/framework/lib/src/widgets/animated_scroll_view.dart b/framework/lib/src/widgets/animated_scroll_view.dart
index 649d4aa..7f40664 100644
--- a/framework/lib/src/widgets/animated_scroll_view.dart
+++ b/framework/lib/src/widgets/animated_scroll_view.dart
@@ -52,8 +52,7 @@
super.shrinkWrap = false,
super.padding,
super.clipBehavior = Clip.hardEdge,
- }) : assert(itemBuilder != null),
- assert(initialItemCount != null && initialItemCount >= 0);
+ }) : assert(initialItemCount >= 0);
/// The state from the closest instance of this class that encloses the given
/// context.
@@ -74,7 +73,6 @@
/// * [maybeOf], a similar function that will return null if no
/// [AnimatedList] ancestor is found.
static AnimatedListState of(BuildContext context) {
- assert(context != null);
final AnimatedListState? result = AnimatedList.maybeOf(context);
assert(() {
if (result == null) {
@@ -116,7 +114,6 @@
/// * [of], a similar function that will throw if no [AnimatedList] ancestor
/// is found.
static AnimatedListState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<AnimatedListState>();
}
@@ -131,6 +128,10 @@
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
/// is needed.
///
+/// When multiple items are inserted with [insertAllItems] an animation begins running.
+/// The animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
+/// is needed.
+///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
@@ -217,8 +218,7 @@
super.physics,
super.padding,
super.clipBehavior = Clip.hardEdge,
- }) : assert(itemBuilder != null),
- assert(initialItemCount != null && initialItemCount >= 0);
+ }) : assert(initialItemCount >= 0);
/// {@template flutter.widgets.AnimatedGrid.gridDelegate}
/// A delegate that controls the layout of the children within the
@@ -252,7 +252,6 @@
/// * [maybeOf], a similar function that will return null if no
/// [AnimatedGrid] ancestor is found.
static AnimatedGridState of(BuildContext context) {
- assert(context != null);
final AnimatedGridState? result = AnimatedGrid.maybeOf(context);
assert(() {
if (result == null) {
@@ -294,7 +293,6 @@
/// * [of], a similar function that will throw if no [AnimatedGrid] ancestor
/// is found.
static AnimatedGridState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<AnimatedGridState>();
}
@@ -373,8 +371,7 @@
this.shrinkWrap = false,
this.padding,
this.clipBehavior = Clip.hardEdge,
- }) : assert(itemBuilder != null),
- assert(initialItemCount != null && initialItemCount >= 0);
+ }) : assert(initialItemCount >= 0);
/// {@template flutter.widgets.AnimatedScrollView.itemBuilder}
/// Called, as needed, to build children widgets.
@@ -493,6 +490,13 @@
_sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration);
}
+ /// Insert multiple items at [index] and start an animation that will be passed
+ /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
+ /// are visible.
+ void insertAllItems(int index, int length, { Duration duration = _kDuration, bool isAsync = false }) {
+ _sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration);
+ }
+
/// Remove the item at `index` and start an animation that will be passed to
/// `builder` when the item is visible.
///
@@ -513,6 +517,19 @@
_sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
}
+ /// Remove all the items and start an animation that will be passed to
+ /// `builder` when the items are visible.
+ ///
+ /// Items are removed immediately. However, the
+ /// items will still appear for `duration`, and during that time
+ /// `builder` must construct its widget as needed.
+ ///
+ /// This method's semantics are the same as Dart's [List.clear] method: it
+ /// removes all the items in the list.
+ void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
+ _sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
+ }
+
Widget _wrap(Widget sliver) {
return CustomScrollView(
scrollDirection: widget.scrollDirection,
@@ -630,8 +647,7 @@
required super.itemBuilder,
super.findChildIndexCallback,
super.initialItemCount = 0,
- }) : assert(itemBuilder != null),
- assert(initialItemCount != null && initialItemCount >= 0);
+ }) : assert(initialItemCount >= 0);
@override
SliverAnimatedListState createState() => SliverAnimatedListState();
@@ -655,7 +671,6 @@
/// * [maybeOf], a similar function that will return null if no
/// [SliverAnimatedList] ancestor is found.
static SliverAnimatedListState of(BuildContext context) {
- assert(context != null);
final SliverAnimatedListState? result = SliverAnimatedList.maybeOf(context);
assert(() {
if (result == null) {
@@ -695,7 +710,6 @@
/// * [of], a similar function that will throw if no [SliverAnimatedList]
/// ancestor is found.
static SliverAnimatedListState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<SliverAnimatedListState>();
}
}
@@ -782,8 +796,7 @@
required this.gridDelegate,
super.findChildIndexCallback,
super.initialItemCount = 0,
- }) : assert(itemBuilder != null),
- assert(initialItemCount != null && initialItemCount >= 0);
+ }) : assert(initialItemCount >= 0);
@override
SliverAnimatedGridState createState() => SliverAnimatedGridState();
@@ -807,7 +820,6 @@
/// * [maybeOf], a similar function that will return null if no
/// [SliverAnimatedGrid] ancestor is found.
static SliverAnimatedGridState of(BuildContext context) {
- assert(context != null);
final SliverAnimatedGridState? result = context.findAncestorStateOfType<SliverAnimatedGridState>();
assert(() {
if (result == null) {
@@ -844,7 +856,6 @@
/// * [of], a similar function that will throw if no [SliverAnimatedGrid]
/// ancestor is found.
static SliverAnimatedGridState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<SliverAnimatedGridState>();
}
}
@@ -908,8 +919,7 @@
required this.itemBuilder,
this.findChildIndexCallback,
this.initialItemCount = 0,
- }) : assert(itemBuilder != null),
- assert(initialItemCount != null && initialItemCount >= 0);
+ }) : assert(initialItemCount >= 0);
/// {@macro flutter.widgets.AnimatedScrollView.itemBuilder}
final AnimatedItemBuilder itemBuilder;
@@ -1022,8 +1032,7 @@
/// increases the length of the list of items by one and shifts
/// all items at or after [index] towards the end of the list of items.
void insertItem(int index, { Duration duration = _kDuration }) {
- assert(index != null && index >= 0);
- assert(duration != null);
+ assert(index >= 0);
final int itemIndex = _indexToItemIndex(index);
assert(itemIndex >= 0 && itemIndex <= _itemsCount);
@@ -1061,6 +1070,15 @@
});
}
+ /// Insert multiple items at [index] and start an animation that will be passed
+ /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
+ /// are visible.
+ void insertAllItems(int index, int length, { Duration duration = _kDuration }) {
+ for (int i = 0; i < length; i++) {
+ insertItem(index + i, duration: duration);
+ }
+ }
+
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
@@ -1074,9 +1092,7 @@
/// decreases the length of items by one and shifts
/// all items at or before [index] towards the beginning of the list of items.
void removeItem(int index, AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
- assert(index != null && index >= 0);
- assert(builder != null);
- assert(duration != null);
+ assert(index >= 0);
final int itemIndex = _indexToItemIndex(index);
assert(itemIndex >= 0 && itemIndex < _itemsCount);
@@ -1111,4 +1127,19 @@
setState(() => _itemsCount -= 1);
});
}
+
+ /// Remove all the items and start an animation that will be passed to
+ /// `builder` when the items are visible.
+ ///
+ /// Items are removed immediately. However, the
+ /// items will still appear for `duration` and during that time
+ /// `builder` must construct its widget as needed.
+ ///
+ /// This method's semantics are the same as Dart's [List.clear] method: it
+ /// removes all the items in the list.
+ void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
+ for(int i = _itemsCount - 1 ; i >= 0; i--) {
+ removeItem(i, builder, duration: duration);
+ }
+ }
}
diff --git a/framework/lib/src/widgets/animated_size.dart b/framework/lib/src/widgets/animated_size.dart
index b8a2b0b..b14cabb 100644
--- a/framework/lib/src/widgets/animated_size.dart
+++ b/framework/lib/src/widgets/animated_size.dart
@@ -32,13 +32,8 @@
this.curve = Curves.linear,
required this.duration,
this.reverseDuration,
- @Deprecated(
- 'This field is now ignored. '
- 'This feature was deprecated after v2.2.0-10.1.pre.'
- )
- TickerProvider? vsync,
this.clipBehavior = Clip.hardEdge,
- }) : assert(clipBehavior != null);
+ });
/// The widget below this widget in the tree.
///
@@ -114,7 +109,7 @@
this.reverseDuration,
required this.vsync,
this.clipBehavior = Clip.hardEdge,
- }) : assert(clipBehavior != null);
+ });
final AlignmentGeometry alignment;
final Curve curve;
diff --git a/framework/lib/src/widgets/animated_switcher.dart b/framework/lib/src/widgets/animated_switcher.dart
index fe8be50..5f3ccc7 100644
--- a/framework/lib/src/widgets/animated_switcher.dart
+++ b/framework/lib/src/widgets/animated_switcher.dart
@@ -19,9 +19,7 @@
required this.animation,
required this.transition,
required this.widgetChild,
- }) : assert(animation != null),
- assert(transition != null),
- assert(controller != null);
+ });
// The animation controller for the child's transition.
final AnimationController controller;
@@ -117,11 +115,7 @@
this.switchOutCurve = Curves.linear,
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
- }) : assert(duration != null),
- assert(switchInCurve != null),
- assert(switchOutCurve != null),
- assert(transitionBuilder != null),
- assert(layoutBuilder != null);
+ });
/// The current child widget to display. If there was a previous child, then
/// that child will be faded out using the [switchOutCurve], while the new
diff --git a/framework/lib/src/widgets/annotated_region.dart b/framework/lib/src/widgets/annotated_region.dart
index 8f7603e..e55e2ca 100644
--- a/framework/lib/src/widgets/annotated_region.dart
+++ b/framework/lib/src/widgets/annotated_region.dart
@@ -24,8 +24,7 @@
required Widget super.child,
required this.value,
this.sized = true,
- }) : assert(value != null),
- assert(child != null);
+ });
/// A value which can be retrieved using [Layer.find].
final T value;
diff --git a/framework/lib/src/widgets/app.dart b/framework/lib/src/widgets/app.dart
index 820ef11..1791729 100644
--- a/framework/lib/src/widgets/app.dart
+++ b/framework/lib/src/widgets/app.dart
@@ -241,9 +241,6 @@
/// It is used by both [MaterialApp] and [CupertinoApp] to implement base
/// functionality for an app.
///
-/// Builds a [MediaQuery] using [MediaQuery.fromWindow]. To use an inherited
-/// [MediaQuery] instead, set [useInheritedMediaQuery] to true.
-///
/// Find references to many of the widgets that [WidgetsApp] wraps in the "See
/// also" section.
///
@@ -343,10 +340,13 @@
this.shortcuts,
this.actions,
this.restorationScopeId,
+ @Deprecated(
+ 'Remove this parameter as it is now ignored. '
+ 'WidgetsApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
this.useInheritedMediaQuery = false,
- }) : assert(navigatorObservers != null),
- assert(routes != null),
- assert(
+ }) : assert(
home == null ||
onGenerateInitialRoutes == null,
'If onGenerateInitialRoutes is specified, the home argument will be '
@@ -398,15 +398,7 @@
'pageRouteBuilder must be specified so that the default handler '
'will know what kind of PageRoute transition to build.',
),
- assert(title != null),
- assert(color != null),
- assert(supportedLocales != null && supportedLocales.isNotEmpty),
- assert(showPerformanceOverlay != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- assert(showSemanticsDebugger != null),
- assert(debugShowCheckedModeBanner != null),
- assert(debugShowWidgetInspector != null),
+ assert(supportedLocales.isNotEmpty),
routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
@@ -447,6 +439,11 @@
this.shortcuts,
this.actions,
this.restorationScopeId,
+ @Deprecated(
+ 'Remove this parameter as it is now ignored. '
+ 'WidgetsApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
this.useInheritedMediaQuery = false,
}) : assert((){
if (routerConfig != null) {
@@ -463,15 +460,7 @@
);
return true;
}()),
- assert(title != null),
- assert(color != null),
- assert(supportedLocales != null && supportedLocales.isNotEmpty),
- assert(showPerformanceOverlay != null),
- assert(checkerboardRasterCacheImages != null),
- assert(checkerboardOffscreenLayers != null),
- assert(showSemanticsDebugger != null),
- assert(debugShowCheckedModeBanner != null),
- assert(debugShowWidgetInspector != null),
+ assert(supportedLocales.isNotEmpty),
navigatorObservers = null,
navigatorKey = null,
onGenerateRoute = null,
@@ -1171,11 +1160,16 @@
final String? restorationScopeId;
/// {@template flutter.widgets.widgetsApp.useInheritedMediaQuery}
- /// If true, an inherited MediaQuery will be used. If one is not available,
- /// or this is false, one will be built from the window.
+ /// Deprecated. This setting is not ignored.
///
- /// Cannot be null, defaults to false.
+ /// The widget never introduces its own [MediaQuery]; the [View] widget takes
+ /// care of that.
/// {@endtemplate}
+ @Deprecated(
+ 'This setting is now ignored. '
+ 'WidgetsApp never introduces its own MediaQuery; the View widget takes care of that. '
+ 'This feature was deprecated after v3.7.0-29.0.pre.'
+ )
final bool useInheritedMediaQuery;
/// If true, forces the performance overlay to be visible in all instances.
@@ -1432,7 +1426,6 @@
settings,
pageContentBuilder,
);
- assert(route != null, 'The pageRouteBuilder for WidgetsApp must return a valid non-null Route.');
return route;
}
if (widget.onGenerateRoute != null) {
@@ -1722,7 +1715,6 @@
// parameter.
builder: (BuildContext context) {
final String title = widget.onGenerateTitle!(context);
- assert(title != null, 'onGenerateTitle must return a non-null String');
return Title(
title: title,
color: widget.color.withOpacity(1.0),
@@ -1744,19 +1736,6 @@
assert(_debugCheckLocalizations(appLocale));
- Widget child = Localizations(
- locale: appLocale,
- delegates: _localizationsDelegates.toList(),
- child: title,
- );
-
- final MediaQueryData? data = MediaQuery.maybeOf(context);
- if (!widget.useInheritedMediaQuery || data == null) {
- child = MediaQuery.fromWindow(
- child: child,
- );
- }
-
return RootRestorationScope(
restorationId: widget.restorationScopeId,
child: SharedAppData(
@@ -1775,7 +1754,11 @@
policy: ReadingOrderTraversalPolicy(),
child: TapRegionSurface(
child: ShortcutRegistrar(
- child: child,
+ child: Localizations(
+ locale: appLocale,
+ delegates: _localizationsDelegates.toList(),
+ child: title,
+ ),
),
),
),
diff --git a/framework/lib/src/widgets/async.dart b/framework/lib/src/widgets/async.dart
index cf99291..fe2b233 100644
--- a/framework/lib/src/widgets/async.dart
+++ b/framework/lib/src/widgets/async.dart
@@ -2,10 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-/// Widgets that handle interaction with asynchronous computations.
-///
-/// Asynchronous computations are represented by [Future]s and [Stream]s.
-
import 'dart:async' show StreamSubscription;
import 'package:flute/foundation.dart';
@@ -204,8 +200,7 @@
/// and optionally either [data] or [error] with an optional [stackTrace]
/// (but not both data and error).
const AsyncSnapshot._(this.connectionState, this.data, this.error, this.stackTrace)
- : assert(connectionState != null),
- assert(!(data != null && error != null)),
+ : assert(!(data != null && error != null)),
assert(stackTrace == null || error != null);
/// Creates an [AsyncSnapshot] in [ConnectionState.none] with null data and error.
@@ -398,7 +393,7 @@
this.initialData,
super.stream,
required this.builder,
- }) : assert(builder != null);
+ });
/// The build strategy currently used by this builder.
///
@@ -444,11 +439,15 @@
Widget build(BuildContext context, AsyncSnapshot<T> currentSummary) => builder(context, currentSummary);
}
-/// Widget that builds itself based on the latest snapshot of interaction with
+/// A widget that builds itself based on the latest snapshot of interaction with
/// a [Future].
///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=ek8ZPdWj4Qo}
+///
/// {@youtube 560 315 https://www.youtube.com/watch?v=zEdw_1B7JHY}
///
+/// ## Managing the future
+///
/// The [future] must have been obtained earlier, e.g. during [State.initState],
/// [State.didUpdateWidget], or [State.didChangeDependencies]. It must not be
/// created during the [State.build] or [StatelessWidget.build] method call when
@@ -459,8 +458,6 @@
/// A general guideline is to assume that every `build` method could get called
/// every frame, and to treat omitted calls as an optimization.
///
-/// {@youtube 560 315 https://www.youtube.com/watch?v=ek8ZPdWj4Qo}
-///
/// ## Timing
///
/// Widget rebuilding is scheduled by the completion of the future, using
@@ -530,7 +527,7 @@
this.future,
this.initialData,
required this.builder,
- }) : assert(builder != null);
+ });
/// The asynchronous computation to which this builder is currently connected,
/// possibly null.
diff --git a/framework/lib/src/widgets/autocomplete.dart b/framework/lib/src/widgets/autocomplete.dart
index f34b9b5..a6aea13 100644
--- a/framework/lib/src/widgets/autocomplete.dart
+++ b/framework/lib/src/widgets/autocomplete.dart
@@ -134,14 +134,11 @@
this.onSelected,
this.textEditingController,
this.initialValue,
- }) : assert(displayStringForOption != null),
- assert(
+ }) : assert(
fieldViewBuilder != null
|| (key != null && focusNode != null && textEditingController != null),
'Pass in a fieldViewBuilder, or otherwise create a separate field and pass in the FocusNode, TextEditingController, and a key. Use the key with RawAutocomplete.onFieldSubmitted.',
),
- assert(optionsBuilder != null),
- assert(optionsViewBuilder != null),
assert((focusNode == null) == (textEditingController == null)),
assert(
!(textEditingController != null && initialValue != null),
@@ -263,8 +260,8 @@
/// The default way to convert an option to a string in
/// [displayStringForOption].
///
- /// Simply uses the `toString` method on the option.
- static String defaultStringForOption(dynamic option) {
+ /// Uses the `toString` method of the given `option`.
+ static String defaultStringForOption(Object? option) {
return option.toString();
}
diff --git a/framework/lib/src/widgets/autofill.dart b/framework/lib/src/widgets/autofill.dart
index a6d02ed..8f6ff16 100644
--- a/framework/lib/src/widgets/autofill.dart
+++ b/framework/lib/src/widgets/autofill.dart
@@ -70,7 +70,7 @@
super.key,
required this.child,
this.onDisposeAction = AutofillContextAction.commit,
- }) : assert(child != null);
+ });
/// Returns the [AutofillGroupState] of the closest [AutofillGroup] widget
/// which encloses the given context, or null if one cannot be found.
@@ -189,7 +189,6 @@
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to update the current [AutofillScope] when needed.
void register(AutofillClient client) {
- assert(client != null);
_clients.putIfAbsent(client.autofillId, () => client);
}
@@ -207,7 +206,7 @@
/// from the current [AutofillScope] when the widget is about to be removed
/// from the tree.
void unregister(String autofillId) {
- assert(autofillId != null && _clients.containsKey(autofillId));
+ assert(_clients.containsKey(autofillId));
_clients.remove(autofillId);
}
@@ -229,7 +228,7 @@
void dispose() {
super.dispose();
- if (!_isTopmostAutofillGroup || widget.onDisposeAction == null) {
+ if (!_isTopmostAutofillGroup) {
return;
}
switch (widget.onDisposeAction) {
diff --git a/framework/lib/src/widgets/automatic_keep_alive.dart b/framework/lib/src/widgets/automatic_keep_alive.dart
index 812cdf1..9377c4d 100644
--- a/framework/lib/src/widgets/automatic_keep_alive.dart
+++ b/framework/lib/src/widgets/automatic_keep_alive.dart
@@ -294,7 +294,7 @@
/// Creates a notification to indicate that a subtree must be kept alive.
///
/// The [handle] must not be null.
- const KeepAliveNotification(this.handle) : assert(handle != null);
+ const KeepAliveNotification(this.handle);
/// A [Listenable] that will inform its clients when the widget that fired the
/// notification no longer needs to be kept alive.
diff --git a/framework/lib/src/widgets/banner.dart b/framework/lib/src/widgets/banner.dart
index 7aa24a7..dbdfd52 100644
--- a/framework/lib/src/widgets/banner.dart
+++ b/framework/lib/src/widgets/banner.dart
@@ -64,12 +64,7 @@
required this.layoutDirection,
this.color = _kColor,
this.textStyle = _kTextStyle,
- }) : assert(message != null),
- assert(textDirection != null),
- assert(location != null),
- assert(color != null),
- assert(textStyle != null),
- super(repaint: PaintingBinding.instance.systemFonts);
+ }) : super(repaint: PaintingBinding.instance.systemFonts);
/// The message to show in the banner.
final String message;
@@ -169,8 +164,6 @@
bool hitTest(Offset position) => false;
double _translationX(double width) {
- assert(location != null);
- assert(layoutDirection != null);
switch (layoutDirection) {
case TextDirection.rtl:
switch (location) {
@@ -198,7 +191,6 @@
}
double _translationY(double height) {
- assert(location != null);
switch (location) {
case BannerLocation.bottomStart:
case BannerLocation.bottomEnd:
@@ -210,8 +202,6 @@
}
double get _rotation {
- assert(location != null);
- assert(layoutDirection != null);
switch (layoutDirection) {
case TextDirection.rtl:
switch (location) {
@@ -257,10 +247,7 @@
this.layoutDirection,
this.color = _kColor,
this.textStyle = _kTextStyle,
- }) : assert(message != null),
- assert(location != null),
- assert(color != null),
- assert(textStyle != null);
+ });
/// The widget to show behind the banner.
///
diff --git a/framework/lib/src/widgets/basic.dart b/framework/lib/src/widgets/basic.dart
index d85c4ae..29c0181 100644
--- a/framework/lib/src/widgets/basic.dart
+++ b/framework/lib/src/widgets/basic.dart
@@ -152,8 +152,7 @@
super.key,
required this.textDirection,
required super.child,
- }) : assert(textDirection != null),
- assert(child != null);
+ });
/// The text direction for this subtree.
final TextDirection textDirection;
@@ -222,7 +221,7 @@
///
/// For values of opacity other than 0.0 and 1.0, this class is relatively
/// expensive because it requires painting the child into an intermediate
-/// buffer. For the value 0.0, the child is simply not painted at all. For the
+/// buffer. For the value 0.0, the child is not painted at all. For the
/// value 1.0, the child is painted immediately without an intermediate buffer.
///
/// The presence of the intermediate buffer which has a transparent background
@@ -311,8 +310,7 @@
required this.opacity,
this.alwaysIncludeSemantics = false,
super.child,
- }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
- assert(alwaysIncludeSemantics != null);
+ }) : assert(opacity >= 0.0 && opacity <= 1.0);
/// The fraction to scale the child's alpha value.
///
@@ -400,8 +398,7 @@
required this.shaderCallback,
this.blendMode = BlendMode.modulate,
super.child,
- }) : assert(shaderCallback != null),
- assert(blendMode != null);
+ });
/// Called to create the [dart:ui.Shader] that generates the mask.
///
@@ -551,7 +548,7 @@
required this.filter,
super.child,
this.blendMode = BlendMode.srcOver,
- }) : assert(filter != null);
+ });
/// The image filter to apply to the existing painted content before painting the child.
///
@@ -643,10 +640,7 @@
this.isComplex = false,
this.willChange = false,
super.child,
- }) : assert(size != null),
- assert(isComplex != null),
- assert(willChange != null),
- assert(painter != null || foregroundPainter != null || (!isComplex && !willChange));
+ }) : assert(painter != null || foregroundPainter != null || (!isComplex && !willChange));
/// The painter that paints before the children.
final CustomPainter? painter;
@@ -763,7 +757,7 @@
this.clipper,
this.clipBehavior = Clip.hardEdge,
super.child,
- }) : assert(clipBehavior != null);
+ });
/// If non-null, determines which clip to use.
final CustomClipper<Rect>? clipper;
@@ -848,8 +842,7 @@
this.clipper,
this.clipBehavior = Clip.antiAlias,
super.child,
- }) : assert(borderRadius != null || clipper != null),
- assert(clipBehavior != null);
+ }) : assert(borderRadius != null || clipper != null);
/// The border radius of the rounded corners.
///
@@ -921,7 +914,7 @@
this.clipper,
this.clipBehavior = Clip.antiAlias,
super.child,
- }) : assert(clipBehavior != null);
+ });
/// If non-null, determines which clip to use.
///
@@ -996,7 +989,7 @@
this.clipper,
this.clipBehavior = Clip.antiAlias,
super.child,
- }) : assert(clipBehavior != null);
+ });
/// Creates a shape clip.
///
@@ -1008,8 +1001,6 @@
Clip clipBehavior = Clip.antiAlias,
Widget? child,
}) {
- assert(clipBehavior != null);
- assert(shape != null);
return Builder(
key: key,
builder: (BuildContext context) {
@@ -1093,11 +1084,7 @@
required this.color,
this.shadowColor = const Color(0xFF000000),
super.child,
- }) : assert(shape != null),
- assert(elevation != null && elevation >= 0.0),
- assert(color != null),
- assert(shadowColor != null),
- assert(clipBehavior != null);
+ }) : assert(elevation >= 0.0);
/// The type of shape.
final BoxShape shape;
@@ -1196,11 +1183,7 @@
required this.color,
this.shadowColor = const Color(0xFF000000),
super.child,
- }) : assert(clipper != null),
- assert(clipBehavior != null),
- assert(elevation != null && elevation >= 0.0),
- assert(color != null),
- assert(shadowColor != null);
+ }) : assert(elevation >= 0.0);
/// Determines which clip to use.
///
@@ -1274,7 +1257,7 @@
/// top right corner pinned to its original position.
///
/// ```dart
-/// Container(
+/// ColoredBox(
/// color: Colors.black,
/// child: Transform(
/// alignment: Alignment.topRight,
@@ -1310,7 +1293,7 @@
this.transformHitTests = true,
this.filterQuality,
super.child,
- }) : assert(transform != null);
+ });
/// Creates a widget that transforms its child using a rotation around the
/// center.
@@ -1424,6 +1407,36 @@
assert(scale == null || (scaleX == null && scaleY == null), "If 'scale' is non-null then 'scaleX' and 'scaleY' must be left null"),
transform = Matrix4.diagonal3Values(scale ?? scaleX ?? 1.0, scale ?? scaleY ?? 1.0, 1.0);
+ /// Creates a widget that mirrors its child about the widget's center point.
+ ///
+ /// If `flipX` is true, the child widget will be flipped horizontally. Defaults to false.
+ ///
+ /// If `flipY` is true, the child widget will be flipped vertically. Defaults to false.
+ ///
+ /// If both are true, the child widget will be flipped both vertically and horizontally, equivalent to a 180 degree rotation.
+ ///
+ /// {@tool snippet}
+ ///
+ /// This example flips the text horizontally.
+ ///
+ /// ```dart
+ /// Transform.flip(
+ /// flipX: true,
+ /// child: const Text('Horizontal Flip'),
+ /// )
+ /// ```
+ /// {@end-tool}
+ Transform.flip({
+ super.key,
+ bool flipX = false,
+ bool flipY = false,
+ this.origin,
+ this.transformHitTests = true,
+ this.filterQuality,
+ super.child,
+ }) : alignment = Alignment.center,
+ transform = Matrix4.diagonal3Values(flipX ? -1.0 : 1.0, flipY ? -1.0 : 1.0, 1.0);
+
// Computes a rotation matrix for an angle in radians, attempting to keep rotations
// at integral values for angles of 0, π/2, π, 3π/2.
static Matrix4 _computeRotation(double radians) {
@@ -1543,7 +1556,7 @@
super.key,
required this.link,
super.child,
- }) : assert(link != null);
+ });
/// The link object that connects this [CompositedTransformTarget] with one or
/// more [CompositedTransformFollower]s.
@@ -1608,11 +1621,7 @@
this.targetAnchor = Alignment.topLeft,
this.followerAnchor = Alignment.topLeft,
super.child,
- }) : assert(link != null),
- assert(showWhenUnlinked != null),
- assert(offset != null),
- assert(targetAnchor != null),
- assert(followerAnchor != null);
+ });
/// The link object that connects this [CompositedTransformFollower] with a
/// [CompositedTransformTarget].
@@ -1708,9 +1717,7 @@
this.alignment = Alignment.center,
this.clipBehavior = Clip.none,
super.child,
- }) : assert(fit != null),
- assert(alignment != null),
- assert(clipBehavior != null);
+ });
/// How to inscribe the child into the space allocated during layout.
final BoxFit fit;
@@ -1789,7 +1796,7 @@
required this.translation,
this.transformHitTests = true,
super.child,
- }) : assert(translation != null);
+ });
/// The translation to apply to the child, scaled to the child's size.
///
@@ -1851,7 +1858,7 @@
super.key,
required this.quarterTurns,
super.child,
- }) : assert(quarterTurns != null);
+ });
/// The number of clockwise quarter turns the child should be rotated.
final int quarterTurns;
@@ -1896,7 +1903,7 @@
/// ### Why use a [Padding] widget rather than a [Container] with a [Container.padding] property?
///
/// There isn't really any difference between the two. If you supply a
-/// [Container.padding] argument, [Container] simply builds a [Padding] widget
+/// [Container.padding] argument, [Container] builds a [Padding] widget
/// for you.
///
/// [Container] doesn't implement its properties directly. Instead, [Container]
@@ -1907,7 +1914,7 @@
/// convenient, feel free to use it. If not, feel free to build these simpler
/// widgets in whatever combination meets your needs.
///
-/// In fact, the majority of widgets in Flutter are simply combinations of other
+/// In fact, the majority of widgets in Flutter are combinations of other
/// simpler widgets. Composition, rather than inheritance, is the primary
/// mechanism for building up widgets.
///
@@ -1925,7 +1932,7 @@
super.key,
required this.padding,
super.child,
- }) : assert(padding != null);
+ });
/// The amount of space by which to inset the child.
final EdgeInsetsGeometry padding;
@@ -2090,8 +2097,7 @@
this.widthFactor,
this.heightFactor,
super.child,
- }) : assert(alignment != null),
- assert(widthFactor == null || widthFactor >= 0.0),
+ }) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0);
/// How to align the child.
@@ -2199,7 +2205,7 @@
super.key,
required this.delegate,
super.child,
- }) : assert(delegate != null);
+ });
/// The delegate that controls the layout of the child.
final SingleChildLayoutDelegate delegate;
@@ -2228,9 +2234,7 @@
Key? key,
required this.id,
required super.child,
- }) : assert(child != null),
- assert(id != null),
- super(key: key ?? ValueKey<Object>(id));
+ }) : super(key: key ?? ValueKey<Object>(id));
/// An object representing the identity of this child.
///
@@ -2300,11 +2304,11 @@
/// Creates a custom multi-child layout.
///
/// The [delegate] argument must not be null.
- CustomMultiChildLayout({
+ const CustomMultiChildLayout({
super.key,
required this.delegate,
super.children,
- }) : assert(delegate != null);
+ });
/// The delegate that controls the layout of the children.
final MultiChildLayoutDelegate delegate;
@@ -2492,8 +2496,7 @@
super.key,
required this.constraints,
super.child,
- }) : assert(constraints != null),
- assert(constraints.debugAssertIsValid());
+ }) : assert(constraints.debugAssertIsValid());
/// The additional constraints to impose on the child.
final BoxConstraints constraints;
@@ -2585,11 +2588,7 @@
required this.constraintsTransform,
this.clipBehavior = Clip.none,
String debugTransformType = '',
- }) : _debugTransformLabel = debugTransformType,
- assert(alignment != null),
- assert(clipBehavior != null),
- assert(constraintsTransform != null),
- assert(debugTransformType != null);
+ }) : _debugTransformLabel = debugTransformType;
/// A [BoxConstraintsTransform] that always returns its argument as-is (i.e.,
/// it is an identity function).
@@ -2774,8 +2773,7 @@
this.alignment = Alignment.center,
this.constrainedAxis,
this.clipBehavior = Clip.none,
- }) : assert(alignment != null),
- assert(clipBehavior != null);
+ });
/// The text direction to use when interpreting the [alignment] if it is an
/// [AlignmentDirectional].
@@ -2876,8 +2874,7 @@
this.widthFactor,
this.heightFactor,
super.child,
- }) : assert(alignment != null),
- assert(widthFactor == null || widthFactor >= 0.0),
+ }) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0);
/// {@template flutter.widgets.basic.fractionallySizedBox.widthFactor}
@@ -2987,8 +2984,8 @@
this.maxWidth = double.infinity,
this.maxHeight = double.infinity,
super.child,
- }) : assert(maxWidth != null && maxWidth >= 0.0),
- assert(maxHeight != null && maxHeight >= 0.0);
+ }) : assert(maxWidth >= 0.0),
+ assert(maxHeight >= 0.0);
/// The maximum width limit to apply in the absence of a
/// [BoxConstraints.maxWidth] constraint.
@@ -3140,8 +3137,7 @@
required this.size,
this.alignment = Alignment.center,
super.child,
- }) : assert(size != null),
- assert(alignment != null);
+ });
/// How to align the child.
///
@@ -3223,8 +3219,7 @@
/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
class Offstage extends SingleChildRenderObjectWidget {
/// Creates a widget that visually hides its child.
- const Offstage({ super.key, this.offstage = true, super.child })
- : assert(offstage != null);
+ const Offstage({ super.key, this.offstage = true, super.child });
/// Whether the child is hidden from the rest of the tree.
///
@@ -3339,8 +3334,7 @@
super.key,
required this.aspectRatio,
super.child,
- }) : assert(aspectRatio != null),
- assert(aspectRatio > 0.0);
+ }) : assert(aspectRatio > 0.0);
/// The aspect ratio to attempt to use.
///
@@ -3505,8 +3499,7 @@
required this.baseline,
required this.baselineType,
super.child,
- }) : assert(baseline != null),
- assert(baselineType != null);
+ });
/// The number of logical pixels from the top of this box at which to position
/// the child's baseline.
@@ -3584,8 +3577,7 @@
super.key,
required this.padding,
Widget? sliver,
- }) : assert(padding != null),
- super(child: sliver);
+ }) : super(child: sliver);
/// The amount of space by which to inset the child sliver.
final EdgeInsetsGeometry padding;
@@ -3674,12 +3666,12 @@
/// given axis.
///
/// By default, the [mainAxis] is [Axis.vertical].
- ListBody({
+ const ListBody({
super.key,
this.mainAxis = Axis.vertical,
this.reverse = false,
super.children,
- }) : assert(mainAxis != null);
+ });
/// The direction to use as the main axis.
final Axis mainAxis;
@@ -3833,14 +3825,14 @@
///
/// By default, the non-positioned children of the stack are aligned by their
/// top left corners.
- Stack({
+ const Stack({
super.key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.clipBehavior = Clip.hardEdge,
super.children,
- }) : assert(clipBehavior != null);
+ });
/// How to align the non-positioned and partially-positioned children in the
/// stack.
@@ -3949,7 +3941,7 @@
/// Creates a [Stack] widget that paints a single child.
///
/// The [index] argument must not be null.
- IndexedStack({
+ const IndexedStack({
super.key,
super.alignment,
super.textDirection,
@@ -4118,7 +4110,6 @@
double? height,
required Widget child,
}) {
- assert(textDirection != null);
double? left;
double? right;
switch (textDirection) {
@@ -4399,8 +4390,8 @@
///
/// Layout for a [Flex] proceeds in six steps:
///
-/// 1. Layout each child a null or zero flex factor (e.g., those that are not
-/// [Expanded]) with unbounded main axis constraints and the incoming
+/// 1. Layout each child with a null or zero flex factor (e.g., those that are
+/// not [Expanded]) with unbounded main axis constraints and the incoming
/// cross axis constraints. If the [crossAxisAlignment] is
/// [CrossAxisAlignment.stretch], instead use tight cross axis constraints
/// that match the incoming max extent in the cross axis.
@@ -4454,7 +4445,7 @@
/// to be necessary to decide which direction to lay the children in or to
/// disambiguate `start` or `end` values for the main or cross axis
/// directions, the [textDirection] must not be null.
- Flex({
+ const Flex({
super.key,
required this.direction,
this.mainAxisAlignment = MainAxisAlignment.start,
@@ -4465,13 +4456,8 @@
this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
this.clipBehavior = Clip.none,
super.children,
- }) : assert(direction != null),
- assert(mainAxisAlignment != null),
- assert(mainAxisSize != null),
- assert(crossAxisAlignment != null),
- assert(verticalDirection != null),
- assert(crossAxisAlignment != CrossAxisAlignment.baseline || textBaseline != null, 'textBaseline is required if you specify the crossAxisAlignment with CrossAxisAlignment.baseline'),
- assert(clipBehavior != null);
+ }) : assert(!identical(crossAxisAlignment, CrossAxisAlignment.baseline) || textBaseline != null, 'textBaseline is required if you specify the crossAxisAlignment with CrossAxisAlignment.baseline');
+ // Cannot use == in the assert above instead of identical because of https://github.com/dart-lang/language/issues/1811.
/// The direction to use as the main axis.
///
@@ -4569,12 +4555,10 @@
final Clip clipBehavior;
bool get _needTextDirection {
- assert(direction != null);
switch (direction) {
case Axis.horizontal:
return true; // because it affects the layout order.
case Axis.vertical:
- assert(crossAxisAlignment != null);
return crossAxisAlignment == CrossAxisAlignment.start
|| crossAxisAlignment == CrossAxisAlignment.end;
}
@@ -4664,8 +4648,8 @@
/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/row.png)
///
/// ```dart
-/// Row(
-/// children: const <Widget>[
+/// const Row(
+/// children: <Widget>[
/// Expanded(
/// child: Text('Deliver features faster', textAlign: TextAlign.center),
/// ),
@@ -4699,8 +4683,8 @@
/// Suppose, for instance, that you had this code:
///
/// ```dart
-/// Row(
-/// children: const <Widget>[
+/// const Row(
+/// children: <Widget>[
/// FlutterLogo(),
/// Text("Flutter's hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android."),
/// Icon(Icons.sentiment_very_satisfied),
@@ -4726,8 +4710,8 @@
/// row that the child should be given the remaining room:
///
/// ```dart
-/// Row(
-/// children: const <Widget>[
+/// const Row(
+/// children: <Widget>[
/// FlutterLogo(),
/// Expanded(
/// child: Text("Flutter's hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android."),
@@ -4755,9 +4739,9 @@
/// [TextDirection.rtl]. This is shown in the example below
///
/// ```dart
-/// Row(
+/// const Row(
/// textDirection: TextDirection.rtl,
-/// children: const <Widget>[
+/// children: <Widget>[
/// FlutterLogo(),
/// Expanded(
/// child: Text("Flutter's hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android."),
@@ -4776,8 +4760,8 @@
///
/// Layout for a [Row] proceeds in six steps:
///
-/// 1. Layout each child a null or zero flex factor (e.g., those that are not
-/// [Expanded]) with unbounded horizontal constraints and the incoming
+/// 1. Layout each child with a null or zero flex factor (e.g., those that are
+/// not [Expanded]) with unbounded horizontal constraints and the incoming
/// vertical constraints. If the [crossAxisAlignment] is
/// [CrossAxisAlignment.stretch], instead use tight vertical constraints that
/// match the incoming max height.
@@ -4829,7 +4813,7 @@
/// unless the row has no children or only one child) or to disambiguate
/// `start` or `end` values for the [mainAxisAlignment], the [textDirection]
/// must not be null.
- Row({
+ const Row({
super.key,
super.mainAxisAlignment,
super.mainAxisSize,
@@ -4866,8 +4850,8 @@
/// ![Using the Column in this way creates two short lines of text with a large Flutter underneath.](https://flutter.github.io/assets-for-api-docs/assets/widgets/column.png)
///
/// ```dart
-/// Column(
-/// children: const <Widget>[
+/// const Column(
+/// children: <Widget>[
/// Text('Deliver features faster'),
/// Text('Craft beautiful UIs'),
/// Expanded(
@@ -4967,8 +4951,8 @@
///
/// Layout for a [Column] proceeds in six steps:
///
-/// 1. Layout each child a null or zero flex factor (e.g., those that are not
-/// [Expanded]) with unbounded vertical constraints and the incoming
+/// 1. Layout each child with a null or zero flex factor (e.g., those that are
+/// not [Expanded]) with unbounded vertical constraints and the incoming
/// horizontal constraints. If the [crossAxisAlignment] is
/// [CrossAxisAlignment.stretch], instead use tight horizontal constraints
/// that match the incoming max width.
@@ -5022,7 +5006,7 @@
/// any. If there is no ambient directionality, and a text direction is going
/// to be necessary to disambiguate `start` or `end` values for the
/// [crossAxisAlignment], the [textDirection] must not be null.
- Column({
+ const Column({
super.key,
super.mainAxisAlignment,
super.mainAxisSize,
@@ -5229,7 +5213,7 @@
/// to be necessary to decide which direction to lay the children in or to
/// disambiguate `start` or `end` values for the main or cross axis
/// directions, the [textDirection] must not be null.
- Wrap({
+ const Wrap({
super.key,
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
@@ -5241,7 +5225,7 @@
this.verticalDirection = VerticalDirection.down,
this.clipBehavior = Clip.none,
super.children,
- }) : assert(clipBehavior != null);
+ });
/// The direction to use as the main axis.
///
@@ -5439,7 +5423,7 @@
/// Rather than positioning the children during layout, the children are
/// positioned using transformation matrices during the paint phase using the
/// matrices from the [FlowDelegate.paintChildren] function. The children can be
-/// repositioned efficiently by simply repainting the flow, which happens
+/// repositioned efficiently by only _repainting_ the flow, which happens
/// without the children being laid out again (contrast this with a [Stack],
/// which does the sizing and positioning together during layout).
///
@@ -5481,9 +5465,7 @@
required this.delegate,
List<Widget> children = const <Widget>[],
this.clipBehavior = Clip.hardEdge,
- }) : assert(delegate != null),
- assert(clipBehavior != null),
- super(children: RepaintBoundary.wrapAll(children));
+ }) : super(children: RepaintBoundary.wrapAll(children));
// https://github.com/dart-lang/sdk/issues/29277
/// Creates a flow layout.
@@ -5493,13 +5475,12 @@
/// a repaint boundary.
///
/// The [delegate] argument must not be null.
- Flow.unwrapped({
+ const Flow.unwrapped({
super.key,
required this.delegate,
super.children,
this.clipBehavior = Clip.hardEdge,
- }) : assert(delegate != null),
- assert(clipBehavior != null);
+ });
/// The delegate that controls the transformation matrices of the children.
final FlowDelegate delegate;
@@ -5626,13 +5607,7 @@
this.textHeightBehavior,
this.selectionRegistrar,
this.selectionColor,
- }) : assert(text != null),
- assert(textAlign != null),
- assert(softWrap != null),
- assert(overflow != null),
- assert(textScaleFactor != null),
- assert(maxLines == null || maxLines > 0),
- assert(textWidthBasis != null),
+ }) : assert(maxLines == null || maxLines > 0),
assert(selectionRegistrar == null || selectionColor != null),
super(children: _extractChildren(text));
@@ -5817,11 +5792,7 @@
this.invertColors = false,
this.filterQuality = FilterQuality.low,
this.isAntiAlias = false,
- }) : assert(scale != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
- assert(isAntiAlias != null);
+ });
/// The image to display.
///
@@ -6094,8 +6065,7 @@
super.key,
required this.bundle,
required super.child,
- }) : assert(bundle != null),
- assert(child != null);
+ });
/// The bundle to use as a default.
final AssetBundle bundle;
@@ -6138,12 +6108,7 @@
required this.renderBox,
this.onBuild,
this.onUnmount,
- }) : assert(renderBox != null),
- // WidgetToRenderBoxAdapter objects are keyed to their render box. This
- // prevents the widget being used in the widget hierarchy in two different
- // places, which would cause the RenderBox to get inserted in multiple
- // places in the RenderObject tree.
- super(key: GlobalObjectKey(renderBox));
+ }) : super(key: GlobalObjectKey(renderBox));
/// The render box to place in the widget tree.
///
@@ -6236,7 +6201,7 @@
this.onPointerSignal,
this.behavior = HitTestBehavior.deferToChild,
super.child,
- }) : assert(behavior != null);
+ });
/// Called when a pointer comes into contact with the screen (for touch
/// pointers), or has its button pressed (for mouse pointers) at this widget's
@@ -6375,8 +6340,7 @@
this.opaque = true,
this.hitTestBehavior,
super.child,
- }) : assert(cursor != null),
- assert(opaque != null);
+ });
/// Triggered when a mouse pointer has entered this widget.
///
@@ -6480,7 +6444,7 @@
/// Because the widget conditionally creates the `MouseRegion`, and leaks the
/// hover state, it needs to take the restriction into consideration. In this
/// case, since it has access to the event that triggers the disappearance of
- /// the `MouseRegion`, it simply trigger the exit callback during that event
+ /// the `MouseRegion`, it triggers the exit callback during that event
/// as well.
///
/// ** See code in examples/api/lib/widgets/basic/mouse_region.on_exit.1.dart **
@@ -6632,7 +6596,6 @@
/// The key for the [RepaintBoundary] is derived either from the child's key
/// (if the child has a non-null key) or from the given `childIndex`.
factory RepaintBoundary.wrap(Widget child, int childIndex) {
- assert(child != null);
final Key key = child.key != null ? ValueKey<Key>(child.key!) : ValueKey<int>(childIndex);
return RepaintBoundary(key: key, child: child);
}
@@ -6687,7 +6650,7 @@
this.ignoring = true,
this.ignoringSemantics,
super.child,
- }) : assert(ignoring != null);
+ });
/// Whether this widget is ignored during hit testing.
///
@@ -6756,7 +6719,7 @@
this.absorbing = true,
super.child,
this.ignoringSemantics,
- }) : assert(absorbing != null);
+ });
/// Whether this widget absorbs pointers during hit testing.
///
@@ -7025,8 +6988,7 @@
this.explicitChildNodes = false,
this.excludeSemantics = false,
required this.properties,
- }) : assert(container != null),
- assert(properties != null);
+ });
/// Contains properties used by assistive technologies to make the application
/// more accessible.
@@ -7217,7 +7179,7 @@
super.key,
this.excluding = true,
super.child,
- }) : assert(excluding != null);
+ });
/// Whether this widget is excluded in the semantics tree.
final bool excluding;
@@ -7277,7 +7239,7 @@
super.key,
required this.index,
super.child,
- }) : assert(index != null);
+ });
/// The index used to annotate the first child semantics node.
final int index;
@@ -7304,7 +7266,7 @@
const KeyedSubtree({
super.key,
required this.child,
- }) : assert(child != null);
+ });
/// Creates a KeyedSubtree for child with a key that's based on the child's existing key or childIndex.
factory KeyedSubtree.wrap(Widget child, int childIndex) {
@@ -7320,7 +7282,7 @@
/// Wrap each item in a KeyedSubtree whose key is based on the item's existing key or
/// the sum of its list index and `baseIndex`.
static List<Widget> ensureUniqueKeysForList(List<Widget> items, { int baseIndex = 0 }) {
- if (items == null || items.isEmpty) {
+ if (items.isEmpty) {
return items;
}
@@ -7373,8 +7335,8 @@
/// )
/// ```
///
-/// The difference between either of the previous examples and simply
-/// creating a child directly, without an intervening widget, is the
+/// The difference between either of the previous examples and
+/// creating a child directly without an intervening widget, is the
/// extra [BuildContext] element that the additional widget adds. This
/// is particularly noticeable when the tree contains an inherited
/// widget that is referred to by a method like [Scaffold.of],
@@ -7433,7 +7395,7 @@
const Builder({
super.key,
required this.builder,
- }) : assert(builder != null);
+ });
/// Called to obtain the child widget.
///
@@ -7506,7 +7468,7 @@
const StatefulBuilder({
super.key,
required this.builder,
- }) : assert(builder != null);
+ });
/// Called to obtain the child widget.
///
@@ -7532,8 +7494,7 @@
/// Creates a widget that paints its area with the specified [Color].
///
/// The [color] parameter must not be null.
- const ColoredBox({ required this.color, super.child, super.key })
- : assert(color != null);
+ const ColoredBox({ required this.color, super.child, super.key });
/// The color to paint the background area with.
final Color color;
@@ -7566,7 +7527,6 @@
Color get color => _color;
Color _color;
set color(Color value) {
- assert(value != null);
if (value == _color) {
return;
}
diff --git a/framework/lib/src/widgets/binding.dart b/framework/lib/src/widgets/binding.dart
index 934f5c4..7633f50 100644
--- a/framework/lib/src/widgets/binding.dart
+++ b/framework/lib/src/widgets/binding.dart
@@ -19,6 +19,7 @@
import 'platform_menu_bar.dart';
import 'router.dart';
import 'service_extensions.dart';
+import 'view.dart';
import 'widget_inspector.dart';
export 'package:engine/ui.dart' show AppLifecycleState, Locale;
@@ -116,7 +117,9 @@
/// @override
/// void initState() {
/// super.initState();
- /// _lastSize = WidgetsBinding.instance.window.physicalSize;
+ /// // [View.of] exposes the view from `WidgetsBinding.instance.platformDispatcher.views`
+ /// // into which this widget is drawn.
+ /// _lastSize = View.of(context).physicalSize;
/// WidgetsBinding.instance.addObserver(this);
/// }
///
@@ -128,7 +131,7 @@
///
/// @override
/// void didChangeMetrics() {
- /// setState(() { _lastSize = WidgetsBinding.instance.window.physicalSize; });
+ /// setState(() { _lastSize = View.of(context).physicalSize; });
/// }
///
/// @override
@@ -185,7 +188,7 @@
///
/// @override
/// void didChangeTextScaleFactor() {
- /// setState(() { _lastTextScaleFactor = WidgetsBinding.instance.window.textScaleFactor; });
+ /// setState(() { _lastTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor; });
/// }
///
/// @override
@@ -896,6 +899,22 @@
@override
bool get framesEnabled => super.framesEnabled && _readyToProduceFrames;
+ /// 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.
+ ///
+ /// The `rootWidget` widget provided to this method must not already be
+ /// wrapped in a [View].
+ Widget wrapWithDefaultView(Widget rootWidget) {
+ return View(
+ view: window,
+ child: rootWidget,
+ );
+ }
+
/// Schedules a [Timer] for attaching the root widget.
///
/// This is called by [runApp] to configure the widget tree. Consider using
@@ -1014,8 +1033,9 @@
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
/// ensure the widget, element, and render trees are all built.
void runApp(Widget app) {
- WidgetsFlutterBinding.ensureInitialized()
- ..scheduleAttachRootWidget(app)
+ final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
+ binding
+ ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
diff --git a/framework/lib/src/widgets/bottom_navigation_bar_item.dart b/framework/lib/src/widgets/bottom_navigation_bar_item.dart
index 7684c07..82b2647 100644
--- a/framework/lib/src/widgets/bottom_navigation_bar_item.dart
+++ b/framework/lib/src/widgets/bottom_navigation_bar_item.dart
@@ -28,8 +28,7 @@
Widget? activeIcon,
this.backgroundColor,
this.tooltip,
- }) : activeIcon = activeIcon ?? icon,
- assert(icon != null);
+ }) : activeIcon = activeIcon ?? icon;
/// The icon of the item.
///
diff --git a/framework/lib/src/widgets/color_filter.dart b/framework/lib/src/widgets/color_filter.dart
index ccb790d..04ad22a 100644
--- a/framework/lib/src/widgets/color_filter.dart
+++ b/framework/lib/src/widgets/color_filter.dart
@@ -34,8 +34,7 @@
/// Creates a widget that applies a [ColorFilter] to its child.
///
/// The [colorFilter] must not be null.
- const ColorFiltered({required this.colorFilter, super.child, super.key})
- : assert(colorFilter != null);
+ const ColorFiltered({required this.colorFilter, super.child, super.key});
/// The color filter to apply to the child of this widget.
final ColorFilter colorFilter;
@@ -61,7 +60,6 @@
ColorFilter get colorFilter => _colorFilter;
ColorFilter _colorFilter;
set colorFilter(ColorFilter value) {
- assert(value != null);
if (value != _colorFilter) {
_colorFilter = value;
markNeedsPaint();
diff --git a/framework/lib/src/widgets/container.dart b/framework/lib/src/widgets/container.dart
index 1a5f174..be46d4a 100644
--- a/framework/lib/src/widgets/container.dart
+++ b/framework/lib/src/widgets/container.dart
@@ -62,8 +62,7 @@
required this.decoration,
this.position = DecorationPosition.background,
super.child,
- }) : assert(decoration != null),
- assert(position != null);
+ });
/// What decoration to paint.
///
@@ -268,7 +267,6 @@
assert(padding == null || padding.isNonNegative),
assert(decoration == null || decoration.debugAssertIsValid()),
assert(constraints == null || constraints.debugAssertIsValid()),
- assert(clipBehavior != null),
assert(decoration != null || clipBehavior == Clip.none),
assert(color == null || decoration == null,
'Cannot provide both a color and a decoration\n'
@@ -369,14 +367,14 @@
final Clip clipBehavior;
EdgeInsetsGeometry? get _paddingIncludingDecoration {
- if (decoration == null || decoration!.padding == null) {
+ if (decoration == null) {
return padding;
}
- final EdgeInsetsGeometry? decorationPadding = decoration!.padding;
+ final EdgeInsetsGeometry decorationPadding = decoration!.padding;
if (padding == null) {
return decorationPadding;
}
- return padding!.add(decorationPadding!);
+ return padding!.add(decorationPadding);
}
@override
@@ -464,8 +462,7 @@
_DecorationClipper({
TextDirection? textDirection,
required this.decoration,
- }) : assert(decoration != null),
- textDirection = textDirection ?? TextDirection.ltr;
+ }) : textDirection = textDirection ?? TextDirection.ltr;
final TextDirection textDirection;
final Decoration decoration;
diff --git a/framework/lib/src/widgets/context_menu_button_item.dart b/framework/lib/src/widgets/context_menu_button_item.dart
index 2c5e723..c89bda3 100644
--- a/framework/lib/src/widgets/context_menu_button_item.dart
+++ b/framework/lib/src/widgets/context_menu_button_item.dart
@@ -23,6 +23,9 @@
/// A button that selects all the contents of the focused text field.
selectAll,
+ /// A button that deletes the current text selection.
+ delete,
+
/// Anything other than the default button types.
custom,
}
diff --git a/framework/lib/src/widgets/context_menu_controller.dart b/framework/lib/src/widgets/context_menu_controller.dart
index bfc6f96..664c27d 100644
--- a/framework/lib/src/widgets/context_menu_controller.dart
+++ b/framework/lib/src/widgets/context_menu_controller.dart
@@ -18,6 +18,11 @@
///
/// ** See code in examples/api/lib/material/context_menu/context_menu_controller.0.dart **
/// {@end-tool}
+///
+/// See also:
+///
+/// * [BrowserContextMenu], which allows the browser's context menu on web to
+/// be disabled and Flutter-rendered context menus to appear.
class ContextMenuController {
/// Creates a context menu that can be shown with [show].
ContextMenuController({
diff --git a/framework/lib/src/widgets/debug.dart b/framework/lib/src/widgets/debug.dart
index 43086e2..6ac04f9 100644
--- a/framework/lib/src/widgets/debug.dart
+++ b/framework/lib/src/widgets/debug.dart
@@ -10,6 +10,7 @@
import 'basic.dart';
import 'framework.dart';
import 'localizations.dart';
+import 'lookup_boundary.dart';
import 'media_query.dart';
import 'overlay.dart';
import 'table.dart';
@@ -169,7 +170,6 @@
Key? _firstNonUniqueKey(Iterable<Widget> widgets) {
final Set<Key> keySet = HashSet<Key>();
for (final Widget widget in widgets) {
- assert(widget != null);
if (widget.key == null) {
continue;
}
@@ -468,12 +468,17 @@
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasOverlay(BuildContext context) {
assert(() {
- if (context.widget is! Overlay && context.findAncestorWidgetOfExactType<Overlay>() == null) {
+ if (LookupBoundary.findAncestorWidgetOfExactType<Overlay>(context) == null) {
+ final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Overlay>(context);
throw FlutterError.fromParts(<DiagnosticsNode>[
- ErrorSummary('No Overlay widget found.'),
+ ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
+ if (hiddenByBoundary)
+ ErrorDescription(
+ 'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
+ ),
ErrorDescription(
'${context.widget.runtimeType} widgets require an Overlay '
- 'widget ancestor.\n'
+ 'widget ancestor within the closest LookupBoundary.\n'
'An overlay lets widgets float on top of other widget children.',
),
ErrorHint(
diff --git a/framework/lib/src/widgets/default_text_editing_shortcuts.dart b/framework/lib/src/widgets/default_text_editing_shortcuts.dart
index 46f8416..ae75678 100644
--- a/framework/lib/src/widgets/default_text_editing_shortcuts.dart
+++ b/framework/lib/src/widgets/default_text_editing_shortcuts.dart
@@ -37,15 +37,15 @@
/// // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
/// // then DefaultTextEditingShortcuts is already being inserted into the
/// // widget tree.
-/// return DefaultTextEditingShortcuts(
+/// return const DefaultTextEditingShortcuts(
/// child: Center(
/// child: Shortcuts(
-/// shortcuts: const <ShortcutActivator, Intent>{
+/// shortcuts: <ShortcutActivator, Intent>{
/// SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): NextFocusIntent(),
/// SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): PreviousFocusIntent(),
/// },
/// child: Column(
-/// children: const <Widget>[
+/// children: <Widget>[
/// TextField(
/// decoration: InputDecoration(
/// hintText: 'alt + down moves to the next field.',
@@ -303,8 +303,8 @@
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: true),
- const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
- const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
+ const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: false),
+ const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
@@ -412,16 +412,10 @@
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): DoNothingAndStopPropagationTextIntent(),
- SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): DoNothingAndStopPropagationTextIntent(),
- SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): DoNothingAndStopPropagationTextIntent(),
- SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): DoNothingAndStopPropagationTextIntent(),
- SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): DoNothingAndStopPropagationTextIntent(),
- SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): DoNothingAndStopPropagationTextIntent(),
- SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.pageUp, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.pageDown, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.end, shift: true): DoNothingAndStopPropagationTextIntent(),
@@ -452,6 +446,12 @@
const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
+ const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const DoNothingAndStopPropagationTextIntent(),
+ const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const DoNothingAndStopPropagationTextIntent(),
+ const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
+ const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
+ const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): const DoNothingAndStopPropagationTextIntent(),
+ const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): const DoNothingAndStopPropagationTextIntent(),
};
// Hand backspace/delete events that do not depend on text layout (delete
@@ -560,8 +560,8 @@
'moveWordLeftAndModifySelection:': ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: false),
'moveWordRightAndModifySelection:': ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: true),
- 'moveParagraphBackwardAndModifySelection:': ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, collapseAtReversal: true),
- 'moveParagraphForwardAndModifySelection:': ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false, collapseAtReversal: true),
+ 'moveParagraphBackwardAndModifySelection:': ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: false),
+ 'moveParagraphForwardAndModifySelection:': ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: true),
'moveToLeftEndOfLine:': ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
'moveToRightEndOfLine:': ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
diff --git a/framework/lib/src/widgets/dismissible.dart b/framework/lib/src/widgets/dismissible.dart
index 60c6777..037d577 100644
--- a/framework/lib/src/widgets/dismissible.dart
+++ b/framework/lib/src/widgets/dismissible.dart
@@ -112,9 +112,7 @@
this.crossAxisEndOffset = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.behavior = HitTestBehavior.opaque,
- }) : assert(key != null),
- assert(secondaryBackground == null || background != null),
- assert(dragStartBehavior != null),
+ }) : assert(secondaryBackground == null || background != null),
super(key: key);
/// The widget below this widget in the tree.
@@ -263,16 +261,13 @@
_DismissibleClipper({
required this.axis,
required this.moveAnimation,
- }) : assert(axis != null),
- assert(moveAnimation != null),
- super(reclip: moveAnimation);
+ }) : super(reclip: moveAnimation);
final Axis axis;
final Animation<Offset> moveAnimation;
@override
Rect getClip(Size size) {
- assert(axis != null);
switch (axis) {
case Axis.horizontal:
final double offset = moveAnimation.value.dx * size.width;
@@ -480,7 +475,6 @@
}
_FlingGestureKind _describeFlingGesture(Velocity velocity) {
- assert(widget.direction != null);
if (_dragExtent == 0.0) {
// If it was a fling, then it was a fling that was let loose at the exact
// middle of the range (i.e. when there's no displacement). In that case,
@@ -506,7 +500,6 @@
assert(vy != 0.0);
flingDirection = _extentToDirection(vy);
}
- assert(_dismissDirection != null);
if (flingDirection == _dismissDirection) {
return _FlingGestureKind.forward;
}
diff --git a/framework/lib/src/widgets/display_feature_sub_screen.dart b/framework/lib/src/widgets/display_feature_sub_screen.dart
index febb215..5253cc0 100644
--- a/framework/lib/src/widgets/display_feature_sub_screen.dart
+++ b/framework/lib/src/widgets/display_feature_sub_screen.dart
@@ -125,7 +125,7 @@
/// Returns the areas of the screen that are obstructed by display features.
///
- /// A [DisplayFeature] obstructs the screen when the the area it occupies is
+ /// A [DisplayFeature] obstructs the screen when the area it occupies is
/// not 0 or the `state` is [DisplayFeatureState.postureHalfOpened].
static Iterable<Rect> avoidBounds(MediaQueryData mediaQuery) {
return mediaQuery.displayFeatures
diff --git a/framework/lib/src/widgets/disposable_build_context.dart b/framework/lib/src/widgets/disposable_build_context.dart
index 3a049ed..2283aca 100644
--- a/framework/lib/src/widgets/disposable_build_context.dart
+++ b/framework/lib/src/widgets/disposable_build_context.dart
@@ -28,8 +28,7 @@
///
/// The [State] must not be null, and [State.mounted] must be true.
DisposableBuildContext(T this._state)
- : assert(_state != null),
- assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.');
+ : assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.');
T? _state;
diff --git a/framework/lib/src/widgets/drag_target.dart b/framework/lib/src/widgets/drag_target.dart
index 01a30db..950d30a 100644
--- a/framework/lib/src/widgets/drag_target.dart
+++ b/framework/lib/src/widgets/drag_target.dart
@@ -179,12 +179,8 @@
this.ignoringFeedbackPointer = true,
this.rootOverlay = false,
this.hitTestBehavior = HitTestBehavior.deferToChild,
- }) : assert(child != null),
- assert(feedback != null),
- assert(ignoringFeedbackSemantics != null),
- assert(ignoringFeedbackPointer != null),
- assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
- assert(dragAnchorStrategy != null);
+ this.allowedButtonsFilter,
+ }) : assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0);
/// The data that will be dropped by this draggable.
final T? data;
@@ -359,6 +355,9 @@
/// Defaults to [HitTestBehavior.deferToChild].
final HitTestBehavior hitTestBehavior;
+ /// {@macro flutter.gestures.multidrag._allowedButtonsFilter}
+ final AllowedButtonsFilter? allowedButtonsFilter;
+
/// Creates a gesture recognizer that recognizes the start of the drag.
///
/// Subclasses can override this function to customize when they start
@@ -367,11 +366,11 @@
MultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
switch (affinity) {
case Axis.horizontal:
- return HorizontalMultiDragGestureRecognizer()..onStart = onStart;
+ return HorizontalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
case Axis.vertical:
- return VerticalMultiDragGestureRecognizer()..onStart = onStart;
+ return VerticalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
case null:
- return ImmediateMultiDragGestureRecognizer()..onStart = onStart;
+ return ImmediateMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
}
}
@@ -409,6 +408,7 @@
super.ignoringFeedbackSemantics,
super.ignoringFeedbackPointer,
this.delay = kLongPressTimeout,
+ super.allowedButtonsFilter,
});
/// Whether haptic feedback should be triggered on drag start.
@@ -421,7 +421,7 @@
@override
DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
- return DelayedMultiDragGestureRecognizer(delay: delay)
+ return DelayedMultiDragGestureRecognizer(delay: delay, allowedButtonsFilter: allowedButtonsFilter)
..onStart = (Offset position) {
final Drag? result = onStart(position);
if (result != null && hapticFeedbackOnStart) {
@@ -447,7 +447,7 @@
@override
void didChangeDependencies() {
- _recognizer!.gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
+ _recognizer!.gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
super.didChangeDependencies();
}
@@ -561,8 +561,7 @@
this.wasAccepted = false,
required this.velocity,
required this.offset,
- }) : assert(velocity != null),
- assert(offset != null);
+ });
/// Determines whether the [DragTarget] accepted this draggable.
final bool wasAccepted;
@@ -581,7 +580,7 @@
/// Creates details for a [DragTarget] callback.
///
/// The [offset] must not be null.
- DragTargetDetails({required this.data, required this.offset}) : assert(offset != null);
+ DragTargetDetails({required this.data, required this.offset});
/// The data that was dropped onto this [DragTarget].
final T data;
@@ -729,7 +728,6 @@
@override
Widget build(BuildContext context) {
- assert(widget.builder != null);
return MetaData(
metaData: this,
behavior: widget.hitTestBehavior,
@@ -758,12 +756,7 @@
this.onDragEnd,
required this.ignoringFeedbackSemantics,
required this.ignoringFeedbackPointer,
- }) : assert(overlayState != null),
- assert(ignoringFeedbackSemantics != null),
- assert(ignoringFeedbackPointer != null),
- assert(dragStartPoint != null),
- assert(feedbackOffset != null),
- _position = initialPosition {
+ }) : _position = initialPosition {
_entry = OverlayEntry(builder: _build);
overlayState.insert(_entry!);
updateDrag(initialPosition);
diff --git a/framework/lib/src/widgets/draggable_scrollable_sheet.dart b/framework/lib/src/widgets/draggable_scrollable_sheet.dart
index 31dd48e..090fa0e 100644
--- a/framework/lib/src/widgets/draggable_scrollable_sheet.dart
+++ b/framework/lib/src/widgets/draggable_scrollable_sheet.dart
@@ -308,21 +308,16 @@
this.snapAnimationDuration,
this.controller,
required this.builder,
- }) : assert(initialChildSize != null),
- assert(minChildSize != null),
- assert(maxChildSize != null),
- assert(minChildSize >= 0.0),
+ }) : assert(minChildSize >= 0.0),
assert(maxChildSize <= 1.0),
assert(minChildSize <= initialChildSize),
assert(initialChildSize <= maxChildSize),
- assert(snapAnimationDuration == null || snapAnimationDuration > Duration.zero),
- assert(expand != null),
- assert(builder != null);
+ assert(snapAnimationDuration == null || snapAnimationDuration > Duration.zero);
/// The initial fractional value of the parent container's height to use when
/// displaying the widget.
///
- /// Rebuilding the sheet with a new [initialChildSize] will only move the
+ /// Rebuilding the sheet with a new [initialChildSize] will only move
/// the sheet to the new value if the sheet has not yet been dragged since it
/// was first built or since the last call to [DraggableScrollableActuator.reset].
///
@@ -436,17 +431,12 @@
required this.maxExtent,
required this.initialExtent,
required this.context,
- }) : assert(extent != null),
- assert(initialExtent != null),
- assert(minExtent != null),
- assert(maxExtent != null),
- assert(0.0 <= minExtent),
+ }) : assert(0.0 <= minExtent),
assert(maxExtent <= 1.0),
assert(minExtent <= extent),
assert(minExtent <= initialExtent),
assert(extent <= maxExtent),
- assert(initialExtent <= maxExtent),
- assert(context != null);
+ assert(initialExtent <= maxExtent);
/// The current value of the extent, between [minExtent] and [maxExtent].
final double extent;
@@ -496,10 +486,7 @@
ValueNotifier<double>? currentSize,
bool? hasDragged,
bool? hasChanged,
- }) : assert(minSize != null),
- assert(maxSize != null),
- assert(initialSize != null),
- assert(minSize >= 0),
+ }) : assert(minSize >= 0),
assert(maxSize <= 1),
assert(minSize <= initialSize),
assert(initialSize <= maxSize),
@@ -577,7 +564,6 @@
/// This can be triggered by a programmatic (e.g. controller triggered) change
/// or a user drag.
void updateSize(double newSize, BuildContext context) {
- assert(newSize != null);
final double clampedSize = clampDouble(newSize, minSize, maxSize);
if (_currentSize.value == clampedSize) {
return;
@@ -783,7 +769,7 @@
class _DraggableScrollableSheetScrollController extends ScrollController {
_DraggableScrollableSheetScrollController({
required this.extent,
- }) : assert(extent != null);
+ });
_DraggableSheetExtent extent;
VoidCallback? onPositionDetached;
@@ -902,7 +888,7 @@
bool get _isAtSnapSize {
return extent.snapSizes.any(
(double snapSize) {
- return (extent.currentSize - snapSize).abs() <= extent.pixelsToSize(physics.tolerance.distance);
+ return (extent.currentSize - snapSize).abs() <= extent.pixelsToSize(physics.toleranceFor(this).distance);
},
);
}
@@ -937,7 +923,7 @@
initialVelocity: velocity,
pixelSnapSize: extent.pixelSnapSizes,
snapAnimationDuration: extent.snapAnimationDuration,
- tolerance: physics.tolerance,
+ tolerance: physics.toleranceFor(this),
);
} else {
// The iOS bouncing simulation just isn't right here - once we delegate
@@ -946,7 +932,7 @@
// Run the simulation in terms of pixels, not extent.
position: extent.currentPixels,
velocity: velocity,
- tolerance: physics.tolerance,
+ tolerance: physics.toleranceFor(this),
);
}
@@ -965,7 +951,7 @@
// Make sure we pass along enough velocity to keep scrolling - otherwise
// we just "bounce" off the top making it look like the list doesn't
// have more to scroll.
- velocity = ballisticController.velocity + (physics.tolerance.velocity * ballisticController.velocity.sign);
+ velocity = ballisticController.velocity + (physics.toleranceFor(this).velocity * ballisticController.velocity.sign);
super.goBallistic(velocity);
ballisticController.stop();
} else if (ballisticController.isCompleted) {
diff --git a/framework/lib/src/widgets/dual_transition_builder.dart b/framework/lib/src/widgets/dual_transition_builder.dart
index 560a39b..7789479 100644
--- a/framework/lib/src/widgets/dual_transition_builder.dart
+++ b/framework/lib/src/widgets/dual_transition_builder.dart
@@ -41,9 +41,7 @@
required this.forwardBuilder,
required this.reverseBuilder,
this.child,
- }) : assert(animation != null),
- assert(forwardBuilder != null),
- assert(reverseBuilder != null);
+ });
/// The animation that drives the [child]'s transition.
///
@@ -134,8 +132,6 @@
required AnimationStatus lastEffective,
required AnimationStatus current,
}) {
- assert(current != null);
- assert(lastEffective != null);
switch (current) {
case AnimationStatus.dismissed:
case AnimationStatus.completed:
diff --git a/framework/lib/src/widgets/editable_text.dart b/framework/lib/src/widgets/editable_text.dart
index 68b721b..95353e9 100644
--- a/framework/lib/src/widgets/editable_text.dart
+++ b/framework/lib/src/widgets/editable_text.dart
@@ -43,12 +43,14 @@
import 'text_selection.dart';
import 'text_selection_toolbar_anchors.dart';
import 'ticker_provider.dart';
+import 'view.dart';
import 'widget_span.dart';
-export 'package:flute/services.dart' show SelectionChangedCause, SmartDashesType, SmartQuotesType, TextEditingValue, TextInputType, TextSelection;
+export 'package:flute/services.dart' show KeyboardInsertedContent, SelectionChangedCause, SmartDashesType, SmartQuotesType, TextEditingValue, TextInputType, TextSelection;
// Examples can assume:
// late BuildContext context;
+// late WidgetTester tester;
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
@@ -69,6 +71,10 @@
EditableTextState editableTextState,
);
+// Signature for a function that determines the target location of the given
+// [TextPosition] after applying the given [TextBoundary].
+typedef _ApplyTextBoundary = TextPosition Function(TextPosition, bool, TextBoundary);
+
// The time it takes for the cursor to fade from fully opaque to fully
// transparent and vice versa. A full cursor blink, from transparent to opaque
// to transparent, is twice this duration.
@@ -78,6 +84,64 @@
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
+/// The default mime types to be used when allowedMimeTypes is not provided.
+///
+/// The default value supports inserting images of any supported format.
+const List<String> kDefaultContentInsertionMimeTypes = <String>[
+ 'image/png',
+ 'image/bmp',
+ 'image/jpg',
+ 'image/tiff',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/webp'
+];
+
+class _CompositionCallback extends SingleChildRenderObjectWidget {
+ const _CompositionCallback({ required this.compositeCallback, required this.enabled, super.child });
+ final CompositionCallback compositeCallback;
+ final bool enabled;
+
+ @override
+ RenderObject createRenderObject(BuildContext context) {
+ return _RenderCompositionCallback(compositeCallback, enabled);
+ }
+ @override
+ void updateRenderObject(BuildContext context, _RenderCompositionCallback renderObject) {
+ super.updateRenderObject(context, renderObject);
+ // _EditableTextState always uses the same callback.
+ assert(renderObject.compositeCallback == compositeCallback);
+ renderObject.enabled = enabled;
+ }
+}
+
+class _RenderCompositionCallback extends RenderProxyBox {
+ _RenderCompositionCallback(this.compositeCallback, this._enabled);
+
+ final CompositionCallback compositeCallback;
+ VoidCallback? _cancelCallback;
+
+ bool get enabled => _enabled;
+ bool _enabled = false;
+ set enabled(bool newValue) {
+ _enabled = newValue;
+ if (!newValue) {
+ _cancelCallback?.call();
+ _cancelCallback = null;
+ } else if (_cancelCallback == null) {
+ markNeedsPaint();
+ }
+ }
+
+ @override
+ void paint(PaintingContext context, ui.Offset offset) {
+ if (enabled) {
+ _cancelCallback ??= context.addCompositionCallback(compositeCallback);
+ }
+ super.paint(context, offset);
+ }
+}
+
/// A controller for an editable text field.
///
/// Whenever the user modifies a text field with an associated
@@ -296,10 +360,7 @@
this.cut = false,
this.paste = false,
this.selectAll = false,
- }) : assert(copy != null),
- assert(cut != null),
- assert(paste != null),
- assert(selectAll != null);
+ });
/// An instance of [ToolbarOptions] with no options enabled.
static const ToolbarOptions empty = ToolbarOptions();
@@ -329,6 +390,72 @@
final bool selectAll;
}
+/// Configures the ability to insert media content through the soft keyboard.
+///
+/// The configuration provides a handler for any rich content inserted through
+/// the system input method, and also provides the ability to limit the mime
+/// types of the inserted content.
+///
+/// See also:
+///
+/// * [EditableText.contentInsertionConfiguration]
+class ContentInsertionConfiguration {
+ /// Creates a content insertion configuration with the specified options.
+ ///
+ /// A handler for inserted content, in the form of [onContentInserted], must
+ /// be supplied.
+ ///
+ /// The allowable mime types of inserted content may also
+ /// be provided via [allowedMimeTypes], which cannot be an empty list.
+ ContentInsertionConfiguration({
+ required this.onContentInserted,
+ this.allowedMimeTypes = kDefaultContentInsertionMimeTypes,
+ }) : assert(allowedMimeTypes.isNotEmpty);
+
+ /// Called when a user inserts content through the virtual / on-screen keyboard,
+ /// currently only used on Android.
+ ///
+ /// [KeyboardInsertedContent] holds the data representing the inserted content.
+ ///
+ /// {@tool dartpad}
+ ///
+ /// This example shows how to access the data for inserted content in your
+ /// `TextField`.
+ ///
+ /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
+ /// {@end-tool}
+ ///
+ /// See also:
+ ///
+ /// * <https://developer.android.com/guide/topics/text/image-keyboard>
+ final ValueChanged<KeyboardInsertedContent> onContentInserted;
+
+ /// {@template flutter.widgets.contentInsertionConfiguration.allowedMimeTypes}
+ /// Used when a user inserts image-based content through the device keyboard,
+ /// currently only used on Android.
+ ///
+ /// The passed list of strings will determine which MIME types are allowed to
+ /// be inserted via the device keyboard.
+ ///
+ /// The default mime types are given by [kDefaultContentInsertionMimeTypes].
+ /// These are all the mime types that are able to be handled and inserted
+ /// from keyboards.
+ ///
+ /// This field cannot be an empty list.
+ ///
+ /// {@tool dartpad}
+ /// This example shows how to limit image insertion to specific file types.
+ ///
+ /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
+ /// {@end-tool}
+ ///
+ /// See also:
+ ///
+ /// * <https://developer.android.com/guide/topics/text/image-keyboard>
+ /// {@endtemplate}
+ final List<String> allowedMimeTypes;
+}
+
// A time-value pair that represents a key frame in an animation.
class _KeyFrame {
const _KeyFrame(this.time, this.value);
@@ -675,44 +802,24 @@
this.scrollBehavior,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
+ this.contentInsertionConfiguration,
this.contextMenuBuilder,
this.spellCheckConfiguration,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
- }) : assert(controller != null),
- assert(focusNode != null),
- assert(obscuringCharacter != null && obscuringCharacter.length == 1),
- assert(obscureText != null),
- assert(autocorrect != null),
+ }) : assert(obscuringCharacter.length == 1),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
- assert(enableSuggestions != null),
- assert(showSelectionHandles != null),
- assert(readOnly != null),
- assert(forceLine != null),
- assert(style != null),
- assert(cursorColor != null),
- assert(cursorOpacityAnimates != null),
- assert(paintCursorAboveText != null),
- assert(backgroundCursorColor != null),
- assert(selectionHeightStyle != null),
- assert(selectionWidthStyle != null),
- assert(textAlign != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
- assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
- assert(autofocus != null),
- assert(rendererIgnoresPointer != null),
- assert(scrollPadding != null),
- assert(dragStartBehavior != null),
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
toolbarOptions = selectionControls is TextSelectionHandleControls && toolbarOptions == null ? ToolbarOptions.empty : toolbarOptions ??
(obscureText
@@ -738,8 +845,6 @@
selectAll: true,
paste: true,
))),
- assert(clipBehavior != null),
- assert(enableIMEPersonalizedLearning != null),
assert(
spellCheckConfiguration == null ||
spellCheckConfiguration == const SpellCheckConfiguration.disabled() ||
@@ -1232,6 +1337,18 @@
/// By default, [onSubmitted] is called after [onChanged] when the user
/// has finalized editing; or, if the default behavior has been overridden,
/// after [onEditingComplete]. See [onEditingComplete] for details.
+ ///
+ /// ## Testing
+ /// The following is the recommended way to trigger [onSubmitted] in a test:
+ ///
+ /// ```dart
+ /// await tester.testTextInput.receiveAction(TextInputAction.done);
+ /// ```
+ ///
+ /// Sending a `LogicalKeyboardKey.enter` via `tester.sendKeyEvent` will not
+ /// trigger [onSubmitted]. This is because on a real device, the engine
+ /// translates the enter key to a done action, but `tester.sendKeyEvent` sends
+ /// the key to the framework only.
/// {@endtemplate}
final ValueChanged<String>? onSubmitted;
@@ -1595,6 +1712,37 @@
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
+ /// {@template flutter.widgets.editableText.contentInsertionConfiguration}
+ /// Configuration of handler for media content inserted via the system input
+ /// method.
+ ///
+ /// Defaults to null in which case media content insertion will be disabled,
+ /// and the system will display a message informing the user that the text field
+ /// does not support inserting media content.
+ ///
+ /// Set [ContentInsertionConfiguration.onContentInserted] to provide a handler.
+ /// Additionally, set [ContentInsertionConfiguration.allowedMimeTypes]
+ /// to limit the allowable mime types for inserted content.
+ ///
+ /// {@tool dartpad}
+ ///
+ /// This example shows how to access the data for inserted content in your
+ /// `TextField`.
+ ///
+ /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
+ /// {@end-tool}
+ ///
+ /// If [contentInsertionConfiguration] is not provided, by default
+ /// an empty list of mime types will be sent to the Flutter Engine.
+ /// A handler function must be provided in order to customize the allowable
+ /// mime types for inserted content.
+ ///
+ /// If rich content is inserted without a handler, the system will display
+ /// a message informing the user that the current text input does not support
+ /// inserting rich content.
+ /// {@endtemplate}
+ final ContentInsertionConfiguration? contentInsertionConfiguration;
+
/// {@template flutter.widgets.EditableText.contextMenuBuilder}
/// Builds the text selection toolbar when requested by the user.
///
@@ -1625,6 +1773,8 @@
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
/// button Widgets for the current platform given
/// [ContextMenuButtonItem]s.
+ /// * [BrowserContextMenu], which allows the browser's context menu on web
+ /// to be disabled and Flutter-rendered context menus to appear.
/// {@endtemplate}
///
/// If not provided, no context menu will be shown.
@@ -1883,6 +2033,7 @@
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableInteractiveSelection', enableInteractiveSelection, defaultValue: true));
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
+ properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
}
}
@@ -1901,7 +2052,7 @@
final GlobalKey _editableKey = GlobalKey();
/// Detects whether the clipboard can paste.
- final ClipboardStatusNotifier? clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
+ final ClipboardStatusNotifier clipboardStatus = ClipboardStatusNotifier();
TextInputConnection? _textInputConnection;
bool get _hasInputConnection => _textInputConnection?.attached ?? false;
@@ -1947,7 +2098,9 @@
/// These results will be updated via calls to spell check through a
/// [SpellCheckService] and used by this widget to build the [TextSpan] tree
/// for text input and menus for replacement suggestions of misspelled words.
- SpellCheckResults? _spellCheckResults;
+ SpellCheckResults? spellCheckResults;
+
+ bool get _spellCheckResultsReceived => spellCheckEnabled && spellCheckResults != null && spellCheckResults!.suggestionSpans.isNotEmpty;
/// Whether to create an input connection with the platform for text editing
/// or not.
@@ -2002,8 +2155,7 @@
return widget.toolbarOptions.paste && !widget.readOnly;
}
return !widget.readOnly
- && (clipboardStatus == null
- || clipboardStatus!.value == ClipboardStatus.pasteable);
+ && (clipboardStatus.value == ClipboardStatus.pasteable);
}
@override
@@ -2052,7 +2204,6 @@
@override
void copySelection(SelectionChangedCause cause) {
final TextSelection selection = textEditingValue.selection;
- assert(selection != null);
if (selection.isCollapsed || widget.obscureText) {
return;
}
@@ -2081,7 +2232,7 @@
break;
}
}
- clipboardStatus?.update();
+ clipboardStatus.update();
}
/// Cut current selection to [Clipboard].
@@ -2092,7 +2243,6 @@
}
final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text;
- assert(selection != null);
if (selection.isCollapsed) {
return;
}
@@ -2107,7 +2257,7 @@
});
hideToolbar();
}
- clipboardStatus?.update();
+ clipboardStatus.update();
}
/// Paste text from [Clipboard].
@@ -2117,7 +2267,6 @@
return;
}
final TextSelection selection = textEditingValue.selection;
- assert(selection != null);
if (!selection.isValid) {
return;
}
@@ -2190,6 +2339,63 @@
}
}
+ /// Replace composing region with specified text.
+ void replaceComposingRegion(SelectionChangedCause cause, String text) {
+ // Replacement cannot be performed if the text is read only or obscured.
+ assert(!widget.readOnly && !widget.obscureText);
+
+ _replaceText(ReplaceTextIntent(textEditingValue, text, textEditingValue.composing, cause));
+
+ if (cause == SelectionChangedCause.toolbar) {
+ // Schedule a call to bringIntoView() after renderEditable updates.
+ SchedulerBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ bringIntoView(textEditingValue.selection.extent);
+ }
+ });
+ hideToolbar();
+ }
+ }
+
+ /// Finds specified [SuggestionSpan] that matches the provided index using
+ /// binary search.
+ ///
+ /// See also:
+ ///
+ /// * [SpellCheckSuggestionsToolbar], the Material style spell check
+ /// suggestions toolbar that uses this method to render the correct
+ /// suggestions in the toolbar for a misspelled word.
+ 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
+ // of range that suggestionSpans covers.
+ return null;
+ }
+
+ final List<SuggestionSpan> suggestionSpans = spellCheckResults!.suggestionSpans;
+ int leftIndex = 0;
+ int rightIndex = suggestionSpans.length - 1;
+ int midIndex = 0;
+
+ while (leftIndex <= rightIndex) {
+ midIndex = ((leftIndex + rightIndex) / 2).floor();
+ final int currentSpanStart = suggestionSpans[midIndex].range.start;
+ final int currentSpanEnd = suggestionSpans[midIndex].range.end;
+
+ if (cursorIndex <= currentSpanEnd && cursorIndex >= currentSpanStart) {
+ return suggestionSpans[midIndex];
+ }
+ else if (cursorIndex <= currentSpanStart) {
+ rightIndex = midIndex - 1;
+ }
+ else {
+ leftIndex = midIndex + 1;
+ }
+ }
+ return null;
+ }
+
/// Infers the [SpellCheckConfiguration] used to perform spell check.
///
/// If spell check is enabled, this will try to infer a value for
@@ -2226,9 +2432,9 @@
if (toolbarOptions.cut && cutEnabled)
ContextMenuButtonItem(
onPressed: () {
- selectAll(SelectionChangedCause.toolbar);
+ cutSelection(SelectionChangedCause.toolbar);
},
- type: ContextMenuButtonType.selectAll,
+ type: ContextMenuButtonType.cut,
),
if (toolbarOptions.copy && copyEnabled)
ContextMenuButtonItem(
@@ -2237,7 +2443,7 @@
},
type: ContextMenuButtonType.copy,
),
- if (toolbarOptions.paste && clipboardStatus != null && pasteEnabled)
+ if (toolbarOptions.paste && pasteEnabled)
ContextMenuButtonItem(
onPressed: () {
pasteText(SelectionChangedCause.toolbar);
@@ -2269,7 +2475,7 @@
final InlineSpan span = renderEditable.text!;
final String prevText = span.toPlainText();
final String currText = textEditingValue.text;
- if (prevText != currText || selection == null || !selection.isValid || selection.isCollapsed) {
+ if (prevText != currText || !selection.isValid || selection.isCollapsed) {
return _GlyphHeights(
start: renderEditable.preferredLineHeight,
end: renderEditable.preferredLineHeight,
@@ -2338,7 +2544,7 @@
/// button Widgets for the current platform given [ContextMenuButtonItem]s.
List<ContextMenuButtonItem> get contextMenuButtonItems {
return buttonItemsForToolbarOptions() ?? EditableText.getEditableButtonItems(
- clipboardStatus: clipboardStatus?.value,
+ clipboardStatus: clipboardStatus.value,
onCopy: copyEnabled
? () => copySelection(SelectionChangedCause.toolbar)
: null,
@@ -2359,7 +2565,7 @@
@override
void initState() {
super.initState();
- clipboardStatus?.addListener(_onChangedClipboardStatus);
+ clipboardStatus.addListener(_onChangedClipboardStatus);
widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(_onEditableScroll);
@@ -2386,6 +2592,7 @@
_didAutoFocus = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted && renderEditable.hasSize) {
+ _flagInternalFocus();
FocusScope.of(context).autofocus(widget.focusNode);
}
});
@@ -2409,7 +2616,7 @@
}
// Hide the text selection toolbar on mobile when orientation changes.
- final Orientation orientation = MediaQuery.of(context).orientation;
+ final Orientation orientation = MediaQuery.orientationOf(context);
if (_lastOrientation == null) {
_lastOrientation = orientation;
return;
@@ -2483,8 +2690,8 @@
final bool canPaste = widget.selectionControls is TextSelectionHandleControls
? pasteEnabled
: widget.selectionControls?.canPaste(this) ?? false;
- if (widget.selectionEnabled && pasteEnabled && clipboardStatus != null && canPaste) {
- clipboardStatus!.update();
+ if (widget.selectionEnabled && pasteEnabled && canPaste) {
+ clipboardStatus.update();
}
}
@@ -2505,9 +2712,10 @@
_selectionOverlay = null;
widget.focusNode.removeListener(_handleFocusChanged);
WidgetsBinding.instance.removeObserver(this);
- clipboardStatus?.removeListener(_onChangedClipboardStatus);
- clipboardStatus?.dispose();
+ clipboardStatus.removeListener(_onChangedClipboardStatus);
+ clipboardStatus.dispose();
_cursorVisibilityNotifier.dispose();
+ FocusManager.instance.removeListener(_unflagInternalFocus);
super.dispose();
assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
}
@@ -2562,9 +2770,12 @@
// `selection` is the only change.
_handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard);
} else {
- // Only hide the toolbar overlay, the selection handle's visibility will be handled
- // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673
- hideToolbar(false);
+ if (value.text != _value.text) {
+ // Hide the toolbar if the text was changed, but only hide the toolbar
+ // overlay; the selection handle's visibility will be handled
+ // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673
+ hideToolbar(false);
+ }
_currentPromptRectRange = null;
final bool revealObscuredInput = _hasInputConnection
@@ -2635,6 +2846,12 @@
widget.onAppPrivateCommand?.call(action, data);
}
+ @override
+ void insertContent(KeyboardInsertedContent content) {
+ assert(widget.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false);
+ widget.contentInsertionConfiguration?.onContentInserted.call(content);
+ }
+
// The original position of the caret on FloatingCursorDragState.start.
Rect? _startCaretRect;
@@ -2671,7 +2888,7 @@
// we cache the position.
_pointOffsetOrigin = point.offset;
- final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
+ final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
@@ -2702,9 +2919,11 @@
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
if (_floatingCursorResetController!.isCompleted) {
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
- if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset) {
+ // Only change if the current selection range is collapsed, to prevent
+ // overwriting the result of the iOS keyboard selection gesture.
+ if (renderEditable.selection!.isCollapsed) {
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
- _handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
+ _handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress);
}
_startCaretRect = null;
_lastTextPosition = null;
@@ -2918,8 +3137,7 @@
? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration)
: TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
_updateSizeAndTransform();
- _updateComposingRectIfNeeded();
- _updateCaretRectIfNeeded();
+ _schedulePeriodicPostFrameCallbacks();
final TextStyle style = widget.style;
_textInputConnection!
..setStyle(
@@ -2947,6 +3165,7 @@
_textInputConnection!.close();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
+ removeTextPlaceholder();
}
}
@@ -3019,6 +3238,23 @@
}
}
+ // Indicates that a call to _handleFocusChanged originated within
+ // EditableText, allowing it to distinguish between internal and external
+ // focus changes.
+ bool _nextFocusChangeIsInternal = false;
+
+ // Sets _nextFocusChangeIsInternal to true only until any subsequent focus
+ // change happens.
+ void _flagInternalFocus() {
+ _nextFocusChangeIsInternal = true;
+ FocusManager.instance.addListener(_unflagInternalFocus);
+ }
+
+ void _unflagInternalFocus() {
+ _nextFocusChangeIsInternal = false;
+ FocusManager.instance.removeListener(_unflagInternalFocus);
+ }
+
/// Express interest in interacting with the keyboard.
///
/// If this control is already attached to the keyboard, this function will
@@ -3030,6 +3266,7 @@
if (_hasFocus) {
_openInputConnection();
} else {
+ _flagInternalFocus();
widget.focusNode.requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged.
}
}
@@ -3201,9 +3438,16 @@
if (selection.isCollapsed) {
rectToReveal = targetOffset.rect;
} else {
- final List<Rect> selectionBoxes = renderEditable.getBoxesForSelection(selection);
- rectToReveal = selection.baseOffset < selection.extentOffset ?
- selectionBoxes.last : selectionBoxes.first;
+ final List<TextBox> selectionBoxes = renderEditable.getBoxesForSelection(selection);
+ // selectionBoxes may be empty if, for example, the selection does not
+ // encompass a full character, like if it only contained part of an
+ // extended grapheme cluster.
+ if (selectionBoxes.isEmpty) {
+ rectToReveal = targetOffset.rect;
+ } else {
+ rectToReveal = selection.baseOffset < selection.extentOffset ?
+ selectionBoxes.last.toRect() : selectionBoxes.first.toRect();
+ }
}
if (withAnimation) {
@@ -3232,17 +3476,21 @@
@override
void didChangeMetrics() {
- if (_lastBottomViewInset != WidgetsBinding.instance.window.viewInsets.bottom) {
+ if (!mounted) {
+ return;
+ }
+ final ui.FlutterView view = View.of(context);
+ if (_lastBottomViewInset != view.viewInsets.bottom) {
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_selectionOverlay?.updateForScroll();
});
- if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) {
+ if (_lastBottomViewInset < view.viewInsets.bottom) {
// Because the metrics change signal from engine will come here every frame
// (on both iOS and Android). So we don't need to show caret with animation.
_scheduleShowCaretOnScreen(withAnimation: false);
}
}
- _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
+ _lastBottomViewInset = view.viewInsets.bottom;
}
Future<void> _performSpellCheck(final String text) async {
@@ -3254,17 +3502,17 @@
'Locale must be specified in widget or Localization widget must be in scope',
);
- final List<SuggestionSpan>? spellCheckResults = await
+ final List<SuggestionSpan>? suggestions = await
_spellCheckConfiguration
.spellCheckService!
.fetchSpellCheckSuggestions(localeForSpellChecking!, text);
- if (spellCheckResults == null) {
+ if (suggestions == null) {
// The request to fetch spell check suggestions was canceled due to ongoing request.
return;
}
- _spellCheckResults = SpellCheckResults(text, spellCheckResults);
+ spellCheckResults = SpellCheckResults(text, suggestions);
renderEditable.text = buildTextSpan();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
@@ -3445,11 +3693,23 @@
if (_hasFocus) {
// Listen for changing viewInsets, which indicates keyboard showing up.
WidgetsBinding.instance.addObserver(this);
- _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
+ _lastBottomViewInset = View.of(context).viewInsets.bottom;
if (!widget.readOnly) {
_scheduleShowCaretOnScreen(withAnimation: true);
}
- if (!_value.selection.isValid) {
+ final bool shouldSelectAll = widget.selectionEnabled && kIsWeb
+ && !_isMultiline && !_nextFocusChangeIsInternal;
+ if (shouldSelectAll) {
+ // On native web, single line <input> tags select all when receiving
+ // focus.
+ _handleSelectionChanged(
+ TextSelection(
+ baseOffset: 0,
+ extentOffset: _value.text.length,
+ ),
+ null,
+ );
+ } else if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
}
@@ -3460,6 +3720,33 @@
updateKeepAlive();
}
+ void _compositeCallback(Layer layer) {
+ // The callback can be invoked when the layer is detached.
+ // The input connection can be closed by the platform in which case this
+ // widget doesn't rebuild.
+ if (!renderEditable.attached || !_hasInputConnection) {
+ return;
+ }
+ assert(mounted);
+ assert((context as Element).debugIsActive);
+ _updateSizeAndTransform();
+ }
+
+ void _updateSizeAndTransform() {
+ final Size size = renderEditable.size;
+ final Matrix4 transform = renderEditable.getTransformTo(null);
+ _textInputConnection!.setEditableSizeAndTransform(size, transform);
+ }
+
+ void _schedulePeriodicPostFrameCallbacks([Duration? duration]) {
+ if (!_hasInputConnection) {
+ return;
+ }
+ _updateSelectionRects();
+ _updateComposingRectIfNeeded();
+ _updateCaretRectIfNeeded();
+ SchedulerBinding.instance.addPostFrameCallback(_schedulePeriodicPostFrameCallbacks);
+ }
_ScribbleCacheKey? _scribbleCacheKey;
void _updateSelectionRects({bool force = false}) {
@@ -3501,11 +3788,11 @@
final CharacterRange characterRange = CharacterRange(plainText);
while (characterRange.moveNext()) {
final int graphemeEnd = graphemeStart + characterRange.current.length;
- final List<Rect> boxes = renderEditable.getBoxesForSelection(
+ final List<TextBox> boxes = renderEditable.getBoxesForSelection(
TextSelection(baseOffset: graphemeStart, extentOffset: graphemeEnd),
);
- final Rect? box = boxes.isEmpty ? null : boxes.first;
+ final TextBox? box = boxes.isEmpty ? null : boxes.first;
if (box != null) {
final Rect paintBounds = renderEditable.paintBounds;
// Stop early when characters are already below the bottom edge of the
@@ -3513,8 +3800,8 @@
if (paintBounds.bottom <= box.top) {
break;
}
- if (paintBounds.contains(box.topLeft) || paintBounds.contains(box.bottomRight)) {
- rects.add(SelectionRect(position: graphemeStart, bounds: box));
+ if (paintBounds.contains(Offset(box.left, box.top)) || paintBounds.contains(Offset(box.right, box.bottom))) {
+ rects.add(SelectionRect(position: graphemeStart, bounds: box.toRect(), direction: box.direction));
}
}
graphemeStart = graphemeEnd;
@@ -3522,18 +3809,6 @@
_textInputConnection!.setSelectionRects(rects);
}
- void _updateSizeAndTransform() {
- if (_hasInputConnection) {
- final Size size = renderEditable.size;
- final Matrix4 transform = renderEditable.getTransformTo(null);
- _textInputConnection!.setEditableSizeAndTransform(size, transform);
- _updateSelectionRects();
- SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
- } else if (_placeholderLocation != -1) {
- removeTextPlaceholder();
- }
- }
-
// Sends the current composing rect to the iOS text input plugin via the text
// input channel. We need to keep sending the information even if no text is
// currently marked, as the information usually lags behind. The text input
@@ -3541,49 +3816,39 @@
// when the composing rect info didn't arrive in time.
void _updateComposingRectIfNeeded() {
final TextRange composingRange = _value.composing;
- if (_hasInputConnection) {
- assert(mounted);
- Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
- // Send the caret location instead if there's no marked text yet.
- if (composingRect == null) {
- assert(!composingRange.isValid || composingRange.isCollapsed);
- final int offset = composingRange.isValid ? composingRange.start : 0;
- composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
- }
- assert(composingRect != null);
- _textInputConnection!.setComposingRect(composingRect);
- SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateComposingRectIfNeeded());
+ assert(mounted);
+ Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
+ // Send the caret location instead if there's no marked text yet.
+ if (composingRect == null) {
+ assert(!composingRange.isValid || composingRange.isCollapsed);
+ final int offset = composingRange.isValid ? composingRange.start : 0;
+ composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
}
+ _textInputConnection!.setComposingRect(composingRect);
}
void _updateCaretRectIfNeeded() {
- if (_hasInputConnection) {
- if (renderEditable.selection != null && renderEditable.selection!.isValid &&
- renderEditable.selection!.isCollapsed) {
- final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
- final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
- _textInputConnection!.setCaretRect(caretRect);
- }
- SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateCaretRectIfNeeded());
+ final TextSelection? selection = renderEditable.selection;
+ if (selection == null || !selection.isValid || !selection.isCollapsed) {
+ return;
}
+ final TextPosition currentTextPosition = TextPosition(offset: selection.baseOffset);
+ final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
+ _textInputConnection!.setCaretRect(caretRect);
}
- TextDirection get _textDirection {
- final TextDirection result = widget.textDirection ?? Directionality.of(context);
- assert(result != null, '$runtimeType created without a textDirection and with no ambient Directionality.');
- return result;
- }
+ TextDirection get _textDirection => widget.textDirection ?? Directionality.of(context);
/// The renderer for this widget's descendant.
///
/// This property is typically used to notify the renderer of input gestures
/// when [RenderEditable.ignorePointer] is true.
- RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
+ late final RenderEditable renderEditable = _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@override
TextEditingValue get textEditingValue => _value;
- double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
+ double get _devicePixelRatio => MediaQuery.devicePixelRatioOf(context);
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
@@ -3601,6 +3866,7 @@
// unfocused field that previously had a selection in the same spot.
if (value == textEditingValue) {
if (!widget.focusNode.hasFocus) {
+ _flagInternalFocus();
widget.focusNode.requestFocus();
_selectionOverlay = _createSelectionOverlay();
}
@@ -3626,17 +3892,18 @@
@override
bool showToolbar() {
// 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;
}
if (_selectionOverlay == null) {
return false;
}
- clipboardStatus?.update();
+ clipboardStatus.update();
_selectionOverlay!.showToolbar();
return true;
}
@@ -3663,6 +3930,38 @@
}
}
+ /// Shows toolbar with spell check suggestions of misspelled words that are
+ /// available for click-and-replace.
+ bool showSpellCheckSuggestionsToolbar() {
+ if (!spellCheckEnabled
+ || widget.readOnly
+ || _selectionOverlay == null
+ || !_spellCheckResultsReceived) {
+ // Only attempt to show the spell check suggestions toolbar if there
+ // is a toolbar specified and spell check suggestions available to show.
+ return false;
+ }
+
+ assert(
+ _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder != null,
+ 'spellCheckSuggestionsToolbarBuilder must be defined in '
+ 'SpellCheckConfiguration to show a toolbar with spell check '
+ 'suggestions',
+ );
+
+ _selectionOverlay!
+ .showSpellCheckSuggestionsToolbar(
+ (BuildContext context) {
+ return _spellCheckConfiguration
+ .spellCheckSuggestionsToolbarBuilder!(
+ context,
+ this,
+ );
+ },
+ );
+ return true;
+ }
+
/// Shows the magnifier at the position given by `positionToShow`,
/// if there is no magnifier visible.
///
@@ -3718,7 +4017,7 @@
@override
void removeTextPlaceholder() {
- if (!widget.scribbleEnabled) {
+ if (!widget.scribbleEnabled || _placeholderLocation == -1) {
return;
}
@@ -3770,6 +4069,9 @@
keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: autofillConfiguration,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
+ allowedMimeTypes: widget.contentInsertionConfiguration == null
+ ? const <String>[]
+ : widget.contentInsertionConfiguration!.allowedMimeTypes,
);
}
@@ -3818,7 +4120,7 @@
&& (widget.selectionControls is TextSelectionHandleControls
? pasteEnabled
: pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false))
- && (clipboardStatus == null || clipboardStatus!.value == ClipboardStatus.pasteable)
+ && (clipboardStatus.value == ClipboardStatus.pasteable)
? () {
controls?.handlePaste(this);
pasteText(SelectionChangedCause.toolbar);
@@ -3826,61 +4128,66 @@
: null;
}
+ // Returns the closest boundary location to `extent` but not including `extent`
+ // itself (unless already at the start/end of the text), in the direction
+ // specified by `forward`.
+ TextPosition _moveBeyondTextBoundary(TextPosition extent, bool forward, TextBoundary textBoundary) {
+ assert(extent.offset >= 0);
+ final int newOffset = forward
+ ? textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? _value.text.length
+ // if x is a boundary defined by `textBoundary`, most textBoundaries (except
+ // LineBreaker) guarantees `x == textBoundary.getLeadingTextBoundaryAt(x)`.
+ // Use x - 1 here to make sure we don't get stuck at the fixed point x.
+ : textBoundary.getLeadingTextBoundaryAt(extent.offset - 1) ?? 0;
+ return TextPosition(offset: newOffset);
+ }
+
+ // Returns the closest boundary location to `extent`, including `extent`
+ // itself, in the direction specified by `forward`.
+ //
+ // This method returns a fixed point of itself: applying `_toTextBoundary`
+ // again on the returned TextPosition gives the same TextPosition. It's used
+ // exclusively for handling line boundaries, since performing "move to line
+ // start" more than once usually doesn't move you to the previous line.
+ TextPosition _moveToTextBoundary(TextPosition extent, bool forward, TextBoundary textBoundary) {
+ assert(extent.offset >= 0);
+ final int caretOffset;
+ switch (extent.affinity) {
+ case TextAffinity.upstream:
+ if (extent.offset < 1 && !forward) {
+ assert (extent.offset == 0);
+ return const TextPosition(offset: 0);
+ }
+ // When the text affinity is upstream, the caret is associated with the
+ // grapheme before the code unit at `extent.offset`.
+ // TODO(LongCatIsLooong): don't assume extent.offset is at a grapheme
+ // boundary, and do this instead:
+ // final int graphemeStart = CharacterRange.at(string, extent.offset).stringBeforeLength - 1;
+ caretOffset = math.max(0, extent.offset - 1);
+ break;
+ case TextAffinity.downstream:
+ caretOffset = extent.offset;
+ break;
+ }
+ // The line boundary range does not include some control characters
+ // (most notably, Line Feed), in which case there's
+ // `x ∉ getTextBoundaryAt(x)`. In case `caretOffset` points to one such
+ // control character, we define that these control characters themselves are
+ // still part of the previous line, but also exclude them from the
+ // line boundary range since they're non-printing. IOW, no additional
+ // processing needed since the LineBoundary class does exactly that.
+ return forward
+ ? TextPosition(offset: textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? _value.text.length, affinity: TextAffinity.upstream)
+ : TextPosition(offset: textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? 0);
+ }
// --------------------------- Text Editing Actions ---------------------------
- TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) {
- final TextBoundary atomicTextBoundary = widget.obscureText ? _CodeUnitBoundary(_value.text) : CharacterBoundary(_value.text);
- return intent.forward ? PushTextPosition.forward + atomicTextBoundary : PushTextPosition.backward + atomicTextBoundary;
- }
-
- TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) {
- final TextBoundary atomicTextBoundary;
- final TextBoundary boundary;
-
- if (widget.obscureText) {
- atomicTextBoundary = _CodeUnitBoundary(_value.text);
- boundary = DocumentBoundary(_value.text);
- } else {
- final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
- atomicTextBoundary = CharacterBoundary(textEditingValue.text);
- // This isn't enough. Newline characters.
- boundary = WhitespaceBoundary(textEditingValue.text) + WordBoundary(renderEditable);
- }
-
- final _MixedBoundary mixedBoundary = intent.forward
- ? _MixedBoundary(atomicTextBoundary, boundary)
- : _MixedBoundary(boundary, atomicTextBoundary);
- // Use a _MixedBoundary to make sure we don't leave invalid codepoints in
- // the field after deletion.
- return intent.forward ? PushTextPosition.forward + mixedBoundary : PushTextPosition.backward + mixedBoundary;
- }
-
- TextBoundary _linebreak(DirectionalTextEditingIntent intent) {
- final TextBoundary atomicTextBoundary;
- final TextBoundary boundary;
-
- if (widget.obscureText) {
- atomicTextBoundary = _CodeUnitBoundary(_value.text);
- boundary = DocumentBoundary(_value.text);
- } else {
- final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
- atomicTextBoundary = CharacterBoundary(textEditingValue.text);
- boundary = LineBreak(renderEditable);
- }
-
- // The _MixedBoundary is to make sure we don't leave invalid code units in
- // the field after deletion.
- // `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary,
- // since the document boundary is unique and the linebreak boundary is
- // already caret-location based.
- final TextBoundary pushed = intent.forward
- ? PushTextPosition.forward + atomicTextBoundary
- : PushTextPosition.backward + atomicTextBoundary;
- return intent.forward ? _MixedBoundary(pushed, boundary) : _MixedBoundary(boundary, pushed);
- }
-
- TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => DocumentBoundary(_value.text);
+ TextBoundary _characterBoundary() => widget.obscureText ? _CodeUnitBoundary(_value.text) : CharacterBoundary(_value.text);
+ TextBoundary _nextWordBoundary() => widget.obscureText ? _documentBoundary() : renderEditable.wordBoundaries.moveByWordBoundary;
+ TextBoundary _linebreak() => widget.obscureText ? _documentBoundary() : LineBoundary(renderEditable);
+ TextBoundary _paragraphBoundary() => ParagraphBoundary(_value.text);
+ TextBoundary _documentBoundary() => DocumentBoundary(_value.text);
Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
return Action<T>.overridable(context: context, defaultAction: defaultAction);
@@ -3895,7 +4202,6 @@
/// When the cursor is at the start of the text, does nothing.
void _transposeCharacters(TransposeCharactersIntent intent) {
if (_value.text.characters.length <= 1
- || _value.selection == null
|| !_value.selection.isCollapsed
|| _value.selection.baseOffset == 0) {
return;
@@ -4057,40 +4363,6 @@
late final _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent> _verticalSelectionUpdateAction =
_UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent>(this);
- void _expandSelectionToDocumentBoundary(ExpandSelectionToDocumentBoundaryIntent intent) {
- final TextBoundary textBoundary = _documentBoundary(intent);
- _expandSelection(intent.forward, textBoundary, true);
- }
-
- void _expandSelectionToLinebreak(ExpandSelectionToLineBreakIntent intent) {
- final TextBoundary textBoundary = _linebreak(intent);
- _expandSelection(intent.forward, textBoundary);
- }
-
- void _expandSelection(bool forward, TextBoundary textBoundary, [bool extentAtIndex = false]) {
- final TextSelection textBoundarySelection = _value.selection;
- if (!textBoundarySelection.isValid) {
- return;
- }
-
- final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset;
- final bool towardsExtent = forward == inOrder;
- final TextPosition position = towardsExtent
- ? textBoundarySelection.extent
- : textBoundarySelection.base;
-
- final TextPosition newExtent = forward
- ? textBoundary.getTrailingTextBoundaryAt(position)
- : textBoundary.getLeadingTextBoundaryAt(position);
-
- final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed || extentAtIndex);
- userUpdateTextEditingValue(
- _value.copyWith(selection: newSelection),
- SelectionChangedCause.keyboard,
- );
- bringIntoView(newSelection.extent);
- }
-
Object? _hideToolbarIfVisible(DismissIntent intent) {
if (_selectionOverlay?.toolbarIsVisible ?? false) {
hideToolbar(false);
@@ -4144,24 +4416,27 @@
DismissIntent: CallbackAction<DismissIntent>(onInvoke: _hideToolbarIfVisible),
// Delete
- DeleteCharacterIntent: _makeOverridable(_DeleteTextAction<DeleteCharacterIntent>(this, _characterBoundary)),
- DeleteToNextWordBoundaryIntent: _makeOverridable(_DeleteTextAction<DeleteToNextWordBoundaryIntent>(this, _nextWordBoundary)),
- DeleteToLineBreakIntent: _makeOverridable(_DeleteTextAction<DeleteToLineBreakIntent>(this, _linebreak)),
+ DeleteCharacterIntent: _makeOverridable(_DeleteTextAction<DeleteCharacterIntent>(this, _characterBoundary, _moveBeyondTextBoundary)),
+ DeleteToNextWordBoundaryIntent: _makeOverridable(_DeleteTextAction<DeleteToNextWordBoundaryIntent>(this, _nextWordBoundary, _moveBeyondTextBoundary)),
+ DeleteToLineBreakIntent: _makeOverridable(_DeleteTextAction<DeleteToLineBreakIntent>(this, _linebreak, _moveToTextBoundary)),
// Extend/Move Selection
- ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary)),
+ ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, _characterBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: false)),
ExtendSelectionByPageIntent: _makeOverridable(CallbackAction<ExtendSelectionByPageIntent>(onInvoke: _extendSelectionByPage)),
- ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
- ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
- ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
- ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _expandSelectionToDocumentBoundary)),
+ ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, _nextWordBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true)),
+ ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, _linebreak, _moveToTextBoundary, ignoreNonCollapsedSelection: true)),
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_verticalSelectionUpdateAction),
ExtendSelectionVerticallyToAdjacentPageIntent: _makeOverridable(_verticalSelectionUpdateAction),
- ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
- ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
+ ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent>(this, _paragraphBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true)),
+ ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, _documentBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true)),
+ ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(this, _nextWordBoundary, _moveBeyondTextBoundary, ignoreNonCollapsedSelection: true)),
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: _scroll),
+ // Expand Selection
+ ExpandSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExpandSelectionToLineBreakIntent>(this, _linebreak, _moveToTextBoundary, ignoreNonCollapsedSelection: true, isExpand: true)),
+ ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(this, _documentBoundary, _moveToTextBoundary, ignoreNonCollapsedSelection: true, isExpand: true, extentAtIndex: true)),
+
// Copy Paste
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
@@ -4176,100 +4451,104 @@
super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls? controls = widget.selectionControls;
- return TextFieldTapRegion(
- onTapOutside: widget.onTapOutside ?? _defaultOnTapOutside,
- debugLabel: kReleaseMode ? null : 'EditableText',
- child: MouseRegion(
- cursor: widget.mouseCursor ?? SystemMouseCursors.text,
- child: Actions(
- actions: _actions,
- child: _TextEditingHistory(
- controller: widget.controller,
- onTriggered: (TextEditingValue value) {
- userUpdateTextEditingValue(value, SelectionChangedCause.keyboard);
- },
- child: Focus(
- focusNode: widget.focusNode,
- includeSemantics: false,
- debugLabel: kReleaseMode ? null : 'EditableText',
- child: Scrollable(
- key: _scrollableKey,
- excludeFromSemantics: true,
- axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
- controller: _scrollController,
- physics: widget.scrollPhysics,
- dragStartBehavior: widget.dragStartBehavior,
- restorationId: widget.restorationId,
- // If a ScrollBehavior is not provided, only apply scrollbars when
- // multiline. The overscroll indicator should not be applied in
- // either case, glowing or stretching.
- scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(
- scrollbars: _isMultiline,
- overscroll: false,
- ),
- viewportBuilder: (BuildContext context, ViewportOffset offset) {
- return CompositedTransformTarget(
- link: _toolbarLayerLink,
- child: Semantics(
- onCopy: _semanticsOnCopy(controls),
- onCut: _semanticsOnCut(controls),
- onPaste: _semanticsOnPaste(controls),
- child: _ScribbleFocusable(
- focusNode: widget.focusNode,
- editableKey: _editableKey,
- enabled: widget.scribbleEnabled,
- updateSelectionRects: () {
- _openInputConnection();
- _updateSelectionRects(force: true);
- },
- child: _Editable(
- key: _editableKey,
- startHandleLayerLink: _startHandleLayerLink,
- endHandleLayerLink: _endHandleLayerLink,
- inlineSpan: buildTextSpan(),
- value: _value,
- cursorColor: _cursorColor,
- backgroundCursorColor: widget.backgroundCursorColor,
- showCursor: EditableText.debugDeterministicCursor
- ? ValueNotifier<bool>(widget.showCursor)
- : _cursorVisibilityNotifier,
- forceLine: widget.forceLine,
- readOnly: widget.readOnly,
- hasFocus: _hasFocus,
- maxLines: widget.maxLines,
- minLines: widget.minLines,
- expands: widget.expands,
- strutStyle: widget.strutStyle,
- selectionColor: widget.selectionColor,
- textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
- textAlign: widget.textAlign,
- textDirection: _textDirection,
- locale: widget.locale,
- textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context),
- textWidthBasis: widget.textWidthBasis,
- obscuringCharacter: widget.obscuringCharacter,
- obscureText: widget.obscureText,
- offset: offset,
- onCaretChanged: _handleCaretChanged,
- rendererIgnoresPointer: widget.rendererIgnoresPointer,
- cursorWidth: widget.cursorWidth,
- cursorHeight: widget.cursorHeight,
- cursorRadius: widget.cursorRadius,
- cursorOffset: widget.cursorOffset ?? Offset.zero,
- selectionHeightStyle: widget.selectionHeightStyle,
- selectionWidthStyle: widget.selectionWidthStyle,
- paintCursorAboveText: widget.paintCursorAboveText,
- enableInteractiveSelection: widget._userSelectionEnabled,
- textSelectionDelegate: this,
- devicePixelRatio: _devicePixelRatio,
- promptRectRange: _currentPromptRectRange,
- promptRectColor: widget.autocorrectionTextRectColor,
- clipBehavior: widget.clipBehavior,
+ return _CompositionCallback(
+ compositeCallback: _compositeCallback,
+ enabled: _hasInputConnection,
+ child: TextFieldTapRegion(
+ onTapOutside: widget.onTapOutside ?? _defaultOnTapOutside,
+ debugLabel: kReleaseMode ? null : 'EditableText',
+ child: MouseRegion(
+ cursor: widget.mouseCursor ?? SystemMouseCursors.text,
+ child: Actions(
+ actions: _actions,
+ child: _TextEditingHistory(
+ controller: widget.controller,
+ onTriggered: (TextEditingValue value) {
+ userUpdateTextEditingValue(value, SelectionChangedCause.keyboard);
+ },
+ child: Focus(
+ focusNode: widget.focusNode,
+ includeSemantics: false,
+ debugLabel: kReleaseMode ? null : 'EditableText',
+ child: Scrollable(
+ key: _scrollableKey,
+ excludeFromSemantics: true,
+ axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
+ controller: _scrollController,
+ physics: widget.scrollPhysics,
+ dragStartBehavior: widget.dragStartBehavior,
+ restorationId: widget.restorationId,
+ // If a ScrollBehavior is not provided, only apply scrollbars when
+ // multiline. The overscroll indicator should not be applied in
+ // either case, glowing or stretching.
+ scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(
+ scrollbars: _isMultiline,
+ overscroll: false,
+ ),
+ viewportBuilder: (BuildContext context, ViewportOffset offset) {
+ return CompositedTransformTarget(
+ link: _toolbarLayerLink,
+ child: Semantics(
+ onCopy: _semanticsOnCopy(controls),
+ onCut: _semanticsOnCut(controls),
+ onPaste: _semanticsOnPaste(controls),
+ child: _ScribbleFocusable(
+ focusNode: widget.focusNode,
+ editableKey: _editableKey,
+ enabled: widget.scribbleEnabled,
+ updateSelectionRects: () {
+ _openInputConnection();
+ _updateSelectionRects(force: true);
+ },
+ child: _Editable(
+ key: _editableKey,
+ startHandleLayerLink: _startHandleLayerLink,
+ endHandleLayerLink: _endHandleLayerLink,
+ inlineSpan: buildTextSpan(),
+ value: _value,
+ cursorColor: _cursorColor,
+ backgroundCursorColor: widget.backgroundCursorColor,
+ showCursor: EditableText.debugDeterministicCursor
+ ? ValueNotifier<bool>(widget.showCursor)
+ : _cursorVisibilityNotifier,
+ forceLine: widget.forceLine,
+ readOnly: widget.readOnly,
+ hasFocus: _hasFocus,
+ maxLines: widget.maxLines,
+ minLines: widget.minLines,
+ expands: widget.expands,
+ strutStyle: widget.strutStyle,
+ selectionColor: widget.selectionColor,
+ textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
+ textAlign: widget.textAlign,
+ textDirection: _textDirection,
+ locale: widget.locale,
+ textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context),
+ textWidthBasis: widget.textWidthBasis,
+ obscuringCharacter: widget.obscuringCharacter,
+ obscureText: widget.obscureText,
+ offset: offset,
+ onCaretChanged: _handleCaretChanged,
+ rendererIgnoresPointer: widget.rendererIgnoresPointer,
+ cursorWidth: widget.cursorWidth,
+ cursorHeight: widget.cursorHeight,
+ cursorRadius: widget.cursorRadius,
+ cursorOffset: widget.cursorOffset ?? Offset.zero,
+ selectionHeightStyle: widget.selectionHeightStyle,
+ selectionWidthStyle: widget.selectionWidthStyle,
+ paintCursorAboveText: widget.paintCursorAboveText,
+ enableInteractiveSelection: widget._userSelectionEnabled,
+ textSelectionDelegate: this,
+ devicePixelRatio: _devicePixelRatio,
+ promptRectRange: _currentPromptRectRange,
+ promptRectColor: widget.autocorrectionTextRectColor,
+ clipBehavior: widget.clipBehavior,
+ ),
),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
),
@@ -4319,9 +4598,8 @@
],
);
}
- final bool spellCheckResultsReceived = spellCheckEnabled && _spellCheckResults != null;
final bool withComposing = !widget.readOnly && _hasFocus;
- if (spellCheckResultsReceived) {
+ if (_spellCheckResultsReceived) {
// If the composing range is out of range for the current text, ignore it to
// preserve the tree integrity, otherwise in release mode a RangeError will
// be thrown and this EditableText will be built with a broken subtree.
@@ -4334,7 +4612,7 @@
composingRegionOutOfRange,
widget.style,
_spellCheckConfiguration.misspelledTextStyle!,
- _spellCheckResults!,
+ spellCheckResults!,
);
}
@@ -4389,9 +4667,7 @@
this.promptRectRange,
this.promptRectColor,
required this.clipBehavior,
- }) : assert(textDirection != null),
- assert(rendererIgnoresPointer != null),
- super(children: _extractChildren(inlineSpan));
+ }) : super(children: _extractChildren(inlineSpan));
// Traverses the InlineSpan tree and depth-first collects the list of
// child widgets that are created in WidgetSpans.
@@ -4679,8 +4955,7 @@
super.alignment,
super.baseline,
required this.size,
- }) : assert(child != null),
- assert(baseline != null || !(
+ }) : assert(baseline != null || !(
identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
identical(alignment, ui.PlaceholderAlignment.belowBaseline) ||
identical(alignment, ui.PlaceholderAlignment.baseline)
@@ -4712,7 +4987,7 @@
///
/// This text boundary treats every character in input string as an utf-16 code
/// unit. This can be useful when handling text without any grapheme cluster,
-/// e.g. the obscure string in [EditableText]. If you are handling text that may
+/// e.g. password input in [EditableText]. If you are handling text that may
/// include grapheme clusters, consider using [CharacterBoundary].
class _CodeUnitBoundary extends TextBoundary {
const _CodeUnitBoundary(this._text);
@@ -4720,113 +4995,51 @@
final String _text;
@override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) {
- if (position.offset <= 0) {
- return const TextPosition(offset: 0);
- }
- if (position.offset > _text.length ||
- (position.offset == _text.length && position.affinity == TextAffinity.downstream)) {
- return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
- }
- switch (position.affinity) {
- case TextAffinity.upstream:
- return TextPosition(offset: math.min(position.offset - 1, _text.length));
- case TextAffinity.downstream:
- return TextPosition(offset: math.min(position.offset, _text.length));
- }
- }
-
+ int getLeadingTextBoundaryAt(int position) => position.clamp(0, _text.length); // ignore_clamp_double_lint
@override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) {
- if (position.offset < 0 ||
- (position.offset == 0 && position.affinity == TextAffinity.upstream)) {
- return const TextPosition(offset: 0);
- }
- if (position.offset >= _text.length) {
- return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
- }
- switch (position.affinity) {
- case TextAffinity.upstream:
- return TextPosition(offset: math.min(position.offset, _text.length), affinity: TextAffinity.upstream);
- case TextAffinity.downstream:
- return TextPosition(offset: math.min(position.offset + 1, _text.length), affinity: TextAffinity.upstream);
- }
- }
-}
-
-// ------------------------ Text Boundary Combinators ------------------------
-
-// A _TextBoundary that creates a [TextRange] where its start is from the
-// specified leading text boundary and its end is from the specified trailing
-// text boundary.
-class _MixedBoundary extends TextBoundary {
- _MixedBoundary(
- this.leadingTextBoundary,
- this.trailingTextBoundary
- );
-
- final TextBoundary leadingTextBoundary;
- final TextBoundary trailingTextBoundary;
-
- @override
- TextPosition getLeadingTextBoundaryAt(TextPosition position) => leadingTextBoundary.getLeadingTextBoundaryAt(position);
-
- @override
- TextPosition getTrailingTextBoundaryAt(TextPosition position) => trailingTextBoundary.getTrailingTextBoundaryAt(position);
+ int getTrailingTextBoundaryAt(int position) => (position + 1).clamp(0, _text.length); // ignore_clamp_double_lint
}
// ------------------------------- Text Actions -------------------------------
class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextAction<T> {
- _DeleteTextAction(this.state, this.getTextBoundariesForIntent);
+ _DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary);
final EditableTextState state;
- final TextBoundary Function(T intent) getTextBoundariesForIntent;
-
- TextRange _expandNonCollapsedRange(TextEditingValue value) {
- final TextRange selection = value.selection;
- assert(selection.isValid);
- assert(!selection.isCollapsed);
- final TextBoundary atomicBoundary = state.widget.obscureText
- ? _CodeUnitBoundary(value.text)
- : CharacterBoundary(value.text);
-
- return TextRange(
- start: atomicBoundary.getLeadingTextBoundaryAt(TextPosition(offset: selection.start)).offset,
- end: atomicBoundary.getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1)).offset,
- );
- }
+ final TextBoundary Function() getTextBoundary;
+ final _ApplyTextBoundary _applyTextBoundary;
@override
Object? invoke(T intent, [BuildContext? context]) {
final TextSelection selection = state._value.selection;
- assert(selection.isValid);
-
- if (!selection.isCollapsed) {
- return Actions.invoke(
- context!,
- ReplaceTextIntent(state._value, '', _expandNonCollapsedRange(state._value), SelectionChangedCause.keyboard),
- );
- }
-
- final TextBoundary textBoundary = getTextBoundariesForIntent(intent);
- if (!state._value.selection.isValid) {
+ if (!selection.isValid) {
return null;
}
- if (!state._value.selection.isCollapsed) {
+ assert(selection.isValid);
+ // Expands the selection to ensure the range covers full graphemes.
+ final TextBoundary atomicBoundary = state._characterBoundary();
+ if (!selection.isCollapsed) {
+ // Expands the selection to ensure the range covers full graphemes.
+ final TextRange range = TextRange(
+ start: atomicBoundary.getLeadingTextBoundaryAt(selection.start) ?? state._value.text.length,
+ end: atomicBoundary.getTrailingTextBoundaryAt(selection.end - 1) ?? 0,
+ );
return Actions.invoke(
context!,
- ReplaceTextIntent(state._value, '', _expandNonCollapsedRange(state._value), SelectionChangedCause.keyboard),
+ ReplaceTextIntent(state._value, '', range, SelectionChangedCause.keyboard),
);
}
+ final int target = _applyTextBoundary(selection.base, intent.forward, getTextBoundary()).offset;
+
+ final TextRange rangeToDelete = TextSelection(
+ baseOffset: intent.forward
+ ? atomicBoundary.getLeadingTextBoundaryAt(selection.baseOffset) ?? state._value.text.length
+ : atomicBoundary.getTrailingTextBoundaryAt(selection.baseOffset - 1) ?? 0,
+ extentOffset: target,
+ );
return Actions.invoke(
context!,
- ReplaceTextIntent(
- state._value,
- '',
- textBoundary.getTextBoundaryAt(state._value.selection.base),
- SelectionChangedCause.keyboard,
- ),
+ ReplaceTextIntent(state._value, '', rangeToDelete, SelectionChangedCause.keyboard),
);
}
@@ -4837,13 +5050,19 @@
class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
_UpdateTextSelectionAction(
this.state,
- this.ignoreNonCollapsedSelection,
- this.getTextBoundariesForIntent,
- );
+ this.getTextBoundary,
+ this.applyTextBoundary, {
+ required this.ignoreNonCollapsedSelection,
+ this.isExpand = false,
+ this.extentAtIndex = false,
+ });
final EditableTextState state;
final bool ignoreNonCollapsedSelection;
- final TextBoundary Function(T intent) getTextBoundariesForIntent;
+ final bool isExpand;
+ final bool extentAtIndex;
+ final TextBoundary Function() getTextBoundary;
+ final _ApplyTextBoundary applyTextBoundary;
static const int NEWLINE_CODE_UNIT = 10;
@@ -4874,25 +5093,14 @@
assert(selection.isValid);
final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled;
- // Collapse to the logical start/end.
- TextSelection collapse(TextSelection selection) {
- assert(selection.isValid);
- assert(!selection.isCollapsed);
- return selection.copyWith(
- baseOffset: intent.forward ? selection.end : selection.start,
- extentOffset: intent.forward ? selection.end : selection.start,
- );
- }
-
if (!selection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) {
- return Actions.invoke(
- context!,
- UpdateSelectionIntent(state._value, collapse(selection), SelectionChangedCause.keyboard),
- );
+ return Actions.invoke(context!, UpdateSelectionIntent(
+ state._value,
+ TextSelection.collapsed(offset: intent.forward ? selection.end : selection.start),
+ SelectionChangedCause.keyboard,
+ ));
}
- final TextBoundary textBoundary = getTextBoundariesForIntent(intent);
-
TextPosition extent = selection.extent;
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
// move it just to the other side of the wordwrap.
@@ -4909,76 +5117,22 @@
}
}
- final TextPosition newExtent = intent.forward
- ? textBoundary.getTrailingTextBoundaryAt(extent)
- : textBoundary.getLeadingTextBoundaryAt(extent);
- final TextSelection newSelection = collapseSelection
+ final bool shouldTargetBase = isExpand && (intent.forward ? selection.baseOffset > selection.extentOffset : selection.baseOffset < selection.extentOffset);
+ final TextPosition newExtent = applyTextBoundary(shouldTargetBase ? selection.base : extent, intent.forward, getTextBoundary());
+ final TextSelection newSelection = collapseSelection || (!isExpand && newExtent.offset == selection.baseOffset)
? TextSelection.fromPosition(newExtent)
- : selection.extendTo(newExtent);
+ : isExpand ? selection.expandTo(newExtent, extentAtIndex || selection.isCollapsed) : selection.extendTo(newExtent);
- // If collapseAtReversal is true and would have an effect, collapse it.
- if (!selection.isCollapsed && intent.collapseAtReversal
- && (selection.baseOffset < selection.extentOffset !=
- newSelection.baseOffset < newSelection.extentOffset)) {
- return Actions.invoke(
- context!,
- UpdateSelectionIntent(
- state._value,
- TextSelection.fromPosition(selection.base),
- SelectionChangedCause.keyboard,
- ),
- );
- }
-
- return Actions.invoke(
- context!,
- UpdateSelectionIntent(state._value, newSelection, SelectionChangedCause.keyboard),
- );
+ final bool shouldCollapseToBase = intent.collapseAtReversal
+ && (selection.baseOffset - selection.extentOffset) * (selection.baseOffset - newSelection.extentOffset) < 0;
+ final TextSelection newRange = shouldCollapseToBase ? TextSelection.fromPosition(selection.base) : newSelection;
+ return Actions.invoke(context!, UpdateSelectionIntent(state._value, newRange, SelectionChangedCause.keyboard));
}
@override
bool get isActionEnabled => state._value.selection.isValid;
}
-class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> {
- _ExtendSelectionOrCaretPositionAction(this.state, this.getTextBoundariesForIntent);
-
- final EditableTextState state;
- final TextBoundary Function(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) getTextBoundariesForIntent;
-
- @override
- Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, [BuildContext? context]) {
- final TextSelection selection = state._value.selection;
- assert(selection.isValid);
-
- final TextBoundary textBoundary = getTextBoundariesForIntent(intent);
- final TextSelection textBoundarySelection = state._value.selection;
- if (!textBoundarySelection.isValid) {
- return null;
- }
-
- final TextPosition extent = textBoundarySelection.extent;
- final TextPosition newExtent = intent.forward
- ? textBoundary.getTrailingTextBoundaryAt(extent)
- : textBoundary.getLeadingTextBoundaryAt(extent);
-
- final TextSelection newSelection = (newExtent.offset - textBoundarySelection.baseOffset) * (textBoundarySelection.extentOffset - textBoundarySelection.baseOffset) < 0
- ? textBoundarySelection.copyWith(
- extentOffset: textBoundarySelection.baseOffset,
- affinity: textBoundarySelection.extentOffset > textBoundarySelection.baseOffset ? TextAffinity.downstream : TextAffinity.upstream,
- )
- : textBoundarySelection.extendTo(newExtent);
-
- return Actions.invoke(
- context!,
- UpdateSelectionIntent(state._value, newSelection, SelectionChangedCause.keyboard),
- );
- }
-
- @override
- bool get isActionEnabled => state.widget.selectionEnabled && state._value.selection.isValid;
-}
-
class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
_UpdateTextSelectionVerticallyAction(this.state);
@@ -5256,7 +5410,7 @@
// If anything has been undone in this stack, remove those irrelevant states
// before adding the new one.
- if (_index != null && _index != _list.length - 1) {
+ if (_index != _list.length - 1) {
_list.removeRange(_index + 1, _list.length);
}
_list.add(value);
diff --git a/framework/lib/src/widgets/fade_in_image.dart b/framework/lib/src/widgets/fade_in_image.dart
index e77ef9b..d346bf5 100644
--- a/framework/lib/src/widgets/fade_in_image.dart
+++ b/framework/lib/src/widgets/fade_in_image.dart
@@ -97,15 +97,7 @@
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
- }) : assert(placeholder != null),
- assert(image != null),
- assert(fadeOutDuration != null),
- assert(fadeOutCurve != null),
- assert(fadeInDuration != null),
- assert(fadeInCurve != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null);
+ });
/// Creates a widget that uses a placeholder image stored in memory while
/// loading the final image from the network.
@@ -162,18 +154,7 @@
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
- }) : assert(placeholder != null),
- assert(image != null),
- assert(placeholderScale != null),
- assert(imageScale != null),
- assert(fadeOutDuration != null),
- assert(fadeOutCurve != null),
- assert(fadeInDuration != null),
- assert(fadeInCurve != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
- placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
+ }) : placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale));
/// Creates a widget that uses a placeholder image stored in an asset bundle
@@ -235,19 +216,9 @@
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
- }) : assert(placeholder != null),
- assert(image != null),
- placeholder = placeholderScale != null
+ }) : placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
: ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
- assert(imageScale != null),
- assert(fadeOutDuration != null),
- assert(fadeOutCurve != null),
- assert(fadeInDuration != null),
- assert(fadeInCurve != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale));
/// Image displayed while the target [image] is loading.
@@ -401,7 +372,6 @@
required FilterQuality filterQuality,
required Animation<double> opacity,
}) {
- assert(image != null);
return Image(
image: image,
errorBuilder: errorBuilder,
@@ -477,14 +447,7 @@
required this.fadeInDuration,
required this.fadeInCurve,
required this.wasSynchronouslyLoaded,
- }) : assert(target != null),
- assert(placeholder != null),
- assert(isTargetLoaded != null),
- assert(fadeOutDuration != null),
- assert(fadeOutCurve != null),
- assert(fadeInDuration != null),
- assert(fadeInCurve != null),
- assert(!wasSynchronouslyLoaded || isTargetLoaded),
+ }) : assert(!wasSynchronouslyLoaded || isTargetLoaded),
super(duration: fadeInDuration + fadeOutDuration);
final Widget target;
diff --git a/framework/lib/src/widgets/focus_manager.dart b/framework/lib/src/widgets/focus_manager.dart
index f38793c..a0226ea 100644
--- a/framework/lib/src/widgets/focus_manager.dart
+++ b/framework/lib/src/widgets/focus_manager.dart
@@ -22,16 +22,38 @@
/// be logged.
bool debugFocusChanges = false;
-bool _focusDebug(String message, [Iterable<String>? details]) {
- if (debugFocusChanges) {
- debugPrint('FOCUS: $message');
- if (details != null && details.isNotEmpty) {
- for (final String detail in details) {
- debugPrint(' $detail');
- }
+// When using _focusDebug, always call it like so:
+//
+// assert(_focusDebug(() => 'Blah $foo'));
+//
+// It needs to be inside the assert in order to be removed in release mode, and
+// it needs to use a closure to generate the string in order to avoid string
+// interpolation when debugFocusChanges is false.
+//
+// It will throw a StateError if you try to call it when the app is in release
+// mode.
+bool _focusDebug(
+ String Function() messageFunc, [
+ Iterable<Object> Function()? detailsFunc,
+]) {
+ if (kReleaseMode) {
+ throw StateError(
+ '_focusDebug was called in Release mode. It should always be wrapped in '
+ 'an assert. Always call _focusDebug like so:\n'
+ r" assert(_focusDebug(() => 'Blah $foo'));"
+ );
+ }
+ if (!debugFocusChanges) {
+ return true;
+ }
+ debugPrint('FOCUS: ${messageFunc()}');
+ final Iterable<Object> details = detailsFunc?.call() ?? const <Object>[];
+ if (details.isNotEmpty) {
+ for (final Object detail in details) {
+ debugPrint(' $detail');
}
}
- // Return true so that it can be easily used inside of an assert.
+ // Return true so that it can be used inside of an assert.
return true;
}
@@ -110,7 +132,7 @@
// The widget tree is responsible for calling reparent/detach on attached
// nodes to keep their parent/manager information up-to-date, so here we can
// safely check if the scope/node involved in each autofocus request is
- // still attached, and discard the ones are no longer attached to the
+ // still attached, and discard the ones which are no longer attached to the
// original manager.
void applyIfValid(FocusManager manager) {
final bool shouldApply = (scope.parent != null || identical(scope, manager.rootScope))
@@ -118,18 +140,18 @@
&& scope.focusedChild == null
&& autofocusNode.ancestors.contains(scope);
if (shouldApply) {
- assert(_focusDebug('Applying autofocus: $autofocusNode'));
+ assert(_focusDebug(() => 'Applying autofocus: $autofocusNode'));
autofocusNode._doRequestFocus(findFirstFocus: true);
} else {
- assert(_focusDebug('Autofocus request discarded for node: $autofocusNode.'));
+ assert(_focusDebug(() => 'Autofocus request discarded for node: $autofocusNode.'));
}
}
}
/// An attachment point for a [FocusNode].
///
-/// Using a [FocusAttachment] is rarely needed, unless you are building
-/// something akin to the [Focus] or [FocusScope] widgets from scratch.
+/// Using a [FocusAttachment] is rarely needed, unless building something
+/// akin to the [Focus] or [FocusScope] widgets from scratch.
///
/// Once created, a [FocusNode] must be attached to the widget tree by its
/// _host_ [StatefulWidget] via a [FocusAttachment] object. [FocusAttachment]s
@@ -151,7 +173,7 @@
class FocusAttachment {
/// A private constructor, because [FocusAttachment]s are only to be created
/// by [FocusNode.attach].
- FocusAttachment._(this._node) : assert(_node != null);
+ FocusAttachment._(this._node);
// The focus node that this attachment manages an attachment for. The node may
// not yet have a parent, or may have been detached from this attachment, so
@@ -169,8 +191,7 @@
///
/// Calling [FocusNode.dispose] will also automatically detach the node.
void detach() {
- assert(_node != null);
- assert(_focusDebug('Detaching node:', <String>[_node.toString(), 'With enclosing scope ${_node.enclosingScope}']));
+ assert(_focusDebug(() => 'Detaching node:', () => <Object>[_node, 'With enclosing scope ${_node.enclosingScope}']));
if (isAttached) {
if (_node.hasPrimaryFocus || (_node._manager != null && _node._manager!._markedForFocus == _node)) {
_node.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
@@ -208,12 +229,10 @@
/// [FocusScope] widgets to build the focus tree, or if there is a need to
/// supply the parent explicitly (which are both uncommon).
void reparent({FocusNode? parent}) {
- assert(_node != null);
if (isAttached) {
assert(_node.context != null);
parent ??= Focus.maybeOf(_node.context!, scopeOk: true);
parent ??= _node.context!.owner!.focusManager.rootScope;
- assert(parent != null);
parent._reparent(_node);
}
}
@@ -261,8 +280,8 @@
///
/// _Please see the [Focus] and [FocusScope] widgets, which are utility widgets
/// that manage their own [FocusNode]s and [FocusScopeNode]s, respectively. If
-/// they aren't appropriate, [FocusNode]s can be managed directly, but doing
-/// this yourself is rare._
+/// they aren't appropriate, [FocusNode]s can be managed directly, but doing this
+/// is rare._
///
/// [FocusNode]s are persistent objects that form a _focus tree_ that is a
/// representation of the widgets in the hierarchy that are interested in focus.
@@ -410,10 +429,7 @@
bool canRequestFocus = true,
bool descendantsAreFocusable = true,
bool descendantsAreTraversable = true,
- }) : assert(skipTraversal != null),
- assert(canRequestFocus != null),
- assert(descendantsAreFocusable != null),
- _skipTraversal = skipTraversal,
+ }) : _skipTraversal = skipTraversal,
_canRequestFocus = canRequestFocus,
_descendantsAreFocusable = descendantsAreFocusable,
_descendantsAreTraversable = descendantsAreTraversable {
@@ -845,7 +861,6 @@
void unfocus({
UnfocusDisposition disposition = UnfocusDisposition.scope,
}) {
- assert(disposition != null);
if (!hasFocus && (_manager == null || _manager!._markedForFocus != this)) {
return;
}
@@ -884,7 +899,7 @@
scope._doRequestFocus(findFirstFocus: true);
break;
}
- assert(_focusDebug('Unfocused node:', <String>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}']));
+ assert(_focusDebug(() => 'Unfocused node:', () => <Object>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}']));
}
/// Removes the keyboard token from this focus node if it has one.
@@ -931,7 +946,6 @@
// Removes the given FocusNode and its children as a child of this node.
@mustCallSuper
void _removeChild(FocusNode node, {bool removeScopeFocus = true}) {
- assert(node != null);
assert(_children.contains(node), "Tried to remove a node that wasn't a child.");
assert(node._parent == this);
assert(node._manager == _manager);
@@ -960,7 +974,6 @@
// Used by FocusAttachment.reparent to perform the actual parenting operation.
@mustCallSuper
void _reparent(FocusNode child) {
- assert(child != null);
assert(child != this, 'Tried to make a child into a parent of itself.');
if (child._parent == this) {
assert(_children.contains(child), "Found a node that says it's a child, but doesn't appear in the child list.");
@@ -1071,9 +1084,8 @@
// Note that this is overridden in FocusScopeNode.
void _doRequestFocus({required bool findFirstFocus}) {
- assert(findFirstFocus != null);
if (!canRequestFocus) {
- assert(_focusDebug('Node NOT requesting focus because canRequestFocus is false: $this'));
+ assert(_focusDebug(() => 'Node NOT requesting focus because canRequestFocus is false: $this'));
return;
}
// If the node isn't part of the tree, then we just defer the focus request
@@ -1088,7 +1100,7 @@
return;
}
_hasKeyboardToken = true;
- assert(_focusDebug('Node requesting focus: $this'));
+ assert(_focusDebug(() => 'Node requesting focus: $this'));
_markNextFocus(this);
}
@@ -1119,7 +1131,7 @@
FocusNode scopeFocus = this;
for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
assert(scopeFocus != ancestor, 'Somehow made a loop by setting focusedChild to its scope.');
- assert(_focusDebug('Setting $scopeFocus as focused child for scope:', <String>[ancestor.toString()]));
+ assert(_focusDebug(() => 'Setting $scopeFocus as focused child for scope:', () => <Object>[ancestor]));
// Remove it anywhere in the focused child history.
ancestor._focusedChildren.remove(scopeFocus);
// Add it to the end of the list, which is also the top of the queue: The
@@ -1214,16 +1226,15 @@
super.skipTraversal,
super.canRequestFocus,
this.traversalEdgeBehavior = TraversalEdgeBehavior.closedLoop,
- }) : assert(skipTraversal != null),
- assert(canRequestFocus != null),
- super(
+ }) : super(
descendantsAreFocusable: true,
);
@override
FocusScopeNode get nearestScope => this;
- /// {@macro flutter.focus.TraversalEdgeBehavior}
+ /// Controls the transfer of focus beyond the first and the last items of a
+ /// [FocusScopeNode].
///
/// Changing this field value has no immediate effect on the UI. Instead, next time
/// focus traversal takes place [FocusTraversalPolicy] will read this value
@@ -1286,9 +1297,8 @@
/// the tree as a child of this scope. If it is already part of the focus
/// tree, the given scope must be a descendant of this scope.
void setFirstFocus(FocusScopeNode scope) {
- assert(scope != null);
assert(scope != this, 'Unexpected self-reference in setFirstFocus.');
- assert(_focusDebug('Setting scope as first focus in $this to node:', <String>[scope.toString()]));
+ assert(_focusDebug(() => 'Setting scope as first focus in $this to node:', () => <Object>[scope]));
if (scope._parent == null) {
_reparent(scope);
}
@@ -1318,14 +1328,13 @@
}
assert(_manager != null);
- assert(_focusDebug('Autofocus scheduled for $node: scope $this'));
+ assert(_focusDebug(() => 'Autofocus scheduled for $node: scope $this'));
_manager?._pendingAutofocuses.add(_Autofocus(scope: this, autofocusNode: node));
_manager?._markNeedsUpdate();
}
@override
void _doRequestFocus({required bool findFirstFocus}) {
- assert(findFirstFocus != null);
// It is possible that a previously focused child is no longer focusable.
while (this.focusedChild != null && !this.focusedChild!.canRequestFocus) {
@@ -1404,8 +1413,8 @@
/// The focus manager is responsible for tracking which [FocusNode] has the
/// primary input focus (the [primaryFocus]), holding the [FocusScopeNode] that
/// is the root of the focus tree (the [rootScope]), and what the current
-/// [highlightMode] is. It also distributes key events from [RawKeyboard] to the
-/// nodes in the focus tree.
+/// [highlightMode] is. It also distributes key events from [KeyEventManager]
+/// to the nodes in the focus tree.
///
/// The singleton [FocusManager] instance is held by the [WidgetsBinding] as
/// [WidgetsBinding.focusManager], and can be conveniently accessed using the
@@ -1466,17 +1475,11 @@
///
/// When this focus manager is no longer needed, calling [dispose] on it will
/// unregister these handlers.
- void registerGlobalHandlers() {
- assert(ServicesBinding.instance.keyEventManager.keyMessageHandler == null);
- ServicesBinding.instance.keyEventManager.keyMessageHandler = _handleKeyMessage;
- GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
- }
+ void registerGlobalHandlers() => _highlightManager.registerGlobalHandlers();
@override
void dispose() {
- if (ServicesBinding.instance.keyEventManager.keyMessageHandler == _handleKeyMessage) {
- GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent);
- }
+ _highlightManager.dispose();
super.dispose();
}
@@ -1484,6 +1487,8 @@
/// the [WidgetsBinding] instance.
static FocusManager get instance => WidgetsBinding.instance.focusManager;
+ final _HighlightModeManager _highlightManager = _HighlightModeManager();
+
/// Sets the strategy by which [highlightMode] is determined.
///
/// If set to [FocusHighlightStrategy.automatic], then the highlight mode will
@@ -1507,33 +1512,12 @@
/// most appropriate for the initial interaction mode.
///
/// Defaults to [FocusHighlightStrategy.automatic].
- FocusHighlightStrategy get highlightStrategy => _highlightStrategy;
- FocusHighlightStrategy _highlightStrategy = FocusHighlightStrategy.automatic;
- set highlightStrategy(FocusHighlightStrategy highlightStrategy) {
- _highlightStrategy = highlightStrategy;
- _updateHighlightMode();
- }
-
- static FocusHighlightMode get _defaultModeForPlatform {
- // Assume that if we're on one of the mobile platforms, and there's no mouse
- // connected, that the initial interaction will be touch-based, and that
- // it's traditional mouse and keyboard on all other platforms.
- //
- // This only affects the initial value: the ongoing value is updated to a
- // known correct value as soon as any pointer/keyboard events are received.
- switch (defaultTargetPlatform) {
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.iOS:
- if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
- return FocusHighlightMode.traditional;
- }
- return FocusHighlightMode.touch;
- case TargetPlatform.linux:
- case TargetPlatform.macOS:
- case TargetPlatform.windows:
- return FocusHighlightMode.traditional;
+ FocusHighlightStrategy get highlightStrategy => _highlightManager.strategy;
+ set highlightStrategy(FocusHighlightStrategy value) {
+ if (_highlightManager.strategy == value) {
+ return;
}
+ _highlightManager.strategy = value;
}
/// Indicates the current interaction mode for focus highlights.
@@ -1549,93 +1533,15 @@
/// draw their focus highlight whenever they are focused.
// Don't want to set _highlightMode here, since it's possible for the target
// platform to change (especially in tests).
- FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform;
- FocusHighlightMode? _highlightMode;
-
- // If set, indicates if the last interaction detected was touch or not.
- // If null, no interactions have occurred yet.
- bool? _lastInteractionWasTouch;
-
- // Update function to be called whenever the state relating to highlightMode
- // changes.
- void _updateHighlightMode() {
- final FocusHighlightMode newMode;
- switch (highlightStrategy) {
- case FocusHighlightStrategy.automatic:
- if (_lastInteractionWasTouch == null) {
- // If we don't have any information about the last interaction yet,
- // then just rely on the default value for the platform, which will be
- // determined based on the target platform if _highlightMode is not
- // set.
- return;
- }
- if (_lastInteractionWasTouch!) {
- newMode = FocusHighlightMode.touch;
- } else {
- newMode = FocusHighlightMode.traditional;
- }
- break;
- case FocusHighlightStrategy.alwaysTouch:
- newMode = FocusHighlightMode.touch;
- break;
- case FocusHighlightStrategy.alwaysTraditional:
- newMode = FocusHighlightMode.traditional;
- break;
- }
- // We can't just compare newMode with _highlightMode here, since
- // _highlightMode could be null, so we want to compare with the return value
- // for the getter, since that's what clients will be looking at.
- final FocusHighlightMode oldMode = highlightMode;
- _highlightMode = newMode;
- if (highlightMode != oldMode) {
- _notifyHighlightModeListeners();
- }
- }
-
- // The list of listeners for [highlightMode] state changes.
- final HashedObserverList<ValueChanged<FocusHighlightMode>> _listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
+ FocusHighlightMode get highlightMode => _highlightManager.highlightMode;
/// Register a closure to be called when the [FocusManager] notifies its listeners
/// that the value of [highlightMode] has changed.
- void addHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.add(listener);
+ void addHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _highlightManager.addListener(listener);
/// Remove a previously registered closure from the list of closures that the
/// [FocusManager] notifies.
- void removeHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.remove(listener);
-
- @pragma('vm:notify-debugger-on-exception')
- void _notifyHighlightModeListeners() {
- if (_listeners.isEmpty) {
- return;
- }
- final List<ValueChanged<FocusHighlightMode>> localListeners = List<ValueChanged<FocusHighlightMode>>.of(_listeners);
- for (final ValueChanged<FocusHighlightMode> listener in localListeners) {
- try {
- if (_listeners.contains(listener)) {
- listener(highlightMode);
- }
- } catch (exception, stack) {
- InformationCollector? collector;
- assert(() {
- collector = () => <DiagnosticsNode>[
- DiagnosticsProperty<FocusManager>(
- 'The $runtimeType sending notification was',
- this,
- style: DiagnosticsTreeStyle.errorProperty,
- ),
- ];
- return true;
- }());
- FlutterError.reportError(FlutterErrorDetails(
- exception: exception,
- stack: stack,
- library: 'widgets library',
- context: ErrorDescription('while dispatching notifications for $runtimeType'),
- informationCollector: collector,
- ));
- }
- }
- }
+ void removeHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _highlightManager.removeListener(listener);
/// The root [FocusScopeNode] in the focus tree.
///
@@ -1643,77 +1549,6 @@
/// for a given [FocusNode], call [FocusNode.nearestScope].
final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
- void _handlePointerEvent(PointerEvent event) {
- final FocusHighlightMode expectedMode;
- switch (event.kind) {
- case PointerDeviceKind.touch:
- case PointerDeviceKind.stylus:
- case PointerDeviceKind.invertedStylus:
- _lastInteractionWasTouch = true;
- expectedMode = FocusHighlightMode.touch;
- break;
- case PointerDeviceKind.mouse:
- case PointerDeviceKind.trackpad:
- case PointerDeviceKind.unknown:
- _lastInteractionWasTouch = false;
- expectedMode = FocusHighlightMode.traditional;
- break;
- }
- if (expectedMode != highlightMode) {
- _updateHighlightMode();
- }
- }
-
- bool _handleKeyMessage(KeyMessage message) {
- // Update highlightMode first, since things responding to the keys might
- // look at the highlight mode, and it should be accurate.
- _lastInteractionWasTouch = false;
- _updateHighlightMode();
-
- assert(_focusDebug('Received key event $message'));
- if (_primaryFocus == null) {
- assert(_focusDebug('No primary focus for key event, ignored: $message'));
- return false;
- }
-
- // Walk the current focus from the leaf to the root, calling each one's
- // onKey on the way up, and if one responds that they handled it or want to
- // stop propagation, stop.
- bool handled = false;
- for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) {
- final List<KeyEventResult> results = <KeyEventResult>[];
- if (node.onKeyEvent != null) {
- for (final KeyEvent event in message.events) {
- results.add(node.onKeyEvent!(node, event));
- }
- }
- if (node.onKey != null && message.rawEvent != null) {
- results.add(node.onKey!(node, message.rawEvent!));
- }
- final KeyEventResult result = combineKeyEventResults(results);
- switch (result) {
- case KeyEventResult.ignored:
- continue;
- case KeyEventResult.handled:
- assert(_focusDebug('Node $node handled key event $message.'));
- handled = true;
- break;
- case KeyEventResult.skipRemainingHandlers:
- assert(_focusDebug('Node $node stopped key event propagation: $message.'));
- handled = false;
- break;
- }
- // Only KeyEventResult.ignored will continue the for loop. All other
- // options will stop the event propagation.
- assert(result != KeyEventResult.ignored);
- break;
- }
- if (!handled) {
- assert(_focusDebug('Key event not handled by anyone: $message.'));
- }
- return handled;
- }
-
/// The node that currently has the primary focus.
FocusNode? get primaryFocus => _primaryFocus;
FocusNode? _primaryFocus;
@@ -1729,7 +1564,7 @@
void _markDetached(FocusNode node) {
// The node has been removed from the tree, so it no longer needs to be
// notified of changes.
- assert(_focusDebug('Node was detached: $node'));
+ assert(_focusDebug(() => 'Node was detached: $node'));
if (_primaryFocus == node) {
_primaryFocus = null;
}
@@ -1738,7 +1573,7 @@
void _markPropertiesChanged(FocusNode node) {
_markNeedsUpdate();
- assert(_focusDebug('Properties changed for node $node.'));
+ assert(_focusDebug(() => 'Properties changed for node $node.'));
_dirtyNodes.add(node);
}
@@ -1762,7 +1597,7 @@
// Request that an update be scheduled, optionally requesting focus for the
// given newFocus node.
void _markNeedsUpdate() {
- assert(_focusDebug('Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus'));
+ assert(_focusDebug(() => 'Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus'));
if (_haveScheduledUpdate) {
return;
}
@@ -1784,7 +1619,7 @@
// then revert to the root scope.
_markedForFocus = rootScope;
}
- assert(_focusDebug('Refreshing focus state. Next focus will be $_markedForFocus'));
+ assert(_focusDebug(() => 'Refreshing focus state. Next focus will be $_markedForFocus'));
// A node has requested to be the next focus, and isn't already the primary
// focus.
if (_markedForFocus != null && _markedForFocus != _primaryFocus) {
@@ -1800,7 +1635,7 @@
}
assert(_markedForFocus == null);
if (previousFocus != _primaryFocus) {
- assert(_focusDebug('Updating focus from $previousFocus to $_primaryFocus'));
+ assert(_focusDebug(() => 'Updating focus from $previousFocus to $_primaryFocus'));
if (previousFocus != null) {
_dirtyNodes.add(previousFocus);
}
@@ -1811,7 +1646,7 @@
for (final FocusNode node in _dirtyNodes) {
node._notify();
}
- assert(_focusDebug('Notified ${_dirtyNodes.length} dirty nodes:', _dirtyNodes.toList().map<String>((FocusNode node) => node.toString())));
+ assert(_focusDebug(() => 'Notified ${_dirtyNodes.length} dirty nodes:', () => _dirtyNodes));
_dirtyNodes.clear();
if (previousFocus != _primaryFocus) {
notifyListeners();
@@ -1843,8 +1678,224 @@
}
}
-/// Provides convenient access to the current [FocusManager.primaryFocus] from the
-/// [WidgetsBinding] instance.
+// A class to detect and manage the highlight mode transitions. An instance of
+// this is owned by the FocusManager.
+//
+// This doesn't extend ChangeNotifier because the callback passes the updated
+// value, and ChangeNotifier requires using VoidCallback.
+class _HighlightModeManager {
+ // If set, indicates if the last interaction detected was touch or not. If
+ // null, no interactions have occurred yet.
+ bool? _lastInteractionWasTouch;
+
+ FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform;
+ FocusHighlightMode? _highlightMode;
+
+ FocusHighlightStrategy get strategy => _strategy;
+ FocusHighlightStrategy _strategy = FocusHighlightStrategy.automatic;
+ set strategy(FocusHighlightStrategy value) {
+ if (_strategy == value) {
+ return;
+ }
+ _strategy = value;
+ updateMode();
+ }
+
+ /// Register a closure to be called when the [FocusManager] notifies its
+ /// listeners that the value of [highlightMode] has changed.
+ void addListener(ValueChanged<FocusHighlightMode> listener) => _listeners.add(listener);
+
+ /// Remove a previously registered closure from the list of closures that the
+ /// [FocusManager] notifies.
+ void removeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.remove(listener);
+
+ // The list of listeners for [highlightMode] state changes.
+ HashedObserverList<ValueChanged<FocusHighlightMode>> _listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
+
+ void registerGlobalHandlers() {
+ assert(ServicesBinding.instance.keyEventManager.keyMessageHandler == null);
+ ServicesBinding.instance.keyEventManager.keyMessageHandler = handleKeyMessage;
+ GestureBinding.instance.pointerRouter.addGlobalRoute(handlePointerEvent);
+ }
+
+ @mustCallSuper
+ void dispose() {
+ if (ServicesBinding.instance.keyEventManager.keyMessageHandler == handleKeyMessage) {
+ GestureBinding.instance.pointerRouter.removeGlobalRoute(handlePointerEvent);
+ ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
+ }
+ _listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>();
+ }
+
+ @pragma('vm:notify-debugger-on-exception')
+ void notifyListeners() {
+ if (_listeners.isEmpty) {
+ return;
+ }
+ final List<ValueChanged<FocusHighlightMode>> localListeners = List<ValueChanged<FocusHighlightMode>>.of(_listeners);
+ for (final ValueChanged<FocusHighlightMode> listener in localListeners) {
+ try {
+ if (_listeners.contains(listener)) {
+ listener(highlightMode);
+ }
+ } catch (exception, stack) {
+ InformationCollector? collector;
+ assert(() {
+ collector = () => <DiagnosticsNode>[
+ DiagnosticsProperty<_HighlightModeManager>(
+ 'The $runtimeType sending notification was',
+ this,
+ style: DiagnosticsTreeStyle.errorProperty,
+ ),
+ ];
+ return true;
+ }());
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets library',
+ context: ErrorDescription('while dispatching notifications for $runtimeType'),
+ informationCollector: collector,
+ ));
+ }
+ }
+ }
+
+ void handlePointerEvent(PointerEvent event) {
+ final FocusHighlightMode expectedMode;
+ switch (event.kind) {
+ case PointerDeviceKind.touch:
+ case PointerDeviceKind.stylus:
+ case PointerDeviceKind.invertedStylus:
+ _lastInteractionWasTouch = true;
+ expectedMode = FocusHighlightMode.touch;
+ break;
+ case PointerDeviceKind.mouse:
+ case PointerDeviceKind.trackpad:
+ case PointerDeviceKind.unknown:
+ _lastInteractionWasTouch = false;
+ expectedMode = FocusHighlightMode.traditional;
+ break;
+ }
+ if (expectedMode != highlightMode) {
+ updateMode();
+ }
+ }
+
+ bool handleKeyMessage(KeyMessage message) {
+ // Update highlightMode first, since things responding to the keys might
+ // look at the highlight mode, and it should be accurate.
+ _lastInteractionWasTouch = false;
+ updateMode();
+
+ assert(_focusDebug(() => 'Received key event $message'));
+ if (FocusManager.instance.primaryFocus == null) {
+ assert(_focusDebug(() => 'No primary focus for key event, ignored: $message'));
+ return false;
+ }
+
+ // Walk the current focus from the leaf to the root, calling each one's
+ // onKey on the way up, and if one responds that they handled it or want to
+ // stop propagation, stop.
+ bool handled = false;
+ for (final FocusNode node in <FocusNode>[
+ FocusManager.instance.primaryFocus!,
+ ...FocusManager.instance.primaryFocus!.ancestors,
+ ]) {
+ final List<KeyEventResult> results = <KeyEventResult>[];
+ if (node.onKeyEvent != null) {
+ for (final KeyEvent event in message.events) {
+ results.add(node.onKeyEvent!(node, event));
+ }
+ }
+ if (node.onKey != null && message.rawEvent != null) {
+ results.add(node.onKey!(node, message.rawEvent!));
+ }
+ final KeyEventResult result = combineKeyEventResults(results);
+ switch (result) {
+ case KeyEventResult.ignored:
+ continue;
+ case KeyEventResult.handled:
+ assert(_focusDebug(() => 'Node $node handled key event $message.'));
+ handled = true;
+ break;
+ case KeyEventResult.skipRemainingHandlers:
+ assert(_focusDebug(() => 'Node $node stopped key event propagation: $message.'));
+ handled = false;
+ break;
+ }
+ // Only KeyEventResult.ignored will continue the for loop. All other
+ // options will stop the event propagation.
+ assert(result != KeyEventResult.ignored);
+ break;
+ }
+ if (!handled) {
+ assert(_focusDebug(() => 'Key event not handled by anyone: $message.'));
+ }
+ return handled;
+ }
+
+ // Update function to be called whenever the state relating to highlightMode
+ // changes.
+ void updateMode() {
+ final FocusHighlightMode newMode;
+ switch (strategy) {
+ case FocusHighlightStrategy.automatic:
+ if (_lastInteractionWasTouch == null) {
+ // If we don't have any information about the last interaction yet,
+ // then just rely on the default value for the platform, which will be
+ // determined based on the target platform if _highlightMode is not
+ // set.
+ return;
+ }
+ if (_lastInteractionWasTouch!) {
+ newMode = FocusHighlightMode.touch;
+ } else {
+ newMode = FocusHighlightMode.traditional;
+ }
+ break;
+ case FocusHighlightStrategy.alwaysTouch:
+ newMode = FocusHighlightMode.touch;
+ break;
+ case FocusHighlightStrategy.alwaysTraditional:
+ newMode = FocusHighlightMode.traditional;
+ break;
+ }
+ // We can't just compare newMode with _highlightMode here, since
+ // _highlightMode could be null, so we want to compare with the return value
+ // for the getter, since that's what clients will be looking at.
+ final FocusHighlightMode oldMode = highlightMode;
+ _highlightMode = newMode;
+ if (highlightMode != oldMode) {
+ notifyListeners();
+ }
+ }
+
+ static FocusHighlightMode get _defaultModeForPlatform {
+ // Assume that if we're on one of the mobile platforms, and there's no mouse
+ // connected, that the initial interaction will be touch-based, and that
+ // it's traditional mouse and keyboard on all other platforms.
+ //
+ // This only affects the initial value: the ongoing value is updated to a
+ // known correct value as soon as any pointer/keyboard events are received.
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.iOS:
+ if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
+ return FocusHighlightMode.traditional;
+ }
+ return FocusHighlightMode.touch;
+ case TargetPlatform.linux:
+ case TargetPlatform.macOS:
+ case TargetPlatform.windows:
+ return FocusHighlightMode.traditional;
+ }
+ }
+}
+
+/// Provides convenient access to the current [FocusManager.primaryFocus] from
+/// the [WidgetsBinding] instance.
FocusNode? get primaryFocus => WidgetsBinding.instance.focusManager.primaryFocus;
/// Returns a text representation of the current focus tree, along with the
@@ -1852,7 +1903,6 @@
///
/// Will return an empty string in release builds.
String debugDescribeFocusTree() {
- assert(WidgetsBinding.instance != null);
String? result;
assert(() {
result = FocusManager.instance.toStringDeep();
diff --git a/framework/lib/src/widgets/focus_scope.dart b/framework/lib/src/widgets/focus_scope.dart
index ad885a4..81bcb45 100644
--- a/framework/lib/src/widgets/focus_scope.dart
+++ b/framework/lib/src/widgets/focus_scope.dart
@@ -136,10 +136,7 @@
_skipTraversal = skipTraversal,
_descendantsAreFocusable = descendantsAreFocusable,
_descendantsAreTraversable = descendantsAreTraversable,
- _debugLabel = debugLabel,
- assert(child != null),
- assert(autofocus != null),
- assert(includeSemantics != null);
+ _debugLabel = debugLabel;
/// Creates a Focus widget that uses the given [focusNode] as the source of
/// truth for attributes on the node, rather than the attributes of this widget.
@@ -195,7 +192,7 @@
/// {@endtemplate}
///
/// A non-null [focusNode] must be supplied if using the
- /// [Focus.withExternalFocusNode] constructor is used.
+ /// [Focus.withExternalFocusNode] constructor.
final FocusNode? focusNode;
/// {@template flutter.widgets.Focus.autofocus}
@@ -386,9 +383,7 @@
/// * [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 }) {
- assert(context != null);
- assert(scopeOk != null);
- final _FocusMarker? marker = context.dependOnInheritedWidgetOfExactType<_FocusMarker>();
+ final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
final FocusNode? node = marker?.notifier;
assert(() {
if (node == null) {
@@ -438,9 +433,7 @@
/// * [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 }) {
- assert(context != null);
- assert(scopeOk != null);
- final _FocusMarker? marker = context.dependOnInheritedWidgetOfExactType<_FocusMarker>();
+ final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
final FocusNode? node = marker?.notifier;
if (node == null) {
return null;
@@ -536,9 +529,7 @@
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
- if (widget.skipTraversal != null) {
- focusNode.skipTraversal = widget.skipTraversal;
- }
+ focusNode.skipTraversal = widget.skipTraversal;
if (widget._canRequestFocus != null) {
focusNode.canRequestFocus = widget._canRequestFocus!;
}
@@ -625,9 +616,7 @@
if (widget.onKeyEvent != focusNode.onKeyEvent) {
focusNode.onKeyEvent = widget.onKeyEvent;
}
- if (widget.skipTraversal != null) {
- focusNode.skipTraversal = widget.skipTraversal;
- }
+ focusNode.skipTraversal = widget.skipTraversal;
if (widget._canRequestFocus != null) {
focusNode.canRequestFocus = widget._canRequestFocus!;
}
@@ -686,7 +675,7 @@
child: widget.child,
);
}
- return _FocusMarker(
+ return _FocusInheritedScope(
node: focusNode,
child: child,
);
@@ -771,9 +760,7 @@
super.onKeyEvent,
super.onKey,
super.debugLabel,
- }) : assert(child != null),
- assert(autofocus != null),
- super(
+ }) : super(
focusNode: node,
);
@@ -797,8 +784,7 @@
///
/// The [context] argument must not be null.
static FocusScopeNode of(BuildContext context) {
- assert(context != null);
- final _FocusMarker? marker = context.dependOnInheritedWidgetOfExactType<_FocusMarker>();
+ final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
return marker?.notifier?.nearestScope ?? context.owner!.focusManager.rootScope;
}
@@ -853,7 +839,7 @@
_focusAttachment!.reparent(parent: widget.parentNode);
return Semantics(
explicitChildNodes: true,
- child: _FocusMarker(
+ child: _FocusInheritedScope(
node: focusNode,
child: widget.child,
),
@@ -861,14 +847,12 @@
}
}
-// The InheritedWidget marker for Focus and FocusScope.
-class _FocusMarker extends InheritedNotifier<FocusNode> {
- const _FocusMarker({
+// The InheritedWidget for Focus and FocusScope.
+class _FocusInheritedScope extends InheritedNotifier<FocusNode> {
+ const _FocusInheritedScope({
required FocusNode node,
required super.child,
- }) : assert(node != null),
- assert(child != null),
- super(notifier: node);
+ }) : super(notifier: node);
}
/// A widget that controls whether or not the descendants of this widget are
@@ -892,8 +876,7 @@
super.key,
this.excluding = true,
required this.child,
- }) : assert(excluding != null),
- assert(child != null);
+ });
/// If true, will make this widget's descendants unfocusable.
///
diff --git a/framework/lib/src/widgets/focus_traversal.dart b/framework/lib/src/widgets/focus_traversal.dart
index fd63d28..ec3a40d 100644
--- a/framework/lib/src/widgets/focus_traversal.dart
+++ b/framework/lib/src/widgets/focus_traversal.dart
@@ -47,7 +47,7 @@
// sorting their contents.
class _FocusTraversalGroupInfo {
_FocusTraversalGroupInfo(
- _FocusTraversalGroupMarker? marker, {
+ _FocusTraversalGroupScope? marker, {
FocusTraversalPolicy? defaultPolicy,
List<FocusNode>? members,
}) : groupNode = marker?.focusNode,
@@ -84,10 +84,8 @@
left,
}
-/// {@template flutter.focus.TraversalEdgeBehavior}
/// Controls the transfer of focus beyond the first and the last items of a
/// [FocusScopeNode].
-/// {@endtemplate}
///
/// This enumeration only controls the traversal behavior performed by
/// [FocusTraversalPolicy]. Other methods of focus transfer, such as direct
@@ -202,7 +200,7 @@
///
/// See also:
///
- /// * [previous], the function that is called to move the focus to the next node.
+ /// * [previous], the function that is called to move the focus to the previous node.
/// * [DirectionalFocusTraversalPolicyMixin.findFirstFocusInDirection], a
/// function that finds the first focusable widget in a particular direction.
FocusNode findLastFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) {
@@ -210,7 +208,6 @@
}
FocusNode _findInitialFocus(FocusNode currentNode, {bool fromEnd = false, bool ignoreCurrentFocus = false}) {
- assert(currentNode != null);
final FocusScopeNode scope = currentNode.nearestScope!;
FocusNode? candidate = scope.focusedChild;
if (ignoreCurrentFocus || candidate == null && scope.descendants.isNotEmpty) {
@@ -321,20 +318,19 @@
@protected
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
- _FocusTraversalGroupMarker? _getMarker(BuildContext? context) {
- return context?.getElementForInheritedWidgetOfExactType<_FocusTraversalGroupMarker>()?.widget as _FocusTraversalGroupMarker?;
+ _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) {
- assert(scope != null);
- final _FocusTraversalGroupMarker? scopeGroupMarker = _getMarker(scope.context);
+ final _FocusTraversalGroupScope? scopeGroupMarker = _getMarker(scope.context);
final FocusTraversalPolicy defaultPolicy = scopeGroupMarker?.policy ?? ReadingOrderTraversalPolicy();
// Build the sorting data structure, separating descendants into groups.
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
for (final FocusNode node in scope.descendants) {
- final _FocusTraversalGroupMarker? groupMarker = _getMarker(node.context);
+ final _FocusTraversalGroupScope? groupMarker = _getMarker(node.context);
final FocusNode? groupNode = groupMarker?.focusNode;
// 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
@@ -346,7 +342,7 @@
// 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 _FocusTraversalGroupMarker? parentMarker = _getMarker(parentContext);
+ final _FocusTraversalGroupScope? parentMarker = _getMarker(parentContext);
final FocusNode? parentNode = parentMarker?.focusNode;
groups[parentNode] ??= _FocusTraversalGroupInfo(parentMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
assert(!groups[parentNode]!.members.contains(node));
@@ -423,7 +419,6 @@
/// Returns true if a node requested focus.
@protected
bool _moveFocus(FocusNode currentNode, {required bool forward}) {
- assert(forward != null);
final FocusScopeNode nearestScope = currentNode.nearestScope!;
invalidateScopeData(nearestScope);
final FocusNode? focusedChild = nearestScope.focusedChild;
@@ -444,20 +439,24 @@
return false;
}
if (forward && focusedChild == sortedNodes.last) {
- if (nearestScope.traversalEdgeBehavior == TraversalEdgeBehavior.leaveFlutterView) {
- focusedChild!.unfocus();
- return false;
+ switch (nearestScope.traversalEdgeBehavior) {
+ case TraversalEdgeBehavior.leaveFlutterView:
+ focusedChild!.unfocus();
+ return false;
+ case TraversalEdgeBehavior.closedLoop:
+ _focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
+ return true;
}
- _focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
- return true;
}
if (!forward && focusedChild == sortedNodes.first) {
- if (nearestScope.traversalEdgeBehavior == TraversalEdgeBehavior.leaveFlutterView) {
- focusedChild!.unfocus();
- return false;
+ switch (nearestScope.traversalEdgeBehavior) {
+ case TraversalEdgeBehavior.leaveFlutterView:
+ focusedChild!.unfocus();
+ return false;
+ case TraversalEdgeBehavior.closedLoop:
+ _focusAndEnsureVisible(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
+ return true;
}
- _focusAndEnsureVisible(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
- return true;
}
final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
@@ -479,16 +478,14 @@
// A policy data object for use by the DirectionalFocusTraversalPolicyMixin so
// it can keep track of the traversal history.
class _DirectionalPolicyDataEntry {
- const _DirectionalPolicyDataEntry({required this.direction, required this.node})
- : assert(direction != null),
- assert(node != null);
+ const _DirectionalPolicyDataEntry({required this.direction, required this.node});
final TraversalDirection direction;
final FocusNode node;
}
class _DirectionalPolicyData {
- const _DirectionalPolicyData({required this.history}) : assert(history != null);
+ const _DirectionalPolicyData({required this.history});
/// A queue of entries that describe the path taken to the current node.
final List<_DirectionalPolicyDataEntry> history;
@@ -545,8 +542,6 @@
@override
FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) {
- assert(direction != null);
- assert(currentNode != null);
switch (direction) {
case TraversalDirection.up:
// Find the bottom-most node so we can go up from there.
@@ -936,8 +931,7 @@
// the sort data.
class _ReadingOrderSortData with Diagnosticable {
_ReadingOrderSortData(this.node)
- : assert(node != null),
- rect = node.rect,
+ : rect = node.rect,
directionality = _findDirectionality(node.context!);
final TextDirection? directionality;
@@ -1137,7 +1131,7 @@
// It has to have at least topmost in it if the topmost is not degenerate.
assert(topmost.rect.isEmpty || inBandOfTop.isNotEmpty);
- // The topmost rect in is in a band by itself, so just return that one.
+ // The topmost rect is in a band by itself, so just return that one.
if (inBandOfTop.length <= 1) {
return topmost;
}
@@ -1174,7 +1168,6 @@
// order based on the directionality of the context for each node.
@override
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) {
- assert(descendants != null);
if (descendants.length <= 1) {
return descendants;
}
@@ -1282,7 +1275,7 @@
/// for the [OrderedTraversalPolicy] to use.
class NumericFocusOrder extends FocusOrder {
/// Creates an object that describes a focus traversal order numerically.
- const NumericFocusOrder(this.order) : assert(order != null);
+ const NumericFocusOrder(this.order);
/// The numerical order to assign to the widget subtree using
/// [FocusTraversalOrder].
@@ -1319,7 +1312,7 @@
/// for the [OrderedTraversalPolicy] to use.
class LexicalFocusOrder extends FocusOrder {
/// Creates an object that describes a focus traversal order lexically.
- const LexicalFocusOrder(this.order) : assert(order != null);
+ const LexicalFocusOrder(this.order);
/// The String that defines the lexical order to assign to the widget subtree
/// using [FocusTraversalOrder].
@@ -1342,9 +1335,7 @@
// Used to help sort the focus nodes in an OrderedFocusTraversalPolicy.
class _OrderedFocusInfo {
- const _OrderedFocusInfo({required this.node, required this.order})
- : assert(node != null),
- assert(order != null);
+ const _OrderedFocusInfo({required this.node, required this.order});
final FocusNode node;
final FocusOrder order;
@@ -1448,7 +1439,6 @@
/// 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) {
- assert(context != null);
final FocusTraversalOrder? marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder?;
assert(() {
if (marker == null) {
@@ -1474,7 +1464,6 @@
///
/// If no [FocusTraversalOrder] ancestor exists, or the order is null, returns null.
static FocusOrder? maybeOf(BuildContext context) {
- assert(context != null);
final FocusTraversalOrder? marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder?;
return marker?.order;
}
@@ -1537,9 +1526,7 @@
this.descendantsAreFocusable = true,
this.descendantsAreTraversable = true,
required this.child,
- }) : assert(descendantsAreFocusable != null),
- assert(descendantsAreTraversable != null),
- policy = policy ?? ReadingOrderTraversalPolicy();
+ }) : policy = policy ?? ReadingOrderTraversalPolicy();
/// The policy used to move the focus from one focus node to another when
/// traversing them using a keyboard.
@@ -1583,8 +1570,7 @@
/// * [maybeOf] for a similar function that will return null if no
/// [FocusTraversalGroup] ancestor is found.
static FocusTraversalPolicy of(BuildContext context) {
- assert(context != null);
- final _FocusTraversalGroupMarker? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupMarker>();
+ final _FocusTraversalGroupScope? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupScope>();
assert(() {
if (inherited == null) {
throw FlutterError(
@@ -1618,8 +1604,7 @@
/// * [of] for a similar function that will throw if no [FocusTraversalGroup]
/// ancestor is found.
static FocusTraversalPolicy? maybeOf(BuildContext context) {
- assert(context != null);
- final _FocusTraversalGroupMarker? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupMarker>();
+ final _FocusTraversalGroupScope? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupScope>();
return inherited?.policy;
}
@@ -1657,7 +1642,7 @@
@override
Widget build(BuildContext context) {
- return _FocusTraversalGroupMarker(
+ return _FocusTraversalGroupScope(
policy: widget.policy,
focusNode: focusNode,
child: Focus(
@@ -1674,13 +1659,12 @@
}
// A "marker" inherited widget to make the group faster to find.
-class _FocusTraversalGroupMarker extends InheritedWidget {
- const _FocusTraversalGroupMarker({
+class _FocusTraversalGroupScope extends InheritedWidget {
+ const _FocusTraversalGroupScope({
required this.policy,
required this.focusNode,
required super.child,
- }) : assert(policy != null),
- assert(focusNode != null);
+ });
final FocusTraversalPolicy policy;
final FocusNode focusNode;
@@ -1695,8 +1679,7 @@
/// Creates an intent used with [RequestFocusAction].
///
/// The argument must not be null.
- const RequestFocusIntent(this.focusNode)
- : assert(focusNode != null);
+ const RequestFocusIntent(this.focusNode);
/// The [FocusNode] that is to be focused.
final FocusNode focusNode;
@@ -1722,7 +1705,7 @@
/// logging, or undo and redo functionality.
///
/// This [RequestFocusAction] class is the default action associated with the
-/// [RequestFocusIntent] in the [WidgetsApp], and it simply requests focus. You
+/// [RequestFocusIntent] in the [WidgetsApp]. It requests focus. You
/// can redefine the associated action with your own [Actions] widget.
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
@@ -1762,8 +1745,8 @@
}
@override
- KeyEventResult toKeyEventResult(NextFocusIntent intent, bool actionResult) {
- return actionResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
+ KeyEventResult toKeyEventResult(NextFocusIntent intent, bool invokeResult) {
+ return invokeResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
}
}
@@ -1797,8 +1780,8 @@
}
@override
- KeyEventResult toKeyEventResult(PreviousFocusIntent intent, bool actionResult) {
- return actionResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
+ KeyEventResult toKeyEventResult(PreviousFocusIntent intent, bool invokeResult) {
+ return invokeResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
}
}
@@ -1813,8 +1796,7 @@
/// See [FocusTraversalPolicy] for more information about focus traversal.
class DirectionalFocusIntent extends Intent {
/// Creates an intent used to move the focus in the given [direction].
- const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true})
- : assert(ignoreTextFields != null);
+ const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true});
/// The direction in which to look for the next focusable node when the
/// associated [DirectionalFocusAction] is invoked.
@@ -1881,8 +1863,7 @@
super.key,
this.excluding = true,
required this.child,
- }) : assert(excluding != null),
- assert(child != null);
+ });
/// If true, will make this widget's descendants untraversable.
///
diff --git a/framework/lib/src/widgets/form.dart b/framework/lib/src/widgets/form.dart
index 24d4afb..12dd37a 100644
--- a/framework/lib/src/widgets/form.dart
+++ b/framework/lib/src/widgets/form.dart
@@ -46,8 +46,7 @@
this.onWillPop,
this.onChanged,
AutovalidateMode? autovalidateMode,
- }) : assert(child != null),
- autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
+ }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
/// Returns the [FormState] of the closest [Form] widget which encloses the
/// given context, or null if none is found.
@@ -296,7 +295,7 @@
/// Use a [GlobalKey] with [FormField] if you want to retrieve its current
/// state, for example if you want one form field to depend on another.
///
-/// A [Form] ancestor is not required. The [Form] simply makes it easier to
+/// A [Form] ancestor is not required. The [Form] allows one to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
@@ -318,8 +317,7 @@
this.enabled = true,
AutovalidateMode? autovalidateMode,
this.restorationId,
- }) : assert(builder != null),
- autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
+ }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
/// An optional method to call with the final value when the form is saved via
/// [FormState.save].
diff --git a/framework/lib/src/widgets/framework.dart b/framework/lib/src/widgets/framework.dart
index 7bffc4f..e576a78 100644
--- a/framework/lib/src/widgets/framework.dart
+++ b/framework/lib/src/widgets/framework.dart
@@ -501,7 +501,7 @@
///
/// @override
/// Widget build(BuildContext context) {
-/// return Container(color: color, child: child);
+/// return ColoredBox(color: color, child: child);
/// }
/// }
/// ```
@@ -654,7 +654,7 @@
/// this ideal, the more efficient it will be.)
///
/// * If a subtree does not change, cache the widget that represents that
-/// subtree and re-use it each time it can be used. To do this, simply assign
+/// subtree and re-use it each time it can be used. To do this, assign
/// a widget to a `final` state variable and re-use it in the build method. It
/// is massively more efficient for a widget to be re-used than for a new (but
/// identically-configured) widget to be created. Another caching strategy
@@ -1091,7 +1091,6 @@
/// whether the [mounted] property is true.
@protected
void setState(VoidCallback fn) {
- assert(fn != null);
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError.fromParts(<DiagnosticsNode>[
@@ -1519,7 +1518,6 @@
}) {
assert(T != dynamic);
assert(T != ParentData);
- assert(debugTypicalAncestorWidgetClass != null);
final String description = 'The ParentDataWidget $this wants to apply ParentData of type $T to a RenderObject';
return <DiagnosticsNode>[
@@ -1583,6 +1581,8 @@
/// Base class for widgets that efficiently propagate information down the tree.
///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=og-vJqLzg2c}
+///
/// To obtain the nearest instance of a particular type of inherited widget from
/// a build context, use [BuildContext.dependOnInheritedWidgetOfExactType].
///
@@ -1855,21 +1855,7 @@
///
/// The [children] argument must not be null and must not contain any null
/// objects.
- MultiChildRenderObjectWidget({ super.key, this.children = const <Widget>[] })
- : assert(children != null) {
- assert(() {
- for (int index = 0; index < children.length; index++) {
- // TODO(a14n): remove this check to have a lot more const widget
- if (children[index] == null) {
- throw FlutterError(
- "$runtimeType's children must not contain any null values, "
- 'but a null value was found at index $index',
- );
- }
- }
- return true;
- }()); // https://github.com/dart-lang/sdk/issues/29276
- }
+ const MultiChildRenderObjectWidget({ super.key, this.children = const <Widget>[] });
/// The widgets below this widget in the tree.
///
@@ -2020,6 +2006,13 @@
/// this callback.
typedef ElementVisitor = void Function(Element element);
+/// Signature for the callback to [BuildContext.visitAncestorElements].
+///
+/// The argument is the ancestor being visited.
+///
+/// Return false to stop the walk.
+typedef ConditionalElementVisitor = bool Function(Element element);
+
/// A handle to the location of a widget in the widget tree.
///
/// This class presents a set of methods that can be used from
@@ -2221,7 +2214,7 @@
///
/// All of the qualifications about when [dependOnInheritedWidgetOfExactType] can
/// be called apply to this method as well.
- InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect });
+ 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
@@ -2229,6 +2222,7 @@
/// 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}
/// This is typically called implicitly from `of()` static methods, e.g.
/// [Theme.of].
///
@@ -2262,6 +2256,7 @@
/// [InheritedWidget] subclasses that supports partial updates, like
/// [InheritedModel]. It specifies what "aspect" of the inherited
/// widget this context depends on.
+ /// {@endtemplate}
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
/// Obtains the element corresponding to the nearest widget of the given type `T`,
@@ -2269,6 +2264,7 @@
///
/// Returns null if no such element is found.
///
+ /// {@template flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType}
/// Calling this method is O(1) with a small constant factor.
///
/// This method does not establish a relationship with the target in the way
@@ -2280,11 +2276,13 @@
/// [dependOnInheritedWidgetOfExactType] in [State.didChangeDependencies]. It is
/// safe to use this method from [State.deactivate], which is called whenever
/// the widget is removed from the tree.
+ /// {@endtemplate}
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
/// Returns the nearest ancestor widget of the given type `T`, which must be the
/// type of a concrete [Widget] subclass.
///
+ /// {@template flutter.widgets.BuildContext.findAncestorWidgetOfExactType}
/// In general, [dependOnInheritedWidgetOfExactType] is more useful, since
/// inherited widgets will trigger consumers to rebuild when they change. This
/// method is appropriate when used in interaction event handlers (e.g.
@@ -2306,11 +2304,13 @@
///
/// Returns null if a widget of the requested type does not appear in the
/// ancestors of this context.
+ /// {@endtemplate}
T? findAncestorWidgetOfExactType<T extends Widget>();
/// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
/// that is an instance of the given type `T`.
///
+ /// {@template flutter.widgets.BuildContext.findAncestorStateOfType}
/// This should not be used from build methods, because the build context will
/// not be rebuilt if the value that would be returned by this method changes.
/// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such
@@ -2332,6 +2332,7 @@
/// because the widget tree is no longer stable at that time. To refer to
/// an ancestor from one of those methods, save a reference to the ancestor
/// by calling [findAncestorStateOfType] in [State.didChangeDependencies].
+ /// {@endtemplate}
///
/// {@tool snippet}
///
@@ -2344,17 +2345,20 @@
/// Returns the [State] object of the furthest ancestor [StatefulWidget] widget
/// that is an instance of the given type `T`.
///
+ /// {@template flutter.widgets.BuildContext.findRootAncestorStateOfType}
/// Functions the same way as [findAncestorStateOfType] but keeps visiting subsequent
/// ancestors until there are none of the type instance of `T` remaining.
/// Then returns the last one found.
///
/// This operation is O(N) as well though N is the entire widget tree rather than
/// a subtree.
+ /// {@endtemplate}
T? findRootAncestorStateOfType<T extends State>();
/// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget
/// that is an instance of the given type `T`.
///
+ /// {@template flutter.widgets.BuildContext.findAncestorRenderObjectOfType}
/// This should not be used from build methods, because the build context will
/// not be rebuilt if the value that would be returned by this method changes.
/// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such
@@ -2371,13 +2375,16 @@
/// because the widget tree is no longer stable at that time. To refer to
/// an ancestor from one of those methods, save a reference to the ancestor
/// by calling [findAncestorRenderObjectOfType] in [State.didChangeDependencies].
+ /// {@endtemplate}
T? findAncestorRenderObjectOfType<T extends RenderObject>();
/// Walks the ancestor chain, starting with the parent of this build context's
- /// widget, invoking the argument for each ancestor. The callback is given a
- /// reference to the ancestor widget's corresponding [Element] object. The
- /// walk stops when it reaches the root widget or when the callback returns
- /// false. The callback must not return null.
+ /// widget, invoking the argument for each ancestor.
+ ///
+ /// {@template flutter.widgets.BuildContext.visitAncestorElements}
+ /// The callback is given a reference to the ancestor widget's corresponding
+ /// [Element] object. The walk stops when it reaches the root widget or when
+ /// the callback returns false. The callback must not return null.
///
/// This is useful for inspecting the widget tree.
///
@@ -2387,10 +2394,12 @@
/// because the element tree is no longer stable at that time. To refer to
/// an ancestor from one of those methods, save a reference to the ancestor
/// by calling [visitAncestorElements] in [State.didChangeDependencies].
- void visitAncestorElements(bool Function(Element element) visitor);
+ /// {@endtemplate}
+ void visitAncestorElements(ConditionalElementVisitor visitor);
/// Walks the children of this widget.
///
+ /// {@template flutter.widgets.BuildContext.visitChildElements}
/// This is useful for applying changes to children after they are built
/// without waiting for the next frame, especially if the children are known,
/// and especially if there is exactly one child (as is always the case for
@@ -2408,6 +2417,7 @@
/// significantly cheaper to use an [InheritedWidget] and have the descendants
/// pull data down, than it is to use [visitChildElements] recursively to push
/// data down to them.
+ /// {@endtemplate}
void visitChildElements(ElementVisitor visitor);
/// Start bubbling this notification at the given build context.
@@ -2519,7 +2529,6 @@
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope].
void scheduleBuildFor(Element element) {
- assert(element != null);
assert(element.owner == this);
assert(() {
if (debugPrintScheduleBuildForStacks) {
@@ -2591,7 +2600,6 @@
/// This mechanism is used to ensure that, for instance, [State.dispose] does
/// not call [State.setState].
void lockState(VoidCallback callback) {
- assert(callback != null);
assert(_debugStateLockLevel >= 0);
assert(() {
_debugStateLockLevel += 1;
@@ -2638,7 +2646,6 @@
if (callback == null && _dirtyElements.isEmpty) {
return;
}
- assert(context != null);
assert(_debugStateLockLevel >= 0);
assert(!_debugBuilding);
assert(() {
@@ -2695,7 +2702,6 @@
int index = 0;
while (index < dirtyCount) {
final Element element = _dirtyElements[index];
- assert(element != null);
assert(element._inDirtyList);
assert(() {
if (element._lifecycleState == _ElementLifecycle.active && !element._debugIsInScope(context)) {
@@ -2846,8 +2852,6 @@
void _debugRemoveGlobalKeyReservationFor(Element parent, Element child) {
assert(() {
- assert(parent != null);
- assert(child != null);
_debugGlobalKeyReservations?[parent]?.remove(child);
return true;
}());
@@ -2856,9 +2860,7 @@
void _registerGlobalKey(GlobalKey key, Element element) {
assert(() {
if (_globalKeyRegistry.containsKey(key)) {
- assert(element.widget != null);
final Element oldElement = _globalKeyRegistry[key]!;
- assert(oldElement.widget != null);
assert(element.widget.runtimeType != oldElement.widget.runtimeType);
_debugIllFatedElements?.add(oldElement);
}
@@ -2870,9 +2872,7 @@
void _unregisterGlobalKey(GlobalKey key, Element element) {
assert(() {
if (_globalKeyRegistry.containsKey(key) && _globalKeyRegistry[key] != element) {
- assert(element.widget != null);
final Element oldElement = _globalKeyRegistry[key]!;
- assert(oldElement.widget != null);
assert(element.widget.runtimeType != oldElement.widget.runtimeType);
}
return true;
@@ -2884,8 +2884,6 @@
void _debugReserveGlobalKeyFor(Element parent, Element child, GlobalKey key) {
assert(() {
- assert(parent != null);
- assert(child != null);
_debugGlobalKeyReservations?[parent] ??= <Element, GlobalKey>{};
_debugGlobalKeyReservations?[parent]![child] = key;
return true;
@@ -2972,8 +2970,6 @@
Map<GlobalKey, Set<Element>>? duplicates;
for (final Element element in _debugIllFatedElements ?? const <Element>{}) {
if (element._lifecycleState != _ElementLifecycle.defunct) {
- assert(element != null);
- assert(element.widget != null);
assert(element.widget.key != null);
final GlobalKey key = element.widget.key! as GlobalKey;
assert(_globalKeyRegistry.containsKey(key));
@@ -3235,8 +3231,7 @@
///
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
- : assert(widget != null),
- _widget = widget {
+ : _widget = widget {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: _flutterWidgetsLibrary,
@@ -3717,7 +3712,6 @@
@mustCallSuper
void mount(Element? parent, Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.initial);
- assert(widget != null);
assert(_parent == null);
assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);
assert(slot == null);
@@ -3758,10 +3752,7 @@
// only call _AssertionError._evaluateAssertion once.
assert(
_lifecycleState == _ElementLifecycle.active
- && widget != null
- && newWidget != null
&& newWidget != widget
- && depth != null
&& Widget.canUpdate(widget, newWidget),
);
// This Element was told to update and we can now release all the global key
@@ -3785,7 +3776,6 @@
@protected
void updateSlotForChild(Element child, Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.active);
- assert(child != null);
assert(child._parent == this);
void visit(Element element) {
element._updateSlot(newSlot);
@@ -3798,10 +3788,8 @@
void _updateSlot(Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.active);
- assert(widget != null);
assert(_parent != null);
assert(_parent!._lifecycleState == _ElementLifecycle.active);
- assert(depth != null);
_slot = newSlot;
}
@@ -3817,7 +3805,7 @@
/// Remove [renderObject] from the render tree.
///
- /// The default implementation of this function simply calls
+ /// The default implementation of this function calls
/// [detachRenderObject] recursively on each child. The
/// [RenderObjectElement.detachRenderObject] override does the actual work of
/// removing [renderObject] from the render tree.
@@ -3832,7 +3820,7 @@
/// Add [renderObject] to the render tree at the location specified by `newSlot`.
///
- /// The default implementation of this function simply calls
+ /// The default implementation of this function calls
/// [attachRenderObject] recursively on each child. The
/// [RenderObjectElement.attachRenderObject] override does the actual work of
/// adding [renderObject] to the render tree.
@@ -3912,7 +3900,6 @@
@protected
@pragma('vm:prefer-inline')
Element inflateWidget(Widget newWidget, Object? newSlot) {
- assert(newWidget != null);
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
if (isTimelineTracked) {
@@ -3989,7 +3976,6 @@
/// [forgetChild] to cause the old parent to update its child model.
@protected
void deactivateChild(Element child) {
- assert(child != null);
assert(child._parent == this);
child._parent = null;
child.detachRenderObject();
@@ -4078,9 +4064,7 @@
@mustCallSuper
void activate() {
assert(_lifecycleState == _ElementLifecycle.inactive);
- assert(widget != null);
assert(owner != null);
- assert(depth != null);
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
// We unregistered our dependencies in deactivate, but never cleared the list.
@@ -4116,7 +4100,6 @@
void deactivate() {
assert(_lifecycleState == _ElementLifecycle.active);
assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
- assert(depth != null);
if (_dependencies != null && _dependencies!.isNotEmpty) {
for (final InheritedElement dependency in _dependencies!) {
dependency._dependents.remove(this);
@@ -4162,7 +4145,6 @@
void unmount() {
assert(_lifecycleState == _ElementLifecycle.inactive);
assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
- assert(depth != null);
assert(owner != null);
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
@@ -4356,7 +4338,6 @@
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
- assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
@@ -4452,7 +4433,7 @@
}
@override
- void visitAncestorElements(bool Function(Element element) visitor) {
+ void visitAncestorElements(ConditionalElementVisitor visitor) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor)) {
@@ -4687,12 +4668,15 @@
owner!._debugCurrentBuildTarget = this;
return true;
}());
- performRebuild();
- assert(() {
- assert(owner!._debugCurrentBuildTarget == this);
- owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
- return true;
- }());
+ try {
+ performRebuild();
+ } finally {
+ assert(() {
+ assert(owner!._debugCurrentBuildTarget == this);
+ owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
+ return true;
+ }());
+ }
assert(!_dirty);
}
@@ -4910,14 +4894,14 @@
///
/// The child should typically be part of the returned widget tree.
///
-/// Used by [AnimatedBuilder.builder], as well as [WidgetsApp.builder] and
-/// [MaterialApp.builder].
+/// Used by [AnimatedBuilder.builder], [ListenableBuilder.builder],
+/// [WidgetsApp.builder], and [MaterialApp.builder].
///
/// See also:
///
-/// * [WidgetBuilder], which is similar but only takes a [BuildContext].
-/// * [IndexedWidgetBuilder], which is similar but also takes an index.
-/// * [ValueWidgetBuilder], which is similar but takes a value and a child.
+/// * [WidgetBuilder], which is similar but only takes a [BuildContext].
+/// * [IndexedWidgetBuilder], which is similar but also takes an index.
+/// * [ValueWidgetBuilder], which is similar but takes a value and a child.
typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);
/// An [Element] that composes other [Element]s.
@@ -5197,7 +5181,6 @@
@override
InheritedWidget dependOnInheritedElement(Element ancestor, { Object? aspect }) {
- assert(ancestor != null);
assert(() {
final Type targetType = ancestor.widget.runtimeType;
if (state._debugLifecycleState == _StateLifecycle.created) {
@@ -5293,7 +5276,6 @@
@override
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget as ProxyWidget;
- assert(widget != null);
assert(widget != newWidget);
super.update(newWidget);
assert(widget == newWidget);
@@ -5370,7 +5352,6 @@
/// It is more efficient than requesting an additional frame just for the
/// purpose of updating the [KeepAlive] widget.
void applyWidgetOutOfTurn(ParentDataWidget<T> newWidget) {
- assert(newWidget != null);
assert(newWidget.debugCanApplyOutOfTurn());
assert(newWidget.child == (widget as ParentDataWidget<T>).child);
_applyParentData(newWidget);
@@ -5622,7 +5603,7 @@
///
/// However, the immediate children of the element may not be the ones that
/// eventually produce the actual [RenderObject] that they correspond to. For
-/// example a [StatelessElement] (the element of a [StatelessWidget]) simply
+/// example, a [StatelessElement] (the element of a [StatelessWidget])
/// corresponds to whatever [RenderObject] its child (the element returned by
/// its [StatelessWidget.build] method) corresponds to.
///
@@ -5899,7 +5880,7 @@
/// list after the [RenderObject] associated with the [Element] provided as
/// [IndexedSlot.value] in the [slot] object.
///
- /// Simply using the previous sibling as a [slot] is not enough, though, because
+ /// Using the previous sibling as a [slot] is not enough, though, because
/// child [RenderObject]s are only moved around when the [slot] of their
/// associated [RenderObjectElement]s is updated. When the order of child
/// [Element]s is changed, some elements in the list may move to a new index
@@ -5915,8 +5896,6 @@
/// previous sibling.
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element>? forgottenChildren, List<Object?>? slots }) {
- assert(oldChildren != null);
- assert(newWidgets != null);
assert(slots == null || newWidgets.length == slots.length);
Element? replaceWithNullIfForgotten(Element child) {
@@ -6062,7 +6041,7 @@
assert(Widget.canUpdate(oldChild.widget, newWidget));
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
assert(newChild._lifecycleState == _ElementLifecycle.active);
- assert(oldChild == newChild || oldChild == null || oldChild._lifecycleState != _ElementLifecycle.active);
+ assert(oldChild == newChild || oldChild._lifecycleState != _ElementLifecycle.active);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
diff --git a/framework/lib/src/widgets/gesture_detector.dart b/framework/lib/src/widgets/gesture_detector.dart
index 100eaba..42adfac 100644
--- a/framework/lib/src/widgets/gesture_detector.dart
+++ b/framework/lib/src/widgets/gesture_detector.dart
@@ -93,9 +93,7 @@
/// Creates a gesture recognizer factory with the given callbacks.
///
/// The arguments must not be null.
- const GestureRecognizerFactoryWithHandlers(this._constructor, this._initializer)
- : assert(_constructor != null),
- assert(_initializer != null);
+ const GestureRecognizerFactoryWithHandlers(this._constructor, this._initializer);
final GestureRecognizerFactoryConstructor<T> _constructor;
@@ -215,11 +213,11 @@
/// Creates a widget that detects gestures.
///
/// Pan and scale callbacks cannot be used simultaneously because scale is a
- /// superset of pan. Simply use the scale callbacks instead.
+ /// superset of pan. Use the scale callbacks instead.
///
/// Horizontal and vertical drag callbacks cannot be used simultaneously
- /// because a combination of a horizontal and vertical drag is a pan. Simply
- /// use the pan callbacks instead.
+ /// because a combination of a horizontal and vertical drag is a pan.
+ /// Use the pan callbacks instead.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=WhVXkCFPmK4}
///
@@ -288,10 +286,10 @@
this.behavior,
this.excludeFromSemantics = false,
this.dragStartBehavior = DragStartBehavior.start,
+ this.trackpadScrollCausesScale = false,
+ this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
this.supportedDevices,
- }) : assert(excludeFromSemantics != null),
- assert(dragStartBehavior != null),
- assert(() {
+ }) : assert(() {
final bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null;
final bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null;
final bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;
@@ -1014,10 +1012,16 @@
/// If set to null, events from all device types will be recognized. Defaults to null.
final Set<PointerDeviceKind>? supportedDevices;
+ /// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
+ final bool trackpadScrollCausesScale;
+
+ /// {@macro flutter.gestures.scale.trackpadScrollToScaleFactor}
+ final Offset trackpadScrollToScaleFactor;
+
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
- final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
+ final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
if (onTapDown != null ||
onTapUp != null ||
@@ -1186,7 +1190,9 @@
..onUpdate = onScaleUpdate
..onEnd = onScaleEnd
..dragStartBehavior = dragStartBehavior
- ..gestureSettings = gestureSettings;
+ ..gestureSettings = gestureSettings
+ ..trackpadScrollCausesScale = trackpadScrollCausesScale
+ ..trackpadScrollToScaleFactor = trackpadScrollToScaleFactor;
},
);
}
@@ -1276,8 +1282,7 @@
this.behavior,
this.excludeFromSemantics = false,
this.semantics,
- }) : assert(gestures != null),
- assert(excludeFromSemantics != null);
+ });
/// The widget below this widget in the tree.
///
@@ -1559,7 +1564,7 @@
super.child,
required this.behavior,
required this.assignSemantics,
- }) : assert(assignSemantics != null);
+ });
final HitTestBehavior behavior;
final _AssignSemantics assignSemantics;
@@ -1632,7 +1637,6 @@
}
return () {
- assert(tap != null);
tap.onTapDown?.call(TapDownDetails());
tap.onTapUp?.call(TapUpDetails(kind: PointerDeviceKind.unknown));
tap.onTap?.call();
diff --git a/framework/lib/src/widgets/heroes.dart b/framework/lib/src/widgets/heroes.dart
index b88d411..06219e5 100644
--- a/framework/lib/src/widgets/heroes.dart
+++ b/framework/lib/src/widgets/heroes.dart
@@ -180,9 +180,7 @@
this.placeholderBuilder,
this.transitionOnUserGestures = false,
required this.child,
- }) : assert(tag != null),
- assert(transitionOnUserGestures != null),
- assert(child != null);
+ });
/// The identifier for this particular hero. If the tag of this hero matches
/// the tag of a hero on a [PageRoute] that we're navigating to or from, then
@@ -272,9 +270,6 @@
bool isUserGestureTransition,
NavigatorState navigator,
) {
- assert(context != null);
- assert(isUserGestureTransition != null);
- assert(navigator != null);
final Map<Object, _HeroState> result = <Object, _HeroState>{};
void inviteHero(StatefulElement hero, Object tag) {
@@ -308,7 +303,6 @@
if (widget is Hero) {
final StatefulElement hero = element as StatefulElement;
final Object tag = widget.tag;
- assert(tag != null);
if (Navigator.of(hero) == navigator) {
inviteHero(hero, tag);
} else {
@@ -372,7 +366,7 @@
_shouldIncludeChild = shouldIncludedChildInPlaceholder;
assert(mounted);
final RenderBox box = context.findRenderObject()! as RenderBox;
- assert(box != null && box.hasSize);
+ assert(box.hasSize);
setState(() {
_placeholderSize = box.size;
});
@@ -480,7 +474,7 @@
static Rect _boundingBoxFor(BuildContext context, BuildContext? ancestorContext) {
assert(ancestorContext != null);
final RenderBox box = context.findRenderObject()! as RenderBox;
- assert(box != null && box.hasSize && box.size.isFinite);
+ assert(box.hasSize && box.size.isFinite);
return MatrixUtils.transformRect(
box.getTransformTo(ancestorContext?.findRenderObject()),
Offset.zero & box.size,
@@ -537,7 +531,6 @@
// The OverlayEntry WidgetBuilder callback for the hero's overlay.
Widget _buildOverlay(BuildContext context) {
- assert(manifest != null);
shuttle ??= manifest.shuttleBuilder(
context,
manifest.animation,
@@ -650,9 +643,7 @@
assert(!_aborted);
assert(() {
final Animation<double> initial = initialManifest.animation;
- assert(initial != null);
final HeroFlightDirection type = initialManifest.type;
- assert(type != null);
switch (type) {
case HeroFlightDirection.pop:
return initial.value == 1.0 && initialManifest.isUserGestureTransition
@@ -796,14 +787,12 @@
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
assert(navigator != null);
- assert(route != null);
_maybeStartHeroTransition(previousRoute, route, HeroFlightDirection.push, false);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
assert(navigator != null);
- assert(route != null);
// Don't trigger another flight when a pop is committed as a user gesture
// back swipe is snapped.
if (!navigator!.userGestureInProgress) {
@@ -823,7 +812,6 @@
@override
void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) {
assert(navigator != null);
- assert(route != null);
_maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, true);
}
@@ -1054,8 +1042,7 @@
super.key,
required this.child,
this.enabled = true,
- }) : assert(child != null),
- assert(enabled != null);
+ });
/// The subtree to place inside the [HeroMode].
final Widget child;
diff --git a/framework/lib/src/widgets/icon.dart b/framework/lib/src/widgets/icon.dart
index 7bea0a9..81964ce 100644
--- a/framework/lib/src/widgets/icon.dart
+++ b/framework/lib/src/widgets/icon.dart
@@ -36,9 +36,9 @@
/// ![The following code snippet would generate a row of icons consisting of a pink heart, a green musical note, and a blue umbrella, each progressively bigger than the last.](https://flutter.github.io/assets-for-api-docs/assets/widgets/icon.png)
///
/// ```dart
-/// Row(
+/// const Row(
/// mainAxisAlignment: MainAxisAlignment.spaceAround,
-/// children: const <Widget>[
+/// children: <Widget>[
/// Icon(
/// Icons.favorite,
/// color: Colors.pink,
diff --git a/framework/lib/src/widgets/icon_data.dart b/framework/lib/src/widgets/icon_data.dart
index 69349df..15a8234 100644
--- a/framework/lib/src/widgets/icon_data.dart
+++ b/framework/lib/src/widgets/icon_data.dart
@@ -78,9 +78,7 @@
super.showName,
super.style,
super.level,
- }) : assert(showName != null),
- assert(style != null),
- assert(level != null);
+ });
@override
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
@@ -93,3 +91,14 @@
return json;
}
}
+
+class _StaticIconProvider {
+ const _StaticIconProvider();
+}
+
+/// Annotation for classes that only provide static const [IconData] instances.
+///
+/// This is a hint to the font tree shaker to ignore the constant instances
+/// of [IconData] appearing in the class when tracking which code points
+/// should be retained in the bundled font.
+const Object staticIconProvider = _StaticIconProvider();
diff --git a/framework/lib/src/widgets/icon_theme.dart b/framework/lib/src/widgets/icon_theme.dart
index 0de6203..1d8a899 100644
--- a/framework/lib/src/widgets/icon_theme.dart
+++ b/framework/lib/src/widgets/icon_theme.dart
@@ -23,8 +23,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(data != null),
- assert(child != null);
+ });
/// Creates an icon theme that controls the properties of
/// descendant widgets, and merges in the current icon theme, if any.
diff --git a/framework/lib/src/widgets/icon_theme_data.dart b/framework/lib/src/widgets/icon_theme_data.dart
index 3e15285..c1285bd 100644
--- a/framework/lib/src/widgets/icon_theme_data.dart
+++ b/framework/lib/src/widgets/icon_theme_data.dart
@@ -76,7 +76,7 @@
/// Returns a new icon theme that matches this icon theme but with some values
/// replaced by the non-null parameters of the given icon theme. If the given
- /// icon theme is null, simply returns this icon theme.
+ /// icon theme is null, returns this icon theme.
IconThemeData merge(IconThemeData? other) {
if (other == null) {
return this;
@@ -167,7 +167,6 @@
///
/// {@macro dart.ui.shadow.lerp}
static IconThemeData lerp(IconThemeData? a, IconThemeData? b, double t) {
- assert(t != null);
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/image.dart b/framework/lib/src/widgets/image.dart
index 6b54548..615fc31 100644
--- a/framework/lib/src/widgets/image.dart
+++ b/framework/lib/src/widgets/image.dart
@@ -53,7 +53,7 @@
ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size? size }) {
return ImageConfiguration(
bundle: DefaultAssetBundle.of(context),
- devicePixelRatio: MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0,
+ devicePixelRatio: MediaQuery.maybeDevicePixelRatioOf(context) ?? 1.0,
locale: Localizations.maybeLocaleOf(context),
textDirection: Directionality.maybeOf(context),
size: size,
@@ -288,8 +288,9 @@
/// memory usage of [ImageCache].
///
/// In the case where a network image is used on the Web platform, the
-/// `cacheWidth` and `cacheHeight` parameters are ignored as the Web engine
-/// delegates image decoding of network images to the Web, which does not support
+/// `cacheWidth` and `cacheHeight` parameters are only supported when the application is
+/// running with the CanvasKit renderer. When the application is using the HTML renderer,
+/// the web engine delegates image decoding of network images to the Web, which does not support
/// custom decode sizes.
///
/// See also:
@@ -342,12 +343,7 @@
this.gaplessPlayback = false,
this.isAntiAlias = false,
this.filterQuality = FilterQuality.low,
- }) : assert(image != null),
- assert(alignment != null),
- assert(repeat != null),
- assert(filterQuality != null),
- assert(matchTextDirection != null),
- assert(isAntiAlias != null);
+ });
/// Creates a widget that displays an [ImageStream] obtained from the network.
///
@@ -405,12 +401,8 @@
int? cacheWidth,
int? cacheHeight,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
- assert(cacheHeight == null || cacheHeight > 0),
- assert(isAntiAlias != null);
+ assert(cacheHeight == null || cacheHeight > 0);
/// Creates a widget that displays an [ImageStream] obtained from a [File].
///
@@ -474,13 +466,8 @@
),
image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)),
loadingBuilder = null,
- assert(alignment != null),
- assert(repeat != null),
- assert(filterQuality != null),
- assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
- assert(cacheHeight == null || cacheHeight > 0),
- assert(isAntiAlias != null);
+ assert(cacheHeight == null || cacheHeight > 0);
// TODO(ianh): Implement the following (see ../services/image_resolution.dart):
//
@@ -640,12 +627,8 @@
: AssetImage(name, bundle: bundle, package: package),
),
loadingBuilder = null,
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
- assert(cacheHeight == null || cacheHeight > 0),
- assert(isAntiAlias != null);
+ assert(cacheHeight == null || cacheHeight > 0);
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
///
@@ -702,12 +685,8 @@
int? cacheHeight,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)),
loadingBuilder = null,
- assert(alignment != null),
- assert(repeat != null),
- assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
- assert(cacheHeight == null || cacheHeight > 0),
- assert(isAntiAlias != null);
+ assert(cacheHeight == null || cacheHeight > 0);
/// The image to display.
final ImageProvider image;
@@ -1106,7 +1085,7 @@
}
void _updateInvertColors() {
- _invertColors = MediaQuery.maybeOf(context)?.invertColors
+ _invertColors = MediaQuery.maybeInvertColorsOf(context)
?? SemanticsBinding.instance.accessibilityFeatures.invertColors;
}
@@ -1120,7 +1099,6 @@
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
- assert(newStream != null);
_updateSourceStream(newStream);
}
diff --git a/framework/lib/src/widgets/image_filter.dart b/framework/lib/src/widgets/image_filter.dart
index 963e231..5dec119 100644
--- a/framework/lib/src/widgets/image_filter.dart
+++ b/framework/lib/src/widgets/image_filter.dart
@@ -41,7 +41,7 @@
required this.imageFilter,
super.child,
this.enabled = true,
- }) : assert(imageFilter != null);
+ });
/// The image filter to apply to the child of this widget.
final ImageFilter imageFilter;
@@ -90,7 +90,6 @@
ImageFilter get imageFilter => _imageFilter;
ImageFilter _imageFilter;
set imageFilter(ImageFilter value) {
- assert(value != null);
if (value != _imageFilter) {
_imageFilter = value;
markNeedsCompositedLayerUpdate();
diff --git a/framework/lib/src/widgets/implicit_animations.dart b/framework/lib/src/widgets/implicit_animations.dart
index 6473601..302e865 100644
--- a/framework/lib/src/widgets/implicit_animations.dart
+++ b/framework/lib/src/widgets/implicit_animations.dart
@@ -281,8 +281,7 @@
this.curve = Curves.linear,
required this.duration,
this.onEnd,
- }) : assert(curve != null),
- assert(duration != null);
+ });
/// The curve to apply when animating the parameters of this container.
final Curve curve;
@@ -816,8 +815,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(padding != null),
- assert(padding.isNonNegative);
+ }) : assert(padding.isNonNegative);
/// The amount of space by which to inset the child.
final EdgeInsetsGeometry padding;
@@ -907,8 +905,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(alignment != null),
- assert(widthFactor == null || widthFactor >= 0.0),
+ }) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0);
/// How to align the child.
@@ -1361,7 +1358,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(scale != null);
+ });
/// The widget below this widget in the tree.
///
@@ -1488,7 +1485,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(turns != null);
+ });
/// The widget below this widget in the tree.
///
@@ -1699,7 +1696,7 @@
required super.duration,
super.onEnd,
this.alwaysIncludeSemantics = false,
- }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0);
+ }) : assert(opacity >= 0.0 && opacity <= 1.0);
/// The widget below this widget in the tree.
///
@@ -1794,7 +1791,7 @@
required super.duration,
super.onEnd,
this.alwaysIncludeSemantics = false,
- }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0);
+ }) : assert(opacity >= 0.0 && opacity <= 1.0);
/// The sliver below this widget in the tree.
final Widget? sliver;
@@ -1889,12 +1886,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(style != null),
- assert(child != null),
- assert(softWrap != null),
- assert(overflow != null),
- assert(maxLines == null || maxLines > 0),
- assert(textWidthBasis != null);
+ }) : assert(maxLines == null || maxLines > 0);
/// The widget below this widget in the tree.
///
@@ -2017,15 +2009,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(child != null),
- assert(shape != null),
- assert(clipBehavior != null),
- assert(borderRadius != null),
- assert(elevation != null && elevation >= 0.0),
- assert(color != null),
- assert(shadowColor != null),
- assert(animateColor != null),
- assert(animateShadowColor != null);
+ }) : assert(elevation >= 0.0);
/// The widget below this widget in the tree.
///
@@ -2151,8 +2135,7 @@
super.curve,
required super.duration,
super.onEnd,
- }) : assert(alignment != null),
- assert(widthFactor == null || widthFactor >= 0.0),
+ }) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0);
/// The widget below this widget in the tree.
diff --git a/framework/lib/src/widgets/inherited_notifier.dart b/framework/lib/src/widgets/inherited_notifier.dart
index d4ad02c..aa77678 100644
--- a/framework/lib/src/widgets/inherited_notifier.dart
+++ b/framework/lib/src/widgets/inherited_notifier.dart
@@ -64,7 +64,7 @@
super.key,
this.notifier,
required super.child,
- }) : assert(child != null);
+ });
/// The [Listenable] object to which to listen.
///
diff --git a/framework/lib/src/widgets/inherited_theme.dart b/framework/lib/src/widgets/inherited_theme.dart
index 920ebaf..673a94f 100644
--- a/framework/lib/src/widgets/inherited_theme.dart
+++ b/framework/lib/src/widgets/inherited_theme.dart
@@ -64,8 +64,6 @@
/// the wrapped `child` - unless this method is called again to re-wrap the
/// child.
static Widget captureAll(BuildContext context, Widget child, {BuildContext? to}) {
- assert(child != null);
- assert(context != null);
return capture(from: context, to: to).wrap(child);
}
@@ -88,7 +86,6 @@
/// This method can be expensive if there are many widgets between `from` and
/// `to` (it walks the element tree between those nodes).
static CapturedThemes capture({ required BuildContext from, required BuildContext? to }) {
- assert(from != null);
if (from == to) {
// Nothing to capture.
@@ -148,7 +145,7 @@
const _CaptureAll({
required this.themes,
required this.child,
- }) : assert(themes != null), assert(child != null);
+ });
final List<InheritedTheme> themes;
final Widget child;
diff --git a/framework/lib/src/widgets/interactive_viewer.dart b/framework/lib/src/widgets/interactive_viewer.dart
index ac80144..27719e5 100644
--- a/framework/lib/src/widgets/interactive_viewer.dart
+++ b/framework/lib/src/widgets/interactive_viewer.dart
@@ -85,24 +85,17 @@
this.onInteractionUpdate,
this.panEnabled = true,
this.scaleEnabled = true,
- this.scaleFactor = 200.0,
+ this.scaleFactor = kDefaultMouseScrollToScaleFactor,
this.transformationController,
this.alignment,
+ this.trackpadScrollCausesScale = false,
required Widget this.child,
- }) : assert(alignPanAxis != null),
- assert(panAxis != null),
- assert(child != null),
- assert(constrained != null),
- assert(minScale != null),
- assert(minScale > 0),
+ }) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
- assert(maxScale != null),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
- assert(panEnabled != null),
- assert(scaleEnabled != null),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
@@ -143,19 +136,14 @@
this.scaleFactor = 200.0,
this.transformationController,
this.alignment,
+ this.trackpadScrollCausesScale = false,
required InteractiveViewerWidgetBuilder this.builder,
- }) : assert(panAxis != null),
- assert(builder != null),
- assert(minScale != null),
- assert(minScale > 0),
+ }) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
- assert(maxScale != null),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
- assert(panEnabled != null),
- assert(scaleEnabled != null),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
@@ -295,10 +283,12 @@
/// * [panEnabled], which is similar but for panning.
final bool scaleEnabled;
+ /// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
+ final bool trackpadScrollCausesScale;
+
/// Determines the amount of scale to be performed per pointer scroll.
///
- /// Defaults to 200.0, which was arbitrarily chosen to feel natural for most
- /// trackpads and mousewheels on all supported platforms.
+ /// Defaults to [kDefaultMouseScrollToScaleFactor].
///
/// Increasing this value above the default causes scaling to feel slower,
/// while decreasing it causes scaling to feel faster.
@@ -556,7 +546,10 @@
final GlobalKey _childKey = GlobalKey();
final GlobalKey _parentKey = GlobalKey();
Animation<Offset>? _animation;
+ Animation<double>? _scaleAnimation;
+ late Offset _scaleAnimationFocalPoint;
late AnimationController _controller;
+ late AnimationController _scaleController;
Axis? _currentAxis; // Used with panAxis.
Offset? _referenceFocalPoint; // Point where the current gesture began.
double? _scaleStart; // Scale value at start of scaling gesture.
@@ -795,6 +788,12 @@
_animation?.removeListener(_onAnimate);
_animation = null;
}
+ if (_scaleController.isAnimating) {
+ _scaleController.stop();
+ _scaleController.reset();
+ _scaleAnimation?.removeListener(_onScaleAnimate);
+ _scaleAnimation = null;
+ }
_gestureType = null;
_currentAxis = null;
@@ -809,6 +808,7 @@
// handled with GestureDetector's scale gesture.
void _onScaleUpdate(ScaleUpdateDetails details) {
final double scale = _transformationController!.value.getMaxScaleOnAxis();
+ _scaleAnimationFocalPoint = details.localFocalPoint;
final Offset focalPointScene = _transformationController!.toScene(
details.localFocalPoint,
);
@@ -913,52 +913,122 @@
_referenceFocalPoint = null;
_animation?.removeListener(_onAnimate);
+ _scaleAnimation?.removeListener(_onScaleAnimate);
_controller.reset();
+ _scaleController.reset();
if (!_gestureIsSupported(_gestureType)) {
_currentAxis = null;
return;
}
- // If the scale ended with enough velocity, animate inertial movement.
- if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
- _currentAxis = null;
- return;
+ if (_gestureType == _GestureType.pan) {
+ if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
+ _currentAxis = null;
+ return;
+ }
+ final Vector3 translationVector = _transformationController!.value.getTranslation();
+ final Offset translation = Offset(translationVector.x, translationVector.y);
+ final FrictionSimulation frictionSimulationX = FrictionSimulation(
+ widget.interactionEndFrictionCoefficient,
+ translation.dx,
+ details.velocity.pixelsPerSecond.dx,
+ );
+ final FrictionSimulation frictionSimulationY = FrictionSimulation(
+ widget.interactionEndFrictionCoefficient,
+ translation.dy,
+ details.velocity.pixelsPerSecond.dy,
+ );
+ final double tFinal = _getFinalTime(
+ details.velocity.pixelsPerSecond.distance,
+ widget.interactionEndFrictionCoefficient,
+ );
+ _animation = Tween<Offset>(
+ begin: translation,
+ end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
+ ).animate(CurvedAnimation(
+ parent: _controller,
+ curve: Curves.decelerate,
+ ));
+ _controller.duration = Duration(milliseconds: (tFinal * 1000).round());
+ _animation!.addListener(_onAnimate);
+ _controller.forward();
+ } else if (_gestureType == _GestureType.scale) {
+ if (details.scaleVelocity.abs() < 0.1) {
+ _currentAxis = null;
+ return;
+ }
+ final double scale = _transformationController!.value.getMaxScaleOnAxis();
+ final FrictionSimulation frictionSimulation = FrictionSimulation(
+ widget.interactionEndFrictionCoefficient * widget.scaleFactor,
+ scale,
+ details.scaleVelocity / 10
+ );
+ final double tFinal = _getFinalTime(details.scaleVelocity.abs(), widget.interactionEndFrictionCoefficient, effectivelyMotionless: 0.1);
+ _scaleAnimation = Tween<double>(
+ begin: scale,
+ end: frictionSimulation.x(tFinal)
+ ).animate(CurvedAnimation(
+ parent: _scaleController,
+ curve: Curves.decelerate
+ ));
+ _scaleController.duration = Duration(milliseconds: (tFinal * 1000).round());
+ _scaleAnimation!.addListener(_onScaleAnimate);
+ _scaleController.forward();
}
-
- final Vector3 translationVector = _transformationController!.value.getTranslation();
- final Offset translation = Offset(translationVector.x, translationVector.y);
- final FrictionSimulation frictionSimulationX = FrictionSimulation(
- widget.interactionEndFrictionCoefficient,
- translation.dx,
- details.velocity.pixelsPerSecond.dx,
- );
- final FrictionSimulation frictionSimulationY = FrictionSimulation(
- widget.interactionEndFrictionCoefficient,
- translation.dy,
- details.velocity.pixelsPerSecond.dy,
- );
- final double tFinal = _getFinalTime(
- details.velocity.pixelsPerSecond.distance,
- widget.interactionEndFrictionCoefficient,
- );
- _animation = Tween<Offset>(
- begin: translation,
- end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
- ).animate(CurvedAnimation(
- parent: _controller,
- curve: Curves.decelerate,
- ));
- _controller.duration = Duration(milliseconds: (tFinal * 1000).round());
- _animation!.addListener(_onAnimate);
- _controller.forward();
}
- // Handle mousewheel scroll events.
+ // Handle mousewheel and web trackpad scroll events.
void _receivedPointerSignal(PointerSignalEvent event) {
final double scaleChange;
if (event is PointerScrollEvent) {
- // Ignore left and right scroll.
+ if (event.kind == PointerDeviceKind.trackpad) {
+ // Trackpad scroll, so treat it as a pan.
+ widget.onInteractionStart?.call(
+ ScaleStartDetails(
+ focalPoint: event.position,
+ localFocalPoint: event.localPosition,
+ ),
+ );
+
+ final Offset localDelta = PointerEvent.transformDeltaViaPositions(
+ untransformedEndPosition: event.position + event.scrollDelta,
+ untransformedDelta: event.scrollDelta,
+ transform: event.transform,
+ );
+
+ if (!_gestureIsSupported(_GestureType.pan)) {
+ widget.onInteractionUpdate?.call(ScaleUpdateDetails(
+ focalPoint: event.position - event.scrollDelta,
+ localFocalPoint: event.localPosition - event.scrollDelta,
+ focalPointDelta: -localDelta,
+ ));
+ widget.onInteractionEnd?.call(ScaleEndDetails());
+ return;
+ }
+
+ final Offset focalPointScene = _transformationController!.toScene(
+ event.localPosition,
+ );
+
+ final Offset newFocalPointScene = _transformationController!.toScene(
+ event.localPosition - localDelta,
+ );
+
+ _transformationController!.value = _matrixTranslate(
+ _transformationController!.value,
+ newFocalPointScene - focalPointScene
+ );
+
+ widget.onInteractionUpdate?.call(ScaleUpdateDetails(
+ focalPoint: event.position - event.scrollDelta,
+ localFocalPoint: event.localPosition - localDelta,
+ focalPointDelta: -localDelta
+ ));
+ widget.onInteractionEnd?.call(ScaleEndDetails());
+ return;
+ }
+ // Ignore left and right mouse wheel scroll.
if (event.scrollDelta.dy == 0.0) {
return;
}
@@ -1039,6 +1109,38 @@
);
}
+ // Handle inertia scale animation.
+ void _onScaleAnimate() {
+ if (!_scaleController.isAnimating) {
+ _currentAxis = null;
+ _scaleAnimation?.removeListener(_onScaleAnimate);
+ _scaleAnimation = null;
+ _scaleController.reset();
+ return;
+ }
+ final double desiredScale = _scaleAnimation!.value;
+ final double scaleChange = desiredScale / _transformationController!.value.getMaxScaleOnAxis();
+ final Offset referenceFocalPoint = _transformationController!.toScene(
+ _scaleAnimationFocalPoint,
+ );
+ _transformationController!.value = _matrixScale(
+ _transformationController!.value,
+ scaleChange,
+ );
+
+ // While scaling, translate such that the user's two fingers stay on
+ // the same places in the scene. That means that the focal point of
+ // the scale should be on the same place in the scene before and after
+ // the scale.
+ final Offset focalPointSceneScaled = _transformationController!.toScene(
+ _scaleAnimationFocalPoint,
+ );
+ _transformationController!.value = _matrixTranslate(
+ _transformationController!.value,
+ focalPointSceneScaled - referenceFocalPoint,
+ );
+ }
+
void _onTransformationControllerChange() {
// A change to the TransformationController's value is a change to the
// state.
@@ -1055,6 +1157,9 @@
_controller = AnimationController(
vsync: this,
);
+ _scaleController = AnimationController(
+ vsync: this
+ );
}
@override
@@ -1085,6 +1190,7 @@
@override
void dispose() {
_controller.dispose();
+ _scaleController.dispose();
_transformationController!.removeListener(_onTransformationControllerChange);
if (widget.transformationController == null) {
_transformationController!.dispose();
@@ -1135,13 +1241,15 @@
onScaleEnd: _onScaleEnd,
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
+ trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
+ trackpadScrollToScaleFactor: Offset(0, -1/widget.scaleFactor),
child: child,
),
);
}
}
-// This widget simply allows us to easily swap in and out the LayoutBuilder in
+// This widget allows us to easily swap in and out the LayoutBuilder in
// InteractiveViewer's depending on if it's using a builder or a child.
class _InteractiveViewerBuilt extends StatelessWidget {
const _InteractiveViewerBuilt({
@@ -1259,8 +1367,7 @@
// Given a velocity and drag, calculate the time at which motion will come to
// a stop, within the margin of effectivelyMotionless.
-double _getFinalTime(double velocity, double drag) {
- const double effectivelyMotionless = 10.0;
+double _getFinalTime(double velocity, double drag, {double effectivelyMotionless = 10}) {
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
}
diff --git a/framework/lib/src/widgets/keyboard_listener.dart b/framework/lib/src/widgets/keyboard_listener.dart
index d17b959..fcca974 100644
--- a/framework/lib/src/widgets/keyboard_listener.dart
+++ b/framework/lib/src/widgets/keyboard_listener.dart
@@ -50,10 +50,7 @@
this.includeSemantics = true,
this.onKeyEvent,
required this.child,
- }) : assert(focusNode != null),
- assert(autofocus != null),
- assert(includeSemantics != null),
- assert(child != null);
+ });
/// Controls whether this widget has keyboard focus.
final FocusNode focusNode;
diff --git a/framework/lib/src/widgets/layout_builder.dart b/framework/lib/src/widgets/layout_builder.dart
index aac041e..be1a907 100644
--- a/framework/lib/src/widgets/layout_builder.dart
+++ b/framework/lib/src/widgets/layout_builder.dart
@@ -40,7 +40,7 @@
const ConstrainedLayoutBuilder({
super.key,
required this.builder,
- }) : assert(builder != null);
+ });
@override
RenderObjectElement createElement() => _LayoutBuilderElement<ConstraintType>(this);
@@ -267,7 +267,7 @@
const LayoutBuilder({
super.key,
required super.builder,
- }) : assert(builder != null);
+ });
@override
RenderObject createRenderObject(BuildContext context) => _RenderLayoutBuilder();
diff --git a/framework/lib/src/widgets/list_wheel_scroll_view.dart b/framework/lib/src/widgets/list_wheel_scroll_view.dart
index bc0f6c9..18464d5 100644
--- a/framework/lib/src/widgets/list_wheel_scroll_view.dart
+++ b/framework/lib/src/widgets/list_wheel_scroll_view.dart
@@ -81,7 +81,7 @@
/// conditions.
class ListWheelChildListDelegate extends ListWheelChildDelegate {
/// Constructs the delegate from a concrete list of children.
- ListWheelChildListDelegate({required this.children}) : assert(children != null);
+ ListWheelChildListDelegate({required this.children});
/// The list containing all children that can be supplied.
final List<Widget> children;
@@ -125,7 +125,7 @@
/// conditions.
class ListWheelChildLoopingListDelegate extends ListWheelChildDelegate {
/// Constructs the delegate from a concrete list of children.
- ListWheelChildLoopingListDelegate({required this.children}) : assert(children != null);
+ ListWheelChildLoopingListDelegate({required this.children});
/// The list containing all children that can be supplied.
final List<Widget> children;
@@ -162,7 +162,7 @@
ListWheelChildBuilderDelegate({
required this.builder,
this.childCount,
- }) : assert(builder != null);
+ });
/// Called lazily to build children.
final NullableIndexedWidgetBuilder builder;
@@ -218,7 +218,7 @@
/// [initialItem] defaults to 0 and must not be null.
FixedExtentScrollController({
this.initialItem = 0,
- }) : assert(initialItem != null);
+ });
/// The page to show when first creating the scroll view.
///
@@ -316,6 +316,7 @@
required super.viewportDimension,
required super.axisDirection,
required this.itemIndex,
+ required super.devicePixelRatio,
});
@override
@@ -326,6 +327,7 @@
double? viewportDimension,
AxisDirection? axisDirection,
int? itemIndex,
+ double? devicePixelRatio,
}) {
return FixedExtentMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
@@ -334,6 +336,7 @@
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
itemIndex: itemIndex ?? this.itemIndex,
+ devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
);
}
@@ -399,6 +402,7 @@
double? viewportDimension,
AxisDirection? axisDirection,
int? itemIndex,
+ double? devicePixelRatio,
}) {
return FixedExtentMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
@@ -407,6 +411,7 @@
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
itemIndex: itemIndex ?? this.itemIndex,
+ devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
);
}
}
@@ -505,8 +510,8 @@
// Scenario 3:
// If there's no velocity and we're already at where we intend to land,
// do nothing.
- if (velocity.abs() < tolerance.velocity
- && (settlingPixels - metrics.pixels).abs() < tolerance.distance) {
+ if (velocity.abs() < toleranceFor(position).velocity
+ && (settlingPixels - metrics.pixels).abs() < toleranceFor(position).distance) {
return null;
}
@@ -519,7 +524,7 @@
metrics.pixels,
settlingPixels,
velocity,
- tolerance: tolerance,
+ tolerance: toleranceFor(position),
);
}
@@ -530,7 +535,7 @@
metrics.pixels,
settlingPixels,
velocity,
- tolerance.velocity * velocity.sign,
+ toleranceFor(position).velocity * velocity.sign,
);
}
}
@@ -569,21 +574,13 @@
this.restorationId,
this.scrollBehavior,
required List<Widget> children,
- }) : assert(children != null),
- assert(diameterRatio != null),
- assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
- assert(perspective != null),
+ }) : assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(perspective > 0),
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
assert(magnification > 0),
- assert(overAndUnderCenterOpacity != null),
assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1),
- assert(itemExtent != null),
assert(itemExtent > 0),
- assert(squeeze != null),
assert(squeeze > 0),
- assert(renderChildrenOutsideViewport != null),
- assert(clipBehavior != null),
assert(
!renderChildrenOutsideViewport || clipBehavior == Clip.none,
RenderListWheelViewport.clipBehaviorAndRenderChildrenOutsideViewportConflict,
@@ -610,21 +607,13 @@
this.restorationId,
this.scrollBehavior,
required this.childDelegate,
- }) : assert(childDelegate != null),
- assert(diameterRatio != null),
- assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
- assert(perspective != null),
+ }) : assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(perspective > 0),
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
assert(magnification > 0),
- assert(overAndUnderCenterOpacity != null),
assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1),
- assert(itemExtent != null),
assert(itemExtent > 0),
- assert(squeeze != null),
assert(squeeze > 0),
- assert(renderChildrenOutsideViewport != null),
- assert(clipBehavior != null),
assert(
!renderChildrenOutsideViewport || clipBehavior == Clip.none,
RenderListWheelViewport.clipBehaviorAndRenderChildrenOutsideViewportConflict,
@@ -982,21 +971,12 @@
required this.offset,
required this.childDelegate,
this.clipBehavior = Clip.hardEdge,
- }) : assert(childDelegate != null),
- assert(offset != null),
- assert(diameterRatio != null),
- assert(diameterRatio > 0, RenderListWheelViewport.diameterRatioZeroMessage),
- assert(perspective != null),
+ }) : assert(diameterRatio > 0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(perspective > 0),
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
- assert(overAndUnderCenterOpacity != null),
assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1),
- assert(itemExtent != null),
assert(itemExtent > 0),
- assert(squeeze != null),
assert(squeeze > 0),
- assert(renderChildrenOutsideViewport != null),
- assert(clipBehavior != null),
assert(
!renderChildrenOutsideViewport || clipBehavior == Clip.none,
RenderListWheelViewport.clipBehaviorAndRenderChildrenOutsideViewportConflict,
diff --git a/framework/lib/src/widgets/localizations.dart b/framework/lib/src/widgets/localizations.dart
index 8e5bdba..77db0fa 100644
--- a/framework/lib/src/widgets/localizations.dart
+++ b/framework/lib/src/widgets/localizations.dart
@@ -235,8 +235,7 @@
required this.localizationsState,
required this.typeToResources,
required super.child,
- }) : assert(localizationsState != null),
- assert(typeToResources != null);
+ });
final Locale locale;
final _LocalizationsState localizationsState;
@@ -319,8 +318,8 @@
/// }
/// ```
///
-/// Each delegate can be viewed as a factory for objects that encapsulate a
-/// a set of localized resources. These objects are retrieved with
+/// Each delegate can be viewed as a factory for objects that encapsulate a set
+/// of localized resources. These objects are retrieved with
/// by runtime type with [Localizations.of].
///
/// The [WidgetsApp] class creates a [Localizations] widget so most apps
@@ -366,9 +365,7 @@
required this.locale,
required this.delegates,
this.child,
- }) : assert(locale != null),
- assert(delegates != null),
- assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>));
+ }) : assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>));
/// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`.
///
@@ -434,7 +431,6 @@
/// If no [Localizations] widget is in scope then the [Localizations.localeOf]
/// method will throw an exception.
static Locale localeOf(BuildContext context) {
- assert(context != null);
final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
assert(() {
if (scope == null) {
@@ -460,7 +456,6 @@
/// If no [Localizations] widget is in scope then this function will return
/// null.
static Locale? maybeLocaleOf(BuildContext context) {
- assert(context != null);
final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
return scope?.localizationsState.locale;
}
@@ -468,7 +463,6 @@
// There doesn't appear to be a need to make this public. See the
// Localizations.override factory constructor.
static List<LocalizationsDelegate<dynamic>> _delegatesOf(BuildContext context) {
- assert(context != null);
final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
assert(scope != null, 'a Localizations ancestor was not found');
return List<LocalizationsDelegate<dynamic>>.of(scope!.localizationsState.widget.delegates);
@@ -490,8 +484,6 @@
/// }
/// ```
static T? of<T>(BuildContext context, Type type) {
- assert(context != null);
- assert(type != null);
final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
return scope?.localizationsState.resourcesFor<T?>(type);
}
@@ -539,17 +531,14 @@
@override
void didUpdateWidget(Localizations old) {
super.didUpdateWidget(old);
- if (widget.locale != old.locale
- || (widget.delegates == null)
- || (widget.delegates != null && old.delegates == null)
- || (widget.delegates != null && _anyDelegatesShouldReload(old))) {
+ if (widget.locale != old.locale || (_anyDelegatesShouldReload(old))) {
load(widget.locale);
}
}
void load(Locale locale) {
final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates;
- if (delegates == null || delegates.isEmpty) {
+ if (delegates.isEmpty) {
_locale = locale;
return;
}
@@ -583,14 +572,12 @@
}
T resourcesFor<T>(Type type) {
- assert(type != null);
final T resources = _typeToResources[type] as T;
return resources;
}
TextDirection get _textDirection {
final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations] as WidgetsLocalizations;
- assert(resources != null);
return resources.textDirection;
}
diff --git a/framework/lib/src/widgets/lookup_boundary.dart b/framework/lib/src/widgets/lookup_boundary.dart
new file mode 100644
index 0000000..40ebaea
--- /dev/null
+++ b/framework/lib/src/widgets/lookup_boundary.dart
@@ -0,0 +1,325 @@
+// 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 'framework.dart';
+
+// Examples can assume:
+// class MyWidget extends StatelessWidget { const MyWidget({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) => child; }
+
+/// A lookup boundary controls what entities are visible to descendants of the
+/// boundary via the static lookup methods provided by the boundary.
+///
+/// The static lookup methods of the boundary mirror the lookup methods by the
+/// same name exposed on [BuildContext] and they can be used as direct
+/// replacements. Unlike the methods on [BuildContext], these methods do not
+/// find any ancestor entities of the closest [LookupBoundary] surrounding the
+/// provided [BuildContext]. The root of the tree is an implicit lookup boundary.
+///
+/// {@tool snippet}
+/// In the example below, the [LookupBoundary.findAncestorWidgetOfExactType]
+/// call returns null because the [LookupBoundary] "hides" `MyWidget` from the
+/// [BuildContext] that was queried.
+///
+/// ```dart
+/// MyWidget(
+/// child: LookupBoundary(
+/// child: Builder(
+/// builder: (BuildContext context) {
+/// MyWidget? widget = LookupBoundary.findAncestorWidgetOfExactType<MyWidget>(context);
+/// return Text('$widget'); // "null"
+/// },
+/// ),
+/// ),
+/// )
+/// ```
+/// {@end-tool}
+///
+/// A [LookupBoundary] only affects the behavior of the static lookup methods
+/// defined on the boundary. It does not affect the behavior of the lookup
+/// methods defined on [BuildContext].
+///
+/// A [LookupBoundary] is rarely instantiated directly. They are inserted at
+/// locations of the widget tree where the render tree diverges from the element
+/// tree, which is rather uncommon. Such anomalies are created by
+/// [RenderObjectElement]s that don't attach their [RenderObject] to the closest
+/// ancestor [RenderObjectElement], e.g. because they bootstrap a separate
+/// stand-alone render tree.
+// TODO(goderbauer): Reference the View widget here once available.
+/// This behavior breaks the assumption some widgets have about the structure of
+/// the render tree: These widgets may try to reach out to an ancestor widget,
+/// assuming that their associated [RenderObject]s are also ancestors, which due
+/// to the anomaly may not be the case. At the point where the divergence in the
+/// two trees is introduced, a [LookupBoundary] can be used to hide that ancestor
+/// from the querying widget.
+///
+/// As an example, [Material.of] relies on lookup boundaries to hide the
+/// [Material] widget from certain descendant button widget. Buttons reach out
+/// to their [Material] ancestor to draw ink splashes on its associated render
+/// object. This only produces the desired effect if the button render object
+/// is a descendant of the [Material] render object. If the element tree and
+/// the render tree are not in sync due to anomalies described above, this may
+/// not be the case. To avoid incorrect visuals, the [Material] relies on
+/// lookup boundaries to hide itself from descendants in subtrees with such
+/// anomalies. Those subtrees are expected to introduce their own [Material]
+/// widget that buttons there can utilize without crossing a lookup boundary.
+class LookupBoundary extends InheritedWidget {
+ /// Creates a [LookupBoundary].
+ ///
+ /// A none-null [child] widget must be provided.
+ const LookupBoundary({super.key, required super.child});
+
+ /// Obtains the nearest widget of the given type `T` within the current
+ /// [LookupBoundary] of `context`, which must be the type of a concrete
+ /// [InheritedWidget] subclass, and registers the provided 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), the build context is
+ /// rebuilt so that it can obtain new values from that widget.
+ ///
+ /// This method behaves exactly like
+ /// [BuildContext.dependOnInheritedWidgetOfExactType], except it only
+ /// considers [InheritedWidget]s of the specified type `T` between the
+ /// provided [BuildContext] and its closest [LookupBoundary] ancestor.
+ /// [InheritedWidget]s past that [LookupBoundary] are invisible to this
+ /// method. The root of the tree is treated as an implicit lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType}
+ static T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(BuildContext context, { Object? aspect }) {
+ // The following call makes sure that context depends on something so
+ // Element.didChangeDependencies is called when context moves in the tree
+ // even when requested dependency remains unfulfilled (i.e. null is
+ // returned).
+ context.dependOnInheritedWidgetOfExactType<LookupBoundary>();
+ final InheritedElement? candidate = getElementForInheritedWidgetOfExactType<T>(context);
+ if (candidate == null) {
+ return null;
+ }
+ context.dependOnInheritedElement(candidate, aspect: aspect);
+ return candidate.widget as T;
+ }
+
+ /// Obtains the element corresponding to the nearest widget of the given type
+ /// `T` within the current [LookupBoundary] of `context`.
+ ///
+ /// `T` must be the type of a concrete [InheritedWidget] subclass. Returns
+ /// null if no such element is found.
+ ///
+ /// This method behaves exactly like
+ /// [BuildContext.getElementForInheritedWidgetOfExactType], except it only
+ /// considers [InheritedWidget]s of the specified type `T` between the
+ /// provided [BuildContext] and its closest [LookupBoundary] ancestor.
+ /// [InheritedWidget]s past that [LookupBoundary] are invisible to this
+ /// method. The root of the tree is treated as an implicit lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType}
+ static InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>(BuildContext context) {
+ final InheritedElement? candidate = context.getElementForInheritedWidgetOfExactType<T>();
+ if (candidate == null) {
+ return null;
+ }
+ final Element? boundary = context.getElementForInheritedWidgetOfExactType<LookupBoundary>();
+ if (boundary != null && boundary.depth > candidate.depth) {
+ return null;
+ }
+ return candidate;
+ }
+
+ /// Returns the nearest ancestor widget of the given type `T` within the
+ /// current [LookupBoundary] of `context`.
+ ///
+ /// `T` must be the type of a concrete [Widget] subclass.
+ ///
+ /// This method behaves exactly like
+ /// [BuildContext.findAncestorWidgetOfExactType], except it only considers
+ /// [Widget]s of the specified type `T` between the provided [BuildContext]
+ /// and its closest [LookupBoundary] ancestor. [Widget]s past that
+ /// [LookupBoundary] are invisible to this method. The root of the tree is
+ /// treated as an implicit lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.findAncestorWidgetOfExactType}
+ static T? findAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
+ Element? target;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor.widget.runtimeType == T) {
+ target = ancestor;
+ return false;
+ }
+ return ancestor.widget.runtimeType != LookupBoundary;
+ });
+ return target?.widget as T?;
+ }
+
+ /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
+ /// within the current [LookupBoundary] of `context` that is an instance of
+ /// the given type `T`.
+ ///
+ /// This method behaves exactly like
+ /// [BuildContext.findAncestorWidgetOfExactType], except it only considers
+ /// [State] objects of the specified type `T` between the provided
+ /// [BuildContext] and its closest [LookupBoundary] ancestor. [State] objects
+ /// past that [LookupBoundary] are invisible to this method. The root of the
+ /// tree is treated as an implicit lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.findAncestorStateOfType}
+ static T? findAncestorStateOfType<T extends State>(BuildContext context) {
+ StatefulElement? target;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor is StatefulElement && ancestor.state is T) {
+ target = ancestor;
+ return false;
+ }
+ return ancestor.widget.runtimeType != LookupBoundary;
+ });
+ return target?.state as T?;
+ }
+
+ /// Returns the [State] object of the furthest ancestor [StatefulWidget]
+ /// widget within the current [LookupBoundary] of `context` that is an
+ /// instance of the given type `T`.
+ ///
+ /// This method behaves exactly like
+ /// [BuildContext.findRootAncestorStateOfType], except it considers the
+ /// closest [LookupBoundary] ancestor of `context` to be the root. [State]
+ /// objects past that [LookupBoundary] are invisible to this method. The root
+ /// of the tree is treated as an implicit lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.findRootAncestorStateOfType}
+ static T? findRootAncestorStateOfType<T extends State>(BuildContext context) {
+ StatefulElement? target;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor is StatefulElement && ancestor.state is T) {
+ target = ancestor;
+ }
+ return ancestor.widget.runtimeType != LookupBoundary;
+ });
+ return target?.state as T?;
+ }
+
+ /// Returns the [RenderObject] object of the nearest ancestor
+ /// [RenderObjectWidget] widget within the current [LookupBoundary] of
+ /// `context` that is an instance of the given type `T`.
+ ///
+ /// This method behaves exactly like
+ /// [BuildContext.findAncestorRenderObjectOfType], except it only considers
+ /// [RenderObject]s of the specified type `T` between the provided
+ /// [BuildContext] and its closest [LookupBoundary] ancestor. [RenderObject]s
+ /// past that [LookupBoundary] are invisible to this method. The root of the
+ /// tree is treated as an implicit lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.findAncestorRenderObjectOfType}
+ static T? findAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
+ Element? target;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
+ target = ancestor;
+ return false;
+ }
+ return ancestor.widget.runtimeType != LookupBoundary;
+ });
+ return target?.renderObject as T?;
+ }
+
+ /// Walks the ancestor chain, starting with the parent of the build context's
+ /// widget, invoking the argument for each ancestor until a [LookupBoundary]
+ /// or the root is reached.
+ ///
+ /// This method behaves exactly like [BuildContext.visitAncestorElements],
+ /// except it only walks the tree up to the closest [LookupBoundary] ancestor
+ /// of the provided context. The root of the tree is treated as an implicit
+ /// lookup boundary.
+ ///
+ /// {@macro flutter.widgets.BuildContext.visitAncestorElements}
+ static void visitAncestorElements(BuildContext context, ConditionalElementVisitor visitor) {
+ context.visitAncestorElements((Element ancestor) {
+ return visitor(ancestor) && ancestor.widget.runtimeType != LookupBoundary;
+ });
+ }
+
+ /// Walks the non-[LookupBoundary] child [Element]s of the provided
+ /// `context`.
+ ///
+ /// This method behaves exactly like [BuildContext.visitChildElements],
+ /// except it only visits children that are not a [LookupBoundary].
+ ///
+ /// {@macro flutter.widgets.BuildContext.visitChildElements}
+ static void visitChildElements(BuildContext context, ElementVisitor visitor) {
+ context.visitChildElements((Element child) {
+ if (child.widget.runtimeType != LookupBoundary) {
+ visitor(child);
+ }
+ });
+ }
+
+ /// Returns true if a [LookupBoundary] is hiding the nearest
+ /// [Widget] of the specified type `T` from the provided [BuildContext].
+ ///
+ /// This method throws when asserts are disabled.
+ static bool debugIsHidingAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
+ bool? result;
+ assert(() {
+ bool hiddenByBoundary = false;
+ bool ancestorFound = false;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor.widget.runtimeType == T) {
+ ancestorFound = true;
+ return false;
+ }
+ hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
+ return true;
+ });
+ result = ancestorFound & hiddenByBoundary;
+ return true;
+ } ());
+ return result!;
+ }
+
+ /// Returns true if a [LookupBoundary] is hiding the nearest [StatefulWidget]
+ /// with a [State] of the specified type `T` from the provided [BuildContext].
+ ///
+ /// This method throws when asserts are disabled.
+ static bool debugIsHidingAncestorStateOfType<T extends State>(BuildContext context) {
+ bool? result;
+ assert(() {
+ bool hiddenByBoundary = false;
+ bool ancestorFound = false;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor is StatefulElement && ancestor.state is T) {
+ ancestorFound = true;
+ return false;
+ }
+ hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
+ return true;
+ });
+ result = ancestorFound & hiddenByBoundary;
+ return true;
+ } ());
+ return result!;
+ }
+
+ /// Returns true if a [LookupBoundary] is hiding the nearest
+ /// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
+ /// from the provided [BuildContext].
+ ///
+ /// This method throws when asserts are disabled.
+ static bool debugIsHidingAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
+ bool? result;
+ assert(() {
+ bool hiddenByBoundary = false;
+ bool ancestorFound = false;
+ context.visitAncestorElements((Element ancestor) {
+ if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
+ ancestorFound = true;
+ return false;
+ }
+ hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
+ return true;
+ });
+ result = ancestorFound & hiddenByBoundary;
+ return true;
+ } ());
+ return result!;
+ }
+
+ @override
+ bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
+}
diff --git a/framework/lib/src/widgets/media_query.dart b/framework/lib/src/widgets/media_query.dart
index 3a9adfd..7757bc9 100644
--- a/framework/lib/src/widgets/media_query.dart
+++ b/framework/lib/src/widgets/media_query.dart
@@ -12,6 +12,7 @@
import 'binding.dart';
import 'debug.dart';
import 'framework.dart';
+import 'inherited_model.dart';
// Examples can assume:
// late BuildContext context;
@@ -25,6 +26,52 @@
landscape
}
+/// Specifies a part of MediaQueryData to depend on.
+///
+/// [MediaQuery] contains a large number of related properties. Widgets frequently
+/// depend on only a few of these attributes. For example, a widget that needs to
+/// rebuild when the [MediaQueryData.textScaleFactor] changes does not need to
+/// be notified when the [MediaQueryData.size] changes. Specifying an aspect avoids
+/// unnecessary rebuilds.
+enum _MediaQueryAspect {
+ /// Specifies the aspect corresponding to [MediaQueryData.size].
+ size,
+ /// Specifies the aspect corresponding to [MediaQueryData.orientation].
+ orientation,
+ /// Specifies the aspect corresponding to [MediaQueryData.devicePixelRatio].
+ devicePixelRatio,
+ /// Specifies the aspect corresponding to [MediaQueryData.textScaleFactor].
+ textScaleFactor,
+ /// Specifies the aspect corresponding to [MediaQueryData.platformBrightness].
+ platformBrightness,
+ /// Specifies the aspect corresponding to [MediaQueryData.padding].
+ padding,
+ /// Specifies the aspect corresponding to [MediaQueryData.viewInsets].
+ viewInsets,
+ /// Specifies the aspect corresponding to [MediaQueryData.systemGestureInsets].
+ systemGestureInsets,
+ /// Specifies the aspect corresponding to [MediaQueryData.viewPadding].
+ viewPadding,
+ /// Specifies the aspect corresponding to [MediaQueryData.alwaysUse24HourFormat].
+ alwaysUse24HourFormat,
+ /// Specifies the aspect corresponding to [MediaQueryData.accessibleNavigation].
+ accessibleNavigation,
+ /// Specifies the aspect corresponding to [MediaQueryData.invertColors].
+ invertColors,
+ /// Specifies the aspect corresponding to [MediaQueryData.highContrast].
+ highContrast,
+ /// Specifies the aspect corresponding to [MediaQueryData.disableAnimations].
+ disableAnimations,
+ /// Specifies the aspect corresponding to [MediaQueryData.boldText].
+ boldText,
+ /// Specifies the aspect corresponding to [MediaQueryData.navigationMode].
+ navigationMode,
+ /// Specifies the aspect corresponding to [MediaQueryData.gestureSettings].
+ gestureSettings,
+ /// Specifies the aspect corresponding to [MediaQueryData.displayFeatures].
+ displayFeatures,
+}
+
/// Information about a piece of media (e.g., a window).
///
/// For example, the [MediaQueryData.size] property contains the width and
@@ -91,8 +138,8 @@
class MediaQueryData {
/// Creates data for a media query with explicit values.
///
- /// Consider using [MediaQueryData.fromWindow] to create data based on a
- /// [dart:ui.PlatformDispatcher].
+ /// Consider using [MediaQueryData.fromView] to create data based on a
+ /// [dart:ui.FlutterView].
const MediaQueryData({
this.size = Size.zero,
this.devicePixelRatio = 1.0,
@@ -111,49 +158,72 @@
this.navigationMode = NavigationMode.traditional,
this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop),
this.displayFeatures = const <ui.DisplayFeature>[],
- }) : assert(size != null),
- assert(devicePixelRatio != null),
- assert(textScaleFactor != null),
- assert(platformBrightness != null),
- assert(padding != null),
- assert(viewInsets != null),
- assert(systemGestureInsets != null),
- assert(viewPadding != null),
- assert(alwaysUse24HourFormat != null),
- assert(accessibleNavigation != null),
- assert(invertColors != null),
- assert(highContrast != null),
- assert(disableAnimations != null),
- assert(boldText != null),
- assert(navigationMode != null),
- assert(gestureSettings != null),
- assert(displayFeatures != null);
+ });
- /// Creates data for a media query based on the given window.
+ /// Deprecated. Use [MediaQueryData.fromView] instead.
///
- /// If you use this, you should ensure that you also register for
- /// notifications so that you can update your [MediaQueryData] when the
- /// window's metrics change. For example, see
- /// [WidgetsBindingObserver.didChangeMetrics] or
- /// [dart:ui.PlatformDispatcher.onMetricsChanged].
- MediaQueryData.fromWindow(ui.FlutterView window)
- : size = window.physicalSize / window.devicePixelRatio,
- devicePixelRatio = window.devicePixelRatio,
- textScaleFactor = window.platformDispatcher.textScaleFactor,
- platformBrightness = window.platformDispatcher.platformBrightness,
- padding = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio),
- viewPadding = EdgeInsets.fromWindowPadding(window.viewPadding, window.devicePixelRatio),
- viewInsets = EdgeInsets.fromWindowPadding(window.viewInsets, window.devicePixelRatio),
- systemGestureInsets = EdgeInsets.fromWindowPadding(window.systemGestureInsets, window.devicePixelRatio),
- accessibleNavigation = window.platformDispatcher.accessibilityFeatures.accessibleNavigation,
- invertColors = window.platformDispatcher.accessibilityFeatures.invertColors,
- disableAnimations = window.platformDispatcher.accessibilityFeatures.disableAnimations,
- boldText = window.platformDispatcher.accessibilityFeatures.boldText,
- highContrast = window.platformDispatcher.accessibilityFeatures.highContrast,
- alwaysUse24HourFormat = window.platformDispatcher.alwaysUse24HourFormat,
- navigationMode = NavigationMode.traditional,
- gestureSettings = DeviceGestureSettings.fromWindow(window),
- displayFeatures = window.displayFeatures;
+ /// This constructor was operating on a single window assumption. In
+ /// preparation for Flutter's upcoming multi-window support, it has been
+ /// deprecated.
+ @Deprecated(
+ 'Use MediaQueryData.fromView instead. '
+ 'This constructor was deprecated in preparation for the upcoming multi-window support. '
+ 'This feature was deprecated after v3.7.0-32.0.pre.'
+ )
+ factory MediaQueryData.fromWindow(ui.FlutterView window) => MediaQueryData.fromView(window);
+
+ /// Creates data for a [MediaQuery] based on the given `view`.
+ ///
+ /// If provided, the `platformData` is used to fill in the platform-specific
+ /// aspects of the newly created [MediaQueryData]. If `platformData` is null,
+ /// the `view`'s [PlatformDispatcher] is consulted to construct the
+ /// platform-specific data.
+ ///
+ /// Data which is exposed directly on the [FlutterView] is considered
+ /// view-specific. Data which is only exposed via the
+ /// [FlutterView.platformDispatcher] property is considered platform-specific.
+ ///
+ /// Callers of this method should ensure that they also register for
+ /// notifications so that the [MediaQueryData] can be updated when any data
+ /// used to construct it changes. Notifications to consider are:
+ ///
+ /// * [WidgetsBindingObserver.didChangeMetrics] or
+ /// [dart:ui.PlatformDispatcher.onMetricsChanged],
+ /// * [WidgetsBindingObserver.didChangeAccessibilityFeatures] or
+ /// [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged],
+ /// * [WidgetsBindingObserver.didChangeTextScaleFactor] or
+ /// [dart:ui.PlatformDispatcher.onTextScaleFactorChanged],
+ /// * [WidgetsBindingObserver.didChangePlatformBrightness] or
+ /// [dart:ui.PlatformDispatcher.onPlatformBrightnessChanged].
+ ///
+ /// The last three notifications are only relevant if no `platformData` is
+ /// provided. If `platformData` is provided, callers should ensure to call
+ /// this method again when it changes to keep the constructed [MediaQueryData]
+ /// updated.
+ ///
+ /// See also:
+ ///
+ /// * [MediaQuery.fromView], which constructs [MediaQueryData] from a provided
+ /// [FlutterView], makes it available to descendant widgets, and sets up
+ /// the appropriate notification listeners to keep the data updated.
+ MediaQueryData.fromView(ui.FlutterView view, {MediaQueryData? platformData})
+ : size = view.physicalSize / view.devicePixelRatio,
+ 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),
+ accessibleNavigation = platformData?.accessibleNavigation ?? view.platformDispatcher.accessibilityFeatures.accessibleNavigation,
+ invertColors = platformData?.invertColors ?? view.platformDispatcher.accessibilityFeatures.invertColors,
+ disableAnimations = platformData?.disableAnimations ?? view.platformDispatcher.accessibilityFeatures.disableAnimations,
+ boldText = platformData?.boldText ?? view.platformDispatcher.accessibilityFeatures.boldText,
+ highContrast = platformData?.highContrast ?? view.platformDispatcher.accessibilityFeatures.highContrast,
+ alwaysUse24HourFormat = platformData?.alwaysUse24HourFormat ?? view.platformDispatcher.alwaysUse24HourFormat,
+ navigationMode = platformData?.navigationMode ?? NavigationMode.traditional,
+ gestureSettings = DeviceGestureSettings.fromView(view),
+ displayFeatures = view.displayFeatures;
/// The size of the media in logical pixels (e.g, the size of the screen).
///
@@ -178,6 +248,8 @@
/// See also:
///
/// * [FlutterView.physicalSize], which returns the size in physical pixels.
+ /// * [MediaQuery.sizeOf], a method to find and depend on the size defined
+ /// for a [BuildContext].
final Size size;
/// The number of device pixels for each logical pixel. This number might not
@@ -192,7 +264,7 @@
///
/// See also:
///
- /// * [MediaQuery.textScaleFactorOf], a convenience method which returns the
+ /// * [MediaQuery.textScaleFactorOf], a method to find and depend on the
/// textScaleFactor defined for a [BuildContext].
final double textScaleFactor;
@@ -203,6 +275,11 @@
///
/// Not all platforms necessarily support a concept of brightness mode. Those
/// platforms will report [Brightness.light] in this property.
+ ///
+ /// See also:
+ ///
+ /// * [MediaQuery.platformBrightnessOf], a method to find and depend on the
+ /// platformBrightness defined for a [BuildContext].
final Brightness platformBrightness;
/// The parts of the display that are completely obscured by system UI,
@@ -217,6 +294,8 @@
/// level MediaQuery created by [WidgetsApp] are the same as the window
/// (often the mobile device screen) that contains the app.
///
+ /// {@youtube 560 315 https://www.youtube.com/watch?v=ceCo8U0XHqw}
+ ///
/// See also:
///
/// * [ui.window], which provides some additional detail about this property
@@ -234,6 +313,8 @@
///
/// Padding is derived from the values of [viewInsets] and [viewPadding].
///
+ /// {@youtube 560 315 https://www.youtube.com/watch?v=ceCo8U0XHqw}
+ ///
/// See also:
///
/// * [ui.window], which provides some additional detail about this
@@ -256,6 +337,8 @@
/// same as the window that contains the app. On mobile devices, this will
/// typically be the full screen.
///
+ /// {@youtube 560 315 https://www.youtube.com/watch?v=ceCo8U0XHqw}
+ ///
/// See also:
///
/// * [ui.window], which provides some additional detail about this
@@ -613,6 +696,7 @@
&& other.padding == padding
&& other.viewPadding == viewPadding
&& other.viewInsets == viewInsets
+ && other.systemGestureInsets == systemGestureInsets
&& other.alwaysUse24HourFormat == alwaysUse24HourFormat
&& other.highContrast == highContrast
&& other.disableAnimations == disableAnimations
@@ -654,6 +738,7 @@
'padding: $padding',
'viewPadding: $viewPadding',
'viewInsets: $viewInsets',
+ 'systemGestureInsets: $systemGestureInsets',
'alwaysUse24HourFormat: $alwaysUse24HourFormat',
'accessibleNavigation: $accessibleNavigation',
'highContrast: $highContrast',
@@ -690,7 +775,7 @@
/// * [WidgetsApp] and [MaterialApp], which introduce a [MediaQuery] and keep
/// it up to date with the current screen metrics as they change.
/// * [MediaQueryData], the data structure that represents the metrics.
-class MediaQuery extends InheritedWidget {
+class MediaQuery extends InheritedModel<_MediaQueryAspect> {
/// Creates a widget that provides [MediaQueryData] to its descendants.
///
/// The [data] and [child] arguments must not be null.
@@ -698,8 +783,7 @@
super.key,
required this.data,
required super.child,
- }) : assert(child != null),
- assert(data != null);
+ });
/// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery]
/// from the given context, but removes the specified padding.
@@ -837,24 +921,57 @@
);
}
- /// Provides a [MediaQuery] which is built and updated using the latest
- /// [WidgetsBinding.window] values.
+ /// Deprecated. Use [MediaQuery.fromView] instead.
///
- /// The [MediaQuery] is wrapped in a separate widget to ensure that only it
- /// and its dependents are updated when `window` changes, instead of
- /// rebuilding the whole widget tree.
+ /// This constructor was operating on a single window assumption. In
+ /// preparation for Flutter's upcoming multi-window support, it has been
+ /// deprecated.
///
- /// This should be inserted into the widget tree when the [MediaQuery] view
- /// padding is consumed by a widget in such a way that the view padding is no
- /// longer exposed to the widget's descendants or siblings.
- ///
- /// The [child] argument is required and must not be null.
+ /// Replaced by [MediaQuery.fromView], which requires specifying the
+ /// [FlutterView] the [MediaQuery] is constructed for. The [FlutterView] can,
+ /// for example, be obtained from the context via [View.of] or from
+ /// [PlatformDispatcher.views].
+ @Deprecated(
+ 'Use MediaQuery.fromView instead. '
+ 'This constructor was deprecated in preparation for the upcoming multi-window support. '
+ 'This feature was deprecated after v3.7.0-32.0.pre.'
+ )
static Widget fromWindow({
Key? key,
required Widget child,
}) {
- return _MediaQueryFromWindow(
+ return _MediaQueryFromView(
key: key,
+ view: WidgetsBinding.instance.window,
+ ignoreParentData: true,
+ child: child,
+ );
+ }
+
+ /// Wraps the [child] in a [MediaQuery] which is built using data from the
+ /// provided [view].
+ ///
+ /// The [MediaQuery] is constructed using the platform-specific data of the
+ /// surrounding [MediaQuery] and the view-specific data of the provided
+ /// [view]. If no surrounding [MediaQuery] exists, the platform-specific data
+ /// is generated from the [PlatformDispatcher] associated with the provided
+ /// [view]. Any information that's exposed via the [PlatformDispatcher] is
+ /// considered platform-specific. Data exposed directly on the [FlutterView]
+ /// (excluding its [FlutterView.platformDispatcher] property) is considered
+ /// view-specific.
+ ///
+ /// The injected [MediaQuery] automatically updates when any of the data used
+ /// to construct it changes.
+ ///
+ /// The [view] and [child] arguments are required and must not be null.
+ static Widget fromView({
+ Key? key,
+ required FlutterView view,
+ required Widget child,
+ }) {
+ return _MediaQueryFromView(
+ key: key,
+ view: view,
child: child,
);
}
@@ -873,6 +990,11 @@
/// examples). When that information changes, your widget will be scheduled to
/// be rebuilt, keeping your widget up-to-date.
///
+ /// If the widget only requires a subset of properties of the [MediaQueryData]
+ /// object, it is preferred to use the specific methods (for example:
+ /// [MediaQuery.sizeOf] and [MediaQuery.paddingOf]), as those methods will not
+ /// cause a widget to rebuild when unrelated properties are updated.
+ ///
/// Typical usage is as follows:
///
/// ```dart
@@ -888,9 +1010,12 @@
/// * [maybeOf], which doesn't throw or assert if it doesn't find a
/// [MediaQuery] ancestor, it returns null instead.
static MediaQueryData of(BuildContext context) {
- assert(context != null);
+ return _of(context);
+ }
+
+ static MediaQueryData _of(BuildContext context, [_MediaQueryAspect? aspect]) {
assert(debugCheckHasMediaQuery(context));
- return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
+ return InheritedModel.inheritFrom<MediaQuery>(context, aspect: aspect)!.data;
}
/// The data from the closest instance of this class that encloses the given
@@ -907,6 +1032,11 @@
/// examples). When that information changes, your widget will be scheduled to
/// be rebuilt, keeping your widget up-to-date.
///
+ /// If the widget only requires a subset of properties of the [MediaQueryData]
+ /// object, it is preferred to use the specific methods (for example:
+ /// [MediaQuery.maybeSizeOf] and [MediaQuery.maybePaddingOf]), as those methods
+ /// will not cause a widget to rebuild when unrelated properties are updated.
+ ///
/// Typical usage is as follows:
///
/// ```dart
@@ -921,24 +1051,182 @@
/// * [of], which will throw if it doesn't find a [MediaQuery] ancestor,
/// instead of returning null.
static MediaQueryData? maybeOf(BuildContext context) {
- assert(context != null);
- return context.dependOnInheritedWidgetOfExactType<MediaQuery>()?.data;
+ return _maybeOf(context);
}
- /// Returns textScaleFactor for the nearest MediaQuery ancestor or 1.0, if
- /// no such ancestor exists.
- static double textScaleFactorOf(BuildContext context) {
- return MediaQuery.maybeOf(context)?.textScaleFactor ?? 1.0;
+ static MediaQueryData? _maybeOf(BuildContext context, [_MediaQueryAspect? aspect]) {
+ return InheritedModel.inheritFrom<MediaQuery>(context, aspect: aspect)?.data;
}
+ /// Returns size for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.size] property of the ancestor [MediaQuery] changes.
+ static Size sizeOf(BuildContext context) => _of(context, _MediaQueryAspect.size).size;
+
+ /// Returns size for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.size] property of the ancestor [MediaQuery] changes.
+ static Size? maybeSizeOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.size)?.size;
+
+ /// Returns orientation for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.orientation] property of the ancestor [MediaQuery] changes.
+ static Orientation orientationOf(BuildContext context) => _of(context, _MediaQueryAspect.orientation).orientation;
+
+ /// Returns orientation for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.orientation] property of the ancestor [MediaQuery] changes.
+ static Orientation? maybeOrientationOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.orientation)?.orientation;
+
+ /// Returns devicePixelRatio for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.devicePixelRatio] property of the ancestor [MediaQuery] changes.
+ static double devicePixelRatioOf(BuildContext context) => _of(context, _MediaQueryAspect.devicePixelRatio).devicePixelRatio;
+
+ /// Returns devicePixelRatio for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.devicePixelRatio] property of the ancestor [MediaQuery] changes.
+ static double? maybeDevicePixelRatioOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.devicePixelRatio)?.devicePixelRatio;
+
+ /// Returns textScaleFactor for the nearest MediaQuery ancestor or
+ /// 1.0, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.textScaleFactor] property of the ancestor [MediaQuery] changes.
+ static double textScaleFactorOf(BuildContext context) => maybeTextScaleFactorOf(context) ?? 1.0;
+
+ /// Returns textScaleFactor for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.textScaleFactor] property of the ancestor [MediaQuery] changes.
+ static double? maybeTextScaleFactorOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.textScaleFactor)?.textScaleFactor;
+
/// Returns platformBrightness for the nearest MediaQuery ancestor or
/// [Brightness.light], if no such ancestor exists.
///
/// Use of this method will cause the given [context] to rebuild any time that
- /// any property of the ancestor [MediaQuery] changes.
- static Brightness platformBrightnessOf(BuildContext context) {
- return MediaQuery.maybeOf(context)?.platformBrightness ?? Brightness.light;
- }
+ /// the [MediaQueryData.platformBrightness] property of the ancestor
+ /// [MediaQuery] changes.
+ static Brightness platformBrightnessOf(BuildContext context) => maybePlatformBrightnessOf(context) ?? Brightness.light;
+
+ /// Returns platformBrightness for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.platformBrightness] property of the ancestor
+ /// [MediaQuery] changes.
+ static Brightness? maybePlatformBrightnessOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.platformBrightness)?.platformBrightness;
+
+ /// Returns padding for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.padding] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets paddingOf(BuildContext context) => _of(context, _MediaQueryAspect.padding).padding;
+
+ /// Returns viewInsets for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.viewInsets] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets? maybePaddingOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.padding)?.padding;
+
+ /// Returns viewInsets for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.viewInsets] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets viewInsetsOf(BuildContext context) => _of(context, _MediaQueryAspect.viewInsets).viewInsets;
+
+ /// Returns viewInsets for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.viewInsets] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets? maybeViewInsetsOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.viewInsets)?.viewInsets;
+
+ /// Returns systemGestureInsets for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.systemGestureInsets] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets systemGestureInsetsOf(BuildContext context) => _of(context, _MediaQueryAspect.systemGestureInsets).systemGestureInsets;
+
+ /// Returns systemGestureInsets for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.systemGestureInsets] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets? maybeSystemGestureInsetsOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.systemGestureInsets)?.systemGestureInsets;
+
+ /// Returns viewPadding for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.viewPadding] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets viewPaddingOf(BuildContext context) => _of(context, _MediaQueryAspect.viewPadding).viewPadding;
+
+ /// Returns viewPadding for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.viewPadding] property of the ancestor [MediaQuery] changes.
+ static EdgeInsets? maybeViewPaddingOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.viewPadding)?.viewPadding;
+
+ /// Returns alwaysUse for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.devicePixelRatio] property of the ancestor [MediaQuery] changes.
+ static bool alwaysUse24HourFormatOf(BuildContext context) => _of(context, _MediaQueryAspect.alwaysUse24HourFormat).alwaysUse24HourFormat;
+
+ /// Returns alwaysUse24HourFormat for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.alwaysUse24HourFormat] property of the ancestor [MediaQuery] changes.
+ static bool? maybeAlwaysUse24HourFormatOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.alwaysUse24HourFormat)?.alwaysUse24HourFormat;
+
+ /// Returns accessibleNavigationOf for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.accessibleNavigation] property of the ancestor [MediaQuery] changes.
+ static bool accessibleNavigationOf(BuildContext context) => _of(context, _MediaQueryAspect.accessibleNavigation).accessibleNavigation;
+
+ /// Returns accessibleNavigation for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.accessibleNavigation] property of the ancestor [MediaQuery] changes.
+ static bool? maybeAccessibleNavigationOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.accessibleNavigation)?.accessibleNavigation;
+
+ /// Returns invertColorsOf for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.invertColors] property of the ancestor [MediaQuery] changes.
+ static bool invertColorsOf(BuildContext context) => _of(context, _MediaQueryAspect.invertColors).invertColors;
+
+ /// Returns invertColors for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.invertColors] property of the ancestor [MediaQuery] changes.
+ static bool? maybeInvertColorsOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.invertColors)?.invertColors;
/// Returns highContrast for the nearest MediaQuery ancestor or false, if no
/// such ancestor exists.
@@ -947,15 +1235,102 @@
///
/// * [MediaQueryData.highContrast], which indicates the platform's
/// desire to increase contrast.
- static bool highContrastOf(BuildContext context) {
- return MediaQuery.maybeOf(context)?.highContrast ?? false;
- }
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.highContrast] property of the ancestor [MediaQuery] changes.
+ static bool highContrastOf(BuildContext context) => maybeHighContrastOf(context) ?? false;
+
+ /// Returns highContrast for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.highContrast] property of the ancestor [MediaQuery] changes.
+ static bool? maybeHighContrastOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.highContrast)?.highContrast;
+
+ /// Returns disableAnimations for the nearest MediaQuery ancestor or
+ /// [Brightness.light], if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.disableAnimations] property of the ancestor
+ /// [MediaQuery] changes.
+ static bool disableAnimationsOf(BuildContext context) => _of(context, _MediaQueryAspect.disableAnimations).disableAnimations;
+
+ /// Returns disableAnimations for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.disableAnimations] property of the ancestor [MediaQuery] changes.
+ static bool? maybeDisableAnimationsOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.disableAnimations)?.disableAnimations;
+
/// Returns the boldText accessibility setting for the nearest MediaQuery
- /// ancestor, or false if no such ancestor exists.
- static bool boldTextOverride(BuildContext context) {
- return MediaQuery.maybeOf(context)?.boldText ?? false;
- }
+ /// ancestor or false, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.boldText] property of the ancestor [MediaQuery] changes.
+ static bool boldTextOf(BuildContext context) => maybeBoldTextOf(context) ?? false;
+
+ /// Returns the boldText accessibility setting for the nearest MediaQuery
+ /// ancestor or false, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.boldText] property of the ancestor [MediaQuery] changes.
+ ///
+ /// Deprecated in favor of [boldTextOf].
+ @Deprecated(
+ 'Migrate to boldTextOf. '
+ 'This feature was deprecated after v3.5.0-9.0.pre.'
+ )
+ static bool boldTextOverride(BuildContext context) => boldTextOf(context);
+
+ /// Returns the boldText accessibility setting for the nearest MediaQuery
+ /// ancestor or null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.boldText] property of the ancestor [MediaQuery] changes.
+ static bool? maybeBoldTextOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.boldText)?.boldText;
+
+ /// Returns navigationMode for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.navigationMode] property of the ancestor [MediaQuery] changes.
+ static NavigationMode navigationModeOf(BuildContext context) => _of(context, _MediaQueryAspect.navigationMode).navigationMode;
+
+ /// Returns navigationMode for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.navigationMode] property of the ancestor [MediaQuery] changes.
+ static NavigationMode? maybeNavigationModeOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.navigationMode)?.navigationMode;
+
+ /// Returns gestureSettings for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.gestureSettings] property of the ancestor [MediaQuery] changes.
+ static DeviceGestureSettings gestureSettingsOf(BuildContext context) => _of(context, _MediaQueryAspect.gestureSettings).gestureSettings;
+
+ /// Returns gestureSettings for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.gestureSettings] property of the ancestor [MediaQuery] changes.
+ static DeviceGestureSettings? maybeGestureSettingsOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.gestureSettings)?.gestureSettings;
+
+ /// Returns displayFeatures for the nearest MediaQuery ancestor or
+ /// throws an exception, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.displayFeatures] property of the ancestor [MediaQuery] changes.
+ static List<ui.DisplayFeature> displayFeaturesOf(BuildContext context) => _of(context, _MediaQueryAspect.displayFeatures).displayFeatures;
+
+ /// Returns displayFeatures for the nearest MediaQuery ancestor or
+ /// null, if no such ancestor exists.
+ ///
+ /// Use of this method will cause the given [context] to rebuild any time that
+ /// the [MediaQueryData.displayFeatures] property of the ancestor [MediaQuery] changes.
+ static List<ui.DisplayFeature>? maybeDisplayFeaturesOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.displayFeatures)?.displayFeatures;
@override
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
@@ -965,6 +1340,107 @@
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<MediaQueryData>('data', data, showName: false));
}
+
+ @override
+ bool updateShouldNotifyDependent(MediaQuery oldWidget, Set<Object> dependencies) {
+ for (final Object dependency in dependencies) {
+ if (dependency is _MediaQueryAspect) {
+ switch (dependency) {
+ case _MediaQueryAspect.size:
+ if (data.size != oldWidget.data.size) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.orientation:
+ if (data.orientation != oldWidget.data.orientation) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.devicePixelRatio:
+ if (data.devicePixelRatio != oldWidget.data.devicePixelRatio) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.textScaleFactor:
+ if (data.textScaleFactor != oldWidget.data.textScaleFactor) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.platformBrightness:
+ if (data.platformBrightness != oldWidget.data.platformBrightness) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.padding:
+ if (data.padding != oldWidget.data.padding) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.viewInsets:
+ if (data.viewInsets != oldWidget.data.viewInsets) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.systemGestureInsets:
+ if (data.systemGestureInsets != oldWidget.data.systemGestureInsets) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.viewPadding:
+ if (data.viewPadding != oldWidget.data.viewPadding) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.alwaysUse24HourFormat:
+ if (data.alwaysUse24HourFormat != oldWidget.data.alwaysUse24HourFormat) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.accessibleNavigation:
+ if (data.accessibleNavigation != oldWidget.data.accessibleNavigation) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.invertColors:
+ if (data.invertColors != oldWidget.data.invertColors) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.highContrast:
+ if (data.highContrast != oldWidget.data.highContrast) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.disableAnimations:
+ if (data.disableAnimations != oldWidget.data.disableAnimations) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.boldText:
+ if (data.boldText != oldWidget.data.boldText) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.navigationMode:
+ if (data.navigationMode != oldWidget.data.navigationMode) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.gestureSettings:
+ if (data.gestureSettings != oldWidget.data.gestureSettings) {
+ return true;
+ }
+ break;
+ case _MediaQueryAspect.displayFeatures:
+ if (data.displayFeatures != oldWidget.data.displayFeatures) {
+ return true;
+ }
+ break;
+ }
+ }
+ }
+ return false;
+ }
}
/// Describes the navigation mode to be set by a [MediaQuery] widget.
@@ -972,7 +1448,7 @@
/// The different modes indicate the type of navigation to be used in a widget
/// subtree for those widgets sensitive to it.
///
-/// Use `MediaQuery.of(context).navigationMode` to determine the navigation mode
+/// Use `MediaQuery.navigationModeOf(context)` to determine the navigation mode
/// in effect for the given context. Use a [MediaQuery] widget to set the
/// navigation mode for its descendant widgets.
enum NavigationMode {
@@ -995,94 +1471,102 @@
directional,
}
-/// Provides a [MediaQuery] which is built and updated using the latest
-/// [WidgetsBinding.window] values.
-///
-/// Receives `window` updates by listening to [WidgetsBinding].
-///
-/// The standalone widget ensures that it rebuilds **only** [MediaQuery] and
-/// its dependents when `window` changes, instead of rebuilding the entire
-/// widget tree.
-///
-/// It is used by [WidgetsApp] if no other [MediaQuery] is available above it.
-///
-/// See also:
-///
-/// * [MediaQuery], which establishes a subtree in which media queries resolve
-/// to a [MediaQueryData].
-class _MediaQueryFromWindow extends StatefulWidget {
- /// Creates a [_MediaQueryFromWindow] that provides a [MediaQuery] to its
- /// descendants using the `window` to keep [MediaQueryData] up to date.
- ///
- /// The [child] must not be null.
- const _MediaQueryFromWindow({
+class _MediaQueryFromView extends StatefulWidget {
+ const _MediaQueryFromView({
super.key,
+ required this.view,
+ this.ignoreParentData = false,
required this.child,
});
- /// {@macro flutter.widgets.ProxyWidget.child}
+ final FlutterView view;
+ final bool ignoreParentData;
final Widget child;
@override
- State<_MediaQueryFromWindow> createState() => _MediaQueryFromWindowState();
+ State<_MediaQueryFromView> createState() => _MediaQueryFromViewState();
}
-class _MediaQueryFromWindowState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver {
+class _MediaQueryFromViewState extends State<_MediaQueryFromView> with WidgetsBindingObserver {
+ MediaQueryData? _parentData;
+ MediaQueryData? _data;
+
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
- // ACCESSIBILITY
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ _updateParentData();
+ _updateData();
+ assert(_data != null);
+ }
+
+ @override
+ void didUpdateWidget(_MediaQueryFromView oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.ignoreParentData != oldWidget.ignoreParentData) {
+ _updateParentData();
+ }
+ if (_data == null || oldWidget.view != widget.view) {
+ _updateData();
+ }
+ assert(_data != null);
+ }
+
+ void _updateParentData() {
+ _parentData = widget.ignoreParentData ? null : MediaQuery.maybeOf(context);
+ _data = null; // _updateData must be called again after changing parent data.
+ }
+
+ void _updateData() {
+ final MediaQueryData newData = MediaQueryData.fromView(widget.view, platformData: _parentData);
+ if (newData != _data) {
+ setState(() {
+ _data = newData;
+ });
+ }
+ }
@override
void didChangeAccessibilityFeatures() {
- setState(() {
- // The properties of window have changed. We use them in our build
- // function, so we need setState(), but we don't cache anything locally.
- });
+ // If we have a parent, it dictates our accessibility features. If we don't
+ // have a parent, we get our accessibility features straight from the
+ // PlatformDispatcher and need to update our data in response to the
+ // PlatformDispatcher changing its accessibility features setting.
+ if (_parentData == null) {
+ _updateData();
+ }
}
- // METRICS
-
@override
void didChangeMetrics() {
- setState(() {
- // The properties of window have changed. We use them in our build
- // function, so we need setState(), but we don't cache anything locally.
- });
+ _updateData();
}
@override
void didChangeTextScaleFactor() {
- setState(() {
- // The textScaleFactor property of window has changed. We reference
- // window in our build function, so we need to call setState(), but
- // we don't need to cache anything locally.
- });
+ // If we have a parent, it dictates our text scale factor. If we don't have
+ // a parent, we get our text scale factor from the PlatformDispatcher and
+ // need to update our data in response to the PlatformDispatcher changing
+ // its text scale factor setting.
+ if (_parentData == null) {
+ _updateData();
+ }
}
- // RENDERING
@override
void didChangePlatformBrightness() {
- setState(() {
- // The platformBrightness property of window has changed. We reference
- // window in our build function, so we need to call setState(), but
- // we don't need to cache anything locally.
- });
- }
-
- @override
- Widget build(BuildContext context) {
- MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
- if (!kReleaseMode) {
- data = data.copyWith(platformBrightness: debugBrightnessOverride);
+ // If we have a parent, it dictates our platform brightness. If we don't
+ // have a parent, we get our platform brightness from the PlatformDispatcher
+ // and need to update our data in response to the PlatformDispatcher
+ // changing its platform brightness setting.
+ if (_parentData == null) {
+ _updateData();
}
- return MediaQuery(
- data: data,
- child: widget.child,
- );
}
@override
@@ -1090,4 +1574,18 @@
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
+
+ @override
+ Widget build(BuildContext context) {
+ MediaQueryData effectiveData = _data!;
+ // If we get our platformBrightness from the PlatformDispatcher (i.e. we have no parentData) replace it
+ // with the debugBrightnessOverride in non-release mode.
+ if (!kReleaseMode && _parentData == null && effectiveData.platformBrightness != debugBrightnessOverride) {
+ effectiveData = effectiveData.copyWith(platformBrightness: debugBrightnessOverride);
+ }
+ return MediaQuery(
+ data: effectiveData,
+ child: widget.child,
+ );
+ }
}
diff --git a/framework/lib/src/widgets/modal_barrier.dart b/framework/lib/src/widgets/modal_barrier.dart
index 1618a94..9a15b11 100644
--- a/framework/lib/src/widgets/modal_barrier.dart
+++ b/framework/lib/src/widgets/modal_barrier.dart
@@ -14,6 +14,102 @@
import 'navigator.dart';
import 'transitions.dart';
+/// A widget that modifies the size of the [SemanticsNode.rect] created by its
+/// child widget.
+///
+/// It clips the focus in potentially four directions based on the
+/// specified [EdgeInsets].
+///
+/// The size of the accessibility focus is adjusted based on value changes
+/// inside the given [ValueNotifier].
+///
+/// See also:
+///
+/// * [ModalBarrier], which utilizes this widget to adjust the barrier focus
+/// size based on the size of the content layer rendered on top of it.
+class _SemanticsClipper extends SingleChildRenderObjectWidget{
+ /// creates a [SemanticsClipper] that updates the size of the
+ /// [SemanticsNode.rect] of its child based on the value inside the provided
+ /// [ValueNotifier], or a default value of [EdgeInsets.zero].
+ const _SemanticsClipper({
+ super.child,
+ required this.clipDetailsNotifier,
+ });
+
+ /// The [ValueNotifier] whose value determines how the child's
+ /// [SemanticsNode.rect] should be clipped in four directions.
+ final ValueNotifier<EdgeInsets> clipDetailsNotifier;
+
+ @override
+ _RenderSemanticsClipper createRenderObject(BuildContext context) {
+ return _RenderSemanticsClipper(clipDetailsNotifier: clipDetailsNotifier,);
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, _RenderSemanticsClipper renderObject) {
+ renderObject.clipDetailsNotifier = clipDetailsNotifier;
+ }
+}
+/// 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
+ /// based on the value inside provided [ValueNotifier].
+ _RenderSemanticsClipper({
+ required ValueNotifier<EdgeInsets> clipDetailsNotifier,
+ RenderBox? child,
+ }) : _clipDetailsNotifier = clipDetailsNotifier,
+ super(child);
+
+ ValueNotifier<EdgeInsets> _clipDetailsNotifier;
+
+ /// The getter and setter retrieves / updates the [ValueNotifier] associated
+ /// with this clipper.
+ ValueNotifier<EdgeInsets> get clipDetailsNotifier => _clipDetailsNotifier;
+ set clipDetailsNotifier (ValueNotifier<EdgeInsets> newNotifier) {
+ if (_clipDetailsNotifier == newNotifier) {
+ return;
+ }
+ if(attached) {
+ _clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate);
+ }
+ _clipDetailsNotifier = newNotifier;
+ _clipDetailsNotifier.addListener(markNeedsSemanticsUpdate);
+ markNeedsSemanticsUpdate();
+ }
+
+ @override
+ Rect get semanticBounds {
+ final EdgeInsets clipDetails = _clipDetailsNotifier.value;
+ final Rect originalRect = super.semanticBounds;
+ final Rect clippedRect = Rect.fromLTRB(
+ originalRect.left + clipDetails.left,
+ originalRect.top + clipDetails.top,
+ originalRect.right - clipDetails.right,
+ originalRect.bottom - clipDetails.bottom,
+ );
+ return clippedRect;
+ }
+
+ @override
+ void attach(PipelineOwner owner) {
+ super.attach(owner);
+ clipDetailsNotifier.addListener(markNeedsSemanticsUpdate);
+ }
+
+ @override
+ void detach() {
+ clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate);
+ super.detach();
+ }
+
+ @override
+ void describeSemanticsConfiguration(SemanticsConfiguration config) {
+ super.describeSemanticsConfiguration(config);
+ config.isSemanticBoundary = true;
+ }
+}
+
/// A widget that prevents the user from interacting with widgets behind itself.
///
/// The modal barrier is the scrim that is rendered behind each route, which
@@ -37,6 +133,8 @@
this.onDismiss,
this.semanticsLabel,
this.barrierSemanticsDismissible = true,
+ this.clipDetailsNotifier,
+ this.semanticsOnTapHint,
});
/// If non-null, fill the barrier with this color.
@@ -91,23 +189,36 @@
/// [ModalBarrier] built by [ModalRoute] pages.
final String? semanticsLabel;
+ /// {@template flutter.widgets.ModalBarrier.clipDetailsNotifier}
+ /// Contains a value of type [EdgeInsets] that specifies how the
+ /// [SemanticsNode.rect] of the widget should be clipped.
+ ///
+ /// See also:
+ ///
+ /// * [_SemanticsClipper], which utilizes the value inside to update the
+ /// [SemanticsNode.rect] for its child.
+ /// {@endtemplate}
+ final ValueNotifier<EdgeInsets>? clipDetailsNotifier;
+
+ /// {@macro flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
+ final String? semanticsOnTapHint;
+
@override
Widget build(BuildContext context) {
assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context));
final bool platformSupportsDismissingBarrier;
switch (defaultTargetPlatform) {
- case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
platformSupportsDismissingBarrier = false;
break;
+ case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.macOS:
platformSupportsDismissingBarrier = true;
break;
}
- assert(platformSupportsDismissingBarrier != null);
final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier;
final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
@@ -123,27 +234,42 @@
}
}
+ Widget barrier = Semantics(
+ onTapHint: semanticsOnTapHint,
+ onTap: semanticsDismissible && semanticsLabel != null ? handleDismiss : null,
+ onDismiss: semanticsDismissible && semanticsLabel != null ? handleDismiss : null,
+ label: semanticsDismissible ? semanticsLabel : null,
+ textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.basic,
+ child: ConstrainedBox(
+ constraints: const BoxConstraints.expand(),
+ child: color == null ? null : ColoredBox(
+ color: color!,
+ ),
+ ),
+ ),
+ );
+
+ // Developers can set [dismissible: true] and [barrierSemanticsDismissible: true]
+ // to allow assistive technology users to dismiss a modal BottomSheet by
+ // tapping on the Scrim focus.
+ // On iOS, some modal barriers are not dismissible in accessibility mode.
+ final bool excluding = !semanticsDismissible || !modalBarrierSemanticsDismissible;
+
+ if (!excluding && clipDetailsNotifier != null) {
+ barrier = _SemanticsClipper(
+ clipDetailsNotifier: clipDetailsNotifier!,
+ child: barrier,
+ );
+ }
+
return BlockSemantics(
child: ExcludeSemantics(
- // On Android, the back button is used to dismiss a modal. On iOS, some
- // modal barriers are not dismissible in accessibility mode.
- excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
+ excluding: excluding,
child: _ModalBarrierGestureDetector(
onDismiss: handleDismiss,
- child: Semantics(
- label: semanticsDismissible ? semanticsLabel : null,
- onDismiss: semanticsDismissible ? handleDismiss : null,
- textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
- child: MouseRegion(
- cursor: SystemMouseCursors.basic,
- child: ConstrainedBox(
- constraints: const BoxConstraints.expand(),
- child: color == null ? null : ColoredBox(
- color: color!,
- ),
- ),
- ),
- ),
+ child: barrier,
),
),
);
@@ -175,6 +301,8 @@
this.semanticsLabel,
this.barrierSemanticsDismissible,
this.onDismiss,
+ this.clipDetailsNotifier,
+ this.semanticsOnTapHint,
}) : super(listenable: color);
/// If non-null, fill the barrier with this color.
@@ -214,6 +342,19 @@
/// {@macro flutter.widgets.ModalBarrier.onDismiss}
final VoidCallback? onDismiss;
+ /// {@macro flutter.widgets.ModalBarrier.clipDetailsNotifier}
+ final ValueNotifier<EdgeInsets>? clipDetailsNotifier;
+
+ /// This hint text instructs users what they are able to do when they tap on
+ /// the [ModalBarrier]
+ ///
+ /// E.g. If the hint text is 'close bottom sheet", it will be announced as
+ /// "Double tap to close bottom sheet".
+ ///
+ /// If this value is null, the default onTapHint will be applied, resulting
+ /// in the announcement of 'Double tap to activate'.
+ final String? semanticsOnTapHint;
+
@override
Widget build(BuildContext context) {
return ModalBarrier(
@@ -222,6 +363,8 @@
semanticsLabel: semanticsLabel,
barrierSemanticsDismissible: barrierSemanticsDismissible,
onDismiss: onDismiss,
+ clipDetailsNotifier: clipDetailsNotifier,
+ semanticsOnTapHint: semanticsOnTapHint,
);
}
}
@@ -266,17 +409,6 @@
String get debugDescription => 'any tap';
}
-class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
- const _ModalBarrierSemanticsDelegate({this.onDismiss});
-
- final VoidCallback? onDismiss;
-
- @override
- void assignSemantics(RenderSemanticsGestureHandler renderObject) {
- renderObject.onTap = onDismiss;
- }
-}
-
class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> {
const _AnyTapGestureRecognizerFactory({this.onAnyTapUp});
@@ -297,8 +429,7 @@
const _ModalBarrierGestureDetector({
required this.child,
required this.onDismiss,
- }) : assert(child != null),
- assert(onDismiss != null);
+ });
/// The widget below this widget in the tree.
/// See [RawGestureDetector.child].
@@ -317,7 +448,6 @@
return RawGestureDetector(
gestures: gestures,
behavior: HitTestBehavior.opaque,
- semantics: _ModalBarrierSemanticsDelegate(onDismiss: onDismiss),
child: child,
);
}
diff --git a/framework/lib/src/widgets/navigation_toolbar.dart b/framework/lib/src/widgets/navigation_toolbar.dart
index c67a789..c2024a0 100644
--- a/framework/lib/src/widgets/navigation_toolbar.dart
+++ b/framework/lib/src/widgets/navigation_toolbar.dart
@@ -30,8 +30,7 @@
this.trailing,
this.centerMiddle = true,
this.middleSpacing = kMiddleSpacing,
- }) : assert(centerMiddle != null),
- assert(middleSpacing != null);
+ });
/// The default spacing around the [middle] widget in dp.
static const double kMiddleSpacing = 16.0;
@@ -85,8 +84,7 @@
required this.centerMiddle,
required this.middleSpacing,
required this.textDirection,
- }) : assert(middleSpacing != null),
- assert(textDirection != null);
+ });
// If false the middle widget should be start-justified within the space
// between the leading and trailing widgets.
diff --git a/framework/lib/src/widgets/navigator.dart b/framework/lib/src/widgets/navigator.dart
index a81d7ae..d5e0b38 100644
--- a/framework/lib/src/widgets/navigator.dart
+++ b/framework/lib/src/widgets/navigator.dart
@@ -55,8 +55,9 @@
///
/// Used by the restorable methods of the [Navigator] that add anonymous routes
/// (e.g. [NavigatorState.restorablePush]). For this use case, the
-/// [RestorableRouteBuilder] must be static function as the [Navigator] will
-/// call it again during state restoration to re-create the route.
+/// [RestorableRouteBuilder] must be static function annotated with
+/// `@pragma('vm:entry-point')`. The [Navigator] will call it again during
+/// state restoration to re-create the route.
typedef RestorableRouteBuilder<T> = Route<T> Function(BuildContext context, Object? arguments);
/// Signature for the [Navigator.popUntil] predicate argument.
@@ -170,7 +171,6 @@
final ValueNotifier<String?> _restorationScopeId = ValueNotifier<String?>(null);
void _updateSettings(RouteSettings newSettings) {
- assert(newSettings != null);
if (_settings != newSettings) {
_settings = newSettings;
changedInternalState();
@@ -611,15 +611,15 @@
/// The navigator that the observer is observing, if any.
NavigatorState? get navigator => _navigators[this];
- /// Expando mapping instances of NavigatorObserver to their associated
- /// NavigatorState (or `null`, if there is no associated NavigatorState). The
- /// reason we don't simply use a private instance field of type
- /// `NavigatorState?` is because as part of implementing
- /// https://github.com/dart-lang/language/issues/2020, it will soon become a
- /// runtime error to invoke a private member that is mocked in another
- /// library. By using an expando rather than an instance field, we ensure
- /// that a mocked NavigatorObserver can still properly keep track of its
- /// associated NavigatorState.
+ // Expando mapping instances of NavigatorObserver to their associated
+ // NavigatorState (or `null`, if there is no associated NavigatorState). The
+ // reason we don't use a private instance field of type
+ // `NavigatorState?` is because as part of implementing
+ // https://github.com/dart-lang/language/issues/2020, it will soon become a
+ // runtime error to invoke a private member that is mocked in another
+ // library. By using an expando rather than an instance field, we ensure
+ // that a mocked NavigatorObserver can still properly keep track of its
+ // associated NavigatorState.
static final Expando<NavigatorState> _navigators = Expando<NavigatorState>();
/// The [Navigator] pushed `route`.
@@ -676,7 +676,7 @@
super.key,
required HeroController this.controller,
required super.child,
- }) : assert(controller != null);
+ });
/// Creates a widget to prevent the subtree from receiving the hero controller
/// above.
@@ -909,7 +909,6 @@
int indexOfNextRouteInNewHistory = 0;
for (final _RouteEntry routeEntry in resultsToVerify.cast<_RouteEntry>()) {
- assert(routeEntry != null);
assert(!routeEntry.isWaitingForEnteringDecision && !routeEntry.isWaitingForExitingDecision);
if (
indexOfNextRouteInNewHistory >= newPageRouteHistory.length ||
@@ -1093,7 +1092,7 @@
}
}
-/// The default value of [MediaQueryData.routeTraversalEdgeBehavior].
+/// The default value of [Navigator.routeTraversalEdgeBehavior].
///
/// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = kIsWeb
@@ -1410,12 +1409,7 @@
this.requestFocus = true,
this.restorationScopeId,
this.routeTraversalEdgeBehavior = kDefaultRouteTraversalEdgeBehavior,
- }) : assert(pages != null),
- assert(onGenerateInitialRoutes != null),
- assert(transitionDelegate != null),
- assert(observers != null),
- assert(routeTraversalEdgeBehavior != null),
- assert(reportsRouteUpdateToEngine != null);
+ });
/// The list of pages with which to populate the history.
///
@@ -2108,9 +2102,10 @@
/// {@macro flutter.widgets.navigator.push}
///
/// {@template flutter.widgets.Navigator.restorablePush}
- /// The method takes a _static_ [RestorableRouteBuilder] as argument, which
- /// must instantiate and return a new [Route] object that will be added to
- /// the navigator. The provided `arguments` object is passed to the
+ /// The method takes a [RestorableRouteBuilder] as argument, which must be a
+ /// _static_ function annotated with `@pragma('vm:entry-point')`. It must
+ /// instantiate and return a new [Route] object that will be added to the
+ /// navigator. The provided `arguments` object is passed to the
/// `routeBuilder`. The navigator calls the static `routeBuilder` function
/// again during state restoration to re-create the route object.
///
@@ -2841,9 +2836,7 @@
required _RouteLifecycle initialState,
required this.pageBased,
this.restorationInformation,
- }) : assert(route != null),
- assert(!pageBased || route.settings is Page),
- assert(initialState != null),
+ }) : assert(!pageBased || route.settings is Page),
assert(
initialState == _RouteLifecycle.staging ||
initialState == _RouteLifecycle.add ||
@@ -2893,7 +2886,6 @@
void handleAdd({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
assert(currentState == _RouteLifecycle.add);
- assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == null);
route._navigator = navigator;
@@ -2907,7 +2899,6 @@
void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route<dynamic>? previous, required Route<dynamic>? previousPresent }) {
assert(currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace || currentState == _RouteLifecycle.replace);
- assert(navigator != null);
assert(navigator._debugLocked);
assert(
route._navigator == null,
@@ -2964,7 +2955,6 @@
/// Returns true if the route is popped; otherwise, returns false if the route
/// refuses to be popped.
bool handlePop({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
- assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == navigator);
currentState = _RouteLifecycle.popping;
@@ -2991,7 +2981,6 @@
}
void handleRemoval({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
- assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == navigator);
currentState = _RouteLifecycle.removing;
@@ -3118,7 +3107,6 @@
// already announced this change by calling didPopNext.
return !(
nextRoute == null &&
- lastAnnouncedPoppedNextRoute != null &&
lastAnnouncedPoppedNextRoute == lastAnnouncedNextRoute
);
}
@@ -3685,7 +3673,7 @@
_RouteEntry? previousOldPageRouteEntry;
while (oldEntriesBottom <= oldEntriesTop) {
final _RouteEntry oldEntry = _history[oldEntriesBottom];
- assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
+ assert(oldEntry.currentState != _RouteLifecycle.disposed);
// Records pageless route. The bottom most pageless routes will be
// stored in key = null.
if (!oldEntry.pageBased) {
@@ -3711,15 +3699,14 @@
oldEntriesBottom += 1;
}
- int pagelessRoutesToSkip = 0;
+ final List<_RouteEntry> unattachedPagelessRoutes=<_RouteEntry>[];
// Scans the top of the list until we found a page-based route that cannot be
// updated.
while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
final _RouteEntry oldEntry = _history[oldEntriesTop];
- assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
+ assert(oldEntry.currentState != _RouteLifecycle.disposed);
if (!oldEntry.pageBased) {
- // This route might need to be skipped if we can not find a page above.
- pagelessRoutesToSkip += 1;
+ unattachedPagelessRoutes.add(oldEntry);
oldEntriesTop -= 1;
continue;
}
@@ -3727,14 +3714,22 @@
if (!oldEntry.canUpdateFrom(newPage)) {
break;
}
- // We found the page for all the consecutive pageless routes below. Those
- // pageless routes do not need to be skipped.
- pagelessRoutesToSkip = 0;
+
+ // We found the page for all the consecutive pageless routes below. Attach these
+ // pageless routes to the page.
+ if(unattachedPagelessRoutes.isNotEmpty) {
+ pageRouteToPagelessRoutes.putIfAbsent(
+ oldEntry,
+ () => List<_RouteEntry>.from(unattachedPagelessRoutes),
+ );
+ unattachedPagelessRoutes.clear();
+ }
+
oldEntriesTop -= 1;
newPagesTop -= 1;
}
// Reverts the pageless routes that cannot be updated.
- oldEntriesTop += pagelessRoutesToSkip;
+ oldEntriesTop += unattachedPagelessRoutes.length;
// Scans middle of the old entries and records the page key to old entry map.
int oldEntriesBottomToScan = oldEntriesBottom;
@@ -3746,7 +3741,6 @@
final _RouteEntry oldEntry = _history[oldEntriesBottomToScan];
oldEntriesBottomToScan += 1;
assert(
- oldEntry != null &&
oldEntry.currentState != _RouteLifecycle.disposed,
);
// Pageless routes will be recorded when we update the middle of the old
@@ -3857,7 +3851,7 @@
// Updates the top of the list.
while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
final _RouteEntry oldEntry = _history[oldEntriesBottom];
- assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
+ assert(oldEntry.currentState != _RouteLifecycle.disposed);
if (!oldEntry.pageBased) {
assert(previousOldPageRouteEntry != null);
final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes
@@ -4138,7 +4132,6 @@
Route<T?>? _routeNamed<T>(String name, { required Object? arguments, bool allowNull = false }) {
assert(!_debugLocked);
- assert(name != null);
if (allowNull && widget.onGenerateRoute == null) {
return null;
}
@@ -4246,7 +4239,6 @@
String routeName, {
Object? arguments,
}) {
- assert(routeName != null);
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.named(
name: routeName,
@@ -4319,7 +4311,6 @@
TO? result,
Object? arguments,
}) {
- assert(routeName != null);
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.named(
name: routeName,
@@ -4455,7 +4446,6 @@
RoutePredicate predicate, {
Object? arguments,
}) {
- assert(newRouteName != null);
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.named(
name: newRouteName,
@@ -4524,7 +4514,6 @@
/// {@end-tool}
@optionalTypeArgs
String restorablePush<T extends Object?>(RestorableRouteBuilder<T> routeBuilder, {Object? arguments}) {
- assert(routeBuilder != null);
assert(_debugIsStaticCallback(routeBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.anonymous(
@@ -4542,7 +4531,6 @@
_debugLocked = true;
return true;
}());
- assert(entry.route != null);
assert(entry.route._navigator == null);
assert(entry.currentState == _RouteLifecycle.push);
_history.add(entry);
@@ -4622,7 +4610,6 @@
/// be restored during state restoration.
@optionalTypeArgs
Future<T?> pushReplacement<T extends Object?, TO extends Object?>(Route<T> newRoute, { TO? result }) {
- assert(newRoute != null);
assert(newRoute._navigator == null);
_pushReplacementEntry(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.pushReplace), result);
return newRoute.popped;
@@ -4647,7 +4634,6 @@
/// {@end-tool}
@optionalTypeArgs
String restorablePushReplacement<T extends Object?, TO extends Object?>(RestorableRouteBuilder<T> routeBuilder, { TO? result, Object? arguments }) {
- assert(routeBuilder != null);
assert(_debugIsStaticCallback(routeBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.anonymous(
@@ -4665,7 +4651,6 @@
_debugLocked = true;
return true;
}());
- assert(entry.route != null);
assert(entry.route._navigator == null);
assert(_history.isNotEmpty);
assert(_history.any(_RouteEntry.isPresentPredicate), 'Navigator has no active routes to replace.');
@@ -4708,7 +4693,6 @@
/// restored during state restoration.
@optionalTypeArgs
Future<T?> pushAndRemoveUntil<T extends Object?>(Route<T> newRoute, RoutePredicate predicate) {
- assert(newRoute != null);
assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty);
_pushEntryAndRemoveUntil(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.push), predicate);
@@ -4733,7 +4717,6 @@
/// {@end-tool}
@optionalTypeArgs
String restorablePushAndRemoveUntil<T extends Object?>(RestorableRouteBuilder<T> newRouteBuilder, RoutePredicate predicate, {Object? arguments}) {
- assert(newRouteBuilder != null);
assert(_debugIsStaticCallback(newRouteBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.anonymous(
@@ -4751,10 +4734,8 @@
_debugLocked = true;
return true;
}());
- assert(entry.route != null);
assert(entry.route._navigator == null);
assert(entry.route.overlayEntries.isEmpty);
- assert(predicate != null);
assert(entry.currentState == _RouteLifecycle.push);
int index = _history.length - 1;
_history.add(entry);
@@ -4786,9 +4767,7 @@
@optionalTypeArgs
void replace<T extends Object?>({ required Route<dynamic> oldRoute, required Route<T> newRoute }) {
assert(!_debugLocked);
- assert(oldRoute != null);
assert(oldRoute._navigator == this);
- assert(newRoute != null);
_replaceEntry(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.replace), oldRoute);
}
@@ -4803,12 +4782,9 @@
/// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
@optionalTypeArgs
String restorableReplace<T extends Object?>({ required Route<dynamic> oldRoute, required RestorableRouteBuilder<T> newRouteBuilder, Object? arguments }) {
- assert(oldRoute != null);
assert(oldRoute._navigator == this);
- assert(newRouteBuilder != null);
assert(_debugIsStaticCallback(newRouteBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
- assert(oldRoute != null);
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: newRouteBuilder,
arguments: arguments,
@@ -4858,9 +4834,7 @@
/// be restored during state restoration.
@optionalTypeArgs
void replaceRouteBelow<T extends Object?>({ required Route<dynamic> anchorRoute, required Route<T> newRoute }) {
- assert(newRoute != null);
assert(newRoute._navigator == null);
- assert(anchorRoute != null);
assert(anchorRoute._navigator == this);
_replaceEntryBelow(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.replace), anchorRoute);
}
@@ -4877,12 +4851,9 @@
/// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
@optionalTypeArgs
String restorableReplaceRouteBelow<T extends Object?>({ required Route<dynamic> anchorRoute, required RestorableRouteBuilder<T> newRouteBuilder, Object? arguments }) {
- assert(anchorRoute != null);
assert(anchorRoute._navigator == this);
- assert(newRouteBuilder != null);
assert(_debugIsStaticCallback(newRouteBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
- assert(anchorRoute != null);
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: newRouteBuilder,
arguments: arguments,
@@ -4960,7 +4931,6 @@
}
assert(lastEntry.route._navigator == this);
final RoutePopDisposition disposition = await lastEntry.route.willPop(); // this is asynchronous
- assert(disposition != null);
if (!mounted) {
// Forget about this pop, we were disposed in the meantime.
return true;
@@ -5072,7 +5042,6 @@
///
/// {@macro flutter.widgets.navigator.removeRoute}
void removeRoute(Route<dynamic> route) {
- assert(route != null);
assert(!_debugLocked);
assert(() {
_debugLocked = true;
@@ -5081,7 +5050,6 @@
assert(route._navigator == this);
final bool wasCurrent = route.isCurrent;
final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route));
- assert(entry != null);
entry.remove();
_flushHistoryUpdates(rearrangeOverlay: false);
assert(() {
@@ -5108,7 +5076,6 @@
_debugLocked = true;
return true;
}());
- assert(anchorRoute != null);
assert(anchorRoute._navigator == this);
final int anchorIndex = _history.indexWhere(_RouteEntry.isRoutePredicate(anchorRoute));
assert(anchorIndex >= 0, 'This Navigator does not contain the specified anchorRoute.');
@@ -5168,7 +5135,6 @@
@optionalTypeArgs
Route<T>? _getRouteById<T>(String id) {
- assert(id != null);
return _history.cast<_RouteEntry?>().firstWhere(
(_RouteEntry? entry) => entry!.restorationId == id,
orElse: () => null,
@@ -5206,7 +5172,6 @@
_history.length - 1,
_RouteEntry.willBePresentPredicate,
);
- assert(routeIndex != null);
final Route<dynamic> route = _history[routeIndex].route;
Route<dynamic>? previousRoute;
if (!route.willHandlePopInternally && routeIndex > 0) {
@@ -5304,7 +5269,7 @@
}
abstract class _RestorationInformation {
- _RestorationInformation(this.type) : assert(type != null);
+ _RestorationInformation(this.type);
factory _RestorationInformation.named({
required String name,
required Object? arguments,
@@ -5317,11 +5282,9 @@
}) = _AnonymousRestorationInformation;
factory _RestorationInformation.fromSerializableData(Object data) {
- assert(data != null);
final List<Object?> casted = data as List<Object?>;
assert(casted.isNotEmpty);
final _RouteRestorationType type = _RouteRestorationType.values[casted[0]! as int];
- assert(type != null);
switch (type) {
case _RouteRestorationType.named:
return _NamedRestorationInformation.fromSerializableData(casted.sublist(1));
@@ -5350,10 +5313,7 @@
Route<dynamic> createRoute(NavigatorState navigator);
_RouteEntry toRouteEntry(NavigatorState navigator, {_RouteLifecycle initialState = _RouteLifecycle.add}) {
- assert(navigator != null);
- assert(initialState != null);
final Route<Object?> route = createRoute(navigator);
- assert(route != null);
return _RouteEntry(
route,
pageBased: false,
@@ -5368,7 +5328,7 @@
required this.name,
required this.arguments,
required this.restorationScopeId,
- }) : assert(name != null), super(_RouteRestorationType.named);
+ }) : super(_RouteRestorationType.named);
factory _NamedRestorationInformation.fromSerializableData(List<Object?> data) {
assert(data.length >= 2);
@@ -5397,7 +5357,6 @@
@override
Route<dynamic> createRoute(NavigatorState navigator) {
final Route<dynamic> route = navigator._routeNamed<dynamic>(name, arguments: arguments)!;
- assert(route != null);
return route;
}
}
@@ -5407,7 +5366,7 @@
required this.routeBuilder,
required this.arguments,
required this.restorationScopeId,
- }) : assert(routeBuilder != null), super(_RouteRestorationType.anonymous);
+ }) : super(_RouteRestorationType.anonymous);
factory _AnonymousRestorationInformation.fromSerializableData(List<Object?> data) {
assert(data.length > 1);
@@ -5444,7 +5403,6 @@
@override
Route<dynamic> createRoute(NavigatorState navigator) {
final Route<dynamic> result = routeBuilder(navigator.context, arguments);
- assert(result != null);
return result;
}
}
@@ -5526,9 +5484,8 @@
Set<String?> pagesToRemove,
) {
assert(page == null || page.pageBased);
- assert(pageToRoutes != null);
assert(!pageToRoutes.containsKey(page?.restorationId));
- if (routes != null && routes.isNotEmpty) {
+ if (routes.isNotEmpty) {
assert(page == null || page.restorationId != null);
final String? restorationId = page?.restorationId;
pageToRoutes[restorationId] = routes;
@@ -5672,7 +5629,7 @@
this.navigatorFinder = _defaultNavigatorFinder,
required this.onPresent,
this.onComplete,
- }) : assert(onPresent != null), assert(navigatorFinder != null);
+ });
/// A callback that given the [BuildContext] of the [State] object to which
/// this property is registered returns the [NavigatorState] of the navigator
@@ -5707,7 +5664,6 @@
assert(!isPresent);
assert(isRegistered);
final String routeId = onPresent(_navigator, arguments);
- assert(routeId != null);
_hookOntoRouteFuture(routeId);
notifyListeners();
}
@@ -5760,12 +5716,10 @@
NavigatorState get _navigator {
final NavigatorState navigator = navigatorFinder(state.context);
- assert(navigator != null);
return navigator;
}
void _hookOntoRouteFuture(String id) {
- assert(id != null);
_route = _navigator._getRouteById<T>(id);
assert(_route != null);
route!.restorationScopeId.addListener(notifyListeners);
diff --git a/framework/lib/src/widgets/nested_scroll_view.dart b/framework/lib/src/widgets/nested_scroll_view.dart
index 5c6d988..01eab7b 100644
--- a/framework/lib/src/widgets/nested_scroll_view.dart
+++ b/framework/lib/src/widgets/nested_scroll_view.dart
@@ -190,12 +190,7 @@
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
- }) : assert(scrollDirection != null),
- assert(reverse != null),
- assert(headerSliverBuilder != null),
- assert(body != null),
- assert(floatHeaderSlivers != null),
- assert(clipBehavior != null);
+ });
/// An object that can be used to control the position to which the outer
/// scroll view is scrolled.
@@ -510,8 +505,7 @@
const _InheritedNestedScrollView({
required this.state,
required super.child,
- }) : assert(state != null),
- assert(child != null);
+ });
final NestedScrollViewState state;
@@ -526,6 +520,7 @@
required super.pixels,
required super.viewportDimension,
required super.axisDirection,
+ required super.devicePixelRatio,
required this.minRange,
required this.maxRange,
required this.correctionOffset,
@@ -538,6 +533,7 @@
double? pixels,
double? viewportDimension,
AxisDirection? axisDirection,
+ double? devicePixelRatio,
double? minRange,
double? maxRange,
double? correctionOffset,
@@ -548,6 +544,7 @@
pixels: pixels ?? (hasPixels ? this.pixels : null),
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
+ devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
minRange: minRange ?? this.minRange,
maxRange: maxRange ?? this.maxRange,
correctionOffset: correctionOffset ?? this.correctionOffset,
@@ -630,7 +627,6 @@
ScrollDirection _userScrollDirection = ScrollDirection.idle;
void updateUserScrollDirection(ScrollDirection value) {
- assert(value != null);
if (userScrollDirection == value) {
return;
}
@@ -749,7 +745,6 @@
}
_NestedScrollMetrics _getMetrics(_NestedScrollPosition innerPosition, double velocity) {
- assert(innerPosition != null);
double pixels, minRange, maxRange, correctionOffset;
double extra = 0.0;
if (innerPosition.pixels == innerPosition.minScrollExtent) {
@@ -815,6 +810,7 @@
minRange: minRange,
maxRange: maxRange,
correctionOffset: correctionOffset,
+ devicePixelRatio: _outerPosition!.devicePixelRatio,
);
}
@@ -903,7 +899,10 @@
// If an update is made to pointer scrolling here, consider if the same
// (or similar) change should be made in
// ScrollPositionWithSingleContext.pointerScroll.
- assert(delta != 0.0);
+ if (delta == 0.0) {
+ goBallistic(0.0);
+ return;
+ }
goIdle();
updateUserScrollDirection(
@@ -1180,7 +1179,7 @@
super.debugLabel,
required this.coordinator,
}) {
- if (!hasPixels && initialPixels != null) {
+ if (!hasPixels) {
correctPixels(initialPixels);
}
if (activity == null) {
@@ -1369,7 +1368,6 @@
if (simulation == null) {
return IdleScrollActivity(this);
}
- assert(mode != null);
switch (mode) {
case _NestedBallisticScrollActivityMode.outer:
assert(metrics != null);
@@ -1670,8 +1668,7 @@
super.key,
required this.handle,
Widget? sliver,
- }) : assert(handle != null),
- super(child: sliver);
+ }) : super(child: sliver);
/// The object in which the absorbed overlap is recorded.
///
@@ -1715,8 +1712,7 @@
RenderSliverOverlapAbsorber({
required SliverOverlapAbsorberHandle handle,
RenderSliver? sliver,
- }) : assert(handle != null),
- _handle = handle {
+ }) : _handle = handle {
child = sliver;
}
@@ -1727,7 +1723,6 @@
SliverOverlapAbsorberHandle get handle => _handle;
SliverOverlapAbsorberHandle _handle;
set handle(SliverOverlapAbsorberHandle value) {
- assert(value != null);
if (handle == value) {
return;
}
@@ -1832,8 +1827,7 @@
super.key,
required this.handle,
Widget? sliver,
- }) : assert(handle != null),
- super(child: sliver);
+ }) : super(child: sliver);
/// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
///
@@ -1873,8 +1867,7 @@
/// The [handle] must not be null.
RenderSliverOverlapInjector({
required SliverOverlapAbsorberHandle handle,
- }) : assert(handle != null),
- _handle = handle;
+ }) : _handle = handle;
double? _currentLayoutExtent;
double? _currentMaxExtent;
@@ -1887,7 +1880,6 @@
SliverOverlapAbsorberHandle get handle => _handle;
SliverOverlapAbsorberHandle _handle;
set handle(SliverOverlapAbsorberHandle value) {
- assert(value != null);
if (handle == value) {
return;
}
@@ -1998,7 +1990,7 @@
super.slivers,
required this.handle,
super.clipBehavior,
- }) : assert(handle != null);
+ });
/// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
final SliverOverlapAbsorberHandle handle;
@@ -2057,15 +2049,13 @@
super.center,
required SliverOverlapAbsorberHandle handle,
super.clipBehavior,
- }) : assert(handle != null),
- _handle = handle;
+ }) : _handle = handle;
/// The object to notify when [markNeedsLayout] is called.
SliverOverlapAbsorberHandle get handle => _handle;
SliverOverlapAbsorberHandle _handle;
/// Setting this will trigger notifications on the new object.
set handle(SliverOverlapAbsorberHandle value) {
- assert(value != null);
if (handle == value) {
return;
}
diff --git a/framework/lib/src/widgets/orientation_builder.dart b/framework/lib/src/widgets/orientation_builder.dart
index 189a621..6289374 100644
--- a/framework/lib/src/widgets/orientation_builder.dart
+++ b/framework/lib/src/widgets/orientation_builder.dart
@@ -31,11 +31,11 @@
const OrientationBuilder({
super.key,
required this.builder,
- }) : assert(builder != null);
+ });
/// Builds the widgets below this widget given this widget's orientation.
///
- /// A widget's orientation is simply a factor of its width relative to its
+ /// A widget's orientation is a factor of its width relative to its
/// height. For example, a [Column] widget will have a landscape orientation
/// if its width exceeds its height, even though it displays its children in
/// a vertical array.
diff --git a/framework/lib/src/widgets/overflow_bar.dart b/framework/lib/src/widgets/overflow_bar.dart
index 4b35bb8..dad0549 100644
--- a/framework/lib/src/widgets/overflow_bar.dart
+++ b/framework/lib/src/widgets/overflow_bar.dart
@@ -59,7 +59,7 @@
/// [overflowDirection], and [clipBehavior] parameters must not be
/// null. The [children] argument must not be null and must not contain
/// any null objects.
- OverflowBar({
+ const OverflowBar({
super.key,
this.spacing = 0.0,
this.alignment,
@@ -69,11 +69,7 @@
this.textDirection,
this.clipBehavior = Clip.none,
super.children,
- }) : assert(spacing != null),
- assert(overflowSpacing != null),
- assert(overflowAlignment != null),
- assert(overflowDirection != null),
- assert(clipBehavior != null);
+ });
/// The width of the gap between [children] for the default
/// horizontal layout.
@@ -257,12 +253,7 @@
VerticalDirection overflowDirection = VerticalDirection.down,
required TextDirection textDirection,
Clip clipBehavior = Clip.none,
- }) : assert(spacing != null),
- assert(overflowSpacing != null),
- assert(overflowAlignment != null),
- assert(textDirection != null),
- assert(clipBehavior != null),
- _spacing = spacing,
+ }) : _spacing = spacing,
_alignment = alignment,
_overflowSpacing = overflowSpacing,
_overflowAlignment = overflowAlignment,
@@ -275,7 +266,6 @@
double get spacing => _spacing;
double _spacing;
set spacing (double value) {
- assert(value != null);
if (_spacing == value) {
return;
}
@@ -296,7 +286,6 @@
double get overflowSpacing => _overflowSpacing;
double _overflowSpacing;
set overflowSpacing (double value) {
- assert(value != null);
if (_overflowSpacing == value) {
return;
}
@@ -307,7 +296,6 @@
OverflowBarAlignment get overflowAlignment => _overflowAlignment;
OverflowBarAlignment _overflowAlignment;
set overflowAlignment (OverflowBarAlignment value) {
- assert(value != null);
if (_overflowAlignment == value) {
return;
}
@@ -318,7 +306,6 @@
VerticalDirection get overflowDirection => _overflowDirection;
VerticalDirection _overflowDirection;
set overflowDirection (VerticalDirection value) {
- assert(value != null);
if (_overflowDirection == value) {
return;
}
@@ -339,7 +326,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
- assert(value != null);
if (value == _clipBehavior) {
return;
}
@@ -519,7 +505,6 @@
x = rtl ? 0 : constraints.maxWidth - child.size.width;
break;
}
- assert(x != null);
childParentData.offset = Offset(x, y);
y += child.size.height + overflowSpacing;
child = nextChild();
diff --git a/framework/lib/src/widgets/overlay.dart b/framework/lib/src/widgets/overlay.dart
index b4aa248..d3cd0de 100644
--- a/framework/lib/src/widgets/overlay.dart
+++ b/framework/lib/src/widgets/overlay.dart
@@ -11,6 +11,7 @@
import 'basic.dart';
import 'framework.dart';
+import 'lookup_boundary.dart';
import 'ticker_provider.dart';
// Examples can assume:
@@ -71,10 +72,7 @@
required this.builder,
bool opaque = false,
bool maintainState = false,
- }) : assert(builder != null),
- assert(opaque != null),
- assert(maintainState != null),
- _opaque = opaque,
+ }) : _opaque = opaque,
_maintainState = maintainState;
/// This entry will include the widget built by this builder in the overlay at
@@ -118,7 +116,6 @@
bool _maintainState;
set maintainState(bool value) {
assert(!_disposedByOwner);
- assert(_maintainState != null);
if (_maintainState == value) {
return;
}
@@ -226,10 +223,7 @@
required Key key,
required this.entry,
this.tickerEnabled = true,
- }) : assert(key != null),
- assert(entry != null),
- assert(tickerEnabled != null),
- super(key: key);
+ }) : super(key: key);
final OverlayEntry entry;
final bool tickerEnabled;
@@ -280,7 +274,7 @@
/// The [Overlay] widget uses a custom stack implementation, which is very
/// similar to the [Stack] widget. The main use case of [Overlay] is related to
/// navigation and being able to insert widgets on top of the pages in an app.
-/// To simply display a stack of widgets, consider using [Stack] instead.
+/// For layout purposes unrelated to navigation, consider using [Stack] instead.
///
/// An [Overlay] widget requires a [Directionality] widget to be in scope, so
/// that it can resolve direction-sensitive coordinates of any
@@ -314,8 +308,7 @@
super.key,
this.initialEntries = const <OverlayEntry>[],
this.clipBehavior = Clip.hardEdge,
- }) : assert(initialEntries != null),
- assert(clipBehavior != null);
+ });
/// The entries to include in the overlay initially.
///
@@ -338,7 +331,8 @@
final Clip clipBehavior;
/// The [OverlayState] from the closest instance of [Overlay] that encloses
- /// the given context, and, in debug mode, will throw if one is not found.
+ /// the given context within the closest [LookupBoundary], and, in debug mode,
+ /// will throw if one is not found.
///
/// In debug mode, if the `debugRequiredFor` argument is provided and an
/// overlay isn't found, then this function will throw an exception containing
@@ -372,8 +366,13 @@
final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay);
assert(() {
if (result == null) {
+ final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType<OverlayState>(context);
final List<DiagnosticsNode> information = <DiagnosticsNode>[
- ErrorSummary('No Overlay widget found.'),
+ ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
+ if (hiddenByBoundary)
+ ErrorDescription(
+ 'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
+ ),
ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'),
ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'),
if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
@@ -389,7 +388,7 @@
}
/// The [OverlayState] from the closest instance of [Overlay] that encloses
- /// the given context, if any.
+ /// the given context within the closest [LookupBoundary], if any.
///
/// Typical usage is as follows:
///
@@ -413,8 +412,8 @@
bool rootOverlay = false,
}) {
return rootOverlay
- ? context.findRootAncestorStateOfType<OverlayState>()
- : context.findAncestorStateOfType<OverlayState>();
+ ? LookupBoundary.findRootAncestorStateOfType<OverlayState>(context)
+ : LookupBoundary.findAncestorStateOfType<OverlayState>(context);
}
@override
@@ -638,15 +637,12 @@
///
/// The first [skipCount] children are considered "offstage".
class _Theatre extends MultiChildRenderObjectWidget {
- _Theatre({
+ const _Theatre({
this.skipCount = 0,
this.clipBehavior = Clip.hardEdge,
super.children,
- }) : assert(skipCount != null),
- assert(skipCount >= 0),
- assert(children != null),
- assert(children.length >= skipCount),
- assert(clipBehavior != null);
+ }) : assert(skipCount >= 0),
+ assert(children.length >= skipCount);
final int skipCount;
@@ -699,10 +695,7 @@
required TextDirection textDirection,
int skipCount = 0,
Clip clipBehavior = Clip.hardEdge,
- }) : assert(skipCount != null),
- assert(skipCount >= 0),
- assert(textDirection != null),
- assert(clipBehavior != null),
+ }) : assert(skipCount >= 0),
_textDirection = textDirection,
_skipCount = skipCount,
_clipBehavior = clipBehavior {
@@ -745,7 +738,6 @@
int get skipCount => _skipCount;
int _skipCount;
set skipCount(int value) {
- assert(value != null);
if (_skipCount != value) {
_skipCount = value;
markNeedsLayout();
@@ -758,7 +750,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
diff --git a/framework/lib/src/widgets/overscroll_indicator.dart b/framework/lib/src/widgets/overscroll_indicator.dart
index 84783f7..eae45a3 100644
--- a/framework/lib/src/widgets/overscroll_indicator.dart
+++ b/framework/lib/src/widgets/overscroll_indicator.dart
@@ -90,11 +90,7 @@
required this.color,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.child,
- }) : assert(showLeading != null),
- assert(showTrailing != null),
- assert(axisDirection != null),
- assert(color != null),
- assert(notificationPredicate != null);
+ });
/// Whether to show the overscroll glow on the side with negative scroll
/// offsets.
@@ -248,9 +244,7 @@
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
- assert(notification.dragDetails!.globalPosition != null);
final RenderBox renderer = notification.context!.findRenderObject()! as RenderBox;
- assert(renderer != null);
assert(renderer.hasSize);
final Size size = renderer.size;
final Offset position = renderer.globalToLocal(notification.dragDetails!.globalPosition);
@@ -317,10 +311,7 @@
required TickerProvider vsync,
required Color color,
required Axis axis,
- }) : assert(vsync != null),
- assert(color != null),
- assert(axis != null),
- _color = color,
+ }) : _color = color,
_axis = axis {
_glowController = AnimationController(vsync: vsync)
..addStatusListener(_changePhase);
@@ -358,7 +349,6 @@
Color get color => _color;
Color _color;
set color(Color value) {
- assert(color != null);
if (color == value) {
return;
}
@@ -369,7 +359,6 @@
Axis get axis => _axis;
Axis _axis;
set axis(Axis value) {
- assert(axis != null);
if (axis == value) {
return;
}
@@ -655,9 +644,7 @@
this.notificationPredicate = defaultScrollNotificationPredicate,
this.clipBehavior = Clip.hardEdge,
this.child,
- }) : assert(axisDirection != null),
- assert(notificationPredicate != null),
- assert(clipBehavior != null);
+ });
/// {@macro flutter.overscroll.axisDirection}
final AxisDirection axisDirection;
@@ -774,7 +761,7 @@
@override
Widget build(BuildContext context) {
- final Size size = MediaQuery.of(context).size;
+ final Size size = MediaQuery.sizeOf(context);
double mainAxisSize;
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
diff --git a/framework/lib/src/widgets/page_storage.dart b/framework/lib/src/widgets/page_storage.dart
index e400819..c52bb85 100644
--- a/framework/lib/src/widgets/page_storage.dart
+++ b/framework/lib/src/widgets/page_storage.dart
@@ -27,8 +27,7 @@
@immutable
class _StorageEntryIdentifier {
- const _StorageEntryIdentifier(this.keys)
- : assert(keys != null);
+ const _StorageEntryIdentifier(this.keys);
final List<PageStorageKey<dynamic>> keys;
@@ -164,7 +163,7 @@
super.key,
required this.bucket,
required this.child,
- }) : assert(bucket != null);
+ });
/// The widget below this widget in the tree.
///
diff --git a/framework/lib/src/widgets/page_view.dart b/framework/lib/src/widgets/page_view.dart
index 15dbd4a..1be9d91 100644
--- a/framework/lib/src/widgets/page_view.dart
+++ b/framework/lib/src/widgets/page_view.dart
@@ -68,7 +68,7 @@
/// body: PageView(
/// controller: _pageController,
/// children: <Widget>[
-/// Container(
+/// ColoredBox(
/// color: Colors.red,
/// child: Center(
/// child: ElevatedButton(
@@ -85,7 +85,7 @@
/// ),
/// ),
/// ),
-/// Container(
+/// ColoredBox(
/// color: Colors.blue,
/// child: Center(
/// child: ElevatedButton(
@@ -118,10 +118,7 @@
this.initialPage = 0,
this.keepPage = true,
this.viewportFraction = 1.0,
- }) : assert(initialPage != null),
- assert(keepPage != null),
- assert(viewportFraction != null),
- assert(viewportFraction > 0.0);
+ }) : assert(viewportFraction > 0.0);
/// The page to show when first creating the [PageView].
final int initialPage;
@@ -273,6 +270,7 @@
required super.viewportDimension,
required super.axisDirection,
required this.viewportFraction,
+ required super.devicePixelRatio,
});
@override
@@ -283,6 +281,7 @@
double? viewportDimension,
AxisDirection? axisDirection,
double? viewportFraction,
+ double? devicePixelRatio,
}) {
return PageMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
@@ -291,6 +290,7 @@
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
viewportFraction: viewportFraction ?? this.viewportFraction,
+ devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
);
}
@@ -314,10 +314,7 @@
bool keepPage = true,
double viewportFraction = 1.0,
super.oldPosition,
- }) : assert(initialPage != null),
- assert(keepPage != null),
- assert(viewportFraction != null),
- assert(viewportFraction > 0.0),
+ }) : assert(viewportFraction > 0.0),
_viewportFraction = viewportFraction,
_pageToUseOnStartup = initialPage.toDouble(),
super(
@@ -423,8 +420,6 @@
@override
void restoreOffset(double offset, {bool initialRestore = false}) {
- assert(initialRestore != null);
- assert(offset != null);
if (initialRestore) {
_pageToUseOnStartup = offset;
} else {
@@ -493,6 +488,7 @@
double? viewportDimension,
AxisDirection? axisDirection,
double? viewportFraction,
+ double? devicePixelRatio,
}) {
return PageMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
@@ -501,6 +497,7 @@
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
viewportFraction: viewportFraction ?? this.viewportFraction,
+ devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
);
}
}
@@ -509,7 +506,7 @@
const _ForceImplicitScrollPhysics({
required this.allowImplicitScrolling,
super.parent,
- }) : assert(allowImplicitScrolling != null);
+ });
@override
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
@@ -573,7 +570,7 @@
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
- final Tolerance tolerance = this.tolerance;
+ final Tolerance tolerance = toleranceFor(position);
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
@@ -657,9 +654,7 @@
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
- }) : assert(allowImplicitScrolling != null),
- assert(clipBehavior != null),
- controller = controller ?? _defaultPageController,
+ }) : controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildListDelegate(children);
/// Creates a scrollable list that works page by page using widgets that are
@@ -704,9 +699,7 @@
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
- }) : assert(allowImplicitScrolling != null),
- assert(clipBehavior != null),
- controller = controller ?? _defaultPageController,
+ }) : controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
@@ -812,10 +805,7 @@
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
- }) : assert(childrenDelegate != null),
- assert(allowImplicitScrolling != null),
- assert(clipBehavior != null),
- controller = controller ?? _defaultPageController;
+ }) : controller = controller ?? _defaultPageController;
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
diff --git a/framework/lib/src/widgets/pages.dart b/framework/lib/src/widgets/pages.dart
index cc726c7..de3ff66 100644
--- a/framework/lib/src/widgets/pages.dart
+++ b/framework/lib/src/widgets/pages.dart
@@ -81,12 +81,7 @@
this.maintainState = true,
super.fullscreenDialog,
super.allowSnapshotting = true,
- }) : assert(pageBuilder != null),
- assert(transitionsBuilder != null),
- assert(opaque != null),
- assert(barrierDismissible != null),
- assert(maintainState != null),
- assert(fullscreenDialog != null);
+ });
/// {@template flutter.widgets.pageRouteBuilder.pageBuilder}
/// Used build the route's primary contents.
diff --git a/framework/lib/src/widgets/performance_overlay.dart b/framework/lib/src/widgets/performance_overlay.dart
index 6ab0b9c..853d610 100644
--- a/framework/lib/src/widgets/performance_overlay.dart
+++ b/framework/lib/src/widgets/performance_overlay.dart
@@ -59,7 +59,7 @@
/// For example, if you want a trace of all pictures that could not be
/// rendered by the rasterizer within the frame boundary (and hence caused
/// jank), specify 1. Specifying 2 will trace all pictures that took more
- /// more than 2 frame intervals to render. Adjust this value to only capture
+ /// than 2 frame intervals to render. Adjust this value to only capture
/// the particularly expensive pictures while skipping the others. Specifying
/// 0 disables all capture.
///
diff --git a/framework/lib/src/widgets/platform_menu_bar.dart b/framework/lib/src/widgets/platform_menu_bar.dart
index 9d1288e..9127760 100644
--- a/framework/lib/src/widgets/platform_menu_bar.dart
+++ b/framework/lib/src/widgets/platform_menu_bar.dart
@@ -954,7 +954,7 @@
///
/// On macOS, this is the `terminate` default menu.
///
- /// This menu item will simply exit the application when activated.
+ /// This menu item will exit the application when activated.
quit,
/// The system provided "Services" submenu.
diff --git a/framework/lib/src/widgets/platform_view.dart b/framework/lib/src/widgets/platform_view.dart
index fa99120..a18f7cc 100644
--- a/framework/lib/src/widgets/platform_view.dart
+++ b/framework/lib/src/widgets/platform_view.dart
@@ -78,10 +78,7 @@
this.creationParams,
this.creationParamsCodec,
this.clipBehavior = Clip.hardEdge,
- }) : assert(viewType != null),
- assert(hitTestBehavior != null),
- assert(creationParams == null || creationParamsCodec != null),
- assert(clipBehavior != null);
+ }) : assert(creationParams == null || creationParamsCodec != null);
/// The unique identifier for Android view type to be embedded by this widget.
///
@@ -202,8 +199,6 @@
// TODO(ychris): remove the documentation for conic path not supported once https://github.com/flutter/flutter/issues/35062 is resolved.
/// Embeds an iOS view in the Widget hierarchy.
///
-/// {@macro flutter.rendering.RenderUiKitView}
-///
/// Embedding iOS views is an expensive operation and should be avoided when a Flutter
/// equivalent is possible.
///
@@ -216,6 +211,7 @@
/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints
/// nothing while maintaining the same layout constraints.
///
+/// Clipping operations on a UiKitView can result slow performance.
/// If a conic path clipping is applied to a UIKitView,
/// a quad path is used to approximate the clip due to limitation of Quartz.
class UiKitView extends StatefulWidget {
@@ -231,9 +227,7 @@
this.creationParams,
this.creationParamsCodec,
this.gestureRecognizers,
- }) : assert(viewType != null),
- assert(hitTestBehavior != null),
- assert(creationParams == null || creationParamsCodec != null);
+ }) : assert(creationParams == null || creationParamsCodec != null);
// TODO(amirh): reference the iOS API doc once available.
/// The unique identifier for iOS view type to be embedded by this widget.
@@ -351,8 +345,7 @@
super.key,
required this.viewType,
this.onPlatformViewCreated,
- }) : assert(viewType != null),
- assert(kIsWeb, 'HtmlElementView is only available on Flutter Web.');
+ }) : assert(kIsWeb, 'HtmlElementView is only available on Flutter Web.');
/// The unique identifier for the HTML view type to be embedded by this widget.
///
@@ -687,10 +680,7 @@
required this.hitTestBehavior,
required this.gestureRecognizers,
this.clipBehavior = Clip.hardEdge,
- }) : assert(controller != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null),
- assert(clipBehavior != null);
+ });
final AndroidViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior;
@@ -720,9 +710,7 @@
required this.controller,
required this.hitTestBehavior,
required this.gestureRecognizers,
- }) : assert(controller != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null);
+ });
final UiKitViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior;
@@ -757,8 +745,7 @@
required this.viewType,
required this.onPlatformViewCreated,
required this.onFocusChanged,
- }) : assert(id != null),
- assert(onPlatformViewCreated != null);
+ });
/// The unique identifier for the new platform view.
///
@@ -845,10 +832,7 @@
required PlatformViewSurfaceFactory surfaceFactory,
required CreatePlatformViewCallback onCreatePlatformView,
required this.viewType,
- }) : assert(surfaceFactory != null),
- assert(onCreatePlatformView != null),
- assert(viewType != null),
- _surfaceFactory = surfaceFactory,
+ }) : _surfaceFactory = surfaceFactory,
_onCreatePlatformView = onCreatePlatformView;
@@ -983,9 +967,7 @@
required this.controller,
required this.hitTestBehavior,
required this.gestureRecognizers,
- }) : assert(controller != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null);
+ });
/// The controller for the platform view integrated by this [PlatformViewSurface].
///
@@ -1078,9 +1060,7 @@
required this.controller,
required this.hitTestBehavior,
required this.gestureRecognizers,
- }) : assert(controller != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null);
+ });
/// The controller for the platform view integrated by this [AndroidViewSurface].
///
@@ -1147,9 +1127,7 @@
required AndroidViewController super.controller,
required super.hitTestBehavior,
required super.gestureRecognizers,
- }) : assert(controller != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null);
+ });
@override
RenderObject createRenderObject(BuildContext context) {
@@ -1172,9 +1150,7 @@
required AndroidViewController super.controller,
required super.hitTestBehavior,
required super.gestureRecognizers,
- }) : assert(controller != null),
- assert(hitTestBehavior != null),
- assert(gestureRecognizers != null);
+ });
@override
RenderObject createRenderObject(BuildContext context) {
diff --git a/framework/lib/src/widgets/primary_scroll_controller.dart b/framework/lib/src/widgets/primary_scroll_controller.dart
index 91a754c..825cab2 100644
--- a/framework/lib/src/widgets/primary_scroll_controller.dart
+++ b/framework/lib/src/widgets/primary_scroll_controller.dart
@@ -17,6 +17,8 @@
/// Associates a [ScrollController] with a subtree.
///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU}
+///
/// When a [ScrollView] has [ScrollView.primary] set to true, the [ScrollView]
/// uses [of] to inherit the [PrimaryScrollController] associated with its
/// subtree.
@@ -51,7 +53,7 @@
this.automaticallyInheritForPlatforms = _kMobilePlatforms,
this.scrollDirection = Axis.vertical,
required super.child,
- }) : assert(controller != null);
+ });
/// Creates a subtree without an associated [ScrollController].
const PrimaryScrollController.none({
diff --git a/framework/lib/src/widgets/raw_keyboard_listener.dart b/framework/lib/src/widgets/raw_keyboard_listener.dart
index 9a6d189..fec0b6c 100644
--- a/framework/lib/src/widgets/raw_keyboard_listener.dart
+++ b/framework/lib/src/widgets/raw_keyboard_listener.dart
@@ -47,10 +47,7 @@
this.includeSemantics = true,
this.onKey,
required this.child,
- }) : assert(focusNode != null),
- assert(autofocus != null),
- assert(includeSemantics != null),
- assert(child != null);
+ });
/// Controls whether this widget has keyboard focus.
final FocusNode focusNode;
diff --git a/framework/lib/src/widgets/reorderable_list.dart b/framework/lib/src/widgets/reorderable_list.dart
index f3ace70..9cc6dcf 100644
--- a/framework/lib/src/widgets/reorderable_list.dart
+++ b/framework/lib/src/widgets/reorderable_list.dart
@@ -268,7 +268,6 @@
/// * [maybeOf], a similar function that will return null if no
/// [ReorderableList] ancestor is found.
static ReorderableListState of(BuildContext context) {
- assert(context != null);
final ReorderableListState? result = context.findAncestorStateOfType<ReorderableListState>();
assert(() {
if (result == null) {
@@ -307,7 +306,6 @@
/// * [of], a similar function that will throw if no [ReorderableList] ancestor
/// is found.
static ReorderableListState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<ReorderableListState>();
}
@@ -496,7 +494,6 @@
/// * [maybeOf], a similar function that will return null if no
/// [SliverReorderableList] ancestor is found.
static SliverReorderableListState of(BuildContext context) {
- assert(context != null);
final SliverReorderableListState? result = context.findAncestorStateOfType<SliverReorderableListState>();
assert(() {
if (result == null) {
@@ -537,7 +534,6 @@
/// * [of], a similar function that will throw if no [SliverReorderableList]
/// ancestor is found.
static SliverReorderableListState? maybeOf(BuildContext context) {
- assert(context != null);
return context.findAncestorStateOfType<SliverReorderableListState>();
}
}
@@ -1163,7 +1159,7 @@
}
void _startDragging(BuildContext context, PointerDownEvent event) {
- final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
+ final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
final SliverReorderableListState? list = SliverReorderableList.maybeOf(context);
list?.startItemDragReorder(
index: index,
diff --git a/framework/lib/src/widgets/restoration.dart b/framework/lib/src/widgets/restoration.dart
index 56f2a2c..67f3714 100644
--- a/framework/lib/src/widgets/restoration.dart
+++ b/framework/lib/src/widgets/restoration.dart
@@ -60,7 +60,7 @@
super.key,
required this.restorationId,
required this.child,
- }) : assert(child != null);
+ });
/// Returns the [RestorationBucket] inserted into the widget tree by the
/// closest ancestor [RestorationScope] of `context`.
@@ -194,7 +194,7 @@
super.key,
this.bucket,
required super.child,
- }) : assert(child != null);
+ });
/// The [RestorationBucket] that this widget will insert into the widget tree.
///
@@ -268,7 +268,7 @@
super.key,
required this.restorationId,
required this.child,
- }) : assert(child != null);
+ });
/// The widget below this widget in the tree.
///
@@ -529,8 +529,6 @@
RestorationMixin? _owner;
void _register(String restorationId, RestorationMixin owner) {
assert(ChangeNotifier.debugAssertNotDisposed(this));
- assert(restorationId != null);
- assert(owner != null);
_restorationId = restorationId;
_owner = owner;
}
@@ -768,8 +766,6 @@
/// unless it has been unregistered with [unregisterFromRestoration].
@protected
void registerForRestoration(RestorableProperty<Object?> property, String restorationId) {
- assert(property != null);
- assert(restorationId != null);
assert(property._restorationId == null || (_debugDoingRestore && property._restorationId == restorationId),
'Property is already registered under ${property._restorationId}.',
);
@@ -822,7 +818,6 @@
/// restoration data by calling this method.
@protected
void unregisterFromRestoration(RestorableProperty<Object?> property) {
- assert(property != null);
assert(property._owner == this);
_bucket?.remove<Object?>(property._restorationId!);
_unregister(property);
@@ -949,10 +944,8 @@
return didReplace;
}
assert(restorationId != null);
- assert(parent != null);
if (restorePending || _bucket == null) {
final RestorationBucket newBucket = parent.claimChild(restorationId!, debugOwner: this);
- assert(newBucket != null);
final bool didReplace = _setNewBucketIfNecessary(newBucket: newBucket, restorePending: restorePending);
assert(_bucket == newBucket);
return didReplace;
diff --git a/framework/lib/src/widgets/restoration_properties.dart b/framework/lib/src/widgets/restoration_properties.dart
index c925654..9f3731d 100644
--- a/framework/lib/src/widgets/restoration_properties.dart
+++ b/framework/lib/src/widgets/restoration_properties.dart
@@ -136,12 +136,10 @@
// See [_RestorablePrimitiveValueN] for the nullable version of this class.
class _RestorablePrimitiveValue<T extends Object> extends _RestorablePrimitiveValueN<T> {
_RestorablePrimitiveValue(super.defaultValue)
- : assert(defaultValue != null),
- assert(debugIsSerializableForRestoration(defaultValue));
+ : assert(debugIsSerializableForRestoration(defaultValue));
@override
set value(T value) {
- assert(value != null);
super.value = value;
}
@@ -153,7 +151,6 @@
@override
Object toPrimitives() {
- assert(value != null);
return super.toPrimitives()!;
}
}
@@ -183,7 +180,7 @@
/// If no restoration data is available to restore the value in this property
/// from, the property will be initialized with the provided `defaultValue`.
/// {@endtemplate}
- RestorableNum(super.defaultValue) : assert(defaultValue != null);
+ RestorableNum(super.defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [double].
@@ -197,7 +194,7 @@
/// Creates a [RestorableDouble].
///
/// {@macro flutter.widgets.RestorableNum.constructor}
- RestorableDouble(super.defaultValue) : assert(defaultValue != null);
+ RestorableDouble(super.defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore an [int].
@@ -211,7 +208,7 @@
/// Creates a [RestorableInt].
///
/// {@macro flutter.widgets.RestorableNum.constructor}
- RestorableInt(super.defaultValue) : assert(defaultValue != null);
+ RestorableInt(super.defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [String].
@@ -225,7 +222,7 @@
/// Creates a [RestorableString].
///
/// {@macro flutter.widgets.RestorableNum.constructor}
- RestorableString(super.defaultValue) : assert(defaultValue != null);
+ RestorableString(super.defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [bool].
@@ -239,7 +236,7 @@
/// Creates a [RestorableBool].
///
/// {@macro flutter.widgets.RestorableNum.constructor}
- RestorableBool(super.defaultValue) : assert(defaultValue != null);
+ RestorableBool(super.defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [bool] that is
@@ -406,7 +403,6 @@
@override
void initWithValue(T value) {
- assert(value != null);
_value?.removeListener(notifyListeners);
_value = value;
_value!.addListener(notifyListeners);
diff --git a/framework/lib/src/widgets/router.dart b/framework/lib/src/widgets/router.dart
index b3ea4f3..2a9050e 100644
--- a/framework/lib/src/widgets/router.dart
+++ b/framework/lib/src/widgets/router.dart
@@ -329,8 +329,7 @@
}) : assert(
routeInformationProvider == null || routeInformationParser != null,
'A routeInformationParser must be provided when a routeInformationProvider is specified.',
- ),
- assert(routerDelegate != null);
+ );
/// Creates a router with a [RouterConfig].
///
@@ -620,7 +619,6 @@
RouteInformationReportingType status,
VoidCallback fn,
) {
- assert(status != null);
assert(status.index >= RouteInformationReportingType.neglect.index);
assert(() {
if (_currentIntentionToReport != null &&
@@ -721,7 +719,6 @@
}
void _handleRouteInformationProviderNotification() {
- assert(widget.routeInformationProvider!.value != null);
_routeParsePending = true;
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
@@ -786,9 +783,7 @@
required this.routerDelegate,
required this.routerState,
required super.child,
- }) : assert(routeInformationProvider == null || routeInformationParser != null),
- assert(routerDelegate != null),
- assert(routerState != null);
+ }) : assert(routeInformationProvider == null || routeInformationParser != null);
final ValueListenable<RouteInformation?>? routeInformationProvider;
final BackButtonDispatcher? backButtonDispatcher;
@@ -1050,7 +1045,7 @@
/// Creates a back button dispatcher that acts as the child of another.
///
/// The [parent] must not be null.
- ChildBackButtonDispatcher(this.parent) : assert(parent != null);
+ ChildBackButtonDispatcher(this.parent);
/// The back button dispatcher that this object will attempt to take priority
/// over when [takePriority] is called.
diff --git a/framework/lib/src/widgets/routes.dart b/framework/lib/src/widgets/routes.dart
index ab94622..c63403b 100644
--- a/framework/lib/src/widgets/routes.dart
+++ b/framework/lib/src/widgets/routes.dart
@@ -204,7 +204,7 @@
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
final Duration duration = transitionDuration;
final Duration reverseDuration = reverseTransitionDuration;
- assert(duration != null && duration >= Duration.zero);
+ assert(duration >= Duration.zero);
return AnimationController(
duration: duration,
reverseDuration: reverseDuration,
@@ -692,7 +692,6 @@
/// The entry's [LocalHistoryEntry.onRemove] callback, if any, will be called
/// synchronously.
void removeLocalHistoryEntry(LocalHistoryEntry entry) {
- assert(entry != null);
assert(entry._owner == this);
assert(_localHistory!.contains(entry));
bool internalStateChanged = false;
@@ -775,10 +774,7 @@
required this.impliesAppBarDismissal,
required this.route,
required super.child,
- }) : assert(isCurrent != null),
- assert(canPop != null),
- assert(route != null),
- assert(child != null);
+ });
final bool isCurrent;
final bool canPop;
@@ -1004,9 +1000,10 @@
/// [BackdropFilter]. This allows blur effects, for example.
final ui.ImageFilter? filter;
- /// {@macro flutter.focus.TraversalEdgeBehavior}
+ /// Controls the transfer of focus beyond the first and the last items of a
+ /// [FocusScopeNode].
///
- /// If set to null, [MediaQueryData.routeTraversalEdgeBehavior] is used.
+ /// If set to null, [Navigator.routeTraversalEdgeBehavior] is used.
final TraversalEdgeBehavior? traversalEdgeBehavior;
// The API for general users of this class
@@ -1681,6 +1678,37 @@
// one of the builders
late OverlayEntry _modalBarrier;
Widget _buildModalBarrier(BuildContext context) {
+ Widget barrier = buildModalBarrier();
+ if (filter != null) {
+ barrier = BackdropFilter(
+ filter: filter!,
+ child: barrier,
+ );
+ }
+ barrier = IgnorePointer(
+ ignoring: animation!.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates
+ animation!.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
+ child: barrier,
+ );
+ if (semanticsDismissible && barrierDismissible) {
+ // To be sorted after the _modalScope.
+ barrier = Semantics(
+ sortKey: const OrdinalSortKey(1.0),
+ child: barrier,
+ );
+ }
+ return barrier;
+ }
+
+ /// Build the barrier for this [ModalRoute], subclasses can override
+ /// this method to create their own barrier with customized features such as
+ /// color or accessibility focus size.
+ ///
+ /// See also:
+ /// * [ModalBarrier], which is typically used to build a barrier.
+ /// * [ModalBottomSheetRoute], which overrides this method to build a
+ /// customized barrier.
+ Widget buildModalBarrier() {
Widget barrier;
if (barrierColor != null && barrierColor!.alpha != 0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
assert(barrierColor != barrierColor!.withOpacity(0.0));
@@ -1703,24 +1731,7 @@
barrierSemanticsDismissible: semanticsDismissible,
);
}
- if (filter != null) {
- barrier = BackdropFilter(
- filter: filter!,
- child: barrier,
- );
- }
- barrier = IgnorePointer(
- ignoring: animation!.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates
- animation!.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
- child: barrier,
- );
- if (semanticsDismissible && barrierDismissible) {
- // To be sorted after the _modalScope.
- barrier = Semantics(
- sortKey: const OrdinalSortKey(1.0),
- child: barrier,
- );
- }
+
return barrier;
}
@@ -1886,8 +1897,6 @@
/// to [route], e.g. when [route] is covered by another route or when [route]
/// is popped off the [Navigator] stack.
void subscribe(RouteAware routeAware, R route) {
- assert(routeAware != null);
- assert(route != null);
final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => <RouteAware>{});
if (subscribers.add(routeAware)) {
routeAware.didPush();
@@ -1899,7 +1908,6 @@
/// [routeAware] is no longer informed about changes to its route. If the given argument was
/// subscribed to multiple types, this will unregister it (once) from each type.
void unsubscribe(RouteAware routeAware) {
- assert(routeAware != null);
final List<R> routes = _listeners.keys.toList();
for (final R route in routes) {
final Set<RouteAware>? subscribers = _listeners[route];
@@ -2023,8 +2031,7 @@
super.settings,
this.anchorPoint,
super.traversalEdgeBehavior,
- }) : assert(barrierDismissible != null),
- _pageBuilder = pageBuilder,
+ }) : _pageBuilder = pageBuilder,
_barrierDismissible = barrierDismissible,
_barrierLabel = barrierLabel,
_barrierColor = barrierColor,
@@ -2171,8 +2178,6 @@
RouteSettings? routeSettings,
Offset? anchorPoint,
}) {
- assert(pageBuilder != null);
- assert(useRootNavigator != null);
assert(!barrierDismissible || barrierLabel != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>(
pageBuilder: pageBuilder,
diff --git a/framework/lib/src/widgets/safe_area.dart b/framework/lib/src/widgets/safe_area.dart
index f4a82da..cb384f3 100644
--- a/framework/lib/src/widgets/safe_area.dart
+++ b/framework/lib/src/widgets/safe_area.dart
@@ -47,10 +47,7 @@
this.minimum = EdgeInsets.zero,
this.maintainBottomViewPadding = false,
required this.child,
- }) : assert(left != null),
- assert(top != null),
- assert(right != null),
- assert(bottom != null);
+ });
/// Whether to avoid system intrusions on the left.
final bool left;
@@ -93,11 +90,10 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final MediaQueryData data = MediaQuery.of(context);
- EdgeInsets padding = data.padding;
+ EdgeInsets padding = MediaQuery.paddingOf(context);
// Bottom padding has been consumed - i.e. by the keyboard
if (maintainBottomViewPadding) {
- padding = padding.copyWith(bottom: data.viewPadding.bottom);
+ padding = padding.copyWith(bottom: MediaQuery.viewPaddingOf(context).bottom);
}
return Padding(
@@ -160,10 +156,7 @@
this.bottom = true,
this.minimum = EdgeInsets.zero,
required this.sliver,
- }) : assert(left != null),
- assert(top != null),
- assert(right != null),
- assert(bottom != null);
+ });
/// Whether to avoid system intrusions on the left.
final bool left;
@@ -192,7 +185,7 @@
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
- final EdgeInsets padding = MediaQuery.of(context).padding;
+ final EdgeInsets padding = MediaQuery.paddingOf(context);
return SliverPadding(
padding: EdgeInsets.only(
left: math.max(left ? padding.left : 0.0, minimum.left),
diff --git a/framework/lib/src/widgets/scroll_activity.dart b/framework/lib/src/widgets/scroll_activity.dart
index 2e41dd1..f555a35 100644
--- a/framework/lib/src/widgets/scroll_activity.dart
+++ b/framework/lib/src/widgets/scroll_activity.dart
@@ -234,9 +234,7 @@
this.onDragCanceled,
this.carriedVelocity,
this.motionStartDistanceThreshold,
- }) : assert(delegate != null),
- assert(details != null),
- assert(
+ }) : assert(
motionStartDistanceThreshold == null || motionStartDistanceThreshold > 0.0,
'motionStartDistanceThreshold must be a positive number or null',
),
@@ -621,11 +619,7 @@
required Duration duration,
required Curve curve,
required TickerProvider vsync,
- }) : assert(from != null),
- assert(to != null),
- assert(duration != null),
- assert(duration > Duration.zero),
- assert(curve != null) {
+ }) : assert(duration > Duration.zero) {
_completer = Completer<void>();
_controller = AnimationController.unbounded(
value: from,
diff --git a/framework/lib/src/widgets/scroll_aware_image_provider.dart b/framework/lib/src/widgets/scroll_aware_image_provider.dart
index 4524705..958cb1a 100644
--- a/framework/lib/src/widgets/scroll_aware_image_provider.dart
+++ b/framework/lib/src/widgets/scroll_aware_image_provider.dart
@@ -54,8 +54,7 @@
const ScrollAwareImageProvider({
required this.context,
required this.imageProvider,
- }) : assert(context != null),
- assert(imageProvider != null);
+ });
/// The context that may or may not be enclosed by a [Scrollable].
///
@@ -113,5 +112,8 @@
ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) => imageProvider.loadBuffer(key, decode);
@override
+ ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) => imageProvider.loadImage(key, decode);
+
+ @override
Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
}
diff --git a/framework/lib/src/widgets/scroll_configuration.dart b/framework/lib/src/widgets/scroll_configuration.dart
index 5b2b6b4..b7aca90 100644
--- a/framework/lib/src/widgets/scroll_configuration.dart
+++ b/framework/lib/src/widgets/scroll_configuration.dart
@@ -5,6 +5,7 @@
import 'package:flute/foundation.dart';
import 'package:flute/gestures.dart';
import 'package:flute/rendering.dart';
+import 'package:flute/services.dart' show LogicalKeyboardKey;
import 'framework.dart';
import 'overscroll_indicator.dart';
@@ -100,6 +101,7 @@
bool? scrollbars,
bool? overscroll,
Set<PointerDeviceKind>? dragDevices,
+ Set<LogicalKeyboardKey>? pointerAxisModifiers,
ScrollPhysics? physics,
TargetPlatform? platform,
@Deprecated(
@@ -112,9 +114,10 @@
delegate: this,
scrollbars: scrollbars ?? true,
overscroll: overscroll ?? true,
+ dragDevices: dragDevices,
+ pointerAxisModifiers: pointerAxisModifiers,
physics: physics,
platform: platform,
- dragDevices: dragDevices,
androidOverscrollIndicator: androidOverscrollIndicator
);
}
@@ -132,6 +135,25 @@
/// impossible to select text in scrollable containers and is not recommended.
Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;
+ /// A set of [LogicalKeyboardKey]s that, when any or all are pressed in
+ /// combination with a [PointerDeviceKind.mouse] pointer scroll event, will
+ /// flip the axes of the scroll input.
+ ///
+ /// This will for example, result in the input of a vertical mouse wheel, to
+ /// move the [ScrollPosition] of a [ScrollView] with an [Axis.horizontal]
+ /// scroll direction.
+ ///
+ /// If other keys exclusive of this set are pressed during a scroll event, in
+ /// conjunction with keys from this set, the scroll input will still be
+ /// flipped.
+ ///
+ /// Defaults to [LogicalKeyboardKey.shiftLeft],
+ /// [LogicalKeyboardKey.shiftRight].
+ Set<LogicalKeyboardKey> get pointerAxisModifiers => <LogicalKeyboardKey>{
+ LogicalKeyboardKey.shiftLeft,
+ LogicalKeyboardKey.shiftRight,
+ };
+
/// Applies a [RawScrollbar] to the child widget on desktop platforms.
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
// When modifying this function, consider modifying the implementation in
@@ -261,12 +283,14 @@
required this.delegate,
this.scrollbars = true,
this.overscroll = true,
+ Set<PointerDeviceKind>? dragDevices,
+ Set<LogicalKeyboardKey>? pointerAxisModifiers,
this.physics,
this.platform,
- Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator,
}) : _androidOverscrollIndicator = androidOverscrollIndicator,
- _dragDevices = dragDevices;
+ _dragDevices = dragDevices,
+ _pointerAxisModifiers = pointerAxisModifiers;
final ScrollBehavior delegate;
final bool scrollbars;
@@ -274,6 +298,7 @@
final ScrollPhysics? physics;
final TargetPlatform? platform;
final Set<PointerDeviceKind>? _dragDevices;
+ final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
@override
final AndroidOverscrollIndicator? _androidOverscrollIndicator;
@@ -281,6 +306,9 @@
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
+ Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
+
+ @override
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
@override
@@ -303,17 +331,19 @@
ScrollBehavior copyWith({
bool? scrollbars,
bool? overscroll,
+ Set<PointerDeviceKind>? dragDevices,
+ Set<LogicalKeyboardKey>? pointerAxisModifiers,
ScrollPhysics? physics,
TargetPlatform? platform,
- Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator
}) {
return delegate.copyWith(
scrollbars: scrollbars ?? this.scrollbars,
overscroll: overscroll ?? this.overscroll,
+ dragDevices: dragDevices ?? this.dragDevices,
+ pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers,
physics: physics ?? this.physics,
platform: platform ?? this.platform,
- dragDevices: dragDevices ?? this.dragDevices,
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
);
}
@@ -333,9 +363,10 @@
return oldDelegate.delegate.runtimeType != delegate.runtimeType
|| oldDelegate.scrollbars != scrollbars
|| oldDelegate.overscroll != overscroll
+ || !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
+ || !setEquals<LogicalKeyboardKey>(oldDelegate.pointerAxisModifiers, pointerAxisModifiers)
|| oldDelegate.physics != physics
|| oldDelegate.platform != platform
- || !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| delegate.shouldNotify(oldDelegate.delegate);
}
@@ -376,7 +407,6 @@
@override
bool updateShouldNotify(ScrollConfiguration oldWidget) {
- assert(behavior != null);
return behavior.runtimeType != oldWidget.behavior.runtimeType
|| (behavior != oldWidget.behavior && behavior.shouldNotify(oldWidget.behavior));
}
diff --git a/framework/lib/src/widgets/scroll_controller.dart b/framework/lib/src/widgets/scroll_controller.dart
index 0315a28..fb7a7a3 100644
--- a/framework/lib/src/widgets/scroll_controller.dart
+++ b/framework/lib/src/widgets/scroll_controller.dart
@@ -49,9 +49,7 @@
double initialScrollOffset = 0.0,
this.keepScrollOffset = true,
this.debugLabel,
- }) : assert(initialScrollOffset != null),
- assert(keepScrollOffset != null),
- _initialScrollOffset = initialScrollOffset;
+ }) : _initialScrollOffset = initialScrollOffset;
/// The initial value to use for [offset].
///
diff --git a/framework/lib/src/widgets/scroll_metrics.dart b/framework/lib/src/widgets/scroll_metrics.dart
index ef02b85..e637e0f 100644
--- a/framework/lib/src/widgets/scroll_metrics.dart
+++ b/framework/lib/src/widgets/scroll_metrics.dart
@@ -46,6 +46,7 @@
double? pixels,
double? viewportDimension,
AxisDirection? axisDirection,
+ double? devicePixelRatio,
}) {
return FixedScrollMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
@@ -53,6 +54,7 @@
pixels: pixels ?? (hasPixels ? this.pixels : null),
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
+ devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
);
}
@@ -124,6 +126,10 @@
/// The quantity of content conceptually "below" the viewport in the scrollable.
/// This is the content below the content described by [extentInside].
double get extentAfter => math.max(maxScrollExtent - pixels, 0.0);
+
+ /// The [FlutterView.devicePixelRatio] of the view that the [Scrollable]
+ /// associated with this metrics object is drawn into.
+ double get devicePixelRatio;
}
/// An immutable snapshot of values associated with a [Scrollable] viewport.
@@ -137,6 +143,7 @@
required double? pixels,
required double? viewportDimension,
required this.axisDirection,
+ required this.devicePixelRatio,
}) : _minScrollExtent = minScrollExtent,
_maxScrollExtent = maxScrollExtent,
_pixels = pixels,
@@ -171,6 +178,9 @@
final AxisDirection axisDirection;
@override
+ final double devicePixelRatio;
+
+ @override
String toString() {
return '${objectRuntimeType(this, 'FixedScrollMetrics')}(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)})';
}
diff --git a/framework/lib/src/widgets/scroll_notification.dart b/framework/lib/src/widgets/scroll_notification.dart
index 1df53a6..3abe9e7 100644
--- a/framework/lib/src/widgets/scroll_notification.dart
+++ b/framework/lib/src/widgets/scroll_notification.dart
@@ -198,10 +198,8 @@
this.dragDetails,
required this.overscroll,
this.velocity = 0.0,
- }) : assert(overscroll != null),
- assert(overscroll.isFinite),
- assert(overscroll != 0.0),
- assert(velocity != null);
+ }) : assert(overscroll.isFinite),
+ assert(overscroll != 0.0);
/// If the [Scrollable] overscrolled because of a drag, the details about that
/// drag update.
diff --git a/framework/lib/src/widgets/scroll_notification_observer.dart b/framework/lib/src/widgets/scroll_notification_observer.dart
index 4f49736..4b46042 100644
--- a/framework/lib/src/widgets/scroll_notification_observer.dart
+++ b/framework/lib/src/widgets/scroll_notification_observer.dart
@@ -78,7 +78,7 @@
const ScrollNotificationObserver({
super.key,
required this.child,
- }) : assert(child != null);
+ });
/// The subtree below this widget.
final Widget child;
diff --git a/framework/lib/src/widgets/scroll_physics.dart b/framework/lib/src/widgets/scroll_physics.dart
index c2425cc..350a298 100644
--- a/framework/lib/src/widgets/scroll_physics.dart
+++ b/framework/lib/src/widgets/scroll_physics.dart
@@ -6,6 +6,7 @@
import 'package:flute/foundation.dart';
import 'package:flute/gestures.dart';
+import 'package:flute/painting.dart' show AxisDirection;
import 'package:flute/physics.dart';
import 'binding.dart' show WidgetsBinding;
@@ -13,6 +14,7 @@
import 'overscroll_indicator.dart';
import 'scroll_metrics.dart';
import 'scroll_simulation.dart';
+import 'view.dart';
export 'package:flute/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance;
@@ -236,8 +238,8 @@
/// the overall scale between the global screen and local scrollable
/// coordinate systems.
///
- /// The default implementation is stateless, and simply provides a point-in-
- /// time decision about how fast the scrollable is scrolling. It would always
+ /// The default implementation is stateless, and provides a point-in-time
+ /// decision about how fast the scrollable is scrolling. It would always
/// return true for a scrollable that is animating back and forth at high
/// velocity in a loop. It is assumed that callers will handle such
/// a case, or that a custom stateful implementation would be written that
@@ -247,11 +249,8 @@
/// is great enough that expensive operations impacting the UI should be
/// deferred.
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
- assert(velocity != null);
- assert(metrics != null);
- assert(context != null);
if (parent == null) {
- final double maxPhysicalPixels = WidgetsBinding.instance.window.physicalSize.longestSide;
+ final double maxPhysicalPixels = View.of(context).physicalSize.longestSide;
return velocity.abs() > maxPhysicalPixels;
}
return parent!.recommendDeferredLoading(velocity, metrics, context);
@@ -382,19 +381,32 @@
/// The spring to use for ballistic simulations.
SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
- /// The default accuracy to which scrolling is computed.
- static final Tolerance _kDefaultTolerance = Tolerance(
- // TODO(ianh): Handle the case of the device pixel ratio changing.
- // TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
- velocity: 1.0 / (0.050 * WidgetsBinding.instance.window.devicePixelRatio), // logical pixels per second
- distance: 1.0 / WidgetsBinding.instance.window.devicePixelRatio, // logical pixels
- );
+ /// Deprecated. Call [toleranceFor] instead.
+ @Deprecated(
+ 'Call toleranceFor instead. '
+ 'This feature was deprecated after v3.7.0-13.0.pre.',
+ )
+ Tolerance get tolerance {
+ return toleranceFor(FixedScrollMetrics(
+ minScrollExtent: null,
+ maxScrollExtent: null,
+ pixels: null,
+ viewportDimension: null,
+ axisDirection: AxisDirection.down,
+ devicePixelRatio: WidgetsBinding.instance.window.devicePixelRatio,
+ ));
+ }
/// The tolerance to use for ballistic simulations.
- Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
+ Tolerance toleranceFor(ScrollMetrics metrics) {
+ return parent?.toleranceFor(metrics) ?? Tolerance(
+ velocity: 1.0 / (0.050 * metrics.devicePixelRatio), // logical pixels per second
+ distance: 1.0 / metrics.devicePixelRatio, // logical pixels
+ );
+ }
- /// The minimum distance an input pointer drag must have moved to
- /// to be considered a scroll fling gesture.
+ /// The minimum distance an input pointer drag must have moved to be
+ /// considered a scroll fling gesture.
///
/// This value is typically compared with the distance traveled along the
/// scrolling axis.
@@ -695,7 +707,7 @@
@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
- final Tolerance tolerance = this.tolerance;
+ final Tolerance tolerance = toleranceFor(position);
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
double constantDeceleration;
switch (decelerationRate) {
@@ -839,7 +851,7 @@
@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
- final Tolerance tolerance = this.tolerance;
+ final Tolerance tolerance = toleranceFor(position);
if (position.outOfRange) {
double? end;
if (position.pixels > position.maxScrollExtent) {
diff --git a/framework/lib/src/widgets/scroll_position.dart b/framework/lib/src/widgets/scroll_position.dart
index 1982720..99420ae 100644
--- a/framework/lib/src/widgets/scroll_position.dart
+++ b/framework/lib/src/widgets/scroll_position.dart
@@ -12,6 +12,7 @@
import 'basic.dart';
import 'framework.dart';
+import 'media_query.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_activity.dart';
@@ -19,6 +20,7 @@
import 'scroll_metrics.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
+import 'view.dart';
export 'scroll_activity.dart' show ScrollHoldController;
@@ -98,10 +100,7 @@
this.keepScrollOffset = true,
ScrollPosition? oldPosition,
this.debugLabel,
- }) : assert(physics != null),
- assert(context != null),
- assert(context.vsync != null),
- assert(keepScrollOffset != null) {
+ }) {
if (oldPosition != null) {
absorb(oldPosition);
}
@@ -217,7 +216,6 @@
@protected
@mustCallSuper
void absorb(ScrollPosition other) {
- assert(other != null);
assert(other.context == context);
assert(_pixels == null);
if (other.hasContentDimensions) {
@@ -242,6 +240,9 @@
isScrollingNotifier.value = activity!.isScrolling;
}
+ @override
+ double get devicePixelRatio => MediaQuery.maybeDevicePixelRatioOf(context.storageContext) ?? View.of(context.storageContext).devicePixelRatio;
+
/// Update the scroll position ([pixels]) to a given pixel value.
///
/// This should only be called by the current [ScrollActivity], either during
@@ -378,7 +379,6 @@
@protected
void forcePixels(double value) {
assert(hasPixels);
- assert(value != null);
_impliedVelocity = value - pixels;
_pixels = value;
notifyListeners();
@@ -445,8 +445,6 @@
/// The method may be called multiple times in the lifecycle of a
/// [ScrollPosition] to restore it to different scroll offsets.
void restoreOffset(double offset, {bool initialRestore = false}) {
- assert(initialRestore != null);
- assert(offset != null);
if (initialRestore) {
correctPixels(offset);
} else {
@@ -531,15 +529,11 @@
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
- assert(minScrollExtent != null);
- assert(maxScrollExtent != null);
assert(haveDimensions == (_lastMetrics != null));
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection ||
_lastAxis != axis) {
- assert(minScrollExtent != null);
- assert(maxScrollExtent != null);
assert(minScrollExtent <= maxScrollExtent);
_minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent;
@@ -701,10 +695,8 @@
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject,
}) {
- assert(alignmentPolicy != null);
assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
- assert(viewport != null);
Rect? targetRect;
if (targetRenderObject != null && targetRenderObject != object) {
@@ -826,7 +818,6 @@
Curve? curve,
bool? clamp = true,
}) {
- assert(to != null);
assert(clamp != null);
if (clamp!) {
@@ -959,10 +950,7 @@
/// evaluate the current scroll velocity to be great enough that expensive
/// operations impacting the UI should be deferred.
bool recommendDeferredLoading(BuildContext context) {
- assert(context != null);
assert(activity != null);
- assert(activity!.velocity != null);
- assert(_impliedVelocity != null);
return physics.recommendDeferredLoading(
activity!.velocity + _impliedVelocity,
copyWith(),
diff --git a/framework/lib/src/widgets/scroll_position_with_single_context.dart b/framework/lib/src/widgets/scroll_position_with_single_context.dart
index 66d6af0..44a6e27 100644
--- a/framework/lib/src/widgets/scroll_position_with_single_context.dart
+++ b/framework/lib/src/widgets/scroll_position_with_single_context.dart
@@ -162,7 +162,6 @@
@protected
@visibleForTesting
void updateUserScrollDirection(ScrollDirection value) {
- assert(value != null);
if (userScrollDirection == value) {
return;
}
@@ -176,7 +175,7 @@
required Duration duration,
required Curve curve,
}) {
- if (nearEqual(to, pixels, physics.tolerance.distance)) {
+ if (nearEqual(to, pixels, physics.toleranceFor(this).distance)) {
// Skip the animation, go straight to the position as we are already close.
jumpTo(to);
return Future<void>.value();
@@ -212,7 +211,10 @@
// If an update is made to pointer scrolling here, consider if the same
// (or similar) change should be made in
// _NestedScrollCoordinator.pointerScroll.
- assert(delta != 0.0);
+ if (delta == 0.0) {
+ goBallistic(0.0);
+ return;
+ }
final double targetPixels =
math.min(math.max(pixels + delta, minScrollExtent), maxScrollExtent);
diff --git a/framework/lib/src/widgets/scroll_simulation.dart b/framework/lib/src/widgets/scroll_simulation.dart
index c1d3290..5e384c4 100644
--- a/framework/lib/src/widgets/scroll_simulation.dart
+++ b/framework/lib/src/widgets/scroll_simulation.dart
@@ -36,12 +36,7 @@
required this.spring,
double constantDeceleration = 0,
super.tolerance,
- }) : assert(position != null),
- assert(velocity != null),
- assert(leadingExtent != null),
- assert(trailingExtent != null),
- assert(leadingExtent <= trailingExtent),
- assert(spring != null) {
+ }) : assert(leadingExtent <= trailingExtent) {
if (position < leadingExtent) {
_springSimulation = _underscrollSimulation(position, velocity);
_springTime = double.negativeInfinity;
@@ -71,7 +66,6 @@
_springTime = double.infinity;
}
}
- assert(_springTime != null);
}
/// The maximum velocity that can be transferred from the inertia of a ballistic
diff --git a/framework/lib/src/widgets/scroll_view.dart b/framework/lib/src/widgets/scroll_view.dart
index 26cd8d8..653a04e 100644
--- a/framework/lib/src/widgets/scroll_view.dart
+++ b/framework/lib/src/widgets/scroll_view.dart
@@ -99,19 +99,13 @@
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
- }) : assert(scrollDirection != null),
- assert(reverse != null),
- assert(shrinkWrap != null),
- assert(dragStartBehavior != null),
- assert(clipBehavior != null),
- assert(
+ }) : assert(
!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance '
'from a PrimaryScrollController widget. You cannot both set primary to '
'true and pass an explicit controller.',
),
assert(!shrinkWrap || center == null),
- assert(anchor != null),
assert(anchor >= 0.0 && anchor <= 1.0),
assert(semanticChildCount == null || semanticChildCount >= 0),
physics = physics ?? ((primary ?? false) || (primary == null && controller == null && identical(scrollDirection, Axis.vertical)) ? const AlwaysScrollableScrollPhysics() : null);
@@ -186,6 +180,12 @@
/// [TargetPlatformVariant.mobile] for ScrollViews in the [Axis.vertical]
/// scroll direction. Adding another to your app will override the
/// PrimaryScrollController above it.
+ ///
+ /// The following video contains more information about scroll controllers,
+ /// the PrimaryScrollController widget, and their impact on your apps:
+ ///
+ /// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU}
+ ///
/// {@endtemplate}
final bool? primary;
@@ -1251,34 +1251,23 @@
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
- }) : assert(itemBuilder != null),
- assert(separatorBuilder != null),
- assert(itemCount != null && itemCount >= 0),
+ }) : assert(itemCount >= 0),
itemExtent = null,
prototypeItem = null,
childrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final int itemIndex = index ~/ 2;
- final Widget? widget;
if (index.isEven) {
- widget = itemBuilder(context, itemIndex);
- } else {
- widget = separatorBuilder(context, itemIndex);
- assert(() {
- if (widget == null) {
- throw FlutterError('separatorBuilder cannot return null.');
- }
- return true;
- }());
+ return itemBuilder(context, itemIndex);
}
- return widget;
+ return separatorBuilder(context, itemIndex);
},
findChildIndexCallback: findChildIndexCallback,
childCount: _computeActualChildCount(itemCount),
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
- semanticIndexCallback: (Widget _, int index) {
+ semanticIndexCallback: (Widget widget, int index) {
return index.isEven ? index ~/ 2 : null;
},
),
@@ -1391,8 +1380,7 @@
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
- }) : assert(childrenDelegate != null),
- assert(
+ }) : assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
);
@@ -1728,8 +1716,7 @@
super.clipBehavior,
super.keyboardDismissBehavior,
super.restorationId,
- }) : assert(gridDelegate != null),
- childrenDelegate = SliverChildListDelegate(
+ }) : childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
@@ -1785,8 +1772,7 @@
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
- }) : assert(gridDelegate != null),
- childrenDelegate = SliverChildBuilderDelegate(
+ }) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
@@ -1822,8 +1808,7 @@
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
- }) : assert(gridDelegate != null),
- assert(childrenDelegate != null);
+ });
/// Creates a scrollable, 2D array of widgets with a fixed number of tiles in
/// the cross axis.
diff --git a/framework/lib/src/widgets/scrollable.dart b/framework/lib/src/widgets/scrollable.dart
index 130776a..9a3ae7c 100644
--- a/framework/lib/src/widgets/scrollable.dart
+++ b/framework/lib/src/widgets/scrollable.dart
@@ -100,11 +100,7 @@
this.restorationId,
this.scrollBehavior,
this.clipBehavior = Clip.hardEdge,
- }) : assert(axisDirection != null),
- assert(dragStartBehavior != null),
- assert(viewportBuilder != null),
- assert(excludeFromSemantics != null),
- assert(semanticChildCount == null || semanticChildCount >= 0);
+ }) : assert(semanticChildCount == null || semanticChildCount >= 0);
/// The direction in which this widget scrolls.
///
@@ -421,8 +417,7 @@
required this.scrollable,
required this.position,
required super.child,
- }) : assert(scrollable != null),
- assert(child != null);
+ });
final ScrollableState scrollable;
final ScrollPosition position;
@@ -461,7 +456,7 @@
late ScrollBehavior _configuration;
ScrollPhysics? _physics;
ScrollController? _fallbackScrollController;
- MediaQueryData? _mediaQueryData;
+ DeviceGestureSettings? _mediaQueryGestureSettings;
ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;
@@ -516,7 +511,7 @@
@override
void didChangeDependencies() {
- _mediaQueryData = MediaQuery.maybeOf(context);
+ _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context);
_updatePosition();
super.didChangeDependencies();
}
@@ -635,7 +630,7 @@
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior
- ..gestureSettings = _mediaQueryData?.gestureSettings;
+ ..gestureSettings = _mediaQueryGestureSettings;
},
),
};
@@ -656,7 +651,7 @@
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior
- ..gestureSettings = _mediaQueryData?.gestureSettings;
+ ..gestureSettings = _mediaQueryGestureSettings;
},
),
};
@@ -727,6 +722,12 @@
}
void _handleDragCancel() {
+ if (_gestureDetectorKey.currentContext == null) {
+ // The cancel was caused by the GestureDetector getting disposed, which
+ // means we will get disposed momentarily as well and shouldn't do
+ // any work.
+ return;
+ }
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
@@ -755,12 +756,32 @@
);
}
- // Returns the delta that should result from applying [event] with axis and
- // direction taken into account.
+ // Returns the delta that should result from applying [event] with axis,
+ // direction, and any modifiers specified by the ScrollBehavior taken into
+ // account.
double _pointerSignalEventDelta(PointerScrollEvent event) {
- double delta = widget.axis == Axis.horizontal
- ? event.scrollDelta.dx
- : event.scrollDelta.dy;
+ late double delta;
+ final Set<LogicalKeyboardKey> pressed = HardwareKeyboard.instance.logicalKeysPressed;
+ final bool flipAxes = pressed.any(_configuration.pointerAxisModifiers.contains) &&
+ // Axes are only flipped for physical mouse wheel input.
+ // On some platforms, like web, trackpad input is handled through pointer
+ // signals, but should not be included in this axis modifying behavior.
+ // This is because on a trackpad, all directional axes are available to
+ // the user, while mouse scroll wheels typically are restricted to one
+ // axis.
+ event.kind == PointerDeviceKind.mouse;
+
+ switch (widget.axis) {
+ case Axis.horizontal:
+ delta = flipAxes
+ ? event.scrollDelta.dy
+ : event.scrollDelta.dx;
+ break;
+ case Axis.vertical:
+ delta = flipAxes
+ ? event.scrollDelta.dx
+ : event.scrollDelta.dy;
+ }
if (axisDirectionIsReversed(widget.axisDirection)) {
delta *= -1;
@@ -780,7 +801,7 @@
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
}
} else if (event is PointerScrollInertiaCancelEvent) {
- position.jumpTo(position.pixels);
+ position.pointerScroll(0);
// Don't use the pointer signal resolver, all hit-tested scrollables should stop.
}
}
@@ -1546,8 +1567,7 @@
required this.allowImplicitScrolling,
required this.semanticChildCount,
super.child,
- }) : assert(position != null),
- assert(semanticChildCount == null || semanticChildCount >= 0);
+ }) : assert(semanticChildCount == null || semanticChildCount >= 0);
final ScrollPosition position;
final bool allowImplicitScrolling;
@@ -1580,7 +1600,6 @@
}) : _position = position,
_allowImplicitScrolling = allowImplicitScrolling,
_semanticChildCount = semanticChildCount,
- assert(position != null),
super(child) {
position.addListener(markNeedsSemanticsUpdate);
}
@@ -1589,7 +1608,6 @@
ScrollPosition get position => _position;
ScrollPosition _position;
set position(ScrollPosition value) {
- assert(value != null);
if (value == _position) {
return;
}
@@ -1726,8 +1744,7 @@
const ScrollIncrementDetails({
required this.type,
required this.metrics,
- }) : assert(type != null),
- assert(metrics != null);
+ });
/// The type of scroll this is (e.g. line, page, etc.).
///
@@ -1750,8 +1767,7 @@
const ScrollIntent({
required this.direction,
this.type = ScrollIncrementType.line,
- }) : assert(direction != null),
- assert(type != null);
+ });
/// The direction in which to scroll the scrollable containing the focused
/// widget.
@@ -1797,12 +1813,7 @@
/// null. The type and state arguments must not be null, and the widget must
/// have already been laid out so that the position fields are valid.
static double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) {
- assert(type != null);
- assert(state.position != null);
assert(state.position.hasPixels);
- assert(state.position.viewportDimension != null);
- assert(state.position.maxScrollExtent != null);
- assert(state.position.minScrollExtent != null);
assert(state._physics == null || state._physics!.shouldAcceptUserOffset(state.position));
if (state.widget.incrementCalculator != null) {
return state.widget.incrementCalculator!(
@@ -1904,9 +1915,6 @@
}
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
assert(state!.position.hasPixels, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
- assert(state!.position.viewportDimension != null);
- assert(state!.position.maxScrollExtent != null);
- assert(state!.position.minScrollExtent != null);
// Don't do anything if the user isn't allowed to scroll.
if (state!._physics != null && !state._physics!.shouldAcceptUserOffset(state.position)) {
diff --git a/framework/lib/src/widgets/scrollbar.dart b/framework/lib/src/widgets/scrollbar.dart
index a80c9f7..65ad29c 100644
--- a/framework/lib/src/widgets/scrollbar.dart
+++ b/framework/lib/src/widgets/scrollbar.dart
@@ -92,21 +92,11 @@
double? minOverscrollLength,
ScrollbarOrientation? scrollbarOrientation,
bool ignorePointer = false,
- }) : assert(color != null),
- assert(radius == null || shape == null),
- assert(thickness != null),
- assert(fadeoutOpacityAnimation != null),
- assert(mainAxisMargin != null),
- assert(crossAxisMargin != null),
- assert(minLength != null),
+ }) : assert(radius == null || shape == null),
assert(minLength >= 0),
assert(minOverscrollLength == null || minOverscrollLength <= minLength),
assert(minOverscrollLength == null || minOverscrollLength >= 0),
- assert(padding != null),
assert(padding.isNonNegative),
- assert(trackColor != null),
- assert(trackBorderColor != null),
- assert(ignorePointer != null),
_color = color,
_textDirection = textDirection,
_thickness = thickness,
@@ -129,7 +119,6 @@
Color get color => _color;
Color _color;
set color(Color value) {
- assert(value != null);
if (color == value) {
return;
}
@@ -142,7 +131,6 @@
Color get trackColor => _trackColor;
Color _trackColor;
set trackColor(Color value) {
- assert(value != null);
if (trackColor == value) {
return;
}
@@ -155,7 +143,6 @@
Color get trackBorderColor => _trackBorderColor;
Color _trackBorderColor;
set trackBorderColor(Color value) {
- assert(value != null);
if (trackBorderColor == value) {
return;
}
@@ -197,7 +184,6 @@
double get thickness => _thickness;
double _thickness;
set thickness(double value) {
- assert(value != null);
if (thickness == value) {
return;
}
@@ -220,7 +206,6 @@
double get mainAxisMargin => _mainAxisMargin;
double _mainAxisMargin;
set mainAxisMargin(double value) {
- assert(value != null);
if (mainAxisMargin == value) {
return;
}
@@ -238,7 +223,6 @@
double get crossAxisMargin => _crossAxisMargin;
double _crossAxisMargin;
set crossAxisMargin(double value) {
- assert(value != null);
if (crossAxisMargin == value) {
return;
}
@@ -296,7 +280,6 @@
EdgeInsets get padding => _padding;
EdgeInsets _padding;
set padding(EdgeInsets value) {
- assert(value != null);
if (padding == value) {
return;
}
@@ -319,7 +302,6 @@
double get minLength => _minLength;
double _minLength;
set minLength(double value) {
- assert(value != null);
if (minLength == value) {
return;
}
@@ -341,7 +323,6 @@
double get minOverscrollLength => _minOverscrollLength;
double _minOverscrollLength;
set minOverscrollLength(double value) {
- assert(value != null);
if (minOverscrollLength == value) {
return;
}
@@ -679,7 +660,6 @@
///
/// thumbOffsetLocal is a position in the thumb track. Cannot be null.
double getTrackToScroll(double thumbOffsetLocal) {
- assert(thumbOffsetLocal != null);
final double scrollableExtent = _lastMetrics!.maxScrollExtent - _lastMetrics!.minScrollExtent;
final double thumbMovableExtent = _traversableTrackExtent - _thumbExtent;
@@ -1012,8 +992,7 @@
'This feature was deprecated after v2.9.0-1.0.pre.',
)
this.isAlwaysShown,
- }) : assert(child != null),
- assert(
+ }) : assert(
thumbVisibility == null || isAlwaysShown == null,
'Scrollbar thumb appearance should only be controlled with thumbVisibility, '
'isAlwaysShown is deprecated.'
@@ -1022,16 +1001,10 @@
!((thumbVisibility == false || isAlwaysShown == false) && (trackVisibility ?? false)),
'A scrollbar track cannot be drawn without a scrollbar thumb.'
),
- assert(minThumbLength != null),
assert(minThumbLength >= 0),
assert(minOverscrollLength == null || minOverscrollLength <= minThumbLength),
assert(minOverscrollLength == null || minOverscrollLength >= 0),
- assert(fadeDuration != null),
- assert(radius == null || shape == null),
- assert(timeToFade != null),
- assert(pressDuration != null),
- assert(mainAxisMargin != null),
- assert(crossAxisMargin != null);
+ assert(radius == null || shape == null);
/// {@template flutter.widgets.Scrollbar.child}
/// The widget below this widget in the tree.
@@ -1675,7 +1648,7 @@
..textDirection = Directionality.of(context)
..thickness = widget.thickness ?? _kScrollbarThickness
..radius = widget.radius
- ..padding = widget.padding ?? MediaQuery.of(context).padding
+ ..padding = widget.padding ?? MediaQuery.paddingOf(context)
..scrollbarOrientation = widget.scrollbarOrientation
..mainAxisMargin = widget.mainAxisMargin
..shape = widget.shape
@@ -2136,7 +2109,7 @@
_cachedController!.hasClients &&
(!_thumbDragging || kIsWeb)) {
final ScrollPosition position = _cachedController!.position;
- if (event is PointerScrollEvent && position != null) {
+ if (event is PointerScrollEvent) {
if (!position.physics.shouldAcceptUserOffset(position)) {
return;
}
diff --git a/framework/lib/src/widgets/selectable_region.dart b/framework/lib/src/widgets/selectable_region.dart
index 289e9bc..fb99008 100644
--- a/framework/lib/src/widgets/selectable_region.dart
+++ b/framework/lib/src/widgets/selectable_region.dart
@@ -350,7 +350,7 @@
}
// Hide the text selection toolbar on mobile when orientation changes.
- final Orientation orientation = MediaQuery.of(context).orientation;
+ final Orientation orientation = MediaQuery.orientationOf(context);
if (_lastOrientation == null) {
_lastOrientation = orientation;
return;
@@ -506,7 +506,7 @@
/// is not pending or users end their gestures.
void _triggerSelectionEndEdgeUpdate() {
// This method can be called when the drag is not in progress. This can
- // happen if the the child scrollable returns SelectionResult.pending, and
+ // happen if the child scrollable returns SelectionResult.pending, and
// the selection area scheduled a selection update for the next frame, but
// the drag is lifted before the scheduled selection update is run.
if (_scheduledSelectionEndEdgeUpdate || !_userDraggingSelectionEnd) {
@@ -559,7 +559,7 @@
/// is not pending or users end their gestures.
void _triggerSelectionStartEdgeUpdate() {
// This method can be called when the drag is not in progress. This can
- // happen if the the child scrollable returns SelectionResult.pending, and
+ // happen if the child scrollable returns SelectionResult.pending, and
// the selection area scheduled a selection update for the next frame, but
// the drag is lifted before the scheduled selection update is run.
if (_scheduledSelectionStartEdgeUpdate || !_userDraggingSelectionStart) {
@@ -1771,11 +1771,11 @@
LayerLink? effectiveStartHandle = _startHandleLayer;
LayerLink? effectiveEndHandle = _endHandleLayer;
if (effectiveStartHandle != null || effectiveEndHandle != null) {
- final Rect drawableArea = Rect
+ final Rect? drawableArea = hasSize ? Rect
.fromLTWH(0, 0, containerSize.width, containerSize.height)
- .inflate(_kSelectionHandleDrawableAreaPadding);
- final bool hideStartHandle = value.startSelectionPoint == null || !drawableArea.contains(value.startSelectionPoint!.localPosition);
- final bool hideEndHandle = value.endSelectionPoint == null || !drawableArea.contains(value.endSelectionPoint!.localPosition);
+ .inflate(_kSelectionHandleDrawableAreaPadding) : null;
+ final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || !drawableArea.contains(value.startSelectionPoint!.localPosition);
+ final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null|| !drawableArea.contains(value.endSelectionPoint!.localPosition);
effectiveStartHandle = hideStartHandle ? null : _startHandleLayer;
effectiveEndHandle = hideEndHandle ? null : _endHandleLayer;
}
diff --git a/framework/lib/src/widgets/selection_container.dart b/framework/lib/src/widgets/selection_container.dart
index 4114690..8714607 100644
--- a/framework/lib/src/widgets/selection_container.dart
+++ b/framework/lib/src/widgets/selection_container.dart
@@ -48,8 +48,7 @@
this.registrar,
required SelectionContainerDelegate this.delegate,
required this.child,
- }) : assert(delegate != null),
- assert(child != null);
+ });
/// Creates a selection container that disables selection for the
/// subtree.
@@ -240,7 +239,7 @@
super.key,
required SelectionRegistrar this.registrar,
required super.child,
- }) : assert(registrar != null);
+ });
/// Creates a selection registrar scope that disables selection for the
/// subtree.
@@ -299,12 +298,26 @@
return box.getTransformTo(ancestor);
}
+ /// Whether the [SelectionContainer] has undergone layout and has a size.
+ ///
+ /// See also:
+ ///
+ /// * [RenderBox.hasSize], which is used internally by this method.
+ bool get hasSize {
+ assert(
+ _selectionContainerContext?.findRenderObject() != null,
+ 'The _selectionContainerContext must have a renderObject, such as after the first build has completed.',
+ );
+ final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
+ return box.hasSize;
+ }
+
/// Gets the size of the [SelectionContainer] of this delegate.
///
/// Can only be called after [SelectionContainer] is laid out.
Size get containerSize {
assert(
- _selectionContainerContext?.findRenderObject() != null,
+ hasSize,
'containerSize cannot be called before SelectionContainer is laid out.',
);
final RenderBox box = _selectionContainerContext!.findRenderObject()! as RenderBox;
diff --git a/framework/lib/src/widgets/semantics_debugger.dart b/framework/lib/src/widgets/semantics_debugger.dart
index e1a1200..a5c8552 100644
--- a/framework/lib/src/widgets/semantics_debugger.dart
+++ b/framework/lib/src/widgets/semantics_debugger.dart
@@ -12,6 +12,7 @@
import 'binding.dart';
import 'framework.dart';
import 'gesture_detector.dart';
+import 'view.dart';
/// A widget that visualizes the semantics for the child.
///
@@ -31,8 +32,7 @@
fontSize: 10.0,
height: 0.8,
),
- }) : assert(child != null),
- assert(labelStyle != null);
+ });
/// The widget below this widget in the tree.
///
@@ -96,7 +96,7 @@
Offset? _lastPointerDownLocation;
void _handlePointerDown(PointerDownEvent event) {
setState(() {
- _lastPointerDownLocation = event.position * WidgetsBinding.instance.window.devicePixelRatio;
+ _lastPointerDownLocation = event.position * View.of(context).devicePixelRatio;
});
// TODO(ianh): Use a gesture recognizer so that we can reset the
// _lastPointerDownLocation when none of the other gesture recognizers win.
@@ -159,7 +159,7 @@
_pipelineOwner,
_client.generation,
_lastPointerDownLocation, // in physical pixels
- WidgetsBinding.instance.window.devicePixelRatio,
+ View.of(context).devicePixelRatio,
widget.labelStyle,
),
child: GestureDetector(
@@ -286,12 +286,15 @@
annotations.add('adjustable');
}
- assert(data.attributedLabel != null);
final String message;
+ // Android will avoid pronouncing duplicating tooltip and label.
+ // Therefore, having two identical strings is the same as having a single
+ // string.
+ final bool shouldIgnoreDuplicatedLabel = defaultTargetPlatform == TargetPlatform.android && data.attributedLabel.string == data.tooltip;
final String tooltipAndLabel = <String>[
if (data.tooltip.isNotEmpty)
data.tooltip,
- if (data.attributedLabel.string.isNotEmpty)
+ if (data.attributedLabel.string.isNotEmpty && !shouldIgnoreDuplicatedLabel)
data.attributedLabel.string,
].join('\n');
if (tooltipAndLabel.isEmpty) {
diff --git a/framework/lib/src/widgets/shortcuts.dart b/framework/lib/src/widgets/shortcuts.dart
index 37cc234..b4f461d 100644
--- a/framework/lib/src/widgets/shortcuts.dart
+++ b/framework/lib/src/widgets/shortcuts.dart
@@ -39,8 +39,7 @@
T? key2,
T? key3,
T? key4,
- ]) : assert(key1 != null),
- _keys = HashSet<T>()..add(key1) {
+ ]) : _keys = HashSet<T>()..add(key1) {
int count = 1;
if (key2 != null) {
_keys.add(key2);
@@ -72,8 +71,7 @@
///
/// The `keys` set must not be empty.
KeySet.fromSet(Set<T> keys)
- : assert(keys != null),
- assert(keys.isNotEmpty),
+ : assert(keys.isNotEmpty),
assert(!keys.contains(null)),
_keys = HashSet<T>.of(keys);
@@ -90,7 +88,6 @@
&& setEquals<T>(other._keys, _keys);
}
-
// Cached hash code value. Improves [hashCode] performance by 27%-900%,
// depending on key set size and read/write ratio.
@override
@@ -334,8 +331,8 @@
}
}
-/// A [DiagnosticsProperty] which handles formatting a `Map<LogicalKeySet,
-/// Intent>` (the same type as the [Shortcuts.shortcuts] property) so that its
+/// A [DiagnosticsProperty] which handles formatting a `Map<LogicalKeySet, Intent>`
+/// (the same type as the [Shortcuts.shortcuts] property) so that its
/// diagnostic output is human-readable.
class ShortcutMapProperty extends DiagnosticsProperty<Map<ShortcutActivator, Intent>> {
/// Create a diagnostics property for `Map<ShortcutActivator, Intent>` objects,
@@ -347,8 +344,7 @@
Object super.defaultValue,
super.level,
super.description,
- }) : assert(showName != null),
- assert(level != null);
+ });
@override
Map<ShortcutActivator, Intent> get value => super.value!;
@@ -751,8 +747,7 @@
ShortcutManager({
Map<ShortcutActivator, Intent> shortcuts = const <ShortcutActivator, Intent>{},
this.modal = false,
- }) : assert(shortcuts != null),
- _shortcuts = shortcuts;
+ }) : _shortcuts = shortcuts;
/// True if the [ShortcutManager] should not pass on keys that it doesn't
/// handle to any key-handling widgets that are ancestors to this one.
@@ -775,7 +770,6 @@
Map<ShortcutActivator, Intent> get shortcuts => _shortcuts;
Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{};
set shortcuts(Map<ShortcutActivator, Intent> value) {
- assert(value != null);
if (!mapEquals<ShortcutActivator, Intent>(_shortcuts, value)) {
_shortcuts = value;
_indexedShortcutsCache = null;
@@ -841,7 +835,6 @@
/// must be mapped to an [Action], and the [Action] must be enabled.
@protected
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
- assert(context != null);
final Intent? matchedIntent = _find(event, RawKeyboard.instance);
if (matchedIntent != null) {
final BuildContext? primaryContext = primaryFocus?.context;
@@ -851,8 +844,8 @@
intent: matchedIntent,
);
if (action != null && action.isEnabled(matchedIntent)) {
- final Object? actionResult = Actions.of(primaryContext).invokeAction(action, matchedIntent, primaryContext);
- return action.toKeyEventResult(matchedIntent, actionResult);
+ final Object? invokeResult = Actions.of(primaryContext).invokeAction(action, matchedIntent, primaryContext);
+ return action.toKeyEventResult(matchedIntent, invokeResult);
}
}
}
@@ -934,9 +927,7 @@
required this.child,
this.debugLabel,
}) : _shortcuts = shortcuts,
- manager = null,
- assert(shortcuts != null),
- assert(child != null);
+ manager = null;
/// Creates a const [Shortcuts] widget that uses the [manager] to
/// manage the map of shortcuts.
@@ -950,9 +941,7 @@
required ShortcutManager this.manager,
required this.child,
this.debugLabel,
- }) : _shortcuts = const <ShortcutActivator, Intent>{},
- assert(manager != null),
- assert(child != null);
+ }) : _shortcuts = const <ShortcutActivator, Intent>{};
/// The [ShortcutManager] that will manage the mapping between key
/// combinations and [Action]s.
@@ -1183,7 +1172,7 @@
///
/// The registry may be listened to (with [addListener]/[removeListener]) for
/// change notifications when the registered shortcuts change. Change
-/// notifications take place after the the current frame is drawn, so that
+/// notifications take place after the current frame is drawn, so that
/// widgets that are not descendants of the registry can listen to it (e.g. in
/// overlays).
class ShortcutRegistry with ChangeNotifier {
@@ -1276,9 +1265,8 @@
/// * [maybeOf], which is similar to this function, but will return null if
/// it doesn't find a [ShortcutRegistrar] ancestor.
static ShortcutRegistry of(BuildContext context) {
- assert(context != null);
- final _ShortcutRegistrarMarker? inherited =
- context.dependOnInheritedWidgetOfExactType<_ShortcutRegistrarMarker>();
+ final _ShortcutRegistrarScope? inherited =
+ context.dependOnInheritedWidgetOfExactType<_ShortcutRegistrarScope>();
assert(() {
if (inherited == null) {
throw FlutterError(
@@ -1312,9 +1300,8 @@
/// result, and will throw an exception if it doesn't find a
/// [ShortcutRegistrar] ancestor.
static ShortcutRegistry? maybeOf(BuildContext context) {
- assert(context != null);
- final _ShortcutRegistrarMarker? inherited =
- context.dependOnInheritedWidgetOfExactType<_ShortcutRegistrarMarker>();
+ final _ShortcutRegistrarScope? inherited =
+ context.dependOnInheritedWidgetOfExactType<_ShortcutRegistrarScope>();
return inherited?.registry;
}
@@ -1431,7 +1418,7 @@
@override
Widget build(BuildContext context) {
- return _ShortcutRegistrarMarker(
+ return _ShortcutRegistrarScope(
registry: registry,
child: Shortcuts.manager(
manager: manager,
@@ -1441,8 +1428,8 @@
}
}
-class _ShortcutRegistrarMarker extends InheritedWidget {
- const _ShortcutRegistrarMarker({
+class _ShortcutRegistrarScope extends InheritedWidget {
+ const _ShortcutRegistrarScope({
required this.registry,
required super.child,
});
@@ -1450,7 +1437,7 @@
final ShortcutRegistry registry;
@override
- bool updateShouldNotify(covariant _ShortcutRegistrarMarker oldWidget) {
+ bool updateShouldNotify(covariant _ShortcutRegistrarScope oldWidget) {
return registry != oldWidget.registry;
}
}
diff --git a/framework/lib/src/widgets/single_child_scroll_view.dart b/framework/lib/src/widgets/single_child_scroll_view.dart
index 1d4eb04..68f4aa6 100644
--- a/framework/lib/src/widgets/single_child_scroll_view.dart
+++ b/framework/lib/src/widgets/single_child_scroll_view.dart
@@ -48,17 +48,18 @@
/// small window in split-screen mode. In any case, as a result, it might
/// make sense to wrap the layout in a [SingleChildScrollView].
///
-/// Simply doing so, however, usually results in a conflict between the [Column],
+/// Doing so, however, usually results in a conflict between the [Column],
/// which typically tries to grow as big as it can, and the [SingleChildScrollView],
/// which provides its children with an infinite amount of space.
///
/// To resolve this apparent conflict, there are a couple of techniques, as
/// discussed below. These techniques should only be used when the content is
-/// normally expected to fit on the screen, so that the lazy instantiation of
-/// a sliver-based [ListView] or [CustomScrollView] is not expected to provide
-/// any performance benefit. If the viewport is expected to usually contain
-/// content beyond the dimensions of the screen, then [SingleChildScrollView]
-/// would be very expensive.
+/// normally expected to fit on the screen, so that the lazy instantiation of a
+/// sliver-based [ListView] or [CustomScrollView] is not expected to provide any
+/// performance benefit. If the viewport is expected to usually contain content
+/// beyond the dimensions of the screen, then [SingleChildScrollView] would be
+/// very expensive (in which case [ListView] may be a better choice than
+/// [Column]).
///
/// ### Centering, spacing, or aligning fixed-height content
///
@@ -150,10 +151,7 @@
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
- }) : assert(scrollDirection != null),
- assert(dragStartBehavior != null),
- assert(clipBehavior != null),
- assert(
+ }) : assert(
!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance '
'from a PrimaryScrollController widget. You cannot both set primary to '
@@ -287,8 +285,7 @@
required this.offset,
super.child,
required this.clipBehavior,
- }) : assert(axisDirection != null),
- assert(clipBehavior != null);
+ });
final AxisDirection axisDirection;
final ViewportOffset offset;
@@ -328,10 +325,7 @@
required ViewportOffset offset,
RenderBox? child,
required Clip clipBehavior,
- }) : assert(axisDirection != null),
- assert(offset != null),
- assert(clipBehavior != null),
- _axisDirection = axisDirection,
+ }) : _axisDirection = axisDirection,
_offset = offset,
_clipBehavior = clipBehavior {
this.child = child;
@@ -340,7 +334,6 @@
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
- assert(value != null);
if (value == _axisDirection) {
return;
}
@@ -353,7 +346,6 @@
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
- assert(value != null);
if (value == _offset) {
return;
}
@@ -373,7 +365,6 @@
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
- assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
@@ -510,7 +501,6 @@
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
return Offset(0.0, position - child!.size.height + size.height);
@@ -616,7 +606,6 @@
final double targetMainAxisExtent;
final double mainAxisExtent;
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
mainAxisExtent = size.height;
@@ -684,7 +673,6 @@
@override
Rect describeSemanticsClip(RenderObject child) {
- assert(axis != null);
final double remainingOffset = _maxScrollExtent - offset.pixels;
switch (axisDirection) {
case AxisDirection.up:
diff --git a/framework/lib/src/widgets/size_changed_layout_notifier.dart b/framework/lib/src/widgets/size_changed_layout_notifier.dart
index 916a06d..2947c99 100644
--- a/framework/lib/src/widgets/size_changed_layout_notifier.dart
+++ b/framework/lib/src/widgets/size_changed_layout_notifier.dart
@@ -74,8 +74,7 @@
_RenderSizeChangedWithCallback({
RenderBox? child,
required this.onLayoutChangedCallback,
- }) : assert(onLayoutChangedCallback != null),
- super(child);
+ }) : super(child);
// There's a 1:1 relationship between the _RenderSizeChangedWithCallback and
// the `context` that is captured by the closure created by createRenderObject
diff --git a/framework/lib/src/widgets/sliver.dart b/framework/lib/src/widgets/sliver.dart
index e5c595f..eafff4b 100644
--- a/framework/lib/src/widgets/sliver.dart
+++ b/framework/lib/src/widgets/sliver.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:collection' show HashMap, SplayTreeMap;
+import 'dart:math' as math;
import 'package:flute/foundation.dart';
import 'package:flute/rendering.dart';
@@ -227,7 +228,7 @@
}
class _SaltedValueKey extends ValueKey<Key> {
- const _SaltedValueKey(super.key): assert(key != null);
+ const _SaltedValueKey(super.key);
}
/// Called to find the new index of a child based on its `key` in case of
@@ -363,11 +364,7 @@
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
- }) : assert(builder != null),
- assert(addAutomaticKeepAlives != null),
- assert(addRepaintBoundaries != null),
- assert(addSemanticIndexes != null),
- assert(semanticIndexCallback != null);
+ });
/// Called to build children for the sliver.
///
@@ -417,7 +414,7 @@
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
- /// and simply repaint the children during scrolling.
+ /// and instead always repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
@@ -464,7 +461,6 @@
if (findChildIndexCallback == null) {
return null;
}
- assert(key != null);
final Key childKey;
if (key is _SaltedValueKey) {
final _SaltedValueKey saltedValueKey = key;
@@ -478,7 +474,6 @@
@override
@pragma('vm:notify-debugger-on-exception')
Widget? build(BuildContext context, int index) {
- assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount!)) {
return null;
}
@@ -582,12 +577,7 @@
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
- }) : assert(children != null),
- assert(addAutomaticKeepAlives != null),
- assert(addRepaintBoundaries != null),
- assert(addSemanticIndexes != null),
- assert(semanticIndexCallback != null),
- _keyToIndex = <Key?, int>{null: 0};
+ }) : _keyToIndex = <Key?, int>{null: 0};
/// Creates a constant version of the delegate that supplies children for
/// slivers using the given list.
@@ -605,12 +595,7 @@
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
- }) : assert(children != null),
- assert(addAutomaticKeepAlives != null),
- assert(addRepaintBoundaries != null),
- assert(addSemanticIndexes != null),
- assert(semanticIndexCallback != null),
- _keyToIndex = null;
+ }) : _keyToIndex = null;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
@@ -632,7 +617,7 @@
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
- /// and simply repaint the children during scrolling.
+ /// and instead always repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
@@ -734,7 +719,6 @@
@override
int? findIndexByKey(Key key) {
- assert(key != null);
final Key childKey;
if (key is _SaltedValueKey) {
final _SaltedValueKey saltedValueKey = key;
@@ -747,16 +731,11 @@
@override
Widget? build(BuildContext context, int index) {
- assert(children != null);
if (index < 0 || index >= children.length) {
return null;
}
Widget child = children[index];
final Key? key = child.key != null? _SaltedValueKey(child.key!) : null;
- assert(
- child != null,
- "The sliver's children must not contain null values, but a null value was found at index $index",
- );
if (addRepaintBoundaries) {
child = RepaintBoundary(child: child);
}
@@ -930,7 +909,7 @@
const SliverMultiBoxAdaptorWidget({
super.key,
required this.delegate,
- }) : assert(delegate != null);
+ });
/// {@template flutter.widgets.SliverMultiBoxAdaptorWidget.delegate}
/// The delegate that provides the children for this widget.
@@ -1035,6 +1014,182 @@
required super.delegate,
});
+ /// A sliver that places multiple box children in a linear array along the main
+ /// axis.
+ ///
+ /// This constructor is appropriate for sliver lists with a large (or
+ /// infinite) number of children because the builder is called only for those
+ /// children that are actually visible.
+ ///
+ /// Providing a non-null `itemCount` improves the ability of the [SliverGrid]
+ /// to estimate the maximum scroll extent.
+ ///
+ /// `itemBuilder` will be called only with indices greater than or equal to
+ /// zero and less than `itemCount`.
+ ///
+ /// {@macro flutter.widgets.ListView.builder.itemBuilder}
+ ///
+ /// {@macro flutter.widgets.PageView.findChildIndexCallback}
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ ///
+ /// {@tool snippet}
+ /// This example, which would be inserted into a [CustomScrollView.slivers]
+ /// list, shows an infinite number of items in varying shades of blue:
+ ///
+ /// ```dart
+ /// SliverList.builder(
+ /// itemBuilder: (BuildContext context, int index) {
+ /// return Container(
+ /// alignment: Alignment.center,
+ /// color: Colors.lightBlue[100 * (index % 9)],
+ /// child: Text('list item $index'),
+ /// );
+ /// },
+ /// )
+ /// ```
+ /// {@end-tool}
+ SliverList.builder({
+ super.key,
+ required NullableIndexedWidgetBuilder itemBuilder,
+ ChildIndexGetter? findChildIndexCallback,
+ int? itemCount,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildBuilderDelegate(
+ itemBuilder,
+ findChildIndexCallback: findChildIndexCallback,
+ childCount: itemCount,
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ ));
+
+ /// A sliver that places multiple box children, separated by box widgets, in a linear array along the main
+ /// axis.
+ ///
+ /// This constructor is appropriate for sliver lists with a large (or
+ /// infinite) number of children because the builder is called only for those
+ /// children that are actually visible.
+ ///
+ /// Providing a non-null `itemCount` improves the ability of the [SliverGrid]
+ /// to estimate the maximum scroll extent.
+ ///
+ /// `itemBuilder` will be called only with indices greater than or equal to
+ /// zero and less than `itemCount`.
+ ///
+ /// {@macro flutter.widgets.ListView.builder.itemBuilder}
+ ///
+ /// {@macro flutter.widgets.PageView.findChildIndexCallback}
+ ///
+ ///
+ /// The `separatorBuilder` is similar to `itemBuilder`, except it is the widget
+ /// that gets placed between itemBuilder(context, index) and itemBuilder(context, index + 1).
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ /// {@tool snippet}
+ ///
+ /// This example shows how to create a [SliverList] whose [Container] items
+ /// are separated by [Divider]s.
+ ///
+ /// ```dart
+ /// SliverList.separated(
+ /// itemBuilder: (BuildContext context, int index) {
+ /// return Container(
+ /// alignment: Alignment.center,
+ /// color: Colors.lightBlue[100 * (index % 9)],
+ /// child: Text('list item $index'),
+ /// );
+ /// },
+ /// separatorBuilder: (BuildContext context, int index) => const Divider(),
+ /// )
+ /// ```
+ /// {@end-tool}
+ SliverList.separated({
+ super.key,
+ required NullableIndexedWidgetBuilder itemBuilder,
+ ChildIndexGetter? findChildIndexCallback,
+ required NullableIndexedWidgetBuilder separatorBuilder,
+ int? itemCount,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildBuilderDelegate(
+ (BuildContext context, int index) {
+ final int itemIndex = index ~/ 2;
+ final Widget? widget;
+ if (index.isEven) {
+ widget = itemBuilder(context, itemIndex);
+ } else {
+ widget = separatorBuilder(context, itemIndex);
+ assert(() {
+ if (widget == null) {
+ throw FlutterError('separatorBuilder cannot return null.');
+ }
+ return true;
+ }());
+ }
+ return widget;
+ },
+ findChildIndexCallback: findChildIndexCallback,
+ childCount: itemCount == null ? null : math.max(0, itemCount * 2 - 1),
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ semanticIndexCallback: (Widget _, int index) {
+ return index.isEven ? index ~/ 2 : null;
+ },
+ ));
+
+ /// A sliver that places multiple box children in a linear array along the main
+ /// axis.
+ ///
+ /// This constructor uses a list of [Widget]s to build the sliver.
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ ///
+ /// {@tool snippet}
+ /// This example, which would be inserted into a [CustomScrollView.slivers]
+ /// list, shows an infinite number of items in varying shades of blue:
+ ///
+ /// ```dart
+ /// SliverList.list(
+ /// children: const <Widget>[
+ /// Text('Hello'),
+ /// Text('World!'),
+ /// ],
+ /// );
+ /// ```
+ /// {@end-tool}
+ SliverList.list({
+ super.key,
+ required List<Widget> children,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildListDelegate(
+ children,
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ ));
+
@override
SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true);
@@ -1098,6 +1253,116 @@
required this.itemExtent,
});
+ /// A sliver that places multiple box children in a linear array along the main
+ /// axis.
+ ///
+ /// [SliverFixedExtentList] places its children in a linear array along the main
+ /// axis starting at offset zero and without gaps. Each child is forced to have
+ /// the [itemExtent] in the main axis and the
+ /// [SliverConstraints.crossAxisExtent] in the cross axis.
+ ///
+ /// This constructor is appropriate for sliver lists with a large (or
+ /// infinite) number of children whose extent is already determined.
+ ///
+ /// Providing a non-null `itemCount` improves the ability of the [SliverGrid]
+ /// to estimate the maximum scroll extent.
+ ///
+ /// `itemBuilder` will be called only with indices greater than or equal to
+ /// zero and less than `itemCount`.
+ ///
+ /// {@macro flutter.widgets.ListView.builder.itemBuilder}
+ ///
+ /// The `itemExtent` argument is the extent of each item.
+ ///
+ /// {@macro flutter.widgets.PageView.findChildIndexCallback}
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ /// {@tool snippet}
+ ///
+ /// This example, which would be inserted into a [CustomScrollView.slivers]
+ /// list, shows an infinite number of items in varying shades of blue:
+ ///
+ /// ```dart
+ /// SliverFixedExtentList.builder(
+ /// itemExtent: 50.0,
+ /// itemBuilder: (BuildContext context, int index) {
+ /// return Container(
+ /// alignment: Alignment.center,
+ /// color: Colors.lightBlue[100 * (index % 9)],
+ /// child: Text('list item $index'),
+ /// );
+ /// },
+ /// )
+ /// ```
+ /// {@end-tool}
+ SliverFixedExtentList.builder({
+ super.key,
+ required NullableIndexedWidgetBuilder itemBuilder,
+ required this.itemExtent,
+ ChildIndexGetter? findChildIndexCallback,
+ int? itemCount,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildBuilderDelegate(
+ itemBuilder,
+ findChildIndexCallback: findChildIndexCallback,
+ childCount: itemCount,
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ ));
+
+ /// A sliver that places multiple box children in a linear array along the main
+ /// axis.
+ ///
+ /// [SliverFixedExtentList] places its children in a linear array along the main
+ /// axis starting at offset zero and without gaps. Each child is forced to have
+ /// the [itemExtent] in the main axis and the
+ /// [SliverConstraints.crossAxisExtent] in the cross axis.
+ ///
+ /// This constructor uses a list of [Widget]s to build the sliver.
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ ///
+ /// {@tool snippet}
+ /// This example, which would be inserted into a [CustomScrollView.slivers]
+ /// list, shows an infinite number of items in varying shades of blue:
+ ///
+ /// ```dart
+ /// SliverFixedExtentList.list(
+ /// itemExtent: 50.0,
+ /// children: const <Widget>[
+ /// Text('Hello'),
+ /// Text('World!'),
+ /// ],
+ /// );
+ /// ```
+ /// {@end-tool}
+ SliverFixedExtentList.list({
+ super.key,
+ required List<Widget> children,
+ required this.itemExtent,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildListDelegate(
+ children,
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ ));
+
/// The extent the children are forced to have in the main axis.
final double itemExtent;
@@ -1203,8 +1468,7 @@
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
- }) : assert(gridDelegate != null),
- super(delegate: SliverChildBuilderDelegate(
+ }) : super(delegate: SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
@@ -1460,7 +1724,6 @@
@override
void forgetChild(Element child) {
- assert(child != null);
assert(child.slot != null);
assert(_childElements.containsKey(child.slot));
_childElements.remove(child.slot);
@@ -1621,7 +1884,6 @@
@override
void insertRenderObjectChild(covariant RenderObject child, int slot) {
- assert(slot != null);
assert(_currentlyUpdatingChildIndex == slot);
assert(renderObject.debugValidateChild(child));
renderObject.insert(child as RenderBox, after: _currentBeforeChild);
@@ -1634,7 +1896,6 @@
@override
void moveRenderObjectChild(covariant RenderObject child, int oldSlot, int newSlot) {
- assert(newSlot != null);
assert(_currentlyUpdatingChildIndex == newSlot);
renderObject.move(child as RenderBox, after: _currentBeforeChild);
}
@@ -1681,7 +1942,7 @@
///
/// For values of opacity other than 0.0 and 1.0, this class is relatively
/// expensive because it requires painting the sliver child into an intermediate
-/// buffer. For the value 0.0, the sliver child is simply not painted at all.
+/// buffer. For the value 0.0, the sliver child is not painted at all.
/// For the value 1.0, the sliver child is painted immediately without an
/// intermediate buffer.
///
@@ -1718,8 +1979,7 @@
required this.opacity,
this.alwaysIncludeSemantics = false,
Widget? sliver,
- }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
- assert(alwaysIncludeSemantics != null),
+ }) : assert(opacity >= 0.0 && opacity <= 1.0),
super(child: sliver);
/// The fraction to scale the sliver child's alpha value.
@@ -1791,8 +2051,7 @@
this.ignoring = true,
this.ignoringSemantics,
Widget? sliver,
- }) : assert(ignoring != null),
- super(child: sliver);
+ }) : super(child: sliver);
/// Whether this sliver is ignored during hit testing.
///
@@ -1848,8 +2107,7 @@
super.key,
this.offstage = true,
Widget? sliver,
- }) : assert(offstage != null),
- super(child: sliver);
+ }) : super(child: sliver);
/// Whether the sliver child is hidden from the rest of the tree.
///
@@ -1917,8 +2175,7 @@
super.key,
required this.keepAlive,
required super.child,
- }) : assert(child != null),
- assert(keepAlive != null);
+ });
/// Whether to keep the child alive.
///
diff --git a/framework/lib/src/widgets/sliver_fill.dart b/framework/lib/src/widgets/sliver_fill.dart
index 0334551..e7bef9e 100644
--- a/framework/lib/src/widgets/sliver_fill.dart
+++ b/framework/lib/src/widgets/sliver_fill.dart
@@ -30,9 +30,7 @@
required this.delegate,
this.viewportFraction = 1.0,
this.padEnds = true,
- }) : assert(viewportFraction != null),
- assert(viewportFraction > 0.0),
- assert(padEnds != null);
+ }) : assert(viewportFraction > 0.0);
/// The fraction of the viewport that each child should fill in the main axis.
///
@@ -73,8 +71,7 @@
const _SliverFillViewportRenderObjectWidget({
required super.delegate,
this.viewportFraction = 1.0,
- }) : assert(viewportFraction != null),
- assert(viewportFraction > 0.0);
+ }) : assert(viewportFraction > 0.0);
final double viewportFraction;
@@ -94,8 +91,7 @@
const _SliverFractionalPadding({
this.viewportFraction = 0,
Widget? sliver,
- }) : assert(viewportFraction != null),
- assert(viewportFraction >= 0),
+ }) : assert(viewportFraction >= 0),
assert(viewportFraction <= 0.5),
super(child: sliver);
@@ -113,8 +109,7 @@
class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding {
_RenderSliverFractionalPadding({
double viewportFraction = 0,
- }) : assert(viewportFraction != null),
- assert(viewportFraction <= 0.5),
+ }) : assert(viewportFraction <= 0.5),
assert(viewportFraction >= 0),
_viewportFraction = viewportFraction;
@@ -123,7 +118,6 @@
double get viewportFraction => _viewportFraction;
double _viewportFraction;
set viewportFraction(double newValue) {
- assert(newValue != null);
if (_viewportFraction == newValue) {
return;
}
@@ -145,7 +139,6 @@
return;
}
- assert(constraints.axis != null);
final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction;
_lastResolvedConstraints = constraints;
switch (constraints.axis) {
@@ -264,8 +257,7 @@
this.child,
this.hasScrollBody = true,
this.fillOverscroll = false,
- }) : assert(hasScrollBody != null),
- assert(fillOverscroll != null);
+ });
/// Box child widget that fills the remaining space in the viewport.
///
diff --git a/framework/lib/src/widgets/sliver_layout_builder.dart b/framework/lib/src/widgets/sliver_layout_builder.dart
index 358e841..3b1e78a 100644
--- a/framework/lib/src/widgets/sliver_layout_builder.dart
+++ b/framework/lib/src/widgets/sliver_layout_builder.dart
@@ -39,7 +39,6 @@
class _RenderSliverLayoutBuilder extends RenderSliver with RenderObjectWithChildMixin<RenderSliver>, RenderConstrainedLayoutBuilder<SliverConstraints, RenderSliver> {
@override
double childMainAxisPosition(RenderObject child) {
- assert(child != null);
assert(child == this.child);
return 0;
}
@@ -53,7 +52,6 @@
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
- assert(child != null);
assert(child == this.child);
// child's offset is always (0, 0), transform.translate(0, 0) does not mutate the transform.
}
diff --git a/framework/lib/src/widgets/sliver_persistent_header.dart b/framework/lib/src/widgets/sliver_persistent_header.dart
index ea39712..edd3aae 100644
--- a/framework/lib/src/widgets/sliver_persistent_header.dart
+++ b/framework/lib/src/widgets/sliver_persistent_header.dart
@@ -124,9 +124,7 @@
required this.delegate,
this.pinned = false,
this.floating = false,
- }) : assert(delegate != null),
- assert(pinned != null),
- assert(floating != null);
+ });
/// Configuration for the sliver's layout.
///
@@ -255,7 +253,7 @@
_SliverPersistentHeaderElement(
_SliverPersistentHeaderRenderObjectWidget super.widget, {
this.floating = false,
- }) : assert(floating != null);
+ });
final bool floating;
@@ -346,8 +344,7 @@
const _SliverPersistentHeaderRenderObjectWidget({
required this.delegate,
this.floating = false,
- }) : assert(delegate != null),
- assert(floating != null);
+ });
final SliverPersistentHeaderDelegate delegate;
final bool floating;
diff --git a/framework/lib/src/widgets/sliver_prototype_extent_list.dart b/framework/lib/src/widgets/sliver_prototype_extent_list.dart
index f5e3f33..c6d847c 100644
--- a/framework/lib/src/widgets/sliver_prototype_extent_list.dart
+++ b/framework/lib/src/widgets/sliver_prototype_extent_list.dart
@@ -38,7 +38,110 @@
super.key,
required super.delegate,
required this.prototypeItem,
- }) : assert(prototypeItem != null);
+ });
+
+ /// A sliver that places its box children in a linear array and constrains them
+ /// to have the same extent as a prototype item along the main axis.
+ ///
+ /// This constructor is appropriate for sliver lists with a large (or
+ /// infinite) number of children whose extent is already determined.
+ ///
+ /// Providing a non-null `itemCount` improves the ability of the [SliverGrid]
+ /// to estimate the maximum scroll extent.
+ ///
+ /// `itemBuilder` will be called only with indices greater than or equal to
+ /// zero and less than `itemCount`.
+ ///
+ /// {@macro flutter.widgets.ListView.builder.itemBuilder}
+ ///
+ /// The `prototypeItem` argument is used to determine the extent of each item.
+ ///
+ /// {@macro flutter.widgets.PageView.findChildIndexCallback}
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ ///
+ /// {@tool snippet}
+ /// This example, which would be inserted into a [CustomScrollView.slivers]
+ /// list, shows an infinite number of items in varying shades of blue:
+ ///
+ /// ```dart
+ /// SliverPrototypeExtentList.builder(
+ /// prototypeItem: Container(
+ /// alignment: Alignment.center,
+ /// child: const Text('list item prototype'),
+ /// ),
+ /// itemBuilder: (BuildContext context, int index) {
+ /// return Container(
+ /// alignment: Alignment.center,
+ /// color: Colors.lightBlue[100 * (index % 9)],
+ /// child: Text('list item $index'),
+ /// );
+ /// },
+ /// )
+ /// ```
+ /// {@end-tool}
+ SliverPrototypeExtentList.builder({
+ super.key,
+ required NullableIndexedWidgetBuilder itemBuilder,
+ required this.prototypeItem,
+ ChildIndexGetter? findChildIndexCallback,
+ int? itemCount,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildBuilderDelegate(
+ itemBuilder,
+ findChildIndexCallback: findChildIndexCallback,
+ childCount: itemCount,
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ ));
+
+ /// A sliver that places multiple box children in a linear array along the main
+ /// axis.
+ ///
+ /// This constructor uses a list of [Widget]s to build the sliver.
+ ///
+ /// The `addAutomaticKeepAlives` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
+ /// `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
+ /// `addSemanticIndexes` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
+ ///
+ /// {@tool snippet}
+ /// This example, which would be inserted into a [CustomScrollView.slivers]
+ /// list, shows an infinite number of items in varying shades of blue:
+ ///
+ /// ```dart
+ /// SliverPrototypeExtentList.list(
+ /// prototypeItem: const Text('Hello'),
+ /// children: const <Widget>[
+ /// Text('Hello'),
+ /// Text('World!'),
+ /// ],
+ /// );
+ /// ```
+ /// {@end-tool}
+ SliverPrototypeExtentList.list({
+ super.key,
+ required List<Widget> children,
+ required this.prototypeItem,
+ bool addAutomaticKeepAlives = true,
+ bool addRepaintBoundaries = true,
+ bool addSemanticIndexes = true,
+ }) : super(delegate: SliverChildListDelegate(
+ children,
+ addAutomaticKeepAlives: addAutomaticKeepAlives,
+ addRepaintBoundaries: addRepaintBoundaries,
+ addSemanticIndexes: addSemanticIndexes,
+ ));
/// Defines the main axis extent of all of this sliver's children.
///
diff --git a/framework/lib/src/widgets/slotted_render_object_widget.dart b/framework/lib/src/widgets/slotted_render_object_widget.dart
index 64c18a1..201ae45 100644
--- a/framework/lib/src/widgets/slotted_render_object_widget.dart
+++ b/framework/lib/src/widgets/slotted_render_object_widget.dart
@@ -116,7 +116,7 @@
/// currently occupied by a child to obtain a name for that slot for debug
/// outputs.
///
- /// The default implementation calls [EnumName.name] on `slot` it it is an
+ /// The default implementation calls [EnumName.name] on `slot` if it is an
/// [Enum] value and `toString` if it is not.
@protected
String debugNameForSlot(S slot) {
diff --git a/framework/lib/src/widgets/snapshot_widget.dart b/framework/lib/src/widgets/snapshot_widget.dart
index 2407df2..dfa4d3f 100644
--- a/framework/lib/src/widgets/snapshot_widget.dart
+++ b/framework/lib/src/widgets/snapshot_widget.dart
@@ -141,7 +141,7 @@
return _RenderSnapshotWidget(
controller: controller,
mode: mode,
- devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
+ devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
painter: painter,
autoresize: autoresize,
);
@@ -153,7 +153,7 @@
(renderObject as _RenderSnapshotWidget)
..controller = controller
..mode = mode
- ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
+ ..devicePixelRatio = MediaQuery.devicePixelRatioOf(context)
..painter = painter
..autoresize = autoresize;
}
diff --git a/framework/lib/src/widgets/spacer.dart b/framework/lib/src/widgets/spacer.dart
index 3f719a5..b84aac8 100644
--- a/framework/lib/src/widgets/spacer.dart
+++ b/framework/lib/src/widgets/spacer.dart
@@ -18,8 +18,8 @@
/// {@tool snippet}
///
/// ```dart
-/// Row(
-/// children: const <Widget>[
+/// const Row(
+/// children: <Widget>[
/// Text('Begin'),
/// Spacer(), // Defaults to a flex of one.
/// Text('Middle'),
@@ -43,8 +43,7 @@
///
/// The [flex] parameter may not be null or less than one.
const Spacer({super.key, this.flex = 1})
- : assert(flex != null),
- assert(flex > 0);
+ : assert(flex > 0);
/// The flex factor to use in determining how much space to take up.
///
diff --git a/framework/lib/src/widgets/spell_check.dart b/framework/lib/src/widgets/spell_check.dart
index 142746e..63e95b6 100644
--- a/framework/lib/src/widgets/spell_check.dart
+++ b/framework/lib/src/widgets/spell_check.dart
@@ -2,11 +2,13 @@
// 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/painting.dart';
import 'package:flute/services.dart'
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
+import 'editable_text.dart' show EditableTextContextMenuBuilder;
+import 'framework.dart' show immutable;
+
/// Controls how spell check is performed for text input.
///
/// This configuration determines the [SpellCheckService] used to fetch the
@@ -19,12 +21,14 @@
const SpellCheckConfiguration({
this.spellCheckService,
this.misspelledTextStyle,
+ this.spellCheckSuggestionsToolbarBuilder,
}) : _spellCheckEnabled = true;
/// Creates a configuration that disables spell check.
const SpellCheckConfiguration.disabled()
: _spellCheckEnabled = false,
spellCheckService = null,
+ spellCheckSuggestionsToolbarBuilder = null,
misspelledTextStyle = null;
/// The service used to fetch spell check results for text input.
@@ -38,6 +42,10 @@
/// assertion error.
final TextStyle? misspelledTextStyle;
+ /// Builds the toolbar used to display spell check suggestions for misspelled
+ /// words.
+ final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
+
final bool _spellCheckEnabled;
/// Whether or not the configuration should enable or disable spell check.
@@ -47,7 +55,8 @@
/// specified overrides.
SpellCheckConfiguration copyWith({
SpellCheckService? spellCheckService,
- TextStyle? misspelledTextStyle}) {
+ TextStyle? misspelledTextStyle,
+ EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder}) {
if (!_spellCheckEnabled) {
// A new configuration should be constructed to enable spell check.
return const SpellCheckConfiguration.disabled();
@@ -56,6 +65,7 @@
return SpellCheckConfiguration(
spellCheckService: spellCheckService ?? this.spellCheckService,
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
+ spellCheckSuggestionsToolbarBuilder : spellCheckSuggestionsToolbarBuilder ?? this.spellCheckSuggestionsToolbarBuilder,
);
}
@@ -65,6 +75,7 @@
spell check enabled : $_spellCheckEnabled
spell check service : $spellCheckService
misspelled text style : $misspelledTextStyle
+ spell check suggesstions toolbar builder: $spellCheckSuggestionsToolbarBuilder
'''
.trim();
}
@@ -78,11 +89,12 @@
return other is SpellCheckConfiguration
&& other.spellCheckService == spellCheckService
&& other.misspelledTextStyle == misspelledTextStyle
+ && other.spellCheckSuggestionsToolbarBuilder == spellCheckSuggestionsToolbarBuilder
&& other._spellCheckEnabled == _spellCheckEnabled;
}
@override
- int get hashCode => Object.hash(spellCheckService, misspelledTextStyle, _spellCheckEnabled);
+ int get hashCode => Object.hash(spellCheckService, misspelledTextStyle, spellCheckSuggestionsToolbarBuilder, _spellCheckEnabled);
}
// Methods for displaying spell check results:
diff --git a/framework/lib/src/widgets/status_transitions.dart b/framework/lib/src/widgets/status_transitions.dart
index d2b2dde..b79bd0d 100644
--- a/framework/lib/src/widgets/status_transitions.dart
+++ b/framework/lib/src/widgets/status_transitions.dart
@@ -13,7 +13,7 @@
const StatusTransitionWidget({
super.key,
required this.animation,
- }) : assert(animation != null);
+ });
/// The animation to which this widget is listening.
final Animation<double> animation;
diff --git a/framework/lib/src/widgets/table.dart b/framework/lib/src/widgets/table.dart
index 1e4c214..8d81642 100644
--- a/framework/lib/src/widgets/table.dart
+++ b/framework/lib/src/widgets/table.dart
@@ -32,7 +32,7 @@
@immutable
class TableRow {
/// Creates a row in a [Table].
- const TableRow({ this.key, this.decoration, this.children });
+ const TableRow({ this.key, this.decoration, this.children = const <Widget>[]});
/// An identifier for the row.
final LocalKey? key;
@@ -49,7 +49,7 @@
/// Children may be wrapped in [TableCell] widgets to provide per-cell
/// configuration to the [Table], but children are not required to be wrapped
/// in [TableCell] widgets.
- final List<Widget>? children;
+ final List<Widget> children;
@override
String toString() {
@@ -61,9 +61,7 @@
if (decoration != null) {
result.write('$decoration, ');
}
- if (children == null) {
- result.write('child list is null');
- } else if (children!.isEmpty) {
+ if (children.isEmpty) {
result.write('no children');
} else {
result.write('$children');
@@ -126,28 +124,7 @@
this.border,
this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
- }) : assert(children != null),
- assert(defaultColumnWidth != null),
- assert(defaultVerticalAlignment != null),
- assert(defaultVerticalAlignment != TableCellVerticalAlignment.baseline || textBaseline != null, 'textBaseline is required if you specify the defaultVerticalAlignment with TableCellVerticalAlignment.baseline'),
- assert(() {
- if (children.any((TableRow row) => row.children == null)) {
- throw FlutterError(
- 'One of the rows of the table had null children.\n'
- 'The children property of TableRow must not be null.',
- );
- }
- return true;
- }()),
- assert(() {
- if (children.any((TableRow row) => row.children!.any((Widget cell) => cell == null))) {
- throw FlutterError(
- 'One of the children of one of the rows of the table was null.\n'
- 'The children of a TableRow must not be null.',
- );
- }
- return true;
- }()),
+ }) : assert(defaultVerticalAlignment != TableCellVerticalAlignment.baseline || textBaseline != null, 'textBaseline is required if you specify the defaultVerticalAlignment with TableCellVerticalAlignment.baseline'),
assert(() {
if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) {
throw FlutterError(
@@ -159,8 +136,8 @@
}()),
assert(() {
if (children.isNotEmpty) {
- final int cellCount = children.first.children!.length;
- if (children.any((TableRow row) => row.children!.length != cellCount)) {
+ final int cellCount = children.first.children.length;
+ if (children.any((TableRow row) => row.children.length != cellCount)) {
throw FlutterError(
'Table contains irregular row lengths.\n'
'Every TableRow in a Table must have the same number of children, so that every cell is filled. '
@@ -174,7 +151,7 @@
? children.map<Decoration?>((TableRow row) => row.decoration).toList(growable: false)
: null {
assert(() {
- final List<Widget> flatChildren = children.expand<Widget>((TableRow row) => row.children!).toList(growable: false);
+ final List<Widget> flatChildren = children.expand<Widget>((TableRow row) => row.children).toList(growable: false);
return !debugChildrenHaveDuplicateKeys(this, flatChildren, message:
'Two or more cells in this Table contain widgets with the same key.\n'
'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are '
@@ -250,7 +227,7 @@
RenderTable createRenderObject(BuildContext context) {
assert(debugCheckHasDirectionality(context));
return RenderTable(
- columns: children.isNotEmpty ? children[0].children!.length : 0,
+ columns: children.isNotEmpty ? children[0].children.length : 0,
rows: children.length,
columnWidths: columnWidths,
defaultColumnWidth: defaultColumnWidth,
@@ -266,7 +243,7 @@
@override
void updateRenderObject(BuildContext context, RenderTable renderObject) {
assert(debugCheckHasDirectionality(context));
- assert(renderObject.columns == (children.isNotEmpty ? children[0].children!.length : 0));
+ assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0));
assert(renderObject.rows == children.length);
renderObject
..columnWidths = columnWidths
@@ -301,8 +278,7 @@
rowIndex += 1;
return _TableElementRow(
key: row.key,
- children: row.children!.map<Element>((Widget child) {
- assert(child != null);
+ children: row.children.map<Element>((Widget child) {
return inflateWidget(child, _TableSlot(columnIndex++, rowIndex));
}).toList(growable: false),
);
@@ -360,12 +336,12 @@
oldChildren = const <Element>[];
}
final List<_TableSlot> slots = List<_TableSlot>.generate(
- row.children!.length,
+ row.children.length,
(int columnIndex) => _TableSlot(columnIndex, rowIndex),
);
newChildren.add(_TableElementRow(
key: row.key,
- children: updateChildren(oldChildren, row.children!, forgottenChildren: _forgottenChildren, slots: slots),
+ children: updateChildren(oldChildren, row.children, forgottenChildren: _forgottenChildren, slots: slots),
));
}
while (oldUnkeyedRows.moveNext()) {
@@ -385,7 +361,6 @@
}
void _updateRenderObjectChildren() {
- assert(renderObject != null);
renderObject.setFlatChildren(
_children.isNotEmpty ? _children[0].children.length : 0,
_children.expand<RenderBox>((_TableElementRow row) {
diff --git a/framework/lib/src/widgets/tap_and_drag_gestures.dart b/framework/lib/src/widgets/tap_and_drag_gestures.dart
new file mode 100644
index 0000000..6e99d1d
--- /dev/null
+++ b/framework/lib/src/widgets/tap_and_drag_gestures.dart
@@ -0,0 +1,1292 @@
+// 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 'dart:async';
+
+import 'package:flute/foundation.dart';
+import 'package:flute/gestures.dart';
+import 'package:flute/services.dart' show HardwareKeyboard, LogicalKeyboardKey;
+
+double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) {
+ assert(originPosition != null);
+ final Offset offset = event.position - originPosition!.global;
+ return offset.distance;
+}
+
+// The possible states of a [TapAndDragGestureRecognizer].
+//
+// The recognizer advances from [ready] to [possible] when it starts tracking
+// a pointer in [TapAndDragGestureRecognizer.addAllowedPointer]. Where it advances
+// from there depends on the sequence of pointer events that is tracked by the
+// recognizer, following the initial [PointerDownEvent]:
+//
+// * If a [PointerUpEvent] has not been tracked, the recognizer stays in the [possible]
+// state as long as it continues to track a pointer.
+// * If a [PointerMoveEvent] is tracked that has moved a sufficient global distance
+// from the initial [PointerDownEvent] and it came before a [PointerUpEvent], then
+// when this recognizer wins the arena, it will move from the [possible] state to [accepted].
+// * If a [PointerUpEvent] is tracked before the pointer has moved a sufficient global
+// distance to be considered a drag, then this recognizer moves from the [possible]
+// state to [ready].
+// * If a [PointerCancelEvent] is tracked then this recognizer moves from its current
+// state to [ready].
+//
+// Once the recognizer has stopped tracking any remaining pointers, the recognizer
+// returns to the [ready] state.
+enum _DragState {
+ // The recognizer is ready to start recognizing a drag.
+ ready,
+
+ // The sequence of pointer events seen thus far is consistent with a drag but
+ // it has not been accepted definitively.
+ possible,
+
+ // The sequence of pointer events has been accepted definitively as a drag.
+ accepted,
+}
+
+/// {@macro flutter.gestures.tap.GestureTapDownCallback}
+///
+/// The consecutive tap count at the time the pointer contacted the
+/// screen is given by [TapDragDownDetails.consecutiveTapCount].
+///
+/// Used by [TapAndDragGestureRecognizer.onTapDown].
+typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details);
+
+/// Details for [GestureTapDragDownCallback], such as the number of
+/// consecutive taps.
+///
+/// See also:
+///
+/// * [TapAndDragGestureRecognizer], which passes this information to its
+/// [TapAndDragGestureRecognizer.onTapDown] callback.
+/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
+/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
+/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
+/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback].
+class TapDragDownDetails with Diagnosticable {
+ /// Creates details for a [GestureTapDragDownCallback].
+ ///
+ /// The [globalPosition], [localPosition], [consecutiveTapCount], and
+ /// [keysPressedOnDown] arguments must be provided and must not be null.
+ TapDragDownDetails({
+ required this.globalPosition,
+ required this.localPosition,
+ this.kind,
+ required this.consecutiveTapCount,
+ required this.keysPressedOnDown,
+ });
+
+ /// The global position at which the pointer contacted the screen.
+ final Offset globalPosition;
+
+ /// The local position at which the pointer contacted the screen.
+ final Offset localPosition;
+
+ /// The kind of the device that initiated the event.
+ final PointerDeviceKind? kind;
+
+ /// If this tap is in a series of taps, then this value represents
+ /// the number in the series this tap is.
+ final int consecutiveTapCount;
+
+ /// The keys that were pressed when the most recent [PointerDownEvent] occurred.
+ final Set<LogicalKeyboardKey> keysPressedOnDown;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition));
+ properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition));
+ properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind));
+ properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount));
+ properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keysPressedOnDown', keysPressedOnDown));
+ }
+}
+
+/// {@macro flutter.gestures.tap.GestureTapUpCallback}
+///
+/// The consecutive tap count at the time the pointer contacted the
+/// screen is given by [TapDragUpDetails.consecutiveTapCount].
+///
+/// Used by [TapAndDragGestureRecognizer.onTapUp].
+typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details);
+
+/// Details for [GestureTapDragUpCallback], such as the number of
+/// consecutive taps.
+///
+/// See also:
+///
+/// * [TapAndDragGestureRecognizer], which passes this information to its
+/// [TapAndDragGestureRecognizer.onTapUp] callback.
+/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
+/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
+/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
+/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback].
+class TapDragUpDetails with Diagnosticable {
+ /// Creates details for a [GestureTapDragUpCallback].
+ ///
+ /// The [kind], [globalPosition], [localPosition], [consecutiveTapCount], and
+ /// [keysPressedOnDown] arguments must be provided and must not be null.
+ TapDragUpDetails({
+ required this.kind,
+ required this.globalPosition,
+ required this.localPosition,
+ required this.consecutiveTapCount,
+ required this.keysPressedOnDown,
+ });
+
+ /// The global position at which the pointer contacted the screen.
+ final Offset globalPosition;
+
+ /// The local position at which the pointer contacted the screen.
+ final Offset localPosition;
+
+ /// The kind of the device that initiated the event.
+ final PointerDeviceKind kind;
+
+ /// If this tap is in a series of taps, then this value represents
+ /// the number in the series this tap is.
+ final int consecutiveTapCount;
+
+ /// The keys that were pressed when the most recent [PointerDownEvent] occurred.
+ final Set<LogicalKeyboardKey> keysPressedOnDown;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition));
+ properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition));
+ properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind));
+ properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount));
+ properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keysPressedOnDown', keysPressedOnDown));
+ }
+}
+
+/// {@macro flutter.gestures.dragdetails.GestureDragStartCallback}
+///
+/// The consecutive tap count at the time the pointer contacted the
+/// screen is given by [TapDragStartDetails.consecutiveTapCount].
+///
+/// Used by [TapAndDragGestureRecognizer.onDragStart].
+typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details);
+
+/// Details for [GestureTapDragStartCallback], such as the number of
+/// consecutive taps.
+///
+/// See also:
+///
+/// * [TapAndDragGestureRecognizer], which passes this information to its
+/// [TapAndDragGestureRecognizer.onDragStart] callback.
+/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
+/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
+/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
+/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback].
+class TapDragStartDetails with Diagnosticable {
+ /// Creates details for a [GestureTapDragStartCallback].
+ ///
+ /// The [globalPosition], [localPosition], [consecutiveTapCount], and
+ /// [keysPressedOnDown] arguments must be provided and must not be null.
+ TapDragStartDetails({
+ this.sourceTimeStamp,
+ required this.globalPosition,
+ required this.localPosition,
+ this.kind,
+ required this.consecutiveTapCount,
+ required this.keysPressedOnDown,
+ });
+
+ /// Recorded timestamp of the source pointer event that triggered the drag
+ /// event.
+ ///
+ /// Could be null if triggered from proxied events such as accessibility.
+ final Duration? sourceTimeStamp;
+
+ /// The global position at which the pointer contacted the screen.
+ ///
+ /// See also:
+ ///
+ /// * [localPosition], which is the [globalPosition] transformed to the
+ /// coordinate space of the event receiver.
+ final Offset globalPosition;
+
+ /// The local position in the coordinate system of the event receiver at
+ /// which the pointer contacted the screen.
+ final Offset localPosition;
+
+ /// The kind of the device that initiated the event.
+ final PointerDeviceKind? kind;
+
+ /// If this tap is in a series of taps, then this value represents
+ /// the number in the series this tap is.
+ final int consecutiveTapCount;
+
+ /// The keys that were pressed when the most recent [PointerDownEvent] occurred.
+ final Set<LogicalKeyboardKey> keysPressedOnDown;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Duration?>('sourceTimeStamp', sourceTimeStamp));
+ properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition));
+ properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition));
+ properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind));
+ properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount));
+ properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keysPressedOnDown', keysPressedOnDown));
+ }
+}
+
+/// {@macro flutter.gestures.dragdetails.GestureDragUpdateCallback}
+///
+/// The consecutive tap count at the time the pointer contacted the
+/// screen is given by [TapDragUpdateDetails.consecutiveTapCount].
+///
+/// Used by [TapAndDragGestureRecognizer.onDragUpdate].
+typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails details);
+
+/// Details for [GestureTapDragUpdateCallback], such as the number of
+/// consecutive taps.
+///
+/// See also:
+///
+/// * [TapAndDragGestureRecognizer], which passes this information to its
+/// [TapAndDragGestureRecognizer.onDragUpdate] callback.
+/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
+/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
+/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
+/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback].
+class TapDragUpdateDetails with Diagnosticable {
+ /// Creates details for a [GestureTapDragUpdateCallback].
+ ///
+ /// The [delta] argument must not be null.
+ ///
+ /// If [primaryDelta] is non-null, then its value must match one of the
+ /// coordinates of [delta] and the other coordinate must be zero.
+ ///
+ /// The [globalPosition], [localPosition], [offsetFromOrigin], [localOffsetFromOrigin],
+ /// [consecutiveTapCount], and [keysPressedOnDown] arguments must be provided and must
+ /// not be null.
+ TapDragUpdateDetails({
+ this.sourceTimeStamp,
+ this.delta = Offset.zero,
+ this.primaryDelta,
+ required this.globalPosition,
+ this.kind,
+ required this.localPosition,
+ required this.offsetFromOrigin,
+ required this.localOffsetFromOrigin,
+ required this.consecutiveTapCount,
+ required this.keysPressedOnDown,
+ }) : assert(
+ primaryDelta == null
+ || (primaryDelta == delta.dx && delta.dy == 0.0)
+ || (primaryDelta == delta.dy && delta.dx == 0.0),
+ );
+
+ /// Recorded timestamp of the source pointer event that triggered the drag
+ /// event.
+ ///
+ /// Could be null if triggered from proxied events such as accessibility.
+ final Duration? sourceTimeStamp;
+
+ /// The amount the pointer has moved in the coordinate space of the event
+ /// receiver since the previous update.
+ ///
+ /// If the [GestureTapDragUpdateCallback] is for a one-dimensional drag (e.g.,
+ /// a horizontal or vertical drag), then this offset contains only the delta
+ /// in that direction (i.e., the coordinate in the other direction is zero).
+ ///
+ /// Defaults to zero if not specified in the constructor.
+ final Offset delta;
+
+ /// The amount the pointer has moved along the primary axis in the coordinate
+ /// space of the event receiver since the previous
+ /// update.
+ ///
+ /// If the [GestureTapDragUpdateCallback] is for a one-dimensional drag (e.g.,
+ /// a horizontal or vertical drag), then this value contains the component of
+ /// [delta] along the primary axis (e.g., horizontal or vertical,
+ /// respectively). Otherwise, if the [GestureTapDragUpdateCallback] is for a
+ /// two-dimensional drag (e.g., a pan), then this value is null.
+ ///
+ /// Defaults to null if not specified in the constructor.
+ final double? primaryDelta;
+
+ /// The pointer's global position when it triggered this update.
+ ///
+ /// See also:
+ ///
+ /// * [localPosition], which is the [globalPosition] transformed to the
+ /// coordinate space of the event receiver.
+ final Offset globalPosition;
+
+ /// The local position in the coordinate system of the event receiver at
+ /// which the pointer contacted the screen.
+ ///
+ /// Defaults to [globalPosition] if not specified in the constructor.
+ final Offset localPosition;
+
+ /// The kind of the device that initiated the event.
+ final PointerDeviceKind? kind;
+
+ /// A delta offset from the point where the drag initially contacted
+ /// the screen to the point where the pointer is currently located in global
+ /// 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.
+ final Offset offsetFromOrigin;
+
+ /// A local delta offset from the point where the drag initially contacted
+ /// the screen to the point where the pointer is currently located in local
+ /// 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.
+ final Offset localOffsetFromOrigin;
+
+ /// If this tap is in a series of taps, then this value represents
+ /// the number in the series this tap is.
+ final int consecutiveTapCount;
+
+ /// The keys that were pressed when the most recent [PointerDownEvent] occurred.
+ final Set<LogicalKeyboardKey> keysPressedOnDown;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Duration?>('sourceTimeStamp', sourceTimeStamp));
+ properties.add(DiagnosticsProperty<Offset>('delta', delta));
+ properties.add(DiagnosticsProperty<double?>('primaryDelta', primaryDelta));
+ properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition));
+ properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition));
+ properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind));
+ properties.add(DiagnosticsProperty<Offset>('offsetFromOrigin', offsetFromOrigin));
+ properties.add(DiagnosticsProperty<Offset>('localOffsetFromOrigin', localOffsetFromOrigin));
+ properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount));
+ properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keysPressedOnDown', keysPressedOnDown));
+ }
+}
+
+/// {@macro flutter.gestures.monodrag.GestureDragEndCallback}
+///
+/// The consecutive tap count at the time the pointer contacted the
+/// screen is given by [TapDragEndDetails.consecutiveTapCount].
+///
+/// Used by [TapAndDragGestureRecognizer.onDragEnd].
+typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails);
+
+/// Details for [GestureTapDragEndCallback], such as the number of
+/// consecutive taps.
+///
+/// See also:
+///
+/// * [TapAndDragGestureRecognizer], which passes this information to its
+/// [TapAndDragGestureRecognizer.onDragEnd] callback.
+/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
+/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
+/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
+/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
+class TapDragEndDetails with Diagnosticable {
+ /// Creates details for a [GestureTapDragEndCallback].
+ ///
+ /// The [velocity] argument must not be null.
+ ///
+ /// The [consecutiveTapCount], and [keysPressedOnDown] arguments must
+ /// be provided and must not be null.
+ TapDragEndDetails({
+ this.velocity = Velocity.zero,
+ this.primaryVelocity,
+ required this.consecutiveTapCount,
+ required this.keysPressedOnDown,
+ }) : assert(
+ primaryVelocity == null
+ || primaryVelocity == velocity.pixelsPerSecond.dx
+ || primaryVelocity == velocity.pixelsPerSecond.dy,
+ );
+
+ /// The velocity the pointer was moving when it stopped contacting the screen.
+ ///
+ /// Defaults to zero if not specified in the constructor.
+ final Velocity velocity;
+
+ /// The velocity the pointer was moving along the primary axis when it stopped
+ /// contacting the screen, in logical pixels per second.
+ ///
+ /// If the [GestureTapDragEndCallback] is for a one-dimensional drag (e.g., a
+ /// horizontal or vertical drag), then this value contains the component of
+ /// [velocity] along the primary axis (e.g., horizontal or vertical,
+ /// respectively). Otherwise, if the [GestureTapDragEndCallback] is for a
+ /// two-dimensional drag (e.g., a pan), then this value is null.
+ ///
+ /// Defaults to null if not specified in the constructor.
+ final double? primaryVelocity;
+
+ /// If this tap is in a series of taps, then this value represents
+ /// the number in the series this tap is.
+ final int consecutiveTapCount;
+
+ /// The keys that were pressed when the most recent [PointerDownEvent] occurred.
+ final Set<LogicalKeyboardKey> keysPressedOnDown;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<Velocity>('velocity', velocity));
+ properties.add(DiagnosticsProperty<double?>('primaryVelocity', primaryVelocity));
+ properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount));
+ properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('keysPressedOnDown', keysPressedOnDown));
+ }
+}
+
+/// Signature for when the pointer that previously triggered a
+/// [GestureTapDragDownCallback] did not complete.
+///
+/// Used by [TapAndDragGestureRecognizer.onCancel].
+typedef GestureCancelCallback = void Function();
+
+// A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps
+// that occur in a series of [PointerEvent]s and the most recent set of
+// [LogicalKeyboardKey]s pressed on the most recent tap down.
+//
+// A tap is tracked as part of a series of taps if:
+//
+// 1. The elapsed time between when a [PointerUpEvent] and the subsequent
+// [PointerDownEvent] does not exceed [kDoubleTapTimeout].
+// 2. The delta between the position tapped in the global coordinate system
+// and the position that was tapped previously must be less than or equal
+// to [kDoubleTapSlop].
+//
+// This mixin's state, i.e. the series of taps being tracked is reset when
+// a tap is tracked that does not meet any of the specifications stated above.
+mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
+ // Public state available to [OneSequenceGestureRecognizer].
+
+ // The [PointerDownEvent] that was most recently tracked in [addAllowedPointer].
+ //
+ // This value will be null if a [PointerDownEvent] has not been tracked yet in
+ // [addAllowedPointer] or the timer between two taps has elapsed.
+ //
+ // This value is only reset when the timer between a [PointerUpEvent] and the
+ // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in
+ // [addAllowedPointer].
+ PointerDownEvent? get currentDown => _down;
+
+ // The [PointerUpEvent] that was most recently tracked in [handleEvent].
+ //
+ // This value will be null if a [PointerUpEvent] has not been tracked yet in
+ // [handleEvent] or the timer between two taps has elapsed.
+ //
+ // This value is only reset when the timer between a [PointerUpEvent] and the
+ // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in
+ // [addAllowedPointer].
+ PointerUpEvent? get currentUp => _up;
+
+ // The number of consecutive taps that the most recently tracked [PointerDownEvent]
+ // in [currentDown] represents.
+ //
+ // This value defaults to zero, meaning a tap series is not currently being tracked.
+ //
+ // When this value is greater than zero it means [addAllowedPointer] has run
+ // and at least one [PointerDownEvent] belongs to the current series of taps
+ // being tracked.
+ //
+ // [addAllowedPointer] will either increment this value by `1` or set the value to `1`
+ // depending if the new [PointerDownEvent] is determined to be in the same series as the
+ // tap that preceded it. If too much time has elapsed between two taps, the recognizer has lost
+ // in the arena, the gesture has been cancelled, or the recognizer is being disposed then
+ // this value will be set to `0`, and a new series will begin.
+ int get consecutiveTapCount => _consecutiveTapCount;
+
+ // The set of [LogicalKeyboardKey]s pressed when the most recent [PointerDownEvent]
+ // was tracked in [addAllowedPointer].
+ //
+ // This value defaults to an empty set.
+ //
+ // When the timer between two taps elapses, the recognizer loses the arena, the gesture is cancelled
+ // or the recognizer is disposed of then this value is reset.
+ Set<LogicalKeyboardKey> get keysPressedOnDown => _keysPressedOnDown ?? <LogicalKeyboardKey>{};
+
+ // The upper limit for the [consecutiveTapCount]. When this limit is reached
+ // all tap related state is reset and a new tap series is tracked.
+ //
+ // If this value is null, [consecutiveTapCount] can grow infinitely large.
+ int? get maxConsecutiveTap;
+
+ // The maximum distance in logical pixels the gesture is allowed to drift
+ // from the initial touch down position before the [consecutiveTapCount]
+ // and [keysPressedOnDown] are frozen and the remaining tracker state is
+ // reset. These values remain frozen until the next [PointerDownEvent] is
+ // tracked in [addAllowedPointer].
+ double? get slopTolerance;
+
+ // Private tap state tracked.
+ PointerDownEvent? _down;
+ PointerUpEvent? _up;
+ int _consecutiveTapCount = 0;
+ Set<LogicalKeyboardKey>? _keysPressedOnDown;
+
+ OffsetPair? _originPosition;
+ int? _previousButtons;
+
+ // For timing taps.
+ Timer? _consecutiveTapTimer;
+ Offset? _lastTapOffset;
+
+ // When tracking a tap, the [consecutiveTapCount] is incremented if the given tap
+ // falls under the tolerance specifications and reset to 1 if not.
+ @override
+ void addAllowedPointer(PointerDownEvent event) {
+ super.addAllowedPointer(event);
+ if (maxConsecutiveTap == _consecutiveTapCount) {
+ _tapTrackerReset();
+ }
+ _up = null;
+ if (_down != null && !_representsSameSeries(event)) {
+ // The given tap does not match the specifications of the series of taps being tracked,
+ // reset the tap count and related state.
+ _consecutiveTapCount = 1;
+ } else {
+ _consecutiveTapCount += 1;
+ }
+ _consecutiveTapTimerStop();
+ // `_down` must be assigned in this method instead of [handleEvent],
+ // because [acceptGesture] might be called before [handleEvent],
+ // which may rely on `_down` to initiate a callback.
+ _trackTap(event);
+ }
+
+ @override
+ void handleEvent(PointerEvent event) {
+ if (event is PointerMoveEvent) {
+ final bool isSlopPastTolerance = slopTolerance != null && _getGlobalDistance(event, _originPosition) > slopTolerance!;
+
+ if (isSlopPastTolerance) {
+ _consecutiveTapTimerStop();
+ _previousButtons = null;
+ _lastTapOffset = null;
+ }
+ } else if (event is PointerUpEvent) {
+ _up = event;
+ if (_down != null) {
+ _consecutiveTapTimerStop();
+ _consecutiveTapTimerStart();
+ }
+ } else if (event is PointerCancelEvent) {
+ _tapTrackerReset();
+ }
+ }
+
+ @override
+ void rejectGesture(int pointer) {
+ _tapTrackerReset();
+ }
+
+ @override
+ void dispose() {
+ _tapTrackerReset();
+ super.dispose();
+ }
+
+ void _trackTap(PointerDownEvent event) {
+ _down = event;
+ _keysPressedOnDown = HardwareKeyboard.instance.logicalKeysPressed;
+ _previousButtons = event.buttons;
+ _lastTapOffset = event.position;
+ _originPosition = OffsetPair(local: event.localPosition, global: event.position);
+ }
+
+ bool _hasSameButton(int buttons) {
+ assert(_previousButtons != null);
+ if (buttons == _previousButtons!) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ bool _isWithinConsecutiveTapTolerance(Offset secondTapOffset) {
+ if (_lastTapOffset == null) {
+ return false;
+ }
+
+ final Offset difference = secondTapOffset - _lastTapOffset!;
+ return difference.distance <= kDoubleTapSlop;
+ }
+
+ bool _representsSameSeries(PointerDownEvent event) {
+ return _consecutiveTapTimer != null
+ && _isWithinConsecutiveTapTolerance(event.position)
+ && _hasSameButton(event.buttons);
+ }
+
+ void _consecutiveTapTimerStart() {
+ _consecutiveTapTimer ??= Timer(kDoubleTapTimeout, _tapTrackerReset);
+ }
+
+ void _consecutiveTapTimerStop() {
+ if (_consecutiveTapTimer != null) {
+ _consecutiveTapTimer!.cancel();
+ _consecutiveTapTimer = null;
+ }
+ }
+
+ void _tapTrackerReset() {
+ // The timer has timed out, i.e. the time between a [PointerUpEvent] and the subsequent
+ // [PointerDownEvent] exceeded the duration of [kDoubleTapTimeout], so the tap belonging
+ // to the [PointerDownEvent] cannot be considered part of the same tap series as the
+ // previous [PointerUpEvent].
+ _consecutiveTapTimerStop();
+ _previousButtons = null;
+ _originPosition = null;
+ _lastTapOffset = null;
+ _consecutiveTapCount = 0;
+ _keysPressedOnDown = null;
+ _down = null;
+ _up = null;
+ }
+}
+
+/// Recognizes taps and movements.
+///
+/// Takes on the responsibilities of [TapGestureRecognizer] and
+/// [DragGestureRecognizer] in one [GestureRecognizer].
+///
+/// ### Gesture arena behavior
+///
+/// [TapAndDragGestureRecognizer] competes on the pointer events of
+/// [kPrimaryButton] only when it has at least one non-null `onTap*`
+/// or `onDrag*` callback.
+///
+/// It will declare defeat if it determines that a gesture is not a
+/// tap (e.g. if the pointer is dragged too far while it's contacting the
+/// screen) or a drag (e.g. if the pointer was not dragged far enough to
+/// be considered a drag.
+///
+/// This recognizer will not immediately declare victory for every tap or drag that it
+/// recognizes.
+///
+/// The recognizer will declare victory when all other recognizer's in
+/// the arena have lost, if the timer of [kPressTimeout] elapses and a tap
+/// series greater than 1 is being tracked.
+///
+/// If this recognizer loses the arena (either by declaring defeat or by
+/// another recognizer declaring victory) while the pointer is contacting the
+/// screen, it will fire [onCancel] instead of [onTapUp] or [onDragEnd].
+///
+/// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer`
+///
+/// Similar to [TapGestureRecognizer] and [DragGestureRecognizer],
+/// [TapAndDragGestureRecognizer] will not aggresively declare victory when it detects
+/// a tap, so when it is competing with those gesture recognizers and others it has a chance
+/// of losing.
+///
+/// When competing against [TapGestureRecognizer], if the pointer does not move past the tap
+/// tolerance, then the recognizer that entered the arena first will win. In this case the
+/// gesture detected is a tap. If the pointer does travel past the tap tolerance then this
+/// recognizer will be declared winner by default. The gesture detected in this case is a drag.
+///
+/// When competing against [DragGestureRecognizer], if the pointer does not move a sufficient
+/// global distance to be considered a drag, the recognizers will tie in the arena. If the
+/// pointer does travel enough distance then the [TapAndDragGestureRecognizer] will lose because
+/// the [DragGestureRecognizer] will declare self-victory when the drag threshold is met.
+class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _TapStatusTrackerMixin {
+ /// Creates a tap and drag gesture recognizer.
+ ///
+ /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
+ TapAndDragGestureRecognizer({
+ super.debugOwner,
+ super.supportedDevices,
+ super.allowedButtonsFilter,
+ }) : _deadline = kPressTimeout,
+ dragStartBehavior = DragStartBehavior.start,
+ slopTolerance = kTouchSlop;
+
+ /// Configure the behavior of offsets passed to [onDragStart].
+ ///
+ /// If set to [DragStartBehavior.start], the [onDragStart] callback will be called
+ /// with the position of the pointer at the time this gesture recognizer won
+ /// the arena. If [DragStartBehavior.down], [onDragStart] will be called with
+ /// the position of the first detected down event for the pointer. When there
+ /// are no other gestures competing with this gesture in the arena, there's
+ /// no difference in behavior between the two settings.
+ ///
+ /// For more information about the gesture arena:
+ /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
+ ///
+ /// By default, the drag start behavior is [DragStartBehavior.start].
+ ///
+ /// See also:
+ ///
+ /// * [DragGestureRecognizer.dragStartBehavior], which includes more details and an example.
+ DragStartBehavior dragStartBehavior;
+
+ /// The frequency at which the [onDragUpdate] callback is called.
+ ///
+ /// The value defaults to null, meaning there is no delay for [onDragUpdate] callback.
+ ///
+ /// See also:
+ /// * [TextSelectionGestureDetector], which uses this parameter to avoid excessive updates
+ /// text layouts in text fields.
+ Duration? dragUpdateThrottleFrequency;
+
+ /// An upper bound for the amount of taps that can belong to one tap series.
+ ///
+ /// When this limit is reached the series of taps being tracked by this
+ /// recognizer will be reset.
+ @override
+ int? maxConsecutiveTap;
+
+ // The maximum distance in logical pixels the gesture is allowed to drift
+ // to still be considered a tap.
+ //
+ // Drifting past the allowed slop amount causes the recognizer to reset
+ // the tap series it is currently tracking, stopping the consecutive tap
+ // count from increasing. The consecutive tap count and the set of hardware
+ // keys that were pressed on tap down will retain their pre-past slop
+ // tolerance values until the next [PointerDownEvent] is tracked.
+ //
+ // If the gesture exceeds this value, then it can only be accepted as a drag
+ // gesture.
+ //
+ // Can be null to indicate that the gesture can drift for any distance.
+ // Defaults to 18 logical pixels.
+ @override
+ final double? slopTolerance;
+
+ /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapDown}
+ ///
+ /// This triggers after the down event, once a short timeout ([kPressTimeout]) has
+ /// elapsed, or once the gestures has won the arena, whichever comes first.
+ ///
+ /// The position of the pointer is provided in the callback's `details`
+ /// argument, which is a [TapDragDownDetails] object.
+ ///
+ /// {@template flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
+ /// The number of consecutive taps, and the keys that were pressed on tap down
+ /// are also provided in the callback's `details` argument.
+ /// {@endtemplate}
+ ///
+ /// See also:
+ ///
+ /// * [kPrimaryButton], the button this callback responds to.
+ /// * [TapDragDownDetails], which is passed as an argument to this callback.
+ GestureTapDragDownCallback? onTapDown;
+
+ /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapUp}
+ ///
+ /// This triggers on the up event, if the recognizer wins the arena with it
+ /// or has previously won.
+ ///
+ /// The position of the pointer is provided in the callback's `details`
+ /// argument, which is a [TapDragUpDetails] object.
+ ///
+ /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
+ ///
+ /// See also:
+ ///
+ /// * [kPrimaryButton], the button this callback responds to.
+ /// * [TapDragUpDetails], which is passed as an argument to this callback.
+ GestureTapDragUpCallback? onTapUp;
+
+ /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onStart}
+ ///
+ /// The position of the pointer is provided in the callback's `details`
+ /// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior]
+ /// determines this position.
+ ///
+ /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
+ ///
+ /// See also:
+ ///
+ /// * [kPrimaryButton], the button this callback responds to.
+ /// * [TapDragStartDetails], which is passed as an argument to this callback.
+ GestureTapDragStartCallback? onDragStart;
+
+ /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onUpdate}
+ ///
+ /// The distance traveled by the pointer since the last update is provided in
+ /// the callback's `details` argument, which is a [TapDragUpdateDetails] object.
+ ///
+ /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
+ ///
+ /// See also:
+ ///
+ /// * [kPrimaryButton], the button this callback responds to.
+ /// * [TapDragUpdateDetails], which is passed as an argument to this callback.
+ GestureTapDragUpdateCallback? onDragUpdate;
+
+ /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onEnd}
+ ///
+ /// The velocity is provided in the callback's `details` argument, which is a
+ /// [TapDragEndDetails] object.
+ ///
+ /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
+ ///
+ /// See also:
+ ///
+ /// * [kPrimaryButton], the button this callback responds to.
+ /// * [TapDragEndDetails], which is passed as an argument to this callback.
+ GestureTapDragEndCallback? onDragEnd;
+
+ /// The pointer that previously triggered [onTapDown] did not complete.
+ ///
+ /// This is called when a [PointerCancelEvent] is tracked when the [onTapDown] callback
+ /// was previously called.
+ ///
+ /// It may also be called if a [PointerUpEvent] is tracked after the pointer has moved
+ /// past the tap tolerance but not past the drag tolerance, and the recognizer has not
+ /// yet won the arena.
+ ///
+ /// See also:
+ ///
+ /// * [kPrimaryButton], the button this callback responds to.
+ GestureCancelCallback? onCancel;
+
+ // Tap related state.
+ bool _pastSlopTolerance = false;
+ bool _sentTapDown = false;
+ bool _wonArenaForPrimaryPointer = false;
+
+ // Primary pointer being tracked by this recognizer.
+ int? _primaryPointer;
+ Timer? _deadlineTimer;
+ // The recognizer will call [onTapDown] after this amount of time has elapsed
+ // since starting to track the primary pointer.
+ //
+ // [onTapDown] will not be called if the primary pointer is
+ // accepted, rejected, or all pointers are up or canceled before [_deadline].
+ final Duration _deadline;
+
+ // Drag related state.
+ _DragState _dragState = _DragState.ready;
+ PointerEvent? _start;
+ late OffsetPair _initialPosition;
+ late double _globalDistanceMoved;
+ OffsetPair? _correctedPosition;
+
+ // For drag update throttle.
+ TapDragUpdateDetails? _lastDragUpdateDetails;
+ Timer? _dragUpdateThrottleTimer;
+
+ final Set<int> _acceptedActivePointers = <int>{};
+
+ bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
+ return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
+ }
+
+ // Drag updates may require throttling to avoid excessive updating, such as for text layouts in text
+ // fields. The frequency of invocations is controlled by the [dragUpdateThrottleFrequency].
+ //
+ // Once the drag gesture ends, any pending drag update will be fired
+ // immediately. See [_checkDragEnd].
+ void _handleDragUpdateThrottled() {
+ assert(_lastDragUpdateDetails != null);
+ if (onDragUpdate != null) {
+ invokeCallback<void>('onDragUpdate', () => onDragUpdate!(_lastDragUpdateDetails!));
+ }
+ _dragUpdateThrottleTimer = null;
+ _lastDragUpdateDetails = null;
+ }
+
+ @override
+ bool isPointerAllowed(PointerEvent event) {
+ if (_primaryPointer == null) {
+ switch (event.buttons) {
+ case kPrimaryButton:
+ if (onTapDown == null &&
+ onDragStart == null &&
+ onDragUpdate == null &&
+ onDragEnd == null &&
+ onTapUp == null &&
+ onCancel == null) {
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+ } else {
+ if (event.pointer != _primaryPointer) {
+ return false;
+ }
+ }
+
+ return super.isPointerAllowed(event as PointerDownEvent);
+ }
+
+ @override
+ void addAllowedPointer(PointerDownEvent event) {
+ if (_dragState == _DragState.ready) {
+ super.addAllowedPointer(event);
+ _primaryPointer = event.pointer;
+ _globalDistanceMoved = 0.0;
+ _dragState = _DragState.possible;
+ _initialPosition = OffsetPair(global: event.position, local: event.localPosition);
+ _deadlineTimer = Timer(_deadline, () => _didExceedDeadlineWithEvent(event));
+ }
+ }
+
+ @override
+ void handleNonAllowedPointer(PointerDownEvent event) {
+ // There can be multiple drags simultaneously. Their effects are combined.
+ if (event.buttons != kPrimaryButton) {
+ if (!_wonArenaForPrimaryPointer) {
+ super.handleNonAllowedPointer(event);
+ }
+ }
+ }
+
+ @override
+ void acceptGesture(int pointer) {
+ if (pointer != _primaryPointer) {
+ return;
+ }
+
+ _stopDeadlineTimer();
+
+ assert(!_acceptedActivePointers.contains(pointer));
+ _acceptedActivePointers.add(pointer);
+
+ // Called when this recognizer is accepted by the [GestureArena].
+ if (currentDown != null) {
+ _checkTapDown(currentDown!);
+ }
+
+ _wonArenaForPrimaryPointer = true;
+
+ if (_start != null) {
+ _acceptDrag(_start!);
+ }
+
+ if (currentUp != null) {
+ _checkTapUp(currentUp!);
+ }
+ }
+
+ @override
+ void didStopTrackingLastPointer(int pointer) {
+ switch (_dragState) {
+ case _DragState.ready:
+ _checkCancel();
+ resolve(GestureDisposition.rejected);
+ break;
+
+ case _DragState.possible:
+ if (_pastSlopTolerance) {
+ // This means the pointer was not accepted as a tap.
+ if (_wonArenaForPrimaryPointer) {
+ // If the recognizer has already won the arena for the primary pointer being tracked
+ // but the pointer has exceeded the tap tolerance, then the pointer is accepted as a
+ // drag gesture.
+ if (currentDown != null) {
+ _acceptDrag(currentDown!);
+ _checkDragEnd();
+ }
+ } else {
+ _checkCancel();
+ resolve(GestureDisposition.rejected);
+ }
+ } else {
+ // The pointer is accepted as a tap.
+ if (currentUp != null) {
+ _checkTapUp(currentUp!);
+ }
+ }
+ break;
+
+ case _DragState.accepted:
+ // For the case when the pointer has been accepted as a drag.
+ // Meaning [_checkTapDown] and [_checkDragStart] have already ran.
+ _checkDragEnd();
+ break;
+ }
+
+ _stopDeadlineTimer();
+ _dragState = _DragState.ready;
+ _pastSlopTolerance = false;
+ }
+
+ @override
+ void handleEvent(PointerEvent event) {
+ if (event.pointer != _primaryPointer) {
+ return;
+ }
+ super.handleEvent(event);
+ if (event is PointerMoveEvent) {
+ // Receiving a [PointerMoveEvent], does not automatically mean the pointer
+ // being tracked is doing a drag gesture. There is some drift that can happen
+ // between the initial [PointerDownEvent] and subsequent [PointerMoveEvent]s.
+ // Accessing [_pastSlopTolerance] lets us know if our tap has moved past the
+ // acceptable tolerance. If the pointer does not move past this tolerance than
+ // it is not considered a drag.
+ //
+ // To be recognized as a drag, the [PointerMoveEvent] must also have moved
+ // a sufficient global distance from the initial [PointerDownEvent] to be
+ // accepted as a drag. This logic is handled in [_hasSufficientGlobalDistanceToAccept].
+ //
+ // The recognizer will also detect the gesture as a drag when the pointer
+ // has been accepted and it has moved past the [slopTolerance] but has not moved
+ // a sufficient global distance from the initial position to be considered a drag.
+ // In this case since the gesture cannot be a tap, it defaults to a drag.
+
+ _pastSlopTolerance = _pastSlopTolerance || slopTolerance != null && _getGlobalDistance(event, _initialPosition) > slopTolerance!;
+
+ if (_dragState == _DragState.accepted) {
+ _checkDragUpdate(event);
+ } else if (_dragState == _DragState.possible) {
+ if (_start == null) {
+ // Only check for a drag if the start of a drag was not already identified.
+ _checkDrag(event);
+ }
+
+ // This can occur when the recognizer is accepted before a [PointerMoveEvent] has been
+ // received that moves the pointer a sufficient global distance to be considered a drag.
+ if (_start != null) {
+ _acceptDrag(_start!);
+ }
+ }
+ } else if (event is PointerUpEvent) {
+ if (_dragState == _DragState.possible) {
+ // The drag has not been accepted before a [PointerUpEvent], therefore the recognizer
+ // attempts to recognize a tap.
+ stopTrackingIfPointerNoLongerDown(event);
+ } else if (_dragState == _DragState.accepted) {
+ _giveUpPointer(event.pointer);
+ }
+ } else if (event is PointerCancelEvent) {
+ _dragState = _DragState.ready;
+ _giveUpPointer(event.pointer);
+ }
+ }
+
+ @override
+ void rejectGesture(int pointer) {
+ if (pointer != _primaryPointer) {
+ return;
+ }
+ super.rejectGesture(pointer);
+
+ _stopDeadlineTimer();
+ _giveUpPointer(pointer);
+ _resetTaps();
+ _resetDragUpdateThrottle();
+ }
+
+ @override
+ void dispose() {
+ _stopDeadlineTimer();
+ _resetDragUpdateThrottle();
+ super.dispose();
+ }
+
+ @override
+ String get debugDescription => 'tap_and_drag';
+
+ void _acceptDrag(PointerEvent event) {
+ if (!_wonArenaForPrimaryPointer) {
+ return;
+ }
+ _dragState = _DragState.accepted;
+ if (dragStartBehavior == DragStartBehavior.start) {
+ _initialPosition = _initialPosition + OffsetPair(global: event.delta, local: event.localDelta);
+ }
+ _checkDragStart(event);
+ if (event.localDelta != Offset.zero) {
+ final Matrix4? localToGlobal = event.transform != null ? Matrix4.tryInvert(event.transform!) : null;
+ final Offset correctedLocalPosition = _initialPosition.local + event.localDelta;
+ final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions(
+ untransformedEndPosition: correctedLocalPosition,
+ untransformedDelta: event.localDelta,
+ transform: localToGlobal,
+ );
+ final OffsetPair updateDelta = OffsetPair(local: event.localDelta, global: globalUpdateDelta);
+ _correctedPosition = _initialPosition + updateDelta; // Only adds delta for down behaviour
+ _checkDragUpdate(event);
+ _correctedPosition = null;
+ }
+ }
+
+ void _checkDrag(PointerMoveEvent event) {
+ final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
+ _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
+ transform: localToGlobalTransform,
+ untransformedDelta: event.localDelta,
+ untransformedEndPosition: event.localPosition
+ ).distance * 1.sign;
+ if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
+ _start = event;
+ }
+ }
+
+ void _checkTapDown(PointerDownEvent event) {
+ if (_sentTapDown) {
+ return;
+ }
+
+ final TapDragDownDetails details = TapDragDownDetails(
+ globalPosition: event.position,
+ localPosition: event.localPosition,
+ kind: getKindForPointer(event.pointer),
+ consecutiveTapCount: consecutiveTapCount,
+ keysPressedOnDown: keysPressedOnDown,
+ );
+
+ if (onTapDown != null) {
+ invokeCallback('onTapDown', () => onTapDown!(details));
+ }
+
+ _sentTapDown = true;
+ }
+
+ void _checkTapUp(PointerUpEvent event) {
+ if (!_wonArenaForPrimaryPointer) {
+ return;
+ }
+
+ final TapDragUpDetails upDetails = TapDragUpDetails(
+ kind: event.kind,
+ globalPosition: event.position,
+ localPosition: event.localPosition,
+ consecutiveTapCount: consecutiveTapCount,
+ keysPressedOnDown: keysPressedOnDown,
+ );
+
+ if (onTapUp != null) {
+ invokeCallback('onTapUp', () => onTapUp!(upDetails));
+ }
+
+ _resetTaps();
+ if (!_acceptedActivePointers.remove(event.pointer)) {
+ resolvePointer(event.pointer, GestureDisposition.rejected);
+ }
+ }
+
+ void _checkDragStart(PointerEvent event) {
+ if (onDragStart != null) {
+ final TapDragStartDetails details = TapDragStartDetails(
+ sourceTimeStamp: event.timeStamp,
+ globalPosition: _initialPosition.global,
+ localPosition: _initialPosition.local,
+ kind: getKindForPointer(event.pointer),
+ consecutiveTapCount: consecutiveTapCount,
+ keysPressedOnDown: keysPressedOnDown,
+ );
+
+ invokeCallback<void>('onDragStart', () => onDragStart!(details));
+ }
+
+ _start = null;
+ }
+
+ void _checkDragUpdate(PointerEvent event) {
+ final Offset globalPosition = _correctedPosition != null ? _correctedPosition!.global : event.position;
+ final Offset localPosition = _correctedPosition != null ? _correctedPosition!.local : event.localPosition;
+
+ final TapDragUpdateDetails details = TapDragUpdateDetails(
+ sourceTimeStamp: event.timeStamp,
+ delta: event.localDelta,
+ globalPosition: globalPosition,
+ kind: getKindForPointer(event.pointer),
+ localPosition: localPosition,
+ offsetFromOrigin: globalPosition - _initialPosition.global,
+ localOffsetFromOrigin: localPosition - _initialPosition.local,
+ consecutiveTapCount: consecutiveTapCount,
+ keysPressedOnDown: keysPressedOnDown,
+ );
+
+ if (dragUpdateThrottleFrequency != null) {
+ _lastDragUpdateDetails = details;
+ // Only schedule a new timer if there's not one pending.
+ _dragUpdateThrottleTimer ??= Timer(dragUpdateThrottleFrequency!, _handleDragUpdateThrottled);
+ } else {
+ if (onDragUpdate != null) {
+ invokeCallback<void>('onDragUpdate', () => onDragUpdate!(details));
+ }
+ }
+ }
+
+ void _checkDragEnd() {
+ if (_dragUpdateThrottleTimer != null) {
+ // If there's already an update scheduled, trigger it immediately and
+ // cancel the timer.
+ _dragUpdateThrottleTimer!.cancel();
+ _handleDragUpdateThrottled();
+ }
+
+ final TapDragEndDetails endDetails =
+ TapDragEndDetails(
+ primaryVelocity: 0.0,
+ consecutiveTapCount: consecutiveTapCount,
+ keysPressedOnDown: keysPressedOnDown,
+ );
+
+ invokeCallback<void>('onDragEnd', () => onDragEnd!(endDetails));
+
+ _resetTaps();
+ _resetDragUpdateThrottle();
+ }
+
+ void _checkCancel() {
+ if (!_sentTapDown) {
+ // Do not fire tap cancel if [onTapDown] was never called.
+ return;
+ }
+ if (onCancel != null) {
+ invokeCallback('onCancel', onCancel!);
+ }
+ _resetDragUpdateThrottle();
+ _resetTaps();
+ }
+
+ void _didExceedDeadlineWithEvent(PointerDownEvent event) {
+ _didExceedDeadline();
+ }
+
+ void _didExceedDeadline() {
+ if (currentDown != null) {
+ _checkTapDown(currentDown!);
+
+ if (consecutiveTapCount > 1) {
+ // If our consecutive tap count is greater than 1, i.e. is a double tap or greater,
+ // then this recognizer declares victory to prevent the [LongPressGestureRecognizer]
+ // from declaring itself the winner if a double tap is held for too long.
+ resolve(GestureDisposition.accepted);
+ }
+ }
+ }
+
+ void _giveUpPointer(int pointer) {
+ stopTrackingPointer(pointer);
+ // If the pointer was never accepted, then it is rejected since this recognizer is no longer
+ // interested in winning the gesture arena for it.
+ if (!_acceptedActivePointers.remove(pointer)) {
+ resolvePointer(pointer, GestureDisposition.rejected);
+ }
+ }
+
+ void _resetTaps() {
+ _sentTapDown = false;
+ _wonArenaForPrimaryPointer = false;
+ _primaryPointer = null;
+ }
+
+ void _resetDragUpdateThrottle() {
+ if (dragUpdateThrottleFrequency == null) {
+ return;
+ }
+ _lastDragUpdateDetails = null;
+ if (_dragUpdateThrottleTimer != null) {
+ _dragUpdateThrottleTimer!.cancel();
+ _dragUpdateThrottleTimer = null;
+ }
+ }
+
+ void _stopDeadlineTimer() {
+ if (_deadlineTimer != null) {
+ _deadlineTimer!.cancel();
+ _deadlineTimer = null;
+ }
+ }
+}
diff --git a/framework/lib/src/widgets/text.dart b/framework/lib/src/widgets/text.dart
index 7669a55..8115435 100644
--- a/framework/lib/src/widgets/text.dart
+++ b/framework/lib/src/widgets/text.dart
@@ -49,12 +49,7 @@
this.textWidthBasis = TextWidthBasis.parent,
this.textHeightBehavior,
required super.child,
- }) : assert(style != null),
- assert(softWrap != null),
- assert(overflow != null),
- assert(maxLines == null || maxLines > 0),
- assert(child != null),
- assert(textWidthBasis != null);
+ }) : assert(maxLines == null || maxLines > 0);
/// A const-constructable default text style that provides fallback values.
///
@@ -98,7 +93,6 @@
TextWidthBasis? textWidthBasis,
required Widget child,
}) {
- assert(child != null);
return Builder(
builder: (BuildContext context) {
final DefaultTextStyle parent = DefaultTextStyle.of(context);
@@ -239,8 +233,7 @@
super.key,
required this.textHeightBehavior,
required super.child,
- }) : assert(textHeightBehavior != null),
- assert(child != null);
+ });
/// {@macro dart.ui.textHeightBehavior}
final TextHeightBehavior textHeightBehavior;
@@ -440,11 +433,7 @@
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
- }) : assert(
- data != null,
- 'A non-null String must be provided to a Text widget.',
- ),
- textSpan = null;
+ }) : textSpan = null;
/// Creates a text widget with a [InlineSpan].
///
@@ -472,11 +461,7 @@
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
- }) : assert(
- textSpan != null,
- 'A non-null TextSpan must be provided to a Text.rich widget.',
- ),
- data = null;
+ }) : data = null;
/// The text to display.
///
@@ -598,7 +583,7 @@
if (style == null || style!.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(style);
}
- if (MediaQuery.boldTextOverride(context)) {
+ if (MediaQuery.boldTextOf(context)) {
effectiveTextStyle = effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold));
}
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
diff --git a/framework/lib/src/widgets/text_editing_intents.dart b/framework/lib/src/widgets/text_editing_intents.dart
index eb12b41..80b2588 100644
--- a/framework/lib/src/widgets/text_editing_intents.dart
+++ b/framework/lib/src/widgets/text_editing_intents.dart
@@ -137,11 +137,11 @@
/// reverse.
///
/// This is typically only used on MacOS.
-class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalTextEditingIntent {
+class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent].
const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({
required bool forward,
- }) : super(forward);
+ }) : super(forward, false, true);
}
/// Expands the current selection to the document boundary in the direction
@@ -154,11 +154,11 @@
///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// moves the extent.
-class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
+class ExpandSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExpandSelectionToDocumentBoundaryIntent].
const ExpandSelectionToDocumentBoundaryIntent({
required bool forward,
- }) : super(forward);
+ }) : super(forward, false);
}
/// Expands the current selection to the closest line break in the direction
@@ -173,11 +173,11 @@
///
/// [ExtendSelectionToLineBreakIntent], which is similar but always moves the
/// extent.
-class ExpandSelectionToLineBreakIntent extends DirectionalTextEditingIntent {
+class ExpandSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExpandSelectionToLineBreakIntent].
const ExpandSelectionToLineBreakIntent({
required bool forward,
- }) : super(forward);
+ }) : super(forward, false);
}
/// Extends, or moves the current selection from the current
@@ -222,6 +222,21 @@
}
/// Extends, or moves the current selection from the current
+/// [TextSelection.extent] position to the previous or the next paragraph
+/// boundary depending on the [forward] parameter.
+///
+/// This [Intent] collapses the selection when the order of [TextSelection.base]
+/// and [TextSelection.extent] would reverse.
+///
+/// This is typically only used on MacOS.
+class ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent extends DirectionalCaretMovementIntent {
+ /// Creates an [ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent].
+ const ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent({
+ required bool forward,
+ }) : super(forward, false, true);
+}
+
+/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the start or the end of the document.
///
/// See also:
diff --git a/framework/lib/src/widgets/text_selection.dart b/framework/lib/src/widgets/text_selection.dart
index fc5eb5b..72d971b 100644
--- a/framework/lib/src/widgets/text_selection.dart
+++ b/framework/lib/src/widgets/text_selection.dart
@@ -24,6 +24,7 @@
import 'magnifier.dart';
import 'overlay.dart';
import 'scrollable.dart';
+import 'tap_and_drag_gestures.dart';
import 'tap_region.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
@@ -35,19 +36,6 @@
/// called.
const Duration _kDragSelectionUpdateThrottle = Duration(milliseconds: 50);
-/// Signature for when a pointer that's dragging to select text has moved again.
-///
-/// The first argument [startDetails] contains the details of the event that
-/// initiated the dragging.
-///
-/// The second argument [updateDetails] contains the details of the current
-/// pointer movement. It's the same as the one passed to [DragGestureRecognizer.onUpdate].
-///
-/// This signature is different from [GestureDragUpdateCallback] to make it
-/// easier for various text fields to use [TextSelectionGestureDetector] without
-/// having to store the start position.
-typedef DragSelectionUpdateCallback = void Function(DragStartDetails startDetails, DragUpdateDetails updateDetails);
-
/// The type for a Function that builds a toolbar's container with the given
/// child.
///
@@ -332,10 +320,7 @@
ClipboardStatusNotifier? clipboardStatus,
this.contextMenuBuilder,
required TextMagnifierConfiguration magnifierConfiguration,
- }) : assert(value != null),
- assert(context != null),
- assert(handlesVisible != null),
- _handlesVisible = handlesVisible,
+ }) : _handlesVisible = handlesVisible,
_value = value {
renderObject.selectionStartInViewport.addListener(_updateTextSelectionOverlayVisibilities);
renderObject.selectionEndInViewport.addListener(_updateTextSelectionOverlayVisibilities);
@@ -431,7 +416,6 @@
bool get handlesVisible => _handlesVisible;
bool _handlesVisible = false;
set handlesVisible(bool visible) {
- assert(visible != null);
if (_handlesVisible == visible) {
return;
}
@@ -469,6 +453,20 @@
return;
}
+ /// Shows toolbar with spell check suggestions of misspelled words that are
+ /// available for click-and-replace.
+ void showSpellCheckSuggestionsToolbar(
+ WidgetBuilder spellCheckSuggestionsToolbarBuilder
+ ) {
+ _updateSelectionOverlay();
+ assert(context.mounted);
+ _selectionOverlay
+ .showSpellCheckSuggestionsToolbar(
+ context: context,
+ builder: spellCheckSuggestionsToolbarBuilder,
+ );
+ }
+
/// {@macro flutter.widgets.SelectionOverlay.showMagnifier}
void showMagnifier(Offset positionToShow) {
final TextPosition position = renderObject.getPositionForPoint(positionToShow);
@@ -595,7 +593,7 @@
// widget.renderObject.getRectForComposingRange might fail. In cases where
// the current frame is different from the previous we fall back to
// renderObject.preferredLineHeight.
- if (renderObject.plainText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
+ if (renderObject.plainText == currText && _selection.isValid && !_selection.isCollapsed) {
final String selectedGraphemes = _selection.textInside(currText);
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
startHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent));
@@ -608,7 +606,7 @@
final int lastSelectedGraphemeExtent;
Rect? endHandleRect;
// See the explanation in _getStartGlyphHeight.
- if (renderObject.plainText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
+ if (renderObject.plainText == currText && _selection.isValid && !_selection.isCollapsed) {
final String selectedGraphemes = _selection.textInside(currText);
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
endHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end));
@@ -894,7 +892,6 @@
return TextSelectionHandleType.collapsed;
}
- assert(textDirection != null);
switch (textDirection) {
case TextDirection.ltr:
return ltrType;
@@ -1347,6 +1344,29 @@
);
}
+ /// Shows toolbar with spell check suggestions of misspelled words that are
+ /// available for click-and-replace.
+ void showSpellCheckSuggestionsToolbar({
+ BuildContext? context,
+ required WidgetBuilder builder,
+ }) {
+ if (context == null) {
+ return;
+ }
+
+ final RenderBox renderBox = context.findRenderObject()! as RenderBox;
+ _contextMenuController.show(
+ context: context,
+ contextMenuBuilder: (BuildContext context) {
+ return _SelectionToolbarWrapper(
+ layerLink: toolbarLayerLink,
+ offset: -renderBox.localToGlobal(Offset.zero),
+ child: builder(context),
+ );
+ },
+ );
+ }
+
bool _buildScheduled = false;
/// Rebuilds the selection toolbar or handles if they are present.
@@ -1554,9 +1574,7 @@
required this.layerLink,
required this.offset,
required this.child,
- }) : assert(layerLink != null),
- assert(offset != null),
- assert(child != null);
+ });
final Widget child;
final Offset offset;
@@ -1825,7 +1843,7 @@
/// The [delegate] must not be null.
TextSelectionGestureDetectorBuilder({
required this.delegate,
- }) : assert(delegate != null);
+ });
/// The delegate for this [TextSelectionGestureDetectorBuilder].
///
@@ -1870,6 +1888,23 @@
&& selection.end >= textPosition.offset;
}
+ /// Returns true if position was on selection.
+ bool _positionOnSelection(Offset position, TextSelection? targetSelection) {
+ if (targetSelection == null) {
+ return false;
+ }
+
+ final TextPosition textPosition = renderEditable.getPositionForPoint(position);
+
+ return targetSelection.start <= textPosition.offset
+ && targetSelection.end >= textPosition.offset;
+ }
+
+ /// Returns true if shift left or right is contained in the given set.
+ static bool _containsShift(Set<LogicalKeyboardKey> keysPressed) {
+ return keysPressed.any(<LogicalKeyboardKey>{ LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight }.contains);
+ }
+
// Expand the selection to the given global position.
//
// Either base or extent will be moved to the last tapped position, whichever
@@ -1883,8 +1918,6 @@
// * [_extendSelection], which is similar but pivots the selection around
// the base.
void _expandSelection(Offset offset, SelectionChangedCause cause, [TextSelection? fromSelection]) {
- assert(cause != null);
- assert(offset != null);
assert(renderEditable.selection?.baseOffset != null);
final TextPosition tappedPosition = renderEditable.getPositionForPoint(offset);
@@ -1914,8 +1947,6 @@
// * [_expandSelection], which is similar but always increases the size of
// the selection.
void _extendSelection(Offset offset, SelectionChangedCause cause) {
- assert(cause != null);
- assert(offset != null);
assert(renderEditable.selection?.baseOffset != null);
final TextPosition tappedPosition = renderEditable.getPositionForPoint(offset);
@@ -1957,15 +1988,6 @@
/// The viewport offset pixels of the [RenderEditable] at the last drag start.
double _dragStartViewportOffset = 0.0;
- // Returns true iff either shift key is currently down.
- bool get _isShiftPressed {
- return HardwareKeyboard.instance.logicalKeysPressed
- .any(<LogicalKeyboardKey>{
- LogicalKeyboardKey.shiftLeft,
- LogicalKeyboardKey.shiftRight,
- }.contains);
- }
-
double get _scrollPosition {
final ScrollableState? scrollableState =
delegate.editableTextKey.currentContext == null
@@ -1976,13 +1998,19 @@
: scrollableState.position.pixels;
}
- // True iff a tap + shift has been detected but the tap has not yet come up.
- bool _isShiftTapping = false;
-
// For a shift + tap + drag gesture, the TextSelection at the point of the
// tap. Mac uses this value to reset to the original selection when an
// inversion of the base and offset happens.
- TextSelection? _shiftTapDragSelection;
+ TextSelection? _dragStartSelection;
+
+ // For tap + drag gesture on iOS, whether the position where the drag started
+ // was on the previous TextSelection. iOS uses this value to determine if
+ // the cursor should move on drag update.
+ //
+ // If the drag started on the previous selection then the cursor will move on
+ // drag update. If the drag did not start on the previous selection then the
+ // cursor will not move on drag update.
+ bool? _dragBeganOnPreviousSelection;
/// Handler for [TextSelectionGestureDetector.onTapDown].
///
@@ -1993,11 +2021,17 @@
///
/// * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
@protected
- void onTapDown(TapDownDetails details) {
+ void onTapDown(TapDragDownDetails details) {
if (!delegate.selectionEnabled) {
return;
}
- renderEditable.handleTapDown(details);
+ // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state
+ // in renderEditable. The gesture callbacks can use the details objects directly
+ // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap]
+ // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in
+ // renderEditable. When this migration is complete we should remove this hack.
+ // See https://github.com/flutter/flutter/issues/115130.
+ renderEditable.handleTapDown(TapDownDetails(globalPosition: details.globalPosition));
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
@@ -2011,21 +2045,20 @@
|| kind == PointerDeviceKind.stylus;
// Handle shift + click selection if needed.
- final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null;
+ final bool isShiftPressed = _containsShift(details.keysPressedOnDown);
+ // It is impossible to extend the selection when the shift key is pressed, if the
+ // renderEditable.selection is invalid.
+ final bool isShiftPressedValid = isShiftPressed && renderEditable.selection?.baseOffset != null;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// On mobile platforms the selection is set on tap up.
- if (_isShiftTapping) {
- _isShiftTapping = false;
- }
break;
case TargetPlatform.macOS:
// On macOS, a shift-tapped unfocused field expands from 0, not from the
// previous selection.
if (isShiftPressedValid) {
- _isShiftTapping = true;
final TextSelection? fromSelection = renderEditable.hasFocus
? null
: const TextSelection.collapsed(offset: 0);
@@ -2045,7 +2078,6 @@
case TargetPlatform.linux:
case TargetPlatform.windows:
if (isShiftPressedValid) {
- _isShiftTapping = true;
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
return;
}
@@ -2109,25 +2141,32 @@
/// * [TextSelectionGestureDetector.onSingleTapUp], which triggers
/// this callback.
@protected
- void onSingleTapUp(TapUpDetails details) {
+ void onSingleTapUp(TapDragUpDetails details) {
if (delegate.selectionEnabled) {
// Handle shift + click selection if needed.
- final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null;
+ final bool isShiftPressed = _containsShift(details.keysPressedOnDown);
+ // It is impossible to extend the selection when the shift key is pressed, if the
+ // renderEditable.selection is invalid.
+ final bool isShiftPressedValid = isShiftPressed && renderEditable.selection?.baseOffset != null;
switch (defaultTargetPlatform) {
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
editableText.hideToolbar();
// On desktop platforms the selection is set on tap down.
- if (_isShiftTapping) {
- _isShiftTapping = false;
- }
break;
case TargetPlatform.android:
+ editableText.hideToolbar();
+ editableText.showSpellCheckSuggestionsToolbar();
+ if (isShiftPressedValid) {
+ _extendSelection(details.globalPosition, SelectionChangedCause.tap);
+ return;
+ }
+ renderEditable.selectPosition(cause: SelectionChangedCause.tap);
+ break;
case TargetPlatform.fuchsia:
editableText.hideToolbar();
if (isShiftPressedValid) {
- _isShiftTapping = true;
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
return;
}
@@ -2137,7 +2176,6 @@
if (isShiftPressedValid) {
// On iOS, a shift-tapped unfocused field expands from 0, not from
// the previous selection.
- _isShiftTapping = true;
final TextSelection? fromSelection = renderEditable.hasFocus
? null
: const TextSelection.collapsed(offset: 0);
@@ -2200,7 +2238,7 @@
/// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
/// this callback.
@protected
- void onSingleTapCancel() {/* Subclass should override this method if needed. */}
+ void onSingleTapCancel() { /* Subclass should override this method if needed. */ }
/// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
///
@@ -2370,7 +2408,13 @@
/// * [onSecondaryTap], which is typically called after this.
@protected
void onSecondaryTapDown(TapDownDetails details) {
- renderEditable.handleSecondaryTapDown(details);
+ // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state
+ // in renderEditable. The gesture callbacks can use the details objects directly
+ // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap]
+ // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in
+ // renderEditable. When this migration is complete we should remove this hack.
+ // See https://github.com/flutter/flutter/issues/115130.
+ renderEditable.handleSecondaryTapDown(TapDownDetails(globalPosition: details.globalPosition));
_shouldShowSelectionToolbar = true;
}
@@ -2384,7 +2428,7 @@
/// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
/// callback.
@protected
- void onDoubleTapDown(TapDownDetails details) {
+ void onDoubleTapDown(TapDragDownDetails details) {
if (delegate.selectionEnabled) {
renderEditable.selectWord(cause: SelectionChangedCause.tap);
if (shouldShowSelectionToolbar) {
@@ -2402,7 +2446,7 @@
/// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
/// this callback.
@protected
- void onDragSelectionStart(DragStartDetails details) {
+ void onDragSelectionStart(TapDragStartDetails details) {
if (!delegate.selectionEnabled) {
return;
}
@@ -2411,8 +2455,18 @@
|| kind == PointerDeviceKind.touch
|| kind == PointerDeviceKind.stylus;
- if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) {
- _isShiftTapping = true;
+ _dragStartSelection = renderEditable.selection;
+ _dragStartScrollOffset = _scrollPosition;
+ _dragStartViewportOffset = renderEditable.offset.pixels;
+
+ if (details.consecutiveTapCount > 1) {
+ // Do not set the selection on a consecutive tap and drag.
+ return;
+ }
+
+ final bool isShiftPressed = _containsShift(details.keysPressedOnDown);
+
+ if (isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
@@ -2425,16 +2479,46 @@
_extendSelection(details.globalPosition, SelectionChangedCause.drag);
break;
}
- _shiftTapDragSelection = renderEditable.selection;
} else {
- renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.drag,
- );
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ switch (details.kind) {
+ case PointerDeviceKind.mouse:
+ case PointerDeviceKind.trackpad:
+ case PointerDeviceKind.stylus:
+ case PointerDeviceKind.invertedStylus:
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ break;
+ case PointerDeviceKind.touch:
+ case PointerDeviceKind.unknown:
+ // For Android, Fucshia, and iOS platforms, a touch drag
+ // does not initiate unless the editable has focus.
+ if (renderEditable.hasFocus) {
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
+ break;
+ case null:
+ break;
+ }
+ break;
+ case TargetPlatform.linux:
+ case TargetPlatform.macOS:
+ case TargetPlatform.windows:
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ break;
+ }
}
-
- _dragStartScrollOffset = _scrollPosition;
- _dragStartViewportOffset = renderEditable.offset.pixels;
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
@@ -2447,12 +2531,14 @@
/// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
/// this callback./lib/src/material/text_field.dart
@protected
- void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) {
+ void onDragSelectionUpdate(TapDragUpdateDetails details) {
if (!delegate.selectionEnabled) {
return;
}
- if (!_isShiftTapping) {
+ final bool isShiftPressed = _containsShift(details.keysPressedOnDown);
+
+ if (!isShiftPressed) {
// Adjust the drag start offset for possible viewport offset changes.
final Offset editableOffset = renderEditable.maxLines == 1
? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
@@ -2461,58 +2547,137 @@
0.0,
_scrollPosition - _dragStartScrollOffset,
);
- return renderEditable.selectPositionAt(
- from: startDetails.globalPosition - editableOffset - scrollableOffset,
- to: updateDetails.globalPosition,
- cause: SelectionChangedCause.drag,
- );
+ final Offset dragStartGlobalPosition = details.globalPosition - details.offsetFromOrigin;
+
+ // Select word by word.
+ if (details.consecutiveTapCount == 2) {
+ return renderEditable.selectWordsInRange(
+ from: dragStartGlobalPosition - editableOffset - scrollableOffset,
+ to: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
+
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ // With a touch device, nothing should happen, unless there was a double tap, or
+ // there was a collapsed selection, and the tap/drag position is at the collapsed selection.
+ // In that case the caret should move with the drag position.
+ //
+ // With a mouse device, a drag should select the range from the origin of the drag
+ // to the current position of the drag.
+ switch (details.kind) {
+ case PointerDeviceKind.mouse:
+ case PointerDeviceKind.trackpad:
+ return renderEditable.selectPositionAt(
+ from: dragStartGlobalPosition - editableOffset - scrollableOffset,
+ to: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ case PointerDeviceKind.stylus:
+ case PointerDeviceKind.invertedStylus:
+ case PointerDeviceKind.touch:
+ case PointerDeviceKind.unknown:
+ _dragBeganOnPreviousSelection ??= _positionOnSelection(dragStartGlobalPosition, _dragStartSelection);
+ assert(_dragBeganOnPreviousSelection != null);
+ if (renderEditable.hasFocus
+ && _dragStartSelection!.isCollapsed
+ && _dragBeganOnPreviousSelection!
+ ) {
+ return renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
+ break;
+ case null:
+ break;
+ }
+ return;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ // With a precise pointer device, such as a mouse, trackpad, or stylus,
+ // the drag will select the text spanning the origin of the drag to the end of the drag.
+ // With a touch device, the cursor should move with the drag.
+ switch (details.kind) {
+ case PointerDeviceKind.mouse:
+ case PointerDeviceKind.trackpad:
+ case PointerDeviceKind.stylus:
+ case PointerDeviceKind.invertedStylus:
+ return renderEditable.selectPositionAt(
+ from: dragStartGlobalPosition - editableOffset - scrollableOffset,
+ to: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ case PointerDeviceKind.touch:
+ case PointerDeviceKind.unknown:
+ if (renderEditable.hasFocus) {
+ return renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
+ break;
+ case null:
+ break;
+ }
+ return;
+ case TargetPlatform.macOS:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ return renderEditable.selectPositionAt(
+ from: dragStartGlobalPosition - editableOffset - scrollableOffset,
+ to: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
}
- if (_shiftTapDragSelection!.isCollapsed
+ if (_dragStartSelection!.isCollapsed
|| (defaultTargetPlatform != TargetPlatform.iOS
&& defaultTargetPlatform != TargetPlatform.macOS)) {
- return _extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag);
+ return _extendSelection(details.globalPosition, SelectionChangedCause.drag);
}
// If the drag inverts the selection, Mac and iOS revert to the initial
// selection.
final TextSelection selection = editableText.textEditingValue.selection;
- final TextPosition nextExtent = renderEditable.getPositionForPoint(updateDetails.globalPosition);
+ final TextPosition nextExtent = renderEditable.getPositionForPoint(details.globalPosition);
final bool isShiftTapDragSelectionForward =
- _shiftTapDragSelection!.baseOffset < _shiftTapDragSelection!.extentOffset;
+ _dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset;
final bool isInverted = isShiftTapDragSelectionForward
- ? nextExtent.offset < _shiftTapDragSelection!.baseOffset
- : nextExtent.offset > _shiftTapDragSelection!.baseOffset;
- if (isInverted && selection.baseOffset == _shiftTapDragSelection!.baseOffset) {
+ ? nextExtent.offset < _dragStartSelection!.baseOffset
+ : nextExtent.offset > _dragStartSelection!.baseOffset;
+ if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) {
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(
selection: TextSelection(
- baseOffset: _shiftTapDragSelection!.extentOffset,
+ baseOffset: _dragStartSelection!.extentOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else if (!isInverted
- && nextExtent.offset != _shiftTapDragSelection!.baseOffset
- && selection.baseOffset != _shiftTapDragSelection!.baseOffset) {
+ && nextExtent.offset != _dragStartSelection!.baseOffset
+ && selection.baseOffset != _dragStartSelection!.baseOffset) {
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(
selection: TextSelection(
- baseOffset: _shiftTapDragSelection!.baseOffset,
+ baseOffset: _dragStartSelection!.baseOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else {
- _extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag);
+ _extendSelection(details.globalPosition, SelectionChangedCause.drag);
}
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
///
- /// By default, it simply cleans up the state used for handling certain
+ /// By default, it cleans up the state used for handling certain
/// built-in behaviors.
///
/// See also:
@@ -2520,10 +2685,12 @@
/// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
/// callback.
@protected
- void onDragSelectionEnd(DragEndDetails details) {
- if (_isShiftTapping) {
- _isShiftTapping = false;
- _shiftTapDragSelection = null;
+ void onDragSelectionEnd(TapDragEndDetails details) {
+ final bool isShiftPressed = _containsShift(details.keysPressedOnDown);
+ _dragBeganOnPreviousSelection = null;
+
+ if (isShiftPressed) {
+ _dragStartSelection = null;
}
}
@@ -2562,8 +2729,8 @@
///
/// An ordinary [GestureDetector] configured to handle events like tap and
/// double tap will only recognize one or the other. This widget detects both:
-/// first the tap and then, if another tap down occurs within a time limit, the
-/// double tap.
+/// the first tap and then any subsequent taps that occurs within a time limit
+/// after the first.
///
/// See also:
///
@@ -2593,12 +2760,12 @@
this.onDragSelectionEnd,
this.behavior,
required this.child,
- }) : assert(child != null);
+ });
/// Called for every tap down including every tap down that's part of a
/// double click or a long press, except touches that include enough movement
/// to not qualify as taps (e.g. pans and flings).
- final GestureTapDownCallback? onTapDown;
+ final GestureTapDragDownCallback? onTapDown;
/// Called when a pointer has tapped down and the force of the pointer has
/// just become greater than [ForcePressGestureRecognizer.startPressure].
@@ -2614,16 +2781,18 @@
/// Called for a tap down event with the secondary mouse button.
final GestureTapDownCallback? onSecondaryTapDown;
- /// Called for each distinct tap except for every second tap of a double tap.
+ /// Called for the first tap in a series of taps, consecutive taps do not call
+ /// this method.
+ ///
/// For example, if the detector was configured with [onTapDown] and
/// [onDoubleTapDown], three quick taps would be recognized as a single tap
- /// down, followed by a double tap down, followed by a single tap down.
- final GestureTapUpCallback? onSingleTapUp;
+ /// down, followed by a tap up, then a double tap down, followed by a single tap down.
+ final GestureTapDragUpCallback? onSingleTapUp;
/// Called for each touch that becomes recognized as a gesture that is not a
/// short tap, such as a long tap or drag. It is called at the moment when
/// another gesture from the touch is recognized.
- final GestureTapCancelCallback? onSingleTapCancel;
+ final GestureCancelCallback? onSingleTapCancel;
/// Called for a single long tap that's sustained for longer than
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
@@ -2638,20 +2807,20 @@
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous short tap.
- final GestureTapDownCallback? onDoubleTapDown;
+ final GestureTapDragDownCallback? onDoubleTapDown;
/// Called when a mouse starts dragging to select text.
- final GestureDragStartCallback? onDragSelectionStart;
+ final GestureTapDragStartCallback? onDragSelectionStart;
/// Called repeatedly as a mouse moves while dragging.
///
/// The frequency of calls is throttled to avoid excessive text layout
/// operations in text fields. The throttling is controlled by the constant
/// [_kDragSelectionUpdateThrottle].
- final DragSelectionUpdateCallback? onDragSelectionUpdate;
+ final GestureTapDragUpdateCallback? onDragSelectionUpdate;
/// Called when a mouse that was previously dragging is released.
- final GestureDragEndCallback? onDragSelectionEnd;
+ final GestureTapDragEndCallback? onDragSelectionEnd;
/// How this gesture detector should behave during hit testing.
///
@@ -2666,100 +2835,50 @@
}
class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetector> {
- // Counts down for a short duration after a previous tap. Null otherwise.
- Timer? _doubleTapTimer;
- Offset? _lastTapOffset;
- // True if a second tap down of a double tap is detected. Used to discard
- // subsequent tap up / tap hold of the same tap.
- bool _isDoubleTap = false;
+ static int? _getDefaultMaxConsecutiveTap() => 2;
@override
void dispose() {
- _doubleTapTimer?.cancel();
- _dragUpdateThrottleTimer?.cancel();
super.dispose();
}
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
- void _handleTapDown(TapDownDetails details) {
+ void _handleTapDown(TapDragDownDetails details) {
widget.onTapDown?.call(details);
// This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
- if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) {
- // If there was already a previous tap, the second down hold/tap is a
- // double tap down.
- widget.onDoubleTapDown?.call(details);
- _doubleTapTimer!.cancel();
- _doubleTapTimeout();
- _isDoubleTap = true;
+ if (details.consecutiveTapCount == 2) {
+ widget.onDoubleTapDown?.call(details);
}
}
- void _handleTapUp(TapUpDetails details) {
- if (!_isDoubleTap) {
+ void _handleTapUp(TapDragUpDetails details) {
+ if (details.consecutiveTapCount == 1) {
widget.onSingleTapUp?.call(details);
- _lastTapOffset = details.globalPosition;
- _doubleTapTimer?.cancel();
- _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
}
- _isDoubleTap = false;
}
void _handleTapCancel() {
widget.onSingleTapCancel?.call();
}
- DragStartDetails? _lastDragStartDetails;
- DragUpdateDetails? _lastDragUpdateDetails;
- Timer? _dragUpdateThrottleTimer;
-
- void _handleDragStart(DragStartDetails details) {
- assert(_lastDragStartDetails == null);
- _lastDragStartDetails = details;
+ void _handleDragStart(TapDragStartDetails details) {
widget.onDragSelectionStart?.call(details);
}
- void _handleDragUpdate(DragUpdateDetails details) {
- _lastDragUpdateDetails = details;
- // Only schedule a new timer if there's no one pending.
- _dragUpdateThrottleTimer ??= Timer(_kDragSelectionUpdateThrottle, _handleDragUpdateThrottled);
+ void _handleDragUpdate(TapDragUpdateDetails details) {
+ widget.onDragSelectionUpdate?.call(details);
}
- /// Drag updates are being throttled to avoid excessive text layouts in text
- /// fields. The frequency of invocations is controlled by the constant
- /// [_kDragSelectionUpdateThrottle].
- ///
- /// Once the drag gesture ends, any pending drag update will be fired
- /// immediately. See [_handleDragEnd].
- void _handleDragUpdateThrottled() {
- assert(_lastDragStartDetails != null);
- assert(_lastDragUpdateDetails != null);
- widget.onDragSelectionUpdate?.call(_lastDragStartDetails!, _lastDragUpdateDetails!);
- _dragUpdateThrottleTimer = null;
- _lastDragUpdateDetails = null;
- }
-
- void _handleDragEnd(DragEndDetails details) {
- assert(_lastDragStartDetails != null);
- if (_dragUpdateThrottleTimer != null) {
- // If there's already an update scheduled, trigger it immediately and
- // cancel the timer.
- _dragUpdateThrottleTimer!.cancel();
- _handleDragUpdateThrottled();
- }
+ void _handleDragEnd(TapDragEndDetails details) {
widget.onDragSelectionEnd?.call(details);
- _dragUpdateThrottleTimer = null;
- _lastDragStartDetails = null;
- _lastDragUpdateDetails = null;
}
void _forcePressStarted(ForcePressDetails details) {
- _doubleTapTimer?.cancel();
- _doubleTapTimer = null;
widget.onForcePressStart?.call(details);
}
@@ -2768,37 +2887,21 @@
}
void _handleLongPressStart(LongPressStartDetails details) {
- if (!_isDoubleTap && widget.onSingleLongTapStart != null) {
+ if (widget.onSingleLongTapStart != null) {
widget.onSingleLongTapStart!(details);
}
}
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
- if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) {
+ if (widget.onSingleLongTapMoveUpdate != null) {
widget.onSingleLongTapMoveUpdate!(details);
}
}
void _handleLongPressEnd(LongPressEndDetails details) {
- if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
+ if (widget.onSingleLongTapEnd != null) {
widget.onSingleLongTapEnd!(details);
}
- _isDoubleTap = false;
- }
-
- void _doubleTapTimeout() {
- _doubleTapTimer = null;
- _lastTapOffset = null;
- }
-
- bool _isWithinDoubleTapTolerance(Offset secondTapOffset) {
- assert(secondTapOffset != null);
- if (_lastTapOffset == null) {
- return false;
- }
-
- final Offset difference = secondTapOffset - _lastTapOffset!;
- return difference.distance <= kDoubleTapSlop;
}
@override
@@ -2810,10 +2913,7 @@
(TapGestureRecognizer instance) {
instance
..onSecondaryTap = widget.onSecondaryTap
- ..onSecondaryTapDown = widget.onSecondaryTapDown
- ..onTapDown = _handleTapDown
- ..onTapUp = _handleTapUp
- ..onTapCancel = _handleTapCancel;
+ ..onSecondaryTapDown = widget.onSecondaryTapDown;
},
);
@@ -2821,7 +2921,7 @@
widget.onSingleLongTapMoveUpdate != null ||
widget.onSingleLongTapEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
- () => LongPressGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.touch),
+ () => LongPressGestureRecognizer(debugOwner: this, supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.touch }),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = _handleLongPressStart
@@ -2834,16 +2934,21 @@
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
- gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
- () => PanGestureRecognizer(debugOwner: this, supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.mouse }),
- (PanGestureRecognizer instance) {
+ gestures[TapAndDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndDragGestureRecognizer>(
+ () => TapAndDragGestureRecognizer(debugOwner: this),
+ (TapAndDragGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
- ..onStart = _handleDragStart
- ..onUpdate = _handleDragUpdate
- ..onEnd = _handleDragEnd;
+ ..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle
+ ..maxConsecutiveTap = _getDefaultMaxConsecutiveTap()
+ ..onTapDown = _handleTapDown
+ ..onDragStart = _handleDragStart
+ ..onDragUpdate = _handleDragUpdate
+ ..onDragEnd = _handleDragEnd
+ ..onTapUp = _handleTapUp
+ ..onCancel = _handleTapCancel;
},
);
}
diff --git a/framework/lib/src/widgets/text_selection_toolbar_layout_delegate.dart b/framework/lib/src/widgets/text_selection_toolbar_layout_delegate.dart
index 7778f96..02ae050 100644
--- a/framework/lib/src/widgets/text_selection_toolbar_layout_delegate.dart
+++ b/framework/lib/src/widgets/text_selection_toolbar_layout_delegate.dart
@@ -41,9 +41,9 @@
/// If not provided, it will be calculated.
final bool? fitsAbove;
- // Return the value that centers width as closely as possible to position
- // while fitting inside of min and max.
- static double _centerOn(double position, double width, double max) {
+ /// Return the value that centers width as closely as possible to position
+ /// while fitting inside of min and max.
+ static double centerOn(double position, double width, double max) {
// If it overflows on the left, put it as far left as possible.
if (position - width / 2.0 < 0.0) {
return 0.0;
@@ -69,7 +69,7 @@
final Offset anchor = fitsAbove ? anchorAbove : anchorBelow;
return Offset(
- _centerOn(
+ centerOn(
anchor.dx,
childSize.width,
size.width,
diff --git a/framework/lib/src/widgets/texture.dart b/framework/lib/src/widgets/texture.dart
index caa4f8c..546f4f4 100644
--- a/framework/lib/src/widgets/texture.dart
+++ b/framework/lib/src/widgets/texture.dart
@@ -40,7 +40,7 @@
required this.textureId,
this.freeze = false,
this.filterQuality = FilterQuality.low,
- }) : assert(textureId != null);
+ });
/// The identity of the backend texture.
final int textureId;
diff --git a/framework/lib/src/widgets/ticker_provider.dart b/framework/lib/src/widgets/ticker_provider.dart
index 00f43d0..7f87844 100644
--- a/framework/lib/src/widgets/ticker_provider.dart
+++ b/framework/lib/src/widgets/ticker_provider.dart
@@ -26,7 +26,7 @@
super.key,
required this.enabled,
required this.child,
- }) : assert(enabled != null);
+ });
/// The requested ticker mode for this subtree.
///
@@ -151,7 +151,7 @@
required this.enabled,
required this.notifier,
required super.child,
- }) : assert(enabled != null);
+ });
final bool enabled;
final ValueNotifier<bool> notifier;
diff --git a/framework/lib/src/widgets/title.dart b/framework/lib/src/widgets/title.dart
index dce6028..088cfce 100644
--- a/framework/lib/src/widgets/title.dart
+++ b/framework/lib/src/widgets/title.dart
@@ -20,8 +20,7 @@
this.title = '',
required this.color,
required this.child,
- }) : assert(title != null),
- assert(color != null && color.alpha == 0xFF);
+ }) : assert(color.alpha == 0xFF);
/// A one-line description of this app for use in the window manager.
/// Must not be null.
diff --git a/framework/lib/src/widgets/transitions.dart b/framework/lib/src/widgets/transitions.dart
index c5616dc..dfcc5d3 100644
--- a/framework/lib/src/widgets/transitions.dart
+++ b/framework/lib/src/widgets/transitions.dart
@@ -22,7 +22,7 @@
/// [ChangeNotifier] and [ValueNotifier].
///
/// [AnimatedWidget] is most useful for widgets that are otherwise stateless. To
-/// use [AnimatedWidget], simply subclass it and implement the build function.
+/// use [AnimatedWidget], subclass it and implement the build function.
///
/// {@tool dartpad}
/// This code defines a widget called `Spinner` that spins a green square
@@ -32,7 +32,7 @@
/// {@end-tool}
///
/// For more complex case involving additional state, consider using
-/// [AnimatedBuilder].
+/// [AnimatedBuilder] or [ListenableBuilder].
///
/// ## Relationship to [ImplicitlyAnimatedWidget]s
///
@@ -55,8 +55,10 @@
/// with subclasses of [ImplicitlyAnimatedWidget] (see above), which are usually
/// named `AnimatedFoo`. Commonly used animated widgets include:
///
-/// * [AnimatedBuilder], which is useful for complex animation use cases and a
-/// notable exception to the naming scheme of [AnimatedWidget] subclasses.
+/// * [ListenableBuilder], which uses a builder pattern that is useful for
+/// complex [Listenable] use cases.
+/// * [AnimatedBuilder], which uses a builder pattern that is useful for
+/// complex [Animation] use cases.
/// * [AlignTransition], which is an animated version of [Align].
/// * [DecoratedBoxTransition], which is an animated version of [DecoratedBox].
/// * [DefaultTextStyleTransition], which is an animated version of
@@ -78,7 +80,7 @@
const AnimatedWidget({
super.key,
required this.listenable,
- }) : assert(listenable != null);
+ });
/// The [Listenable] to which this widget is listening.
///
@@ -97,7 +99,7 @@
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
- properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
+ properties.add(DiagnosticsProperty<Listenable>('listenable', listenable));
}
}
@@ -174,8 +176,7 @@
this.transformHitTests = true,
this.textDirection,
this.child,
- }) : assert(position != null),
- super(listenable: position);
+ }) : super(listenable: position);
/// The animation that controls the position of the child.
///
@@ -256,8 +257,7 @@
this.alignment = Alignment.center,
this.filterQuality,
this.child,
- }) : assert(scale != null),
- super(listenable: scale);
+ }) : super(listenable: scale);
/// The animation that controls the scale of the child.
///
@@ -340,8 +340,7 @@
this.alignment = Alignment.center,
this.filterQuality,
this.child,
- }) : assert(turns != null),
- super(listenable: turns);
+ }) : super(listenable: turns);
/// The animation that controls the rotation of the child.
///
@@ -442,10 +441,7 @@
required Animation<double> sizeFactor,
this.axisAlignment = 0.0,
this.child,
- }) : assert(axis != null),
- assert(sizeFactor != null),
- assert(axisAlignment != null),
- super(listenable: sizeFactor);
+ }) : super(listenable: sizeFactor);
/// [Axis.horizontal] if [sizeFactor] modifies the width, otherwise
/// [Axis.vertical].
@@ -529,7 +525,7 @@
required this.opacity,
this.alwaysIncludeSemantics = false,
super.child,
- }) : assert(opacity != null);
+ });
/// The animation that controls the opacity of the child.
///
@@ -599,8 +595,7 @@
required this.opacity,
this.alwaysIncludeSemantics = false,
Widget? sliver,
- }) : assert(opacity != null),
- super(child: sliver);
+ }) : super(child: sliver);
/// The animation that controls the opacity of the sliver child.
///
@@ -700,8 +695,7 @@
super.key,
required Animation<RelativeRect> rect,
required this.child,
- }) : assert(rect != null),
- super(listenable: rect);
+ }) : super(listenable: rect);
/// The animation that controls the child's size and position.
Animation<RelativeRect> get rect => listenable as Animation<RelativeRect>;
@@ -760,10 +754,7 @@
required Animation<Rect?> rect,
required this.size,
required this.child,
- }) : assert(rect != null),
- assert(size != null),
- assert(child != null),
- super(listenable: rect);
+ }) : super(listenable: rect);
/// The animation that controls the child's size and position.
///
@@ -830,9 +821,7 @@
required this.decoration,
this.position = DecorationPosition.background,
required this.child,
- }) : assert(decoration != null),
- assert(child != null),
- super(listenable: decoration);
+ }) : super(listenable: decoration);
/// Animation of the decoration to paint.
///
@@ -898,9 +887,7 @@
required this.child,
this.widthFactor,
this.heightFactor,
- }) : assert(alignment != null),
- assert(child != null),
- super(listenable: alignment);
+ }) : super(listenable: alignment);
/// The animation that controls the child's alignment.
Animation<AlignmentGeometry> get alignment => listenable as Animation<AlignmentGeometry>;
@@ -954,9 +941,7 @@
this.softWrap = true,
this.overflow = TextOverflow.clip,
this.maxLines,
- }) : assert(style != null),
- assert(child != null),
- super(listenable: style);
+ }) : super(listenable: style);
/// The animation that controls the descendants' text style.
Animation<TextStyle> get style => listenable as Animation<TextStyle>;
@@ -996,11 +981,109 @@
}
}
+/// A general-purpose widget for building a widget subtree when a [Listenable]
+/// changes.
+///
+/// [ListenableBuilder] is useful for more complex widgets that wish to listen
+/// to changes in other objects as part of a larger build function. To use
+/// [ListenableBuilder], construct the widget and pass it a [builder]
+/// function.
+///
+/// Any subtype of [Listenable] (such as a [ChangeNotifier], [ValueNotifier], or
+/// [Animation]) can be used with a [ListenableBuilder] to rebuild only certain
+/// parts of a widget when the [Listenable] notifies its listeners. Although
+/// they have identical implementations, if an [Animation] is being listened to,
+/// consider using an [AnimatedBuilder] instead for better readability.
+///
+/// ## Performance optimizations
+///
+/// {@template flutter.widgets.transitions.ListenableBuilder.optimizations}
+/// If the [builder] function contains a subtree that does not depend on the
+/// [listenable], it's often more efficient to build that subtree once instead
+/// of rebuilding it on every change of the listenable.
+///
+/// If a pre-built subtree is passed as the [child] parameter, the
+/// [ListenableBuilder] will pass it back to the [builder] function so that it
+/// can be incorporated into the build.
+///
+/// Using this pre-built [child] is entirely optional, but can improve
+/// performance significantly in some cases and is therefore a good practice.
+/// {@endtemplate}
+///
+/// {@tool dartpad}
+/// This example shows how a [ListenableBuilder] can be used to listen to a
+/// [FocusNode] (which is also a [ChangeNotifier]) to see when a subtree has
+/// focus, and modify a decoration when its focus state changes.
+///
+/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.0.dart **
+/// {@end-tool}
+///
+/// {@template flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild}
+/// ## Improve rebuild performance
+///
+/// Performance can be improved by specifying any widgets that don't need to
+/// change as a result of changes in the listener as the prebuilt
+/// [ListenableBuilder.child] attribute.
+///
+/// {@tool dartpad}
+/// The following example implements a simple counter that utilizes a
+/// [ListenableBuilder] to limit rebuilds to only the [Text] widget containing
+/// the count. The current count is stored in a [ValueNotifier], which rebuilds
+/// the [ListenableBuilder]'s contents when its value is changed.
+///
+/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.1.dart **
+/// {@end-tool}
+/// {@endtemplate}
+///
+/// See also:
+///
+/// * [AnimatedBuilder], which has the same functionality, but is named more
+/// appropriately for a builder triggered by [Animation]s.
+class ListenableBuilder extends AnimatedWidget {
+ /// Creates a builder that responds to changes in [listenable].
+ ///
+ /// The [listenable] and [builder] arguments must not be null.
+ const ListenableBuilder({
+ super.key,
+ required super.listenable,
+ required this.builder,
+ this.child,
+ });
+
+ // Overridden getter to replace with documentation tailored to
+ // ListenableBuilder.
+
+ /// The [Listenable] supplied to the constructor.
+ ///
+ /// Also accessible through the [listenable] getter.
+ ///
+ /// See also:
+ ///
+ /// * [AnimatedBuilder], a widget with an identical functionality commonly
+ /// used with [Animation] [Listenable]s for better readability.
+ @override
+ Listenable get listenable => super.listenable;
+
+ /// Called every time the [listenable] notifies about a change.
+ ///
+ /// The child given to the builder should typically be part of the returned
+ /// widget tree.
+ final TransitionBuilder builder;
+
+ /// The child widget to pass to the [builder].
+ ///
+ /// {@macro flutter.widgets.transitions.ListenableBuilder.optimizations}
+ final Widget? child;
+
+ @override
+ Widget build(BuildContext context) => builder(context, child);
+}
+
/// A general-purpose widget for building animations.
///
-/// AnimatedBuilder is useful for more complex widgets that wish to include
-/// an animation as part of a larger build function. To use AnimatedBuilder,
-/// simply construct the widget and pass it a builder function.
+/// [AnimatedBuilder] is useful for more complex widgets that wish to include
+/// an animation as part of a larger build function. To use [AnimatedBuilder],
+/// construct the widget and pass it a builder function.
///
/// For simple cases without additional state, consider using
/// [AnimatedWidget].
@@ -1009,16 +1092,18 @@
///
/// ## Performance optimizations
///
-/// If your [builder] function contains a subtree that does not depend on the
-/// animation, it's more efficient to build that subtree once instead of
-/// rebuilding it on every animation tick.
+/// {@template flutter.widgets.transitions.AnimatedBuilder.optimizations}
+/// If the [builder] function contains a subtree that does not depend on the
+/// animation passed to the constructor, it's more efficient to build that
+/// subtree once instead of rebuilding it on every animation tick.
///
-/// If you pass the pre-built subtree as the [child] parameter, the
-/// [AnimatedBuilder] will pass it back to your builder function so that you
-/// can incorporate it into your build.
+/// If a pre-built subtree is passed as the [child] parameter, the
+/// [AnimatedBuilder] will pass it back to the [builder] function so that it can
+/// be incorporated into the build.
///
/// Using this pre-built child is entirely optional, but can improve
/// performance significantly in some cases and is therefore a good practice.
+/// {@endtemplate}
///
/// {@tool dartpad}
/// This code defines a widget that spins a green square continually. It is
@@ -1028,61 +1113,64 @@
/// ** See code in examples/api/lib/widgets/transitions/animated_builder.0.dart **
/// {@end-tool}
///
-/// {@template flutter.flutter.animatedbuilder_changenotifier.rebuild}
-/// ## Improve rebuilds performance using AnimatedBuilder
+/// ## Improve rebuild performance
///
-/// Despite the name, [AnimatedBuilder] is not limited to [Animation]s. Any subtype
-/// of [Listenable] (such as [ChangeNotifier] and [ValueNotifier]) can be used with
-/// an [AnimatedBuilder] to rebuild only certain parts of a widget when the
-/// [Listenable] notifies its listeners. This technique is a performance improvement
-/// that allows rebuilding only specific widgets leaving others untouched.
+/// Despite the name, [AnimatedBuilder] is not limited to [Animation]s, any
+/// subtype of [Listenable] (such as [ChangeNotifier] or [ValueNotifier]) can be
+/// used to trigger rebuilds. Although they have identical implementations, if
+/// an [Animation] is not being listened to, consider using a
+/// [ListenableBuilder] for better readability.
+///
+/// You can use an [AnimatedBuilder] or [ListenableBuilder] to rebuild only
+/// certain parts of a widget when the [Listenable] notifies its listeners. You
+/// can improve performance by specifying any widgets that don't need to change
+/// as a result of changes in the listener as the prebuilt [child] attribute.
///
/// {@tool dartpad}
/// The following example implements a simple counter that utilizes an
-/// [AnimatedBuilder] to limit rebuilds to only the [Text] widget. The current count
-/// is stored in a [ValueNotifier], which rebuilds the [AnimatedBuilder]'s contents
-/// when its value is changed.
+/// [AnimatedBuilder] to limit rebuilds to only the [Text] widget. The current
+/// count is stored in a [ValueNotifier], which rebuilds the [AnimatedBuilder]'s
+/// contents when its value is changed.
///
-/// ** See code in examples/api/lib/foundation/change_notifier/change_notifier.0.dart **
+/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.1.dart **
/// {@end-tool}
-/// {@endtemplate}
///
/// See also:
///
-/// * [TweenAnimationBuilder], which animates a property to a target value
-/// without requiring manual management of an [AnimationController].
-class AnimatedBuilder extends AnimatedWidget {
+/// * [ListenableBuilder], a widget with similar functionality, but is named
+/// more appropriately for a builder triggered on changes in [Listenable]s
+/// that aren't [Animation]s.
+/// * [TweenAnimationBuilder], which animates a property to a target value
+/// without requiring manual management of an [AnimationController].
+class AnimatedBuilder extends ListenableBuilder {
/// Creates an animated builder.
///
- /// The [animation] and [builder] arguments must not be null.
+ /// The [animation] and [builder] arguments are required.
const AnimatedBuilder({
super.key,
required Listenable animation,
- required this.builder,
- this.child,
- }) : assert(animation != null),
- assert(builder != null),
- super(listenable: animation);
+ required super.builder,
+ super.child,
+ }) : super(listenable: animation);
- /// Called every time the animation changes value.
- final TransitionBuilder builder;
+ /// The [Listenable] supplied to the constructor (typically an [Animation]).
+ ///
+ /// Also accessible through the [listenable] getter.
+ ///
+ /// See also:
+ ///
+ /// * [ListenableBuilder], a widget with similar functionality commonly used
+ /// with [Listenable]s (such as [ChangeNotifier]) for better readability
+ /// when the [animation] isn't an [Animation].
+ Listenable get animation => super.listenable;
- /// The child widget to pass to the [builder].
- ///
- /// If a [builder] callback's return value contains a subtree that does not
- /// depend on the animation, it's more efficient to build that subtree once
- /// instead of rebuilding it on every animation tick.
- ///
- /// If the pre-built subtree is passed as the [child] parameter, the
- /// [AnimatedBuilder] will pass it back to the [builder] function so that it
- /// can be incorporated into the build.
- ///
- /// Using this pre-built child is entirely optional, but can improve
- /// performance significantly in some cases and is therefore a good practice.
- final Widget? child;
+ // Overridden getter to replace with documentation tailored to
+ // AnimatedBuilder.
+ /// Called every time the [animation] notifies about a change.
+ ///
+ /// The child given to the builder should typically be part of the returned
+ /// widget tree.
@override
- Widget build(BuildContext context) {
- return builder(context, child);
- }
+ TransitionBuilder get builder => super.builder;
}
diff --git a/framework/lib/src/widgets/tween_animation_builder.dart b/framework/lib/src/widgets/tween_animation_builder.dart
index 4d3a428..d660c84 100644
--- a/framework/lib/src/widgets/tween_animation_builder.dart
+++ b/framework/lib/src/widgets/tween_animation_builder.dart
@@ -108,9 +108,7 @@
required this.builder,
super.onEnd,
this.child,
- }) : assert(tween != null),
- assert(curve != null),
- assert(builder != null);
+ });
/// Defines the target value for the animation.
///
diff --git a/framework/lib/src/widgets/unique_widget.dart b/framework/lib/src/widgets/unique_widget.dart
index ee90cb5..fa6c078 100644
--- a/framework/lib/src/widgets/unique_widget.dart
+++ b/framework/lib/src/widgets/unique_widget.dart
@@ -24,8 +24,7 @@
/// inflated instance of this widget.
const UniqueWidget({
required GlobalKey<T> key,
- }) : assert(key != null),
- super(key: key);
+ }) : super(key: key);
@override
T createState();
diff --git a/framework/lib/src/widgets/value_listenable_builder.dart b/framework/lib/src/widgets/value_listenable_builder.dart
index 0ff24fb..4b5a54d 100644
--- a/framework/lib/src/widgets/value_listenable_builder.dart
+++ b/framework/lib/src/widgets/value_listenable_builder.dart
@@ -119,8 +119,7 @@
required this.valueListenable,
required this.builder,
this.child,
- }) : assert(valueListenable != null),
- assert(builder != null);
+ });
/// The [ValueListenable] whose value you depend on in order to build.
///
@@ -141,10 +140,11 @@
/// A [valueListenable]-independent widget which is passed back to the [builder].
///
- /// This argument is optional and can be null if the entire widget subtree
- /// the [builder] builds depends on the value of the [valueListenable]. For
- /// example, if the [valueListenable] is a [String] and the [builder] simply
- /// returns a [Text] widget with the [String] value.
+ /// This argument is optional and can be null if the entire widget subtree the
+ /// [builder] builds depends on the value of the [valueListenable]. For
+ /// example, in the case where the [valueListenable] is a [String] and the
+ /// [builder] returns a [Text] widget with the current [String] value, there
+ /// would be no useful [child].
final Widget? child;
@override
diff --git a/framework/lib/src/widgets/view.dart b/framework/lib/src/widgets/view.dart
new file mode 100644
index 0000000..4e73d22
--- /dev/null
+++ b/framework/lib/src/widgets/view.dart
@@ -0,0 +1,118 @@
+// 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:engine/ui.dart' show FlutterView;
+
+import 'framework.dart';
+import 'lookup_boundary.dart';
+import 'media_query.dart';
+
+/// Injects a [FlutterView] into the tree and makes it available to descendants
+/// within the same [LookupBoundary] via [View.of] and [View.maybeOf].
+///
+/// The provided [child] is wrapped in a [MediaQuery] constructed from the given
+/// [view].
+///
+/// In a future version of Flutter, the functionality of this widget will be
+/// extended to actually bootstrap the render tree that is going to be rendered
+/// into the provided [view]. This will enable rendering content into multiple
+/// [FlutterView]s from a single widget tree.
+///
+/// Each [FlutterView] can be associated with at most one [View] widget in the
+/// widget tree. Two or more [View] widgets configured with the same
+/// [FlutterView] must never exist within the same widget tree at the same time.
+/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives
+/// its identity from the [view] provided to this widget.
+class View extends StatelessWidget {
+ /// Injects the provided [view] into the widget tree.
+ View({required this.view, required this.child}) : super(key: GlobalObjectKey(view));
+
+ /// The [FlutterView] to be injected into the tree.
+ final FlutterView view;
+
+ /// {@macro flutter.widgets.ProxyWidget.child}
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ return _ViewScope(
+ view: view,
+ child: MediaQuery.fromView(
+ view: view,
+ child: child,
+ ),
+ );
+ }
+
+ /// Returns the [FlutterView] that the provided `context` will render into.
+ ///
+ /// Returns null if the `context` is not associated with a [FlutterView].
+ ///
+ /// The method creates a dependency on the `context`, which will be informed
+ /// when the identity of the [FlutterView] changes (i.e. the `context` is
+ /// moved to render into a different [FlutterView] then before). The context
+ /// will not be informed when the properties on the [FlutterView] itself
+ /// change their values. To access the property values of a [FlutterView] it
+ /// is best practise to use [MediaQuery.maybeOf] instead, which will ensure
+ /// that the `context` is informed when the view properties change.
+ ///
+ /// See also:
+ ///
+ /// * [View.of], which throws instead of returning null if no [FlutterView]
+ /// is found.
+ static FlutterView? maybeOf(BuildContext context) {
+ return LookupBoundary.dependOnInheritedWidgetOfExactType<_ViewScope>(context)?.view;
+ }
+
+ /// Returns the [FlutterView] that the provided `context` will render into.
+ ///
+ /// Throws if the `context` is not associated with a [FlutterView].
+ ///
+ /// The method creates a dependency on the `context`, which will be informed
+ /// when the identity of the [FlutterView] changes (i.e. the `context` is
+ /// moved to render into a different [FlutterView] then before). The context
+ /// will not be informed when the properties on the [FlutterView] itself
+ /// change their values. To access the property values of a [FlutterView] it
+ /// is best practise to use [MediaQuery.of] instead, which will ensure that
+ /// the `context` is informed when the view properties change.
+ ///
+ /// See also:
+ ///
+ /// * [View.maybeOf], which throws instead of returning null if no
+ /// [FlutterView] is found.
+ static FlutterView of(BuildContext context) {
+ final FlutterView? result = maybeOf(context);
+ assert(() {
+ if (result == null) {
+ final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<_ViewScope>(context);
+ final List<DiagnosticsNode> information = <DiagnosticsNode>[
+ if (hiddenByBoundary) ...<DiagnosticsNode>[
+ ErrorSummary('View.of() was called with a context that does not have access to a View widget.'),
+ ErrorDescription('The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.'),
+ ] else ...<DiagnosticsNode>[
+ ErrorSummary('View.of() was called with a context that does not contain a View widget.'),
+ ErrorDescription('No View widget ancestor could be found starting from the context that was passed to View.of().'),
+ ],
+ ErrorDescription(
+ 'The context used was:\n'
+ ' $context',
+ ),
+ ErrorHint('This usually means that the provided context is not associated with a View.'),
+ ];
+ throw FlutterError.fromParts(information);
+ }
+ return true;
+ }());
+ return result!;
+ }
+}
+
+class _ViewScope extends InheritedWidget {
+ const _ViewScope({required this.view, required super.child});
+
+ final FlutterView view;
+
+ @override
+ bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view;
+}
diff --git a/framework/lib/src/widgets/viewport.dart b/framework/lib/src/widgets/viewport.dart
index 6b1494c..c0127de 100644
--- a/framework/lib/src/widgets/viewport.dart
+++ b/framework/lib/src/widgets/viewport.dart
@@ -67,12 +67,8 @@
this.cacheExtentStyle = CacheExtentStyle.pixel,
this.clipBehavior = Clip.hardEdge,
List<Widget> slivers = const <Widget>[],
- }) : assert(offset != null),
- assert(slivers != null),
- assert(center == null || slivers.where((Widget child) => child.key == center).length == 1),
- assert(cacheExtentStyle != null),
+ }) : assert(center == null || slivers.where((Widget child) => child.key == center).length == 1),
assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
- assert(clipBehavior != null),
super(children: slivers);
/// The direction in which the [offset]'s [ViewportOffset.pixels] increases.
@@ -142,7 +138,6 @@
/// This depends on the [Directionality] if the `axisDirection` is vertical;
/// otherwise, the default cross axis direction is downwards.
static AxisDirection getDefaultCrossAxisDirection(BuildContext context, AxisDirection axisDirection) {
- assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
assert(debugCheckHasDirectionality(
@@ -329,15 +324,14 @@
/// rebuild this widget when the [offset] changes.
///
/// The [offset] argument must not be null.
- ShrinkWrappingViewport({
+ const ShrinkWrappingViewport({
super.key,
this.axisDirection = AxisDirection.down,
this.crossAxisDirection,
required this.offset,
this.clipBehavior = Clip.hardEdge,
List<Widget> slivers = const <Widget>[],
- }) : assert(offset != null),
- super(children: slivers);
+ }) : super(children: slivers);
/// The direction in which the [offset]'s [ViewportOffset.pixels] increases.
///
diff --git a/framework/lib/src/widgets/visibility.dart b/framework/lib/src/widgets/visibility.dart
index 2be5cdf..d914049 100644
--- a/framework/lib/src/widgets/visibility.dart
+++ b/framework/lib/src/widgets/visibility.dart
@@ -62,13 +62,7 @@
this.maintainSize = false,
this.maintainSemantics = false,
this.maintainInteractivity = false,
- }) : assert(child != null),
- assert(replacement != null),
- assert(visible != null),
- assert(maintainState != null),
- assert(maintainAnimation != null),
- assert(maintainSize != null),
- assert(
+ }) : assert(
maintainState == true || maintainAnimation == false,
'Cannot maintain animations if the state is not also maintained.',
),
@@ -98,10 +92,7 @@
required this.child,
this.replacement = const SizedBox.shrink(),
this.visible = true,
- }) : assert(child != null),
- assert(replacement != null),
- assert(visible != null),
- maintainState = true,
+ }) : maintainState = true,
maintainAnimation = true,
maintainSize = true,
maintainSemantics = true,
@@ -329,15 +320,7 @@
this.maintainSize = false,
this.maintainSemantics = false,
this.maintainInteractivity = false,
- }) : assert(sliver != null),
- assert(replacementSliver != null),
- assert(visible != null),
- assert(maintainState != null),
- assert(maintainAnimation != null),
- assert(maintainSize != null),
- assert(maintainSemantics != null),
- assert(maintainInteractivity != null),
- assert(
+ }) : assert(
maintainState == true || maintainAnimation == false,
'Cannot maintain animations if the state is not also maintained.',
),
@@ -367,10 +350,7 @@
required this.sliver,
this.replacementSliver = const SliverToBoxAdapter(),
this.visible = true,
- }) : assert(sliver != null),
- assert(replacementSliver != null),
- assert(visible != null),
- maintainState = true,
+ }) : maintainState = true,
maintainAnimation = true,
maintainSize = true,
maintainSemantics = true,
diff --git a/framework/lib/src/widgets/widget_inspector.dart b/framework/lib/src/widgets/widget_inspector.dart
index 7124150..ee4922e 100644
--- a/framework/lib/src/widgets/widget_inspector.dart
+++ b/framework/lib/src/widgets/widget_inspector.dart
@@ -10,6 +10,7 @@
import 'package:engine/ui.dart' as ui
show
ClipOp,
+ FlutterView,
Image,
ImageByteFormat,
Paragraph,
@@ -30,6 +31,7 @@
import 'framework.dart';
import 'gesture_detector.dart';
import 'service_extensions.dart';
+import 'view.dart';
/// Signature for the builder callback used by
/// [WidgetInspector.selectButtonBuilder].
@@ -77,9 +79,7 @@
_MulticastCanvas({
required Canvas main,
required Canvas screenshot,
- }) : assert(main != null),
- assert(screenshot != null),
- _main = main,
+ }) : _main = main,
_screenshot = screenshot;
final Canvas _main;
@@ -335,8 +335,7 @@
class _ScreenshotData {
_ScreenshotData({
required this.target,
- }) : assert(target != null),
- containerLayer = _ScreenshotContainerLayer();
+ }) : containerLayer = _ScreenshotContainerLayer();
/// Target to take a screenshot of.
final RenderObject target;
@@ -552,10 +551,9 @@
bool debugPaint = false,
}) {
RenderObject repaintBoundary = renderObject;
- while (repaintBoundary != null && !repaintBoundary.isRepaintBoundary) {
+ while (!repaintBoundary.isRepaintBoundary) {
repaintBoundary = repaintBoundary.parent! as RenderObject;
}
- assert(repaintBoundary != null);
final _ScreenshotData data = _ScreenshotData(target: renderObject);
final _ScreenshotPaintingContext context = _ScreenshotPaintingContext(
containerLayer: repaintBoundary.debugLayer!,
@@ -623,8 +621,7 @@
required this.node,
required this.children,
this.childIndex,
- }) : assert(node != null),
- assert(children != null);
+ });
/// Node at the point in the path this [_DiagnosticsPathNode] is describing.
final DiagnosticsNode node;
@@ -827,9 +824,6 @@
required AsyncValueGetter<bool> getter,
required AsyncValueSetter<bool> setter,
}) {
- assert(name != null);
- assert(getter != null);
- assert(setter != null);
registerServiceExtension(
name: name,
callback: (Map<String, String> parameters) async {
@@ -1331,8 +1325,7 @@
/// appropriate to display the Widget tree in the inspector.
@protected
bool isWidgetTreeReady([ String? groupName ]) {
- return WidgetsBinding.instance != null &&
- WidgetsBinding.instance.debugDidSendFirstFrameEvent;
+ return WidgetsBinding.instance.debugDidSendFirstFrameEvent;
}
/// Returns the Dart object associated with a reference id.
@@ -1498,13 +1491,13 @@
return false;
}
selection.currentElement = object;
- developer.inspect(selection.currentElement);
+ _sendInspectEvent(selection.currentElement);
} else {
if (object == selection.current) {
return false;
}
selection.current = object! as RenderObject;
- developer.inspect(selection.current);
+ _sendInspectEvent(selection.current);
}
if (selectionChangedCallback != null) {
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
@@ -1523,6 +1516,25 @@
return false;
}
+ /// Notify attached tools to navigate to an object's source location.
+ void _sendInspectEvent(Object? object){
+ inspect(object);
+
+ final _Location? location = _getSelectedSummaryWidgetLocation(null);
+ if (location != null) {
+ postEvent(
+ 'navigate',
+ <String, Object>{
+ 'fileUri': location.file, // URI file path of the location.
+ 'line': location.line, // 1-based line number.
+ 'column': location.column, // 1-based column number.
+ 'source': 'flutter.inspector',
+ },
+ stream: 'ToolEvent',
+ );
+ }
+ }
+
/// Returns a DevTools uri linking to a specific element on the inspector page.
String? _devToolsInspectorUriForElement(Element element) {
if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
@@ -1977,7 +1989,6 @@
if (renderObject.debugNeedsLayout) {
final PipelineOwner owner = renderObject.owner!;
- assert(owner != null);
assert(!owner.debugDoingLayout);
owner
..flushLayout()
@@ -2212,9 +2223,16 @@
}
Map<String, Object?>? _getSelectedWidget(String? previousSelectionId, String groupName) {
+ return _nodeToJson(
+ _getSelectedWidgetDiagnosticsNode(previousSelectionId),
+ InspectorSerializationDelegate(groupName: groupName, service: this),
+ );
+ }
+
+ DiagnosticsNode? _getSelectedWidgetDiagnosticsNode(String? previousSelectionId) {
final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
final Element? current = selection.currentElement;
- return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this));
+ return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
}
/// Returns a [DiagnosticsNode] representing the currently selected [Element]
@@ -2229,9 +2247,13 @@
return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName));
}
- Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
+ _Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) {
+ return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value);
+ }
+
+ DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) {
if (!isWidgetCreationTracked()) {
- return _getSelectedWidget(previousSelectionId, groupName);
+ return _getSelectedWidgetDiagnosticsNode(previousSelectionId);
}
final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
Element? current = selection.currentElement;
@@ -2245,7 +2267,11 @@
}
current = firstLocal;
}
- return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this));
+ return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
+ }
+
+ Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
+ return _nodeToJson(_getSelectedSummaryDiagnosticsNode(previousSelectionId), InspectorSerializationDelegate(groupName: groupName, service: this));
}
/// Returns whether [Widget] creation locations are available.
@@ -2279,12 +2305,27 @@
}
/// All events dispatched by a [WidgetInspectorService] use this method
- /// instead of calling [developer.postEvent] directly so that tests for
- /// [WidgetInspectorService] can track which events were dispatched by
- /// overriding this method.
+ /// instead of calling [developer.postEvent] directly.
+ ///
+ /// This allows tests for [WidgetInspectorService] to track which events were
+ /// dispatched by overriding this method.
@protected
- void postEvent(String eventKind, Map<Object, Object?> eventData) {
- developer.postEvent(eventKind, eventData);
+ void postEvent(
+ String eventKind,
+ Map<Object, Object?> eventData, {
+ String stream = 'Extension',
+ }) {
+ developer.postEvent(eventKind, eventData, stream: stream);
+ }
+
+ /// All events dispatched by a [WidgetInspectorService] use this method
+ /// instead of calling [developer.inspect].
+ ///
+ /// This allows tests for [WidgetInspectorService] to track which events were
+ /// dispatched by overriding this method.
+ @protected
+ void inspect(Object? object) {
+ developer.inspect(object);
}
final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
@@ -2559,7 +2600,7 @@
super.key,
required this.child,
required this.selectButtonBuilder,
- }) : assert(child != null);
+ });
/// The widget that is being inspected.
final Widget child;
@@ -2637,7 +2678,6 @@
final List<DiagnosticsNode> children = object.debugDescribeChildren();
for (int i = children.length - 1; i >= 0; i -= 1) {
final DiagnosticsNode diagnostics = children[i];
- assert(diagnostics != null);
if (diagnostics.style == DiagnosticsTreeStyle.offstage ||
diagnostics.value is! RenderObject) {
continue;
@@ -2726,7 +2766,8 @@
// on the edge of the display. If the pointer is being dragged off the edge
// of the display we do not want to select anything. A user can still select
// a widget that is only at the exact screen margin by tapping.
- final Rect bounds = (Offset.zero & (WidgetsBinding.instance.window.physicalSize / WidgetsBinding.instance.window.devicePixelRatio)).deflate(_kOffScreenMargin);
+ final ui.FlutterView view = View.of(context);
+ final Rect bounds = (Offset.zero & (view.physicalSize / view.devicePixelRatio)).deflate(_kOffScreenMargin);
if (!bounds.contains(_lastPointerLocation!)) {
setState(() {
selection.clear();
@@ -2740,9 +2781,7 @@
}
if (_lastPointerLocation != null) {
_inspectAt(_lastPointerLocation!);
-
- // Notify debuggers to open an inspector on the object.
- developer.inspect(selection.current);
+ WidgetInspectorService.instance._sendInspectEvent(selection.current);
}
setState(() {
// Only exit select mode if there is a button to return to select mode.
@@ -2890,8 +2929,7 @@
class _RenderInspectorOverlay extends RenderBox {
/// The arguments must not be null.
_RenderInspectorOverlay({ required InspectorSelection selection })
- : _selection = selection,
- assert(selection != null);
+ : _selection = selection;
InspectorSelection get selection => _selection;
InspectorSelection _selection;
@@ -2999,8 +3037,7 @@
required this.overlayRect,
required this.selection,
required this.rootRenderObject,
- }) : assert(overlayRect != null),
- assert(selection != null) {
+ }) {
bool inDebugMode = false;
assert(() {
inDebugMode = true;
@@ -3440,9 +3477,7 @@
///
/// The `description` and `url` arguments must not be null.
DevToolsDeepLinkProperty(String description, String url)
- : assert(description != null),
- assert(url != null),
- super('', url, description: description, level: DiagnosticLevel.info);
+ : super('', url, description: description, level: DiagnosticLevel.info);
}
/// Returns if an object is user created.
diff --git a/framework/lib/src/widgets/widget_span.dart b/framework/lib/src/widgets/widget_span.dart
index 3b37366..87620ef 100644
--- a/framework/lib/src/widgets/widget_span.dart
+++ b/framework/lib/src/widgets/widget_span.dart
@@ -77,8 +77,7 @@
super.alignment,
super.baseline,
super.style,
- }) : assert(child != null),
- assert(
+ }) : assert(
baseline != null || !(
identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
identical(alignment, ui.PlaceholderAlignment.belowBaseline) ||
diff --git a/framework/lib/src/widgets/will_pop_scope.dart b/framework/lib/src/widgets/will_pop_scope.dart
index d620939..ab90c7f 100644
--- a/framework/lib/src/widgets/will_pop_scope.dart
+++ b/framework/lib/src/widgets/will_pop_scope.dart
@@ -33,7 +33,7 @@
super.key,
required this.child,
required this.onWillPop,
- }) : assert(child != null);
+ });
/// The widget below this widget in the tree.
///
diff --git a/framework/lib/src/widgets/window.dart b/framework/lib/src/widgets/window.dart
new file mode 100644
index 0000000..873df8f
--- /dev/null
+++ b/framework/lib/src/widgets/window.dart
@@ -0,0 +1,8 @@
+// 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.
+
+/// Placeholder to be used in a future version of Flutter.
+abstract class Window {
+ const Window._();
+}
diff --git a/framework/lib/widgets.dart b/framework/lib/widgets.dart
index 03fe927..27b8865 100644
--- a/framework/lib/widgets.dart
+++ b/framework/lib/widgets.dart
@@ -72,6 +72,7 @@
export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart';
+export 'src/widgets/lookup_boundary.dart';
export 'src/widgets/magnifier.dart';
export 'src/widgets/media_query.dart';
export 'src/widgets/modal_barrier.dart';
@@ -134,6 +135,7 @@
export 'src/widgets/spell_check.dart';
export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart';
+export 'src/widgets/tap_and_drag_gestures.dart';
export 'src/widgets/tap_region.dart';
export 'src/widgets/text.dart';
export 'src/widgets/text_editing_intents.dart';
@@ -147,6 +149,7 @@
export 'src/widgets/tween_animation_builder.dart';
export 'src/widgets/unique_widget.dart';
export 'src/widgets/value_listenable_builder.dart';
+export 'src/widgets/view.dart';
export 'src/widgets/viewport.dart';
export 'src/widgets/visibility.dart';
export 'src/widgets/widget_inspector.dart';
diff --git a/script/bin/sync.dart b/script/bin/sync.dart
index 12a1bd0..673d371 100644
--- a/script/bin/sync.dart
+++ b/script/bin/sync.dart
@@ -1,3 +1,5 @@
+import 'dart:io' as io;
+
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as p;
@@ -39,18 +41,27 @@
String source = file.readAsStringSync();
source = source.replaceAll("'package:flutter/", "'package:flute/");
source = source.replaceAll('exception is NullThrownError', 'false');
- if (relPath == r'src\material\dialog.dart' ||
- relPath == r'src\material\navigation_rail.dart' ||
- relPath == r'src\material\switch.dart' ||
- relPath == r'src\widgets\icon.dart') {
+ if (relPath == p.join('src', 'material', 'dialog.dart') ||
+ relPath == p.join('src', 'material', 'navigation_rail.dart') ||
+ relPath == p.join('src', 'material', 'switch.dart') ||
+ relPath == p.join('src', 'widgets', 'icon.dart') ||
+ relPath == p.join('src', 'material', 'time_picker.dart') ||
+ relPath == p.join('src', 'material', 'time_picker_theme.dart')) {
source = source.replaceAll("'dart:ui'", "'package:engine/ui.dart' hide TextStyle");
} else {
source = source.replaceAll("'dart:ui'", "'package:engine/ui.dart'");
}
- if (relPath != r'src\foundation\bitfield.dart' &&
- relPath != r'src\foundation\platform.dart' &&
- relPath != r'src\services\platform_channel.dart') {
+ if (relPath == p.join('src', 'foundation', 'math.dart')) {
+ source = source.split('\n').where((String line) {
+ return line.trim().isEmpty || line.trim().startsWith('//');
+ }).join('\n');
+ source += "\nexport 'package:engine/ui.dart' show clampDouble;";
+ }
+
+ if (relPath != p.join('src', 'foundation', 'bitfield.dart') &&
+ relPath != p.join('src', 'foundation', 'platform.dart') &&
+ relPath != p.join('src', 'services', 'platform_channel.dart')) {
source = source.split('\n').map<String>((String line) {
final int indexOfConditionalImport = line.indexOf(r'if (dart.library');
if (indexOfConditionalImport == -1) {
@@ -67,7 +78,7 @@
}).join('\n');
}
- if (relPath == r'src\foundation\_platform_web.dart') {
+ if (relPath == p.join('src', 'foundation', '_platform_web.dart')) {
source = source.replaceAll('if (ui.debugEmulateFlutterTesterEnvironment as bool)', 'if (true)');
}
@@ -90,7 +101,9 @@
}
Future<Directory> _findFlutterRepo() async {
- final String where = (await pm.run(<String>['where', 'flutter'])).stdout as String;
+ final String whereOrWhich = io.Platform.operatingSystem == 'window'
+ ? 'where' : 'which';
+ final String where = (await pm.run(<String>[whereOrWhich, 'flutter'])).stdout as String;
final String pathToFlutterBin = where.split('\n').first;
final Directory bin = fs.directory(p.dirname(pathToFlutterBin));
return bin.parent;