flutter/packages/flutter/test/rendering/recording_canvas.dart
Yegor 34c692659e
Teach render objects to reuse engine layers (#36402)
Teach Layer and its implementations, RenderObject and its implementations, and PaintingContext to reuse engine layers. The idea is that a concrete RenderObject creates a Layer and holds on to it as long as it needs it (i.e. when it is composited, and the layer type does not change). In return, each Layer object holds on to an EngineLayer and reports it to the engine via addRetained and oldLayer. This allows the Web engine to reuse DOM elements across frames. Without it, each frame drops all previously rendered HTML and regenerates it from scratch.
2019-08-16 15:27:12 -07:00

215 lines
7.1 KiB
Dart

// Copyright 2017 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/rendering.dart';
import 'package:flutter/src/rendering/layer.dart';
/// An [Invocation] and the [stack] trace that led to it.
///
/// Used by [TestRecordingCanvas] to trace canvas calls.
class RecordedInvocation {
/// Create a record for an invocation list.
const RecordedInvocation(this.invocation, { this.stack });
/// The method that was called and its arguments.
///
/// The arguments preserve identity, but not value. Thus, if two invocations
/// were made with the same [Paint] object, but with that object configured
/// differently each time, then they will both have the same object as their
/// argument, and inspecting that object will return the object's current
/// values (mostly likely those passed to the second call).
final Invocation invocation;
/// The stack trace at the time of the method call.
final StackTrace stack;
@override
String toString() => _describeInvocation(invocation);
/// Converts [stack] to a string using the [FlutterError.defaultStackFilter] logic.
String stackToString({ String indent = '' }) {
assert(indent != null);
return indent + FlutterError.defaultStackFilter(
stack.toString().trimRight().split('\n')
).join('\n$indent');
}
}
/// A [Canvas] for tests that records its method calls.
///
/// This class can be used in conjunction with [TestRecordingPaintingContext]
/// to record the [Canvas] method calls made by a renderer. For example:
///
/// ```dart
/// RenderBox box = tester.renderObject(find.text('ABC'));
/// TestRecordingCanvas canvas = TestRecordingCanvas();
/// TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
/// box.paint(context, Offset.zero);
/// // Now test the expected canvas.invocations.
/// ```
///
/// In some cases it may be useful to define a subclass that overrides the
/// [Canvas] methods the test is checking and squirrels away the parameters
/// that the test requires.
///
/// For simple tests, consider using the [paints] matcher, which overlays a
/// pattern matching API over [TestRecordingCanvas].
class TestRecordingCanvas implements Canvas {
/// All of the method calls on this canvas.
final List<RecordedInvocation> invocations = <RecordedInvocation>[];
int _saveCount = 0;
@override
int getSaveCount() => _saveCount;
@override
void save() {
_saveCount += 1;
invocations.add(RecordedInvocation(_MethodCall(#save), stack: StackTrace.current));
}
@override
void saveLayer(Rect bounds, Paint paint) {
_saveCount += 1;
invocations.add(RecordedInvocation(_MethodCall(#saveLayer, <dynamic>[bounds, paint]), stack: StackTrace.current));
}
@override
void restore() {
_saveCount -= 1;
assert(_saveCount >= 0);
invocations.add(RecordedInvocation(_MethodCall(#restore), stack: StackTrace.current));
}
@override
void noSuchMethod(Invocation invocation) {
invocations.add(RecordedInvocation(invocation, stack: StackTrace.current));
}
}
/// A [PaintingContext] for tests that use [TestRecordingCanvas].
class TestRecordingPaintingContext extends ClipContext implements PaintingContext {
/// Creates a [PaintingContext] for tests that use [TestRecordingCanvas].
TestRecordingPaintingContext(this.canvas);
@override
final Canvas canvas;
@override
void paintChild(RenderObject child, Offset offset) {
child.paint(this, offset);
}
@override
ClipRectLayer pushClipRect(bool needsCompositing, Offset offset, Rect clipRect,
PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge, ClipRectLayer oldLayer }) {
clipRectAndPaint(clipRect.shift(offset), clipBehavior, clipRect.shift(offset), () => painter(this, offset));
return null;
}
@override
ClipRRectLayer pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect,
PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipRRectLayer oldLayer }) {
assert(clipBehavior != null);
clipRRectAndPaint(clipRRect.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset));
return null;
}
@override
ClipPathLayer pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath,
PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipPathLayer oldLayer }) {
clipPathAndPaint(clipPath.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset));
return null;
}
@override
TransformLayer pushTransform(bool needsCompositing, Offset offset, Matrix4 transform,
PaintingContextCallback painter, { TransformLayer oldLayer }) {
canvas.save();
canvas.transform(transform.storage);
painter(this, offset);
canvas.restore();
return null;
}
@override
OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter,
{ OpacityLayer oldLayer }) {
canvas.saveLayer(null, null); // TODO(ianh): Expose the alpha somewhere.
painter(this, offset);
canvas.restore();
return null;
}
@override
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset,
{ Rect childPaintBounds }) {
painter(this, offset);
}
@override
void noSuchMethod(Invocation invocation) { }
}
class _MethodCall implements Invocation {
_MethodCall(this._name, [ this._arguments = const <dynamic>[], this._typeArguments = const <Type> []]);
final Symbol _name;
final List<dynamic> _arguments;
final List<Type> _typeArguments;
@override
bool get isAccessor => false;
@override
bool get isGetter => false;
@override
bool get isMethod => true;
@override
bool get isSetter => false;
@override
Symbol get memberName => _name;
@override
Map<Symbol, dynamic> get namedArguments => <Symbol, dynamic>{};
@override
List<dynamic> get positionalArguments => _arguments;
@override
List<Type> get typeArguments => _typeArguments;
}
String _valueName(Object value) {
if (value is double)
return value.toStringAsFixed(1);
return value.toString();
}
// Workaround for https://github.com/dart-lang/sdk/issues/28372
String _symbolName(Symbol symbol) {
// WARNING: Assumes a fixed format for Symbol.toString which is *not*
// guaranteed anywhere.
final String s = '$symbol';
return s.substring(8, s.length - 2);
}
// Workaround for https://github.com/dart-lang/sdk/issues/28373
String _describeInvocation(Invocation call) {
final StringBuffer buffer = StringBuffer();
buffer.write(_symbolName(call.memberName));
if (call.isSetter) {
buffer.write(call.positionalArguments[0].toString());
} else if (call.isMethod) {
buffer.write('(');
buffer.writeAll(call.positionalArguments.map<String>(_valueName), ', ');
String separator = call.positionalArguments.isEmpty ? '' : ', ';
call.namedArguments.forEach((Symbol name, Object value) {
buffer.write(separator);
buffer.write(_symbolName(name));
buffer.write(': ');
buffer.write(_valueName(value));
separator = ', ';
});
buffer.write(')');
}
return buffer.toString();
}