Commit f974c295 authored by Kate Lovett's avatar Kate Lovett Committed by Flutter GitHub Bot

SliverFadeTransition (#45950)

parent 4f9b6cf0
...@@ -823,25 +823,12 @@ class RenderOpacity extends RenderProxyBox { ...@@ -823,25 +823,12 @@ class RenderOpacity extends RenderProxyBox {
} }
} }
/// Makes its child partially transparent, driven from an [Animation]. /// Implementation of [RenderAnimatedOpacity] and [RenderSliverAnimatedOpacity].
/// ///
/// This is a variant of [RenderOpacity] that uses an [Animation<double>] rather /// Use this mixin in situations where the proxying behavior
/// than a [double] to control the opacity. /// of [RenderProxyBox] or [RenderProxySliver] is desired for animating opacity,
class RenderAnimatedOpacity extends RenderProxyBox { /// but would like to use the same methods for both types of render objects.
/// Creates a partially transparent render object. mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChildMixin<T> {
///
/// The [opacity] argument must not be null.
RenderAnimatedOpacity({
@required Animation<double> opacity,
bool alwaysIncludeSemantics = false,
RenderBox child,
}) : assert(opacity != null),
assert(alwaysIncludeSemantics != null),
_alwaysIncludeSemantics = alwaysIncludeSemantics,
super(child) {
this.opacity = opacity;
}
int _alpha; int _alpha;
@override @override
...@@ -943,6 +930,26 @@ class RenderAnimatedOpacity extends RenderProxyBox { ...@@ -943,6 +930,26 @@ class RenderAnimatedOpacity 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 with RenderProxyBoxMixin, RenderAnimatedOpacityMixin<RenderBox> {
/// Creates a partially transparent render object.
///
/// The [opacity] argument must not be null.
RenderAnimatedOpacity({
@required Animation<double> opacity,
bool alwaysIncludeSemantics = false,
RenderBox child,
}) : assert(opacity != null),
assert(alwaysIncludeSemantics != null),
super(child) {
this.opacity = opacity;
this.alwaysIncludeSemantics = alwaysIncludeSemantics;
}
}
/// 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.
......
...@@ -4,11 +4,13 @@ ...@@ -4,11 +4,13 @@
import 'dart:ui' as ui show Color; import 'dart:ui' as ui show Color;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'layer.dart'; import 'layer.dart';
import 'object.dart'; import 'object.dart';
import 'proxy_box.dart';
import 'sliver.dart'; import 'sliver.dart';
/// A base class for sliver render objects that resemble their children. /// A base class for sliver render objects that resemble their children.
...@@ -51,6 +53,12 @@ abstract class RenderProxySliver extends RenderSliver with RenderObjectWithChild ...@@ -51,6 +53,12 @@ abstract class RenderProxySliver extends RenderSliver with RenderObjectWithChild
geometry = child.geometry; geometry = child.geometry;
} }
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}
@override @override
bool hitTestChildren(SliverHitTestResult result, {double mainAxisPosition, double crossAxisPosition}) { bool hitTestChildren(SliverHitTestResult result, {double mainAxisPosition, double crossAxisPosition}) {
return child != null return child != null
...@@ -152,7 +160,6 @@ class RenderSliverOpacity extends RenderProxySliver { ...@@ -152,7 +160,6 @@ class RenderSliverOpacity extends RenderProxySliver {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
void _paintWithOpacity(PaintingContext context, Offset offset) => context.paintChild(child, offset);
if (child != null && child.geometry.visible) { if (child != null && child.geometry.visible) {
if (_alpha == 0) { if (_alpha == 0) {
// No need to keep the layer. We'll create a new one if necessary. // No need to keep the layer. We'll create a new one if necessary.
...@@ -169,7 +176,7 @@ class RenderSliverOpacity extends RenderProxySliver { ...@@ -169,7 +176,7 @@ class RenderSliverOpacity extends RenderProxySliver {
layer = context.pushOpacity( layer = context.pushOpacity(
offset, offset,
_alpha, _alpha,
_paintWithOpacity, super.paint,
oldLayer: layer as OpacityLayer, oldLayer: layer as OpacityLayer,
); );
} }
...@@ -370,3 +377,23 @@ class RenderSliverOffstage extends RenderProxySliver { ...@@ -370,3 +377,23 @@ class RenderSliverOffstage extends RenderProxySliver {
]; ];
} }
} }
/// Makes its sliver child partially transparent, driven from an [Animation].
///
/// This is a variant of [RenderSliverOpacity] that uses an [Animation<double>]
/// rather than a [double] to control the opacity.
class RenderSliverAnimatedOpacity extends RenderProxySliver with RenderAnimatedOpacityMixin<RenderSliver>{
/// Creates a partially transparent render object.
///
/// The [opacity] argument must not be null.
RenderSliverAnimatedOpacity({
@required Animation<double> opacity,
bool alwaysIncludeSemantics = false,
RenderSliver sliver,
}) : assert(opacity != null),
assert(alwaysIncludeSemantics != null) {
this.opacity = opacity;
this.alwaysIncludeSemantics = alwaysIncludeSemantics;
child = sliver;
}
}
...@@ -1776,10 +1776,9 @@ class SliverOpacity extends SingleChildRenderObjectWidget { ...@@ -1776,10 +1776,9 @@ class SliverOpacity extends SingleChildRenderObjectWidget {
@required this.opacity, @required this.opacity,
this.alwaysIncludeSemantics = false, this.alwaysIncludeSemantics = false,
Widget sliver, Widget sliver,
}) }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
: assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), assert(alwaysIncludeSemantics != null),
assert(alwaysIncludeSemantics != null), super(key: key, child: sliver);
super(key: key, child: sliver);
/// The fraction to scale the sliver child's alpha value. /// The fraction to scale the sliver child's alpha value.
/// ///
......
...@@ -520,6 +520,7 @@ class SizeTransition extends AnimatedWidget { ...@@ -520,6 +520,7 @@ class SizeTransition extends AnimatedWidget {
/// ///
/// Here's an illustration of the [FadeTransition] widget, with it's [opacity] /// Here's an illustration of the [FadeTransition] widget, with it's [opacity]
/// animated by a [CurvedAnimation] set to [Curves.fastOutSlowIn]: /// animated by a [CurvedAnimation] set to [Curves.fastOutSlowIn]:
///
/// {@animation 300 378 https://flutter.github.io/assets-for-api-docs/assets/widgets/fade_transition.mp4} /// {@animation 300 378 https://flutter.github.io/assets-for-api-docs/assets/widgets/fade_transition.mp4}
/// ///
/// See also: /// See also:
...@@ -580,6 +581,121 @@ class FadeTransition extends SingleChildRenderObjectWidget { ...@@ -580,6 +581,121 @@ class FadeTransition extends SingleChildRenderObjectWidget {
} }
} }
/// Animates the opacity of a sliver widget.
///
/// {@tool snippet --template=stateful_widget_scaffold_center_freeform_state}
/// Creates a [CustomScrollView] with a [SliverFixedExtentList] that uses a
/// [SliverFadeTransition] to fade the list in and out.
///
/// ```dart
/// class _MyStatefulWidgetState extends State<MyStatefulWidget> with SingleTickerProviderStateMixin {
/// AnimationController controller;
/// Animation<double> animation;
///
/// initState() {
/// super.initState();
/// controller = AnimationController(
/// duration: const Duration(milliseconds: 1000), vsync: this);
/// animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
///
/// animation.addStatusListener((status) {
/// if (status == AnimationStatus.completed) {
/// controller.reverse();
/// } else if (status == AnimationStatus.dismissed) {
/// controller.forward();
/// }
/// });
/// controller.forward();
/// }
///
/// Widget build(BuildContext context) {
/// return CustomScrollView(
/// slivers: <Widget>[
/// SliverFadeTransition(
/// opacity: animation,
/// sliver: SliverFixedExtentList(
/// itemExtent: 100.0,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// return Container(
/// color: index % 2 == 0
/// ? Colors.indigo[200]
/// : Colors.orange[200],
/// );
/// },
/// childCount: 5,
/// ),
/// ),
/// )
/// ]
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// Here's an illustration of the [FadeTransition] widget, the [RenderBox]
/// equivalent widget, with it's [opacity] animated by a [CurvedAnimation] set
/// to [Curves.fastOutSlowIn]:
///
/// {@animation 300 378 https://flutter.github.io/assets-for-api-docs/assets/widgets/fade_transition.mp4}
///
/// See also:
///
/// * [SliverOpacity], which does not animate changes in opacity.
class SliverFadeTransition extends SingleChildRenderObjectWidget {
/// Creates an opacity transition.
///
/// The [opacity] argument must not be null.
const SliverFadeTransition({
Key key,
@required this.opacity,
this.alwaysIncludeSemantics = false,
Widget sliver,
}) : assert(opacity != null),
super(key: key, child: sliver);
/// The animation that controls the opacity of the sliver child.
///
/// If the current value of the opacity animation is v, 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
/// completely transparent.
final Animation<double> opacity;
/// Whether the semantic information of the sliver child is always included.
///
/// Defaults to false.
///
/// When true, regardless of the opacity settings the sliver child's semantic
/// information is exposed as if the widget were fully visible. This is
/// useful in cases where labels may be hidden during animations that
/// would otherwise contribute relevant semantics.
final bool alwaysIncludeSemantics;
@override
RenderSliverAnimatedOpacity createRenderObject(BuildContext context) {
return RenderSliverAnimatedOpacity(
opacity: opacity,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
@override
void updateRenderObject(BuildContext context, RenderSliverAnimatedOpacity renderObject) {
renderObject
..opacity = opacity
..alwaysIncludeSemantics = alwaysIncludeSemantics;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('opacity', opacity));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
/// An interpolation between two relative rects. /// An interpolation between two relative rects.
/// ///
/// This class specializes the interpolation of [Tween<RelativeRect>] to /// This class specializes the interpolation of [Tween<RelativeRect>] to
......
...@@ -9,7 +9,6 @@ import 'package:flutter/animation.dart'; ...@@ -9,7 +9,6 @@ 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/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/src/scheduler/ticker.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
import 'rendering_tester.dart'; import 'rendering_tester.dart';
...@@ -277,7 +276,7 @@ void main() { ...@@ -277,7 +276,7 @@ void main() {
test('RenderAnimatedOpacity does not composite if it is transparent', () async { test('RenderAnimatedOpacity does not composite if it is transparent', () async {
final Animation<double> opacityAnimation = AnimationController( final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(), vsync: FakeTickerProvider(),
)..value = 0.0; )..value = 0.0;
final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity( final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
...@@ -292,7 +291,7 @@ void main() { ...@@ -292,7 +291,7 @@ void main() {
test('RenderAnimatedOpacity does not composite if it is opaque', () { test('RenderAnimatedOpacity does not composite if it is opaque', () {
final Animation<double> opacityAnimation = AnimationController( final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(), vsync: FakeTickerProvider(),
)..value = 1.0; )..value = 1.0;
final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity( final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
...@@ -307,7 +306,7 @@ void main() { ...@@ -307,7 +306,7 @@ void main() {
test('RenderAnimatedOpacity reuses its layer', () { test('RenderAnimatedOpacity reuses its layer', () {
final Animation<double> opacityAnimation = AnimationController( final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(), vsync: FakeTickerProvider(),
)..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer )..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer
_testLayerReuse<OpacityLayer>(RenderAnimatedOpacity( _testLayerReuse<OpacityLayer>(RenderAnimatedOpacity(
...@@ -483,61 +482,6 @@ class _TestRRectClipper extends CustomClipper<RRect> { ...@@ -483,61 +482,6 @@ class _TestRRectClipper extends CustomClipper<RRect> {
bool shouldReclip(_TestRRectClipper oldClipper) => true; bool shouldReclip(_TestRRectClipper oldClipper) => true;
} }
class _FakeTickerProvider implements TickerProvider {
@override
Ticker createTicker(TickerCallback onTick, [ bool disableAnimations = false ]) {
return _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() { }
@override
String toString({ bool debugIncludeStack = false }) => super.toString();
@override
DiagnosticsNode describeForError(String name) {
return DiagnosticsProperty<Ticker>(name, this, style: DiagnosticsTreeStyle.errorProperty);
}
}
// Forces two frames and checks that: // Forces two frames and checks that:
// - a layer is created on the first frame // - a layer is created on the first frame
// - the layer is reused on the second frame // - the layer is reused on the second frame
......
// Copyright 2014 The Flutter 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/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../flutter_test_alternative.dart';
import 'rendering_tester.dart';
void main() {
test('RenderSliverOpacity does not composite if it is transparent', () {
final RenderSliverOpacity renderSliverOpacity = RenderSliverOpacity(
opacity: 0.0,
sliver: RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
)
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 250.0,
children: <RenderSliver>[renderSliverOpacity],
);
layout(root, phase: EnginePhase.composite);
expect(renderSliverOpacity.needsCompositing, false);
});
test('RenderSliverOpacity does not composite if it is opaque', () {
final RenderSliverOpacity renderSliverOpacity = RenderSliverOpacity(
opacity: 1.0,
sliver: RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
)
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 250.0,
children: <RenderSliver>[renderSliverOpacity],
);
layout(root, phase: EnginePhase.composite);
expect(renderSliverOpacity.needsCompositing, false);
});
test('RenderSliverOpacity reuses its layer', () {
final RenderSliverOpacity renderSliverOpacity = RenderSliverOpacity(
opacity: 0.5,
sliver: RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
)
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 250.0,
children: <RenderSliver>[renderSliverOpacity]
);
expect(renderSliverOpacity.debugLayer, null);
layout(root, phase: EnginePhase.paint, constraints: BoxConstraints.tight(const Size(10, 10)));
final ContainerLayer layer = renderSliverOpacity.debugLayer;
expect(layer, isNotNull);
// Mark for repaint otherwise pumpFrame is a noop.
renderSliverOpacity.markNeedsPaint();
expect(renderSliverOpacity.debugNeedsPaint, true);
pumpFrame(phase: EnginePhase.paint);
expect(renderSliverOpacity.debugNeedsPaint, false);
expect(renderSliverOpacity.debugLayer, same(layer));
});
test('RenderSliverAnimatedOpacity does not composite if it is transparent', () async {
final Animation<double> opacityAnimation = AnimationController(
vsync: FakeTickerProvider(),
)..value = 0.0;
final RenderSliverAnimatedOpacity renderSliverAnimatedOpacity = RenderSliverAnimatedOpacity(
alwaysIncludeSemantics: false,
opacity: opacityAnimation,
sliver: RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
)
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 250.0,
children: <RenderSliver>[renderSliverAnimatedOpacity],
);
layout(root, phase: EnginePhase.composite);
expect(renderSliverAnimatedOpacity.needsCompositing, false);
});
test('RenderSliverAnimatedOpacity does not composite if it is opaque', () {
final Animation<double> opacityAnimation = AnimationController(
vsync: FakeTickerProvider(),
)..value = 1.0;
final RenderSliverAnimatedOpacity renderSliverAnimatedOpacity = RenderSliverAnimatedOpacity(
alwaysIncludeSemantics: false,
opacity: opacityAnimation,
sliver: RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
)
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 250.0,
children: <RenderSliver>[renderSliverAnimatedOpacity],
);
layout(root, phase: EnginePhase.composite);
expect(renderSliverAnimatedOpacity.needsCompositing, false);
});
test('RenderSliverAnimatedOpacity reuses its layer', () {
final Animation<double> opacityAnimation = AnimationController(
vsync: FakeTickerProvider(),
)..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer
final RenderSliverAnimatedOpacity renderSliverAnimatedOpacity = RenderSliverAnimatedOpacity(
opacity: opacityAnimation,
sliver: RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
)
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 250.0,
children: <RenderSliver>[renderSliverAnimatedOpacity]
);
expect(renderSliverAnimatedOpacity.debugLayer, null);
layout(root, phase: EnginePhase.paint, constraints: BoxConstraints.tight(const Size(10, 10)));
final ContainerLayer layer = renderSliverAnimatedOpacity.debugLayer;
expect(layer, isNotNull);
// Mark for repaint otherwise pumpFrame is a noop.
renderSliverAnimatedOpacity.markNeedsPaint();
expect(renderSliverAnimatedOpacity.debugNeedsPaint, true);
pumpFrame(phase: EnginePhase.paint);
expect(renderSliverAnimatedOpacity.debugNeedsPaint, false);
expect(renderSliverAnimatedOpacity.debugLayer, same(layer));
});
}
...@@ -229,3 +229,58 @@ class RenderSizedBox extends RenderBox { ...@@ -229,3 +229,58 @@ class RenderSizedBox extends RenderBox {
@override @override
bool hitTestSelf(Offset position) => true; bool hitTestSelf(Offset position) => true;
} }
class FakeTickerProvider implements TickerProvider {
@override
Ticker createTicker(TickerCallback onTick, [ bool disableAnimations = false ]) {
return 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() { }
@override
String toString({ bool debugIncludeStack = false }) => super.toString();
@override
DiagnosticsNode describeForError(String name) {
return DiagnosticsProperty<Ticker>(name, this, style: DiagnosticsTreeStyle.errorProperty);
}
}
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