Commit 02b65bc9 authored by Yegor's avatar Yegor Committed by GitHub

AnimatedCrossFade: shut off animations & semantics in faded out widgets (#11276)

* AnimatedCrossFade: shut off animations & semantics in faded out widgets

* address comments
parent 63b68670
...@@ -151,15 +151,26 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid ...@@ -151,15 +151,26 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid
} }
Animation<double> _initAnimation(Curve curve, bool inverted) { Animation<double> _initAnimation(Curve curve, bool inverted) {
final CurvedAnimation animation = new CurvedAnimation( Animation<double> animation = new CurvedAnimation(
parent: _controller, parent: _controller,
curve: curve curve: curve
); );
return inverted ? new Tween<double>( if (inverted) {
begin: 1.0, animation = new Tween<double>(
end: 0.0 begin: 1.0,
).animate(animation) : animation; end: 0.0
).animate(animation);
}
animation.addStatusListener((AnimationStatus status) {
setState(() {
// Trigger a rebuild because it depends on _isTransitioning, which
// changes its value together with animation status.
});
});
return animation;
} }
@override @override
...@@ -189,49 +200,73 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid ...@@ -189,49 +200,73 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid
} }
} }
@override /// Whether we're in the middle of cross-fading this frame.
Widget build(BuildContext context) { bool get _isTransitioning => _controller.status == AnimationStatus.forward || _controller.status == AnimationStatus.reverse;
List<Widget> children;
if (_controller.status == AnimationStatus.completed || List<Widget> _buildCrossFadedChildren() {
_controller.status == AnimationStatus.forward) { const Key kFirstChildKey = const ValueKey<CrossFadeState>(CrossFadeState.showFirst);
children = <Widget>[ const Key kSecondChildKey = const ValueKey<CrossFadeState>(CrossFadeState.showSecond);
new FadeTransition( final bool transitioningForwards = _controller.status == AnimationStatus.completed || _controller.status == AnimationStatus.forward;
opacity: _secondAnimation,
child: widget.secondChild, Key topKey;
), Widget topChild;
new Positioned( Animation<double> topAnimation;
Key bottomKey;
Widget bottomChild;
Animation<double> bottomAnimation;
if (transitioningForwards) {
topKey = kSecondChildKey;
topChild = widget.secondChild;
topAnimation = _secondAnimation;
bottomKey = kFirstChildKey;
bottomChild = widget.firstChild;
bottomAnimation = _firstAnimation;
} else {
topKey = kFirstChildKey;
topChild = widget.firstChild;
topAnimation = _firstAnimation;
bottomKey = kSecondChildKey;
bottomChild = widget.secondChild;
bottomAnimation = _secondAnimation;
}
return <Widget>[
new TickerMode(
key: bottomKey,
enabled: _isTransitioning,
child: new Positioned(
// TODO(dragostis): Add a way to crop from top right for // TODO(dragostis): Add a way to crop from top right for
// right-to-left languages. // right-to-left languages.
left: 0.0, left: 0.0,
top: 0.0, top: 0.0,
right: 0.0, right: 0.0,
child: new FadeTransition( child: new ExcludeSemantics(
opacity: _firstAnimation, excluding: true, // always exclude the semantics of the widget that's fading out
child: widget.firstChild, child: new FadeTransition(
opacity: bottomAnimation,
child: bottomChild,
),
), ),
), ),
]; ),
} else { new TickerMode(
children = <Widget>[ key: topKey,
new FadeTransition( enabled: true, // top widget always has its animations enabled
opacity: _firstAnimation, child: new Positioned(
child: widget.firstChild, child: new ExcludeSemantics(
), excluding: false, // always publish semantics for the widget that's fading in
new Positioned( child: new FadeTransition(
// TODO(dragostis): Add a way to crop from top right for opacity: topAnimation,
// right-to-left languages. child: topChild,
left: 0.0, ),
top: 0.0,
right: 0.0,
child: new FadeTransition(
opacity: _secondAnimation,
child: widget.secondChild,
), ),
), ),
]; ),
} ];
}
@override
Widget build(BuildContext context) {
return new ClipRect( return new ClipRect(
child: new AnimatedSize( child: new AnimatedSize(
key: new ValueKey<Key>(widget.key), key: new ValueKey<Key>(widget.key),
...@@ -241,7 +276,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid ...@@ -241,7 +276,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid
vsync: this, vsync: this,
child: new Stack( child: new Stack(
overflow: Overflow.visible, overflow: Overflow.visible,
children: children, children: _buildCrossFadedChildren(),
), ),
), ),
); );
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -131,4 +132,81 @@ void main() { ...@@ -131,4 +132,81 @@ void main() {
expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0));
}); });
Widget crossFadeWithWatcher({bool towardsSecond: false}) {
return new AnimatedCrossFade(
firstChild: const _TickerWatchingWidget(),
secondChild: new Container(),
crossFadeState: towardsSecond ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 50),
);
}
testWidgets('AnimatedCrossFade preserves widget state', (WidgetTester tester) async {
await tester.pumpWidget(crossFadeWithWatcher());
_TickerWatchingWidgetState findState() => tester.state(find.byType(_TickerWatchingWidget));
final _TickerWatchingWidgetState state = findState();
await tester.pumpWidget(crossFadeWithWatcher(towardsSecond: true));
for (int i = 0; i < 3; i += 1) {
await tester.pump(const Duration(milliseconds: 25));
expect(findState(), same(state));
}
});
testWidgets('AnimatedCrossFade switches off TickerMode and semantics on faded out widget', (WidgetTester tester) async {
ExcludeSemantics findSemantics() {
return tester.widget(find.descendant(
of: find.byKey(const ValueKey<CrossFadeState>(CrossFadeState.showFirst)),
matching: find.byType(ExcludeSemantics),
));
}
await tester.pumpWidget(crossFadeWithWatcher());
final _TickerWatchingWidgetState state = tester.state(find.byType(_TickerWatchingWidget));
expect(state.ticker.muted, false);
expect(findSemantics().excluding, false);
await tester.pumpWidget(crossFadeWithWatcher(towardsSecond: true));
for (int i = 0; i < 2; i += 1) {
await tester.pump(const Duration(milliseconds: 25));
// Animations are kept alive in the middle of cross-fade
expect(state.ticker.muted, false);
// Semantics are turned off immediately on the widget that's fading out
expect(findSemantics().excluding, true);
}
// In the final state both animations and semantics should be off on the
// widget that's faded out.
await tester.pump(const Duration(milliseconds: 25));
expect(state.ticker.muted, true);
expect(findSemantics().excluding, true);
});
}
class _TickerWatchingWidget extends StatefulWidget {
const _TickerWatchingWidget();
@override
State<StatefulWidget> createState() => new _TickerWatchingWidgetState();
}
class _TickerWatchingWidgetState extends State<_TickerWatchingWidget> with SingleTickerProviderStateMixin {
Ticker ticker;
@override
void initState() {
super.initState();
ticker = createTicker((_) {})..start();
}
@override
Widget build(BuildContext context) => new Container();
@override
void dispose() {
ticker.dispose();
super.dispose();
}
} }
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