diff --git a/examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart b/examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart new file mode 100644 index 0000000000..03f9120385 --- /dev/null +++ b/examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart @@ -0,0 +1,164 @@ +// 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.chipAnimationStyle]. + +void main() => runApp(const ChipAnimationStyleExampleApp()); + +class ChipAnimationStyleExampleApp extends StatelessWidget { + const ChipAnimationStyleExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: ChipAnimationStyleExample(), + ), + ), + ); + } +} + +class ChipAnimationStyleExample extends StatefulWidget { + const ChipAnimationStyleExample({super.key}); + + @override + State createState() => + _ChipAnimationStyleExampleState(); +} + +class _ChipAnimationStyleExampleState extends State { + bool enabled = true; + bool selected = false; + bool showCheckmark = true; + bool showDeleteIcon = true; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FilterChip.elevated( + chipAnimationStyle: ChipAnimationStyle( + enableAnimation: AnimationStyle( + duration: const Duration(seconds: 3), + reverseDuration: const Duration(seconds: 1), + ), + ), + onSelected: !enabled ? null : (bool value) {}, + disabledColor: Colors.red.withOpacity(0.12), + backgroundColor: Colors.amber, + label: Text(enabled ? 'Enabled' : 'Disabled'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + enabled = !enabled; + }); + }, + child: Text(enabled ? 'Disable' : 'Enable'), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FilterChip.elevated( + chipAnimationStyle: ChipAnimationStyle( + selectAnimation: AnimationStyle( + duration: const Duration(seconds: 3), + reverseDuration: const Duration(seconds: 1), + ), + ), + backgroundColor: Colors.amber, + selectedColor: Colors.blue, + selected: selected, + showCheckmark: false, + onSelected: (bool value) {}, + label: Text(selected ? 'Selected' : 'Unselected'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + selected = !selected; + }); + }, + child: Text(selected ? 'Unselect' : 'Select'), + ), + ], + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FilterChip.elevated( + chipAnimationStyle: ChipAnimationStyle( + avatarDrawerAnimation: AnimationStyle( + duration: const Duration(seconds: 2), + reverseDuration: const Duration(seconds: 1), + ), + ), + selected: showCheckmark, + onSelected: (bool value) {}, + label: Text(showCheckmark ? 'Checked' : 'Unchecked'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + showCheckmark = !showCheckmark; + }); + }, + child: + Text(showCheckmark ? 'Hide checkmark' : 'Show checkmark'), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FilterChip.elevated( + chipAnimationStyle: ChipAnimationStyle( + deleteDrawerAnimation: AnimationStyle( + duration: const Duration(seconds: 2), + reverseDuration: const Duration(seconds: 1), + ), + ), + onDeleted: showDeleteIcon ? () {} : null, + onSelected: (bool value) {}, + label: Text(showDeleteIcon ? 'Deletable' : 'Undeletable'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + showDeleteIcon = !showDeleteIcon; + }); + }, + child: Text( + showDeleteIcon ? 'Hide delete icon' : 'Show delete icon'), + ), + ], + ), + ], + ), + ], + ); + } +} diff --git a/examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart b/examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart new file mode 100644 index 0000000000..a22e91b69a --- /dev/null +++ b/examples/api/test/material/chip/chip_attributes.chip_animation_style.0_test.dart @@ -0,0 +1,135 @@ +// 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.chip_animation_style.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ChipAnimationStyle.enableAnimation overrides chip enable animation', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ChipAnimationStyleExampleApp(), + ); + + final RenderBox materialBox = tester.firstRenderObject( + find.descendant( + of: find.widgetWithText(RawChip, 'Enabled'), + matching: find.byType(CustomPaint), + ), + ); + + expect(materialBox, paints..rrect(color: const Color(0xffffc107))); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Disable')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); // Advance enable animation by 500ms. + + expect(materialBox, paints..rrect(color: const Color(0x1f882f2b))); + + await tester.pump(const Duration(milliseconds: 500)); // Advance enable animation by 500ms. + + expect(materialBox, paints..rrect(color: const Color(0x1ff44336))); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Enable')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 1500)); // Advance enable animation by 1500ms. + + expect(materialBox, paints..rrect(color: const Color(0xfffbd980))); + + await tester.pump(const Duration(milliseconds: 1500)); // Advance enable animation by 1500ms. + + expect(materialBox, paints..rrect(color: const Color(0xffffc107))); + }); + + testWidgets('ChipAnimationStyle.selectAnimation overrides chip select animation', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ChipAnimationStyleExampleApp(), + ); + + final RenderBox materialBox = tester.firstRenderObject( + find.descendant( + of: find.widgetWithText(RawChip, 'Unselected'), + matching: find.byType(CustomPaint), + ), + ); + + expect(materialBox, paints..rrect(color: const Color(0xffffc107))); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Select')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 1500)); // Advance select animation by 1500ms. + + expect(materialBox, paints..rrect(color: const Color(0xff4da6f4))); + + await tester.pump(const Duration(milliseconds: 1500)); // Advance select animation by 1500ms. + + expect(materialBox, paints..rrect(color: const Color(0xff2196f3))); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Unselect')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); // Advance select animation by 500ms. + + expect(materialBox, paints..rrect(color: const Color(0xfff8e7c3))); + + await tester.pump(const Duration(milliseconds: 500)); // Advance select animation by 500ms. + + expect(materialBox, paints..rrect(color: const Color(0xffffc107))); + }); + + testWidgets('ChipAnimationStyle.avatarDrawerAnimation overrides chip checkmark animation', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ChipAnimationStyleExampleApp(), + ); + + expect(tester.getSize(find.widgetWithText(RawChip, 'Checked')).width, closeTo(152.6, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Hide checkmark')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); // Advance avatar animation by 500ms. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Unchecked')).width, closeTo(160.9, 0.1)); + + await tester.pump(const Duration(milliseconds: 500)); // Advance avatar animation by 500ms. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Unchecked')).width, closeTo(160.9, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Show checkmark')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // Advance avatar animation by 1sec. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Checked')).width, closeTo(132.7, 0.1)); + + await tester.pump(const Duration(seconds: 1)); // Advance avatar animation by 1sec. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Checked')).width, closeTo(152.6, 0.1)); + }); + + testWidgets('ChipAnimationStyle.deleteDrawerAnimation overrides chip delete icon animation', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ChipAnimationStyleExampleApp(), + ); + + expect(tester.getSize(find.widgetWithText(RawChip, 'Deletable')).width, closeTo(180.9, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Hide delete icon')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); // Advance delete icon animation by 500ms. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Undeletable')).width, closeTo(204.6, 0.1)); + + await tester.pump(const Duration(milliseconds: 500)); // Advance delete icon animation by 500ms. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Undeletable')).width, closeTo(189.1, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Show delete icon')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // Advance delete icon animation by 1sec. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Deletable')).width, closeTo(176.4, 0.1)); + + await tester.pump(const Duration(seconds: 1)); // Advance delete icon animation by 1sec. + + expect(tester.getSize(find.widgetWithText(RawChip, 'Deletable')).width, closeTo(180.9, 0.1)); + }); +} diff --git a/packages/flutter/lib/src/material/action_chip.dart b/packages/flutter/lib/src/material/action_chip.dart index c819c48fec..8c06a79e5f 100644 --- a/packages/flutter/lib/src/material/action_chip.dart +++ b/packages/flutter/lib/src/material/action_chip.dart @@ -110,6 +110,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.surfaceTintColor, this.iconTheme, this.avatarBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -145,6 +146,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.surfaceTintColor, this.iconTheme, this.avatarBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -195,6 +197,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip final IconThemeData? iconTheme; @override final BoxConstraints? avatarBoxConstraints; + @override + final ChipAnimationStyle? chipAnimationStyle; @override bool get isEnabled => onPressed != null; @@ -233,6 +237,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip surfaceTintColor: surfaceTintColor, iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, + chipAnimationStyle: chipAnimationStyle, ); } } diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index c3bfbc3f82..a9deb21fe2 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -243,6 +243,32 @@ abstract interface class ChipAttributes { /// ** See code in examples/api/lib/material/chip/chip_attributes.avatar_box_constraints.0.dart ** /// {@end-tool} BoxConstraints? get avatarBoxConstraints; + + /// Used to override the default chip animations durations. + /// + /// If [ChipAnimationStyle.enableAnimation] with duration or reverse duration is + /// provided, it will be used to override the chip enable and disable animation durations. + /// If it is null, then default duration will be 75ms. + /// + /// If [ChipAnimationStyle.selectAnimation] with duration or reverse duration is provided, + /// it will be used to override the chip select and unselect animation durations. + /// If it is null, then default duration will be 195ms. + /// + /// If [ChipAnimationStyle.avatarDrawerAnimation] with duration or reverse duration + /// is provided, it will be used to override the chip checkmark animation duration. + /// If it is null, then default duration will be 150ms. + /// + /// If [ChipAnimationStyle.deleteDrawerAnimation] with duration or reverse duration + /// is provided, it will be used to override the chip delete icon animation duration. + /// If it is null, then default duration will be 150ms. + /// + /// {@tool dartpad} + /// This sample showcases how to override the chip animations durations using + /// [ChipAnimationStyle]. + /// + /// ** See code in examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart ** + /// {@end-tool} + ChipAnimationStyle? get chipAnimationStyle; } /// An interface for Material Design chips that can be deleted. @@ -568,6 +594,37 @@ abstract interface class TappableChipAttributes { String? get tooltip; } +/// A helper class that overrides the default chip animation parameters. +class ChipAnimationStyle { + /// Creates an instance of Chip Animation Style class. + ChipAnimationStyle({ + this.enableAnimation, + this.selectAnimation, + this.avatarDrawerAnimation, + this.deleteDrawerAnimation, + }); + + /// If [enableAnimation] with duration or reverse duration is provided, + /// it will be used to override the chip enable and disable animation durations. + /// If it is null, then default duration will be 75ms. + final AnimationStyle? enableAnimation; + + /// If [selectAnimation] with duration or reverse duration is provided, + /// it will be used to override the chip select and unselect animation durations. + /// If it is null, then default duration will be 195ms. + final AnimationStyle? selectAnimation; + + /// If [avatarDrawerAnimation] with duration or reverse duration is provided, + /// it will be used to override the chip checkmark animation duration. If it + /// is null, then default duration will be 150ms. + final AnimationStyle? avatarDrawerAnimation; + + /// If [deleteDrawerAnimation] with duration or reverse duration is provided, + /// it will be used to override the chip delete icon animation duration. If it + /// is null, then default duration will be 150ms. + final AnimationStyle? deleteDrawerAnimation; +} + /// A Material Design chip. /// /// Chips are compact elements that represent an attribute, text, entity, or @@ -637,6 +694,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri this.iconTheme, this.avatarBoxConstraints, this.deleteIconBoxConstraints, + this.chipAnimationStyle, }) : assert(elevation == null || elevation >= 0.0); @override @@ -687,6 +745,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri final BoxConstraints? avatarBoxConstraints; @override final BoxConstraints? deleteIconBoxConstraints; + @override + final ChipAnimationStyle? chipAnimationStyle; @override Widget build(BuildContext context) { @@ -717,6 +777,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, deleteIconBoxConstraints: deleteIconBoxConstraints, + chipAnimationStyle: chipAnimationStyle, ); } } @@ -806,6 +867,7 @@ class RawChip extends StatefulWidget this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, this.deleteIconBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), deleteIcon = deleteIcon ?? _kDefaultDeleteIcon; @@ -889,6 +951,8 @@ class RawChip extends StatefulWidget final BoxConstraints? avatarBoxConstraints; @override final BoxConstraints? deleteIconBoxConstraints; + @override + final ChipAnimationStyle? chipAnimationStyle; /// If set, this indicates that the chip should be disabled if all of the /// tap callbacks ([onSelected], [onPressed]) are null. @@ -936,7 +1000,8 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid setMaterialState(MaterialState.disabled, !widget.isEnabled); setMaterialState(MaterialState.selected, widget.selected); selectController = AnimationController( - duration: _kSelectDuration, + duration: widget.chipAnimationStyle?.selectAnimation?.duration ?? _kSelectDuration, + reverseDuration: widget.chipAnimationStyle?.selectAnimation?.reverseDuration, value: widget.selected ? 1.0 : 0.0, vsync: this, ); @@ -945,17 +1010,20 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid curve: Curves.fastOutSlowIn, ); avatarDrawerController = AnimationController( - duration: _kDrawerDuration, + duration: widget.chipAnimationStyle?.avatarDrawerAnimation?.duration ?? _kDrawerDuration, + reverseDuration: widget.chipAnimationStyle?.avatarDrawerAnimation?.reverseDuration, value: hasAvatar || widget.selected ? 1.0 : 0.0, vsync: this, ); deleteDrawerController = AnimationController( - duration: _kDrawerDuration, + duration: widget.chipAnimationStyle?.deleteDrawerAnimation?.duration ?? _kDrawerDuration, + reverseDuration: widget.chipAnimationStyle?.deleteDrawerAnimation?.reverseDuration, value: hasDeleteButton ? 1.0 : 0.0, vsync: this, ); enableController = AnimationController( - duration: _kDisableDuration, + duration: widget.chipAnimationStyle?.enableAnimation?.duration ?? _kDisableDuration, + reverseDuration: widget.chipAnimationStyle?.enableAnimation?.reverseDuration, value: widget.isEnabled ? 1.0 : 0.0, vsync: this, ); diff --git a/packages/flutter/lib/src/material/choice_chip.dart b/packages/flutter/lib/src/material/choice_chip.dart index 14885ec994..aa8ae11aa7 100644 --- a/packages/flutter/lib/src/material/choice_chip.dart +++ b/packages/flutter/lib/src/material/choice_chip.dart @@ -93,6 +93,7 @@ class ChoiceChip extends StatelessWidget this.checkmarkColor, this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -134,6 +135,7 @@ class ChoiceChip extends StatelessWidget this.checkmarkColor, this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -196,6 +198,8 @@ class ChoiceChip extends StatelessWidget final IconThemeData? iconTheme; @override final BoxConstraints? avatarBoxConstraints; + @override + final ChipAnimationStyle? chipAnimationStyle; @override bool get isEnabled => onSelected != null; @@ -241,6 +245,7 @@ class ChoiceChip extends StatelessWidget avatarBorder: avatarBorder, iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, + chipAnimationStyle: chipAnimationStyle, ); } } diff --git a/packages/flutter/lib/src/material/filter_chip.dart b/packages/flutter/lib/src/material/filter_chip.dart index bede721c0f..0cd7b24b49 100644 --- a/packages/flutter/lib/src/material/filter_chip.dart +++ b/packages/flutter/lib/src/material/filter_chip.dart @@ -103,6 +103,7 @@ class FilterChip extends StatelessWidget this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, this.deleteIconBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -149,6 +150,7 @@ class FilterChip extends StatelessWidget this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, this.deleteIconBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -221,6 +223,8 @@ class FilterChip extends StatelessWidget final BoxConstraints? avatarBoxConstraints; @override final BoxConstraints? deleteIconBoxConstraints; + @override + final ChipAnimationStyle? chipAnimationStyle; @override bool get isEnabled => onSelected != null; @@ -272,6 +276,7 @@ class FilterChip extends StatelessWidget iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, deleteIconBoxConstraints: deleteIconBoxConstraints, + chipAnimationStyle: chipAnimationStyle, ); } } diff --git a/packages/flutter/lib/src/material/input_chip.dart b/packages/flutter/lib/src/material/input_chip.dart index eb7aef7b34..a599e8a5a3 100644 --- a/packages/flutter/lib/src/material/input_chip.dart +++ b/packages/flutter/lib/src/material/input_chip.dart @@ -124,6 +124,7 @@ class InputChip extends StatelessWidget this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, this.deleteIconBoxConstraints, + this.chipAnimationStyle, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0); @@ -199,6 +200,8 @@ class InputChip extends StatelessWidget final BoxConstraints? avatarBoxConstraints; @override final BoxConstraints? deleteIconBoxConstraints; + @override + final ChipAnimationStyle? chipAnimationStyle; @override Widget build(BuildContext context) { @@ -246,6 +249,7 @@ class InputChip extends StatelessWidget iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, deleteIconBoxConstraints: deleteIconBoxConstraints, + chipAnimationStyle: chipAnimationStyle, ); } } diff --git a/packages/flutter/test/material/action_chip_test.dart b/packages/flutter/test/material/action_chip_test.dart index a350df4b55..92732d47ce 100644 --- a/packages/flutter/test/material/action_chip_test.dart +++ b/packages/flutter/test/material/action_chip_test.dart @@ -493,4 +493,40 @@ void main() { labelTopLeft = tester.getTopLeft(find.byType(Container)); expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); }); + + testWidgets('ActionChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle(duration: Durations.extralong4), + selectAnimation: AnimationStyle.noAnimation, + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: ActionChip( + chipAnimationStyle: chipAnimationStyle, + label: const Text('ActionChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); + + testWidgets('Elevated ActionChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle(duration: Durations.extralong4), + selectAnimation: AnimationStyle.noAnimation, + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: ActionChip.elevated( + chipAnimationStyle: chipAnimationStyle, + label: const Text('ActionChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); } diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index d86147d099..e53d800a5f 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -5793,6 +5793,318 @@ void main() { expect(renderLayoutCount.layoutCount, 1); }); + + testWidgets('ChipAnimationStyle.enableAnimation overrides chip enable animation duration', (WidgetTester tester) async { + const Color disabledColor = Color(0xffff0000); + const Color backgroundColor = Color(0xff00ff00); + bool enabled = true; + + await tester.pumpWidget(MaterialApp( + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + RawChip( + chipAnimationStyle: ChipAnimationStyle( + enableAnimation: AnimationStyle( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 150), + ), + ), + isEnabled: enabled, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + label: const Text('RawChip'), + ), + ElevatedButton( + onPressed: () { + setState(() { + enabled = !enabled; + }); + }, + child: Text('${enabled ? 'Disable' : 'Enable'} Chip'), + ), + ], + ); + }, + ), + ), + ), + )); + + final RenderBox materialBox = tester.firstRenderObject( + find.descendant( + of: find.byType(RawChip), + matching: find.byType(CustomPaint), + ), + ); + + // Test background color when the chip is enabled. + expect(materialBox, paints..rrect(color: backgroundColor)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Disable Chip')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 75)); + + expect(materialBox, paints..rrect(color: const Color(0x80ff0000))); + + await tester.pump(const Duration(milliseconds: 75)); + + // Test background color when the chip is disabled. + expect(materialBox, paints..rrect(color: disabledColor)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Enable Chip')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + + expect(materialBox, paints..rrect(color: const Color(0x8000ff00))); + + await tester.pump(const Duration(milliseconds: 150)); + + // Test background color when the chip is enabled. + expect(materialBox, paints..rrect(color: backgroundColor)); + }); + + testWidgets('ChipAnimationStyle.selectAnimation overrides chip selection animation duration', (WidgetTester tester) async { + const Color backgroundColor = Color(0xff00ff00); + const Color selectedColor = Color(0xff0000ff); + bool selected = false; + + await tester.pumpWidget(MaterialApp( + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + RawChip( + chipAnimationStyle: ChipAnimationStyle( + selectAnimation: AnimationStyle( + duration: const Duration(milliseconds: 600), + reverseDuration: const Duration(milliseconds: 300), + ), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selected: selected, + onSelected: (bool value) {}, + label: const Text('RawChip'), + ), + ElevatedButton( + onPressed: () { + setState(() { + selected = !selected; + }); + }, + child: Text('${selected ? 'Unselect' : 'Select'} Chip'), + ), + ], + ); + }, + ), + ), + ), + )); + + final RenderBox materialBox = tester.firstRenderObject( + find.descendant( + of: find.byType(RawChip), + matching: find.byType(CustomPaint), + ), + ); + + // Test background color when the chip is unselected. + expect(materialBox, paints..rrect(color: backgroundColor)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Select Chip')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + expect(materialBox, paints..rrect(color: const Color(0xc60000ff))); + + await tester.pump(const Duration(milliseconds: 300)); + + // Test background color when the chip is selected. + expect(materialBox, paints..rrect(color: selectedColor)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Unselect Chip')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + + expect(materialBox, paints..rrect(color: const Color(0x3900ff00))); + + await tester.pump(const Duration(milliseconds: 150)); + + // Test background color when the chip is unselected. + expect(materialBox, paints..rrect(color: backgroundColor)); + }); + + testWidgets('ChipAnimationStyle.avatarDrawerAnimation overrides chip avatar animation duration', (WidgetTester tester) async { + const Color checkmarkColor = Color(0xffff0000); + bool selected = false; + + await tester.pumpWidget(MaterialApp( + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + RawChip( + chipAnimationStyle: ChipAnimationStyle( + avatarDrawerAnimation: AnimationStyle( + duration: const Duration(milliseconds: 800), + reverseDuration: const Duration(milliseconds: 400), + ), + ), + checkmarkColor: checkmarkColor, + selected: selected, + onSelected: (bool value) {}, + label: const Text('RawChip'), + ), + ElevatedButton( + onPressed: () { + setState(() { + selected = !selected; + }); + }, + child: Text('${selected ? 'Unselect' : 'Select'} Chip'), + ), + ], + ); + }, + ), + ), + ), + )); + + final RenderBox materialBox = tester.firstRenderObject( + find.descendant( + of: find.byType(RawChip), + matching: find.byType(CustomPaint), + ), + ); + + // Test the chechmark is not visible yet. + expect(materialBox, isNot(paints..path(color:checkmarkColor))); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(132.6, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Select Chip')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + + expect(materialBox, paints..path(color: checkmarkColor)); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(148.2, 0.1)); + + await tester.pump(const Duration(milliseconds: 400)); + + // Test the checkmark is fully visible. + expect(materialBox, paints..path(color: checkmarkColor)); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(152.6, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Unselect Chip')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(materialBox, isNot(paints..path(color:checkmarkColor))); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(148.2, 0.1)); + + await tester.pump(const Duration(milliseconds: 200)); + + // Test if checkmark is removed. + expect(materialBox, isNot(paints..path(color:checkmarkColor))); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(132.6, 0.1)); + }, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933 + + testWidgets('ChipAnimationStyle.deleteDrawerAnimation overrides chip delete icon animation duration', (WidgetTester tester) async { + bool showDeleteIcon = false; + await tester.pumpWidget(MaterialApp( + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + RawChip( + chipAnimationStyle: ChipAnimationStyle( + deleteDrawerAnimation: AnimationStyle( + duration: const Duration(milliseconds: 500), + reverseDuration: const Duration(milliseconds: 250), + ), + ), + onDeleted: showDeleteIcon ? () {} : null, + label: const Text('RawChip'), + ), + ElevatedButton( + onPressed: () { + setState(() { + showDeleteIcon = !showDeleteIcon; + }); + }, + child: Text('${showDeleteIcon ? 'Hide' : 'Show'} delete icon'), + ), + ], + ); + }, + ), + ), + ), + )); + + // Test the delete icon is not visible yet. + expect(find.byIcon(Icons.cancel), findsNothing); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(132.6, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Show delete icon')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + expect(find.byIcon(Icons.cancel), findsOneWidget); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(148.2, 0.1)); + + await tester.pump(const Duration(milliseconds: 250)); + + // Test the delete icon is fully visible. + expect(find.byIcon(Icons.cancel), findsOneWidget); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(152.6, 0.1)); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Hide delete icon')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 125)); + + expect(find.byIcon(Icons.cancel), findsOneWidget); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(148.2, 0.1)); + + await tester.pump(const Duration(milliseconds: 125)); + + // Test if delete icon is removed. + expect(find.byIcon(Icons.cancel), findsNothing); + expect(tester.getSize(find.byType(RawChip)).width, closeTo(132.6, 0.1)); + }, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933 + + testWidgets('Chip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle.noAnimation, + selectAnimation: AnimationStyle(duration: Durations.long3), + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: Chip( + chipAnimationStyle: chipAnimationStyle, + label: const Text('Chip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); } 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 441fac3bc6..553aaeeba0 100644 --- a/packages/flutter/test/material/choice_chip_test.dart +++ b/packages/flutter/test/material/choice_chip_test.dart @@ -775,4 +775,42 @@ void main() { labelTopLeft = tester.getTopLeft(find.byType(Container)); expect(labelTopLeft.dx, avatarCenter.dx + (iconSize / 2) + labelPadding); }); + + testWidgets('ChoiceChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle(duration: Durations.extralong4), + selectAnimation: AnimationStyle.noAnimation, + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: ChoiceChip( + chipAnimationStyle: chipAnimationStyle, + selected: true, + label: const Text('ChoiceChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); + + testWidgets('Elevated ChoiceChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle(duration: Durations.extralong4), + selectAnimation: AnimationStyle.noAnimation, + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: ChoiceChip.elevated( + chipAnimationStyle: chipAnimationStyle, + selected: true, + label: const Text('ChoiceChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); } diff --git a/packages/flutter/test/material/filter_chip_test.dart b/packages/flutter/test/material/filter_chip_test.dart index 1d249a2737..459df5f154 100644 --- a/packages/flutter/test/material/filter_chip_test.dart +++ b/packages/flutter/test/material/filter_chip_test.dart @@ -1286,4 +1286,42 @@ void main() { labelTopRight = tester.getTopRight(find.byType(Container)); expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); }); + + testWidgets('FilterChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle.noAnimation, + selectAnimation: AnimationStyle(duration: Durations.extralong4), + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: FilterChip( + chipAnimationStyle: chipAnimationStyle, + onSelected: (bool value) { }, + label: const Text('FilterChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); + + testWidgets('Elevated FilterChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle.noAnimation, + selectAnimation: AnimationStyle(duration: Durations.extralong4), + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: FilterChip.elevated( + chipAnimationStyle: chipAnimationStyle, + onSelected: (bool value) { }, + label: const Text('FilterChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); } diff --git a/packages/flutter/test/material/input_chip_test.dart b/packages/flutter/test/material/input_chip_test.dart index 7a2002ec1d..ea00c019f9 100644 --- a/packages/flutter/test/material/input_chip_test.dart +++ b/packages/flutter/test/material/input_chip_test.dart @@ -599,4 +599,22 @@ void main() { labelTopRight = tester.getTopRight(find.byType(Container)); expect(labelTopRight.dx, deleteIconCenter.dx - (iconSize / 2) - labelPadding); }); + + testWidgets('InputChip.chipAnimationStyle is passed to RawChip', (WidgetTester tester) async { + final ChipAnimationStyle chipAnimationStyle = ChipAnimationStyle( + enableAnimation: AnimationStyle(duration: Durations.short2), + selectAnimation: AnimationStyle.noAnimation, + ); + + await tester.pumpWidget(wrapForChip( + child: Center( + child: InputChip( + chipAnimationStyle: chipAnimationStyle, + label: const Text('InputChip'), + ), + ), + )); + + expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); + }); }