Skip to content

Commit

Permalink
Reland Added MaterialStatesController, updated InkWell et al. #103167…
Browse files Browse the repository at this point in the history
… (#105656)
  • Loading branch information
HansMuller committed Jun 10, 2022
1 parent 5d0e35c commit d8783ff
Show file tree
Hide file tree
Showing 13 changed files with 850 additions and 106 deletions.
102 changes: 102 additions & 0 deletions examples/api/lib/material/text_button/text_button.1.dart
@@ -0,0 +1,102 @@
// 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';

void main() {
runApp(const MaterialApp(home: Home()));
}

class SelectableButton extends StatefulWidget {
const SelectableButton({
super.key,
required this.selected,
this.style,
required this.onPressed,
required this.child,
});

final bool selected;
final ButtonStyle? style;
final VoidCallback? onPressed;
final Widget child;

@override
State<SelectableButton> createState() => _SelectableButtonState();

}

class _SelectableButtonState extends State<SelectableButton> {
late final MaterialStatesController statesController;

@override
void initState() {
super.initState();
statesController = MaterialStatesController(<MaterialState>{
if (widget.selected) MaterialState.selected
});
}

@override
void didUpdateWidget(SelectableButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selected != oldWidget.selected) {
statesController.update(MaterialState.selected, widget.selected);
}
}

@override
Widget build(BuildContext context) {
return TextButton(
statesController: statesController,
style: widget.style,
onPressed: widget.onPressed,
child: widget.child,
);
}
}

class Home extends StatefulWidget {
const Home({ super.key });

@override
State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
bool selected = false;

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SelectableButton(
selected: selected,
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return Colors.white;
}
return null; // defer to the defaults
},
),
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return Colors.indigo;
}
return null; // defer to the defaults
},
),
),
onPressed: () {
setState(() { selected = !selected; });
},
child: const Text('toggle selected'),
),
),
);
}
}
51 changes: 51 additions & 0 deletions examples/api/test/material/text_button/text_button.1_test.dart
@@ -0,0 +1,51 @@
// 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/text_button/text_button.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';

void main() {

testWidgets('SelectableButton', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.light(),
),
home: const example.Home(),
),
);

final Finder button = find.byType(example.SelectableButton);

example.SelectableButton buttonWidget() => tester.widget<example.SelectableButton>(button);

Material buttonMaterial() {
return tester.widget<Material>(
find.descendant(
of: find.byType(example.SelectableButton),
matching: find.byType(Material),
),
);
}

expect(buttonWidget().selected, false);
expect(buttonMaterial().textStyle!.color, const ColorScheme.light().primary); // default button foreground color
expect(buttonMaterial().color, Colors.transparent); // default button background color

await tester.tap(button); // Toggles the button's selected property.
await tester.pumpAndSettle();
expect(buttonWidget().selected, true);
expect(buttonMaterial().textStyle!.color, Colors.white);
expect(buttonMaterial().color, Colors.indigo);


await tester.tap(button); // Toggles the button's selected property.
await tester.pumpAndSettle();
expect(buttonWidget().selected, false);
expect(buttonMaterial().textStyle!.color, const ColorScheme.light().primary);
expect(buttonMaterial().color, Colors.transparent);
});
}
112 changes: 68 additions & 44 deletions packages/flutter/lib/src/material/button_style_button.dart
Expand Up @@ -14,7 +14,6 @@ import 'constants.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_state.dart';
import 'material_state_mixin.dart';
import 'theme_data.dart';

/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
Expand All @@ -39,6 +38,7 @@ abstract class ButtonStyleButton extends StatefulWidget {
required this.focusNode,
required this.autofocus,
required this.clipBehavior,
this.statesController,
required this.child,
}) : assert(autofocus != null),
assert(clipBehavior != null);
Expand Down Expand Up @@ -95,6 +95,9 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;

/// {@macro flutter.material.inkwell.statesController}
final MaterialStatesController? statesController;

/// Typically the button's label.
final Widget? child;

Expand Down Expand Up @@ -191,36 +194,61 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// * [TextButton], a simple button without a shadow.
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [OutlinedButton], similar to [TextButton], but with an outline.
class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin, TickerProviderStateMixin {
AnimationController? _controller;
double? _elevation;
Color? _backgroundColor;
class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStateMixin {
AnimationController? controller;
double? elevation;
Color? backgroundColor;
MaterialStatesController? internalStatesController;

void handleStatesControllerChange() {
// Force a rebuild to resolve MaterialStateProperty properties
setState(() { });
}

@override
void initState() {
super.initState();
setMaterialState(MaterialState.disabled, !widget.enabled);
MaterialStatesController get statesController => widget.statesController ?? internalStatesController!;

void initStatesController() {
if (widget.statesController == null) {
internalStatesController = MaterialStatesController();
}
statesController.update(MaterialState.disabled, !widget.enabled);
statesController.addListener(handleStatesControllerChange);
}

@override
void dispose() {
_controller?.dispose();
super.dispose();
void initState() {
super.initState();
initStatesController();
}

@override
void didUpdateWidget(ButtonStyleButton oldWidget) {
super.didUpdateWidget(oldWidget);
setMaterialState(MaterialState.disabled, !widget.enabled);
// If the button is disabled while a press gesture is currently ongoing,
// InkWell makes a call to handleHighlightChanged. This causes an exception
// because it calls setState in the middle of a build. To preempt this, we
// manually update pressed to false when this situation occurs.
if (isDisabled && isPressed) {
removeMaterialState(MaterialState.pressed);
if (widget.statesController != oldWidget.statesController) {
oldWidget.statesController?.removeListener(handleStatesControllerChange);
if (widget.statesController != null) {
internalStatesController?.dispose();
internalStatesController = null;
}
initStatesController();
}
if (widget.enabled != oldWidget.enabled) {
statesController.update(MaterialState.disabled, !widget.enabled);
if (!widget.enabled) {
// The button may have been disabled while a press gesture is currently underway.
statesController.update(MaterialState.pressed, false);
}
}
}

@override
void dispose() {
statesController.removeListener(handleStatesControllerChange);
internalStatesController?.dispose();
controller?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final ButtonStyle? widgetStyle = widget.style;
Expand All @@ -237,7 +265,9 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin

T? resolve<T>(MaterialStateProperty<T>? Function(ButtonStyle? style) getProperty) {
return effectiveValue(
(ButtonStyle? style) => getProperty(style)?.resolve(materialStates),
(ButtonStyle? style) {
return getProperty(style)?.resolve(statesController.value);
},
);
}

Expand All @@ -254,7 +284,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
final BorderSide? resolvedSide = resolve<BorderSide?>((ButtonStyle? style) => style?.side);
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>((ButtonStyle? style) => style?.shape);

final MaterialStateMouseCursor resolvedMouseCursor = _MouseCursor(
final MaterialStateMouseCursor mouseCursor = _MouseCursor(
(Set<MaterialState> states) => effectiveValue((ButtonStyle? style) => style?.mouseCursor?.resolve(states)),
);

Expand Down Expand Up @@ -309,16 +339,16 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
// animates its elevation but not its color. SKIA renders non-zero
// elevations as a shadow colored fill behind the Material's background.
if (resolvedAnimationDuration! > Duration.zero
&& _elevation != null
&& _backgroundColor != null
&& _elevation != resolvedElevation
&& _backgroundColor!.value != resolvedBackgroundColor!.value
&& _backgroundColor!.opacity == 1
&& elevation != null
&& backgroundColor != null
&& elevation != resolvedElevation
&& backgroundColor!.value != resolvedBackgroundColor!.value
&& backgroundColor!.opacity == 1
&& resolvedBackgroundColor.opacity < 1
&& resolvedElevation == 0) {
if (_controller?.duration != resolvedAnimationDuration) {
_controller?.dispose();
_controller = AnimationController(
if (controller?.duration != resolvedAnimationDuration) {
controller?.dispose();
controller = AnimationController(
duration: resolvedAnimationDuration,
vsync: this,
)
Expand All @@ -328,12 +358,12 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
}
});
}
resolvedBackgroundColor = _backgroundColor; // Defer changing the background color.
_controller!.value = 0;
_controller!.forward();
resolvedBackgroundColor = backgroundColor; // Defer changing the background color.
controller!.value = 0;
controller!.forward();
}
_elevation = resolvedElevation;
_backgroundColor = resolvedBackgroundColor;
elevation = resolvedElevation;
backgroundColor = resolvedBackgroundColor;

final Widget result = ConstrainedBox(
constraints: effectiveConstraints,
Expand All @@ -350,24 +380,18 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
child: InkWell(
onTap: widget.onPressed,
onLongPress: widget.onLongPress,
onHighlightChanged: updateMaterialState(MaterialState.pressed),
onHover: updateMaterialState(
MaterialState.hovered,
onChanged: widget.onHover,
),
mouseCursor: resolvedMouseCursor,
onHover: widget.onHover,
mouseCursor: mouseCursor,
enableFeedback: resolvedEnableFeedback,
focusNode: widget.focusNode,
canRequestFocus: widget.enabled,
onFocusChange: updateMaterialState(
MaterialState.focused,
onChanged: widget.onFocusChange,
),
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
splashFactory: resolvedSplashFactory,
overlayColor: overlayColor,
highlightColor: Colors.transparent,
customBorder: resolvedShape.copyWith(side: resolvedSide),
statesController: statesController,
child: IconTheme.merge(
data: IconThemeData(color: resolvedForegroundColor),
child: Padding(
Expand Down
1 change: 1 addition & 0 deletions packages/flutter/lib/src/material/elevated_button.dart
Expand Up @@ -72,6 +72,7 @@ class ElevatedButton extends ButtonStyleButton {
super.focusNode,
super.autofocus = false,
super.clipBehavior = Clip.none,
super.statesController,
required super.child,
});

Expand Down

0 comments on commit d8783ff

Please sign in to comment.