diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index ecbaf6f0be..fc878d5b14 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -837,7 +837,7 @@ class RenderAnimatedOpacity extends RenderProxyBox { _alpha = _getAlphaFromOpacity(_opacity.value.clamp(0.0, 1.0)); if (oldAlpha != _alpha) { final bool didNeedCompositing = _currentlyNeedsCompositing; - _currentlyNeedsCompositing = _alpha > 0 || _alpha < 255; + _currentlyNeedsCompositing = _alpha > 0 && _alpha < 255; if (child != null && didNeedCompositing != _currentlyNeedsCompositing) markNeedsCompositingBitsUpdate(); markNeedsPaint(); diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 0de27b3a34..3bceccfd27 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -4,6 +4,7 @@ import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'package:vector_math/vector_math_64.dart'; import 'basic.dart'; @@ -12,6 +13,7 @@ import 'debug.dart'; import 'framework.dart'; import 'text.dart'; import 'ticker_provider.dart'; +import 'transitions.dart'; /// An interpolation between two [BoxConstraints]. /// @@ -257,6 +259,7 @@ abstract class ImplicitlyAnimatedWidgetState ); _updateCurve(); _constructTweens(); + didUpdateTweens(); } @override @@ -273,6 +276,7 @@ abstract class ImplicitlyAnimatedWidgetState _controller ..value = 0.0 ..forward(); + didUpdateTweens(); } } @@ -329,9 +333,23 @@ abstract class ImplicitlyAnimatedWidgetState /// as the begin value. /// /// 2. Take the value returned from the callback, and store it. This is the - /// value to use as the current value the next time that the forEachTween() + /// value to use as the current value the next time that the [forEachTween] /// method is called. + /// + /// Subclasses that contain properties based on tweens created by + /// [forEachTween] should override [didUpdateTweens] to update those + /// properties. Dependent properties should not be updated within + /// [forEachTween]. + @protected void forEachTween(TweenVisitor visitor); + + /// Optional hook for subclasses that runs after all tweens have been updated + /// via [forEachTween]. + /// + /// Any properties that depend upon tweens created by [forEachTween] should be + /// updated within [didUpdateTweens], not within [forEachTween]. + @protected + void didUpdateTweens() {} } /// A base class for widgets with implicit animations that need to rebuild their @@ -1003,18 +1021,24 @@ class AnimatedOpacity extends ImplicitlyAnimatedWidget { } } -class _AnimatedOpacityState extends AnimatedWidgetBaseState { +class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState { Tween _opacity; + Animation _opacityAnimation; @override void forEachTween(TweenVisitor visitor) { _opacity = visitor(_opacity, widget.opacity, (dynamic value) => new Tween(begin: value)); } + @override + void didUpdateTweens() { + _opacityAnimation = _opacity.animate(controller); + } + @override Widget build(BuildContext context) { - return new Opacity( - opacity: _opacity.evaluate(animation), + return new FadeTransition( + opacity: _opacityAnimation, child: widget.child ); } diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 88b2673562..5d40880a15 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -221,8 +221,8 @@ void main() { }); Iterable opacities = titles.map((Element element) { - final RenderOpacity renderOpacity = element.ancestorRenderObjectOfType(const TypeMatcher()); - return renderOpacity.opacity; + final RenderAnimatedOpacity renderOpacity = element.ancestorRenderObjectOfType(const TypeMatcher()); + return renderOpacity.opacity.value; }); expect(opacities, [ @@ -246,8 +246,8 @@ void main() { }); opacities = titles.map((Element element) { - final RenderOpacity renderOpacity = element.ancestorRenderObjectOfType(const TypeMatcher()); - return renderOpacity.opacity; + final RenderAnimatedOpacity renderOpacity = element.ancestorRenderObjectOfType(const TypeMatcher()); + return renderOpacity.opacity.value; }); expect(opacities, [ @@ -302,11 +302,11 @@ void main() { expect(find.text('Title'), findsOneWidget); expect(find.text('Different title'), findsOneWidget); - RenderOpacity largeTitleOpacity = - tester.element(find.text('Title')).ancestorRenderObjectOfType(const TypeMatcher()); + RenderAnimatedOpacity largeTitleOpacity = + tester.element(find.text('Title')).ancestorRenderObjectOfType(const TypeMatcher()); // Large title initially visible. expect( - largeTitleOpacity.opacity, + largeTitleOpacity.opacity.value, 1.0 ); // Middle widget not even wrapped with RenderOpacity, i.e. is always visible. @@ -322,10 +322,10 @@ void main() { await tester.pump(const Duration(milliseconds: 300)); largeTitleOpacity = - tester.element(find.text('Title')).ancestorRenderObjectOfType(const TypeMatcher()); + tester.element(find.text('Title')).ancestorRenderObjectOfType(const TypeMatcher()); // Large title no longer visible. expect( - largeTitleOpacity.opacity, + largeTitleOpacity.opacity.value, 0.0 ); diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index c01f72df15..dc4ddfef7f 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -78,13 +78,13 @@ double getBorderWeight(WidgetTester tester) => getBorderSide(tester)?.width; Color getBorderColor(WidgetTester tester) => getBorderSide(tester)?.color; double getHintOpacity(WidgetTester tester) { - final Opacity opacityWidget = tester.widget( + final FadeTransition opacityWidget = tester.widget( find.ancestor( of: find.text('hint'), - matching: find.byType(Opacity), + matching: find.byType(FadeTransition), ).last ); - return opacityWidget.opacity; + return opacityWidget.opacity.value; } void main() { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 73258add2c..bb2b0aab20 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -105,12 +105,12 @@ Future skipPastScrollingAnimation(WidgetTester tester) async { } double getOpacity(WidgetTester tester, Finder finder) { - return tester.widget( + return tester.widget( find.ancestor( of: finder, - matching: find.byType(Opacity), + matching: find.byType(FadeTransition), ) - ).opacity; + ).opacity.value; } void main() { diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index fc4f34d0f6..08ae7584a0 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:typed_data'; - import 'dart:ui' as ui show Image; + +import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/src/scheduler/ticker.dart'; import 'package:test/test.dart'; import 'rendering_tester.dart'; @@ -190,4 +192,99 @@ void main() { expect(data.getUint32(0), equals(0x00000080)); expect(data.getUint32(stride - 4), equals(0xffffffff)); }); + + test('RenderOpacity does not composite if it is transparent', () { + final RenderOpacity renderOpacity = new RenderOpacity( + opacity: 0.0, + child: new RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter + ); + + layout(renderOpacity, phase: EnginePhase.composite); + expect(renderOpacity.needsCompositing, false); + }); + + test('RenderOpacity does not composite if it is opaque', () { + final RenderOpacity renderOpacity = new RenderOpacity( + opacity: 1.0, + child: new RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter + ); + + layout(renderOpacity, phase: EnginePhase.composite); + expect(renderOpacity.needsCompositing, false); + }); + + test('RenderAnimatedOpacity does not composite if it is transparent', () async { + final Animation opacityAnimation = new AnimationController( + vsync: new _FakeTickerProvider(), + )..value = 0.0; + + final RenderAnimatedOpacity renderAnimatedOpacity = new RenderAnimatedOpacity( + opacity: opacityAnimation, + child: new RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter + ); + + layout(renderAnimatedOpacity, phase: EnginePhase.composite); + expect(renderAnimatedOpacity.needsCompositing, false); + }); + + test('RenderAnimatedOpacity does not composite if it is opaque', () { + final Animation opacityAnimation = new AnimationController( + vsync: new _FakeTickerProvider(), + )..value = 1.0; + + final RenderAnimatedOpacity renderAnimatedOpacity = new RenderAnimatedOpacity( + opacity: opacityAnimation, + child: new RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter + ); + + layout(renderAnimatedOpacity, phase: EnginePhase.composite); + expect(renderAnimatedOpacity.needsCompositing, false); + }); } + +class _FakeTickerProvider implements TickerProvider { + @override + Ticker createTicker(TickerCallback onTick) { + return new _FakeTicker(); + } +} + +class _FakeTicker implements Ticker { + @override + bool muted; + + @override + void absorbTicker(Ticker originalTicker) {} + + @override + String get debugLabel => null; + + @override + bool get isActive => null; + + @override + bool get isTicking => null; + + @override + bool get scheduled => null; + + @override + bool get shouldScheduleTick => null; + + @override + void dispose() {} + + @override + void scheduleTick({bool rescheduling = false}) {} + + @override + TickerFuture start() { + return null; + } + + @override + void stop({bool canceled = false}) {} + + @override + void unscheduleTick() {} +} \ No newline at end of file