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
}
Animation<double> _initAnimation(Curve curve, bool inverted) {
final CurvedAnimation animation = new CurvedAnimation(
Animation<double> animation = new CurvedAnimation(
parent: _controller,
curve: curve
);
return inverted ? new Tween<double>(
begin: 1.0,
end: 0.0
).animate(animation) : animation;
if (inverted) {
animation = new Tween<double>(
begin: 1.0,
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
......@@ -189,49 +200,73 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid
}
}
@override
Widget build(BuildContext context) {
List<Widget> children;
/// Whether we're in the middle of cross-fading this frame.
bool get _isTransitioning => _controller.status == AnimationStatus.forward || _controller.status == AnimationStatus.reverse;
if (_controller.status == AnimationStatus.completed ||
_controller.status == AnimationStatus.forward) {
children = <Widget>[
new FadeTransition(
opacity: _secondAnimation,
child: widget.secondChild,
),
new Positioned(
List<Widget> _buildCrossFadedChildren() {
const Key kFirstChildKey = const ValueKey<CrossFadeState>(CrossFadeState.showFirst);
const Key kSecondChildKey = const ValueKey<CrossFadeState>(CrossFadeState.showSecond);
final bool transitioningForwards = _controller.status == AnimationStatus.completed || _controller.status == AnimationStatus.forward;
Key topKey;
Widget topChild;
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
// right-to-left languages.
left: 0.0,
top: 0.0,
right: 0.0,
child: new FadeTransition(
opacity: _firstAnimation,
child: widget.firstChild,
child: new ExcludeSemantics(
excluding: true, // always exclude the semantics of the widget that's fading out
child: new FadeTransition(
opacity: bottomAnimation,
child: bottomChild,
),
),
),
];
} else {
children = <Widget>[
new FadeTransition(
opacity: _firstAnimation,
child: widget.firstChild,
),
new Positioned(
// TODO(dragostis): Add a way to crop from top right for
// right-to-left languages.
left: 0.0,
top: 0.0,
right: 0.0,
child: new FadeTransition(
opacity: _secondAnimation,
child: widget.secondChild,
),
new TickerMode(
key: topKey,
enabled: true, // top widget always has its animations enabled
child: new Positioned(
child: new ExcludeSemantics(
excluding: false, // always publish semantics for the widget that's fading in
child: new FadeTransition(
opacity: topAnimation,
child: topChild,
),
),
),
];
}
),
];
}
@override
Widget build(BuildContext context) {
return new ClipRect(
child: new AnimatedSize(
key: new ValueKey<Key>(widget.key),
......@@ -241,7 +276,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProvid
vsync: this,
child: new Stack(
overflow: Overflow.visible,
children: children,
children: _buildCrossFadedChildren(),
),
),
);
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -131,4 +132,81 @@ void main() {
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