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) {
|
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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
// TODO(dragostis): Add a way to crop from top right for
|
Key bottomKey;
|
||||||
// right-to-left languages.
|
Widget bottomChild;
|
||||||
left: 0.0,
|
Animation<double> bottomAnimation;
|
||||||
top: 0.0,
|
if (transitioningForwards) {
|
||||||
right: 0.0,
|
topKey = kSecondChildKey;
|
||||||
child: new FadeTransition(
|
topChild = widget.secondChild;
|
||||||
opacity: _firstAnimation,
|
topAnimation = _secondAnimation;
|
||||||
child: widget.firstChild,
|
bottomKey = kFirstChildKey;
|
||||||
),
|
bottomChild = widget.firstChild;
|
||||||
),
|
bottomAnimation = _firstAnimation;
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
children = <Widget>[
|
topKey = kFirstChildKey;
|
||||||
new FadeTransition(
|
topChild = widget.firstChild;
|
||||||
opacity: _firstAnimation,
|
topAnimation = _firstAnimation;
|
||||||
child: widget.firstChild,
|
bottomKey = kSecondChildKey;
|
||||||
),
|
bottomChild = widget.secondChild;
|
||||||
new Positioned(
|
bottomAnimation = _secondAnimation;
|
||||||
// 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
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
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
child: new Stack(
|
child: new Stack(
|
||||||
overflow: Overflow.visible,
|
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
|
// 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() {
|
|||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user