Unverified Commit bed71b9a authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Make FadeTransition more efficient (#14155)

* Make FadeTransition more efficient

* Update fade_transition_test.dart

* Update proxy_box.dart
parent 3ac9449a
...@@ -152,8 +152,8 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo> ...@@ -152,8 +152,8 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo>
// We want to have the newly animating (fading in) views on top. // We want to have the newly animating (fading in) views on top.
transitions.sort((FadeTransition a, FadeTransition b) { transitions.sort((FadeTransition a, FadeTransition b) {
final Animation<double> aAnimation = a.listenable; final Animation<double> aAnimation = a.opacity;
final Animation<double> bAnimation = b.listenable; final Animation<double> bAnimation = b.opacity;
final double aValue = aAnimation.value; final double aValue = aAnimation.value;
final double bValue = bAnimation.value; final double bValue = bAnimation.value;
return aValue.compareTo(bValue); return aValue.compareTo(bValue);
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' as ui show ImageFilter, Gradient; import 'dart:ui' as ui show ImageFilter, Gradient;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
...@@ -776,6 +777,99 @@ class RenderOpacity extends RenderProxyBox { ...@@ -776,6 +777,99 @@ class RenderOpacity extends RenderProxyBox {
} }
} }
/// Makes its child partially transparent, driven from an [Animation].
///
/// This is a variant of [RenderOpacity] that uses an [Animation<double>] rather
/// than a [double] to control the opacity.
class RenderAnimatedOpacity extends RenderProxyBox {
/// Creates a partially transparent render object.
///
/// The [opacity] argument must not be null.
RenderAnimatedOpacity({ @required Animation<double> opacity, RenderBox child }) : super(child) {
this.opacity = opacity;
}
int _alpha;
@override
bool get alwaysNeedsCompositing => child != null && _currentlyNeedsCompositing;
bool _currentlyNeedsCompositing;
/// The animation that drives this render object's opacity.
///
/// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent
/// (i.e., invisible).
///
/// To change the opacity of a child in a static manner, not animated,
/// consider [RenderOpacity] instead.
Animation<double> get opacity => _opacity;
Animation<double> _opacity;
set opacity(Animation<double> value) {
assert(value != null);
if (_opacity == value)
return;
if (attached && _opacity != null)
_opacity.removeListener(_updateOpacity);
_opacity = value;
if (attached)
_opacity.addListener(_updateOpacity);
_updateOpacity();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_opacity.addListener(_updateOpacity);
_updateOpacity(); // in case it changed while we weren't listening
}
@override
void detach() {
_opacity.removeListener(_updateOpacity);
super.detach();
}
void _updateOpacity() {
final int oldAlpha = _alpha;
_alpha = _getAlphaFromOpacity(_opacity.value.clamp(0.0, 1.0));
if (oldAlpha != _alpha) {
final bool didNeedCompositing = _currentlyNeedsCompositing;
_currentlyNeedsCompositing = _alpha > 0 || _alpha < 255;
if (child != null && didNeedCompositing != _currentlyNeedsCompositing)
markNeedsCompositingBitsUpdate();
markNeedsPaint();
if (oldAlpha == 0 || _alpha == 0)
markNeedsSemanticsUpdate();
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0)
return;
if (_alpha == 255) {
context.paintChild(child, offset);
return;
}
assert(needsCompositing);
context.pushOpacity(offset, _alpha, super.paint);
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && _alpha != 0)
visitor(child);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<Animation<double>>('opacity', opacity));
}
}
/// Signature for a function that creates a [Shader] for a given [Rect]. /// Signature for a function that creates a [Shader] for a given [Rect].
/// ///
/// Used by [RenderShaderMask] and the [ShaderMask] widget. /// Used by [RenderShaderMask] and the [ShaderMask] widget.
...@@ -969,7 +1063,7 @@ class ShapeBorderClipper extends CustomClipper<Path> { ...@@ -969,7 +1063,7 @@ class ShapeBorderClipper extends CustomClipper<Path> {
this.textDirection, this.textDirection,
}) : assert(shapeBorder != null); }) : assert(shapeBorder != null);
// The shape border whose outer path this clipper clips to. /// The shape border whose outer path this clipper clips to.
final ShapeBorder shapeBorder; final ShapeBorder shapeBorder;
/// The text direction to use for getting the outer path for [shapeBorder]. /// The text direction to use for getting the outer path for [shapeBorder].
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4; import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'basic.dart'; import 'basic.dart';
...@@ -301,15 +302,15 @@ class SizeTransition extends AnimatedWidget { ...@@ -301,15 +302,15 @@ class SizeTransition extends AnimatedWidget {
/// ///
/// For a widget that automatically animates between the sizes of two children, /// For a widget that automatically animates between the sizes of two children,
/// fading between them, see [AnimatedCrossFade]. /// fading between them, see [AnimatedCrossFade].
class FadeTransition extends AnimatedWidget { class FadeTransition extends SingleChildRenderObjectWidget {
/// Creates an opacity transition. /// Creates an opacity transition.
/// ///
/// The [opacity] argument must not be null. /// The [opacity] argument must not be null.
const FadeTransition({ const FadeTransition({
Key key, Key key,
@required Animation<double> opacity, @required this.opacity,
this.child, Widget child,
}) : super(key: key, listenable: opacity); }) : super(key: key, child: child);
/// The animation that controls the opacity of the child. /// The animation that controls the opacity of the child.
/// ///
...@@ -317,16 +318,19 @@ class FadeTransition extends AnimatedWidget { ...@@ -317,16 +318,19 @@ class FadeTransition extends AnimatedWidget {
/// painted with an opacity of v. For example, if v is 0.5, the child will be /// painted with an opacity of v. For example, if v is 0.5, the child will be
/// blended 50% with its background. Similarly, if v is 0.0, the child will be /// blended 50% with its background. Similarly, if v is 0.0, the child will be
/// completely transparent. /// completely transparent.
Animation<double> get opacity => listenable; final Animation<double> opacity;
/// The widget below this widget in the tree. @override
/// RenderAnimatedOpacity createRenderObject(BuildContext context) {
/// {@macro flutter.widgets.child} return new RenderAnimatedOpacity(
final Widget child; opacity: opacity,
);
}
@override @override
Widget build(BuildContext context) { void updateRenderObject(BuildContext context, RenderAnimatedOpacity renderObject) {
return new Opacity(opacity: opacity.value, child: child); renderObject
..opacity = opacity;
} }
} }
......
...@@ -142,11 +142,11 @@ void main() { ...@@ -142,11 +142,11 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Check opacity // Check opacity
final Opacity opacity = tester.widget(find.descendant( final FadeTransition opacity = tester.widget(find.descendant(
of: find.byType(CupertinoButton), of: find.byType(CupertinoButton),
matching: find.byType(Opacity), matching: find.byType(FadeTransition),
)); ));
expect(opacity.opacity, 0.1); expect(opacity.opacity.value, 0.1);
}); });
testWidgets('pressedOpacity parameter', (WidgetTester tester) async { testWidgets('pressedOpacity parameter', (WidgetTester tester) async {
...@@ -163,11 +163,11 @@ void main() { ...@@ -163,11 +163,11 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Check opacity // Check opacity
final Opacity opacity = tester.widget(find.descendant( final FadeTransition opacity = tester.widget(find.descendant(
of: find.byType(CupertinoButton), of: find.byType(CupertinoButton),
matching: find.byType(Opacity), matching: find.byType(FadeTransition),
)); ));
expect(opacity.opacity, pressedOpacity); expect(opacity.opacity.value, pressedOpacity);
}); });
testWidgets('Cupertino button is semantically a button', (WidgetTester tester) async { testWidgets('Cupertino button is semantically a button', (WidgetTester tester) async {
......
...@@ -28,8 +28,8 @@ void main() { ...@@ -28,8 +28,8 @@ void main() {
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
Opacity widget2Opacity = FadeTransition widget2Opacity =
tester.element(find.text('Page 2')).ancestorWidgetOfExactType(Opacity); tester.element(find.text('Page 2')).ancestorWidgetOfExactType(FadeTransition);
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
final Size widget2Size = tester.getSize(find.text('Page 2')); final Size widget2Size = tester.getSize(find.text('Page 2'));
...@@ -40,7 +40,7 @@ void main() { ...@@ -40,7 +40,7 @@ void main() {
// Animation begins 3/4 of the way up the page. // Animation begins 3/4 of the way up the page.
expect(widget2TopLeft.dy < widget2Size.height / 4.0, true); expect(widget2TopLeft.dy < widget2Size.height / 4.0, true);
// Animation starts with page 2 being near transparent. // Animation starts with page 2 being near transparent.
expect(widget2Opacity.opacity < 0.01, MaterialPageRoute.debugEnableFadingRoutes); // ignore: deprecated_member_use expect(widget2Opacity.opacity.value < 0.01, MaterialPageRoute.debugEnableFadingRoutes); // ignore: deprecated_member_use
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
...@@ -53,13 +53,13 @@ void main() { ...@@ -53,13 +53,13 @@ void main() {
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
widget2Opacity = widget2Opacity =
tester.element(find.text('Page 2')).ancestorWidgetOfExactType(Opacity); tester.element(find.text('Page 2')).ancestorWidgetOfExactType(FadeTransition);
widget2TopLeft = tester.getTopLeft(find.text('Page 2')); widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 2 starts to move down. // Page 2 starts to move down.
expect(widget1TopLeft.dy < widget2TopLeft.dy, true); expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
// Page 2 starts to lose opacity. // Page 2 starts to lose opacity.
expect(widget2Opacity.opacity < 1.0, MaterialPageRoute.debugEnableFadingRoutes); // ignore: deprecated_member_use expect(widget2Opacity.opacity.value < 1.0, MaterialPageRoute.debugEnableFadingRoutes); // ignore: deprecated_member_use
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
......
...@@ -491,15 +491,16 @@ void main() { ...@@ -491,15 +491,16 @@ void main() {
// Toolbar should fade in. Starting at 0% opacity. // Toolbar should fade in. Starting at 0% opacity.
final Element target = tester.element(find.text('SELECT ALL')); final Element target = tester.element(find.text('SELECT ALL'));
Opacity opacity = target.ancestorWidgetOfExactType(Opacity); final FadeTransition opacity = target.ancestorWidgetOfExactType(FadeTransition);
expect(opacity, isNotNull); expect(opacity, isNotNull);
expect(opacity.opacity, equals(0.0)); expect(opacity.opacity.value, equals(0.0));
// Still fading in. // Still fading in.
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
opacity = target.ancestorWidgetOfExactType(Opacity); final FadeTransition opacity2 = target.ancestorWidgetOfExactType(FadeTransition);
expect(opacity.opacity, greaterThan(0.0)); expect(opacity, same(opacity2));
expect(opacity.opacity, lessThan(1.0)); expect(opacity.opacity.value, greaterThan(0.0));
expect(opacity.opacity.value, lessThan(1.0));
// End the test here to ensure the animation is properly disposed of. // End the test here to ensure the animation is properly disposed of.
}); });
......
...@@ -4,8 +4,11 @@ ...@@ -4,8 +4,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart' show TestVSync;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'rendering_tester.dart';
int countSemanticsChildren(RenderObject object) { int countSemanticsChildren(RenderObject object) {
int count = 0; int count = 0;
object.visitChildrenForSemantics((RenderObject child) { object.visitChildrenForSemantics((RenderObject child) {
...@@ -13,7 +16,7 @@ int countSemanticsChildren(RenderObject object) { ...@@ -13,7 +16,7 @@ int countSemanticsChildren(RenderObject object) {
}); });
return count; return count;
} }
void main() { void main() {
test('RenderOpacity and children and semantics', () { test('RenderOpacity and children and semantics', () {
final RenderOpacity box = new RenderOpacity( final RenderOpacity box = new RenderOpacity(
...@@ -36,4 +39,36 @@ void main() { ...@@ -36,4 +39,36 @@ void main() {
box.opacity = 0.0; box.opacity = 0.0;
expect(countSemanticsChildren(box), 0); expect(countSemanticsChildren(box), 0);
}); });
test('RenderOpacity and children and semantics', () {
final AnimationController controller = new AnimationController(vsync: const TestVSync());
final RenderAnimatedOpacity box = new RenderAnimatedOpacity(
opacity: controller,
child: new RenderParagraph(
const TextSpan(),
textDirection: TextDirection.ltr,
),
);
expect(countSemanticsChildren(box), 0); // controller defaults to 0.0
controller.value = 0.2; // has no effect, box isn't subscribed yet
expect(countSemanticsChildren(box), 0);
controller.value = 1.0; // ditto
expect(countSemanticsChildren(box), 0); // alpha is still 0
layout(box); // this causes the box to attach, which makes it subscribe
expect(countSemanticsChildren(box), 1);
controller.value = 1.0;
expect(countSemanticsChildren(box), 1);
controller.value = 0.5;
expect(countSemanticsChildren(box), 1);
controller.value = 0.25;
expect(countSemanticsChildren(box), 1);
controller.value = 0.125;
expect(countSemanticsChildren(box), 1);
controller.value = 0.0;
expect(countSemanticsChildren(box), 0);
controller.value = 0.125;
expect(countSemanticsChildren(box), 1);
controller.value = 0.0;
expect(countSemanticsChildren(box), 0);
});
} }
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('FadeTransition', (WidgetTester tester) async {
final DebugPrintCallback oldPrint = debugPrint;
final List<String> log = <String>[];
debugPrint = (String message, { int wrapWidth }) {
log.add(message);
};
debugPrintBuildScope = true;
final AnimationController controller = new AnimationController(
vsync: const TestVSync(),
duration: const Duration(seconds: 2),
);
await tester.pumpWidget(new FadeTransition(
opacity: controller,
child: const Placeholder(),
));
expect(log, hasLength(2));
expect(log.last, 'buildScope finished');
await tester.pump();
expect(log, hasLength(2));
controller.forward();
await tester.pumpAndSettle();
expect(log, hasLength(2));
debugPrint = oldPrint;
debugPrintBuildScope = false;
});
}
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