diff --git a/examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart b/examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart new file mode 100644 index 000000000000..4d409f51aa8f --- /dev/null +++ b/examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart @@ -0,0 +1,75 @@ +// 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 'package:flutter/material.dart'; + +/// Flutter code sample for [ChipAttributes.avatarBoxConstraints]. + +void main() => runApp(const AvatarBoxConstraintsApp()); + +class AvatarBoxConstraintsApp extends StatelessWidget { + const AvatarBoxConstraintsApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: AvatarBoxConstraintsExample(), + ), + ), + ); + } +} + +class AvatarBoxConstraintsExample extends StatelessWidget { + const AvatarBoxConstraintsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RawChip( + avatarBoxConstraints: BoxConstraints.tightForFinite(), + avatar: Icon(Icons.star), + label: SizedBox( + width: 150, + child: Text( + 'One line text.', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + SizedBox(height: 10), + RawChip( + avatarBoxConstraints: BoxConstraints.tightForFinite(), + avatar: Icon(Icons.star), + label: SizedBox( + width: 150, + child: Text( + 'This text will wrap into two lines.', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + SizedBox(height: 10), + RawChip( + avatarBoxConstraints: BoxConstraints.tightForFinite(), + avatar: Icon(Icons.star), + label: SizedBox( + width: 150, + child: Text( + 'This is a very long text that will wrap into three lines.', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } +} diff --git a/examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart b/examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart new file mode 100644 index 000000000000..efff540f876f --- /dev/null +++ b/examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart @@ -0,0 +1,75 @@ +// 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 'package:flutter/material.dart'; + +/// Flutter code sample for [DeletableChipAttributes.deleteIconBoxConstraints]. + +void main() => runApp(const DeleteIconBoxConstraintsApp()); + +class DeleteIconBoxConstraintsApp extends StatelessWidget { + const DeleteIconBoxConstraintsApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: DeleteIconBoxConstraintsExample(), + ), + ), + ); + } +} + +class DeleteIconBoxConstraintsExample extends StatelessWidget { + const DeleteIconBoxConstraintsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RawChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + onDeleted: () {}, + label: const SizedBox( + width: 150, + child: Text( + 'One line text.', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(height: 10), + RawChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + onDeleted: () {}, + label: const SizedBox( + width: 150, + child: Text( + 'This text will wrap into two lines.', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(height: 10), + RawChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + onDeleted: () {}, + label: const SizedBox( + width: 150, + child: Text( + 'This is a very long text that will wrap into three lines.', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } +} diff --git a/examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart b/examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart new file mode 100644 index 000000000000..e28dedf1b672 --- /dev/null +++ b/examples/api/test/material/chip/chip_attributes.avatar_box_constraints.0_test.dart @@ -0,0 +1,55 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/chip/chip_attributes.avatar_box_constraints.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('RawChip.avatarBoxConstraints updates avatar size constraints', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double padding = 8.0; + + await tester.pumpWidget( + const example.AvatarBoxConstraintsApp(), + ); + + expect(tester.getSize(find.byType(RawChip).at(0)).width, equals(202.0)); + expect(tester.getSize(find.byType(RawChip).at(0)).height, equals(58.0)); + + Offset chipTopLeft = tester.getTopLeft(find.byWidget(tester.widget( + find.descendant( + of: find.byType(RawChip).at(0), + matching: find.byType(Material), + ), + ))); + Offset avatarCenter = tester.getCenter(find.byIcon(Icons.star).at(0)); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + + expect(tester.getSize(find.byType(RawChip).at(1)).width, equals(202.0)); + expect(tester.getSize(find.byType(RawChip).at(1)).height, equals(78.0)); + + chipTopLeft = tester.getTopLeft(find.byWidget(tester.widget( + find.descendant( + of: find.byType(RawChip).at(1), + matching: find.byType(Material), + ), + ))); + avatarCenter = tester.getCenter(find.byIcon(Icons.star).at(1)); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + + expect(tester.getSize(find.byType(RawChip).at(2)).width, equals(202.0)); + expect(tester.getSize(find.byType(RawChip).at(2)).height, equals(78.0)); + + chipTopLeft = tester.getTopLeft(find.byWidget(tester.widget( + find.descendant( + of: find.byType(RawChip).at(2), + matching: find.byType(Material), + ), + ))); + avatarCenter = tester.getCenter(find.byIcon(Icons.star).at(2)); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + }); +} diff --git a/examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart b/examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart new file mode 100644 index 000000000000..8ffe215fcb58 --- /dev/null +++ b/examples/api/test/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0_test.dart @@ -0,0 +1,55 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('RawChip.deleteIconBoxConstraints updates delete icon size constraints', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double padding = 8.0; + + await tester.pumpWidget( + const example.DeleteIconBoxConstraintsApp(), + ); + + expect(tester.getSize(find.byType(RawChip).at(0)).width, equals(202.0)); + expect(tester.getSize(find.byType(RawChip).at(0)).height, equals(58.0)); + + Offset chipToRight = tester.getTopRight(find.byWidget(tester.widget( + find.descendant( + of: find.byType(RawChip).at(0), + matching: find.byType(Material), + ), + ))); + Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.cancel).at(0)); + expect(chipToRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + + expect(tester.getSize(find.byType(RawChip).at(1)).width, equals(202.0)); + expect(tester.getSize(find.byType(RawChip).at(1)).height, equals(78.0)); + + chipToRight = tester.getTopRight(find.byWidget(tester.widget( + find.descendant( + of: find.byType(RawChip).at(1), + matching: find.byType(Material), + ), + ))); + deleteIconCenter = tester.getCenter(find.byIcon(Icons.cancel).at(1)); + expect(chipToRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + + expect(tester.getSize(find.byType(RawChip).at(2)).width, equals(202.0)); + expect(tester.getSize(find.byType(RawChip).at(2)).height, equals(78.0)); + + chipToRight = tester.getTopRight(find.byWidget(tester.widget( + find.descendant( + of: find.byType(RawChip).at(2), + matching: find.byType(Material), + ), + ))); + deleteIconCenter = tester.getCenter(find.byIcon(Icons.cancel).at(2)); + expect(chipToRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + }); +} diff --git a/packages/flutter/lib/src/material/action_chip.dart b/packages/flutter/lib/src/material/action_chip.dart index 870f4d0e1a6a..31e85ded65ec 100644 --- a/packages/flutter/lib/src/material/action_chip.dart +++ b/packages/flutter/lib/src/material/action_chip.dart @@ -109,6 +109,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.shadowColor, this.surfaceTintColor, this.iconTheme, + this.avatarBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -143,6 +144,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.shadowColor, this.surfaceTintColor, this.iconTheme, + this.avatarBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -191,6 +193,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip final Color? surfaceTintColor; @override final IconThemeData? iconTheme; + @override + final BoxConstraints? avatarBoxConstraints; @override bool get isEnabled => onPressed != null; @@ -228,6 +232,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, iconTheme: iconTheme, + avatarBoxConstraints: avatarBoxConstraints, ); } } diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index 15979d1c288a..ea269dcc48cb 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -225,6 +225,22 @@ abstract interface class ChipAttributes { /// color and a size of 18.0 is used when the chip is disabled. Otherwise, /// it defaults to null. IconThemeData? get iconTheme; + + /// Optional size constraints for the avatar. + /// + /// When unspecified, defaults to a minimum size of chip height or label height + /// (whichever is greater) and a padding of 8.0 pixels on all sides. + /// + /// The default constraints ensure that the avatar is accessible. + /// Specifying this parameter enables creation of avatar smaller than + /// the minimum size, but it is not recommended. + /// + /// {@tool dartpad} + /// This sample shows how to use [avatarBoxConstraints] to adjust avatar size constraints + /// + /// ** See code in examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart ** + /// {@end-tool} + BoxConstraints? get avatarBoxConstraints; } /// An interface for Material Design chips that can be deleted. @@ -283,6 +299,23 @@ abstract interface class DeletableChipAttributes { /// /// If the chip is disabled, the delete button tooltip will not be shown. String? get deleteButtonTooltipMessage; + + /// Optional size constraints for the delete icon. + /// + /// When unspecified, defaults to a minimum size of chip height or label height + /// (whichever is greater) and a padding of 8.0 pixels on all sides. + /// + /// The default constraints ensure that the delete icon is accessible. + /// Specifying this parameter enables creation of delete icon smaller than + /// the minimum size, but it is not recommended. + /// + /// {@tool dartpad} + /// This sample shows how to use [deleteIconBoxConstraints] to adjust delete icon + /// size constraints. + /// + /// ** See code in examples/api/lib/material/chip/deletable_chip_attributes.delete_icon_box_constraints.0.dart ** + /// {@end-tool} + BoxConstraints? get deleteIconBoxConstraints; } /// An interface for Material Design chips that can have check marks. @@ -590,6 +623,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri this.shadowColor, this.surfaceTintColor, this.iconTheme, + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }) : assert(elevation == null || elevation >= 0.0); @override @@ -636,6 +671,10 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri final Color? surfaceTintColor; @override final IconThemeData? iconTheme; + @override + final BoxConstraints? avatarBoxConstraints; + @override + final BoxConstraints? deleteIconBoxConstraints; @override Widget build(BuildContext context) { @@ -664,6 +703,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, iconTheme: iconTheme, + avatarBoxConstraints: avatarBoxConstraints, + deleteIconBoxConstraints: deleteIconBoxConstraints, ); } } @@ -751,6 +792,8 @@ class RawChip extends StatefulWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), deleteIcon = deleteIcon ?? _kDefaultDeleteIcon; @@ -830,6 +873,10 @@ class RawChip extends StatefulWidget final Color? checkmarkColor; @override final ShapeBorder avatarBorder; + @override + final BoxConstraints? avatarBoxConstraints; + @override + final BoxConstraints? deleteIconBoxConstraints; /// If set, this indicates that the chip should be disabled if all of the /// tap callbacks ([onSelected], [onPressed]) are null. @@ -1205,6 +1252,10 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid final IconThemeData? iconTheme = widget.iconTheme ?? chipTheme.iconTheme ?? chipDefaults.iconTheme; + final BoxConstraints? avatarBoxConstraints = widget.avatarBoxConstraints + ?? chipTheme.avatarBoxConstraints; + final BoxConstraints? deleteIconBoxConstraints = widget.deleteIconBoxConstraints + ?? chipTheme.deleteIconBoxConstraints; final TextStyle effectiveLabelStyle = labelStyle.merge(widget.labelStyle); final Color? resolvedLabelColor = MaterialStateProperty.resolveAs(effectiveLabelStyle.color, materialStates); @@ -1300,6 +1351,8 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid deleteDrawerAnimation: deleteDrawerAnimation, isEnabled: widget.isEnabled, avatarBorder: widget.avatarBorder, + avatarBoxConstraints: avatarBoxConstraints, + deleteIconBoxConstraints: deleteIconBoxConstraints, ), ), ), @@ -1423,6 +1476,8 @@ class _ChipRenderWidget extends SlottedMultiChildRenderObjectWidget<_ChipSlot, R required this.deleteDrawerAnimation, required this.enableAnimation, this.avatarBorder, + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }); final _ChipRenderTheme theme; @@ -1433,6 +1488,8 @@ class _ChipRenderWidget extends SlottedMultiChildRenderObjectWidget<_ChipSlot, R final Animation deleteDrawerAnimation; final Animation enableAnimation; final ShapeBorder? avatarBorder; + final BoxConstraints? avatarBoxConstraints; + final BoxConstraints? deleteIconBoxConstraints; @override Iterable<_ChipSlot> get slots => _ChipSlot.values; @@ -1457,7 +1514,9 @@ class _ChipRenderWidget extends SlottedMultiChildRenderObjectWidget<_ChipSlot, R ..avatarDrawerAnimation = avatarDrawerAnimation ..deleteDrawerAnimation = deleteDrawerAnimation ..enableAnimation = enableAnimation - ..avatarBorder = avatarBorder; + ..avatarBorder = avatarBorder + ..avatarBoxConstraints = avatarBoxConstraints + ..deleteIconBoxConstraints = deleteIconBoxConstraints; } @override @@ -1472,6 +1531,8 @@ class _ChipRenderWidget extends SlottedMultiChildRenderObjectWidget<_ChipSlot, R deleteDrawerAnimation: deleteDrawerAnimation, enableAnimation: enableAnimation, avatarBorder: avatarBorder, + avatarBoxConstraints: avatarBoxConstraints, + deleteIconBoxConstraints: deleteIconBoxConstraints, ); } } @@ -1557,8 +1618,12 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip required this.deleteDrawerAnimation, required this.enableAnimation, this.avatarBorder, + BoxConstraints? avatarBoxConstraints, + BoxConstraints? deleteIconBoxConstraints, }) : _theme = theme, - _textDirection = textDirection; + _textDirection = textDirection, + _avatarBoxConstraints = avatarBoxConstraints, + _deleteIconBoxConstraints = deleteIconBoxConstraints; bool? value; bool? isEnabled; @@ -1594,6 +1659,26 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip markNeedsLayout(); } + BoxConstraints? get avatarBoxConstraints => _avatarBoxConstraints; + BoxConstraints? _avatarBoxConstraints; + set avatarBoxConstraints(BoxConstraints? value) { + if (_avatarBoxConstraints == value) { + return; + } + _avatarBoxConstraints = value; + markNeedsLayout(); + } + + BoxConstraints? get deleteIconBoxConstraints => _deleteIconBoxConstraints; + BoxConstraints? _deleteIconBoxConstraints; + set deleteIconBoxConstraints(BoxConstraints? value) { + if (_deleteIconBoxConstraints == value) { + return; + } + _deleteIconBoxConstraints = value; + markNeedsLayout(); + } + // The returned list is ordered for hit testing. @override Iterable get children { @@ -1712,9 +1797,9 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip ); } - Size _layoutAvatar(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) { + Size _layoutAvatar(double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) { final double requestedSize = math.max(0.0, contentSize); - final BoxConstraints avatarConstraints = BoxConstraints.tightFor( + final BoxConstraints avatarConstraints = avatarBoxConstraints ?? BoxConstraints.tightFor( width: requestedSize, height: requestedSize, ); @@ -1733,9 +1818,9 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip return Size(avatarWidth, avatarHeight); } - Size _layoutDeleteIcon(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) { + Size _layoutDeleteIcon(double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) { final double requestedSize = math.max(0.0, contentSize); - final BoxConstraints deleteIconConstraints = BoxConstraints.tightFor( + final BoxConstraints deleteIconConstraints = deleteIconBoxConstraints ?? BoxConstraints.tightFor( width: requestedSize, height: requestedSize, ); @@ -1795,8 +1880,8 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip _kChipHeight - theme.padding.vertical + theme.labelPadding.vertical, rawLabelSize.height + theme.labelPadding.vertical, ); - final Size avatarSize = _layoutAvatar(contentConstraints, contentSize, layoutChild); - final Size deleteIconSize = _layoutDeleteIcon(contentConstraints, contentSize, layoutChild); + final Size avatarSize = _layoutAvatar(contentSize, layoutChild); + final Size deleteIconSize = _layoutDeleteIcon(contentSize, layoutChild); final Size labelSize = _layoutLabel( contentConstraints, avatarSize.width + deleteIconSize.width, diff --git a/packages/flutter/lib/src/material/chip_theme.dart b/packages/flutter/lib/src/material/chip_theme.dart index 44aa0b0016bb..76afdf18bece 100644 --- a/packages/flutter/lib/src/material/chip_theme.dart +++ b/packages/flutter/lib/src/material/chip_theme.dart @@ -198,6 +198,8 @@ class ChipThemeData with Diagnosticable { this.elevation, this.pressElevation, this.iconTheme, + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }); /// Generates a ChipThemeData from a brightness, a primary color, and a text @@ -436,6 +438,19 @@ class ChipThemeData with Diagnosticable { /// [FilterChip], [InputChip], [RawChip]. final IconThemeData? iconTheme; + /// Overrides the default for [ChipAttributes.avatarBoxConstraints], + /// the size constraints for the avatar widget. + /// + /// This property applies to [ActionChip], [Chip], [ChoiceChip], + /// [FilterChip], [InputChip], [RawChip]. + final BoxConstraints? avatarBoxConstraints; + + /// Overrides the default for [DeletableChipAttributes.deleteIconBoxConstraints]. + /// the size constraints for the delete icon widget. + /// + /// This property applies to [Chip], [FilterChip], [InputChip], [RawChip]. + final BoxConstraints? deleteIconBoxConstraints; + /// Creates a copy of this object but with the given fields replaced with the /// new values. ChipThemeData copyWith({ @@ -460,6 +475,8 @@ class ChipThemeData with Diagnosticable { double? elevation, double? pressElevation, IconThemeData? iconTheme, + BoxConstraints? avatarBoxConstraints, + BoxConstraints? deleteIconBoxConstraints, }) { return ChipThemeData( color: color ?? this.color, @@ -483,6 +500,8 @@ class ChipThemeData with Diagnosticable { elevation: elevation ?? this.elevation, pressElevation: pressElevation ?? this.pressElevation, iconTheme: iconTheme ?? this.iconTheme, + avatarBoxConstraints: avatarBoxConstraints ?? this.avatarBoxConstraints, + deleteIconBoxConstraints: deleteIconBoxConstraints ?? this.deleteIconBoxConstraints, ); } @@ -517,6 +536,8 @@ class ChipThemeData with Diagnosticable { iconTheme: a?.iconTheme != null || b?.iconTheme != null ? IconThemeData.lerp(a?.iconTheme, b?.iconTheme, t) : null, + avatarBoxConstraints: BoxConstraints.lerp(a?.avatarBoxConstraints, b?.avatarBoxConstraints, t), + deleteIconBoxConstraints: BoxConstraints.lerp(a?.deleteIconBoxConstraints, b?.deleteIconBoxConstraints, t), ); } @@ -565,6 +586,8 @@ class ChipThemeData with Diagnosticable { elevation, pressElevation, iconTheme, + avatarBoxConstraints, + deleteIconBoxConstraints, ]); @override @@ -596,7 +619,9 @@ class ChipThemeData with Diagnosticable { && other.brightness == brightness && other.elevation == elevation && other.pressElevation == pressElevation - && other.iconTheme == iconTheme; + && other.iconTheme == iconTheme + && other.avatarBoxConstraints == avatarBoxConstraints + && other.deleteIconBoxConstraints == deleteIconBoxConstraints; } @override @@ -623,5 +648,7 @@ class ChipThemeData with Diagnosticable { properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); properties.add(DoubleProperty('pressElevation', pressElevation, defaultValue: null)); properties.add(DiagnosticsProperty('iconTheme', iconTheme, defaultValue: null)); + properties.add(DiagnosticsProperty('avatarBoxConstraints', avatarBoxConstraints, defaultValue: null)); + properties.add(DiagnosticsProperty('deleteIconBoxConstraints', deleteIconBoxConstraints, defaultValue: null)); } } diff --git a/packages/flutter/lib/src/material/choice_chip.dart b/packages/flutter/lib/src/material/choice_chip.dart index 8db24000cee1..fa03a133a01d 100644 --- a/packages/flutter/lib/src/material/choice_chip.dart +++ b/packages/flutter/lib/src/material/choice_chip.dart @@ -92,6 +92,7 @@ class ChoiceChip extends StatelessWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), + this.avatarBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -132,6 +133,7 @@ class ChoiceChip extends StatelessWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), + this.avatarBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -192,6 +194,8 @@ class ChoiceChip extends StatelessWidget final ShapeBorder avatarBorder; @override final IconThemeData? iconTheme; + @override + final BoxConstraints? avatarBoxConstraints; @override bool get isEnabled => onSelected != null; @@ -236,6 +240,7 @@ class ChoiceChip extends StatelessWidget selectedShadowColor: selectedShadowColor, avatarBorder: avatarBorder, iconTheme: iconTheme, + avatarBoxConstraints: avatarBoxConstraints, ); } } diff --git a/packages/flutter/lib/src/material/filter_chip.dart b/packages/flutter/lib/src/material/filter_chip.dart index 19f623456c1b..4cc1403983d0 100644 --- a/packages/flutter/lib/src/material/filter_chip.dart +++ b/packages/flutter/lib/src/material/filter_chip.dart @@ -101,6 +101,8 @@ class FilterChip extends StatelessWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -145,6 +147,8 @@ class FilterChip extends StatelessWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -213,6 +217,10 @@ class FilterChip extends StatelessWidget final ShapeBorder avatarBorder; @override final IconThemeData? iconTheme; + @override + final BoxConstraints? avatarBoxConstraints; + @override + final BoxConstraints? deleteIconBoxConstraints; @override bool get isEnabled => onSelected != null; @@ -262,6 +270,8 @@ class FilterChip extends StatelessWidget checkmarkColor: checkmarkColor, avatarBorder: avatarBorder, iconTheme: iconTheme, + avatarBoxConstraints: avatarBoxConstraints, + deleteIconBoxConstraints: deleteIconBoxConstraints, ); } } diff --git a/packages/flutter/lib/src/material/input_chip.dart b/packages/flutter/lib/src/material/input_chip.dart index 8f21484f5a27..eb7aef7b343a 100644 --- a/packages/flutter/lib/src/material/input_chip.dart +++ b/packages/flutter/lib/src/material/input_chip.dart @@ -122,6 +122,8 @@ class InputChip extends StatelessWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), + this.avatarBoxConstraints, + this.deleteIconBoxConstraints, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0); @@ -193,6 +195,10 @@ class InputChip extends StatelessWidget final ShapeBorder avatarBorder; @override final IconThemeData? iconTheme; + @override + final BoxConstraints? avatarBoxConstraints; + @override + final BoxConstraints? deleteIconBoxConstraints; @override Widget build(BuildContext context) { @@ -238,6 +244,8 @@ class InputChip extends StatelessWidget isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null), avatarBorder: avatarBorder, iconTheme: iconTheme, + avatarBoxConstraints: avatarBoxConstraints, + deleteIconBoxConstraints: deleteIconBoxConstraints, ); } } diff --git a/packages/flutter/test/material/action_chip_test.dart b/packages/flutter/test/material/action_chip_test.dart index 0a6c708d6f0e..9006b86bcb84 100644 --- a/packages/flutter/test/material/action_chip_test.dart +++ b/packages/flutter/test/material/action_chip_test.dart @@ -437,4 +437,60 @@ void main() { expect(getIconData(tester).color, const Color(0xff00ff00)); }); + + testWidgets('ActionChip avatar layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? avatarBoxConstraints}) { + return wrapForChip( + child: Center( + child: ActionChip( + avatarBoxConstraints: avatarBoxConstraints, + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default avatar layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(ActionChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(ActionChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite())); + await tester.pump(); + + expect(tester.getSize(find.byType(ActionChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(ActionChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); } diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 82fd4a946a9d..84528ed8f0e4 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -5088,7 +5088,7 @@ void main() { }); }, child: Text('${isEnabled ? 'Disable' : 'Enable'} Chip'), - ) + ), ], ); }, @@ -5119,7 +5119,7 @@ void main() { isEnabled: false, label: const Text('Label'), onDeleted: () { }, - ) + ), ), ); @@ -5134,7 +5134,7 @@ void main() { isEnabled: enabled, label: const Text('Label'), onDeleted: () { }, - ) + ), ); } @@ -5157,6 +5157,234 @@ void main() { expect(findTooltipContainer('Delete'), findsNothing); }); + testWidgets('Chip avatar layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? avatarBoxConstraints}) { + return wrapForChip( + child: Center( + child: Chip( + avatarBoxConstraints: avatarBoxConstraints, + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default avatar layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(Chip)).width, equals(234.0)); + expect(tester.getSize(find.byType(Chip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite())); + await tester.pump(); + + expect(tester.getSize(find.byType(Chip)).width, equals(152.0)); + expect(tester.getSize(find.byType(Chip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); + + testWidgets('RawChip avatar layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? avatarBoxConstraints}) { + return wrapForChip( + child: Center( + child: RawChip( + avatarBoxConstraints: avatarBoxConstraints, + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default avatar layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(RawChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(RawChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite())); + await tester.pump(); + + expect(tester.getSize(find.byType(RawChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(RawChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); + + testWidgets('Chip delete icon layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? deleteIconBoxConstraints}) { + return wrapForChip( + child: Center( + child: Chip( + deleteIconBoxConstraints: deleteIconBoxConstraints, + onDeleted: () { }, + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default delete icon layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(Chip)).width, equals(234.0)); + expect(tester.getSize(find.byType(Chip)).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + Offset chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + final Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.cancel)); + expect(chipTopRight.dx, deleteIconCenter.dx + (labelSize.width / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + Offset labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (labelSize.width / 2) - labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + )); + await tester.pump(); + + expect(tester.getSize(find.byType(Chip)).width, equals(152.0)); + expect(tester.getSize(find.byType(Chip)).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + expect(chipTopRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); + }); + + testWidgets('RawChip delete icon layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? deleteIconBoxConstraints}) { + return wrapForChip( + child: Center( + child: RawChip( + deleteIconBoxConstraints: deleteIconBoxConstraints, + onDeleted: () { }, + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default delete icon layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(RawChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(RawChip)).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + Offset chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + final Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.cancel)); + expect(chipTopRight.dx, deleteIconCenter.dx + (labelSize.width / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + Offset labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (labelSize.width / 2) - labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + )); + await tester.pump(); + + expect(tester.getSize(find.byType(RawChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(RawChip )).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + expect(chipTopRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests diff --git a/packages/flutter/test/material/chip_theme_test.dart b/packages/flutter/test/material/chip_theme_test.dart index b2ba20524ce2..e474d6661b2c 100644 --- a/packages/flutter/test/material/chip_theme_test.dart +++ b/packages/flutter/test/material/chip_theme_test.dart @@ -79,6 +79,8 @@ void main() { expect(themeData.elevation, null); expect(themeData.pressElevation, null); expect(themeData.iconTheme, null); + expect(themeData.avatarBoxConstraints, null); + expect(themeData.deleteIconBoxConstraints, null); }); testWidgets('Default ChipThemeData debugFillProperties', (WidgetTester tester) async { @@ -117,6 +119,8 @@ void main() { elevation: 5, pressElevation: 6, iconTheme: IconThemeData(color: Color(0xffffff10)), + avatarBoxConstraints: BoxConstraints.tightForFinite(), + deleteIconBoxConstraints: BoxConstraints.tightForFinite(), ).debugFillProperties(builder); final List description = builder.properties @@ -145,7 +149,9 @@ void main() { 'brightness: dark', 'elevation: 5.0', 'pressElevation: 6.0', - 'iconTheme: IconThemeData#00000(color: Color(0xffffff10))' + 'iconTheme: IconThemeData#00000(color: Color(0xffffff10))', + 'avatarBoxConstraints: BoxConstraints(unconstrained)', + 'deleteIconBoxConstraints: BoxConstraints(unconstrained)', ])); }); @@ -1314,6 +1320,86 @@ void main() { expect(getIconData(tester).size, 23.0); expect(getIconData(tester).color, const Color(0xff112233)); }); + + testWidgets('ChipThemeData.avatarBoxConstraints updates avatar size constraints', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(75, 75); + + // Test default avatar layout constraints. + await tester.pumpWidget(MaterialApp( + theme: ThemeData(chipTheme: const ChipThemeData( + avatarBoxConstraints: BoxConstraints.tightForFinite(), + )), + home: Material( + child: Center( + child: RawChip( + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ), + )); + + expect(tester.getSize(find.byType(RawChip)).width, equals(127.0)); + expect(tester.getSize(find.byType(RawChip)).height, equals(93.0)); + + // Calculate the distance between avatar and chip edges. + final Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + final Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); + + testWidgets('ChipThemeData.deleteIconBoxConstraints updates delete icon size constraints', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(75, 75); + + // Test custom delete layout constraints. + await tester.pumpWidget(MaterialApp( + theme: ThemeData(chipTheme: const ChipThemeData( + deleteIconBoxConstraints: BoxConstraints.tightForFinite(), + )), + home: Material( + child: Center( + child: RawChip( + onDeleted: () { }, + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ), + )); + + expect(tester.getSize(find.byType(RawChip)).width, equals(127.0)); + expect(tester.getSize(find.byType(RawChip)).height, equals(93.0)); + + // Calculate the distance between delete icon and chip edges. + final Offset chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + final Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.cancel)); + expect(chipTopRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + final Offset labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); + }); } class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder { diff --git a/packages/flutter/test/material/choice_chip_test.dart b/packages/flutter/test/material/choice_chip_test.dart index 63ba7e2b0fb1..04a613dbe959 100644 --- a/packages/flutter/test/material/choice_chip_test.dart +++ b/packages/flutter/test/material/choice_chip_test.dart @@ -718,4 +718,61 @@ void main() { expect(getIconData(tester).color, const Color(0xff00ff00)); }); + + testWidgets('ChoiceChip avatar layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? avatarBoxConstraints}) { + return wrapForChip( + child: Center( + child: ChoiceChip( + avatarBoxConstraints: avatarBoxConstraints, + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + selected: false, + ), + ), + ); + } + + // Test default avatar layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(ChoiceChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(ChoiceChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite())); + await tester.pump(); + + expect(tester.getSize(find.byType(ChoiceChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(ChoiceChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between avatar and label. + labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); } diff --git a/packages/flutter/test/material/filter_chip_test.dart b/packages/flutter/test/material/filter_chip_test.dart index 1a412334e100..0265c6bc4961 100644 --- a/packages/flutter/test/material/filter_chip_test.dart +++ b/packages/flutter/test/material/filter_chip_test.dart @@ -1170,4 +1170,120 @@ void main() { theme.colorScheme.onSecondaryContainer, ); }); + + testWidgets('FilterChip avatar layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? avatarBoxConstraints}) { + return wrapForChip( + child: Center( + child: FilterChip( + avatarBoxConstraints: avatarBoxConstraints, + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + onSelected: (bool value) { }, + ), + ), + ); + } + + // Test default avatar layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(FilterChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(FilterChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distnance between avatar and label. + Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite())); + await tester.pump(); + + expect(tester.getSize(find.byType(FilterChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(FilterChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distnance between avatar and label. + labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); + + testWidgets('FilterChip delete icon layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? deleteIconBoxConstraints}) { + return wrapForChip( + child: Center( + child: FilterChip( + deleteIconBoxConstraints: deleteIconBoxConstraints, + onDeleted: () { }, + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + onSelected: (bool value) { }, + ), + ), + ); + } + + // Test default delete icon layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(FilterChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(FilterChip)).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + Offset chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + final Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.clear)); + expect(chipTopRight.dx, deleteIconCenter.dx + (labelSize.width / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + Offset labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (labelSize.width / 2) - labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + )); + await tester.pump(); + + expect(tester.getSize(find.byType(FilterChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(FilterChip )).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + expect(chipTopRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); + }); } diff --git a/packages/flutter/test/material/input_chip_test.dart b/packages/flutter/test/material/input_chip_test.dart index 2d8b19870880..12dd2ec1060c 100644 --- a/packages/flutter/test/material/input_chip_test.dart +++ b/packages/flutter/test/material/input_chip_test.dart @@ -97,6 +97,15 @@ RenderBox getMaterialBox(WidgetTester tester) { ); } +Material getMaterial(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.byType(InputChip), + matching: find.byType(Material), + ), + ); +} + IconThemeData getIconData(WidgetTester tester) { final IconTheme iconTheme = tester.firstWidget( find.descendant( @@ -476,4 +485,118 @@ void main() { // Delete button tooltip should not be visible. expect(findTooltipContainer('Delete'), findsNothing); }); + + testWidgets('InputChip avatar layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? avatarBoxConstraints}) { + return wrapForChip( + child: Center( + child: InputChip( + avatarBoxConstraints: avatarBoxConstraints, + avatar: const Icon(Icons.favorite), + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default avatar layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(InputChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(InputChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + Offset chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + final Offset avatarCenter = tester.getCenter(find.byIcon(Icons.favorite)); + expect(chipTopLeft.dx, avatarCenter.dx - (labelSize.width / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distnance between avatar and label. + Offset labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (labelSize.width / 2) + labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip(avatarBoxConstraints: const BoxConstraints.tightForFinite())); + await tester.pump(); + + expect(tester.getSize(find.byType(InputChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(InputChip)).height, equals(118.0)); + + // Calculate the distance between avatar and chip edges. + chipTopLeft = tester.getTopLeft(find.byWidget(getMaterial(tester))); + expect(chipTopLeft.dx, avatarCenter.dx - (iconSize / 2) - padding - border); + expect(chipTopLeft.dy, avatarCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distnance between avatar and label. + labelTopLeft = tester.getTopLeft(find.byType(Container)); + expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); + }); + + testWidgets('InputChip delete icon layout constraints can be customized', (WidgetTester tester) async { + const double border = 1.0; + const double iconSize = 18.0; + const double labelPadding = 8.0; + const double padding = 8.0; + const Size labelSize = Size(100, 100); + + Widget buildChip({BoxConstraints? deleteIconBoxConstraints}) { + return wrapForChip( + child: Center( + child: InputChip( + deleteIconBoxConstraints: deleteIconBoxConstraints, + onDeleted: () { }, + label: Container( + width: labelSize.width, + height: labelSize.width, + color: const Color(0xFFFF0000), + ), + ), + ), + ); + } + + // Test default delete icon layout constraints. + await tester.pumpWidget(buildChip()); + + expect(tester.getSize(find.byType(InputChip)).width, equals(234.0)); + expect(tester.getSize(find.byType(InputChip)).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + Offset chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + final Offset deleteIconCenter = tester.getCenter(find.byIcon(Icons.clear)); + expect(chipTopRight.dx, deleteIconCenter.dx + (labelSize.width / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + Offset labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (labelSize.width / 2) - labelPadding); + + // Test custom avatar layout constraints. + await tester.pumpWidget(buildChip( + deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), + )); + await tester.pump(); + + expect(tester.getSize(find.byType(InputChip)).width, equals(152.0)); + expect(tester.getSize(find.byType(InputChip)).height, equals(118.0)); + + // Calculate the distance between delete icon and chip edges. + chipTopRight = tester.getTopRight(find.byWidget(getMaterial(tester))); + expect(chipTopRight.dx, deleteIconCenter.dx + (iconSize / 2) + padding + border); + expect(chipTopRight.dy, deleteIconCenter.dy - (labelSize.width / 2) - padding - border); + + // Calculate the distance between delete icon and label. + labelTopRight = tester.getTopRight(find.byType(Container)); + expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); + }); }