mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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==.
This commit is contained in:
parent
7ff3a50fe7
commit
9e744c5710
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user