Unverified Commit 9e744c57 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Implement VisualDensity for text fields. (#51438)

This implements VisualDensity changes for text fields*. By default, the layout of the text field does not change.

If the ThemeData.visualDensity is set to a value other than zero, then the density of the UI will increase or decrease. See the VisualDensity docs for more information.

(*In reality, the changes are on the InputDecorator class, not on the text field.)

I also fixed a problem that I think I found with _Decoration where it doesn't compare isDense or isCollapsed as part of its operator==.
parent 7ff3a50f
......@@ -401,11 +401,13 @@ class _ControlTile extends StatelessWidget {
class _MyHomePageState extends State<MyHomePage> {
static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final OptionModel _model = OptionModel();
TextEditingController textController;
@override
void initState() {
super.initState();
_model.addListener(_modelChanged);
textController = TextEditingController();
}
@override
......@@ -430,8 +432,36 @@ class _MyHomePageState extends State<MyHomePage> {
primarySwatch: m2Swatch,
);
final Widget label = Text(_model.rtl ? 'اضغط علي' : 'Press Me');
textController.text = _model.rtl ? 'يعتمد القرار الجيد على المعرفة وليس على الأرقام.' : 'A good decision is based on knowledge and not on numbers.';
final List<Widget> tiles = <Widget>[
_ControlTile(
label: _model.rtl ? 'حقل النص' : 'Text Field',
child: SizedBox(
width: 300,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: textController,
decoration: const InputDecoration(
hintText: 'Hint',
helperText: 'Helper',
labelText: 'Label',
border: OutlineInputBorder(),
),
),
TextField(
controller: textController,
),
TextField(
controller: textController,
maxLines: 3,
),
],
),
),
),
_ControlTile(
label: _model.rtl ? 'رقائق' : 'Chips',
child: Column(
......
......@@ -13,6 +13,7 @@ import 'colors.dart';
import 'constants.dart';
import 'input_border.dart';
import 'theme.dart';
import 'theme_data.dart';
const Duration _kTransitionDuration = Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
......@@ -496,6 +497,9 @@ class _Decoration {
@required this.floatingLabelProgress,
this.border,
this.borderGap,
this.alignLabelWithHint,
this.isDense,
this.visualDensity,
this.icon,
this.input,
this.label,
......@@ -507,8 +511,6 @@ class _Decoration {
this.helperError,
this.counter,
this.container,
this.alignLabelWithHint,
this.isDense,
}) : assert(contentPadding != null),
assert(isCollapsed != null),
assert(floatingLabelHeight != null),
......@@ -522,6 +524,7 @@ class _Decoration {
final _InputBorderGap borderGap;
final bool alignLabelWithHint;
final bool isDense;
final VisualDensity visualDensity;
final Widget icon;
final Widget input;
final Widget label;
......@@ -542,10 +545,14 @@ class _Decoration {
return false;
return other is _Decoration
&& other.contentPadding == contentPadding
&& other.isCollapsed == isCollapsed
&& other.floatingLabelHeight == floatingLabelHeight
&& other.floatingLabelProgress == floatingLabelProgress
&& other.border == border
&& other.borderGap == borderGap
&& other.alignLabelWithHint == alignLabelWithHint
&& other.isDense == isDense
&& other.visualDensity == visualDensity
&& other.icon == icon
&& other.input == input
&& other.label == label
......@@ -556,8 +563,7 @@ class _Decoration {
&& other.suffixIcon == suffixIcon
&& other.helperError == helperError
&& other.counter == counter
&& other.container == container
&& other.alignLabelWithHint == alignLabelWithHint;
&& other.container == container;
}
@override
......@@ -568,6 +574,9 @@ class _Decoration {
floatingLabelProgress,
border,
borderGap,
alignLabelWithHint,
isDense,
visualDensity,
icon,
input,
label,
......@@ -579,7 +588,6 @@ class _Decoration {
helperError,
counter,
container,
alignLabelWithHint,
);
}
}
......@@ -1045,6 +1053,7 @@ class _RenderDecoration extends RenderBox {
);
// Calculate the height of the input text container.
final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment;
final double prefixIconHeight = prefixIcon == null ? 0 : prefixIcon.size.height;
final double suffixIconHeight = suffixIcon == null ? 0 : suffixIcon.size.height;
final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight);
......@@ -1055,12 +1064,13 @@ class _RenderDecoration extends RenderBox {
+ fixAboveInput
+ inputHeight
+ fixBelowInput
+ contentPadding.bottom,
+ contentPadding.bottom
+ densityOffset.dy,
);
final double minContainerHeight = decoration.isDense || expands
? 0.0
: kMinInteractiveDimension;
final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight;
: kMinInteractiveDimension + densityOffset.dy;
final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight + densityOffset.dy;
final double containerHeight = expands
? maxContainerHeight
: math.min(math.max(contentHeight, minContainerHeight), maxContainerHeight);
......@@ -1096,7 +1106,7 @@ class _RenderDecoration extends RenderBox {
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
final double maxVerticalOffset = maxContentHeight - alignableHeight;
final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor;
final double inputBaseline = topInputBaseline + textAlignVerticalOffset;
final double inputBaseline = topInputBaseline + textAlignVerticalOffset + densityOffset.dy / 2.0;
// The three main alignments for the baseline when an outline is present are
//
......@@ -2201,9 +2211,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
widthFactor: 1.0,
heightFactor: 1.0,
child: ConstrainedBox(
constraints: decoration.prefixIconConstraints ?? const BoxConstraints(
minWidth: kMinInteractiveDimension,
minHeight: kMinInteractiveDimension,
constraints: decoration.prefixIconConstraints ?? themeData.visualDensity.effectiveConstraints(
const BoxConstraints(
minWidth: kMinInteractiveDimension,
minHeight: kMinInteractiveDimension,
),
),
child: IconTheme.merge(
data: IconThemeData(
......@@ -2220,9 +2232,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
widthFactor: 1.0,
heightFactor: 1.0,
child: ConstrainedBox(
constraints: decoration.suffixIconConstraints ?? const BoxConstraints(
minWidth: kMinInteractiveDimension,
minHeight: kMinInteractiveDimension,
constraints: decoration.suffixIconConstraints ?? themeData.visualDensity.effectiveConstraints(
const BoxConstraints(
minWidth: kMinInteractiveDimension,
minHeight: kMinInteractiveDimension,
),
),
child: IconTheme.merge(
data: IconThemeData(
......@@ -2300,11 +2314,12 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
floatingLabelProgress: _floatingLabelController.value,
border: border,
borderGap: _borderGap,
alignLabelWithHint: decoration.alignLabelWithHint,
isDense: decoration.isDense,
visualDensity: themeData.visualDensity,
icon: icon,
input: widget.child,
label: label,
alignLabelWithHint: decoration.alignLabelWithHint,
isDense: decoration.isDense,
hint: hint,
prefix: prefix,
suffix: suffix,
......
......@@ -21,6 +21,7 @@ Widget buildInputDecorator({
bool isHovering = false,
TextStyle baseStyle,
TextAlignVertical textAlignVertical,
VisualDensity visualDensity,
Widget child = const Text(
'text',
style: TextStyle(fontFamily: 'Ahem', fontSize: 16.0),
......@@ -33,6 +34,7 @@ Widget buildInputDecorator({
return Theme(
data: Theme.of(context).copyWith(
inputDecorationTheme: inputDecorationTheme,
visualDensity: visualDensity,
),
child: Align(
alignment: Alignment.topLeft,
......@@ -1495,6 +1497,196 @@ void main() {
expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 16.0);
});
testWidgets('InputDecorator respects reduced theme visualDensity', (WidgetTester tester) async {
// Label is visible, hint is not (opacity 0.0).
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
visualDensity: const VisualDensity(horizontal: -2.0, vertical: -2.0),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// The label is not floating so it's vertically centered.
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0));
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 16.0);
expect(tester.getBottomLeft(find.text('label')).dy, 32.0);
expect(getOpacity(tester, 'hint'), 0.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 1.0);
// Label moves upwards, hint is visible (opacity 1.0).
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: true,
visualDensity: const VisualDensity(horizontal: -2.0, vertical: -2.0),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// The hint's opacity animates from 0.0 to 1.0.
// The animation's duration is 200ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity100ms = getOpacity(tester, 'hint');
expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0));
}
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0));
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(tester.getTopLeft(find.text('hint')).dy, 24.0);
expect(tester.getBottomLeft(find.text('hint')).dy, 40.0);
expect(getOpacity(tester, 'hint'), 1.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 2.0);
await tester.pumpWidget(
buildInputDecorator(
isEmpty: false,
isFocused: true,
visualDensity: const VisualDensity(horizontal: -2.0, vertical: -2.0),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// The hint's opacity animates from 1.0 to 0.0.
// The animation's duration is 200ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity100ms = getOpacity(tester, 'hint');
expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms));
}
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0));
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(tester.getTopLeft(find.text('hint')).dy, 24.0);
expect(tester.getBottomLeft(find.text('hint')).dy, 40.0);
expect(getOpacity(tester, 'hint'), 0.0);
expect(getBorderBottom(tester), 48.0);
expect(getBorderWeight(tester), 2.0);
});
testWidgets('InputDecorator respects increased theme visualDensity', (WidgetTester tester) async {
// Label is visible, hint is not (opacity 0.0).
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
visualDensity: const VisualDensity(horizontal: 2.0, vertical: 2.0),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// The label is not floating so it's vertically centered.
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 64.0));
expect(tester.getTopLeft(find.text('text')).dy, 32.0);
expect(tester.getBottomLeft(find.text('text')).dy, 48.0);
expect(tester.getTopLeft(find.text('label')).dy, 24.0);
expect(tester.getBottomLeft(find.text('label')).dy, 40.0);
expect(getOpacity(tester, 'hint'), 0.0);
expect(getBorderBottom(tester), 64.0);
expect(getBorderWeight(tester), 1.0);
// Label moves upwards, hint is visible (opacity 1.0).
await tester.pumpWidget(
buildInputDecorator(
isEmpty: true,
isFocused: true,
visualDensity: const VisualDensity(horizontal: 2.0, vertical: 2.0),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// The hint's opacity animates from 0.0 to 1.0.
// The animation's duration is 200ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity100ms = getOpacity(tester, 'hint');
expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0));
}
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 64.0));
expect(tester.getTopLeft(find.text('text')).dy, 32.0);
expect(tester.getBottomLeft(find.text('text')).dy, 48.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(tester.getTopLeft(find.text('hint')).dy, 32.0);
expect(tester.getBottomLeft(find.text('hint')).dy, 48.0);
expect(getOpacity(tester, 'hint'), 1.0);
expect(getBorderBottom(tester), 64.0);
expect(getBorderWeight(tester), 2.0);
await tester.pumpWidget(
buildInputDecorator(
isEmpty: false,
isFocused: true,
visualDensity: const VisualDensity(horizontal: 2.0, vertical: 2.0),
decoration: const InputDecoration(
labelText: 'label',
hintText: 'hint',
),
),
);
// The hint's opacity animates from 1.0 to 0.0.
// The animation's duration is 200ms.
{
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity50ms = getOpacity(tester, 'hint');
expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0));
await tester.pump(const Duration(milliseconds: 50));
final double hintOpacity100ms = getOpacity(tester, 'hint');
expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms));
}
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 64.0));
expect(tester.getTopLeft(find.text('text')).dy, 32.0);
expect(tester.getBottomLeft(find.text('text')).dy, 48.0);
expect(tester.getTopLeft(find.text('label')).dy, 12.0);
expect(tester.getBottomLeft(find.text('label')).dy, 24.0);
expect(tester.getTopLeft(find.text('hint')).dy, 32.0);
expect(tester.getBottomLeft(find.text('hint')).dy, 48.0);
expect(getOpacity(tester, 'hint'), 0.0);
expect(getBorderBottom(tester), 64.0);
expect(getBorderWeight(tester), 2.0);
});
testWidgets('prefix/suffix icons increase height of decoration when larger than 48 by 48', (WidgetTester tester) async {
const Key prefixKey = Key('prefix');
await tester.pumpWidget(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment