AnimatedCrossFade: shut off animations & semantics in faded out widgets (#11276)
* AnimatedCrossFade: shut off animations & semantics in faded out widgets * address comments
This commit is contained in:
parent
63b686709d
commit
02b65bc984
@ -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(
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
];
|
||||
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 {
|
||||
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,
|
||||
),
|
||||
),
|
||||
];
|
||||
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 ExcludeSemantics(
|
||||
excluding: true, // always exclude the semantics of the widget that's fading out
|
||||
child: new FadeTransition(
|
||||
opacity: bottomAnimation,
|
||||
child: bottomChild,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user