Skip to content

Commit

Permalink
CupertinoTextField vertical alignment (#34723)
Browse files Browse the repository at this point in the history
CupertinoTextField now supports vertical alignment via the textAlignVertical parameter.
  • Loading branch information
justinmc committed Jul 3, 2019
1 parent 4cd12fc commit 2d2bb6b
Show file tree
Hide file tree
Showing 5 changed files with 572 additions and 108 deletions.
166 changes: 103 additions & 63 deletions packages/flutter/lib/src/cupertino/text_field.dart
Expand Up @@ -174,6 +174,7 @@ class CupertinoTextField extends StatefulWidget {
this.style,
this.strutStyle,
this.textAlign = TextAlign.start,
this.textAlignVertical,
this.readOnly = false,
this.showCursor,
this.autofocus = false,
Expand Down Expand Up @@ -324,6 +325,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.textAlign}
final TextAlign textAlign;

/// {@macro flutter.material.inputDecorator.textAlignVertical}
final TextAlignVertical textAlignVertical;

/// {@macro flutter.widgets.editableText.readOnly}
final bool readOnly;

Expand Down Expand Up @@ -491,10 +495,13 @@ class CupertinoTextField extends StatefulWidget {
properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
}
}

class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin {
final GlobalKey _clearGlobalKey = GlobalKey();
final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();

TextEditingController _controller;
Expand Down Expand Up @@ -584,6 +591,18 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
}

void _handleSingleTapUp(TapUpDetails details) {
// Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger
// this handler. If the the clear button widget recognizes the up event,
// then do not handle it.
if (_clearGlobalKey.currentContext != null) {
final RenderBox renderBox = _clearGlobalKey.currentContext.findRenderObject();
final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
if(renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
return;
}
}

if (widget.selectionEnabled) {
_renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
}
Expand Down Expand Up @@ -719,16 +738,31 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
);
}

// True if any surrounding decoration widgets will be shown.
bool get _hasDecoration {
return widget.placeholder != null ||
widget.clearButtonMode != OverlayVisibilityMode.never ||
widget.prefix != null ||
widget.suffix != null;
}

// Provide default behavior if widget.textAlignVertical is not set.
// CupertinoTextField has top alignment by default, unless it has decoration
// like a prefix or suffix, in which case it's aligned to the center.
TextAlignVertical get _textAlignVertical {
if (widget.textAlignVertical != null) {
return widget.textAlignVertical;
}
return _hasDecoration ? TextAlignVertical.center : TextAlignVertical.top;
}

Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) {
assert(editableText != null);
assert(textStyle != null);
assert(placeholderStyle != null);
// If there are no surrounding widgets, just return the core editable text
// part.
if (widget.placeholder == null &&
widget.clearButtonMode == OverlayVisibilityMode.never &&
widget.prefix == null &&
widget.suffix == null) {
if (!_hasDecoration) {
return editableText;
}

Expand Down Expand Up @@ -776,6 +810,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
} else if (_showClearButton(text)) {
rowChildren.add(
GestureDetector(
key: _clearGlobalKey,
onTap: widget.enabled ?? true ? () {
// Special handle onChanged for ClearButton
// Also call onChanged when the clear button is tapped.
Expand Down Expand Up @@ -825,64 +860,50 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
? widget.decoration
: widget.decoration?.copyWith(color: widget.decoration?.color ?? disabledColor);

final Widget paddedEditable = TextSelectionGestureDetector(
onTapDown: _handleTapDown,
onForcePressStart: _handleForcePressStarted,
onForcePressEnd: _handleForcePressEnded,
onSingleTapUp: _handleSingleTapUp,
onSingleLongTapStart: _handleSingleLongTapStart,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
onDragSelectionEnd: _handleMouseDragSelectionEnd,
behavior: HitTestBehavior.translucent,
child: Padding(
padding: widget.padding,
child: RepaintBoundary(
child: EditableText(
key: _editableTextKey,
controller: controller,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
focusNode: _effectiveFocusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: textStyle,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: _kSelectionHighlightColor,
selectionControls: widget.selectionEnabled
? cupertinoTextSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
inputFormatters: formatters,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
cursorColor: cursorColor,
cursorOpacityAnimates: true,
cursorOffset: cursorOffset,
paintCursorAboveText: true,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
),
final Widget paddedEditable = Padding(
padding: widget.padding,
child: RepaintBoundary(
child: EditableText(
key: _editableTextKey,
controller: controller,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
focusNode: _effectiveFocusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: textStyle,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: _kSelectionHighlightColor,
selectionControls: widget.selectionEnabled
? cupertinoTextSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
inputFormatters: formatters,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
cursorColor: cursorColor,
cursorOpacityAnimates: true,
cursorOffset: cursorOffset,
paintCursorAboveText: true,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
),
),
);
Expand All @@ -898,7 +919,26 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
ignoring: !enabled,
child: Container(
decoration: effectiveDecoration,
child: _addTextDependentAttachments(paddedEditable, textStyle, placeholderStyle),
child: TextSelectionGestureDetector(
onTapDown: _handleTapDown,
onForcePressStart: _handleForcePressStarted,
onForcePressEnd: _handleForcePressEnded,
onSingleTapUp: _handleSingleTapUp,
onSingleLongTapStart: _handleSingleLongTapStart,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
onDragSelectionEnd: _handleMouseDragSelectionEnd,
behavior: HitTestBehavior.translucent,
child: Align(
alignment: Alignment(-1.0, _textAlignVertical.y),
widthFactor: 1.0,
heightFactor: 1.0,
child: _addTextDependentAttachments(paddedEditable, textStyle, placeholderStyle),
),
),
),
),
);
Expand Down
39 changes: 0 additions & 39 deletions packages/flutter/lib/src/material/input_decorator.dart
Expand Up @@ -3576,42 +3576,3 @@ class InputDecorationTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<bool>('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint));
}
}

/// The vertical alignment of text within an input.
///
/// A single [y] value that can range from -1.0 to 1.0. -1.0 aligns to the top
/// of the input so that the top of the first line of text fits within the input
/// and its padding. 0.0 aligns to the center of the input. 1.0 aligns so that
/// the bottom of the last line of text aligns with the bottom interior edge of
/// the input.
///
/// See also:
///
/// * [TextField.textAlignVertical], which is passed on to the [InputDecorator].
/// * [InputDecorator.textAlignVertical], which defines the alignment of
/// prefix, input, and suffix, within the [InputDecorator].
class TextAlignVertical {
/// Construct TextAlignVertical from any given y value.
const TextAlignVertical({
@required this.y,
}) : assert(y != null),
assert(y >= -1.0 && y <= 1.0);

/// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost
/// locations of the top and bottom of the input text box.
final double y;

/// Aligns a TextField's input Text with the topmost location within the
/// TextField.
static const TextAlignVertical top = TextAlignVertical(y: -1.0);
/// Aligns a TextField's input Text to the center of the TextField.
static const TextAlignVertical center = TextAlignVertical(y: 0.0);
/// Aligns a TextField's input Text with the bottommost location within the
/// TextField.
static const TextAlignVertical bottom = TextAlignVertical(y: 1.0);

@override
String toString() {
return '$runtimeType(y: $y)';
}
}
41 changes: 41 additions & 0 deletions packages/flutter/lib/src/painting/alignment.dart
Expand Up @@ -633,3 +633,44 @@ class _MixedAlignment extends AlignmentGeometry {
return null;
}
}

/// The vertical alignment of text within an input box.
///
/// A single [y] value that can range from -1.0 to 1.0. -1.0 aligns to the top
/// of an input box so that the top of the first line of text fits within the
/// box and its padding. 0.0 aligns to the center of the box. 1.0 aligns so that
/// the bottom of the last line of text aligns with the bottom interior edge of
/// the input box.
///
/// See also:
///
/// * [TextField.textAlignVertical], which is passed on to the [InputDecorator].
/// * [CupertinoTextField.textAlignVertical], which behaves in the same way as
/// the parameter in TextField.
/// * [InputDecorator.textAlignVertical], which defines the alignment of
/// prefix, input, and suffix within an [InputDecorator].
class TextAlignVertical {
/// Creates a TextAlignVertical from any y value between -1.0 and 1.0.
const TextAlignVertical({
@required this.y,
}) : assert(y != null),
assert(y >= -1.0 && y <= 1.0);

/// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost
/// locations of the top and bottom of the input box.
final double y;

/// Aligns a TextField's input Text with the topmost location within a
/// TextField's input box.
static const TextAlignVertical top = TextAlignVertical(y: -1.0);
/// Aligns a TextField's input Text to the center of the TextField.
static const TextAlignVertical center = TextAlignVertical(y: 0.0);
/// Aligns a TextField's input Text with the bottommost location within a
/// TextField.
static const TextAlignVertical bottom = TextAlignVertical(y: 1.0);

@override
String toString() {
return '$runtimeType(y: $y)';
}
}
6 changes: 3 additions & 3 deletions packages/flutter/lib/src/rendering/editable.dart
Expand Up @@ -1252,15 +1252,15 @@ class RenderEditable extends RenderBox {
double get preferredLineHeight => _textPainter.preferredLineHeight;

double _preferredHeight(double width) {
// Lock height to maxLines if needed
// Lock height to maxLines if needed.
final bool lockedMax = maxLines != null && minLines == null;
final bool lockedBoth = minLines != null && minLines == maxLines;
final bool singleLine = maxLines == 1;
if (singleLine || lockedMax || lockedBoth) {
return preferredLineHeight * maxLines;
}

// Clamp height to minLines or maxLines if needed
// Clamp height to minLines or maxLines if needed.
final bool minLimited = minLines != null && minLines > 1;
final bool maxLimited = maxLines != null;
if (minLimited || maxLimited) {
Expand All @@ -1273,7 +1273,7 @@ class RenderEditable extends RenderBox {
}
}

// Set the height based on the content
// Set the height based on the content.
if (width == double.infinity) {
final String text = _textPainter.text.toPlainText();
int lines = 1;
Expand Down

0 comments on commit 2d2bb6b

Please sign in to comment.