Unverified Commit bc396d1b authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add onVisible callback to snackbar. (#42344)

parent a7aa6616
...@@ -161,7 +161,7 @@ class _SnackBarActionState extends State<SnackBarAction> { ...@@ -161,7 +161,7 @@ class _SnackBarActionState extends State<SnackBarAction> {
/// * [SnackBarThemeData], to configure the default property values for /// * [SnackBarThemeData], to configure the default property values for
/// [SnackBar] widgets. /// [SnackBar] widgets.
/// * <https://material.io/design/components/snackbars.html> /// * <https://material.io/design/components/snackbars.html>
class SnackBar extends StatelessWidget { class SnackBar extends StatefulWidget {
/// Creates a snack bar. /// Creates a snack bar.
/// ///
/// The [content] argument must be non-null. The [elevation] must be null or /// The [content] argument must be non-null. The [elevation] must be null or
...@@ -176,6 +176,7 @@ class SnackBar extends StatelessWidget { ...@@ -176,6 +176,7 @@ class SnackBar extends StatelessWidget {
this.action, this.action,
this.duration = _snackBarDisplayDuration, this.duration = _snackBarDisplayDuration,
this.animation, this.animation,
this.onVisible,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(content != null), assert(content != null),
assert(duration != null), assert(duration != null),
...@@ -245,10 +246,86 @@ class SnackBar extends StatelessWidget { ...@@ -245,10 +246,86 @@ class SnackBar extends StatelessWidget {
/// The animation driving the entrance and exit of the snack bar. /// The animation driving the entrance and exit of the snack bar.
final Animation<double> animation; final Animation<double> animation;
/// Called the first time that the snackbar is visible within a [Scaffold].
final VoidCallback onVisible;
// API for Scaffold.showSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ @required TickerProvider vsync }) {
return AnimationController(
duration: _snackBarTransitionDuration,
debugLabel: 'SnackBar',
vsync: vsync,
);
}
/// Creates a copy of this snack bar but with the animation replaced with the given animation.
///
/// If the original snack bar lacks a key, the newly created snack bar will
/// use the given fallback key.
SnackBar withAnimation(Animation<double> newAnimation, { Key fallbackKey }) {
return SnackBar(
key: key ?? fallbackKey,
content: content,
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
behavior: behavior,
action: action,
duration: duration,
animation: newAnimation,
onVisible: onVisible,
);
}
@override
State<SnackBar> createState() => _SnackBarState();
}
class _SnackBarState extends State<SnackBar> {
bool _wasVisible = false;
@override
void initState() {
super.initState();
widget.animation.addStatusListener(_onAnimationStatusChanged);
}
@override
void didUpdateWidget(SnackBar oldWidget) {
if (widget.animation != oldWidget.animation) {
oldWidget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.animation.removeStatusListener(_onAnimationStatusChanged);
super.dispose();
}
void _onAnimationStatusChanged(AnimationStatus animationStatus) {
switch (animationStatus) {
case AnimationStatus.dismissed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
case AnimationStatus.completed:
if (widget.onVisible != null && !_wasVisible) {
widget.onVisible();
}
_wasVisible = true;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context); final MediaQueryData mediaQueryData = MediaQuery.of(context);
assert(animation != null); assert(widget.animation != null);
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme; final ColorScheme colorScheme = theme.colorScheme;
final SnackBarThemeData snackBarTheme = theme.snackBarTheme; final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
...@@ -284,14 +361,14 @@ class SnackBar extends StatelessWidget { ...@@ -284,14 +361,14 @@ class SnackBar extends StatelessWidget {
); );
final TextStyle contentTextStyle = snackBarTheme.contentTextStyle ?? inverseTheme.textTheme.subhead; final TextStyle contentTextStyle = snackBarTheme.contentTextStyle ?? inverseTheme.textTheme.subhead;
final SnackBarBehavior snackBarBehavior = behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed; final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating; final bool isFloatingSnackBar = snackBarBehavior == SnackBarBehavior.floating;
final double snackBarPadding = isFloatingSnackBar ? 16.0 : 24.0; final double snackBarPadding = isFloatingSnackBar ? 16.0 : 24.0;
final CurvedAnimation heightAnimation = CurvedAnimation(parent: animation, curve: _snackBarHeightCurve); final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation, curve: _snackBarHeightCurve);
final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: animation, curve: _snackBarFadeInCurve); final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation, curve: _snackBarFadeInCurve);
final CurvedAnimation fadeOutAnimation = CurvedAnimation( final CurvedAnimation fadeOutAnimation = CurvedAnimation(
parent: animation, parent: widget.animation,
curve: _snackBarFadeOutCurve, curve: _snackBarFadeOutCurve,
reverseCurve: const Threshold(0.0), reverseCurve: const Threshold(0.0),
); );
...@@ -308,16 +385,16 @@ class SnackBar extends StatelessWidget { ...@@ -308,16 +385,16 @@ class SnackBar extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding), padding: const EdgeInsets.symmetric(vertical: _singleLineVerticalPadding),
child: DefaultTextStyle( child: DefaultTextStyle(
style: contentTextStyle, style: contentTextStyle,
child: content, child: widget.content,
), ),
), ),
), ),
if (action != null) if (widget.action != null)
ButtonTheme( ButtonTheme(
textTheme: ButtonTextTheme.accent, textTheme: ButtonTextTheme.accent,
minWidth: 64.0, minWidth: 64.0,
padding: EdgeInsets.symmetric(horizontal: snackBarPadding), padding: EdgeInsets.symmetric(horizontal: snackBarPadding),
child: action, child: widget.action,
) )
else else
SizedBox(width: snackBarPadding), SizedBox(width: snackBarPadding),
...@@ -325,9 +402,9 @@ class SnackBar extends StatelessWidget { ...@@ -325,9 +402,9 @@ class SnackBar extends StatelessWidget {
), ),
); );
final double elevation = this.elevation ?? snackBarTheme.elevation ?? 6.0; final double elevation = widget.elevation ?? snackBarTheme.elevation ?? 6.0;
final Color backgroundColor = this.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.backgroundColor; final Color backgroundColor = widget.backgroundColor ?? snackBarTheme.backgroundColor ?? inverseTheme.backgroundColor;
final ShapeBorder shape = this.shape final ShapeBorder shape = widget.shape
?? snackBarTheme.shape ?? snackBarTheme.shape
?? (isFloatingSnackBar ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)) : null); ?? (isFloatingSnackBar ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)) : null);
...@@ -394,33 +471,4 @@ class SnackBar extends StatelessWidget { ...@@ -394,33 +471,4 @@ class SnackBar extends StatelessWidget {
return ClipRect(child: snackBarTransition); return ClipRect(child: snackBarTransition);
} }
// API for Scaffold.addSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController({ @required TickerProvider vsync }) {
return AnimationController(
duration: _snackBarTransitionDuration,
debugLabel: 'SnackBar',
vsync: vsync,
);
}
/// Creates a copy of this snack bar but with the animation replaced with the given animation.
///
/// If the original snack bar lacks a key, the newly created snack bar will
/// use the given fallback key.
SnackBar withAnimation(Animation<double> newAnimation, { Key fallbackKey }) {
return SnackBar(
key: key ?? fallbackKey,
content: content,
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
behavior: behavior,
action: action,
duration: duration,
animation: newAnimation,
);
}
} }
...@@ -1065,4 +1065,85 @@ void main() { ...@@ -1065,4 +1065,85 @@ void main() {
await tester.tap(find.byKey(tapTarget)); await tester.tap(find.byKey(tapTarget));
expect(tester.takeException(), isNotNull); expect(tester.takeException(), isNotNull);
}); });
testWidgets('Snackbar calls onVisible once', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
int called = 0;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: const Text('hello'),
duration: const Duration(seconds: 1),
onVisible: () {
called += 1;
},
));
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pump(); // start animation
await tester.pumpAndSettle();
expect(find.text('hello'), findsOneWidget);
expect(called, 1);
});
testWidgets('Snackbar does not call onVisible when it is queued', (WidgetTester tester) async {
const Key tapTarget = Key('tap-target');
int called = 0;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: const Text('hello'),
duration: const Duration(seconds: 1),
onVisible: () {
called += 1;
},
));
Scaffold.of(context).showSnackBar(SnackBar(
content: const Text('hello 2'),
duration: const Duration(seconds: 1),
onVisible: () {
called += 1;
},
));
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 100.0,
width: 100.0,
key: tapTarget,
),
);
},
),
),
));
await tester.tap(find.byKey(tapTarget));
await tester.pump(); // start animation
await tester.pumpAndSettle();
expect(find.text('hello'), findsOneWidget);
expect(called, 1);
});
} }
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