Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
f014c1e6
Unverified
Commit
f014c1e6
authored
Aug 02, 2022
by
Anthony Oleinik
Committed by
GitHub
Aug 02, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Loupe Android + iOS (#107477)
* added Magnifier for iOS and Android
parent
5bf6ac18
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
3252 additions
and
31 deletions
+3252
-31
cupertino.dart
packages/flutter/lib/cupertino.dart
+1
-0
material.dart
packages/flutter/lib/material.dart
+1
-0
magnifier.dart
packages/flutter/lib/src/cupertino/magnifier.dart
+323
-0
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+40
-0
magnifier.dart
packages/flutter/lib/src/material/magnifier.dart
+337
-0
selectable_text.dart
packages/flutter/lib/src/material/selectable_text.dart
+15
-0
selection_area.dart
packages/flutter/lib/src/material/selection_area.dart
+14
-0
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+14
-1
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+9
-0
magnifier.dart
packages/flutter/lib/src/widgets/magnifier.dart
+536
-0
selectable_region.dart
packages/flutter/lib/src/widgets/selectable_region.dart
+65
-4
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+303
-2
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
magnifier_test.dart
packages/flutter/test/cupertino/magnifier_test.dart
+259
-0
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+166
-21
magnifier_test.dart
packages/flutter/test/material/magnifier_test.dart
+500
-0
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+168
-3
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+34
-0
magnifier_test.dart
packages/flutter/test/widgets/magnifier_test.dart
+325
-0
selectable_region_test.dart
packages/flutter/test/widgets/selectable_region_test.dart
+68
-0
selectable_text_test.dart
packages/flutter/test/widgets/selectable_text_test.dart
+73
-0
No files found.
packages/flutter/lib/cupertino.dart
View file @
f014c1e6
...
@@ -41,6 +41,7 @@ export 'src/cupertino/interface_level.dart';
...
@@ -41,6 +41,7 @@ export 'src/cupertino/interface_level.dart';
export
'src/cupertino/list_section.dart'
;
export
'src/cupertino/list_section.dart'
;
export
'src/cupertino/list_tile.dart'
;
export
'src/cupertino/list_tile.dart'
;
export
'src/cupertino/localizations.dart'
;
export
'src/cupertino/localizations.dart'
;
export
'src/cupertino/magnifier.dart'
;
export
'src/cupertino/nav_bar.dart'
;
export
'src/cupertino/nav_bar.dart'
;
export
'src/cupertino/page_scaffold.dart'
;
export
'src/cupertino/page_scaffold.dart'
;
export
'src/cupertino/picker.dart'
;
export
'src/cupertino/picker.dart'
;
...
...
packages/flutter/lib/material.dart
View file @
f014c1e6
...
@@ -102,6 +102,7 @@ export 'src/material/input_date_picker_form_field.dart';
...
@@ -102,6 +102,7 @@ export 'src/material/input_date_picker_form_field.dart';
export
'src/material/input_decorator.dart'
;
export
'src/material/input_decorator.dart'
;
export
'src/material/list_tile.dart'
;
export
'src/material/list_tile.dart'
;
export
'src/material/list_tile_theme.dart'
;
export
'src/material/list_tile_theme.dart'
;
export
'src/material/magnifier.dart'
;
export
'src/material/material.dart'
;
export
'src/material/material.dart'
;
export
'src/material/material_button.dart'
;
export
'src/material/material_button.dart'
;
export
'src/material/material_localizations.dart'
;
export
'src/material/material_localizations.dart'
;
...
...
packages/flutter/lib/src/cupertino/magnifier.dart
0 → 100644
View file @
f014c1e6
// 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:math'
as
math
;
import
'package:flutter/widgets.dart'
;
/// A [CupertinoMagnifier] used for magnifying text in cases where a user's
/// finger may be blocking the point of interest, like a selection handle.
///
/// Delegates styling to [CupertinoMagnifier] with its position depending on
/// [magnifierOverlayInfoBearer].
///
/// Specifically, the [CupertinoTextMagnifier] follows the following rules.
/// [CupertinoTextMagnifier]:
/// - is positioned horizontally inside the screen width, with [horizontalScreenEdgePadding] padding.
/// - is hidden if a gesture is detected [hideBelowThreshold] units below the line
/// that the magnifier is on, shown otherwise.
/// - follows the x coordinate of the gesture directly (with respect to rule 1).
/// - has some vertical drag resistance; i.e. if a gesture is detected k units below the field,
/// then has vertical offset [dragResistance] * k.
class
CupertinoTextMagnifier
extends
StatefulWidget
{
/// Construct a [RawMagnifier] in the Cupertino style, positioning with respect to
/// [magnifierOverlayInfoBearer].
///
/// The default constructor parameters and constants were eyeballed on
/// an iPhone XR iOS v15.5.
const
CupertinoTextMagnifier
({
super
.
key
,
this
.
animationCurve
=
Curves
.
easeOut
,
required
this
.
controller
,
this
.
dragResistance
=
10.0
,
this
.
hideBelowThreshold
=
48.0
,
this
.
horizontalScreenEdgePadding
=
10.0
,
required
this
.
magnifierOverlayInfoBearer
,
});
/// The curve used for the in / out animations.
final
Curve
animationCurve
;
/// This magnifier's controller.
///
/// The [CupertinoTextMagnifier] requires a [MagnifierController]
/// in order to show / hide itself without removing itself from the
/// overlay.
final
MagnifierController
controller
;
/// A drag resistance on the downward Y position of the lens.
final
double
dragResistance
;
/// The difference in Y between the gesture position and the caret center
/// so that the magnifier hides itself.
final
double
hideBelowThreshold
;
/// The padding on either edge of the screen that any part of the magnifier
/// cannot exist past.
///
/// This includes any part of the magnifier, not just the center; for example,
/// the left edge of the magnifier cannot be outside the [horizontalScreenEdgePadding].v
///
/// If the screen has width w, then the magnifier is bound to
/// `_kHorizontalScreenEdgePadding, w - _kHorizontalScreenEdgePadding`.
final
double
horizontalScreenEdgePadding
;
/// [CupertinoTextMagnifier] will determine its own positioning
/// based on the [MagnifierOverlayInfoBearer] of this notifier.
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierOverlayInfoBearer
;
/// The duration that the magnifier drags behind its final position.
static
const
Duration
_kDragAnimationDuration
=
Duration
(
milliseconds:
45
);
@override
State
<
CupertinoTextMagnifier
>
createState
()
=>
_CupertinoTextMagnifierState
();
}
class
_CupertinoTextMagnifierState
extends
State
<
CupertinoTextMagnifier
>
with
SingleTickerProviderStateMixin
{
// Initalize to dummy values for the event that the inital call to
// _determineMagnifierPositionAndFocalPoint calls hide, and thus does not
// set these values.
Offset
_currentAdjustedMagnifierPosition
=
Offset
.
zero
;
double
_verticalFocalPointAdjustment
=
0
;
late
AnimationController
_ioAnimationController
;
late
Animation
<
double
>
_ioAnimation
;
@override
void
initState
()
{
super
.
initState
();
_ioAnimationController
=
AnimationController
(
value:
0
,
vsync:
this
,
duration:
CupertinoMagnifier
.
_kInOutAnimationDuration
,
)..
addListener
(()
=>
setState
(()
{}));
widget
.
controller
.
animationController
=
_ioAnimationController
;
widget
.
magnifierOverlayInfoBearer
.
addListener
(
_determineMagnifierPositionAndFocalPoint
);
_ioAnimation
=
Tween
<
double
>(
begin:
0.0
,
end:
1.0
,
).
animate
(
CurvedAnimation
(
parent:
_ioAnimationController
,
curve:
widget
.
animationCurve
,
));
}
@override
void
dispose
()
{
widget
.
controller
.
animationController
=
null
;
_ioAnimationController
.
dispose
();
widget
.
magnifierOverlayInfoBearer
.
removeListener
(
_determineMagnifierPositionAndFocalPoint
);
super
.
dispose
();
}
@override
void
didUpdateWidget
(
CupertinoTextMagnifier
oldWidget
)
{
if
(
oldWidget
.
magnifierOverlayInfoBearer
!=
widget
.
magnifierOverlayInfoBearer
)
{
oldWidget
.
magnifierOverlayInfoBearer
.
removeListener
(
_determineMagnifierPositionAndFocalPoint
);
widget
.
magnifierOverlayInfoBearer
.
addListener
(
_determineMagnifierPositionAndFocalPoint
);
}
super
.
didUpdateWidget
(
oldWidget
);
}
@override
void
didChangeDependencies
()
{
_determineMagnifierPositionAndFocalPoint
();
super
.
didChangeDependencies
();
}
void
_determineMagnifierPositionAndFocalPoint
()
{
final
MagnifierOverlayInfoBearer
textEditingContext
=
widget
.
magnifierOverlayInfoBearer
.
value
;
// The exact Y of the center of the current line.
final
double
verticalCenterOfCurrentLine
=
textEditingContext
.
caretRect
.
center
.
dy
;
// If the magnifier is currently showing, but we have dragged out of threshold,
// we should hide it.
if
(
verticalCenterOfCurrentLine
-
textEditingContext
.
globalGesturePosition
.
dy
<
-
widget
.
hideBelowThreshold
)
{
// Only signal a hide if we are currently showing.
if
(
widget
.
controller
.
shown
)
{
widget
.
controller
.
hide
(
removeFromOverlay:
false
);
}
return
;
}
// If we are gone, but got to this point, we shouldn't be: show.
if
(!
widget
.
controller
.
shown
)
{
_ioAnimationController
.
forward
();
}
// Never go above the center of the line, but have some resistance
// going downward if the drag goes too far.
final
double
verticalPositionOfLens
=
math
.
max
(
verticalCenterOfCurrentLine
,
verticalCenterOfCurrentLine
-
(
verticalCenterOfCurrentLine
-
textEditingContext
.
globalGesturePosition
.
dy
)
/
widget
.
dragResistance
);
// The raw position, tracking the gesture directly.
final
Offset
rawMagnifierPosition
=
Offset
(
textEditingContext
.
globalGesturePosition
.
dx
-
CupertinoMagnifier
.
kDefaultSize
.
width
/
2
,
verticalPositionOfLens
-
(
CupertinoMagnifier
.
kDefaultSize
.
height
-
CupertinoMagnifier
.
kMagnifierAboveFocalPoint
),
);
final
Rect
screenRect
=
Offset
.
zero
&
MediaQuery
.
of
(
context
).
size
;
// Adjust the magnifier position so that it never exists outside the horizontal
// padding.
final
Offset
adjustedMagnifierPosition
=
MagnifierController
.
shiftWithinBounds
(
bounds:
Rect
.
fromLTRB
(
screenRect
.
left
+
widget
.
horizontalScreenEdgePadding
,
// iOS doesn't reposition for Y, so we should expand the threshold
// so we can send the whole magnifier out of bounds if need be.
screenRect
.
top
-
(
CupertinoMagnifier
.
kDefaultSize
.
height
+
CupertinoMagnifier
.
kMagnifierAboveFocalPoint
),
screenRect
.
right
-
widget
.
horizontalScreenEdgePadding
,
screenRect
.
bottom
+
(
CupertinoMagnifier
.
kDefaultSize
.
height
+
CupertinoMagnifier
.
kMagnifierAboveFocalPoint
)),
rect:
rawMagnifierPosition
&
CupertinoMagnifier
.
kDefaultSize
,
).
topLeft
;
setState
(()
{
_currentAdjustedMagnifierPosition
=
adjustedMagnifierPosition
;
// The lens should always point to the center of the line.
_verticalFocalPointAdjustment
=
verticalCenterOfCurrentLine
-
verticalPositionOfLens
;
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
AnimatedPositioned
(
duration:
CupertinoTextMagnifier
.
_kDragAnimationDuration
,
curve:
widget
.
animationCurve
,
left:
_currentAdjustedMagnifierPosition
.
dx
,
top:
_currentAdjustedMagnifierPosition
.
dy
,
child:
CupertinoMagnifier
(
inOutAnimation:
_ioAnimation
,
additionalFocalPointOffset:
Offset
(
0
,
_verticalFocalPointAdjustment
),
),
);
}
}
/// A [RawMagnifier] used for magnifying text in cases where a user's
/// finger may be blocking the point of interest, like a selection handle.
///
/// [CupertinoMagnifier] is a wrapper around [RawMagnifier] that handles styling
/// and transitions.
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// See also:
///
/// * [RawMagnifier], the backing implementation.
/// * [CupertinoTextMagnifier], a widget that positions [CupertinoMagnifier] based on
/// [MagnifierOverlayInfoBearer].
/// * [MagnifierController], the controller for this magnifier.
class
CupertinoMagnifier
extends
StatelessWidget
{
/// Creates a [RawMagnifier] in the Cupertino style.
///
/// The default constructor parameters and constants were eyeballed on
/// an iPhone XR iOS v15.5.
const
CupertinoMagnifier
({
super
.
key
,
this
.
size
=
kDefaultSize
,
this
.
borderRadius
=
const
BorderRadius
.
all
(
Radius
.
elliptical
(
60
,
50
)),
this
.
additionalFocalPointOffset
=
Offset
.
zero
,
this
.
shadows
=
const
<
BoxShadow
>[
BoxShadow
(
color:
Color
.
fromARGB
(
25
,
0
,
0
,
0
),
blurRadius:
11
,
spreadRadius:
0.2
,
),
],
this
.
borderSide
=
const
BorderSide
(
color:
Color
.
fromARGB
(
255
,
232
,
232
,
232
)),
this
.
inOutAnimation
,
});
/// The shadows displayed under the magnifier.
final
List
<
BoxShadow
>
shadows
;
/// The border, or "rim", of this magnifier.
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 overrideable.
@visibleForTesting
static
const
Size
kDefaultSize
=
Size
(
80
,
47.5
);
/// The duration that this magnifier animates in / out for.
///
/// The animation is a translation and a fade. The translation
/// begins at the focal point, and ends at [kMagnifierAboveFocalPoint].
/// The opacity begins at 0 and ends at 1.
static
const
Duration
_kInOutAnimationDuration
=
Duration
(
milliseconds:
150
);
/// The size of this magnifier.
final
Size
size
;
/// The border radius of this magnifier.
final
BorderRadius
borderRadius
;
/// This [RawMagnifier]'s controller.
///
/// Since [CupertinoMagnifier] has no knowledge of shown / hidden state,
/// this animation should be driven by an external actor.
final
Animation
<
double
>?
inOutAnimation
;
/// Any additional focal point offset, applied over the regular focal
/// point offset defined in [kMagnifierAboveFocalPoint].
final
Offset
additionalFocalPointOffset
;
@override
Widget
build
(
BuildContext
context
)
{
Offset
focalPointOffset
=
Offset
(
0
,
(
kDefaultSize
.
height
/
2
)
-
kMagnifierAboveFocalPoint
);
focalPointOffset
.
scale
(
1
,
inOutAnimation
?.
value
??
1
);
focalPointOffset
+=
additionalFocalPointOffset
;
return
Transform
.
translate
(
offset:
Offset
.
lerp
(
const
Offset
(
0
,
-
kMagnifierAboveFocalPoint
),
Offset
.
zero
,
inOutAnimation
?.
value
??
1
,
)!,
child:
RawMagnifier
(
size:
size
,
focalPointOffset:
focalPointOffset
,
decoration:
MagnifierDecoration
(
opacity:
inOutAnimation
?.
value
??
1
,
shape:
RoundedRectangleBorder
(
borderRadius:
borderRadius
,
side:
borderSide
,
),
shadows:
shadows
,
),
),
);
}
}
packages/flutter/lib/src/cupertino/text_field.dart
View file @
f014c1e6
...
@@ -13,6 +13,7 @@ import 'package:flutter/widgets.dart';
...
@@ -13,6 +13,7 @@ import 'package:flutter/widgets.dart';
import
'colors.dart'
;
import
'colors.dart'
;
import
'desktop_text_selection.dart'
;
import
'desktop_text_selection.dart'
;
import
'icons.dart'
;
import
'icons.dart'
;
import
'magnifier.dart'
;
import
'text_selection.dart'
;
import
'text_selection.dart'
;
import
'theme.dart'
;
import
'theme.dart'
;
...
@@ -273,6 +274,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -273,6 +274,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
restorationId
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
magnifierConfiguration
,
})
:
assert
(
textAlign
!=
null
),
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
autofocus
!=
null
),
...
@@ -434,6 +436,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -434,6 +436,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
restorationId
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
magnifierConfiguration
,
})
:
assert
(
textAlign
!=
null
),
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
autofocus
!=
null
),
...
@@ -783,6 +786,21 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -783,6 +786,21 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final
bool
enableIMEPersonalizedLearning
;
final
bool
enableIMEPersonalizedLearning
;
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and Android nothing on all other
/// platforms. If it is desired to supress the magnifier, consider passing
/// [TextMagnifierConfiguration.disabled].
///
// TODO(antholeole): https://github.com/flutter/flutter/issues/108041
// once the magnifier PR lands, I should enrich this area of the
// docs with images of what a magnifier is.
final
TextMagnifierConfiguration
?
magnifierConfiguration
;
@override
@override
State
<
CupertinoTextField
>
createState
()
=>
_CupertinoTextFieldState
();
State
<
CupertinoTextField
>
createState
()
=>
_CupertinoTextFieldState
();
...
@@ -827,6 +845,27 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -827,6 +845,27 @@ class CupertinoTextField extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
}
}
static
final
TextMagnifierConfiguration
_iosMagnifierConfiguration
=
TextMagnifierConfiguration
(
magnifierBuilder:
(
BuildContext
context
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierOverlayInfoBearer
)
{
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
android
:
case
TargetPlatform
.
iOS
:
return
CupertinoTextMagnifier
(
controller:
controller
,
magnifierOverlayInfoBearer:
magnifierOverlayInfoBearer
,
);
case
TargetPlatform
.
fuchsia
:
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
windows
:
return
null
;
}
});
}
}
class
_CupertinoTextFieldState
extends
State
<
CupertinoTextField
>
with
RestorationMixin
,
AutomaticKeepAliveClientMixin
<
CupertinoTextField
>
implements
TextSelectionGestureDetectorBuilderDelegate
,
AutofillClient
{
class
_CupertinoTextFieldState
extends
State
<
CupertinoTextField
>
with
RestorationMixin
,
AutomaticKeepAliveClientMixin
<
CupertinoTextField
>
implements
TextSelectionGestureDetectorBuilderDelegate
,
AutofillClient
{
...
@@ -1274,6 +1313,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
...
@@ -1274,6 +1313,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
maxLines:
widget
.
maxLines
,
maxLines:
widget
.
maxLines
,
minLines:
widget
.
minLines
,
minLines:
widget
.
minLines
,
expands:
widget
.
expands
,
expands:
widget
.
expands
,
magnifierConfiguration:
widget
.
magnifierConfiguration
??
CupertinoTextField
.
_iosMagnifierConfiguration
,
// Only show the selection highlight when the text field is focused.
// Only show the selection highlight when the text field is focused.
selectionColor:
_effectiveFocusNode
.
hasFocus
?
selectionColor
:
null
,
selectionColor:
_effectiveFocusNode
.
hasFocus
?
selectionColor
:
null
,
selectionControls:
widget
.
selectionEnabled
selectionControls:
widget
.
selectionEnabled
...
...
packages/flutter/lib/src/material/magnifier.dart
0 → 100644
View file @
f014c1e6
// 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: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}
class
TextMagnifier
extends
StatefulWidget
{
/// {@macro widgets.material.magnifier.magnifier}
///
/// {@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}
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.
static
TextMagnifierConfiguration
adaptiveMagnifierConfiguration
=
TextMagnifierConfiguration
(
shouldDisplayHandlesInMagnifier:
defaultTargetPlatform
==
TargetPlatform
.
iOS
,
magnifierBuilder:
(
BuildContext
context
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierOverlayInfoBearer
,
)
{
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
iOS
:
return
CupertinoTextMagnifier
(
controller:
controller
,
magnifierOverlayInfoBearer:
magnifierOverlayInfoBearer
,
);
case
TargetPlatform
.
android
:
return
TextMagnifier
(
magnifierInfo:
magnifierOverlayInfoBearer
,
);
case
TargetPlatform
.
fuchsia
:
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
windows
:
return
null
;
}
}
);
/// The duration that the position is animated if [TextMagnifier] just switched
/// between lines.
@visibleForTesting
static
const
Duration
jumpBetweenLinesAnimationDuration
=
Duration
(
milliseconds:
70
);
/// [TextMagnifier] positions itself based on [magnifierInfo].
///
/// {@macro widgets.material.magnifier.positionRules}
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierInfo
;
@override
State
<
TextMagnifier
>
createState
()
=>
_TextMagnifierState
();
}
class
_TextMagnifierState
extends
State
<
TextMagnifier
>
{
// Should _only_ be null on construction. This is because of the animation logic.
//
// Animations are added when `last_build_y != current_build_y`. This condition
// is true on the inital render, which would mean that the inital
// build would be animated - this is undesired. Thus, this is null for the
// first frame and the condition becomes `magnifierPosition != null && last_build_y != this_build_y`.
Offset
?
_magnifierPosition
;
// A timer that unsets itself after an animation duration.
// If the timer exists, then the magnifier animates its position -
// if this timer does not exist, the magnifier tracks the gesture (with respect
// to the positioning rules) directly.
Timer
?
_positionShouldBeAnimatedTimer
;
bool
get
_positionShouldBeAnimated
=>
_positionShouldBeAnimatedTimer
!=
null
;
Offset
_extraFocalPointOffset
=
Offset
.
zero
;
@override
void
initState
()
{
super
.
initState
();
widget
.
magnifierInfo
.
addListener
(
_determineMagnifierPositionAndFocalPoint
);
}
@override
void
dispose
()
{
widget
.
magnifierInfo
.
removeListener
(
_determineMagnifierPositionAndFocalPoint
);
_positionShouldBeAnimatedTimer
?.
cancel
();
super
.
dispose
();
}
@override
void
didChangeDependencies
()
{
_determineMagnifierPositionAndFocalPoint
();
super
.
didChangeDependencies
();
}
@override
void
didUpdateWidget
(
TextMagnifier
oldWidget
)
{
if
(
oldWidget
.
magnifierInfo
!=
widget
.
magnifierInfo
)
{
oldWidget
.
magnifierInfo
.
removeListener
(
_determineMagnifierPositionAndFocalPoint
);
widget
.
magnifierInfo
.
addListener
(
_determineMagnifierPositionAndFocalPoint
);
}
super
.
didUpdateWidget
(
oldWidget
);
}
/// {@macro widgets.material.magnifier.positionRules}
void
_determineMagnifierPositionAndFocalPoint
()
{
final
MagnifierOverlayInfoBearer
selectionInfo
=
widget
.
magnifierInfo
.
value
;
final
Rect
screenRect
=
Offset
.
zero
&
MediaQuery
.
of
(
context
).
size
;
// 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
// the "above touch point" shift.
final
Offset
basicMagnifierOffset
=
Offset
(
Magnifier
.
kDefaultMagnifierSize
.
width
/
2
,
Magnifier
.
kDefaultMagnifierSize
.
height
+
Magnifier
.
kStandardVerticalFocalPointShift
);
// Since the magnifier should not go past the edges of the line,
// but must track the gesture otherwise, constrain the X of the magnifier
// to always stay between line start and end.
final
double
magnifierX
=
clampDouble
(
selectionInfo
.
globalGesturePosition
.
dx
,
selectionInfo
.
currentLineBoundries
.
left
,
selectionInfo
.
currentLineBoundries
.
right
);
// Place the magnifier at the previously calculated X, and the Y should be
// exactly at the center of the handle.
final
Rect
unadjustedMagnifierRect
=
Offset
(
magnifierX
,
selectionInfo
.
caretRect
.
center
.
dy
)
-
basicMagnifierOffset
&
Magnifier
.
kDefaultMagnifierSize
;
// Shift the magnifier so that, if we are ever out of the screen, we become in bounds.
// This probably won't have much of an effect on the X, since it is already bound
// to the currentLineBoundries, but will shift vertically if the magnifier is out of bounds.
final
Rect
screenBoundsAdjustedMagnifierRect
=
MagnifierController
.
shiftWithinBounds
(
bounds:
screenRect
,
rect:
unadjustedMagnifierRect
);
// Done with the magnifier position!
final
Offset
finalMagnifierPosition
=
screenBoundsAdjustedMagnifierRect
.
topLeft
;
// The insets, from either edge, that the focal point should not point
// past lest the magnifier displays something out of bounds.
final
double
horizontalMaxFocalPointEdgeInsets
=
(
Magnifier
.
kDefaultMagnifierSize
.
width
/
2
)
/
Magnifier
.
_magnification
;
// Adjust the focal point horizontally such that none of the magnifier
// ever points to anything out of bounds.
final
double
newGlobalFocalPointX
;
// If the text field is so narrow that we must show out of bounds,
// then settle for pointing to the center all the time.
if
(
selectionInfo
.
fieldBounds
.
width
<
horizontalMaxFocalPointEdgeInsets
*
2
)
{
newGlobalFocalPointX
=
selectionInfo
.
fieldBounds
.
center
.
dx
;
}
else
{
// Otherwise, we can clamp the focal point to always point in bounds.
newGlobalFocalPointX
=
clampDouble
(
screenBoundsAdjustedMagnifierRect
.
center
.
dx
,
selectionInfo
.
fieldBounds
.
left
+
horizontalMaxFocalPointEdgeInsets
,
selectionInfo
.
fieldBounds
.
right
-
horizontalMaxFocalPointEdgeInsets
);
}
// Since the previous value is now a global offset (i.e. `newGlobalFocalPoint`
// is now a global offset), we must subtract the magnifier's global offset
// to obtain the relative shift in the focal point.
final
double
newRelativeFocalPointX
=
newGlobalFocalPointX
-
screenBoundsAdjustedMagnifierRect
.
center
.
dx
;
// The Y component means that if we are pressed up against the top of the screen,
// then we should adjust the focal point such that it now points to how far we moved
// the magnifier. screenBoundsAdjustedMagnifierRect.top == unadjustedMagnifierRect.top for most cases,
// but when pressed up against the top of the screen, we adjust the focal point by
// the amount that we shifted from our "natural" position.
final
Offset
focalPointAdjustmentForScreenBoundsAdjustment
=
Offset
(
newRelativeFocalPointX
,
unadjustedMagnifierRect
.
top
-
screenBoundsAdjustedMagnifierRect
.
top
,
);
Timer
?
positionShouldBeAnimated
=
_positionShouldBeAnimatedTimer
;
if
(
_magnifierPosition
!=
null
&&
finalMagnifierPosition
.
dy
!=
_magnifierPosition
!.
dy
)
{
if
(
_positionShouldBeAnimatedTimer
!=
null
&&
_positionShouldBeAnimatedTimer
!.
isActive
)
{
_positionShouldBeAnimatedTimer
!.
cancel
();
}
// Create a timer that deletes itself when the timer is complete.
// This is `mounted` safe, since the timer is canceled in `dispose`.
positionShouldBeAnimated
=
Timer
(
TextMagnifier
.
jumpBetweenLinesAnimationDuration
,
()
=>
setState
(()
{
_positionShouldBeAnimatedTimer
=
null
;
}));
}
setState
(()
{
_magnifierPosition
=
finalMagnifierPosition
;
_positionShouldBeAnimatedTimer
=
positionShouldBeAnimated
;
_extraFocalPointOffset
=
focalPointAdjustmentForScreenBoundsAdjustment
;
});
}
@override
Widget
build
(
BuildContext
context
)
{
assert
(
_magnifierPosition
!=
null
,
'Magnifier position should only be null before the first build.'
);
return
AnimatedPositioned
(
top:
_magnifierPosition
!.
dy
,
left:
_magnifierPosition
!.
dx
,
// Material magnifier typically does not animate, unless we jump between lines,
// in which case we animate between lines.
duration:
_positionShouldBeAnimated
?
TextMagnifier
.
jumpBetweenLinesAnimationDuration
:
Duration
.
zero
,
child:
Magnifier
(
additionalFocalPointOffset:
_extraFocalPointOffset
,
),
);
}
}
/// 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].
class
Magnifier
extends
StatelessWidget
{
/// Creates a [RawMagnifier] in the Material style.
///
/// {@macro widgets.material.magnifier.androidDisclaimer}
const
Magnifier
({
super
.
key
,
this
.
additionalFocalPointOffset
=
Offset
.
zero
,
this
.
borderRadius
=
const
BorderRadius
.
all
(
Radius
.
circular
(
_borderRadius
)),
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
))
],
this
.
size
=
Magnifier
.
kDefaultMagnifierSize
,
});
/// The default size of this [Magnifier].
///
/// 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 garunteed size, as opposed to an estimate.
@visibleForTesting
static
const
double
kStandardVerticalFocalPointShift
=
22
;
static
const
double
_borderRadius
=
40
;
static
const
double
_magnification
=
1.25
;
/// 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.
final
Offset
additionalFocalPointOffset
;
/// The border radius for this magnifier.
final
BorderRadius
borderRadius
;
/// The color to tint the image in this [Magnifier].
///
/// On native Android, there is a almost transparent gray tint to the
/// magnifier, in order to better distinguish the contents of the lens from
/// the background.
final
Color
filmColor
;
/// The shadows for this [Magnifier].
final
List
<
BoxShadow
>
shadows
;
/// The [Size] of this [Magnifier].
///
/// This size does not include the border.
final
Size
size
;
@override
Widget
build
(
BuildContext
context
)
{
return
RawMagnifier
(
decoration:
MagnifierDecoration
(
shape:
RoundedRectangleBorder
(
borderRadius:
borderRadius
),
shadows:
shadows
,
),
magnificationScale:
_magnification
,
focalPointOffset:
additionalFocalPointOffset
+
Offset
(
0
,
kStandardVerticalFocalPointShift
+
kDefaultMagnifierSize
.
height
/
2
),
size:
size
,
child:
ColoredBox
(
color:
filmColor
,
),
);
}
}
packages/flutter/lib/src/material/selectable_text.dart
View file @
f014c1e6
...
@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
...
@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
import
'desktop_text_selection.dart'
;
import
'desktop_text_selection.dart'
;
import
'feedback.dart'
;
import
'feedback.dart'
;
import
'magnifier.dart'
;
import
'text_selection.dart'
;
import
'text_selection.dart'
;
import
'theme.dart'
;
import
'theme.dart'
;
...
@@ -203,6 +204,7 @@ class SelectableText extends StatefulWidget {
...
@@ -203,6 +204,7 @@ class SelectableText extends StatefulWidget {
this
.
textHeightBehavior
,
this
.
textHeightBehavior
,
this
.
textWidthBasis
,
this
.
textWidthBasis
,
this
.
onSelectionChanged
,
this
.
onSelectionChanged
,
this
.
magnifierConfiguration
,
})
:
assert
(
showCursor
!=
null
),
})
:
assert
(
showCursor
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
dragStartBehavior
!=
null
),
assert
(
dragStartBehavior
!=
null
),
...
@@ -260,6 +262,7 @@ class SelectableText extends StatefulWidget {
...
@@ -260,6 +262,7 @@ class SelectableText extends StatefulWidget {
this
.
textHeightBehavior
,
this
.
textHeightBehavior
,
this
.
textWidthBasis
,
this
.
textWidthBasis
,
this
.
onSelectionChanged
,
this
.
onSelectionChanged
,
this
.
magnifierConfiguration
,
})
:
assert
(
showCursor
!=
null
),
})
:
assert
(
showCursor
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
dragStartBehavior
!=
null
),
assert
(
dragStartBehavior
!=
null
),
...
@@ -427,6 +430,17 @@ class SelectableText extends StatefulWidget {
...
@@ -427,6 +430,17 @@ class SelectableText extends StatefulWidget {
/// {@macro flutter.widgets.editableText.onSelectionChanged}
/// {@macro flutter.widgets.editableText.onSelectionChanged}
final
SelectionChangedCallback
?
onSelectionChanged
;
final
SelectionChangedCallback
?
onSelectionChanged
;
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] on
/// Android, and builds nothing on all other platforms. If it is desired to supress
/// the magnifier, consider passing [TextMagnifierConfiguration.disabled].
final
TextMagnifierConfiguration
?
magnifierConfiguration
;
@override
@override
State
<
SelectableText
>
createState
()
=>
_SelectableTextState
();
State
<
SelectableText
>
createState
()
=>
_SelectableTextState
();
...
@@ -705,6 +719,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
...
@@ -705,6 +719,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
paintCursorAboveText:
paintCursorAboveText
,
paintCursorAboveText:
paintCursorAboveText
,
backgroundCursorColor:
CupertinoColors
.
inactiveGray
,
backgroundCursorColor:
CupertinoColors
.
inactiveGray
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
,
magnifierConfiguration:
widget
.
magnifierConfiguration
??
TextMagnifier
.
adaptiveMagnifierConfiguration
,
dragStartBehavior:
widget
.
dragStartBehavior
,
dragStartBehavior:
widget
.
dragStartBehavior
,
scrollPhysics:
widget
.
scrollPhysics
,
scrollPhysics:
widget
.
scrollPhysics
,
autofillHints:
null
,
autofillHints:
null
,
...
...
packages/flutter/lib/src/material/selection_area.dart
View file @
f014c1e6
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'desktop_text_selection.dart'
;
import
'desktop_text_selection.dart'
;
import
'magnifier.dart'
;
import
'text_selection.dart'
;
import
'text_selection.dart'
;
import
'theme.dart'
;
import
'theme.dart'
;
...
@@ -34,9 +35,21 @@ class SelectionArea extends StatefulWidget {
...
@@ -34,9 +35,21 @@ class SelectionArea extends StatefulWidget {
super
.
key
,
super
.
key
,
this
.
focusNode
,
this
.
focusNode
,
this
.
selectionControls
,
this
.
selectionControls
,
this
.
magnifierConfiguration
,
required
this
.
child
,
required
this
.
child
,
});
});
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] on
/// Android, and builds nothing on all other platforms. If it is desired to supress
/// the magnifier, consider passing [TextMagnifierConfiguration.disabled].
final
TextMagnifierConfiguration
?
magnifierConfiguration
;
/// {@macro flutter.widgets.Focus.focusNode}
/// {@macro flutter.widgets.Focus.focusNode}
final
FocusNode
?
focusNode
;
final
FocusNode
?
focusNode
;
...
@@ -92,6 +105,7 @@ class _SelectionAreaState extends State<SelectionArea> {
...
@@ -92,6 +105,7 @@ class _SelectionAreaState extends State<SelectionArea> {
return
SelectableRegion
(
return
SelectableRegion
(
focusNode:
_effectiveFocusNode
,
focusNode:
_effectiveFocusNode
,
selectionControls:
controls
,
selectionControls:
controls
,
magnifierConfiguration:
widget
.
magnifierConfiguration
??
TextMagnifier
.
adaptiveMagnifierConfiguration
,
child:
widget
.
child
,
child:
widget
.
child
,
);
);
}
}
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
f014c1e6
...
@@ -14,7 +14,7 @@ import 'debug.dart';
...
@@ -14,7 +14,7 @@ import 'debug.dart';
import
'desktop_text_selection.dart'
;
import
'desktop_text_selection.dart'
;
import
'feedback.dart'
;
import
'feedback.dart'
;
import
'input_decorator.dart'
;
import
'input_decorator.dart'
;
import
'ma
terial
.dart'
;
import
'ma
gnifier
.dart'
;
import
'material_localizations.dart'
;
import
'material_localizations.dart'
;
import
'material_state.dart'
;
import
'material_state.dart'
;
import
'selectable_text.dart'
show
iOSHorizontalOffset
;
import
'selectable_text.dart'
show
iOSHorizontalOffset
;
...
@@ -330,6 +330,7 @@ class TextField extends StatefulWidget {
...
@@ -330,6 +330,7 @@ class TextField extends StatefulWidget {
this
.
restorationId
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
magnifierConfiguration
,
})
:
assert
(
textAlign
!=
null
),
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
autofocus
!=
null
),
...
@@ -392,6 +393,17 @@ class TextField extends StatefulWidget {
...
@@ -392,6 +393,17 @@ class TextField extends StatefulWidget {
paste:
true
,
paste:
true
,
)));
)));
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] on
/// Android, and builds nothing on all other platforms. If it is desired to supress
/// the magnifier, consider passing [TextMagnifierConfiguration.disabled].
final
TextMagnifierConfiguration
?
magnifierConfiguration
;
/// Controls the text being edited.
/// Controls the text being edited.
///
///
/// If null, this widget will create its own [TextEditingController].
/// If null, this widget will create its own [TextEditingController].
...
@@ -1312,6 +1324,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
...
@@ -1312,6 +1324,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
restorationId:
'editable'
,
restorationId:
'editable'
,
scribbleEnabled:
widget
.
scribbleEnabled
,
scribbleEnabled:
widget
.
scribbleEnabled
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
magnifierConfiguration:
widget
.
magnifierConfiguration
??
TextMagnifier
.
adaptiveMagnifierConfiguration
,
),
),
),
),
);
);
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
f014c1e6
...
@@ -636,6 +636,7 @@ class EditableText extends StatefulWidget {
...
@@ -636,6 +636,7 @@ class EditableText extends StatefulWidget {
this
.
scrollBehavior
,
this
.
scrollBehavior
,
this
.
scribbleEnabled
=
true
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
magnifierConfiguration
=
TextMagnifierConfiguration
.
disabled
,
})
:
assert
(
controller
!=
null
),
})
:
assert
(
controller
!=
null
),
assert
(
focusNode
!=
null
),
assert
(
focusNode
!=
null
),
assert
(
obscuringCharacter
!=
null
&&
obscuringCharacter
.
length
==
1
),
assert
(
obscuringCharacter
!=
null
&&
obscuringCharacter
.
length
==
1
),
...
@@ -1547,6 +1548,13 @@ class EditableText extends StatefulWidget {
...
@@ -1547,6 +1548,13 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final
bool
enableIMEPersonalizedLearning
;
final
bool
enableIMEPersonalizedLearning
;
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
final
TextMagnifierConfiguration
magnifierConfiguration
;
bool
get
_userSelectionEnabled
=>
enableInteractiveSelection
&&
(!
readOnly
||
!
obscureText
);
bool
get
_userSelectionEnabled
=>
enableInteractiveSelection
&&
(!
readOnly
||
!
obscureText
);
// Infer the keyboard type of an `EditableText` if it's not specified.
// Infer the keyboard type of an `EditableText` if it's not specified.
...
@@ -2629,6 +2637,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2629,6 +2637,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
selectionDelegate:
this
,
selectionDelegate:
this
,
dragStartBehavior:
widget
.
dragStartBehavior
,
dragStartBehavior:
widget
.
dragStartBehavior
,
onSelectionHandleTapped:
widget
.
onSelectionHandleTapped
,
onSelectionHandleTapped:
widget
.
onSelectionHandleTapped
,
magnifierConfiguration:
widget
.
magnifierConfiguration
,
);
);
}
}
...
...
packages/flutter/lib/src/widgets/magnifier.dart
0 → 100644
View file @
f014c1e6
// 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
'dart:math'
as
math
;
import
'dart:ui'
;
import
'package:flutter/rendering.dart'
;
import
'basic.dart'
;
import
'container.dart'
;
import
'framework.dart'
;
import
'inherited_theme.dart'
;
import
'navigator.dart'
;
import
'overlay.dart'
;
/// [MagnifierController]'s main benefit over holding a raw [OverlayEntry] is that
/// [MagnifierController] will handle logic around waiting for a magnifier to animate in or out.
///
/// If a magnifier chooses to have an entry / exit animation, it should provide the animation
/// controller to [MagnifierController.animationController]. [MagnifierController] will then drive
/// the [AnimationController] and wait for it to be complete before removing it from the
/// [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.
MagnifierController
({
this
.
animationController
})
{
animationController
?.
value
=
0
;
}
/// The controller that will be driven in / out when show / hide is triggered,
/// respectively.
AnimationController
?
animationController
;
/// The magnifier's [OverlayEntry], if currently in the overlay.
///
/// This is public in case other overlay entries need to be positioned
/// above or below this [overlayEntry]. Anything in the paint order after
/// the [RawMagnifier] will not be displayed in the magnifier; this means that if it
/// is desired for an overlay entry to be displayed in the magnifier,
/// it _must_ be positioned below the magnifier.
///
/// {@tool snippet}
/// ```dart
/// void magnifierShowExample(BuildContext context) {
/// final MagnifierController myMagnifierController = MagnifierController();
///
/// // Placed below the magnifier, so it will show.
/// Overlay.of(context)!.insert(OverlayEntry(
/// builder: (BuildContext context) => const Text('I WILL display in the magnifier')));
///
/// // Will display in the magnifier, since this entry was passed to show.
/// final OverlayEntry displayInMagnifier = OverlayEntry(
/// builder: (BuildContext context) =>
/// const Text('I WILL display in the magnifier'));
///
/// Overlay.of(context)!
/// .insert(displayInMagnifier);
/// myMagnifierController.show(
/// context: context,
/// below: displayInMagnifier,
/// builder: (BuildContext context) => const RawMagnifier(
/// size: Size(100, 100),
/// ));
///
/// // By default, new entries will be placed over the top entry.
/// Overlay.of(context)!.insert(OverlayEntry(
/// builder: (BuildContext context) => const Text('I WILL NOT display in the magnifier')));
///
/// Overlay.of(context)!.insert(
/// below:
/// myMagnifierController.overlayEntry, // Explicitly placed below the magnifier.
/// OverlayEntry(
/// builder: (BuildContext context) => const Text('I WILL display in the magnifier')));
/// }
/// ```
/// {@end-tool}
///
/// A null check on [overlayEntry] will not suffice to check if a magnifier is in the
/// overlay or not; instead, you should check [shown]. This is because it is possible,
/// such as in cases where [hide] was called with `removeFromOverlay` false, that the magnifier
/// is not shown, but the entry is not null.
OverlayEntry
?
get
overlayEntry
=>
_overlayEntry
;
OverlayEntry
?
_overlayEntry
;
/// If the magnifier is shown or not.
///
/// [shown] is:
/// - false when nothing is in the overlay.
/// - false when [animationController] is [AnimationStatus.dismissed].
/// - false when [animationController] is animating out.
/// and true in all other circumstances.
bool
get
shown
{
if
(
overlayEntry
==
null
)
{
return
false
;
}
if
(
animationController
!=
null
)
{
return
animationController
!.
status
==
AnimationStatus
.
completed
||
animationController
!.
status
==
AnimationStatus
.
forward
;
}
return
true
;
}
/// Shows the [RawMagnifier] that this controller controls.
///
/// Returns a future that completes when the magnifier is fully shown, i.e. done
/// with its entry animation.
///
/// To control what overlays are shown in the magnifier, utilize [below]. See
/// [overlayEntry] for more details on how to utilize [below].
///
/// If the magnifier already exists (i.e. [overlayEntry] != null), then [show] will
/// override the old overlay and not play an exit animation. Consider awaiting [hide]
/// first, to guarantee
Future
<
void
>
show
({
required
BuildContext
context
,
required
WidgetBuilder
builder
,
Widget
?
debugRequiredFor
,
OverlayEntry
?
below
,
})
async
{
if
(
overlayEntry
!=
null
)
{
overlayEntry
!.
remove
();
}
final
OverlayState
?
overlayState
=
Overlay
.
of
(
context
,
rootOverlay:
true
,
debugRequiredFor:
debugRequiredFor
,
);
final
CapturedThemes
capturedThemes
=
InheritedTheme
.
capture
(
from:
context
,
to:
Navigator
.
maybeOf
(
context
)?.
context
,
);
_overlayEntry
=
OverlayEntry
(
builder:
(
BuildContext
context
)
=>
capturedThemes
.
wrap
(
builder
(
context
)),
);
overlayState
!.
insert
(
overlayEntry
!,
below:
below
);
if
(
animationController
!=
null
)
{
await
animationController
?.
forward
();
}
}
/// Schedules a hide of the magnifier.
///
/// If this [MagnifierController] has an [AnimationController],
/// then [hide] reverses the animation controller and waits
/// for the animation to complete. Then, if [removeFromOverlay]
/// is true, remove the magnifier from the overlay.
///
/// In general, [removeFromOverlay] should be true, unless
/// the magnifier needs to preserve states between shows / hides.
Future
<
void
>
hide
({
bool
removeFromOverlay
=
true
})
async
{
if
(
overlayEntry
==
null
)
{
return
;
}
if
(
animationController
!=
null
)
{
await
animationController
?.
reverse
();
}
if
(
removeFromOverlay
)
{
this
.
removeFromOverlay
();
}
}
/// Remove the [OverlayEntry] from the [Overlay].
///
/// This method removes the [OverlayEntry] synchronously,
/// regardless of exit animation: this leads to abrupt removals
/// of [OverlayEntry]s with animations.
///
/// To allow the [OverlayEntry] to play its exit animation, consider calling
/// [hide] with `removeFromOverlay` true, and optionally awaiting the future
@visibleForTesting
void
removeFromOverlay
()
{
_overlayEntry
?.
remove
();
_overlayEntry
=
null
;
}
/// A utility for calculating a new [Rect] from [rect] such that
/// [rect] is fully constrained within [bounds].
///
/// Any point in the output rect is guaranteed to also be a point contained in [bounds].
///
/// It is a runtime error for [rect].width to be greater than [bounds].width,
/// and it is also an error for [rect].height to be greater than [bounds].height.
///
/// This algorithm translates [rect] the shortest distance such that it is entirely within
/// [bounds].
///
/// If [rect] is already within [bounds], no shift will be applied to [rect] and
/// [rect] will be returned as-is.
///
/// It is perfectly valid for the output rect to have a point along the edge of the
/// [bounds]. If the desired output rect requires that no edges are parallel to edges
/// of [bounds], see [Rect.deflate] by 1 on [bounds] to achieve this effect.
static
Rect
shiftWithinBounds
({
required
Rect
rect
,
required
Rect
bounds
,
})
{
assert
(
rect
.
width
<=
bounds
.
width
,
'attempted to shift
$rect
within
$bounds
, but the rect has a greater width.'
);
assert
(
rect
.
height
<=
bounds
.
height
,
'attempted to shift
$rect
within
$bounds
, but the rect has a greater height.'
);
Offset
rectShift
=
Offset
.
zero
;
if
(
rect
.
left
<
bounds
.
left
)
{
rectShift
+=
Offset
(
bounds
.
left
-
rect
.
left
,
0
);
}
else
if
(
rect
.
right
>
bounds
.
right
)
{
rectShift
+=
Offset
(
bounds
.
right
-
rect
.
right
,
0
);
}
if
(
rect
.
top
<
bounds
.
top
)
{
rectShift
+=
Offset
(
0
,
bounds
.
top
-
rect
.
top
);
}
else
if
(
rect
.
bottom
>
bounds
.
bottom
)
{
rectShift
+=
Offset
(
0
,
bounds
.
bottom
-
rect
.
bottom
);
}
return
rect
.
shift
(
rectShift
);
}
}
/// A decoration for a [RawMagnifier].
///
/// [MagnifierDecoration] does not expose [ShapeDecoration.color], [ShapeDecoration.image],
/// or [ShapeDecoration.gradient], since they will be covered by the [RawMagnifier]'s lens.
///
/// Also takes an [opacity] (see https://github.com/flutter/engine/pull/34435).
class
MagnifierDecoration
extends
ShapeDecoration
{
/// Constructs a [MagnifierDecoration].
///
/// By default, [MagnifierDecoration] is a rectangular magnifier with no shadows, and
/// fully opaque.
const
MagnifierDecoration
({
this
.
opacity
=
1
,
super
.
shadows
,
super
.
shape
=
const
RoundedRectangleBorder
(),
});
/// The magnifier's opacity.
final
double
opacity
;
@override
bool
operator
==(
Object
other
)
{
if
(
identical
(
this
,
other
))
{
return
true
;
}
return
super
==
other
&&
other
is
MagnifierDecoration
&&
other
.
opacity
==
opacity
;
}
@override
int
get
hashCode
=>
Object
.
hash
(
super
.
hashCode
,
opacity
);
}
/// A common base class for magnifiers.
///
/// {@template flutter.widgets.magnifier.intro}
/// This magnifying glass is useful for scenarios on mobile devices where
/// the user's finger may be covering part of the screen where a granular
/// action is being performed, such as navigating a small cursor with a drag
/// gesture, on an image or text.
/// {@endtemplate}
///
/// A magnifier can be convienently managed by [MagnifierController], which handles
/// showing and hiding the magnifier, with an optional entry / exit animation.
///
/// See:
/// * [MagnifierController], a controller to handle magnifiers in an overlay.
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
/// orignally.
/// {@endtemplate}
const
RawMagnifier
({
super
.
key
,
this
.
child
,
this
.
decoration
=
const
MagnifierDecoration
(),
this
.
focalPointOffset
=
Offset
.
zero
,
this
.
magnificationScale
=
1
,
required
this
.
size
,
})
:
assert
(
magnificationScale
!=
0
,
'Magnification scale of 0 results in undefined behavior.'
);
/// An optional widget to posiiton 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.
final
Widget
?
child
;
/// This magnifier's decoration.
///
/// {@macro flutter.widgets.magnifier.RawMagnifier.invisibility_warning}
final
MagnifierDecoration
decoration
;
/// The offset of the magnifier from [RawMagnifier]'s center.
///
/// {@template flutter.widgets.magnifier.offset}
/// For example, if [RawMagnifier] is globally positioned at Offset(100, 100),
/// and [focalPointOffset] is Offset(-20, -20), then [RawMagnifier] will see
/// the content at global offset (80, 80).
///
/// If left as [Offset.zero], the [RawMagnifier] will show the content that
/// is directly below it.
/// {@endtemplate}
final
Offset
focalPointOffset
;
/// How "zoomed in" the magnification subject is in the lens.
final
double
magnificationScale
;
/// The size of the magnifier.
///
/// This does not include added border; it only includes
/// the size of the magnifier.
final
Size
size
;
@override
Widget
build
(
BuildContext
context
)
{
return
Stack
(
clipBehavior:
Clip
.
none
,
alignment:
Alignment
.
center
,
children:
<
Widget
>[
ClipPath
.
shape
(
shape:
decoration
.
shape
,
child:
Opacity
(
opacity:
decoration
.
opacity
,
child:
_Magnifier
(
shape:
decoration
.
shape
,
focalPointOffset:
focalPointOffset
,
magnificationScale:
magnificationScale
,
child:
SizedBox
.
fromSize
(
size:
size
,
child:
child
,
),
),
),
),
// Because `BackdropFilter` will filter any widgets before it, we should
// apply the style 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
,
),
)
],
);
}
}
class
_MagnifierStyle
extends
StatelessWidget
{
const
_MagnifierStyle
(
this
.
decoration
,
{
required
this
.
size
});
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
;
}
@override
bool
shouldReclip
(
_DonutClip
oldClipper
)
=>
oldClipper
.
shape
!=
shape
;
}
class
_Magnifier
extends
SingleChildRenderObjectWidget
{
/// Construct a [_Magnifier].
const
_Magnifier
({
super
.
child
,
required
this
.
shape
,
this
.
magnificationScale
=
1
,
this
.
focalPointOffset
=
Offset
.
zero
,
});
/// [focalPointOffset] is the area the center of the
/// [_Magnifier] points to, relative to the center of the magnifier.
///
/// {@macro flutter.widgets.magnifier.offset}
final
Offset
focalPointOffset
;
/// The scale of the magnification.
///
/// A [magnificationScale] of 1 means that the content in the magnifier
/// is true to it's real size. Anything greater than one will appear bigger
/// in the magnifier, and anything less than one will appear smaller in
/// the magnifier.
final
double
magnificationScale
;
/// The shape of the magnifier is dictated by [shape.getOuterPath].
final
ShapeBorder
shape
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
_RenderMagnification
(
focalPointOffset
,
magnificationScale
,
shape
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderMagnification
renderObject
)
{
renderObject
..
focalPointOffset
=
focalPointOffset
..
shape
=
shape
..
magnificationScale
=
magnificationScale
;
}
}
class
_RenderMagnification
extends
RenderProxyBox
{
_RenderMagnification
(
this
.
_focalPointOffset
,
this
.
_magnificationScale
,
this
.
_shape
,
{
RenderBox
?
child
,
})
:
super
(
child
);
Offset
get
focalPointOffset
=>
_focalPointOffset
;
Offset
_focalPointOffset
;
set
focalPointOffset
(
Offset
value
)
{
if
(
_focalPointOffset
==
value
)
{
return
;
}
_focalPointOffset
=
value
;
markNeedsPaint
();
}
double
get
magnificationScale
=>
_magnificationScale
;
double
_magnificationScale
;
set
magnificationScale
(
double
value
)
{
if
(
_magnificationScale
==
value
)
{
return
;
}
_magnificationScale
=
value
;
markNeedsPaint
();
}
ShapeBorder
get
shape
=>
_shape
;
ShapeBorder
_shape
;
set
shape
(
ShapeBorder
value
)
{
if
(
_shape
==
value
)
{
return
;
}
_shape
=
value
;
markNeedsPaint
();
}
@override
bool
get
alwaysNeedsCompositing
=>
true
;
@override
BackdropFilterLayer
?
get
layer
=>
super
.
layer
as
BackdropFilterLayer
?;
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
final
Offset
thisCenter
=
Alignment
.
center
.
alongSize
(
size
)
+
offset
;
final
Matrix4
matrix
=
Matrix4
.
identity
()
..
translate
(
magnificationScale
*
((
focalPointOffset
.
dx
*
-
1
)
-
thisCenter
.
dx
)
+
thisCenter
.
dx
,
magnificationScale
*
((
focalPointOffset
.
dy
*
-
1
)
-
thisCenter
.
dy
)
+
thisCenter
.
dy
)
..
scale
(
magnificationScale
);
final
ImageFilter
filter
=
ImageFilter
.
matrix
(
matrix
.
storage
,
filterQuality:
FilterQuality
.
high
);
if
(
layer
==
null
)
{
layer
=
BackdropFilterLayer
(
filter:
filter
,
);
}
else
{
layer
!.
filter
=
filter
;
}
context
.
pushLayer
(
layer
!,
super
.
paint
,
offset
);
}
}
packages/flutter/lib/src/widgets/selectable_region.dart
View file @
f014c1e6
...
@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
...
@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import
'package:flutter/rendering.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:vector_math/vector_math_64.dart'
;
import
'actions.dart'
;
import
'actions.dart'
;
import
'basic.dart'
;
import
'basic.dart'
;
...
@@ -179,8 +180,18 @@ class SelectableRegion extends StatefulWidget {
...
@@ -179,8 +180,18 @@ class SelectableRegion extends StatefulWidget {
required
this
.
focusNode
,
required
this
.
focusNode
,
required
this
.
selectionControls
,
required
this
.
selectionControls
,
required
this
.
child
,
required
this
.
child
,
this
.
magnifierConfiguration
=
TextMagnifierConfiguration
.
disabled
,
});
});
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled.
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
final
TextMagnifierConfiguration
magnifierConfiguration
;
/// {@macro flutter.widgets.Focus.focusNode}
/// {@macro flutter.widgets.Focus.focusNode}
final
FocusNode
focusNode
;
final
FocusNode
focusNode
;
...
@@ -403,7 +414,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -403,7 +414,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
});
});
return
;
return
;
}
}
}
}
void
_onAnyDragEnd
(
DragEndDetails
details
)
{
_selectionOverlay
!.
hideMagnifier
(
shouldShowToolbar:
true
);
_stopSelectionEndEdgeUpdate
();
}
void
_stopSelectionEndEdgeUpdate
()
{
void
_stopSelectionEndEdgeUpdate
()
{
_scheduledSelectionEndEdgeUpdate
=
false
;
_scheduledSelectionEndEdgeUpdate
=
false
;
...
@@ -451,11 +467,19 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -451,11 +467,19 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
late
Offset
_selectionStartHandleDragPosition
;
late
Offset
_selectionStartHandleDragPosition
;
late
Offset
_selectionEndHandleDragPosition
;
late
Offset
_selectionEndHandleDragPosition
;
late
List
<
TextSelectionPoint
>
points
;
void
_handleSelectionStartHandleDragStart
(
DragStartDetails
details
)
{
void
_handleSelectionStartHandleDragStart
(
DragStartDetails
details
)
{
assert
(
_selectionDelegate
.
value
.
startSelectionPoint
!=
null
);
assert
(
_selectionDelegate
.
value
.
startSelectionPoint
!=
null
);
final
Offset
localPosition
=
_selectionDelegate
.
value
.
startSelectionPoint
!.
localPosition
;
final
Offset
localPosition
=
_selectionDelegate
.
value
.
startSelectionPoint
!.
localPosition
;
final
Matrix4
globalTransform
=
_selectable
!.
getTransformTo
(
null
);
final
Matrix4
globalTransform
=
_selectable
!.
getTransformTo
(
null
);
_selectionStartHandleDragPosition
=
MatrixUtils
.
transformPoint
(
globalTransform
,
localPosition
);
_selectionStartHandleDragPosition
=
MatrixUtils
.
transformPoint
(
globalTransform
,
localPosition
);
_selectionOverlay
!.
showMagnifier
(
_buildInfoForMagnifier
(
details
.
globalPosition
,
_selectionDelegate
.
value
.
startSelectionPoint
!,
));
}
}
void
_handleSelectionStartHandleDragUpdate
(
DragUpdateDetails
details
)
{
void
_handleSelectionStartHandleDragUpdate
(
DragUpdateDetails
details
)
{
...
@@ -464,6 +488,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -464,6 +488,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
// Offset it to the center of the line to make it feel more natural.
// Offset it to the center of the line to make it feel more natural.
_selectionStartPosition
=
_selectionStartHandleDragPosition
-
Offset
(
0
,
_selectionDelegate
.
value
.
startSelectionPoint
!.
lineHeight
/
2
);
_selectionStartPosition
=
_selectionStartHandleDragPosition
-
Offset
(
0
,
_selectionDelegate
.
value
.
startSelectionPoint
!.
lineHeight
/
2
);
_triggerSelectionStartEdgeUpdate
();
_triggerSelectionStartEdgeUpdate
();
_selectionOverlay
!.
updateMagnifier
(
_buildInfoForMagnifier
(
details
.
globalPosition
,
_selectionDelegate
.
value
.
startSelectionPoint
!,
));
}
}
void
_handleSelectionEndHandleDragStart
(
DragStartDetails
details
)
{
void
_handleSelectionEndHandleDragStart
(
DragStartDetails
details
)
{
...
@@ -471,6 +500,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -471,6 +500,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
final
Offset
localPosition
=
_selectionDelegate
.
value
.
endSelectionPoint
!.
localPosition
;
final
Offset
localPosition
=
_selectionDelegate
.
value
.
endSelectionPoint
!.
localPosition
;
final
Matrix4
globalTransform
=
_selectable
!.
getTransformTo
(
null
);
final
Matrix4
globalTransform
=
_selectable
!.
getTransformTo
(
null
);
_selectionEndHandleDragPosition
=
MatrixUtils
.
transformPoint
(
globalTransform
,
localPosition
);
_selectionEndHandleDragPosition
=
MatrixUtils
.
transformPoint
(
globalTransform
,
localPosition
);
_selectionOverlay
!.
showMagnifier
(
_buildInfoForMagnifier
(
details
.
globalPosition
,
_selectionDelegate
.
value
.
endSelectionPoint
!,
));
}
}
void
_handleSelectionEndHandleDragUpdate
(
DragUpdateDetails
details
)
{
void
_handleSelectionEndHandleDragUpdate
(
DragUpdateDetails
details
)
{
...
@@ -479,6 +513,30 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -479,6 +513,30 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
// Offset it to the center of the line to make it feel more natural.
// Offset it to the center of the line to make it feel more natural.
_selectionEndPosition
=
_selectionEndHandleDragPosition
-
Offset
(
0
,
_selectionDelegate
.
value
.
endSelectionPoint
!.
lineHeight
/
2
);
_selectionEndPosition
=
_selectionEndHandleDragPosition
-
Offset
(
0
,
_selectionDelegate
.
value
.
endSelectionPoint
!.
lineHeight
/
2
);
_triggerSelectionEndEdgeUpdate
();
_triggerSelectionEndEdgeUpdate
();
_selectionOverlay
!.
updateMagnifier
(
_buildInfoForMagnifier
(
details
.
globalPosition
,
_selectionDelegate
.
value
.
endSelectionPoint
!,
));
}
MagnifierOverlayInfoBearer
_buildInfoForMagnifier
(
Offset
globalGesturePosition
,
SelectionPoint
selectionPoint
)
{
final
Vector3
globalTransform
=
_selectable
!.
getTransformTo
(
null
).
getTranslation
();
final
Offset
globalTransformAsOffset
=
Offset
(
globalTransform
.
x
,
globalTransform
.
y
);
final
Offset
globalSelectionPointPosition
=
selectionPoint
.
localPosition
+
globalTransformAsOffset
;
final
Rect
caretRect
=
Rect
.
fromLTWH
(
globalSelectionPointPosition
.
dx
,
globalSelectionPointPosition
.
dy
-
selectionPoint
.
lineHeight
,
0
,
selectionPoint
.
lineHeight
);
return
MagnifierOverlayInfoBearer
(
globalGesturePosition:
globalGesturePosition
,
caretRect:
caretRect
,
fieldBounds:
globalTransformAsOffset
&
_selectable
!.
size
,
currentLineBoundries:
globalTransformAsOffset
&
_selectable
!.
size
,
);
}
}
void
_createSelectionOverlay
()
{
void
_createSelectionOverlay
()
{
...
@@ -488,7 +546,6 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -488,7 +546,6 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
}
}
final
SelectionPoint
?
start
=
_selectionDelegate
.
value
.
startSelectionPoint
;
final
SelectionPoint
?
start
=
_selectionDelegate
.
value
.
startSelectionPoint
;
final
SelectionPoint
?
end
=
_selectionDelegate
.
value
.
endSelectionPoint
;
final
SelectionPoint
?
end
=
_selectionDelegate
.
value
.
endSelectionPoint
;
late
List
<
TextSelectionPoint
>
points
;
final
Offset
startLocalPosition
=
start
?.
localPosition
??
end
!.
localPosition
;
final
Offset
startLocalPosition
=
start
?.
localPosition
??
end
!.
localPosition
;
final
Offset
endLocalPosition
=
end
?.
localPosition
??
start
!.
localPosition
;
final
Offset
endLocalPosition
=
end
?.
localPosition
??
start
!.
localPosition
;
if
(
startLocalPosition
.
dy
>
endLocalPosition
.
dy
)
{
if
(
startLocalPosition
.
dy
>
endLocalPosition
.
dy
)
{
...
@@ -509,12 +566,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -509,12 +566,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
lineHeightAtStart:
start
?.
lineHeight
??
end
!.
lineHeight
,
lineHeightAtStart:
start
?.
lineHeight
??
end
!.
lineHeight
,
onStartHandleDragStart:
_handleSelectionStartHandleDragStart
,
onStartHandleDragStart:
_handleSelectionStartHandleDragStart
,
onStartHandleDragUpdate:
_handleSelectionStartHandleDragUpdate
,
onStartHandleDragUpdate:
_handleSelectionStartHandleDragUpdate
,
onStartHandleDragEnd:
(
DragEndDetails
details
)
=>
_stopSelectionStartEdgeUpdate
()
,
onStartHandleDragEnd:
_onAnyDragEnd
,
endHandleType:
end
?.
handleType
??
TextSelectionHandleType
.
right
,
endHandleType:
end
?.
handleType
??
TextSelectionHandleType
.
right
,
lineHeightAtEnd:
end
?.
lineHeight
??
start
!.
lineHeight
,
lineHeightAtEnd:
end
?.
lineHeight
??
start
!.
lineHeight
,
onEndHandleDragStart:
_handleSelectionEndHandleDragStart
,
onEndHandleDragStart:
_handleSelectionEndHandleDragStart
,
onEndHandleDragUpdate:
_handleSelectionEndHandleDragUpdate
,
onEndHandleDragUpdate:
_handleSelectionEndHandleDragUpdate
,
onEndHandleDragEnd:
(
DragEndDetails
details
)
=>
_stopSelectionEndEdgeUpdate
()
,
onEndHandleDragEnd:
_onAnyDragEnd
,
selectionEndpoints:
points
,
selectionEndpoints:
points
,
selectionControls:
widget
.
selectionControls
,
selectionControls:
widget
.
selectionControls
,
selectionDelegate:
this
,
selectionDelegate:
this
,
...
@@ -522,6 +579,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -522,6 +579,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
startHandleLayerLink:
_startHandleLayerLink
,
startHandleLayerLink:
_startHandleLayerLink
,
endHandleLayerLink:
_endHandleLayerLink
,
endHandleLayerLink:
_endHandleLayerLink
,
toolbarLayerLink:
_toolbarLayerLink
,
toolbarLayerLink:
_toolbarLayerLink
,
magnifierConfiguration:
widget
.
magnifierConfiguration
);
);
}
}
...
@@ -798,6 +856,9 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
...
@@ -798,6 +856,9 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
_selectable
?.
removeListener
(
_updateSelectionStatus
);
_selectable
?.
removeListener
(
_updateSelectionStatus
);
_selectable
?.
pushHandleLayers
(
null
,
null
);
_selectable
?.
pushHandleLayers
(
null
,
null
);
_selectionDelegate
.
dispose
();
_selectionDelegate
.
dispose
();
// In case dispose was triggered before gesture end, remove the magnifier
// so it doesn't remain stuck in the overlay forever.
_selectionOverlay
?.
hideMagnifier
(
shouldShowToolbar:
false
);
_selectionOverlay
?.
dispose
();
_selectionOverlay
?.
dispose
();
_selectionOverlay
=
null
;
_selectionOverlay
=
null
;
super
.
dispose
();
super
.
dispose
();
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
f014c1e6
...
@@ -20,6 +20,7 @@ import 'debug.dart';
...
@@ -20,6 +20,7 @@ import 'debug.dart';
import
'editable_text.dart'
;
import
'editable_text.dart'
;
import
'framework.dart'
;
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'gesture_detector.dart'
;
import
'magnifier.dart'
;
import
'overlay.dart'
;
import
'overlay.dart'
;
import
'tap_region.dart'
;
import
'tap_region.dart'
;
import
'ticker_provider.dart'
;
import
'ticker_provider.dart'
;
...
@@ -71,6 +72,159 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
...
@@ -71,6 +72,159 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
String
toString
()
=>
'
${super.toString()}
; shouldPaint=
$shouldPaint
'
;
String
toString
()
=>
'
${super.toString()}
; shouldPaint=
$shouldPaint
'
;
}
}
/// {@template flutter.widgets.textSelection.MagnifierBuilder}
/// Signature for a builder that builds a Widget with a [MagnifierController].
///
/// Consuming [MagnifierController] or [ValueNotifier]<[MagnifierOverlayInfoBearer]> is not
/// required, although if a Widget intends to have entry or exit animations, it should take
/// [MagnifierController] and provide it an [AnimationController], so that [MagnifierController]
/// can wait before removing it from the overlay.
/// {@endtemplate}
///
/// See also:
///
/// - [MagnifierOverlayInfoBearer], the dataclass that updates the
/// magnifier.
typedef
MagnifierBuilder
=
Widget
?
Function
(
BuildContext
context
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
textSelectionData
);
/// A data class that allows the [SelectionOverlay] to delegate
/// the magnifier's positioning to the magnifier itself, based on the
/// info in [MagnifierOverlayInfoBearer].
@immutable
class
MagnifierOverlayInfoBearer
{
/// Construct a [MagnifierOverlayInfoBearer] from raw values.
const
MagnifierOverlayInfoBearer
({
required
this
.
globalGesturePosition
,
required
this
.
caretRect
,
required
this
.
fieldBounds
,
required
this
.
currentLineBoundries
,
});
factory
MagnifierOverlayInfoBearer
.
_fromRenderEditable
({
required
RenderEditable
renderEditable
,
required
Offset
globalGesturePosition
,
required
TextPosition
currentTextPosition
,
})
{
final
Offset
globalRenderEditableTopLeft
=
renderEditable
.
localToGlobal
(
Offset
.
zero
);
final
Rect
localCaretRect
=
renderEditable
.
getLocalRectForCaret
(
currentTextPosition
);
final
TextSelection
lineAtOffset
=
renderEditable
.
getLineAtOffset
(
currentTextPosition
);
final
TextPosition
positionAtEndOfLine
=
TextPosition
(
offset:
lineAtOffset
.
extentOffset
,
affinity:
TextAffinity
.
upstream
,
);
// Default affinity is downstream.
final
TextPosition
positionAtBeginningOfLine
=
TextPosition
(
offset:
lineAtOffset
.
baseOffset
,
);
final
Rect
lineBoundries
=
Rect
.
fromPoints
(
renderEditable
.
getLocalRectForCaret
(
positionAtBeginningOfLine
).
topCenter
,
renderEditable
.
getLocalRectForCaret
(
positionAtEndOfLine
).
bottomCenter
);
return
MagnifierOverlayInfoBearer
(
fieldBounds:
globalRenderEditableTopLeft
&
renderEditable
.
size
,
globalGesturePosition:
globalGesturePosition
,
caretRect:
localCaretRect
.
shift
(
globalRenderEditableTopLeft
),
currentLineBoundries:
lineBoundries
.
shift
(
globalRenderEditableTopLeft
)
);
}
/// Construct an empty [MagnifierOverlayInfoBearer], with all
/// values set to 0.
const
MagnifierOverlayInfoBearer
.
empty
()
:
globalGesturePosition
=
Offset
.
zero
,
caretRect
=
Rect
.
zero
,
currentLineBoundries
=
Rect
.
zero
,
fieldBounds
=
Rect
.
zero
;
/// The offset of the gesture position that the magnifier should be shown at.
final
Offset
globalGesturePosition
;
/// The rect of the current line the magnifier should be shown at. Do not take
/// into account any padding of the field; only the position of the first
/// and last character.
final
Rect
currentLineBoundries
;
/// The rect of the handle that the magnifier should follow.
final
Rect
caretRect
;
/// The bounds of the entire text field that the magnifier is bound to.
final
Rect
fieldBounds
;
@override
bool
operator
==(
Object
other
)
{
if
(
identical
(
this
,
other
))
{
return
true
;
}
if
(
other
is
!
MagnifierOverlayInfoBearer
)
{
return
false
;
}
return
other
.
globalGesturePosition
==
globalGesturePosition
&&
other
.
caretRect
==
caretRect
&&
other
.
currentLineBoundries
==
currentLineBoundries
&&
other
.
fieldBounds
==
fieldBounds
;
}
@override
int
get
hashCode
=>
Object
.
hash
(
globalGesturePosition
,
caretRect
,
fieldBounds
,
currentLineBoundries
);
}
/// {@template flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
/// A configuration object for a magnifier.
/// {@endtemplate}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@template flutter.widgets.text_selection.TextMagnifierConfiguration.details}
/// In general, most features of the magnifier can be configured through
/// [MagnifierBuilder]. [TextMagnifierConfiguration] is used to configure
/// the magnifier's behavior through the [SelectionOverlay].
/// {@endtemplate}
class
TextMagnifierConfiguration
{
/// Construct a [TextMagnifierConfiguration] from parts.
///
/// If [magnifierBuilder] is null, a default [MagnifierBuilder] will be used
/// that never builds a magnifier.
const
TextMagnifierConfiguration
({
MagnifierBuilder
?
magnifierBuilder
,
this
.
shouldDisplayHandlesInMagnifier
=
true
})
:
_magnifierBuilder
=
magnifierBuilder
;
/// The passed in [MagnifierBuilder].
///
/// This is nullable because [disabled] needs to be static const,
/// so that it can be used as a default parameter. If left null,
/// the [magnifierBuilder] getter will be a function that always returns
/// null.
final
MagnifierBuilder
?
_magnifierBuilder
;
/// {@macro flutter.widgets.textSelection.MagnifierBuilder}
MagnifierBuilder
get
magnifierBuilder
=>
_magnifierBuilder
??
(
_
,
__
,
___
)
=>
null
;
/// Determines whether a magnifier should show the text editing handles or not.
final
bool
shouldDisplayHandlesInMagnifier
;
/// A constant for a [TextMagnifierConfiguration] that is disabled.
///
/// In particular, this [TextMagnifierConfiguration] is considered disabled
/// because it never builds anything, regardless of platform.
static
const
TextMagnifierConfiguration
disabled
=
TextMagnifierConfiguration
();
}
/// An interface for building the selection UI, to be provided by the
/// An interface for building the selection UI, to be provided by the
/// implementer of the toolbar widget.
/// implementer of the toolbar widget.
///
///
...
@@ -224,7 +378,7 @@ class TextSelectionOverlay {
...
@@ -224,7 +378,7 @@ class TextSelectionOverlay {
/// The [context] must not be null and must have an [Overlay] as an ancestor.
/// The [context] must not be null and must have an [Overlay] as an ancestor.
TextSelectionOverlay
({
TextSelectionOverlay
({
required
TextEditingValue
value
,
required
TextEditingValue
value
,
required
BuildContext
context
,
required
this
.
context
,
Widget
?
debugRequiredFor
,
Widget
?
debugRequiredFor
,
required
LayerLink
toolbarLayerLink
,
required
LayerLink
toolbarLayerLink
,
required
LayerLink
startHandleLayerLink
,
required
LayerLink
startHandleLayerLink
,
...
@@ -236,6 +390,7 @@ class TextSelectionOverlay {
...
@@ -236,6 +390,7 @@ class TextSelectionOverlay {
DragStartBehavior
dragStartBehavior
=
DragStartBehavior
.
start
,
DragStartBehavior
dragStartBehavior
=
DragStartBehavior
.
start
,
VoidCallback
?
onSelectionHandleTapped
,
VoidCallback
?
onSelectionHandleTapped
,
ClipboardStatusNotifier
?
clipboardStatus
,
ClipboardStatusNotifier
?
clipboardStatus
,
required
TextMagnifierConfiguration
magnifierConfiguration
,
})
:
assert
(
value
!=
null
),
})
:
assert
(
value
!=
null
),
assert
(
context
!=
null
),
assert
(
context
!=
null
),
assert
(
handlesVisible
!=
null
),
assert
(
handlesVisible
!=
null
),
...
@@ -245,6 +400,7 @@ class TextSelectionOverlay {
...
@@ -245,6 +400,7 @@ class TextSelectionOverlay {
renderObject
.
selectionEndInViewport
.
addListener
(
_updateTextSelectionOverlayVisibilities
);
renderObject
.
selectionEndInViewport
.
addListener
(
_updateTextSelectionOverlayVisibilities
);
_updateTextSelectionOverlayVisibilities
();
_updateTextSelectionOverlayVisibilities
();
_selectionOverlay
=
SelectionOverlay
(
_selectionOverlay
=
SelectionOverlay
(
magnifierConfiguration:
magnifierConfiguration
,
context:
context
,
context:
context
,
debugRequiredFor:
debugRequiredFor
,
debugRequiredFor:
debugRequiredFor
,
// The metrics will be set when show handles.
// The metrics will be set when show handles.
...
@@ -253,11 +409,13 @@ class TextSelectionOverlay {
...
@@ -253,11 +409,13 @@ class TextSelectionOverlay {
lineHeightAtStart:
0.0
,
lineHeightAtStart:
0.0
,
onStartHandleDragStart:
_handleSelectionStartHandleDragStart
,
onStartHandleDragStart:
_handleSelectionStartHandleDragStart
,
onStartHandleDragUpdate:
_handleSelectionStartHandleDragUpdate
,
onStartHandleDragUpdate:
_handleSelectionStartHandleDragUpdate
,
onEndHandleDragEnd:
_handleAnyDragEnd
,
endHandleType:
TextSelectionHandleType
.
collapsed
,
endHandleType:
TextSelectionHandleType
.
collapsed
,
endHandlesVisible:
_effectiveEndHandleVisibility
,
endHandlesVisible:
_effectiveEndHandleVisibility
,
lineHeightAtEnd:
0.0
,
lineHeightAtEnd:
0.0
,
onEndHandleDragStart:
_handleSelectionEndHandleDragStart
,
onEndHandleDragStart:
_handleSelectionEndHandleDragStart
,
onEndHandleDragUpdate:
_handleSelectionEndHandleDragUpdate
,
onEndHandleDragUpdate:
_handleSelectionEndHandleDragUpdate
,
onStartHandleDragEnd:
_handleAnyDragEnd
,
toolbarVisible:
_effectiveToolbarVisibility
,
toolbarVisible:
_effectiveToolbarVisibility
,
selectionEndpoints:
const
<
TextSelectionPoint
>[],
selectionEndpoints:
const
<
TextSelectionPoint
>[],
selectionControls:
selectionControls
,
selectionControls:
selectionControls
,
...
@@ -303,6 +461,13 @@ class TextSelectionOverlay {
...
@@ -303,6 +461,13 @@ class TextSelectionOverlay {
final
ValueNotifier
<
bool
>
_effectiveStartHandleVisibility
=
ValueNotifier
<
bool
>(
false
);
final
ValueNotifier
<
bool
>
_effectiveStartHandleVisibility
=
ValueNotifier
<
bool
>(
false
);
final
ValueNotifier
<
bool
>
_effectiveEndHandleVisibility
=
ValueNotifier
<
bool
>(
false
);
final
ValueNotifier
<
bool
>
_effectiveEndHandleVisibility
=
ValueNotifier
<
bool
>(
false
);
final
ValueNotifier
<
bool
>
_effectiveToolbarVisibility
=
ValueNotifier
<
bool
>(
false
);
final
ValueNotifier
<
bool
>
_effectiveToolbarVisibility
=
ValueNotifier
<
bool
>(
false
);
/// The context in which the selection handles should appear.
///
/// This context must have an [Overlay] as an ancestor because this object
/// will display the text selection handles in that [Overlay].
final
BuildContext
context
;
void
_updateTextSelectionOverlayVisibilities
()
{
void
_updateTextSelectionOverlayVisibilities
()
{
_effectiveStartHandleVisibility
.
value
=
_handlesVisible
&&
renderObject
.
selectionStartInViewport
.
value
;
_effectiveStartHandleVisibility
.
value
=
_handlesVisible
&&
renderObject
.
selectionStartInViewport
.
value
;
_effectiveEndHandleVisibility
.
value
=
_handlesVisible
&&
renderObject
.
selectionEndInViewport
.
value
;
_effectiveEndHandleVisibility
.
value
=
_handlesVisible
&&
renderObject
.
selectionEndInViewport
.
value
;
...
@@ -451,7 +616,15 @@ class TextSelectionOverlay {
...
@@ -451,7 +616,15 @@ class TextSelectionOverlay {
final
Size
handleSize
=
selectionControls
!.
getHandleSize
(
final
Size
handleSize
=
selectionControls
!.
getHandleSize
(
renderObject
.
preferredLineHeight
,
renderObject
.
preferredLineHeight
,
);
);
_dragEndPosition
=
details
.
globalPosition
+
Offset
(
0.0
,
-
handleSize
.
height
);
_dragEndPosition
=
details
.
globalPosition
+
Offset
(
0.0
,
-
handleSize
.
height
);
final
TextPosition
position
=
renderObject
.
getPositionForPoint
(
_dragEndPosition
);
_selectionOverlay
.
showMagnifier
(
MagnifierOverlayInfoBearer
.
_fromRenderEditable
(
currentTextPosition:
position
,
globalGesturePosition:
details
.
globalPosition
,
renderEditable:
renderObject
,
));
}
}
void
_handleSelectionEndHandleDragUpdate
(
DragUpdateDetails
details
)
{
void
_handleSelectionEndHandleDragUpdate
(
DragUpdateDetails
details
)
{
...
@@ -459,10 +632,18 @@ class TextSelectionOverlay {
...
@@ -459,10 +632,18 @@ class TextSelectionOverlay {
return
;
return
;
}
}
_dragEndPosition
+=
details
.
delta
;
_dragEndPosition
+=
details
.
delta
;
final
TextPosition
position
=
renderObject
.
getPositionForPoint
(
_dragEndPosition
);
final
TextPosition
position
=
renderObject
.
getPositionForPoint
(
_dragEndPosition
);
final
TextSelection
currentSelection
=
TextSelection
.
fromPosition
(
position
);
if
(
_selection
.
isCollapsed
)
{
if
(
_selection
.
isCollapsed
)
{
_handleSelectionHandleChanged
(
TextSelection
.
fromPosition
(
position
),
isEnd:
true
);
_selectionOverlay
.
updateMagnifier
(
MagnifierOverlayInfoBearer
.
_fromRenderEditable
(
currentTextPosition:
position
,
globalGesturePosition:
details
.
globalPosition
,
renderEditable:
renderObject
,
));
_handleSelectionHandleChanged
(
currentSelection
,
isEnd:
true
);
return
;
return
;
}
}
...
@@ -494,6 +675,12 @@ class TextSelectionOverlay {
...
@@ -494,6 +675,12 @@ class TextSelectionOverlay {
}
}
_handleSelectionHandleChanged
(
newSelection
,
isEnd:
true
);
_handleSelectionHandleChanged
(
newSelection
,
isEnd:
true
);
_selectionOverlay
.
updateMagnifier
(
MagnifierOverlayInfoBearer
.
_fromRenderEditable
(
currentTextPosition:
newSelection
.
extent
,
globalGesturePosition:
details
.
globalPosition
,
renderEditable:
renderObject
,
));
}
}
late
Offset
_dragStartPosition
;
late
Offset
_dragStartPosition
;
...
@@ -506,6 +693,13 @@ class TextSelectionOverlay {
...
@@ -506,6 +693,13 @@ class TextSelectionOverlay {
renderObject
.
preferredLineHeight
,
renderObject
.
preferredLineHeight
,
);
);
_dragStartPosition
=
details
.
globalPosition
+
Offset
(
0.0
,
-
handleSize
.
height
);
_dragStartPosition
=
details
.
globalPosition
+
Offset
(
0.0
,
-
handleSize
.
height
);
final
TextPosition
position
=
renderObject
.
getPositionForPoint
(
_dragStartPosition
);
_selectionOverlay
.
showMagnifier
(
MagnifierOverlayInfoBearer
.
_fromRenderEditable
(
currentTextPosition:
position
,
globalGesturePosition:
details
.
globalPosition
,
renderEditable:
renderObject
,
));
}
}
void
_handleSelectionStartHandleDragUpdate
(
DragUpdateDetails
details
)
{
void
_handleSelectionStartHandleDragUpdate
(
DragUpdateDetails
details
)
{
...
@@ -516,6 +710,12 @@ class TextSelectionOverlay {
...
@@ -516,6 +710,12 @@ class TextSelectionOverlay {
final
TextPosition
position
=
renderObject
.
getPositionForPoint
(
_dragStartPosition
);
final
TextPosition
position
=
renderObject
.
getPositionForPoint
(
_dragStartPosition
);
if
(
_selection
.
isCollapsed
)
{
if
(
_selection
.
isCollapsed
)
{
_selectionOverlay
.
updateMagnifier
(
MagnifierOverlayInfoBearer
.
_fromRenderEditable
(
currentTextPosition:
position
,
globalGesturePosition:
details
.
globalPosition
,
renderEditable:
renderObject
,
));
_handleSelectionHandleChanged
(
TextSelection
.
fromPosition
(
position
),
isEnd:
false
);
_handleSelectionHandleChanged
(
TextSelection
.
fromPosition
(
position
),
isEnd:
false
);
return
;
return
;
}
}
...
@@ -547,9 +747,17 @@ class TextSelectionOverlay {
...
@@ -547,9 +747,17 @@ class TextSelectionOverlay {
break
;
break
;
}
}
_selectionOverlay
.
updateMagnifier
(
MagnifierOverlayInfoBearer
.
_fromRenderEditable
(
currentTextPosition:
newSelection
.
extent
.
offset
<
newSelection
.
base
.
offset
?
newSelection
.
extent
:
newSelection
.
base
,
globalGesturePosition:
details
.
globalPosition
,
renderEditable:
renderObject
,
));
_handleSelectionHandleChanged
(
newSelection
,
isEnd:
false
);
_handleSelectionHandleChanged
(
newSelection
,
isEnd:
false
);
}
}
void
_handleAnyDragEnd
(
DragEndDetails
details
)
=>
_selectionOverlay
.
hideMagnifier
(
shouldShowToolbar:
!
_selection
.
isCollapsed
);
void
_handleSelectionHandleChanged
(
TextSelection
newSelection
,
{
required
bool
isEnd
})
{
void
_handleSelectionHandleChanged
(
TextSelection
newSelection
,
{
required
bool
isEnd
})
{
final
TextPosition
textPosition
=
isEnd
?
newSelection
.
extent
:
newSelection
.
base
;
final
TextPosition
textPosition
=
isEnd
?
newSelection
.
extent
:
newSelection
.
base
;
selectionDelegate
.
userUpdateTextEditingValue
(
selectionDelegate
.
userUpdateTextEditingValue
(
...
@@ -612,6 +820,7 @@ class SelectionOverlay {
...
@@ -612,6 +820,7 @@ class SelectionOverlay {
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
this
.
onSelectionHandleTapped
,
this
.
onSelectionHandleTapped
,
Offset
?
toolbarLocation
,
Offset
?
toolbarLocation
,
this
.
magnifierConfiguration
=
TextMagnifierConfiguration
.
disabled
,
})
:
_startHandleType
=
startHandleType
,
})
:
_startHandleType
=
startHandleType
,
_lineHeightAtStart
=
lineHeightAtStart
,
_lineHeightAtStart
=
lineHeightAtStart
,
_endHandleType
=
endHandleType
,
_endHandleType
=
endHandleType
,
...
@@ -626,6 +835,81 @@ class SelectionOverlay {
...
@@ -626,6 +835,81 @@ class SelectionOverlay {
/// will display the text selection handles in that [Overlay].
/// will display the text selection handles in that [Overlay].
final
BuildContext
context
;
final
BuildContext
context
;
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
_magnifierOverlayInfoBearer
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
());
/// [MagnifierController.show] and [MagnifierController.hide] should not be called directly, except
/// from inside [showMagnifier] and [hideMagnifier]. If it is desired to show or hide the magnifier,
/// call [showMagnifier] or [hideMagnifier]. This is because the magnifier needs to orchestrate
/// with other properties in [SelectionOverlay].
final
MagnifierController
_magnifierController
=
MagnifierController
();
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// By default, [SelectionOverlay]'s [TextMagnifierConfiguration] is disabled.
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
final
TextMagnifierConfiguration
magnifierConfiguration
;
/// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier]
/// was called. This is safe to call on platforms not mobile, since
/// a magnifierBuilder will not be provided, or the magnifierBuilder will return null
/// on platforms not mobile.
///
/// This is NOT the souce of truth for if the magnifier is up or not,
/// since magnifiers may hide themselves. If this info is needed, check
/// [MagnifierController.shown].
void
showMagnifier
(
MagnifierOverlayInfoBearer
initalInfoBearer
)
{
if
(
_toolbar
!=
null
)
{
hideToolbar
();
}
// Start from empty, so we don't utilize any rememnant values.
_magnifierOverlayInfoBearer
.
value
=
initalInfoBearer
;
// Pre-build the magnifiers so we can tell if we've built something
// or not. If we don't build a magnifiers, then we should not
// insert anything in the overlay.
final
Widget
?
builtMagnifier
=
magnifierConfiguration
.
magnifierBuilder
(
context
,
_magnifierController
,
_magnifierOverlayInfoBearer
,
);
if
(
builtMagnifier
==
null
)
{
return
;
}
_magnifierController
.
show
(
context:
context
,
below:
magnifierConfiguration
.
shouldDisplayHandlesInMagnifier
?
null
:
_handles
!.
first
,
builder:
(
_
)
=>
builtMagnifier
);
}
/// Hide the current magnifier, optionally immediately showing
/// the toolbar.
///
/// This does nothing if there is no magnifier.
void
hideMagnifier
({
required
bool
shouldShowToolbar
})
{
// This cannot be a check on `MagnifierController.shown`, since
// it's possible that the magnifier is still in the overlay, but
// not shown in cases where the magnifier hides itself.
if
(
_magnifierController
.
overlayEntry
==
null
)
{
return
;
}
_magnifierController
.
hide
();
if
(
shouldShowToolbar
)
{
showToolbar
();
}
}
/// The type of start selection handle.
/// The type of start selection handle.
///
///
/// Changing the value while the handles are visible causes them to rebuild.
/// Changing the value while the handles are visible causes them to rebuild.
...
@@ -903,6 +1187,7 @@ class SelectionOverlay {
...
@@ -903,6 +1187,7 @@ class SelectionOverlay {
/// Hides the entire overlay including the toolbar and the handles.
/// Hides the entire overlay including the toolbar and the handles.
/// {@endtemplate}
/// {@endtemplate}
void
hide
()
{
void
hide
()
{
_magnifierController
.
hide
();
if
(
_handles
!=
null
)
{
if
(
_handles
!=
null
)
{
_handles
![
0
].
remove
();
_handles
![
0
].
remove
();
_handles
![
1
].
remove
();
_handles
![
1
].
remove
();
...
@@ -1031,6 +1316,22 @@ class SelectionOverlay {
...
@@ -1031,6 +1316,22 @@ class SelectionOverlay {
),
),
);
);
}
}
/// Update the current magnifier with new selection data, so the magnifier
/// can respond accordingly.
///
/// If the magnifier is not shown, this still updates the magnifier position
/// because the magnifier may have hidden itself and is looking for a cue to reshow
/// itself.
///
/// If there is no magnifier in the overlay, this does nothing,
void
updateMagnifier
(
MagnifierOverlayInfoBearer
magnifierOverlayInfoBearer
)
{
if
(
_magnifierController
.
overlayEntry
==
null
)
{
return
;
}
_magnifierOverlayInfoBearer
.
value
=
magnifierOverlayInfoBearer
;
}
}
}
/// This widget represents a selection toolbar.
/// This widget represents a selection toolbar.
...
...
packages/flutter/lib/widgets.dart
View file @
f014c1e6
...
@@ -70,6 +70,7 @@ export 'src/widgets/keyboard_listener.dart';
...
@@ -70,6 +70,7 @@ export 'src/widgets/keyboard_listener.dart';
export
'src/widgets/layout_builder.dart'
;
export
'src/widgets/layout_builder.dart'
;
export
'src/widgets/list_wheel_scroll_view.dart'
;
export
'src/widgets/list_wheel_scroll_view.dart'
;
export
'src/widgets/localizations.dart'
;
export
'src/widgets/localizations.dart'
;
export
'src/widgets/magnifier.dart'
;
export
'src/widgets/media_query.dart'
;
export
'src/widgets/media_query.dart'
;
export
'src/widgets/modal_barrier.dart'
;
export
'src/widgets/modal_barrier.dart'
;
export
'src/widgets/navigation_toolbar.dart'
;
export
'src/widgets/navigation_toolbar.dart'
;
...
...
packages/flutter/test/cupertino/magnifier_test.dart
0 → 100644
View file @
f014c1e6
// 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.
@Tags
(<
String
>[
'reduced-test-set'
])
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
final
Offset
basicOffset
=
Offset
(
CupertinoMagnifier
.
kDefaultSize
.
width
/
2
,
CupertinoMagnifier
.
kDefaultSize
.
height
-
CupertinoMagnifier
.
kMagnifierAboveFocalPoint
);
const
Rect
reasonableTextField
=
Rect
.
fromLTRB
(
0
,
100
,
200
,
200
);
final
MagnifierController
magnifierController
=
MagnifierController
();
// Make sure that your gesture in infoBearer is within the line in infoBearer,
// or else the magnifier status will stay hidden and this will not complete.
Future
<
void
>
showCupertinoMagnifier
(
BuildContext
context
,
WidgetTester
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
infoBearer
,
)
async
{
final
Future
<
void
>
magnifierShown
=
magnifierController
.
show
(
context:
context
,
builder:
(
_
)
=>
CupertinoTextMagnifier
(
controller:
magnifierController
,
magnifierOverlayInfoBearer:
infoBearer
,
));
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pumpAndSettle
();
await
magnifierShown
;
}
tearDown
(()
async
{
magnifierController
.
removeFromOverlay
();
});
group
(
'CupertinoTextEditingMagnifier'
,
()
{
group
(
'position'
,
()
{
Offset
getMagnifierPosition
(
WidgetTester
tester
)
{
final
AnimatedPositioned
animatedPositioned
=
tester
.
firstWidget
(
find
.
byType
(
AnimatedPositioned
));
return
Offset
(
animatedPositioned
.
left
??
0
,
animatedPositioned
.
top
??
0
);
}
testWidgets
(
'should be at gesture position if does not violate any positioning rules'
,
(
WidgetTester
tester
)
async
{
final
Key
fakeTextFieldKey
=
UniqueKey
();
final
Key
outerKey
=
UniqueKey
();
await
tester
.
pumpWidget
(
Container
(
key:
outerKey
,
color:
const
Color
.
fromARGB
(
255
,
0
,
255
,
179
),
child:
MaterialApp
(
home:
Center
(
child:
Container
(
key:
fakeTextFieldKey
,
width:
10
,
height:
10
,
color:
Colors
.
red
,
child:
const
Placeholder
(),
),
),
),
),
);
final
BuildContext
context
=
tester
.
element
(
find
.
byType
(
Placeholder
));
// Magnifier should be positioned directly over the red square.
final
RenderBox
tapPointRenderBox
=
tester
.
firstRenderObject
(
find
.
byKey
(
fakeTextFieldKey
))
as
RenderBox
;
final
Rect
fakeTextFieldRect
=
tapPointRenderBox
.
localToGlobal
(
Offset
.
zero
)
&
tapPointRenderBox
.
size
;
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifier
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
fakeTextFieldRect
,
fieldBounds:
fakeTextFieldRect
,
caretRect:
fakeTextFieldRect
,
// The tap position is dragBelow units below the text field.
globalGesturePosition:
fakeTextFieldRect
.
center
,
),
);
await
showCupertinoMagnifier
(
context
,
tester
,
magnifier
);
// Should show two red squares; original, and one in the magnifier,
// directly ontop of one another.
await
expectLater
(
find
.
byKey
(
outerKey
),
matchesGoldenFile
(
'cupertino_magnifier.position.default.png'
),
);
});
testWidgets
(
'should never horizontally be outside of Screen Padding'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
color:
Color
.
fromARGB
(
7
,
0
,
129
,
90
),
home:
Placeholder
(),
),
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showCupertinoMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
// The tap position is far out of the right side of the app.
globalGesturePosition:
Offset
(
MediaQuery
.
of
(
context
).
size
.
width
+
100
,
0
),
),
),
);
// Should be less than the right edge, since we have padding.
expect
(
getMagnifierPosition
(
tester
).
dx
,
lessThan
(
MediaQuery
.
of
(
context
).
size
.
width
));
});
testWidgets
(
'should have some vertical drag'
,
(
WidgetTester
tester
)
async
{
final
double
dragPositionBelowTextField
=
reasonableTextField
.
center
.
dy
+
30
;
await
tester
.
pumpWidget
(
const
MaterialApp
(
color:
Color
.
fromARGB
(
7
,
0
,
129
,
90
),
home:
Placeholder
(),
),
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showCupertinoMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
// The tap position is dragBelow units below the text field.
globalGesturePosition:
Offset
(
MediaQuery
.
of
(
context
).
size
.
width
/
2
,
dragPositionBelowTextField
),
),
),
);
// The magnifier Y should be greater than the text field, since we "dragged" it down.
expect
(
getMagnifierPosition
(
tester
).
dy
+
basicOffset
.
dy
,
greaterThan
(
reasonableTextField
.
center
.
dy
));
expect
(
getMagnifierPosition
(
tester
).
dy
+
basicOffset
.
dy
,
lessThan
(
dragPositionBelowTextField
));
});
});
group
(
'status'
,
()
{
testWidgets
(
'should hide if gesture is far below the text field'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
color:
Color
.
fromARGB
(
7
,
0
,
129
,
90
),
home:
Placeholder
(),
),
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierinfo
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
// The tap position is dragBelow units below the text field.
globalGesturePosition:
Offset
(
MediaQuery
.
of
(
context
).
size
.
width
/
2
,
reasonableTextField
.
top
),
),
);
// Show the magnifier initally, so that we get it in a not hidden state.
await
showCupertinoMagnifier
(
context
,
tester
,
magnifierinfo
);
// Move the gesture to one that should hide it.
magnifierinfo
.
value
=
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
magnifierinfo
.
value
.
globalGesturePosition
+
const
Offset
(
0
,
100
),
);
await
tester
.
pumpAndSettle
();
expect
(
magnifierController
.
shown
,
false
);
expect
(
magnifierController
.
overlayEntry
,
isNotNull
);
});
testWidgets
(
'should re-show if gesture moves back up'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
color:
Color
.
fromARGB
(
7
,
0
,
129
,
90
),
home:
Placeholder
(),
),
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierInfo
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
// The tap position is dragBelow units below the text field.
globalGesturePosition:
Offset
(
MediaQuery
.
of
(
context
).
size
.
width
/
2
,
reasonableTextField
.
top
),
),
);
// Show the magnifier initally, so that we get it in a not hidden state.
await
showCupertinoMagnifier
(
context
,
tester
,
magnifierInfo
);
// Move the gesture to one that should hide it.
magnifierInfo
.
value
=
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
magnifierInfo
.
value
.
globalGesturePosition
+
const
Offset
(
0
,
100
));
await
tester
.
pumpAndSettle
();
expect
(
magnifierController
.
shown
,
false
);
expect
(
magnifierController
.
overlayEntry
,
isNotNull
);
// Return the gesture to one that shows it.
magnifierInfo
.
value
=
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
Offset
(
MediaQuery
.
of
(
context
).
size
.
width
/
2
,
reasonableTextField
.
top
));
await
tester
.
pumpAndSettle
();
expect
(
magnifierController
.
shown
,
true
);
expect
(
magnifierController
.
overlayEntry
,
isNotNull
);
});
});
});
}
packages/flutter/test/cupertino/text_field_test.dart
View file @
f014c1e6
...
@@ -5961,6 +5961,148 @@ void main() {
...
@@ -5961,6 +5961,148 @@ void main() {
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
});
});
group
(
'magnifier'
,
()
{
late
ValueNotifier
<
MagnifierOverlayInfoBearer
>
infoBearer
;
final
Widget
fakeMagnifier
=
Container
(
key:
UniqueKey
());
group
(
'magnifier builder'
,
()
{
testWidgets
(
'should build custom magnifier if given'
,
(
WidgetTester
tester
)
async
{
final
Widget
customMagnifier
=
Container
(
key:
UniqueKey
(),
);
final
CupertinoTextField
defaultCupertinoTextField
=
CupertinoTextField
(
magnifierConfiguration:
TextMagnifierConfiguration
(
magnifierBuilder:
(
_
,
__
,
___
)
=>
customMagnifier
),
);
await
tester
.
pumpWidget
(
const
CupertinoApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
expect
(
defaultCupertinoTextField
.
magnifierConfiguration
!.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isA
<
Widget
>().
having
(
(
Widget
widget
)
=>
widget
.
key
,
'key'
,
equals
(
customMagnifier
.
key
)));
});
group
(
'defaults'
,
()
{
testWidgets
(
'should build CupertinoMagnifier on iOS and Android'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
CupertinoApp
(
home:
CupertinoTextField
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
CupertinoTextField
));
final
EditableText
editableText
=
tester
.
widget
(
find
.
byType
(
EditableText
));
expect
(
editableText
.
magnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isA
<
CupertinoTextMagnifier
>());
},
variant:
const
TargetPlatformVariant
(
<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
android
}));
});
testWidgets
(
'should build nothing on all platforms but iOS and Android'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
CupertinoApp
(
home:
CupertinoTextField
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
CupertinoTextField
));
final
EditableText
editableText
=
tester
.
widget
(
find
.
byType
(
EditableText
));
expect
(
editableText
.
magnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isNull
);
},
variant:
TargetPlatformVariant
.
all
(
excluding:
<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
android
}));
});
testWidgets
(
'Can drag handles to show, unshow, and update magnifier'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
();
await
tester
.
pumpWidget
(
CupertinoApp
(
home:
CupertinoPageScaffold
(
child:
Builder
(
builder:
(
BuildContext
context
)
=>
CupertinoTextField
(
dragStartBehavior:
DragStartBehavior
.
down
,
controller:
controller
,
magnifierConfiguration:
TextMagnifierConfiguration
(
magnifierBuilder:
(
_
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
localInfoBearer
)
{
infoBearer
=
localInfoBearer
;
return
fakeMagnifier
;
}),
),
),
),
),
);
const
String
testValue
=
'abc def ghi'
;
await
tester
.
enterText
(
find
.
byType
(
CupertinoTextField
),
testValue
);
// Double tap the 'e' to select 'def'.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
30
));
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
30
));
final
TextSelection
selection
=
controller
.
selection
;
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
final
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
selection
),
renderEditable
,
);
// Drag the right handle 2 letters to the right.
final
Offset
handlePos
=
endpoints
.
last
.
point
+
const
Offset
(
1.0
,
1.0
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
handlePos
,
pointer:
7
);
Offset
?
firstDragGesturePosition
;
await
gesture
.
moveTo
(
textOffsetToPosition
(
tester
,
testValue
.
length
-
2
));
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsOneWidget
);
firstDragGesturePosition
=
infoBearer
.
value
.
globalGesturePosition
;
await
gesture
.
moveTo
(
textOffsetToPosition
(
tester
,
testValue
.
length
));
await
tester
.
pump
();
// Expect the position the magnifier gets to have moved.
expect
(
firstDragGesturePosition
,
isNot
(
infoBearer
.
value
.
globalGesturePosition
));
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsNothing
);
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
iOS
));
group
(
'TapRegion integration'
,
()
{
group
(
'TapRegion integration'
,
()
{
testWidgets
(
'Tapping outside loses focus on desktop'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Tapping outside loses focus on desktop'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
...
@@ -6073,31 +6215,34 @@ void main() {
...
@@ -6073,31 +6215,34 @@ void main() {
variant:
TargetPlatformVariant
.
all
(),
variant:
TargetPlatformVariant
.
all
(),
skip:
kIsWeb
,
// [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
skip:
kIsWeb
,
// [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
);
);
testWidgets
(
"Tapping on border doesn't lose focus"
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
testWidgets
(
"Tapping on border doesn't lose focus"
,
await
tester
.
pumpWidget
(
(
WidgetTester
tester
)
async
{
CupertinoApp
(
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
home:
Center
(
await
tester
.
pumpWidget
(
child:
SizedBox
(
CupertinoApp
(
width:
100
,
home:
Center
(
height:
100
,
child:
SizedBox
(
child:
CupertinoTextField
(
width:
100
,
autofocus:
true
,
height:
100
,
focusNode:
focusNode
,
child:
CupertinoTextField
(
autofocus:
true
,
focusNode:
focusNode
,
),
),
),
),
),
),
),
),
);
);
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
final
Rect
borderBox
=
tester
.
getRect
(
find
.
byType
(
CupertinoTextField
));
final
Rect
borderBox
=
tester
.
getRect
(
find
.
byType
(
CupertinoTextField
));
// Tap just inside the border, but not inside the EditableText.
// Tap just inside the border, but not inside the EditableText.
await
tester
.
tapAt
(
borderBox
.
topLeft
+
const
Offset
(
1
,
1
));
await
tester
.
tapAt
(
borderBox
.
topLeft
+
const
Offset
(
1
,
1
));
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
expect
(
focusNode
.
hasPrimaryFocus
,
isTrue
);
},
variant:
TargetPlatformVariant
.
all
());
},
variant:
TargetPlatformVariant
.
all
());
});
});
});
}
}
packages/flutter/test/material/magnifier_test.dart
0 → 100644
View file @
f014c1e6
// 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.
@Tags
(<
String
>[
'reduced-test-set'
])
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
final
MagnifierController
magnifierController
=
MagnifierController
();
const
Rect
reasonableTextField
=
Rect
.
fromLTRB
(
50
,
100
,
200
,
100
);
final
Offset
basicOffset
=
Offset
(
Magnifier
.
kDefaultMagnifierSize
.
width
/
2
,
Magnifier
.
kStandardVerticalFocalPointShift
+
Magnifier
.
kDefaultMagnifierSize
.
height
);
Offset
getMagnifierPosition
(
WidgetTester
tester
,
[
bool
animated
=
false
])
{
if
(
animated
)
{
final
AnimatedPositioned
animatedPositioned
=
tester
.
firstWidget
(
find
.
byType
(
AnimatedPositioned
));
return
Offset
(
animatedPositioned
.
left
??
0
,
animatedPositioned
.
top
??
0
);
}
else
{
final
Positioned
positioned
=
tester
.
firstWidget
(
find
.
byType
(
Positioned
));
return
Offset
(
positioned
.
left
??
0
,
positioned
.
top
??
0
);
}
}
Future
<
void
>
showMagnifier
(
BuildContext
context
,
WidgetTester
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
infoBearer
,
)
async
{
final
Future
<
void
>
magnifierShown
=
magnifierController
.
show
(
context:
context
,
builder:
(
_
)
=>
TextMagnifier
(
magnifierInfo:
infoBearer
,
));
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pumpAndSettle
();
// Verify that the magnifier is shown.
await
magnifierShown
;
}
tearDown
(()
{
magnifierController
.
removeFromOverlay
();
magnifierController
.
animationController
=
null
;
});
group
(
'adaptiveMagnifierControllerBuilder'
,
()
{
testWidgets
(
'should return a TextEditingMagnifier on Android'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
Widget
?
builtWidget
=
TextMagnifier
.
adaptiveMagnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
),
);
expect
(
builtWidget
,
isA
<
TextMagnifier
>());
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
android
));
testWidgets
(
'should return a CupertinoMagnifier on iOS'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
Widget
?
builtWidget
=
TextMagnifier
.
adaptiveMagnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
()));
expect
(
builtWidget
,
isA
<
CupertinoTextMagnifier
>());
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
iOS
));
testWidgets
(
'should return null on all platforms not Android, iOS'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
Widget
?
builtWidget
=
TextMagnifier
.
adaptiveMagnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
),
);
expect
(
builtWidget
,
isNull
);
},
variant:
TargetPlatformVariant
.
all
(
excluding:
<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
android
}),
);
});
group
(
'magnifier'
,
()
{
group
(
'position'
,
()
{
testWidgets
(
'should be at gesture position if does not violate any positioning rules'
,
(
WidgetTester
tester
)
async
{
final
Key
textField
=
UniqueKey
();
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
await
tester
.
pumpWidget
(
Container
(
color:
const
Color
.
fromARGB
(
255
,
0
,
255
,
179
),
child:
MaterialApp
(
home:
Center
(
child:
Container
(
key:
textField
,
width:
10
,
height:
10
,
color:
Colors
.
red
,
child:
const
Placeholder
(),
)),
),
),
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
// Magnifier should be positioned directly over the red square.
final
RenderBox
tapPointRenderBox
=
tester
.
firstRenderObject
(
find
.
byKey
(
textField
))
as
RenderBox
;
final
Rect
fakeTextFieldRect
=
tapPointRenderBox
.
localToGlobal
(
Offset
.
zero
)
&
tapPointRenderBox
.
size
;
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierInfo
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
fakeTextFieldRect
,
fieldBounds:
fakeTextFieldRect
,
caretRect:
fakeTextFieldRect
,
// The tap position is dragBelow units below the text field.
globalGesturePosition:
fakeTextFieldRect
.
center
,
));
await
showMagnifier
(
context
,
tester
,
magnifierInfo
);
// Should show two red squares; original, and one in the magnifier,
// directly ontop of one another.
await
expectLater
(
find
.
byType
(
MaterialApp
),
matchesGoldenFile
(
'magnifier.position.default.png'
),
);
});
testWidgets
(
'should never move outside the right bounds of the editing line'
,
(
WidgetTester
tester
)
async
{
const
double
gestureOutsideLine
=
100
;
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
// Inflate these two to make sure we're bounding on the
// current line boundries, not anything else.
fieldBounds:
reasonableTextField
.
inflate
(
gestureOutsideLine
),
caretRect:
reasonableTextField
.
inflate
(
gestureOutsideLine
),
// The tap position is far out of the right side of the app.
globalGesturePosition:
Offset
(
reasonableTextField
.
right
+
gestureOutsideLine
,
0
),
),
),
);
// Should be less than the right edge, since we have padding.
expect
(
getMagnifierPosition
(
tester
).
dx
,
lessThanOrEqualTo
(
reasonableTextField
.
right
));
});
testWidgets
(
'should never move outside the left bounds of the editing line'
,
(
WidgetTester
tester
)
async
{
const
double
gestureOutsideLine
=
100
;
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
// Inflate these two to make sure we're bounding on the
// current line boundries, not anything else.
fieldBounds:
reasonableTextField
.
inflate
(
gestureOutsideLine
),
caretRect:
reasonableTextField
.
inflate
(
gestureOutsideLine
),
// The tap position is far out of the left side of the app.
globalGesturePosition:
Offset
(
reasonableTextField
.
left
-
gestureOutsideLine
,
0
),
),
),
);
expect
(
getMagnifierPosition
(
tester
).
dx
+
basicOffset
.
dx
,
greaterThanOrEqualTo
(
reasonableTextField
.
left
));
});
testWidgets
(
'should position vertically at the center of the line'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
reasonableTextField
.
center
,
)));
expect
(
getMagnifierPosition
(
tester
).
dy
,
reasonableTextField
.
center
.
dy
-
basicOffset
.
dy
);
});
testWidgets
(
'should reposition vertically if mashed against the ceiling'
,
(
WidgetTester
tester
)
async
{
final
Rect
topOfScreenTextFieldRect
=
Rect
.
fromPoints
(
Offset
.
zero
,
const
Offset
(
200
,
0
));
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
topOfScreenTextFieldRect
,
fieldBounds:
topOfScreenTextFieldRect
,
caretRect:
topOfScreenTextFieldRect
,
globalGesturePosition:
topOfScreenTextFieldRect
.
topCenter
,
),
),
);
expect
(
getMagnifierPosition
(
tester
).
dy
,
greaterThanOrEqualTo
(
0
));
});
});
group
(
'focal point'
,
()
{
Offset
getMagnifierAdditionalFocalPoint
(
WidgetTester
tester
)
{
final
Magnifier
magnifier
=
tester
.
firstWidget
(
find
.
byType
(
Magnifier
));
return
magnifier
.
additionalFocalPointOffset
;
}
testWidgets
(
'should shift focal point so that the lens sees nothing out of bounds'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
// Gesture on the far right of the magnifier.
globalGesturePosition:
reasonableTextField
.
topLeft
,
),
),
);
expect
(
getMagnifierAdditionalFocalPoint
(
tester
).
dx
,
lessThan
(
reasonableTextField
.
left
));
});
testWidgets
(
'focal point should shift if mashed against the top to always point to text'
,
(
WidgetTester
tester
)
async
{
final
Rect
topOfScreenTextFieldRect
=
Rect
.
fromPoints
(
Offset
.
zero
,
const
Offset
(
200
,
0
));
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
topOfScreenTextFieldRect
,
fieldBounds:
topOfScreenTextFieldRect
,
caretRect:
topOfScreenTextFieldRect
,
globalGesturePosition:
topOfScreenTextFieldRect
.
topCenter
,
),
),
);
expect
(
getMagnifierAdditionalFocalPoint
(
tester
).
dy
,
lessThan
(
0
));
});
});
group
(
'animation state'
,
()
{
bool
getIsAnimated
(
WidgetTester
tester
)
{
final
AnimatedPositioned
animatedPositioned
=
tester
.
firstWidget
(
find
.
byType
(
AnimatedPositioned
));
return
animatedPositioned
.
duration
.
compareTo
(
Duration
.
zero
)
!=
0
;
}
testWidgets
(
'should not be animated on the inital state'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
await
showMagnifier
(
context
,
tester
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
reasonableTextField
.
center
,
),
),
);
expect
(
getIsAnimated
(
tester
),
false
);
});
testWidgets
(
'should not be animated on horizontal shifts'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierPositioner
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
reasonableTextField
.
center
,
),
);
await
showMagnifier
(
context
,
tester
,
magnifierPositioner
);
// New position has a horizontal shift.
magnifierPositioner
.
value
=
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
reasonableTextField
.
center
+
const
Offset
(
200
,
0
),
);
await
tester
.
pumpAndSettle
();
expect
(
getIsAnimated
(
tester
),
false
);
});
testWidgets
(
'should be animated on vertical shifts'
,
(
WidgetTester
tester
)
async
{
const
Offset
verticalShift
=
Offset
(
0
,
200
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierPositioner
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
reasonableTextField
.
center
,
),
);
await
showMagnifier
(
context
,
tester
,
magnifierPositioner
);
// New position has a vertical shift.
magnifierPositioner
.
value
=
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
.
shift
(
verticalShift
),
fieldBounds:
Rect
.
fromPoints
(
reasonableTextField
.
topLeft
,
reasonableTextField
.
bottomRight
+
verticalShift
),
caretRect:
reasonableTextField
.
shift
(
verticalShift
),
globalGesturePosition:
reasonableTextField
.
center
+
verticalShift
,
);
await
tester
.
pump
();
expect
(
getIsAnimated
(
tester
),
true
);
});
testWidgets
(
'should stop being animated when timer is up'
,
(
WidgetTester
tester
)
async
{
const
Offset
verticalShift
=
Offset
(
0
,
200
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
final
ValueNotifier
<
MagnifierOverlayInfoBearer
>
magnifierPositioner
=
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
,
fieldBounds:
reasonableTextField
,
caretRect:
reasonableTextField
,
globalGesturePosition:
reasonableTextField
.
center
,
),
);
await
showMagnifier
(
context
,
tester
,
magnifierPositioner
);
// New position has a vertical shift.
magnifierPositioner
.
value
=
MagnifierOverlayInfoBearer
(
currentLineBoundries:
reasonableTextField
.
shift
(
verticalShift
),
fieldBounds:
Rect
.
fromPoints
(
reasonableTextField
.
topLeft
,
reasonableTextField
.
bottomRight
+
verticalShift
),
caretRect:
reasonableTextField
.
shift
(
verticalShift
),
globalGesturePosition:
reasonableTextField
.
center
+
verticalShift
,
);
await
tester
.
pump
();
expect
(
getIsAnimated
(
tester
),
true
);
await
tester
.
pump
(
TextMagnifier
.
jumpBetweenLinesAnimationDuration
+
const
Duration
(
seconds:
2
));
expect
(
getIsAnimated
(
tester
),
false
);
});
});
});
}
packages/flutter/test/material/text_field_test.dart
View file @
f014c1e6
...
@@ -11785,6 +11785,170 @@ void main() {
...
@@ -11785,6 +11785,170 @@ void main() {
expect
(
controller
.
selection
.
extentOffset
,
5
);
expect
(
controller
.
selection
.
extentOffset
,
5
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
});
});
group
(
'magnifier builder'
,
()
{
testWidgets
(
'should build custom magnifier if given'
,
(
WidgetTester
tester
)
async
{
final
Widget
customMagnifier
=
Container
(
key:
UniqueKey
(),
);
final
TextField
textField
=
TextField
(
magnifierConfiguration:
TextMagnifierConfiguration
(
magnifierBuilder:
(
_
,
__
,
___
)
=>
customMagnifier
,
),
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
expect
(
textField
.
magnifierConfiguration
!.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isA
<
Widget
>().
having
(
(
Widget
widget
)
=>
widget
.
key
,
'built magnifier key equal to passed in magnifier key'
,
equals
(
customMagnifier
.
key
)));
});
group
(
'defaults'
,
()
{
testWidgets
(
'should build Magnifier on Android'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Scaffold
(
body:
TextField
()))
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
TextField
));
final
EditableText
editableText
=
tester
.
widget
(
find
.
byType
(
EditableText
));
expect
(
editableText
.
magnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isA
<
TextMagnifier
>());
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
android
));
testWidgets
(
'should build CupertinoMagnifier on iOS'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Scaffold
(
body:
TextField
()))
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
TextField
));
final
EditableText
editableText
=
tester
.
widget
(
find
.
byType
(
EditableText
));
expect
(
editableText
.
magnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isA
<
CupertinoTextMagnifier
>());
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
iOS
));
testWidgets
(
'should build nothing on Android and iOS'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Scaffold
(
body:
TextField
()))
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
TextField
));
final
EditableText
editableText
=
tester
.
widget
(
find
.
byType
(
EditableText
));
expect
(
editableText
.
magnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
(),
)),
isNull
);
},
variant:
TargetPlatformVariant
.
all
(
excluding:
<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
android
}));
});
});
group
(
'magnifier'
,
()
{
late
ValueNotifier
<
MagnifierOverlayInfoBearer
>
infoBearer
;
final
Widget
fakeMagnifier
=
Container
(
key:
UniqueKey
());
testWidgets
(
'Can drag handles to show, unshow, and update magnifier'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
();
await
tester
.
pumpWidget
(
overlay
(
child:
TextField
(
dragStartBehavior:
DragStartBehavior
.
down
,
controller:
controller
,
magnifierConfiguration:
TextMagnifierConfiguration
(
magnifierBuilder:
(
_
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
localInfoBearer
)
{
infoBearer
=
localInfoBearer
;
return
fakeMagnifier
;
},
),
),
),
);
const
String
testValue
=
'abc def ghi'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
await
skipPastScrollingAnimation
(
tester
);
// Double tap the 'e' to select 'def'.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
30
));
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
30
));
final
TextSelection
selection
=
controller
.
selection
;
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
final
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
selection
),
renderEditable
,
);
// Drag the right handle 2 letters to the right.
final
Offset
handlePos
=
endpoints
.
last
.
point
+
const
Offset
(
1.0
,
1.0
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
handlePos
);
await
gesture
.
moveTo
(
textOffsetToPosition
(
tester
,
testValue
.
length
-
2
));
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsOneWidget
);
final
Offset
firstDragGesturePosition
=
infoBearer
.
value
.
globalGesturePosition
;
await
gesture
.
moveTo
(
textOffsetToPosition
(
tester
,
testValue
.
length
));
await
tester
.
pump
();
// Expect the position the magnifier gets to have moved.
expect
(
firstDragGesturePosition
,
isNot
(
infoBearer
.
value
.
globalGesturePosition
));
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsNothing
);
});
group
(
'TapRegion integration'
,
()
{
group
(
'TapRegion integration'
,
()
{
testWidgets
(
'Tapping outside loses focus on desktop'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Tapping outside loses focus on desktop'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Node'
);
...
@@ -12001,8 +12165,9 @@ void main() {
...
@@ -12001,8 +12165,9 @@ void main() {
case
PointerDeviceKind
.
unknown
:
case
PointerDeviceKind
.
unknown
:
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
expect
(
focusNode
.
hasPrimaryFocus
,
isFalse
);
break
;
break
;
}
}
},
variant:
TargetPlatformVariant
.
all
());
},
variant:
TargetPlatformVariant
.
all
());
}
}
});
});
});
}
}
packages/flutter/test/widgets/editable_text_test.dart
View file @
f014c1e6
...
@@ -12563,6 +12563,40 @@ void main() {
...
@@ -12563,6 +12563,40 @@ void main() {
);
);
});
});
});
});
group
(
'magnifier'
,
()
{
testWidgets
(
'should build nothing by default'
,
(
WidgetTester
tester
)
async
{
final
EditableText
editableText
=
EditableText
(
controller:
controller
,
showSelectionHandles:
true
,
autofocus:
true
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
selectionControls:
materialTextSelectionControls
,
keyboardType:
TextInputType
.
text
,
textAlign:
TextAlign
.
right
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
editableText
,
),
);
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
EditableText
));
expect
(
editableText
.
magnifierConfiguration
.
magnifierBuilder
(
context
,
MagnifierController
(),
ValueNotifier
<
MagnifierOverlayInfoBearer
>(
const
MagnifierOverlayInfoBearer
.
empty
())
),
isNull
);
});
});
}
}
class
UnsettableController
extends
TextEditingController
{
class
UnsettableController
extends
TextEditingController
{
...
...
packages/flutter/test/widgets/magnifier_test.dart
0 → 100644
View file @
f014c1e6
// 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.
@Tags
(<
String
>[
'reduced-test-set'
])
import
'package:fake_async/fake_async.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
class
_MockAnimationController
extends
AnimationController
{
_MockAnimationController
()
:
super
(
duration:
const
Duration
(
minutes:
1
),
vsync:
const
TestVSync
());
int
forwardCalls
=
0
;
int
reverseCalls
=
0
;
@override
TickerFuture
forward
({
double
?
from
})
{
forwardCalls
++;
return
super
.
forward
(
from:
from
);
}
@override
TickerFuture
reverse
({
double
?
from
})
{
reverseCalls
++;
return
super
.
reverse
(
from:
from
);
}
}
void
main
(
)
{
Future
<
T
>
runFakeAsync
<
T
>(
Future
<
T
>
Function
(
FakeAsync
time
)
f
)
async
{
return
FakeAsync
().
run
((
FakeAsync
time
)
async
{
bool
pump
=
true
;
final
Future
<
T
>
future
=
f
(
time
).
whenComplete
(()
=>
pump
=
false
);
while
(
pump
)
{
time
.
flushMicrotasks
();
}
return
future
;
});
}
group
(
'Raw Magnifier'
,
()
{
testWidgets
(
'should render with correct focal point and decoration'
,
(
WidgetTester
tester
)
async
{
final
Key
appKey
=
UniqueKey
();
const
Size
magnifierSize
=
Size
(
100
,
100
);
const
Offset
magnifierFocalPoint
=
Offset
(
50
,
50
);
const
Offset
magnifierPosition
=
Offset
(
200
,
200
);
const
double
magnificationScale
=
2
;
await
tester
.
pumpWidget
(
MaterialApp
(
key:
appKey
,
home:
Container
(
color:
Colors
.
orange
,
width:
double
.
infinity
,
height:
double
.
infinity
,
child:
Stack
(
children:
<
Widget
>[
Positioned
(
// Positioned so that it is right in the center of the magnifier
// focal point.
left:
magnifierPosition
.
dx
+
magnifierFocalPoint
.
dx
,
top:
magnifierPosition
.
dy
+
magnifierFocalPoint
.
dy
,
child:
Container
(
color:
Colors
.
pink
,
// 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
,
height:
(
magnifierSize
.
height
*
1.5
)
/
magnificationScale
,
),
),
Positioned
(
left:
magnifierPosition
.
dx
,
top:
magnifierPosition
.
dy
,
child:
const
RawMagnifier
(
size:
magnifierSize
,
focalPointOffset:
magnifierFocalPoint
,
magnificationScale:
magnificationScale
,
decoration:
MagnifierDecoration
(
shadows:
<
BoxShadow
>[
BoxShadow
(
spreadRadius:
10
,
blurRadius:
10
,
color:
Colors
.
green
,
offset:
Offset
(
5
,
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.
await
expectLater
(
find
.
byKey
(
appKey
),
matchesGoldenFile
(
'widgets.magnifier.styled.png'
),
);
},
skip:
kIsWeb
);
// [intended] Bdf does not display on web.
group
(
'transition states'
,
()
{
final
AnimationController
animationController
=
AnimationController
(
vsync:
const
TestVSync
(),
duration:
const
Duration
(
minutes:
2
));
final
MagnifierController
magnifierController
=
MagnifierController
();
tearDown
(()
{
animationController
.
value
=
0
;
magnifierController
.
hide
();
magnifierController
.
removeFromOverlay
();
});
testWidgets
(
'should immediately remove from overlay on no animation controller'
,
(
WidgetTester
tester
)
async
{
await
runFakeAsync
((
FakeAsync
async
)
async
{
const
RawMagnifier
testMagnifier
=
RawMagnifier
(
size:
Size
(
100
,
100
),
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
magnifierController
.
show
(
context:
context
,
builder:
(
BuildContext
context
)
=>
testMagnifier
,
);
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pump
();
expect
(
magnifierController
.
overlayEntry
,
isNot
(
isNull
));
magnifierController
.
hide
();
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pump
();
expect
(
magnifierController
.
overlayEntry
,
isNull
);
});
});
testWidgets
(
'should update shown based on animation status'
,
(
WidgetTester
tester
)
async
{
await
runFakeAsync
((
FakeAsync
async
)
async
{
final
MagnifierController
magnifierController
=
MagnifierController
(
animationController:
animationController
);
const
RawMagnifier
testMagnifier
=
RawMagnifier
(
size:
Size
(
100
,
100
),
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
magnifierController
.
show
(
context:
context
,
builder:
(
BuildContext
context
)
=>
testMagnifier
,
);
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pump
();
// No time has passed, so the animation controller has not completed.
expect
(
magnifierController
.
animationController
?.
status
,
AnimationStatus
.
forward
);
expect
(
magnifierController
.
shown
,
true
);
async
.
elapse
(
animationController
.
duration
!);
await
tester
.
pumpAndSettle
();
expect
(
magnifierController
.
animationController
?.
status
,
AnimationStatus
.
completed
);
expect
(
magnifierController
.
shown
,
true
);
magnifierController
.
hide
();
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pump
();
expect
(
magnifierController
.
animationController
?.
status
,
AnimationStatus
.
reverse
);
expect
(
magnifierController
.
shown
,
false
);
async
.
elapse
(
animationController
.
duration
!);
await
tester
.
pumpAndSettle
();
expect
(
magnifierController
.
animationController
?.
status
,
AnimationStatus
.
dismissed
);
expect
(
magnifierController
.
shown
,
false
);
});
});
});
});
group
(
'magnifier controller'
,
()
{
final
MagnifierController
magnifierController
=
MagnifierController
();
tearDown
(()
{
magnifierController
.
removeFromOverlay
();
});
group
(
'show'
,
()
{
testWidgets
(
'should insert below below widget'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Text
(
'text'
),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Text
));
final
Widget
fakeMagnifier
=
Placeholder
(
key:
UniqueKey
());
final
Widget
fakeBefore
=
Placeholder
(
key:
UniqueKey
());
final
OverlayEntry
fakeBeforeOverlayEntry
=
OverlayEntry
(
builder:
(
_
)
=>
fakeBefore
);
Overlay
.
of
(
context
)!.
insert
(
fakeBeforeOverlayEntry
);
magnifierController
.
show
(
context:
context
,
builder:
(
_
)
=>
fakeMagnifier
,
below:
fakeBeforeOverlayEntry
);
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pumpAndSettle
();
final
Iterable
<
Element
>
allOverlayChildren
=
find
.
descendant
(
of:
find
.
byType
(
Overlay
),
matching:
find
.
byType
(
Placeholder
))
.
evaluate
();
// Expect the magnifier to be the first child, even though it was inserted
// after the fakeBefore.
expect
(
allOverlayChildren
.
last
.
widget
.
key
,
fakeBefore
.
key
);
expect
(
allOverlayChildren
.
first
.
widget
.
key
,
fakeMagnifier
.
key
);
});
testWidgets
(
'should insert newly built widget without animating out if overlay != null'
,
(
WidgetTester
tester
)
async
{
await
runFakeAsync
((
FakeAsync
async
)
async
{
final
_MockAnimationController
animationController
=
_MockAnimationController
();
const
RawMagnifier
testMagnifier
=
RawMagnifier
(
size:
Size
(
100
,
100
),
);
const
RawMagnifier
testMagnifier2
=
RawMagnifier
(
size:
Size
(
100
,
100
),
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Placeholder
(),
));
final
BuildContext
context
=
tester
.
firstElement
(
find
.
byType
(
Placeholder
));
magnifierController
.
show
(
context:
context
,
builder:
(
BuildContext
context
)
=>
testMagnifier
,
);
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pump
();
async
.
elapse
(
animationController
.
duration
!);
await
tester
.
pumpAndSettle
();
magnifierController
.
show
(
context:
context
,
builder:
(
_
)
=>
testMagnifier2
);
WidgetsBinding
.
instance
.
scheduleFrame
();
await
tester
.
pump
();
expect
(
animationController
.
reverseCalls
,
0
,
reason:
'should not have called reverse on animation controller due to force remove'
);
expect
(
find
.
byWidget
(
testMagnifier2
),
findsOneWidget
);
});
});
});
group
(
'shift within bounds'
,
()
{
final
List
<
Rect
>
boundsRects
=
<
Rect
>[
const
Rect
.
fromLTRB
(
0
,
0
,
100
,
100
),
const
Rect
.
fromLTRB
(
0
,
0
,
100
,
100
),
const
Rect
.
fromLTRB
(
0
,
0
,
100
,
100
),
const
Rect
.
fromLTRB
(
0
,
0
,
100
,
100
),
];
final
List
<
Rect
>
inputRects
=
<
Rect
>[
const
Rect
.
fromLTRB
(-
100
,
-
100
,
-
80
,
-
80
),
const
Rect
.
fromLTRB
(
0
,
0
,
20
,
20
),
const
Rect
.
fromLTRB
(
110
,
0
,
120
,
10
),
const
Rect
.
fromLTRB
(
110
,
110
,
120
,
120
)
];
final
List
<
Rect
>
outputRects
=
<
Rect
>[
const
Rect
.
fromLTRB
(
0
,
0
,
20
,
20
),
const
Rect
.
fromLTRB
(
0
,
0
,
20
,
20
),
const
Rect
.
fromLTRB
(
90
,
0
,
100
,
10
),
const
Rect
.
fromLTRB
(
90
,
90
,
100
,
100
)
];
for
(
int
i
=
0
;
i
<
boundsRects
.
length
;
i
++)
{
test
(
'should shift
${inputRects[i]}
to
${outputRects[i]}
for bounds
${boundsRects[i]}
'
,
()
{
final
Rect
outputRect
=
MagnifierController
.
shiftWithinBounds
(
bounds:
boundsRects
[
i
],
rect:
inputRects
[
i
]);
expect
(
outputRect
,
outputRects
[
i
]);
});
}
});
});
}
packages/flutter/test/widgets/selectable_region_test.dart
View file @
f014c1e6
...
@@ -1099,6 +1099,74 @@ void main() {
...
@@ -1099,6 +1099,74 @@ void main() {
final
Map
<
String
,
dynamic
>
clipboardData
=
mockClipboard
.
clipboardData
as
Map
<
String
,
dynamic
>;
final
Map
<
String
,
dynamic
>
clipboardData
=
mockClipboard
.
clipboardData
as
Map
<
String
,
dynamic
>;
expect
(
clipboardData
[
'text'
],
'thank'
);
expect
(
clipboardData
[
'text'
],
'thank'
);
},
skip:
kIsWeb
);
// [intended] Web uses its native context menu.
},
skip:
kIsWeb
);
// [intended] Web uses its native context menu.
group
(
'magnifier'
,
()
{
late
ValueNotifier
<
MagnifierOverlayInfoBearer
>
infoBearer
;
final
Widget
fakeMagnifier
=
Container
(
key:
UniqueKey
());
testWidgets
(
'Can drag handles to show, unshow, and update magnifier'
,
(
WidgetTester
tester
)
async
{
const
String
text
=
'Monkies and rabbits in my soup'
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
SelectableRegion
(
magnifierConfiguration:
TextMagnifierConfiguration
(
magnifierBuilder:
(
_
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
localInfoBearer
)
{
infoBearer
=
localInfoBearer
;
return
fakeMagnifier
;
},
),
focusNode:
FocusNode
(),
selectionControls:
materialTextSelectionControls
,
child:
const
Text
(
text
),
),
),
);
final
RenderParagraph
paragraph
=
tester
.
renderObject
<
RenderParagraph
>(
find
.
descendant
(
of:
find
.
text
(
text
),
matching:
find
.
byType
(
RichText
)));
// Show the selection handles.
final
TestGesture
activateSelectionGesture
=
await
tester
.
startGesture
(
textOffsetToPosition
(
paragraph
,
text
.
length
~/
2
));
addTearDown
(
activateSelectionGesture
.
removePointer
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
activateSelectionGesture
.
up
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Drag the handle around so that the magnifier shows.
final
TextBox
selectionBox
=
paragraph
.
getBoxesForSelection
(
paragraph
.
selections
.
first
).
first
;
final
Offset
leftHandlePos
=
globalize
(
selectionBox
.
toRect
().
bottomLeft
,
paragraph
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
leftHandlePos
);
await
gesture
.
moveTo
(
textOffsetToPosition
(
paragraph
,
text
.
length
-
2
));
await
tester
.
pump
();
// Expect the magnifier to show and then store it's position.
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsOneWidget
);
final
Offset
firstDragGesturePosition
=
infoBearer
.
value
.
globalGesturePosition
;
await
gesture
.
moveTo
(
textOffsetToPosition
(
paragraph
,
text
.
length
));
await
tester
.
pump
();
// Expect the position the magnifier gets to have moved.
expect
(
firstDragGesturePosition
,
isNot
(
infoBearer
.
value
.
globalGesturePosition
));
// Lift the pointer and expect the magnifier to disappear.
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsNothing
);
});
});
});
});
testWidgets
(
'toolbar is hidden on mobile when orientation changes'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'toolbar is hidden on mobile when orientation changes'
,
(
WidgetTester
tester
)
async
{
...
...
packages/flutter/test/widgets/selectable_text_test.dart
View file @
f014c1e6
...
@@ -5152,6 +5152,79 @@ void main() {
...
@@ -5152,6 +5152,79 @@ void main() {
expect
(
find
.
byType
(
SelectableText
,
skipOffstage:
false
),
findsOneWidget
);
expect
(
find
.
byType
(
SelectableText
,
skipOffstage:
false
),
findsOneWidget
);
});
});
group
(
'magnifier'
,
()
{
late
ValueNotifier
<
MagnifierOverlayInfoBearer
>
infoBearer
;
final
Widget
fakeMagnifier
=
Container
(
key:
UniqueKey
());
testWidgets
(
'Can drag handles to show, unshow, and update magnifier'
,
(
WidgetTester
tester
)
async
{
const
String
testValue
=
'abc def ghi'
;
final
SelectableText
selectableText
=
SelectableText
(
testValue
,
magnifierConfiguration:
TextMagnifierConfiguration
(
magnifierBuilder:
(
_
,
MagnifierController
controller
,
ValueNotifier
<
MagnifierOverlayInfoBearer
>
localInfoBearer
)
{
infoBearer
=
localInfoBearer
;
return
fakeMagnifier
;
},
)
);
await
tester
.
pumpWidget
(
overlay
(
child:
selectableText
,
),
);
await
skipPastScrollingAnimation
(
tester
);
// Double tap the 'e' to select 'def'.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
30
));
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
30
));
final
TextSelection
selection
=
TextSelection
(
baseOffset:
testValue
.
indexOf
(
'd'
),
extentOffset:
testValue
.
indexOf
(
'f'
)
);
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
final
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
selection
),
renderEditable
,
);
// Drag the right handle 2 letters to the right.
final
Offset
handlePos
=
endpoints
.
last
.
point
+
const
Offset
(
1.0
,
1.0
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
handlePos
,
pointer:
7
);
Offset
?
firstDragGesturePosition
;
await
gesture
.
moveTo
(
textOffsetToPosition
(
tester
,
testValue
.
length
-
2
));
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsOneWidget
);
firstDragGesturePosition
=
infoBearer
.
value
.
globalGesturePosition
;
await
gesture
.
moveTo
(
textOffsetToPosition
(
tester
,
testValue
.
length
));
await
tester
.
pump
();
// Expect the position the magnifier gets to have moved.
expect
(
firstDragGesturePosition
,
isNot
(
infoBearer
.
value
.
globalGesturePosition
));
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
find
.
byKey
(
fakeMagnifier
.
key
!),
findsNothing
);
});
});
testWidgets
(
'SelectableText text span style is merged with default text style'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'SelectableText text span style is merged with default text style'
,
(
WidgetTester
tester
)
async
{
// This is a regression test for https://github.com/flutter/flutter/issues/71389
// This is a regression test for https://github.com/flutter/flutter/issues/71389
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment