Magnifier cleanup (#143558)
- Introduces the ability to control whether RawMagnifier uses a clip on its decoration. (It still has to use a clip for its magnified image.) - Uses `BlurStyle.outer` to remove the clip for CupertinoMagnifier. - Many changes to the documentation around magnifiers and shadows. - Implements `BoxShadow.copyWith` which somehow we had never needed before. - Makes `debugDisableShadows` handle `BlurStyle.outer` correctly. - Adds `MagnifierInfo.toString`. - Aligns various `operator ==`s with the style guide. - Makes MagnifierDecoration a separate concept from Decoration, since it's not actually compatible anywhere. - Removes some dead code and makes other minor code simplifications. - Uses double syntax rather than integer syntax for various double literals for clarity. I expect one minor golden image change (antialiasing change on Cupertino's magnifier test).
This commit is contained in:
@@ -245,29 +245,60 @@ class CupertinoMagnifier extends StatelessWidget {
|
||||
color: Color.fromARGB(25, 0, 0, 0),
|
||||
blurRadius: 11,
|
||||
spreadRadius: 0.2,
|
||||
blurStyle: BlurStyle.outer,
|
||||
),
|
||||
],
|
||||
this.clipBehavior = Clip.none,
|
||||
this.borderSide =
|
||||
const BorderSide(color: Color.fromARGB(255, 232, 232, 232)),
|
||||
this.inOutAnimation,
|
||||
});
|
||||
|
||||
/// The shadows displayed under the magnifier.
|
||||
/// A list of shadows cast by the [Magnifier].
|
||||
///
|
||||
/// If the shadows use a [BlurStyle] that paints inside the shape, or if they
|
||||
/// are offset, then a [clipBehavior] that enables clipping (such as
|
||||
/// [Clip.hardEdge]) is recommended, otherwise the shadow will occlude the
|
||||
/// magnifier (the shadow is drawn above the magnifier so as to not be
|
||||
/// included in the magnified image).
|
||||
///
|
||||
/// A shadow that uses [BlurStyle.outer] and is not offset does not need
|
||||
/// clipping.
|
||||
///
|
||||
/// By default, the [shadows] are not offset and use [BlurStyle.outer], and
|
||||
/// correspondingly the default [clipBehavior] is [Clip.none].
|
||||
final List<BoxShadow> shadows;
|
||||
|
||||
/// Whether and how to clip the [shadows] that render inside the loupe.
|
||||
///
|
||||
/// Defaults to [Clip.none], which is useful if the shadow will not paint
|
||||
/// where the magnified image appears, or if doing so is intentional (e.g. to
|
||||
/// blur the edges of the magnified image).
|
||||
///
|
||||
/// The default configuration of [CupertinoMagnifier] does not render inside
|
||||
/// the loupe (the shadows are not offset and use [BlurStyle.outer]).
|
||||
///
|
||||
/// Other values (e.g. [Clip.hardEdge]) are recommended when the [shadows]
|
||||
/// have an offset.
|
||||
///
|
||||
/// See the discussion at [shadows].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The border, or "rim", of this magnifier.
|
||||
///
|
||||
/// This border is drawn on a [RoundedRectangleBorder] with radius
|
||||
/// [borderRadius], and increases the [size] of the magnifier by the
|
||||
/// [BorderSide.width].
|
||||
final BorderSide borderSide;
|
||||
|
||||
/// The vertical offset that the magnifier is along the Y axis above
|
||||
/// the focal point.
|
||||
@visibleForTesting
|
||||
static const double kMagnifierAboveFocalPoint = -26;
|
||||
|
||||
/// The default size of the magnifier.
|
||||
///
|
||||
/// This is public so that positioners can choose to depend on it, although
|
||||
/// it is overridable.
|
||||
@visibleForTesting
|
||||
static const Size kDefaultSize = Size(80, 47.5);
|
||||
|
||||
/// The duration that this magnifier animates in / out for.
|
||||
@@ -278,9 +309,13 @@ class CupertinoMagnifier extends StatelessWidget {
|
||||
static const Duration _kInOutAnimationDuration = Duration(milliseconds: 150);
|
||||
|
||||
/// The size of this magnifier.
|
||||
///
|
||||
/// The size does not include the [borderSide] or [shadows].
|
||||
final Size size;
|
||||
|
||||
/// The border radius of this magnifier.
|
||||
///
|
||||
/// The magnifier's shape is a [RoundedRectangleBorder] with this radius.
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// This [RawMagnifier]'s controller.
|
||||
@@ -317,6 +352,7 @@ class CupertinoMagnifier extends StatelessWidget {
|
||||
),
|
||||
shadows: shadows,
|
||||
),
|
||||
clipBehavior: clipBehavior,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,39 +7,38 @@ import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// {@template widgets.material.magnifier.magnifier}
|
||||
/// A [Magnifier] positioned by rules dictated by the native Android magnifier.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@template widgets.material.magnifier.positionRules}
|
||||
/// Positions itself based on [magnifierInfo]. Specifically, follows the
|
||||
/// following rules:
|
||||
/// - Tracks the gesture's x coordinate, but clamped to the beginning and end of the
|
||||
/// currently editing line.
|
||||
/// - Focal point may never contain anything out of bounds.
|
||||
/// - Never goes out of bounds vertically; offset until the entire magnifier is in the screen. The
|
||||
/// focal point, regardless of this transformation, always points to the touch y coordinate.
|
||||
/// - If just jumped between lines (prevY != currentY) then animate for duration
|
||||
/// [jumpBetweenLinesAnimationDuration].
|
||||
/// {@endtemplate}
|
||||
/// The positioning rules are based on [magnifierInfo], as follows:
|
||||
///
|
||||
/// - The loupe tracks the gesture's _x_ coordinate, clamping to the beginning
|
||||
/// and end of the currently editing line.
|
||||
///
|
||||
/// - The focal point never contains anything out of the bounds of the text
|
||||
/// field or other widget being magnified (the [MagnifierInfo.fieldBounds]).
|
||||
///
|
||||
/// - The focal point always remains aligned with the _y_ coordinate of the touch.
|
||||
///
|
||||
/// - The loupe always remains on the screen.
|
||||
///
|
||||
/// - When the line targeted by the touch's _y_ coordinate changes, the position
|
||||
/// is animated over [jumpBetweenLinesAnimationDuration].
|
||||
///
|
||||
/// This behavior was based on the Android 12 source code, where possible, and
|
||||
/// on eyeballing a Pixel 6 running Android 12 otherwise.
|
||||
class TextMagnifier extends StatefulWidget {
|
||||
/// {@macro widgets.material.magnifier.magnifier}
|
||||
/// Creates a [TextMagnifier].
|
||||
///
|
||||
/// {@template widgets.material.magnifier.androidDisclaimer}
|
||||
/// These constants and default parameters were taken from the
|
||||
/// Android 12 source code where directly transferable, and eyeballed on
|
||||
/// a Pixel 6 running Android 12 otherwise.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro widgets.material.magnifier.positionRules}
|
||||
/// The [magnifierInfo] must be provided, and must be updated with new values
|
||||
/// as the user's touch changes.
|
||||
const TextMagnifier({
|
||||
super.key,
|
||||
required this.magnifierInfo,
|
||||
});
|
||||
|
||||
/// A [TextMagnifierConfiguration] that returns a [CupertinoTextMagnifier] on iOS,
|
||||
/// [TextMagnifier] on Android, and null on all other platforms, and shows the editing handles
|
||||
/// only on iOS.
|
||||
/// A [TextMagnifierConfiguration] that returns a [CupertinoTextMagnifier] on
|
||||
/// iOS, [TextMagnifier] on Android, and null on all other platforms, and
|
||||
/// shows the editing handles only on iOS.
|
||||
static TextMagnifierConfiguration adaptiveMagnifierConfiguration = TextMagnifierConfiguration(
|
||||
shouldDisplayHandlesInMagnifier: defaultTargetPlatform == TargetPlatform.iOS,
|
||||
magnifierBuilder: (
|
||||
@@ -55,7 +54,7 @@ class TextMagnifier extends StatefulWidget {
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
return TextMagnifier(
|
||||
magnifierInfo: magnifierInfo,
|
||||
magnifierInfo: magnifierInfo,
|
||||
);
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
@@ -68,15 +67,14 @@ class TextMagnifier extends StatefulWidget {
|
||||
|
||||
/// The duration that the position is animated if [TextMagnifier] just switched
|
||||
/// between lines.
|
||||
@visibleForTesting
|
||||
static const Duration jumpBetweenLinesAnimationDuration =
|
||||
Duration(milliseconds: 70);
|
||||
static const Duration jumpBetweenLinesAnimationDuration = Duration(milliseconds: 70);
|
||||
|
||||
/// [TextMagnifier] positions itself based on [magnifierInfo].
|
||||
/// The current status of the user's touch.
|
||||
///
|
||||
/// {@macro widgets.material.magnifier.positionRules}
|
||||
final ValueNotifier<MagnifierInfo>
|
||||
magnifierInfo;
|
||||
/// As the value of the [magnifierInfo] changes, the position of the loupe is
|
||||
/// adjusted automatically, according to the rules described in the
|
||||
/// [TextMagnifier] class description.
|
||||
final ValueNotifier<MagnifierInfo> magnifierInfo;
|
||||
|
||||
@override
|
||||
State<TextMagnifier> createState() => _TextMagnifierState();
|
||||
@@ -130,7 +128,6 @@ class _TextMagnifierState extends State<TextMagnifier> {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
/// {@macro widgets.material.magnifier.positionRules}
|
||||
void _determineMagnifierPositionAndFocalPoint() {
|
||||
final MagnifierInfo selectionInfo =
|
||||
widget.magnifierInfo.value;
|
||||
@@ -250,16 +247,19 @@ class _TextMagnifierState extends State<TextMagnifier> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Material styled magnifying glass.
|
||||
/// A Material-styled magnifying glass.
|
||||
///
|
||||
/// {@macro flutter.widgets.magnifier.intro}
|
||||
///
|
||||
/// This widget focuses on mimicking the _style_ of the magnifier on material. For a
|
||||
/// widget that is focused on mimicking the behavior of a material magnifier, see [TextMagnifier].
|
||||
/// This widget focuses on mimicking the _style_ of the magnifier on material.
|
||||
/// For a widget that is focused on mimicking the _behavior_ of a material
|
||||
/// magnifier, see [TextMagnifier], which uses [Magnifier].
|
||||
///
|
||||
/// The styles implemented in this widget were based on the Android 12 source
|
||||
/// code, where possible, and on eyeballing a Pixel 6 running Android 12
|
||||
/// otherwise.
|
||||
class Magnifier extends StatelessWidget {
|
||||
/// Creates a [RawMagnifier] in the Material style.
|
||||
///
|
||||
/// {@macro widgets.material.magnifier.androidDisclaimer}
|
||||
const Magnifier({
|
||||
super.key,
|
||||
this.additionalFocalPointOffset = Offset.zero,
|
||||
@@ -267,11 +267,13 @@ class Magnifier extends StatelessWidget {
|
||||
this.filmColor = const Color.fromARGB(8, 158, 158, 158),
|
||||
this.shadows = const <BoxShadow>[
|
||||
BoxShadow(
|
||||
blurRadius: 1.5,
|
||||
offset: Offset(0, 2),
|
||||
spreadRadius: 0.75,
|
||||
color: Color.fromARGB(25, 0, 0, 0))
|
||||
blurRadius: 1.5,
|
||||
offset: Offset(0.0, 2.0),
|
||||
spreadRadius: 0.75,
|
||||
color: Color.fromARGB(25, 0, 0, 0),
|
||||
)
|
||||
],
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.size = Magnifier.kDefaultMagnifierSize,
|
||||
});
|
||||
|
||||
@@ -280,14 +282,13 @@ class Magnifier extends StatelessWidget {
|
||||
/// The size of the magnifier may be modified through the constructor;
|
||||
/// [kDefaultMagnifierSize] is extracted from the default parameter of
|
||||
/// [Magnifier]'s constructor so that positioners may depend on it.
|
||||
@visibleForTesting
|
||||
static const Size kDefaultMagnifierSize = Size(77.37, 37.9);
|
||||
|
||||
/// The vertical distance that the magnifier should be above the focal point.
|
||||
///
|
||||
/// [kStandardVerticalFocalPointShift] is an unmodifiable constant so that positioning of this
|
||||
/// [Magnifier] can be done with a guaranteed size, as opposed to an estimate.
|
||||
@visibleForTesting
|
||||
/// The [kStandardVerticalFocalPointShift] value is a constant so that
|
||||
/// positioning of this [Magnifier] can be done with a guaranteed size, as
|
||||
/// opposed to an estimate.
|
||||
static const double kStandardVerticalFocalPointShift = 22;
|
||||
|
||||
static const double _borderRadius = 40;
|
||||
@@ -296,11 +297,16 @@ class Magnifier extends StatelessWidget {
|
||||
/// Any additional offset the focal point requires to "point"
|
||||
/// to the correct place.
|
||||
///
|
||||
/// This is useful for instances where the magnifier is not pointing to something
|
||||
/// directly below it.
|
||||
/// This value is added to [kStandardVerticalFocalPointShift] to obtain the
|
||||
/// actual offset.
|
||||
///
|
||||
/// This is useful for instances where the magnifier is not pointing to
|
||||
/// something directly below it.
|
||||
final Offset additionalFocalPointOffset;
|
||||
|
||||
/// The border radius for this magnifier.
|
||||
///
|
||||
/// The magnifier's shape is a [RoundedRectangleBorder] with this radius.
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// The color to tint the image in this [Magnifier].
|
||||
@@ -310,12 +316,35 @@ class Magnifier extends StatelessWidget {
|
||||
/// the background.
|
||||
final Color filmColor;
|
||||
|
||||
/// The shadows for this [Magnifier].
|
||||
/// A list of shadows cast by the [Magnifier].
|
||||
///
|
||||
/// If the shadows use a [BlurStyle] that paints inside the shape, or if they
|
||||
/// are offset, then a [clipBehavior] that enables clipping (such as the
|
||||
/// default [Clip.hardEdge]) is recommended, otherwise the shadow will occlude
|
||||
/// the magnifier (the shadow is drawn above the magnifier so as to not be
|
||||
/// included in the magnified image).
|
||||
///
|
||||
/// By default, the shadows are offset vertically by two logical pixels, so
|
||||
/// clipping is recommended.
|
||||
///
|
||||
/// A shadow that uses [BlurStyle.outer] and is not offset does not need
|
||||
/// clipping; in that case, consider setting [clipBehavior] to [Clip.none].
|
||||
final List<BoxShadow> shadows;
|
||||
|
||||
/// Whether and how to clip the [shadows] that render inside the loupe.
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
///
|
||||
/// A value of [Clip.none] can be used if the shadow will not paint where the
|
||||
/// magnified image appears, or if doing so is intentional (e.g. to blur the
|
||||
/// edges of the magnified image).
|
||||
///
|
||||
/// See the discussion at [shadows].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The [Size] of this [Magnifier].
|
||||
///
|
||||
/// This size does not include the border.
|
||||
/// The [shadows] are drawn outside of the [size].
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
@@ -325,11 +354,17 @@ class Magnifier extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
shadows: shadows,
|
||||
),
|
||||
clipBehavior: clipBehavior,
|
||||
magnificationScale: _magnification,
|
||||
focalPointOffset: additionalFocalPointOffset +
|
||||
Offset(0, kStandardVerticalFocalPointShift + kDefaultMagnifierSize.height / 2),
|
||||
size: size,
|
||||
child: ColoredBox(
|
||||
// This couldn't be part of the decoration (even if the
|
||||
// MagnifierDecoration supported specifying a color) because the
|
||||
// decoration's shadows are offset and therefore we set a clipBehavior
|
||||
// that clips the inner part of the decoration to avoid occluding the
|
||||
// magnified image with the shadow.
|
||||
color: filmColor,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -18,6 +18,15 @@ import 'package:flutter/painting.dart';
|
||||
/// This is useful when simulating a shadow with a [BoxDecoration] or other
|
||||
/// class that uses a list of [BoxShadow] objects.
|
||||
///
|
||||
/// Shadows defined by [kElevationToShadow] use [BlurStyle.normal]. To convert a
|
||||
/// shadow from [kElevationToShadow] to use a different [BlurStyle] (e.g. to use
|
||||
/// it in a [MagnifierDecoration]), consider an expression such as the
|
||||
/// following:
|
||||
///
|
||||
/// ```dart
|
||||
/// kElevationToShadow[12]!.map((BoxShadow shadow) => shadow.copyWith(blurStyle: BlurStyle.outer)).toList(),
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Material], which takes an arbitrary double for its elevation and generates
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'border_radius.dart';
|
||||
import 'box_border.dart';
|
||||
import 'box_shadow.dart';
|
||||
import 'colors.dart';
|
||||
import 'debug.dart';
|
||||
import 'decoration.dart';
|
||||
import 'decoration_image.dart';
|
||||
import 'edge_insets.dart';
|
||||
@@ -439,7 +440,20 @@ class _BoxDecorationPainter extends BoxPainter {
|
||||
for (final BoxShadow boxShadow in _decoration.boxShadow!) {
|
||||
final Paint paint = boxShadow.toPaint();
|
||||
final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
|
||||
assert(() {
|
||||
if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) {
|
||||
canvas.save();
|
||||
canvas.clipRect(bounds);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_paintBox(canvas, bounds, paint, textDirection);
|
||||
assert(() {
|
||||
if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) {
|
||||
canvas.restore();
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ class BoxShadow extends ui.Shadow {
|
||||
/// The [BlurStyle] to use for this shadow.
|
||||
///
|
||||
/// Defaults to [BlurStyle.normal].
|
||||
///
|
||||
/// When [debugDisableShadows] is true, [toPaint] ignores the [blurStyle] and
|
||||
/// acts as if [BlurStyle.normal] was used.
|
||||
final BlurStyle blurStyle;
|
||||
|
||||
/// Create the [Paint] object that corresponds to this shadow description.
|
||||
@@ -52,6 +55,12 @@ class BoxShadow extends ui.Shadow {
|
||||
/// To honor those as well, the shape should be inflated by [spreadRadius] pixels
|
||||
/// in every direction and then translated by [offset] before being filled using
|
||||
/// this [Paint].
|
||||
///
|
||||
/// The [blurStyle] is ignored if [debugDisableShadows] is true. This causes
|
||||
/// an especially significant change to the rendering when [BlurStyle.outer]
|
||||
/// is used; the caller is responsible for adjusting for that case if
|
||||
/// necessary. (This only matters when using [debugDisableShadows], e.g. in
|
||||
/// tests that use [matchesGoldenFile].)
|
||||
@override
|
||||
Paint toPaint() {
|
||||
final Paint result = Paint()
|
||||
@@ -66,7 +75,8 @@ class BoxShadow extends ui.Shadow {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns a new box shadow with its offset, blurRadius, and spreadRadius scaled by the given factor.
|
||||
/// Returns a new box shadow with its offset, blurRadius, and spreadRadius
|
||||
/// scaled by the given factor.
|
||||
@override
|
||||
BoxShadow scale(double factor) {
|
||||
return BoxShadow(
|
||||
@@ -78,6 +88,24 @@ class BoxShadow extends ui.Shadow {
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a copy of this object but with the given fields replaced with the
|
||||
/// new values.
|
||||
BoxShadow copyWith({
|
||||
Color? color,
|
||||
Offset? offset,
|
||||
double? blurRadius,
|
||||
double? spreadRadius,
|
||||
BlurStyle? blurStyle,
|
||||
}) {
|
||||
return BoxShadow(
|
||||
color: color ?? this.color,
|
||||
offset: offset ?? this.offset,
|
||||
blurRadius: blurRadius ?? this.blurRadius,
|
||||
spreadRadius: spreadRadius ?? this.spreadRadius,
|
||||
blurStyle: blurStyle ?? this.blurStyle,
|
||||
);
|
||||
}
|
||||
|
||||
/// Linearly interpolate between two box shadows.
|
||||
///
|
||||
/// If either box shadow is null, this function linearly interpolates from
|
||||
|
||||
@@ -13,11 +13,17 @@ import 'package:flutter/foundation.dart';
|
||||
/// the rendering of shadows is not guaranteed to be pixel-for-pixel identical from
|
||||
/// version to version (or even from run to run).
|
||||
///
|
||||
/// In those tests, this is usually set to false at the beginning of a test and back
|
||||
/// to true before the end of the test case.
|
||||
/// This is set to true in [AutomatedTestWidgetsFlutterBinding]. Tests will fail
|
||||
/// if they change this value and do not reset it before the end of the test.
|
||||
///
|
||||
/// If it remains true when the test ends, an exception is thrown to avoid state
|
||||
/// leaking from one test case to another.
|
||||
/// When this is set, [BoxShadow.toPaint] acts as if the [BoxShadow.blurStyle]
|
||||
/// was [BlurStyle.normal] regardless of the actual specified blur style. This
|
||||
/// is compensated for in [BoxDecoration] and [ShapeDecoration] but may need to
|
||||
/// be explicitly considered in other situations.
|
||||
///
|
||||
/// This property should not be changed during a frame (e.g. during a call to
|
||||
/// [ShapeBorder.paintInterior] or [ShapeBorder.getOuterPath]); doing so may
|
||||
/// cause undefined effects.
|
||||
bool debugDisableShadows = false;
|
||||
|
||||
/// Signature for a method that returns an [HttpClient].
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'box_decoration.dart';
|
||||
import 'box_shadow.dart';
|
||||
import 'circle_border.dart';
|
||||
import 'colors.dart';
|
||||
import 'debug.dart';
|
||||
import 'decoration.dart';
|
||||
import 'decoration_image.dart';
|
||||
import 'edge_insets.dart';
|
||||
@@ -359,14 +360,43 @@ class _ShapeDecorationPainter extends BoxPainter {
|
||||
}
|
||||
|
||||
void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) {
|
||||
// The debugHandleDisabledShadowStart and debugHandleDisabledShadowEnd
|
||||
// methods are used in debug mode only to support BlurStyle.outer when
|
||||
// debugDisableShadows is set. Without these clips, the shadows would extend
|
||||
// to the inside of the shape, which would likely obscure important
|
||||
// portions of the rendering and would cause unit tests of widgets that use
|
||||
// BlurStyle.outer to significantly diverge from the original intent.
|
||||
// It is assumed that [debugDisableShadows] will not change when calling
|
||||
// paintInterior or getOuterPath; if it does, the results are undefined.
|
||||
bool debugHandleDisabledShadowStart(Canvas canvas, BoxShadow boxShadow, Path path) {
|
||||
if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) {
|
||||
canvas.save();
|
||||
final Path clipPath = Path();
|
||||
clipPath.fillType = PathFillType.evenOdd;
|
||||
clipPath.addRect(Rect.largest);
|
||||
clipPath.addPath(path, Offset.zero);
|
||||
canvas.clipPath(clipPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool debugHandleDisabledShadowEnd(Canvas canvas, BoxShadow boxShadow) {
|
||||
if (debugDisableShadows && boxShadow.blurStyle == BlurStyle.outer) {
|
||||
canvas.restore();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_shadowCount != null) {
|
||||
if (_decoration.shape.preferPaintInterior) {
|
||||
for (int index = 0; index < _shadowCount!; index += 1) {
|
||||
assert(debugHandleDisabledShadowStart(canvas, _decoration.shadows![index], _decoration.shape.getOuterPath(_shadowBounds[index], textDirection: textDirection)));
|
||||
_decoration.shape.paintInterior(canvas, _shadowBounds[index], _shadowPaints[index], textDirection: textDirection);
|
||||
assert(debugHandleDisabledShadowEnd(canvas, _decoration.shadows![index]));
|
||||
}
|
||||
} else {
|
||||
for (int index = 0; index < _shadowCount!; index += 1) {
|
||||
assert(debugHandleDisabledShadowStart(canvas, _decoration.shadows![index], _shadowPaths[index]));
|
||||
canvas.drawPath(_shadowPaths[index], _shadowPaints[index]);
|
||||
assert(debugHandleDisabledShadowEnd(canvas, _decoration.shadows![index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
@@ -68,8 +68,8 @@ class MagnifierInfo {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is MagnifierInfo
|
||||
&& other.globalGesturePosition == globalGesturePosition
|
||||
@@ -85,6 +85,16 @@ class MagnifierInfo {
|
||||
fieldBounds,
|
||||
currentLineBoundaries,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'MagnifierInfo')}('
|
||||
'position: $globalGesturePosition, '
|
||||
'line: $currentLineBoundaries, '
|
||||
'caret: $caretRect, '
|
||||
'field: $fieldBounds'
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
||||
/// A configuration object for a magnifier (e.g. in a text field).
|
||||
@@ -135,9 +145,6 @@ class TextMagnifierConfiguration {
|
||||
/// [Overlay].
|
||||
///
|
||||
/// To check the status of the magnifier, see [MagnifierController.shown].
|
||||
// TODO(antholeole): This whole paradigm can be removed once portals
|
||||
// lands - then the magnifier can be controlled though a widget in the tree.
|
||||
// https://github.com/flutter/flutter/pull/105335
|
||||
class MagnifierController {
|
||||
/// If there is no in / out animation for the magnifier, [animationController] should be left
|
||||
/// null.
|
||||
@@ -350,37 +357,83 @@ class MagnifierController {
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoration for a [RawMagnifier].
|
||||
/// The decorations to put around the loupe in a [RawMagnifier].
|
||||
///
|
||||
/// [MagnifierDecoration] does not expose [ShapeDecoration.color], [ShapeDecoration.image],
|
||||
/// or [ShapeDecoration.gradient], since they will be covered by the [RawMagnifier]'s lens.
|
||||
/// See also:
|
||||
///
|
||||
/// Also takes an [opacity] (see https://github.com/flutter/engine/pull/34435).
|
||||
class MagnifierDecoration extends ShapeDecoration {
|
||||
/// * [Decoration], a more general solution for [DecoratedBox].
|
||||
@immutable
|
||||
class MagnifierDecoration {
|
||||
/// Constructs a [MagnifierDecoration].
|
||||
///
|
||||
/// By default, [MagnifierDecoration] is a rectangular magnifier with no shadows, and
|
||||
/// fully opaque.
|
||||
/// By default, [MagnifierDecoration] is a rectangular magnifier with no
|
||||
/// shadows, and fully opaque.
|
||||
const MagnifierDecoration({
|
||||
this.opacity = 1,
|
||||
super.shadows,
|
||||
super.shape = const RoundedRectangleBorder(),
|
||||
this.opacity = 1.0,
|
||||
this.shadows,
|
||||
this.shape = const RoundedRectangleBorder(),
|
||||
});
|
||||
|
||||
/// The magnifier's opacity.
|
||||
// TODO(ianh): deprecate [opacity] (moving it to [RawMagnifier]), and then
|
||||
// once [opacity] can be removed, replace [MagnifierDecoration] with a
|
||||
// `typedef` to [ShapeDecoration] and make anywhere that accepts a
|
||||
// [MagnifierDecoration] accept a [ShapeDecoration] instead. This would allow
|
||||
// magnifiers that don't offset the shadows to use the decoration to paint
|
||||
// over the loupe rather than having to have a Stack of widgets to do so.
|
||||
|
||||
/// The opacity of the magnifier and decorations around the magnifier.
|
||||
///
|
||||
/// When this is 1.0, the magnified image shows in the [shape] of the
|
||||
/// magnifier. When this is less than 1.0, the magnified image is transparent
|
||||
/// and shows through the unmagnified background.
|
||||
///
|
||||
/// Generally this is only useful for animating the magnifier in and out, as a
|
||||
/// transparent magnifier looks quite confusing.
|
||||
final double opacity;
|
||||
|
||||
/// A list of shadows cast by the [shape].
|
||||
///
|
||||
/// If the shadows are offset, consider setting [RawMagnifier.clipBehavior] to
|
||||
/// [Clip.hardEdge] (or similar) to ensure the shadow does not occlude the
|
||||
/// magnifier (the shadow is drawn above the magnifier).
|
||||
///
|
||||
/// If the shadows are _not_ offset, consider using [BlurStyle.outer] in the
|
||||
/// shadows instead, to avoid having to introduce a clip.
|
||||
///
|
||||
/// In the event that [shape] consists of a stack of borders, the shadow is
|
||||
/// drawn using the bounds of the last one.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kElevationToShadow], which defines some shadows for Material design.
|
||||
/// Those shadows use [BlurStyle.normal] and may need to be converted to
|
||||
/// [BlurStyle.outer] for use with [MagnifierDecoration].
|
||||
final List<BoxShadow>? shadows;
|
||||
|
||||
/// The shape of the magnifier and the outline (border) around it.
|
||||
///
|
||||
/// Shapes can be stacked (using the `+` operator). In that case, the
|
||||
/// magnifier and shadow are drawn according to the outside edge of the last
|
||||
/// shape, with the borders painted on top.
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super == other && other is MagnifierDecoration && other.opacity == opacity;
|
||||
return other is MagnifierDecoration
|
||||
&& other.opacity == opacity
|
||||
&& listEquals<BoxShadow>(other.shadows, shadows)
|
||||
&& other.shape == shape;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(super.hashCode, opacity);
|
||||
int get hashCode => Object.hash(
|
||||
opacity,
|
||||
shape,
|
||||
shadows == null ? null : Object.hashAll(shadows!),
|
||||
);
|
||||
}
|
||||
|
||||
/// A common base class for magnifiers.
|
||||
@@ -406,17 +459,16 @@ class MagnifierDecoration extends ShapeDecoration {
|
||||
class RawMagnifier extends StatelessWidget {
|
||||
/// Constructs a [RawMagnifier].
|
||||
///
|
||||
/// {@template flutter.widgets.magnifier.RawMagnifier.invisibility_warning}
|
||||
/// By default, this magnifier uses the default [MagnifierDecoration],
|
||||
/// the focal point is directly under the magnifier, and there is no magnification:
|
||||
/// This means that a default magnifier will be entirely invisible to the naked eye,
|
||||
/// since it is painting exactly what is under it, exactly where it was painted
|
||||
/// originally.
|
||||
/// {@endtemplate}
|
||||
/// By default, this magnifier uses the default [MagnifierDecoration] (which
|
||||
/// draws nothing), the focal point is directly under the magnifier, and there
|
||||
/// is no magnification; this means that a default magnifier will be entirely
|
||||
/// invisible to the naked eye, painting exactly what is under it, exactly
|
||||
/// where it was painted originally.
|
||||
const RawMagnifier({
|
||||
super.key,
|
||||
this.child,
|
||||
this.decoration = const MagnifierDecoration(),
|
||||
this.clipBehavior = Clip.none,
|
||||
this.focalPointOffset = Offset.zero,
|
||||
this.magnificationScale = 1,
|
||||
required this.size,
|
||||
@@ -426,14 +478,29 @@ class RawMagnifier extends StatelessWidget {
|
||||
/// An optional widget to position inside the len of the [RawMagnifier].
|
||||
///
|
||||
/// This is positioned over the [RawMagnifier] - it may be useful for tinting the
|
||||
/// [RawMagnifier], or drawing a crosshair like UI.
|
||||
/// [RawMagnifier], or drawing a crosshair-like UI.
|
||||
final Widget? child;
|
||||
|
||||
/// This magnifier's decoration.
|
||||
///
|
||||
/// {@macro flutter.widgets.magnifier.RawMagnifier.invisibility_warning}
|
||||
/// This sets the shape of the loupe, plus any borders and shadows that it
|
||||
/// casts. The default has no border and no shadow; combined with the default
|
||||
/// [magnificationScale] of 1.0, this results in the magnifier having no
|
||||
/// visible effect.
|
||||
///
|
||||
/// If the [decoration] has a [MagnifierDecoration.shadows] that uses offset
|
||||
/// shadows or uses a [BlurStyle] that would obscure the magnified image,
|
||||
/// consider setting [clipBehavior] to [Clip.hardEdge] (or similar) to ensure
|
||||
/// the magnified image is visible.
|
||||
final MagnifierDecoration decoration;
|
||||
|
||||
/// Whether and how to clip the parts of [decoration] that render inside the
|
||||
/// loupe.
|
||||
///
|
||||
/// Defaults to [Clip.none].
|
||||
///
|
||||
/// See the discussion at [decoration].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The offset of the magnifier from [RawMagnifier]'s center.
|
||||
///
|
||||
@@ -448,11 +515,13 @@ class RawMagnifier extends StatelessWidget {
|
||||
final Offset focalPointOffset;
|
||||
|
||||
/// How "zoomed in" the magnification subject is in the lens.
|
||||
///
|
||||
/// The default is 1.0, which is no magnification.
|
||||
final double magnificationScale;
|
||||
|
||||
/// The size of the magnifier.
|
||||
///
|
||||
/// This does not include added border; it only includes
|
||||
/// This does not include the border from the [decoration]; it only includes
|
||||
/// the size of the magnifier.
|
||||
final Size size;
|
||||
|
||||
@@ -462,12 +531,12 @@ class RawMagnifier extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
// The magnified image is clipped to the outer path of the shape.
|
||||
ClipPath.shape(
|
||||
shape: decoration.shape,
|
||||
child: Opacity(
|
||||
opacity: decoration.opacity,
|
||||
child: _Magnifier(
|
||||
shape: decoration.shape,
|
||||
focalPointOffset: focalPointOffset,
|
||||
magnificationScale: magnificationScale,
|
||||
child: SizedBox.fromSize(
|
||||
@@ -477,86 +546,53 @@ class RawMagnifier extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Because `BackdropFilter` will filter any widgets before it, we should
|
||||
// apply the style after (i.e. in a younger sibling) to avoid the magnifier
|
||||
// Because `BackdropFilter` will filter any widgets before it, we apply
|
||||
// these styles after (i.e. in a younger sibling) to avoid the magnifier
|
||||
// from seeing its own styling.
|
||||
Opacity(
|
||||
opacity: decoration.opacity,
|
||||
child: _MagnifierStyle(
|
||||
decoration,
|
||||
size: size,
|
||||
IgnorePointer(
|
||||
child: Opacity(
|
||||
opacity: decoration.opacity,
|
||||
child: ClipPath(
|
||||
clipBehavior: clipBehavior,
|
||||
clipper: _NegativeClip(shape: decoration.shape),
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: decoration.shape,
|
||||
shadows: decoration.shadows,
|
||||
),
|
||||
child: SizedBox.fromSize(
|
||||
size: size,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MagnifierStyle extends StatelessWidget {
|
||||
const _MagnifierStyle(this.decoration, {required this.size});
|
||||
// A clip that renders everything except the inside of a shape.
|
||||
class _NegativeClip extends CustomClipper<Path> {
|
||||
_NegativeClip({required this.shape});
|
||||
|
||||
final MagnifierDecoration decoration;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double largestShadow = 0;
|
||||
for (final BoxShadow shadow in decoration.shadows ?? <BoxShadow>[]) {
|
||||
largestShadow = math.max(
|
||||
largestShadow,
|
||||
(shadow.blurRadius + shadow.spreadRadius) +
|
||||
math.max(shadow.offset.dy.abs(), shadow.offset.dx.abs()));
|
||||
}
|
||||
|
||||
return ClipPath(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
clipper: _DonutClip(
|
||||
shape: decoration.shape,
|
||||
spreadRadius: largestShadow,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: SizedBox.fromSize(
|
||||
size: size,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A `clipPath` that looks like a donut if you were to fill its area.
|
||||
///
|
||||
/// This is necessary because the shadow must be added after the magnifier is drawn,
|
||||
/// so that the shadow does not end up in the magnifier. Without this clip, the magnifier would be
|
||||
/// entirely covered by the shadow.
|
||||
///
|
||||
/// The negative space of the donut is clipped out (the donut hole, outside the donut).
|
||||
/// The donut hole is cut out exactly like the shape of the magnifier.
|
||||
class _DonutClip extends CustomClipper<Path> {
|
||||
_DonutClip({required this.shape, required this.spreadRadius});
|
||||
|
||||
final double spreadRadius;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final Path path = Path();
|
||||
final Rect rect = Offset.zero & size;
|
||||
|
||||
path.fillType = PathFillType.evenOdd;
|
||||
path.addPath(shape.getOuterPath(rect.inflate(spreadRadius)), Offset.zero);
|
||||
path.addPath(shape.getInnerPath(rect), Offset.zero);
|
||||
return path;
|
||||
return Path()
|
||||
..fillType = PathFillType.evenOdd
|
||||
..addRect(Rect.largest)
|
||||
..addPath(shape.getInnerPath(Offset.zero & size), Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(_DonutClip oldClipper) => oldClipper.shape != shape;
|
||||
bool shouldReclip(_NegativeClip oldClipper) => oldClipper.shape != shape;
|
||||
}
|
||||
|
||||
class _Magnifier extends SingleChildRenderObjectWidget {
|
||||
const _Magnifier({
|
||||
super.child,
|
||||
required this.shape,
|
||||
this.magnificationScale = 1,
|
||||
this.focalPointOffset = Offset.zero,
|
||||
});
|
||||
@@ -571,12 +607,9 @@ class _Magnifier extends SingleChildRenderObjectWidget {
|
||||
// If greater than 1.0, the content appears bigger in the magnifier.
|
||||
final double magnificationScale;
|
||||
|
||||
// Shape of the magnifier.
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderMagnification(focalPointOffset, magnificationScale, shape);
|
||||
return _RenderMagnification(focalPointOffset, magnificationScale);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -584,7 +617,6 @@ class _Magnifier extends SingleChildRenderObjectWidget {
|
||||
BuildContext context, _RenderMagnification renderObject) {
|
||||
renderObject
|
||||
..focalPointOffset = focalPointOffset
|
||||
..shape = shape
|
||||
..magnificationScale = magnificationScale;
|
||||
}
|
||||
}
|
||||
@@ -592,8 +624,7 @@ class _Magnifier extends SingleChildRenderObjectWidget {
|
||||
class _RenderMagnification extends RenderProxyBox {
|
||||
_RenderMagnification(
|
||||
this._focalPointOffset,
|
||||
this._magnificationScale,
|
||||
this._shape, {
|
||||
this._magnificationScale, {
|
||||
RenderBox? child,
|
||||
}) : super(child);
|
||||
|
||||
@@ -617,16 +648,6 @@ class _RenderMagnification extends RenderProxyBox {
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
ShapeBorder get shape => _shape;
|
||||
ShapeBorder _shape;
|
||||
set shape(ShapeBorder value) {
|
||||
if (_shape == value) {
|
||||
return;
|
||||
}
|
||||
_shape = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => true;
|
||||
|
||||
|
||||
@@ -23,15 +23,14 @@ void main() {
|
||||
ValueNotifier<MagnifierInfo> magnifierInfo,
|
||||
) async {
|
||||
final Future<void> magnifierShown = magnifierController.show(
|
||||
context: context,
|
||||
builder: (_) => CupertinoTextMagnifier(
|
||||
controller: magnifierController,
|
||||
magnifierInfo: magnifierInfo,
|
||||
));
|
||||
|
||||
WidgetsBinding.instance.scheduleFrame();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoTextMagnifier(
|
||||
controller: magnifierController,
|
||||
magnifierInfo: magnifierInfo,
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await magnifierShown;
|
||||
}
|
||||
|
||||
|
||||
@@ -168,8 +168,9 @@ void main() {
|
||||
|
||||
await showMagnifier(context, tester, magnifierInfo);
|
||||
|
||||
// Should show two red squares; original, and one in the magnifier,
|
||||
// directly ontop of one another.
|
||||
// Should show two red crossed-out squares: the original in the center,
|
||||
// and one in the magnifier, in the upper half of the image, surrounded
|
||||
// by a faint offset rounded rectangle shadow.
|
||||
await expectLater(
|
||||
find.byType(MaterialApp),
|
||||
matchesGoldenFile('magnifier.position.default.png'),
|
||||
|
||||
@@ -812,4 +812,17 @@ void main() {
|
||||
|
||||
info.dispose();
|
||||
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
|
||||
|
||||
test('BoxShadow.copyWith', () {
|
||||
expect(const BoxShadow(), isNot(const BoxShadow(color: Color(0xFF112233))));
|
||||
expect(const BoxShadow().copyWith(color: const Color(0xFF112233)), const BoxShadow(color: Color(0xFF112233)));
|
||||
expect(const BoxShadow(), isNot(const BoxShadow(offset: Offset(1.0, 2.0))));
|
||||
expect(const BoxShadow().copyWith(offset: const Offset(1.0, 2.0)), const BoxShadow(offset: Offset(1.0, 2.0)));
|
||||
expect(const BoxShadow(), isNot(const BoxShadow(blurRadius: 123.0)));
|
||||
expect(const BoxShadow().copyWith(blurRadius: 123.0), const BoxShadow(blurRadius: 123.0));
|
||||
expect(const BoxShadow(), isNot(const BoxShadow(spreadRadius: 123.0)));
|
||||
expect(const BoxShadow().copyWith(spreadRadius: 123.0), const BoxShadow(spreadRadius: 123.0));
|
||||
expect(const BoxShadow(), isNot(const BoxShadow(blurStyle: BlurStyle.outer)));
|
||||
expect(const BoxShadow().copyWith(blurStyle: BlurStyle.outer), const BoxShadow(blurStyle: BlurStyle.outer));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
key: appKey,
|
||||
home: Container(
|
||||
color: Colors.orange,
|
||||
color: Colors.blue,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Stack(
|
||||
@@ -64,7 +64,7 @@ void main() {
|
||||
left: magnifierPosition.dx + magnifierFocalPoint.dx,
|
||||
top: magnifierPosition.dy + magnifierFocalPoint.dy,
|
||||
child: Container(
|
||||
color: Colors.pink,
|
||||
color: Colors.black,
|
||||
// Since it is the size of the magnifier but over its
|
||||
// magnificationScale, it should take up the whole magnifier.
|
||||
width: (magnifierSize.width * 1.5) / magnificationScale,
|
||||
@@ -78,26 +78,27 @@ void main() {
|
||||
size: magnifierSize,
|
||||
focalPointOffset: magnifierFocalPoint,
|
||||
magnificationScale: magnificationScale,
|
||||
decoration: MagnifierDecoration(shadows: <BoxShadow>[
|
||||
BoxShadow(
|
||||
spreadRadius: 10,
|
||||
blurRadius: 10,
|
||||
color: Colors.green,
|
||||
offset: Offset(5, 5),
|
||||
),
|
||||
]),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: MagnifierDecoration(
|
||||
shadows: <BoxShadow>[
|
||||
BoxShadow(
|
||||
spreadRadius: 10.0,
|
||||
blurRadius: 10.0,
|
||||
color: Colors.yellow,
|
||||
offset: Offset(5.0, 5.0),
|
||||
),
|
||||
],
|
||||
opacity: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should look like an orange screen, with two pink boxes.
|
||||
// One pink box is in the magnifier (so has a green shadow) and is double
|
||||
// size (from magnification). Also, the magnifier should be slightly orange
|
||||
// since it has opacity.
|
||||
// Should look like a blue screen, with two black boxes. The larger black
|
||||
// box is in the magnifier, is outlined in yellow, and is doubled in size
|
||||
// (from magnification). The magnifier should be slightly transparent.
|
||||
await expectLater(
|
||||
find.byKey(appKey),
|
||||
matchesGoldenFile('widgets.magnifier.styled.png'),
|
||||
@@ -325,4 +326,11 @@ void main() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('MagnifierInfo.toString', (WidgetTester tester) async {
|
||||
expect(MagnifierInfo.empty.toString(),
|
||||
'MagnifierInfo(position: Offset(0.0, 0.0), line: Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), '
|
||||
'caret: Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), field: Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user