flutter/packages/flutter_test/lib/src/animation_sheet.dart
Greg Spencer 4ce7fdd92b
Remove 'must be non-null' and 'must not be null' comments from non-framework libraries (#134994)
## Description

This removes all of the comments that are of the form "so-and-so must not be null" or "so-and-so must be non-null" from the cases where those values are defines as non-nullable values.

This PR removes them from the library in the repo that don't have anything to do with the framework.

This was done by hand, since it really didn't lend itself to scripting, so it needs to be more than just spot-checked, I think. I was careful to leave any comment that referred to parameters that were nullable, but I may have missed some.

In addition to being no longer relevant after null safety has been made the default, these comments were largely fragile, in that it was easy for them to get out of date, and not be accurate anymore anyhow.

This did create a number of constructor comments which basically say "Creates a [Foo].", but I don't really know how to avoid that in a large scale change, since there's not much you can really say in a lot of cases.  I think we might consider some leniency for constructors to the "Comment must be meaningful" style guidance (which we de facto have already, since there are a bunch of these).

## Related PRs
- https://github.com/flutter/flutter/pull/134984
- https://github.com/flutter/flutter/pull/134991
- https://github.com/flutter/flutter/pull/134992
- https://github.com/flutter/flutter/pull/134993

## Tests
 - Documentation only change.
2023-09-19 17:26:07 +00:00

393 lines
13 KiB
Dart

// 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 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
// A Future<ui.Image> that stores the resolved result.
class _AsyncImage {
_AsyncImage(Future<ui.Image> task) {
_task = task.then((ui.Image image) {
_result = image;
});
}
// Returns the resolved image.
Future<ui.Image> result() async {
if (_result != null) {
return _result!;
}
await _task;
assert(_result != null);
return _result!;
}
late final Future<void> _task;
ui.Image? _result;
// Wait for a list of `_AsyncImage` and returns the list of its resolved
// images.
static Future<List<ui.Image>> resolveList(List<_AsyncImage> targets) {
final Iterable<Future<ui.Image>> images = targets.map<Future<ui.Image>>(
(_AsyncImage target) => target.result());
return Future.wait<ui.Image>(images);
}
}
/// Records the frames of an animating widget, and later displays the frames as a
/// grid in an animation sheet.
///
/// This class does not support Web, because the animation sheet utilizes taking
/// screenshots, which is unsupported on the Web. Tests that use this class must
/// be noted with `skip: isBrowser`.
/// (https://github.com/flutter/flutter/issues/56001)
///
/// Using this class includes the following steps:
///
/// * Create an instance of this class.
/// * Register [dispose] to the test's tear down callbacks.
/// * Pump frames that render the target widget wrapped in [record]. Every frame
/// that has `recording` being true will be recorded.
/// * Acquire the output image with [collate] and compare against the golden
/// file.
///
/// {@tool snippet}
/// The following example shows how to record an animation sheet of an [InkWell]
/// being pressed then released.
///
/// ```dart
/// testWidgets('Inkwell animation sheet', (WidgetTester tester) async {
/// // Create instance
/// final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(48, 24));
/// addTearDown(animationSheet.dispose);
///
/// final Widget target = Material(
/// child: Directionality(
/// textDirection: TextDirection.ltr,
/// child: InkWell(
/// splashColor: Colors.blue,
/// onTap: () {},
/// ),
/// ),
/// );
///
/// // Optional: setup before recording (`recording` is false)
/// await tester.pumpWidget(animationSheet.record(
/// target,
/// recording: false,
/// ));
///
/// final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell)));
///
/// // Start recording (`recording` is true)
/// await tester.pumpFrames(animationSheet.record(
/// target,
/// recording: true, // ignore: avoid_redundant_argument_values
/// ), const Duration(seconds: 1));
///
/// await gesture.up();
///
/// await tester.pumpFrames(animationSheet.record(
/// target,
/// recording: true, // ignore: avoid_redundant_argument_values
/// ), const Duration(seconds: 1));
///
/// // Compare against golden file
/// await expectLater(
/// animationSheet.collate(800),
/// matchesGoldenFile('inkwell.press.animation.png'),
/// );
/// }, skip: isBrowser); // Animation sheet does not support browser https://github.com/flutter/flutter/issues/56001
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [GoldenFileComparator], which introduces Golden File Testing.
class AnimationSheetBuilder {
/// Starts a session of building an animation sheet.
///
/// The [frameSize] is a tight constraint for the child to be recorded, and must not
/// be null.
///
/// The [allLayers] controls whether to record elements drawn out of the subtree,
/// and defaults to false.
AnimationSheetBuilder({
required this.frameSize,
this.allLayers = false,
}) : assert(!kIsWeb);
/// Dispose all recorded frames and result images.
///
/// This method must be called before the test case ends (usually as a tear
/// down callback) to properly deallocate the images.
///
/// After this method is called, there will be no frames to [collate].
Future<void> dispose() async {
final List<_AsyncImage> targets = <_AsyncImage>[
..._recordedFrames,
..._results,
];
_recordedFrames.clear();
_results.clear();
for (final ui.Image image in await _AsyncImage.resolveList(targets)) {
image.dispose();
}
}
/// The size of the child to be recorded.
///
/// This size is applied as a tight layout constraint for the child, and is
/// fixed throughout the building session.
final Size frameSize;
/// Whether the captured image comes from the entire tree, or only the
/// subtree of [record].
///
/// If [allLayers] is false, then the [record] widget will capture the image
/// composited by its subtree. If [allLayers] is true, then the [record] will
/// capture the entire tree composited and clipped by [record]'s region.
///
/// The two modes are identical if there is nothing in front of [record].
/// But in rare cases, what needs to be captured has to be rendered out of
/// [record]'s subtree in its front. By setting [allLayers] to true, [record]
/// captures everything within its region even if drawn outside of its
/// subtree.
///
/// Defaults to false.
final bool allLayers;
final List<_AsyncImage> _recordedFrames = <_AsyncImage>[];
/// Returns a widget that renders a widget in a box that can be recorded.
///
/// The returned widget wraps `child` in a box with a fixed size specified by
/// [frameSize]. The `key` is also applied to the returned widget.
///
/// The frame is only recorded if the `recording` argument is true, or during
/// a procedure that is wrapped within [recording]. In either case, the
/// painted result of each frame will be stored and later available for
/// [collate]. If neither condition is met, the frames are not recorded, which
/// is useful during setup phases.
///
/// See also:
///
/// * [WidgetTester.pumpFrames], which renders a widget in a series of frames
/// with a fixed time interval.
Widget record(Widget child, {
Key? key,
bool recording = true,
}) {
return _AnimationSheetRecorder(
key: key,
size: frameSize,
allLayers: allLayers,
handleRecorded: !recording ? null : (Future<ui.Image> futureImage) {
_recordedFrames.add(_AsyncImage(() async {
final ui.Image image = await futureImage;
assert(image.width == frameSize.width && image.height == frameSize.height,
'Unexpected size mismatch: frame has (${image.width}, ${image.height}) '
'while `frameSize` is $frameSize.'
);
return image;
}()));
},
child: child,
);
}
// The result images generated by `collate`.
//
// They're stored here to be disposed by [dispose].
final List<_AsyncImage> _results = <_AsyncImage>[];
/// Returns an result image by putting all frames together in a table.
///
/// This method returns an image that arranges the captured frames in a table,
/// which has `cellsPerRow` images per row with the order from left to right,
/// top to bottom.
///
/// The result image of this method is managed by [AnimationSheetBuilder],
/// and should not be disposed by the caller.
///
/// An example of using this method can be found at [AnimationSheetBuilder].
Future<ui.Image> collate(int cellsPerRow) async {
assert(_recordedFrames.isNotEmpty,
'No frames are collected. Have you forgot to set `recording` to true?');
final _AsyncImage result = _AsyncImage(_collateFrames(_recordedFrames, frameSize, cellsPerRow));
_results.add(result);
return result.result();
}
}
typedef _RecordedHandler = void Function(Future<ui.Image> image);
class _AnimationSheetRecorder extends StatefulWidget {
const _AnimationSheetRecorder({
this.handleRecorded,
required this.child,
required this.size,
required this.allLayers,
super.key,
});
final _RecordedHandler? handleRecorded;
final Widget child;
final Size size;
final bool allLayers;
@override
State<StatefulWidget> createState() => _AnimationSheetRecorderState();
}
class _AnimationSheetRecorderState extends State<_AnimationSheetRecorder> {
GlobalKey boundaryKey = GlobalKey();
void _record(Duration duration) {
assert(widget.handleRecorded != null);
final _RenderRootableRepaintBoundary boundary = boundaryKey.currentContext!.findRenderObject()! as _RenderRootableRepaintBoundary;
if (widget.allLayers) {
widget.handleRecorded!(boundary.allLayersToImage());
} else {
widget.handleRecorded!(boundary.toImage());
}
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topLeft,
child: SizedBox.fromSize(
size: widget.size,
child: _RootableRepaintBoundary(
key: boundaryKey,
child: _PostFrameCallbacker(
callback: widget.handleRecorded == null ? null : _record,
child: widget.child,
),
),
),
);
}
}
// Invokes `callback` and [markNeedsPaint] during the post-frame callback phase
// of every frame.
//
// If `callback` is non-null, `_PostFrameCallbacker` adds a post-frame callback
// every time it paints, during which it calls the provided `callback` then
// invokes [markNeedsPaint].
//
// If `callback` is null, `_PostFrameCallbacker` is equivalent to a proxy box.
class _PostFrameCallbacker extends SingleChildRenderObjectWidget {
const _PostFrameCallbacker({
super.child,
this.callback,
});
final FrameCallback? callback;
@override
_RenderPostFrameCallbacker createRenderObject(BuildContext context) => _RenderPostFrameCallbacker(
callback: callback,
);
@override
void updateRenderObject(BuildContext context, _RenderPostFrameCallbacker renderObject) {
renderObject.callback = callback;
}
}
class _RenderPostFrameCallbacker extends RenderProxyBox {
_RenderPostFrameCallbacker({
FrameCallback? callback,
}) : _callback = callback;
FrameCallback? get callback => _callback;
FrameCallback? _callback;
set callback(FrameCallback? value) {
_callback = value;
if (value != null) {
markNeedsPaint();
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (callback != null) {
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
callback!(duration);
markNeedsPaint();
});
}
super.paint(context, offset);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('callback', value: callback != null, ifTrue: 'has a callback'));
}
}
Future<ui.Image> _collateFrames(List<_AsyncImage> futureFrames, Size frameSize, int cellsPerRow) async {
final List<ui.Image> frames = await _AsyncImage.resolveList(futureFrames);
final int rowNum = (frames.length / cellsPerRow).ceil();
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas canvas = Canvas(
recorder,
Rect.fromLTWH(0, 0, frameSize.width * cellsPerRow, frameSize.height * rowNum),
);
for (int i = 0; i < frames.length; i += 1) {
canvas.drawImage(
frames[i],
Offset(frameSize.width * (i % cellsPerRow), frameSize.height * (i / cellsPerRow).floor()),
Paint(),
);
}
final ui.Picture picture = recorder.endRecording();
final ui.Image image = await picture.toImage(
(frameSize.width * cellsPerRow).toInt(),
(frameSize.height * rowNum).toInt(),
);
picture.dispose();
return image;
}
class _RenderRootableRepaintBoundary extends RenderRepaintBoundary {
// Like [toImage], but captures an image of all layers (composited by
// RenderView and its children) clipped by the region of this object.
Future<ui.Image> allLayersToImage() {
final TransformLayer rootLayer = _rootLayer();
final Matrix4 rootTransform = (rootLayer.transform ?? Matrix4.identity()).clone();
final Matrix4 transform = rootTransform.multiplied(getTransformTo(null));
final Rect rect = MatrixUtils.transformRect(transform, Offset.zero & size);
// The scale was used to fit the actual device. Revert it since the target
// is the logical display. Take transform[0] as the scale.
return rootLayer.toImage(rect, pixelRatio: 1 / transform[0]);
}
TransformLayer _rootLayer() {
Layer layer = this.layer!;
while (layer.parent != null) {
layer = layer.parent!;
}
return layer as TransformLayer;
}
}
// A [RepaintBoundary], except that its render object has a `fullscreenToImage` method.
class _RootableRepaintBoundary extends SingleChildRenderObjectWidget {
/// Creates a widget that isolates repaints.
const _RootableRepaintBoundary({ super.key, super.child });
@override
_RenderRootableRepaintBoundary createRenderObject(BuildContext context) => _RenderRootableRepaintBoundary();
}