Unverified Commit 51dd5bf1 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added ButtonStyle.splashFactory and NoSplash.splashFactory (#74949)

parent 9c434c82
......@@ -99,6 +99,7 @@ export 'src/material/material_state.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/navigation_rail.dart';
export 'src/material/navigation_rail_theme.dart';
export 'src/material/no_splash.dart';
export 'src/material/outline_button.dart';
export 'src/material/outlined_button.dart';
export 'src/material/outlined_button_theme.dart';
......
......@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'theme_data.dart';
......@@ -116,6 +117,7 @@ class ButtonStyle with Diagnosticable {
this.animationDuration,
this.enableFeedback,
this.alignment,
this.splashFactory,
});
/// The style for a button's [Text] widget descendants.
......@@ -230,6 +232,21 @@ class ButtonStyle with Diagnosticable {
/// Always defaults to [Alignment.center].
final AlignmentGeometry? alignment;
/// Creates the [InkWell] splash factory, which defines the appearance of
/// "ink" splashes that occur in response to taps.
///
/// Use [NoSplash.splashFactory] to defeat ink splash rendering. For example:
/// ```dart
/// ElevatedButton(
/// style: ElevatedButton.styleFrom(
/// splashFactory: NoSplash.splashFactory,
/// ),
/// onPressed: () { },
/// child: Text('No Splash'),
/// )
/// ```
final InteractiveInkFeatureFactory? splashFactory;
/// Returns a copy of this ButtonStyle with the given fields replaced with
/// the new values.
ButtonStyle copyWith({
......@@ -250,6 +267,7 @@ class ButtonStyle with Diagnosticable {
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
return ButtonStyle(
textStyle: textStyle ?? this.textStyle,
......@@ -269,6 +287,7 @@ class ButtonStyle with Diagnosticable {
animationDuration: animationDuration ?? this.animationDuration,
enableFeedback: enableFeedback ?? this.enableFeedback,
alignment: alignment ?? this.alignment,
splashFactory: splashFactory ?? this.splashFactory,
);
}
......@@ -298,6 +317,7 @@ class ButtonStyle with Diagnosticable {
animationDuration: animationDuration ?? style.animationDuration,
enableFeedback: enableFeedback ?? style.enableFeedback,
alignment: alignment ?? style.alignment,
splashFactory: splashFactory ?? style.splashFactory,
);
}
......@@ -321,6 +341,7 @@ class ButtonStyle with Diagnosticable {
animationDuration,
enableFeedback,
alignment,
splashFactory,
);
}
......@@ -347,7 +368,8 @@ class ButtonStyle with Diagnosticable {
&& other.tapTargetSize == tapTargetSize
&& other.animationDuration == animationDuration
&& other.enableFeedback == enableFeedback
&& other.alignment == alignment;
&& other.alignment == alignment
&& other.splashFactory == splashFactory;
}
@override
......@@ -395,6 +417,7 @@ class ButtonStyle with Diagnosticable {
animationDuration: t < 0.5 ? a?.animationDuration : b?.animationDuration,
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t),
splashFactory: t < 0.5 ? a?.splashFactory : b?.splashFactory,
);
}
......
......@@ -11,7 +11,6 @@ import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'colors.dart';
import 'constants.dart';
import 'ink_ripple.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_state.dart';
......@@ -285,6 +284,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
final bool? resolvedEnableFeedback = effectiveValue((ButtonStyle? style) => style?.enableFeedback);
final AlignmentGeometry? resolvedAlignment = effectiveValue((ButtonStyle? style) => style?.alignment);
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue((ButtonStyle? style) => style?.splashFactory);
BoxConstraints effectiveConstraints = resolvedVisualDensity.effectiveConstraints(
BoxConstraints(
......@@ -370,7 +370,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
canRequestFocus: widget.enabled,
onFocusChange: _handleFocusedChanged,
autofocus: widget.autofocus,
splashFactory: InkRipple.splashFactory,
splashFactory: resolvedSplashFactory,
overlayColor: overlayColor,
highlightColor: Colors.transparent,
customBorder: resolvedShape,
......
......@@ -14,6 +14,8 @@ import 'button_style_button.dart';
import 'color_scheme.dart';
import 'constants.dart';
import 'elevated_button_theme.dart';
import 'ink_ripple.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
......@@ -149,6 +151,7 @@ class ElevatedButton extends ButtonStyleButton {
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? backgroundColor = (onSurface == null && primary == null)
? null
......@@ -184,6 +187,7 @@ class ElevatedButton extends ButtonStyleButton {
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
);
}
......@@ -244,6 +248,7 @@ class ElevatedButton extends ButtonStyleButton {
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - InkRipple.splashFactory
///
/// The default padding values for the [ElevatedButton.icon] factory are slightly different:
///
......@@ -287,6 +292,7 @@ class ElevatedButton extends ButtonStyleButton {
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
splashFactory: InkRipple.splashFactory,
);
}
......
......@@ -183,8 +183,8 @@ class InkRipple extends InteractiveInkFeature {
late Animation<int> _fadeOut;
late AnimationController _fadeOutController;
/// Used to specify this type of ink splash for an [InkWell], [InkResponse]
/// or material [Theme].
/// Used to specify this type of ink splash for an [InkWell], [InkResponse],
/// material [Theme], or [ButtonStyle].
static const InteractiveInkFeatureFactory splashFactory = _InkRippleFactory();
static final Animatable<double> _easeCurveTween = CurveTween(curve: Curves.ease);
......
......@@ -166,8 +166,8 @@ class InkSplash extends InteractiveInkFeature {
late Animation<int> _alpha;
AnimationController? _alphaController;
/// Used to specify this type of ink splash for an [InkWell], [InkResponse]
/// or material [Theme].
/// Used to specify this type of ink splash for an [InkWell], [InkResponse],
/// material [Theme], or [ButtonStyle].
static const InteractiveInkFeatureFactory splashFactory = _InkSplashFactory();
@override
......
// 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/rendering.dart';
import 'package:flutter/widgets.dart';
import 'ink_well.dart';
import 'material.dart';
class _NoSplashFactory extends InteractiveInkFeatureFactory {
const _NoSplashFactory();
@override
InteractiveInkFeature create({
required MaterialInkController controller,
required RenderBox referenceBox,
required Offset position,
required Color color,
required TextDirection textDirection,
bool containedInkWell = false,
RectCallback? rectCallback,
BorderRadius? borderRadius,
ShapeBorder? customBorder,
double? radius,
VoidCallback? onRemoved,
}) {
return NoSplash(
controller: controller,
referenceBox: referenceBox,
color: color
);
}
}
/// An [InteractiveInkFeature] that doesn't paint a splash.
///
/// Use [NoSplash.splashFactory] to defeat the default ink splash drawn by
/// an [InkWell] or [ButtonStyle]. For example, to create an [ElevatedButton]
/// that does not draw the default "ripple" ink splash when it's tapped:
/// ```dart
/// ElevatedButton(
/// style: ElevatedButton.styleFrom(
/// splashFactory: NoSplash.splashFactory,
/// ),
/// onPressed: () { },
/// child: Text('No Splash'),
/// )
/// ```
class NoSplash extends InteractiveInkFeature {
/// Create an [InteractiveInkFeature] that doesn't paint a splash.
NoSplash({
required MaterialInkController controller,
required RenderBox referenceBox,
required Color color,
VoidCallback? onRemoved,
}) : super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved);
/// Used to specify this type of ink splash for an [InkWell], [InkResponse]
/// material [Theme], or [ButtonStyle].
static const InteractiveInkFeatureFactory splashFactory = _NoSplashFactory();
@override
void paintFeature(Canvas canvas, Matrix4 transform) {
}
}
......@@ -14,6 +14,8 @@ import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'ink_ripple.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'outlined_button_theme.dart';
import 'theme.dart';
......@@ -141,6 +143,7 @@ class OutlinedButton extends ButtonStyleButton {
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? foregroundColor = (onSurface == null && primary == null)
? null
......@@ -170,6 +173,7 @@ class OutlinedButton extends ButtonStyleButton {
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
);
}
......@@ -223,6 +227,7 @@ class OutlinedButton extends ButtonStyleButton {
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - InkRipple.splashFactory
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
......@@ -256,6 +261,7 @@ class OutlinedButton extends ButtonStyleButton {
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
splashFactory: InkRipple.splashFactory,
);
}
......
......@@ -14,6 +14,8 @@ import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'ink_ripple.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'text_button_theme.dart';
import 'theme.dart';
......@@ -147,6 +149,7 @@ class TextButton extends ButtonStyleButton {
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? foregroundColor = (onSurface == null && primary == null)
? null
......@@ -176,6 +179,7 @@ class TextButton extends ButtonStyleButton {
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
);
}
......@@ -232,6 +236,7 @@ class TextButton extends ButtonStyleButton {
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - InkRipple.splashFactory
///
/// The default padding values for the [TextButton.icon] factory are slightly different:
///
......@@ -274,6 +279,7 @@ class TextButton extends ButtonStyleButton {
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
splashFactory: InkRipple.splashFactory,
);
}
......
......@@ -1072,6 +1072,46 @@ void main() {
expect(tester.getSize(find.widgetWithText(ElevatedButton, '200xh')).width, 200);
expect(tester.getSize(find.widgetWithText(ElevatedButton, 'wx200')).height, 200);
});
testWidgets('ElevatedButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async {
Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
splashFactory: splashFactory,
),
onPressed: () { },
child: const Text('test'),
),
),
),
);
}
// NoSplash.splashFactory, no splash circles drawn
await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
await gesture.up();
await tester.pumpAndSettle();
}
// Default splashFactory (from Theme.of().splashFactory), one splash circle drawn.
await tester.pumpWidget(buildFrame());
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
await gesture.up();
await tester.pumpAndSettle();
}
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
......@@ -5,6 +5,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
void main() {
// Regression test for https://github.com/flutter/flutter/issues/21506.
testWidgets('InkSplash receives textDirection', (WidgetTester tester) async {
......@@ -23,4 +25,44 @@ void main() {
await tester.pumpAndSettle(const Duration(milliseconds: 30));
expect(tester.takeException(), isNull);
});
testWidgets('InkWell with NoSplash splashFactory paints nothing', (WidgetTester tester) async {
Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Material(
child: InkWell(
splashFactory: splashFactory,
onTap: () { },
child: const Text('test'),
),
),
),
),
);
}
// NoSplash.splashFactory, no splash circles drawn
await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
await gesture.up();
await tester.pumpAndSettle();
}
// Default splashFactory (from Theme.of().splashFactory), one splash circle drawn.
await tester.pumpWidget(buildFrame());
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
await gesture.up();
await tester.pumpAndSettle();
}
});
}
......@@ -1248,6 +1248,46 @@ void main() {
expect(tester.getSize(find.widgetWithText(OutlinedButton, '200xh')).width, 200);
expect(tester.getSize(find.widgetWithText(OutlinedButton, 'wx200')).height, 200);
});
testWidgets('OutlinedButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async {
Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
splashFactory: splashFactory,
),
onPressed: () { },
child: const Text('test'),
),
),
),
);
}
// NoSplash.splashFactory, no splash circles drawn
await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
await gesture.up();
await tester.pumpAndSettle();
}
// Default splashFactory (from Theme.of().splashFactory), one splash circle drawn.
await tester.pumpWidget(buildFrame());
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
await gesture.up();
await tester.pumpAndSettle();
}
});
}
PhysicalModelLayer _findPhysicalLayer(Element element) {
......
......@@ -1044,6 +1044,46 @@ void main() {
expect(tester.getSize(find.widgetWithText(TextButton, '200xh')).width, 200);
expect(tester.getSize(find.widgetWithText(TextButton, 'wx200')).height, 200);
});
testWidgets('TextButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async {
Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) {
return MaterialApp(
home: Scaffold(
body: Center(
child: TextButton(
style: TextButton.styleFrom(
splashFactory: splashFactory,
),
onPressed: () { },
child: const Text('test'),
),
),
),
);
}
// NoSplash.splashFactory, no splash circles drawn
await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
await gesture.up();
await tester.pumpAndSettle();
}
// Default splashFactory (from Theme.of().splashFactory), one splash circle drawn.
await tester.pumpWidget(buildFrame());
{
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
final MaterialInkController material = Material.of(tester.element(find.text('test')))!;
await tester.pump(const Duration(milliseconds: 200));
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
await gesture.up();
await tester.pumpAndSettle();
}
});
}
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment