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
2338576a
Unverified
Commit
2338576a
authored
Jul 19, 2019
by
chunhtai
Committed by
GitHub
Jul 19, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
implement selectable text (#34019)
parent
41bc10fa
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
4461 additions
and
55 deletions
+4461
-55
material.dart
packages/flutter/lib/material.dart
+1
-0
selectable_text.dart
packages/flutter/lib/src/material/selectable_text.dart
+580
-0
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+2
-8
text_painter.dart
packages/flutter/lib/src/painting/text_painter.dart
+2
-2
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+65
-24
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+64
-20
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+25
-1
selectable_text_test.dart
packages/flutter/test/widgets/selectable_text_test.dart
+3722
-0
No files found.
packages/flutter/lib/material.dart
View file @
2338576a
...
...
@@ -93,6 +93,7 @@ export 'src/material/reorderable_list.dart';
export
'src/material/scaffold.dart'
;
export
'src/material/scrollbar.dart'
;
export
'src/material/search.dart'
;
export
'src/material/selectable_text.dart'
;
export
'src/material/shadows.dart'
;
export
'src/material/slider.dart'
;
export
'src/material/slider_theme.dart'
;
...
...
packages/flutter/lib/src/material/selectable_text.dart
0 → 100644
View file @
2338576a
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'feedback.dart'
;
import
'text_selection.dart'
;
import
'theme.dart'
;
/// An eyeballed value that moves the cursor slightly left of where it is
/// rendered for text on Android so its positioning more accurately matches the
/// native iOS text cursor positioning.
///
/// This value is in device pixels, not logical pixels as is typically used
/// throughout the codebase.
const
int
iOSHorizontalOffset
=
-
2
;
class
_TextSpanEditingController
extends
TextEditingController
{
_TextSpanEditingController
({
@required
TextSpan
textSpan
}):
assert
(
textSpan
!=
null
),
_textSpan
=
textSpan
,
super
(
text:
textSpan
.
toPlainText
());
final
TextSpan
_textSpan
;
@override
TextSpan
buildTextSpan
({
TextStyle
style
,
bool
withComposing
})
{
// TODO(chunhtai): Implement composing.
return
TextSpan
(
style:
style
,
children:
<
TextSpan
>[
_textSpan
],
);
}
@override
set
text
(
String
newText
)
{
// TODO(chunhtai): Implement value editing.
}
}
class
_SelectableTextSelectionGestureDetectorBuilder
extends
TextSelectionGestureDetectorBuilder
{
_SelectableTextSelectionGestureDetectorBuilder
({
@required
_SelectableTextState
state
})
:
_state
=
state
,
super
(
delegate:
state
);
final
_SelectableTextState
_state
;
@override
void
onForcePressStart
(
ForcePressDetails
details
)
{
super
.
onForcePressStart
(
details
);
if
(
delegate
.
selectionEnabled
&&
shouldShowSelectionToolbar
)
{
editableText
.
showToolbar
();
}
}
@override
void
onForcePressEnd
(
ForcePressDetails
details
)
{
// Not required.
}
@override
void
onSingleLongTapMoveUpdate
(
LongPressMoveUpdateDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
_state
.
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
-
details
.
offsetFromOrigin
,
to:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
}
}
}
@override
void
onSingleTapUp
(
TapUpDetails
details
)
{
editableText
.
hideToolbar
();
if
(
delegate
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
_state
.
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
tap
);
break
;
}
}
if
(
_state
.
widget
.
onTap
!=
null
)
_state
.
widget
.
onTap
();
}
@override
void
onSingleLongTapStart
(
LongPressStartDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
_state
.
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
longPress
);
Feedback
.
forLongPress
(
_state
.
context
);
break
;
}
}
}
}
/// A run of selectable text with a single style.
///
/// The [SelectableText] widget displays a string of text with a single style.
/// The string might break across multiple lines or might all be displayed on
/// the same line depending on the layout constraints.
///
/// The [style] argument is optional. When omitted, the text will use the style
/// from the closest enclosing [DefaultTextStyle]. If the given style's
/// [TextStyle.inherit] property is true (the default), the given style will
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
/// behavior is useful, for example, to make the text bold while using the
/// default font family and size.
///
/// {@tool sample}
///
/// ```dart
/// SelectableText(
/// 'Hello! How are you?',
/// textAlign: TextAlign.center,
/// style: TextStyle(fontWeight: FontWeight.bold),
/// )
/// ```
/// {@end-tool}
///
/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can
/// display a paragraph with differently styled [TextSpan]s. The sample
/// that follows displays "Hello beautiful world" with different styles
/// for each word.
///
/// {@tool sample}
///
/// ```dart
/// const SelectableText.rich(
/// TextSpan(
/// text: 'Hello', // default text style
/// children: <TextSpan>[
/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
/// ],
/// ),
/// )
/// ```
/// {@end-tool}
///
/// ## Interactivity
///
/// To make [SelectableText] react to touch events, use callback [onTap] to achieve
/// the desired behavior.
///
/// See also:
///
/// * [Text], which is the non selectable version of this widget.
/// * [TextField], which is the editable version of this widget.
class
SelectableText
extends
StatefulWidget
{
/// Creates a selectable text widget.
///
/// If the [style] argument is null, the text will use the style from the
/// closest enclosing [DefaultTextStyle].
///
/// The [data] parameter must not be null.
const
SelectableText
(
this
.
data
,
{
Key
key
,
this
.
focusNode
,
this
.
style
,
this
.
strutStyle
,
this
.
textAlign
,
this
.
textDirection
,
this
.
showCursor
=
false
,
this
.
autofocus
=
false
,
this
.
maxLines
,
this
.
cursorWidth
=
2.0
,
this
.
cursorRadius
,
this
.
cursorColor
,
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
this
.
enableInteractiveSelection
=
true
,
this
.
onTap
,
this
.
scrollPhysics
,
this
.
textWidthBasis
,
})
:
assert
(
showCursor
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
dragStartBehavior
!=
null
),
assert
(
maxLines
==
null
||
maxLines
>
0
),
assert
(
data
!=
null
,
'A non-null String must be provided to a SelectableText widget.'
,
),
textSpan
=
null
,
super
(
key:
key
);
/// Creates a selectable text widget with a [TextSpan].
///
/// The [textSpan] parameter must not be null and only contain [TextSpan] in
/// [textSpan.children]. Other type of [InlineSpan] is not allowed.
const
SelectableText
.
rich
(
this
.
textSpan
,
{
Key
key
,
this
.
focusNode
,
this
.
style
,
this
.
strutStyle
,
this
.
textAlign
,
this
.
textDirection
,
this
.
showCursor
=
false
,
this
.
autofocus
=
false
,
this
.
maxLines
,
this
.
cursorWidth
=
2.0
,
this
.
cursorRadius
,
this
.
cursorColor
,
this
.
dragStartBehavior
=
DragStartBehavior
.
start
,
this
.
enableInteractiveSelection
=
true
,
this
.
onTap
,
this
.
scrollPhysics
,
this
.
textWidthBasis
,
})
:
assert
(
showCursor
!=
null
),
assert
(
autofocus
!=
null
),
assert
(
dragStartBehavior
!=
null
),
assert
(
maxLines
==
null
||
maxLines
>
0
),
assert
(
textSpan
!=
null
,
'A non-null TextSpan must be provided to a SelectableText.rich widget.'
,
),
data
=
null
,
super
(
key:
key
);
/// The text to display.
///
/// This will be null if a [textSpan] is provided instead.
final
String
data
;
/// The text to display as a [TextSpan].
///
/// This will be null if [data] is provided instead.
final
TextSpan
textSpan
;
/// Defines the focus for this widget.
///
/// Text is only selectable when widget is focused.
///
/// The [focusNode] is a long-lived object that's typically managed by a
/// [StatefulWidget] parent. See [FocusNode] for more information.
///
/// To give the focus to this widget, provide a [focusNode] and then
/// use the current [FocusScope] to request the focus:
///
/// ```dart
/// FocusScope.of(context).requestFocus(myFocusNode);
/// ```
///
/// This happens automatically when the widget is tapped.
///
/// To be notified when the widget gains or loses the focus, add a listener
/// to the [focusNode]:
///
/// ```dart
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
/// ```
///
/// If null, this widget will create its own [FocusNode].
final
FocusNode
focusNode
;
/// The style to use for the text.
///
/// If null, defaults [DefaultTextStyle] of context.
final
TextStyle
style
;
/// {@macro flutter.widgets.editableText.strutStyle}
final
StrutStyle
strutStyle
;
/// {@macro flutter.widgets.editableText.textAlign}
final
TextAlign
textAlign
;
/// {@macro flutter.widgets.editableText.textDirection}
final
TextDirection
textDirection
;
/// {@macro flutter.widgets.editableText.autofocus}
final
bool
autofocus
;
/// {@macro flutter.widgets.editableText.maxLines}
final
int
maxLines
;
/// {@macro flutter.widgets.editableText.showCursor}
final
bool
showCursor
;
/// {@macro flutter.widgets.editableText.cursorWidth}
final
double
cursorWidth
;
/// {@macro flutter.widgets.editableText.cursorRadius}
final
Radius
cursorRadius
;
/// The color to use when painting the cursor.
///
/// Defaults to the theme's `cursorColor` when null.
final
Color
cursorColor
;
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final
bool
enableInteractiveSelection
;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final
DragStartBehavior
dragStartBehavior
;
/// {@macro flutter.rendering.editable.selectionEnabled}
bool
get
selectionEnabled
{
return
enableInteractiveSelection
;
}
/// Called when the user taps on this selectable text.
///
/// The selectable text builds a [GestureDetector] to handle input events like tap,
/// to trigger focus requests, to move the caret, adjust the selection, etc.
/// Handling some of those events by wrapping the selectable text with a competing
/// GestureDetector is problematic.
///
/// To unconditionally handle taps, without interfering with the selectable text's
/// internal gesture detector, provide this callback.
///
/// To be notified when the text field gains or loses the focus, provide a
/// [focusNode] and add a listener to that.
///
/// To listen to arbitrary pointer events without competing with the
/// selectable text's internal gesture detector, use a [Listener].
final
GestureTapCallback
onTap
;
/// {@macro flutter.widgets.edtiableText.scrollPhysics}
final
ScrollPhysics
scrollPhysics
;
/// {@macro flutter.dart:ui.text.TextWidthBasis}
final
TextWidthBasis
textWidthBasis
;
@override
_SelectableTextState
createState
()
=>
_SelectableTextState
();
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
String
>(
'data'
,
data
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focusNode'
,
focusNode
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
TextStyle
>(
'style'
,
style
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'autofocus'
,
autofocus
,
defaultValue:
false
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'showCursor'
,
showCursor
,
defaultValue:
false
));
properties
.
add
(
IntProperty
(
'maxLines'
,
maxLines
,
defaultValue:
null
));
properties
.
add
(
EnumProperty
<
TextAlign
>(
'textAlign'
,
textAlign
,
defaultValue:
null
));
properties
.
add
(
EnumProperty
<
TextDirection
>(
'textDirection'
,
textDirection
,
defaultValue:
null
));
properties
.
add
(
DoubleProperty
(
'cursorWidth'
,
cursorWidth
,
defaultValue:
2.0
));
properties
.
add
(
DiagnosticsProperty
<
Radius
>(
'cursorRadius'
,
cursorRadius
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Color
>(
'cursorColor'
,
cursorColor
,
defaultValue:
null
));
properties
.
add
(
FlagProperty
(
'selectionEnabled'
,
value:
selectionEnabled
,
defaultValue:
true
,
ifFalse:
'selection disabled'
));
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
}
}
class
_SelectableTextState
extends
State
<
SelectableText
>
with
AutomaticKeepAliveClientMixin
implements
TextSelectionGestureDetectorBuilderDelegate
{
EditableTextState
get
_editableText
=>
editableTextKey
.
currentState
;
_TextSpanEditingController
_controller
;
FocusNode
_focusNode
;
FocusNode
get
_effectiveFocusNode
=>
widget
.
focusNode
??
(
_focusNode
??=
FocusNode
());
bool
_showSelectionHandles
=
false
;
_SelectableTextSelectionGestureDetectorBuilder
_selectionGestureDetectorBuilder
;
// API for TextSelectionGestureDetectorBuilderDelegate.
@override
bool
forcePressEnabled
;
@override
final
GlobalKey
<
EditableTextState
>
editableTextKey
=
GlobalKey
<
EditableTextState
>();
@override
bool
get
selectionEnabled
=>
widget
.
selectionEnabled
;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
@override
void
initState
()
{
super
.
initState
();
_selectionGestureDetectorBuilder
=
_SelectableTextSelectionGestureDetectorBuilder
(
state:
this
);
_controller
=
_TextSpanEditingController
(
textSpan:
widget
.
textSpan
??
TextSpan
(
text:
widget
.
data
)
);
}
@override
void
didUpdateWidget
(
SelectableText
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
data
!=
oldWidget
.
data
||
widget
.
textSpan
!=
oldWidget
.
textSpan
)
{
_controller
=
_TextSpanEditingController
(
textSpan:
widget
.
textSpan
??
TextSpan
(
text:
widget
.
data
)
);
}
if
(
_effectiveFocusNode
.
hasFocus
&&
_controller
.
selection
.
isCollapsed
)
{
_showSelectionHandles
=
false
;
}
}
@override
void
dispose
()
{
_focusNode
?.
dispose
();
super
.
dispose
();
}
void
_handleSelectionChanged
(
TextSelection
selection
,
SelectionChangedCause
cause
)
{
final
bool
willShowSelectionHandles
=
_shouldShowSelectionHandles
(
cause
);
if
(
willShowSelectionHandles
!=
_showSelectionHandles
)
{
setState
(()
{
_showSelectionHandles
=
willShowSelectionHandles
;
});
}
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
if
(
cause
==
SelectionChangedCause
.
longPress
)
{
_editableText
?.
bringIntoView
(
selection
.
base
);
}
return
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
// Do nothing.
}
}
/// Toggle the toolbar when a selection handle is tapped.
void
_handleSelectionHandleTapped
()
{
if
(
_controller
.
selection
.
isCollapsed
)
{
_editableText
.
toggleToolbar
();
}
}
bool
_shouldShowSelectionHandles
(
SelectionChangedCause
cause
)
{
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
if
(!
_selectionGestureDetectorBuilder
.
shouldShowSelectionToolbar
)
return
false
;
if
(
_controller
.
selection
.
isCollapsed
)
return
false
;
if
(
cause
==
SelectionChangedCause
.
keyboard
)
return
false
;
if
(
cause
==
SelectionChangedCause
.
longPress
)
return
true
;
if
(
_controller
.
text
.
isNotEmpty
)
return
true
;
return
false
;
}
@override
bool
get
wantKeepAlive
=>
true
;
@override
Widget
build
(
BuildContext
context
)
{
super
.
build
(
context
);
// See AutomaticKeepAliveClientMixin.
assert
(()
{
return
_controller
.
_textSpan
.
visitChildren
((
InlineSpan
span
)
=>
span
.
runtimeType
==
TextSpan
);
}(),
'SelectableText only supports TextSpan; Other type of InlineSpan is not allowed'
);
assert
(
debugCheckHasMediaQuery
(
context
));
assert
(
debugCheckHasDirectionality
(
context
));
assert
(
!(
widget
.
style
!=
null
&&
widget
.
style
.
inherit
==
false
&&
(
widget
.
style
.
fontSize
==
null
||
widget
.
style
.
textBaseline
==
null
)),
'inherit false style must supply fontSize and textBaseline'
,
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
FocusNode
focusNode
=
_effectiveFocusNode
;
TextSelectionControls
textSelectionControls
;
bool
paintCursorAboveText
;
bool
cursorOpacityAnimates
;
Offset
cursorOffset
;
Color
cursorColor
=
widget
.
cursorColor
;
Radius
cursorRadius
=
widget
.
cursorRadius
;
switch
(
themeData
.
platform
)
{
case
TargetPlatform
.
iOS
:
forcePressEnabled
=
true
;
textSelectionControls
=
cupertinoTextSelectionControls
;
paintCursorAboveText
=
true
;
cursorOpacityAnimates
=
true
;
cursorColor
??=
CupertinoTheme
.
of
(
context
).
primaryColor
;
cursorRadius
??=
const
Radius
.
circular
(
2.0
);
cursorOffset
=
Offset
(
iOSHorizontalOffset
/
MediaQuery
.
of
(
context
).
devicePixelRatio
,
0
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
forcePressEnabled
=
false
;
textSelectionControls
=
materialTextSelectionControls
;
paintCursorAboveText
=
false
;
cursorOpacityAnimates
=
false
;
cursorColor
??=
themeData
.
cursorColor
;
break
;
}
final
DefaultTextStyle
defaultTextStyle
=
DefaultTextStyle
.
of
(
context
);
TextStyle
effectiveTextStyle
=
widget
.
style
;
if
(
widget
.
style
==
null
||
widget
.
style
.
inherit
)
effectiveTextStyle
=
defaultTextStyle
.
style
.
merge
(
widget
.
style
);
if
(
MediaQuery
.
boldTextOverride
(
context
))
effectiveTextStyle
=
effectiveTextStyle
.
merge
(
const
TextStyle
(
fontWeight:
FontWeight
.
bold
));
final
Widget
child
=
RepaintBoundary
(
child:
EditableText
(
key:
editableTextKey
,
style:
effectiveTextStyle
,
readOnly:
true
,
textWidthBasis:
widget
.
textWidthBasis
??
defaultTextStyle
.
textWidthBasis
,
showSelectionHandles:
_showSelectionHandles
,
showCursor:
widget
.
showCursor
,
controller:
_controller
,
focusNode:
focusNode
,
strutStyle:
widget
.
strutStyle
??
StrutStyle
.
disabled
,
textAlign:
widget
.
textAlign
??
defaultTextStyle
.
textAlign
??
TextAlign
.
start
,
textDirection:
widget
.
textDirection
,
autofocus:
widget
.
autofocus
,
forceLine:
false
,
maxLines:
widget
.
maxLines
??
defaultTextStyle
.
maxLines
,
selectionColor:
themeData
.
textSelectionColor
,
selectionControls:
widget
.
selectionEnabled
?
textSelectionControls
:
null
,
onSelectionChanged:
_handleSelectionChanged
,
onSelectionHandleTapped:
_handleSelectionHandleTapped
,
rendererIgnoresPointer:
true
,
cursorWidth:
widget
.
cursorWidth
,
cursorRadius:
cursorRadius
,
cursorColor:
cursorColor
,
cursorOpacityAnimates:
cursorOpacityAnimates
,
cursorOffset:
cursorOffset
,
paintCursorAboveText:
paintCursorAboveText
,
backgroundCursorColor:
CupertinoColors
.
inactiveGray
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
,
dragStartBehavior:
widget
.
dragStartBehavior
,
scrollPhysics:
widget
.
scrollPhysics
,
),
);
return
Semantics
(
onTap:
()
{
if
(!
_controller
.
selection
.
isValid
)
_controller
.
selection
=
TextSelection
.
collapsed
(
offset:
_controller
.
text
.
length
);
_effectiveFocusNode
.
requestFocus
();
},
onLongPress:
()
{
_effectiveFocusNode
.
requestFocus
();
},
child:
_selectionGestureDetectorBuilder
.
buildGestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
child:
child
,
),
);
}
}
packages/flutter/lib/src/material/text_field.dart
View file @
2338576a
...
...
@@ -17,6 +17,7 @@ import 'ink_well.dart' show InteractiveInkFeature;
import
'input_decorator.dart'
;
import
'material.dart'
;
import
'material_localizations.dart'
;
import
'selectable_text.dart'
show
iOSHorizontalOffset
;
import
'text_selection.dart'
;
import
'theme.dart'
;
...
...
@@ -932,14 +933,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
cursorOpacityAnimates
=
true
;
cursorColor
??=
CupertinoTheme
.
of
(
context
).
primaryColor
;
cursorRadius
??=
const
Radius
.
circular
(
2.0
);
// An eyeballed value that moves the cursor slightly left of where it is
// rendered for text on Android so its positioning more accurately matches the
// native iOS text cursor positioning.
//
// This value is in device pixels, not logical pixels as is typically used
// throughout the codebase.
const
int
_iOSHorizontalOffset
=
-
2
;
cursorOffset
=
Offset
(
_iOSHorizontalOffset
/
MediaQuery
.
of
(
context
).
devicePixelRatio
,
0
);
cursorOffset
=
Offset
(
iOSHorizontalOffset
/
MediaQuery
.
of
(
context
).
devicePixelRatio
,
0
);
break
;
case
TargetPlatform
.
android
:
...
...
packages/flutter/lib/src/painting/text_painter.dart
View file @
2338576a
...
...
@@ -652,7 +652,7 @@ class TextPainter {
final
double
caretEnd
=
box
.
end
;
final
double
dx
=
box
.
direction
==
TextDirection
.
rtl
?
caretEnd
-
caretPrototype
.
width
:
caretEnd
;
return
Rect
.
fromLTRB
(
min
(
dx
,
width
),
box
.
top
,
min
(
dx
,
width
),
box
.
bottom
);
return
Rect
.
fromLTRB
(
min
(
dx
,
_paragraph
.
width
),
box
.
top
,
min
(
dx
,
_paragraph
.
width
),
box
.
bottom
);
}
return
null
;
}
...
...
@@ -694,7 +694,7 @@ class TextPainter {
final
TextBox
box
=
boxes
.
last
;
final
double
caretStart
=
box
.
start
;
final
double
dx
=
box
.
direction
==
TextDirection
.
rtl
?
caretStart
-
caretPrototype
.
width
:
caretStart
;
return
Rect
.
fromLTRB
(
min
(
dx
,
width
),
box
.
top
,
min
(
dx
,
width
),
box
.
bottom
);
return
Rect
.
fromLTRB
(
min
(
dx
,
_paragraph
.
width
),
box
.
top
,
min
(
dx
,
_paragraph
.
width
),
box
.
bottom
);
}
return
null
;
}
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
2338576a
...
...
@@ -157,6 +157,9 @@ class RenderEditable extends RenderBox {
this
.
onSelectionChanged
,
this
.
onCaretChanged
,
this
.
ignorePointer
=
false
,
bool
readOnly
=
false
,
bool
forceLine
=
true
,
TextWidthBasis
textWidthBasis
=
TextWidthBasis
.
parent
,
bool
obscureText
=
false
,
Locale
locale
,
double
cursorWidth
=
1.0
,
...
...
@@ -185,10 +188,13 @@ class RenderEditable extends RenderBox {
assert
(
textScaleFactor
!=
null
),
assert
(
offset
!=
null
),
assert
(
ignorePointer
!=
null
),
assert
(
textWidthBasis
!=
null
),
assert
(
paintCursorAboveText
!=
null
),
assert
(
obscureText
!=
null
),
assert
(
textSelectionDelegate
!=
null
),
assert
(
cursorWidth
!=
null
&&
cursorWidth
>=
0.0
),
assert
(
readOnly
!=
null
),
assert
(
forceLine
!=
null
),
assert
(
devicePixelRatio
!=
null
),
_textPainter
=
TextPainter
(
text:
text
,
...
...
@@ -197,6 +203,7 @@ class RenderEditable extends RenderBox {
textScaleFactor:
textScaleFactor
,
locale:
locale
,
strutStyle:
strutStyle
,
textWidthBasis:
textWidthBasis
,
),
_cursorColor
=
cursorColor
,
_backgroundCursorColor
=
backgroundCursorColor
,
...
...
@@ -216,7 +223,9 @@ class RenderEditable extends RenderBox {
_devicePixelRatio
=
devicePixelRatio
,
_startHandleLayerLink
=
startHandleLayerLink
,
_endHandleLayerLink
=
endHandleLayerLink
,
_obscureText
=
obscureText
{
_obscureText
=
obscureText
,
_readOnly
=
readOnly
,
_forceLine
=
forceLine
{
assert
(
_showCursor
!=
null
);
assert
(!
_showCursor
.
value
||
cursorColor
!=
null
);
this
.
hasFocus
=
hasFocus
??
false
;
...
...
@@ -245,12 +254,15 @@ class RenderEditable extends RenderBox {
/// The default value of this property is false.
bool
ignorePointer
;
/// Whether text is composed.
///
/// Text is composed when user selects it for editing. The [TextSpan] will have
/// children with composing effect and leave text property to be null.
@visibleForTesting
bool
get
isComposingText
=>
text
.
text
==
null
;
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
TextWidthBasis
get
textWidthBasis
=>
_textPainter
.
textWidthBasis
;
set
textWidthBasis
(
TextWidthBasis
value
)
{
assert
(
value
!=
null
);
if
(
_textPainter
.
textWidthBasis
==
value
)
return
;
_textPainter
.
textWidthBasis
=
value
;
markNeedsTextLayout
();
}
/// The pixel ratio of the current device.
///
...
...
@@ -444,7 +456,7 @@ class RenderEditable extends RenderBox {
if
(
leftArrow
&&
_extentOffset
>
2
)
{
final
TextSelection
textSelection
=
_selectWordAtOffset
(
TextPosition
(
offset:
_extentOffset
-
2
));
newOffset
=
textSelection
.
baseOffset
+
1
;
}
else
if
(
rightArrow
&&
_extentOffset
<
text
.
t
ext
.
length
-
2
)
{
}
else
if
(
rightArrow
&&
_extentOffset
<
text
.
t
oPlainText
()
.
length
-
2
)
{
final
TextSelection
textSelection
=
_selectWordAtOffset
(
TextPosition
(
offset:
_extentOffset
+
1
));
newOffset
=
textSelection
.
extentOffset
-
1
;
}
...
...
@@ -487,7 +499,7 @@ class RenderEditable extends RenderBox {
// case that the user wants to unhighlight some text.
if
(
position
.
offset
==
_extentOffset
)
{
if
(
downArrow
)
newOffset
=
text
.
t
ext
.
length
;
newOffset
=
text
.
t
oPlainText
()
.
length
;
else
if
(
upArrow
)
newOffset
=
0
;
_resetCursor
=
shift
;
...
...
@@ -554,16 +566,16 @@ class RenderEditable extends RenderBox {
case
_kCKeyCode:
if
(!
selection
.
isCollapsed
)
{
Clipboard
.
setData
(
ClipboardData
(
text:
selection
.
textInside
(
text
.
t
ext
)));
ClipboardData
(
text:
selection
.
textInside
(
text
.
t
oPlainText
()
)));
}
break
;
case
_kXKeyCode:
if
(!
selection
.
isCollapsed
)
{
Clipboard
.
setData
(
ClipboardData
(
text:
selection
.
textInside
(
text
.
t
ext
)));
ClipboardData
(
text:
selection
.
textInside
(
text
.
t
oPlainText
()
)));
textSelectionDelegate
.
textEditingValue
=
TextEditingValue
(
text:
selection
.
textBefore
(
text
.
t
ext
)
+
selection
.
textAfter
(
text
.
t
ext
),
text:
selection
.
textBefore
(
text
.
t
oPlainText
()
)
+
selection
.
textAfter
(
text
.
t
oPlainText
()
),
selection:
TextSelection
.
collapsed
(
offset:
selection
.
start
),
);
}
...
...
@@ -601,15 +613,15 @@ class RenderEditable extends RenderBox {
}
void
_handleDelete
()
{
if
(
selection
.
textAfter
(
text
.
t
ext
).
isNotEmpty
)
{
if
(
selection
.
textAfter
(
text
.
t
oPlainText
()
).
isNotEmpty
)
{
textSelectionDelegate
.
textEditingValue
=
TextEditingValue
(
text:
selection
.
textBefore
(
text
.
t
ext
)
+
selection
.
textAfter
(
text
.
t
ext
).
substring
(
1
),
text:
selection
.
textBefore
(
text
.
t
oPlainText
()
)
+
selection
.
textAfter
(
text
.
t
oPlainText
()
).
substring
(
1
),
selection:
TextSelection
.
collapsed
(
offset:
selection
.
start
),
);
}
else
{
textSelectionDelegate
.
textEditingValue
=
TextEditingValue
(
text:
selection
.
textBefore
(
text
.
t
ext
),
text:
selection
.
textBefore
(
text
.
t
oPlainText
()
),
selection:
TextSelection
.
collapsed
(
offset:
selection
.
start
),
);
}
...
...
@@ -758,6 +770,28 @@ class RenderEditable extends RenderBox {
markNeedsSemanticsUpdate
();
}
/// Whether this rendering object will take a full line regardless the text width.
bool
get
forceLine
=>
_forceLine
;
bool
_forceLine
=
false
;
set
forceLine
(
bool
value
)
{
assert
(
value
!=
null
);
if
(
_forceLine
==
value
)
return
;
_forceLine
=
value
;
markNeedsLayout
();
}
/// Whether this rendering object is read only.
bool
get
readOnly
=>
_readOnly
;
bool
_readOnly
=
false
;
set
readOnly
(
bool
value
)
{
assert
(
value
!=
null
);
if
(
_readOnly
==
value
)
return
;
_readOnly
=
value
;
markNeedsSemanticsUpdate
();
}
/// The maximum number of lines for the text to span, wrapping if necessary.
///
/// If this is 1 (the default), the text will not wrap, but will extend
...
...
@@ -983,6 +1017,8 @@ class RenderEditable extends RenderBox {
return
enableInteractiveSelection
??
!
obscureText
;
}
double
get
_caretMargin
=>
_kCaretGap
+
cursorWidth
;
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
...
...
@@ -995,7 +1031,8 @@ class RenderEditable extends RenderBox {
..
isMultiline
=
_isMultiline
..
textDirection
=
textDirection
..
isFocused
=
hasFocus
..
isTextField
=
true
;
..
isTextField
=
true
..
isReadOnly
=
readOnly
;
if
(
hasFocus
&&
selectionEnabled
)
config
.
onSetSelection
=
_handleSetSelection
;
...
...
@@ -1526,10 +1563,12 @@ class RenderEditable extends RenderBox {
assert
(
constraintWidth
!=
null
);
if
(
_textLayoutLastWidth
==
constraintWidth
)
return
;
final
double
caretMargin
=
_kCaretGap
+
cursorWidth
;
final
double
availableWidth
=
math
.
max
(
0.0
,
constraintWidth
-
caretMargin
);
final
double
availableWidth
=
math
.
max
(
0.0
,
constraintWidth
-
_caretMargin
);
final
double
maxWidth
=
_isMultiline
?
availableWidth
:
double
.
infinity
;
_textPainter
.
layout
(
minWidth:
availableWidth
,
maxWidth:
maxWidth
);
_textPainter
.
layout
(
minWidth:
forceLine
?
availableWidth
:
0
,
maxWidth:
maxWidth
,
);
_textLayoutLastWidth
=
constraintWidth
;
}
...
...
@@ -1566,8 +1605,10 @@ class RenderEditable extends RenderBox {
// though we currently don't use those here.
// See also RenderParagraph which has a similar issue.
final
Size
textPainterSize
=
_textPainter
.
size
;
size
=
Size
(
constraints
.
maxWidth
,
constraints
.
constrainHeight
(
_preferredHeight
(
constraints
.
maxWidth
)));
final
Size
contentSize
=
Size
(
textPainterSize
.
width
+
_kCaretGap
+
cursorWidth
,
textPainterSize
.
height
);
final
double
width
=
forceLine
?
constraints
.
maxWidth
:
constraints
.
constrainWidth
(
_textPainter
.
size
.
width
+
_caretMargin
);
size
=
Size
(
width
,
constraints
.
constrainHeight
(
_preferredHeight
(
constraints
.
maxWidth
)));
final
Size
contentSize
=
Size
(
textPainterSize
.
width
+
_caretMargin
,
textPainterSize
.
height
);
_maxScrollExtent
=
_getMaxScrollExtent
(
contentSize
);
offset
.
applyViewportDimension
(
_viewportExtent
);
offset
.
applyContentDimensions
(
0.0
,
_maxScrollExtent
);
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
2338576a
...
...
@@ -150,6 +150,29 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
);
}
/// Builds [TextSpan] from current editing value.
///
/// By default makes text in composing range appear as underlined.
/// Descendants can override this method to customize appearance of text.
TextSpan
buildTextSpan
({
TextStyle
style
,
bool
withComposing
})
{
if
(!
value
.
composing
.
isValid
||
!
withComposing
)
{
return
TextSpan
(
style:
style
,
text:
text
);
}
final
TextStyle
composingStyle
=
style
.
merge
(
const
TextStyle
(
decoration:
TextDecoration
.
underline
),
);
return
TextSpan
(
style:
style
,
children:
<
TextSpan
>[
TextSpan
(
text:
value
.
composing
.
textBefore
(
value
.
text
)),
TextSpan
(
style:
composingStyle
,
text:
value
.
composing
.
textInside
(
value
.
text
),
),
TextSpan
(
text:
value
.
composing
.
textAfter
(
value
.
text
)),
]);
}
/// The currently selected [text].
///
/// If the selection is collapsed, then this property gives the offset of the
...
...
@@ -288,6 +311,8 @@ class EditableText extends StatefulWidget {
this
.
maxLines
=
1
,
this
.
minLines
,
this
.
expands
=
false
,
this
.
forceLine
=
true
,
this
.
textWidthBasis
=
TextWidthBasis
.
parent
,
this
.
autofocus
=
false
,
bool
showCursor
,
this
.
showSelectionHandles
=
false
,
...
...
@@ -320,6 +345,7 @@ class EditableText extends StatefulWidget {
assert
(
autocorrect
!=
null
),
assert
(
showSelectionHandles
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
forceLine
!=
null
),
assert
(
style
!=
null
),
assert
(
cursorColor
!=
null
),
assert
(
cursorOpacityAnimates
!=
null
),
...
...
@@ -368,6 +394,9 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final
bool
obscureText
;
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
final
TextWidthBasis
textWidthBasis
;
/// {@template flutter.widgets.editableText.readOnly}
/// Whether the text can be changed.
///
...
...
@@ -378,6 +407,18 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final
bool
readOnly
;
/// Whether the text will take the full width regardless of the text width.
///
/// When this is set to false, the width will be based on text width, which
/// will also be affected by [textWidthBasis].
///
/// Defaults to true. Must not be null.
///
/// See also:
///
/// * [textWidthBasis], which controls the calculation of text width.
final
bool
forceLine
;
/// Whether to show selection handles.
///
/// When a selection is active, there will be two handles at each side of
...
...
@@ -396,7 +437,7 @@ class EditableText extends StatefulWidget {
///
/// See also:
///
/// * [showSelectionHandles], which controls the visibility of the selection handles.
.
/// * [showSelectionHandles], which controls the visibility of the selection handles.
/// {@endtemplate}
final
bool
showCursor
;
...
...
@@ -1622,6 +1663,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
showCursor:
EditableText
.
debugDeterministicCursor
?
ValueNotifier
<
bool
>(
widget
.
showCursor
)
:
_cursorVisibilityNotifier
,
forceLine:
widget
.
forceLine
,
readOnly:
widget
.
readOnly
,
hasFocus:
_hasFocus
,
maxLines:
widget
.
maxLines
,
minLines:
widget
.
minLines
,
...
...
@@ -1632,6 +1675,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textAlign:
widget
.
textAlign
,
textDirection:
_textDirection
,
locale:
widget
.
locale
,
textWidthBasis:
widget
.
textWidthBasis
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
offset:
offset
,
...
...
@@ -1657,33 +1701,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// By default makes text in composing range appear as underlined.
/// Descendants can override this method to customize appearance of text.
TextSpan
buildTextSpan
()
{
// Read only mode should not paint text composing.
if
(!
widget
.
obscureText
&&
_value
.
composing
.
isValid
&&
!
widget
.
readOnly
)
{
final
TextStyle
composingStyle
=
widget
.
style
.
merge
(
const
TextStyle
(
decoration:
TextDecoration
.
underline
),
);
return
TextSpan
(
style:
widget
.
style
,
children:
<
TextSpan
>[
TextSpan
(
text:
_value
.
composing
.
textBefore
(
_value
.
text
)),
TextSpan
(
style:
composingStyle
,
text:
_value
.
composing
.
textInside
(
_value
.
text
),
),
TextSpan
(
text:
_value
.
composing
.
textAfter
(
_value
.
text
)),
]);
}
String
text
=
_value
.
text
;
if
(
widget
.
obscureText
)
{
String
text
=
_value
.
text
;
text
=
RenderEditable
.
obscuringCharacter
*
text
.
length
;
final
int
o
=
_obscureShowCharTicksPending
>
0
?
_obscureLatestCharIndex
:
null
;
if
(
o
!=
null
&&
o
>=
0
&&
o
<
text
.
length
)
text
=
text
.
replaceRange
(
o
,
o
+
1
,
_value
.
text
.
substring
(
o
,
o
+
1
));
}
return
TextSpan
(
style:
widget
.
style
,
text:
text
);
}
// Read only mode should not paint text composing.
return
widget
.
controller
.
buildTextSpan
(
style:
widget
.
style
,
withComposing:
!
widget
.
readOnly
,
);
}
}
class
_Editable
extends
LeafRenderObjectWidget
{
...
...
@@ -1696,6 +1728,9 @@ class _Editable extends LeafRenderObjectWidget {
this
.
cursorColor
,
this
.
backgroundCursorColor
,
this
.
showCursor
,
this
.
forceLine
,
this
.
readOnly
,
this
.
textWidthBasis
,
this
.
hasFocus
,
this
.
maxLines
,
this
.
minLines
,
...
...
@@ -1730,6 +1765,8 @@ class _Editable extends LeafRenderObjectWidget {
final
LayerLink
endHandleLayerLink
;
final
Color
backgroundCursorColor
;
final
ValueNotifier
<
bool
>
showCursor
;
final
bool
forceLine
;
final
bool
readOnly
;
final
bool
hasFocus
;
final
int
maxLines
;
final
int
minLines
;
...
...
@@ -1741,6 +1778,7 @@ class _Editable extends LeafRenderObjectWidget {
final
TextDirection
textDirection
;
final
Locale
locale
;
final
bool
obscureText
;
final
TextWidthBasis
textWidthBasis
;
final
bool
autocorrect
;
final
ViewportOffset
offset
;
final
SelectionChangedHandler
onSelectionChanged
;
...
...
@@ -1763,6 +1801,8 @@ class _Editable extends LeafRenderObjectWidget {
endHandleLayerLink:
endHandleLayerLink
,
backgroundCursorColor:
backgroundCursorColor
,
showCursor:
showCursor
,
forceLine:
forceLine
,
readOnly:
readOnly
,
hasFocus:
hasFocus
,
maxLines:
maxLines
,
minLines:
minLines
,
...
...
@@ -1779,6 +1819,7 @@ class _Editable extends LeafRenderObjectWidget {
onCaretChanged:
onCaretChanged
,
ignorePointer:
rendererIgnoresPointer
,
obscureText:
obscureText
,
textWidthBasis:
textWidthBasis
,
cursorWidth:
cursorWidth
,
cursorRadius:
cursorRadius
,
cursorOffset:
cursorOffset
,
...
...
@@ -1797,6 +1838,8 @@ class _Editable extends LeafRenderObjectWidget {
..
startHandleLayerLink
=
startHandleLayerLink
..
endHandleLayerLink
=
endHandleLayerLink
..
showCursor
=
showCursor
..
forceLine
=
forceLine
..
readOnly
=
readOnly
..
hasFocus
=
hasFocus
..
maxLines
=
maxLines
..
minLines
=
minLines
...
...
@@ -1812,6 +1855,7 @@ class _Editable extends LeafRenderObjectWidget {
..
onSelectionChanged
=
onSelectionChanged
..
onCaretChanged
=
onCaretChanged
..
ignorePointer
=
rendererIgnoresPointer
..
textWidthBasis
=
textWidthBasis
..
obscureText
=
obscureText
..
cursorWidth
=
cursorWidth
..
cursorRadius
=
cursorRadius
...
...
packages/flutter/test/material/text_field_test.dart
View file @
2338576a
...
...
@@ -973,7 +973,7 @@ void main() {
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
// There should be no composing.
expect
(
renderEditable
.
isComposingText
,
false
);
expect
(
renderEditable
.
text
,
TextSpan
(
text:
'readonly'
,
style:
renderEditable
.
text
.
style
)
);
});
testWidgets
(
'Dynamically switching between read only and not read only should hide or show collapse cursor'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -3231,6 +3231,30 @@ void main() {
semantics
.
dispose
();
});
testWidgets
(
'Read only TextField identifies as read only text field in semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
maxLength:
10
,
readOnly:
true
,
),
),
),
),
);
expect
(
semantics
,
includesNodeWith
(
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isTextField
,
SemanticsFlag
.
isReadOnly
])
);
semantics
.
dispose
();
});
void
sendFakeKeyEvent
(
Map
<
String
,
dynamic
>
data
)
{
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
keyEvent
.
name
,
...
...
packages/flutter/test/widgets/selectable_text_test.dart
0 → 100644
View file @
2338576a
This source diff could not be displayed because it is too large. You can
view the blob
instead.
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