blob: 7c15465dc9ea8a03486ebe2ffbfb5ad609f84ea0 [file] [log] [blame]
// Copyright 2013 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:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:vector_math/vector_math_64.dart';
import 'scenario.dart';
import 'scenarios.dart';
List<int> _to32(int value) {
final Uint8List temp = Uint8List(4);
temp.buffer.asByteData().setInt32(0, value, Endian.little);
return temp;
}
List<int> _to64(num value) {
final Uint8List temp = Uint8List(15);
if (value is double) {
temp.buffer.asByteData().setFloat64(7, value, Endian.little);
} else if (value is int) { // ignore: avoid_double_and_int_checks
temp.buffer.asByteData().setInt64(7, value, Endian.little);
}
return temp;
}
List<int> _encodeString(String value) {
return <int>[
value.length, // This won't work if we use multi-byte characters.
...utf8.encode(value),
];
}
/// A simple platform view.
class PlatformViewScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// A simple platform view.
class NonFullScreenFlutterViewPlatformViewScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
NonFullScreenFlutterViewPlatformViewScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// A simple platform view with overlay that doesn't intersect with the platform view.
class PlatformViewNoOverlayIntersectionScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewNoOverlayIntersectionScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(
builder,
overlayOffset: const Offset(150, 350),
);
}
}
/// A platform view that is larger than the display size.
/// This is only applicable on Android while using virtual displays.
/// Related issue: https://github.com/flutter/flutter/issues/28978.
class PlatformViewLargerThanDisplaySize extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewLargerThanDisplaySize(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
width: 15000,
height: 60000,
);
finishBuilder(
builder,
);
}
}
/// A simple platform view with an overlay that partially intersects with the platform view.
class PlatformViewPartialIntersectionScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewPartialIntersectionScenario(
super.view, {
required this.id,
});
/// The platform view identifier .
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(
builder,
overlayOffset: const Offset(150, 240),
);
}
}
/// A simple platform view with two overlays that intersect with each other and the platform view.
class PlatformViewTwoIntersectingOverlaysScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewTwoIntersectingOverlaysScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawCircle(
const Offset(50, 50),
50,
Paint()..color = const Color(0xFFABCDEF),
);
canvas.drawCircle(
const Offset(100, 100),
50,
Paint()..color = const Color(0xFFABCDEF),
);
final Picture picture = recorder.endRecording();
builder.addPicture(const Offset(300, 300), picture);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// A simple platform view with one overlay and two overlays that intersect with each other and the platform view.
class PlatformViewOneOverlayTwoIntersectingOverlaysScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewOneOverlayTwoIntersectingOverlaysScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawCircle(
const Offset(50, 50),
50,
Paint()..color = const Color(0xFFABCDEF),
);
canvas.drawCircle(
const Offset(100, 100),
50,
Paint()..color = const Color(0xFFABCDEF),
);
canvas.drawCircle(
const Offset(-100, 200),
50,
Paint()..color = const Color(0xFFABCDEF),
);
final Picture picture = recorder.endRecording();
builder.addPicture(const Offset(300, 300), picture);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Two platform views without an overlay intersecting either platform view.
class MultiPlatformViewWithoutOverlaysScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
MultiPlatformViewWithoutOverlaysScenario(
super.view, {
required this.firstId,
required this.secondId,
});
/// The platform view identifier to use for the first platform view.
final int firstId;
/// The platform view identifier to use for the second platform view.
final int secondId;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 600);
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTRB(0, 0, 100, 1000),
Paint()..color = const Color(0xFFFF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(const Offset(580, 0), picture);
builder.pop();
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// A simple platform view with too many overlays result in a single native view.
class PlatformViewMaxOverlaysScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewMaxOverlaysScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawCircle(
const Offset(50, 50),
50,
Paint()..color = const Color(0xFFABCDEF),
);
canvas.drawCircle(
const Offset(100, 100),
50,
Paint()..color = const Color(0xFFABCDEF),
);
canvas.drawCircle(
const Offset(-100, 200),
50,
Paint()..color = const Color(0xFFABCDEF),
);
canvas.drawCircle(
const Offset(-100, -80),
50,
Paint()..color = const Color(0xFFABCDEF),
);
final Picture picture = recorder.endRecording();
builder.addPicture(const Offset(300, 300), picture);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Builds a scene with 2 platform views.
class MultiPlatformViewScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
MultiPlatformViewScenario(
super.view, {
required this.firstId,
required this.secondId,
});
/// The platform view identifier to use for the first platform view.
final int firstId;
/// The platform view identifier to use for the second platform view.
final int secondId;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 600);
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
finishBuilder(builder);
}
}
/// Scenario for verifying platform views after background and foregrounding the app.
///
/// Renders a frame with 2 platform views covered by a flutter drawn rectangle,
/// when the app goes to the background and comes back to the foreground renders a new frame
/// with the 2 platform views but without the flutter drawn rectangle.
class MultiPlatformViewBackgroundForegroundScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
MultiPlatformViewBackgroundForegroundScenario(
super.view, {
required this.firstId,
required this.secondId,
}) {
_nextFrame = _firstFrame;
channelBuffers.setListener('flutter/lifecycle', _onPlatformMessage);
}
/// The platform view identifier to use for the first platform view.
final int firstId;
/// The platform view identifier to use for the second platform view.
final int secondId;
late void Function() _nextFrame;
@override
void onBeginFrame(Duration duration) {
_nextFrame();
}
void _firstFrame() {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(50, 600);
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
builder.pushOffset(50, 0);
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
builder.pop();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTRB(0, 0, 500, 1000),
Paint()..color = const Color(0xFFFF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
void _secondFrame() {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 600);
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
String _lastLifecycleState = '';
void _onPlatformMessage(
ByteData? data,
PlatformMessageResponseCallback? callback,
) {
final String message = utf8.decode(data!.buffer.asUint8List());
if (_lastLifecycleState == 'AppLifecycleState.inactive' &&
message == 'AppLifecycleState.resumed') {
_nextFrame = _secondFrame;
view.platformDispatcher.scheduleFrame();
}
_lastLifecycleState = message;
}
@override
void unmount() {
channelBuffers.clearListener('flutter/lifecycle');
super.unmount();
}
}
/// Platform view with clip rect.
class PlatformViewClipRectScenario extends Scenario with _BasePlatformViewScenarioMixin {
/// Constructs a platform view with clip rect scenario.
PlatformViewClipRectScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder()
..pushClipRect(const Rect.fromLTRB(100, 100, 400, 400));
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// Platform view with clip rect then the PlatformView is moved for 10 frames.
///
/// The clip rect moves with the same transform matrix with the PlatformView.
class PlatformViewClipRectAfterMovedScenario extends Scenario with _BasePlatformViewScenarioMixin {
/// Constructs a platform view with clip rect scenario.
PlatformViewClipRectAfterMovedScenario(
super.view, {
required this.id,
});
/// The platform view identifier.
final int id;
int _numberOfFrames = 0;
double _y = 100.0;
@override
void onBeginFrame(Duration duration) {
final Matrix4 translateMatrix = Matrix4.identity()..translate(0.0, _y);
final SceneBuilder builder = SceneBuilder()
..pushTransform(translateMatrix.storage)
..pushClipRect(const Rect.fromLTRB(100, 100, 400, 400));
addPlatformView(
_numberOfFrames == 10? 10000: id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
// Add a translucent rect that has the same size of PlatformView.
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 500, 500),
Paint()..color = const Color(0x22FF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
finishBuilder(builder);
super.onBeginFrame(duration);
}
@override
void onDrawFrame() {
if (_numberOfFrames < 10) {
_numberOfFrames ++;
_y -= 10;
view.platformDispatcher.scheduleFrame();
}
super.onDrawFrame();
}
}
/// Platform view with clip rrect.
class PlatformViewClipRRectScenario extends PlatformViewScenario {
/// Constructs a platform view with clip rrect scenario.
PlatformViewClipRRectScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
100,
100,
400,
400,
topLeft: const Radius.circular(15),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// Platform view with clip rrect.
/// The bounding rect of the rrect is the same as PlatformView and only the corner radii clips the PlatformView.
class PlatformViewLargeClipRRectScenario extends PlatformViewScenario {
/// Constructs a platform view with large clip rrect scenario.
PlatformViewLargeClipRRectScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
0,
0,
500,
500,
topLeft: const Radius.circular(15),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// Platform view with clip path.
class PlatformViewClipPathScenario extends PlatformViewScenario {
/// Constructs a platform view with clip path scenario.
PlatformViewClipPathScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final Path path = Path()
..moveTo(100, 100)
..quadraticBezierTo(50, 250, 100, 400)
..lineTo(350, 400)
..cubicTo(400, 300, 300, 200, 350, 100)
..close();
final SceneBuilder builder = SceneBuilder()..pushClipPath(path);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// Platform view with clip rect after transformed.
class PlatformViewClipRectWithTransformScenario extends PlatformViewScenario {
/// Constructs a platform view with clip rect with transform scenario.
PlatformViewClipRectWithTransformScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final Matrix4 matrix4 = Matrix4.identity()
..rotateZ(1)
..scale(0.5, 0.5, 1.0)
..translate(1000.0, 100.0);
final SceneBuilder builder = SceneBuilder()..pushTransform(matrix4.storage);
builder.pushClipRect(const Rect.fromLTRB(100, 100, 400, 400));
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
// Add a translucent rect that has the same size of PlatformView.
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 500, 500),
Paint()..color = const Color(0x22FF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
finishBuilder(builder);
}
}
/// Platform view with clip rrect after transformed.
class PlatformViewClipRRectWithTransformScenario extends PlatformViewScenario {
/// Constructs a platform view with clip rrect with transform scenario.
PlatformViewClipRRectWithTransformScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final Matrix4 matrix4 = Matrix4.identity()
..rotateZ(1)
..scale(0.5, 0.5, 1.0)
..translate(1000.0, 100.0);
final SceneBuilder builder = SceneBuilder()..pushTransform(matrix4.storage);
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
100,
100,
400,
400,
topLeft: const Radius.circular(15),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
// Add a translucent rect that has the same size of PlatformView.
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 500, 500),
Paint()..color = const Color(0x22FF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
finishBuilder(builder);
}
}
/// Platform view with clip rrect after transformed.
/// The bounding rect of the rrect is the same as PlatformView and only the corner radii clips the PlatformView.
class PlatformViewLargeClipRRectWithTransformScenario extends PlatformViewScenario {
/// Constructs a platform view with large clip rrect with transform scenario.
PlatformViewLargeClipRRectWithTransformScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final Matrix4 matrix4 = Matrix4.identity()
..rotateZ(1)
..scale(0.5, 0.5, 1.0)
..translate(1000.0, 100.0);
final SceneBuilder builder = SceneBuilder()..pushTransform(matrix4.storage);
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
0,
0,
500,
500,
topLeft: const Radius.circular(15),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
// Add a translucent rect that has the same size of PlatformView.
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 500, 500),
Paint()..color = const Color(0x22FF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
finishBuilder(builder);
}
}
/// Platform view with clip path after transformed.
class PlatformViewClipPathWithTransformScenario extends PlatformViewScenario {
/// Constructs a platform view with clip path with transform scenario.
PlatformViewClipPathWithTransformScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final Matrix4 matrix4 = Matrix4.identity()
..rotateZ(1)
..scale(0.5, 0.5, 1.0)
..translate(1000.0, 100.0);
final SceneBuilder builder = SceneBuilder()..pushTransform(matrix4.storage);
final Path path = Path()
..moveTo(100, 100)
..quadraticBezierTo(50, 250, 100, 400)
..lineTo(350, 400)
..cubicTo(400, 300, 300, 200, 350, 100)
..close();
builder.pushClipPath(path);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
// Add a translucent rect that has the same size of PlatformView.
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 500, 500),
Paint()..color = const Color(0x22FF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
finishBuilder(builder);
}
}
/// Two platform views, both have clip rects
class TwoPlatformViewClipRect extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
TwoPlatformViewClipRect(
super.view, {
required this.firstId,
required this.secondId,
});
/// The platform view identifier to use for the first platform view.
final int firstId;
/// The platform view identifier to use for the second platform view.
final int secondId;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 600);
builder.pushClipRect(const Rect.fromLTRB(100, 100, 400, 400));
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
builder.pop();
// Use a different rect to differentiate from the 1st clip rect.
builder.pushClipRect(const Rect.fromLTRB(100, 100, 300, 300));
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
builder.pop();
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Two platform views, both have clip rrects
class TwoPlatformViewClipRRect extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
TwoPlatformViewClipRRect(
super.view, {
required this.firstId,
required this.secondId,
});
/// The platform view identifier to use for the first platform view.
final int firstId;
/// The platform view identifier to use for the second platform view.
final int secondId;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 600);
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
0,
0,
500,
500,
topLeft: const Radius.circular(15),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
builder.pop();
// Use a different rrect to differentiate from the 1st clip rrect.
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
0,
0,
500,
500,
topLeft: const Radius.circular(100),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
builder.pop();
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Two platform views, both have clip path
class TwoPlatformViewClipPath extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
TwoPlatformViewClipPath(
super.view, {
required this.firstId,
required this.secondId,
});
/// The platform view identifier to use for the first platform view.
final int firstId;
/// The platform view identifier to use for the second platform view.
final int secondId;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 600);
final Path path = Path()
..moveTo(100, 100)
..quadraticBezierTo(50, 250, 100, 400)
..lineTo(350, 400)
..cubicTo(400, 300, 300, 200, 350, 100)
..close();
builder.pushClipPath(path);
addPlatformView(
firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 1',
);
builder.pop();
builder.pop();
// Use a different path to differentiate from the 1st clip path.
final Path path2 = Path()
..moveTo(100, 100)
..quadraticBezierTo(100, 150, 100, 400)
..lineTo(350, 350)
..cubicTo(400, 300, 300, 200, 350, 200)
..close();
builder.pushClipPath(path2);
addPlatformView(
secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
builder.pop();
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Platform view with transform.
class PlatformViewTransformScenario extends PlatformViewScenario {
/// Constructs a platform view with transform scenario.
PlatformViewTransformScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final Matrix4 matrix4 = Matrix4.identity()
..rotateZ(1)
..scale(0.5, 0.5, 1.0)
..translate(1000.0, 100.0);
final SceneBuilder builder = SceneBuilder()..pushTransform(matrix4.storage);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// Platform view with opacity.
class PlatformViewOpacityScenario extends PlatformViewScenario {
/// Constructs a platform view with transform scenario.
PlatformViewOpacityScenario(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder()..pushOpacity(150);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// A simple platform view for testing touch events from iOS.
class PlatformViewForTouchIOSScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewForTouchIOSScenario(
super.view, {
this.id = 0,
this.rejectUntilTouchesEnded = false,
required this.accept,
}) {
_nextFrame = _firstFrame;
}
late void Function() _nextFrame;
/// Whether gestures should be accepted or rejected.
final bool accept;
/// The platform view identifier.
final int id;
/// Whether touches should be rejected until the gesture ends.
final bool rejectUntilTouchesEnded;
@override
void onBeginFrame(Duration duration) {
_nextFrame();
}
@override
void onDrawFrame() {
// Some iOS gesture recognizers bugs are introduced in the second frame (with a different platform view rect) after laying out the platform view.
// So in this test, we load 2 frames to ensure that we cover those cases.
// See https://github.com/flutter/flutter/issues/66044
if (_nextFrame == _firstFrame) {
_nextFrame = _secondFrame;
view.platformDispatcher.scheduleFrame();
}
super.onDrawFrame();
}
@override
void onPointerDataPacket(PointerDataPacket packet) {
if (packet.data.first.change == PointerChange.add) {
String method = 'rejectGesture';
if (accept) {
method = 'acceptGesture';
}
const int valueString = 7;
const int valueInt32 = 3;
const int valueMap = 13;
final Uint8List message = Uint8List.fromList(<int>[
valueString,
..._encodeString(method),
valueMap,
1,
valueString,
..._encodeString('id'),
valueInt32,
..._to32(id),
]);
view.platformDispatcher.sendPlatformMessage(
'flutter/platform_views',
message.buffer.asByteData(),
(ByteData? response) {},
);
}
}
void _firstFrame() {
final SceneBuilder builder = SceneBuilder();
if (rejectUntilTouchesEnded) {
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
viewType: 'scenarios/textPlatformView_blockPolicyUntilTouchesEnded',
);
} else {
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
}
finishBuilder(builder);
}
void _secondFrame() {
final SceneBuilder builder = SceneBuilder()..pushOffset(5, 5);
if (rejectUntilTouchesEnded) {
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
viewType: 'scenarios/textPlatformView_blockPolicyUntilTouchesEnded',
);
} else {
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
}
finishBuilder(builder);
}
}
/// Scenario for verifying overlapping platform views can accept touch gesture.
/// See: https://github.com/flutter/flutter/issues/118366.
///
/// Renders the first frame with a foreground platform view.
/// Then renders the second frame with the foreground platform view covering
/// a new background platform view.
///
class PlatformViewForOverlappingPlatformViewsScenario extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformViewForOverlappingPlatformViewsScenario.
PlatformViewForOverlappingPlatformViewsScenario(
super.view, {
required this.foregroundId,
required this.backgroundId,
}) {
_nextFrame = _firstFrame;
}
/// The id for a foreground platform view that covers another background platform view.
/// A good example is a dialog prompt in a real app.
final int foregroundId;
/// The id for a background platform view that is covered by a foreground platform view.
final int backgroundId;
late void Function() _nextFrame;
@override
void onBeginFrame(Duration duration) {
_nextFrame();
}
void _firstFrame() {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(100, 100);
addPlatformView(
foregroundId,
width: 100,
height: 100,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'Foreground',
);
builder.pop();
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
void _secondFrame() {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 0);
addPlatformView(
backgroundId,
width: 300,
height: 300,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'Background',
);
builder.pop();
builder.pushOffset(100, 100);
addPlatformView(
foregroundId,
width: 100,
height: 100,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'Foreground',
);
builder.pop();
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
int _frameCount = 0;
@override
void onDrawFrame() {
_frameCount += 1;
// TODO(hellohuanlin): Need further investigation - the first 2 frames are dropped for some reason.
// Wait for 60 frames to ensure the first frame has actually been rendered
// (Minimum required is 3 frames, but just to be safe)
if (_nextFrame == _firstFrame && _frameCount == 60) {
_nextFrame = _secondFrame;
}
view.platformDispatcher.scheduleFrame();
super.onDrawFrame();
}
@override
void onPointerDataPacket(PointerDataPacket packet) {
final PointerData data = packet.data.first;
final double x = data.physicalX;
final double y = data.physicalY;
if (data.change == PointerChange.up && 100 <= x && x < 200 && 100 <= y && y < 200) {
const int valueString = 7;
const int valueInt32 = 3;
const int valueMap = 13;
final Uint8List message = Uint8List.fromList(<int>[
valueString,
..._encodeString('acceptGesture'),
valueMap,
1,
valueString,
..._encodeString('id'),
valueInt32,
..._to32(foregroundId),
]);
view.platformDispatcher.sendPlatformMessage(
'flutter/platform_views',
message.buffer.asByteData(),
(ByteData? response) {},
);
}
}
}
/// A simple platform view for testing platform view with a continuous texture layer.
/// For example, it simulates a video being played.
class PlatformViewWithContinuousTexture extends PlatformViewScenario {
/// Constructs a platform view with continuous texture layer.
PlatformViewWithContinuousTexture(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
builder.addTexture(0,
width: 300, height: 300, offset: const Offset(200, 200));
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
finishBuilder(builder);
}
}
/// A simple platform view for testing backDropFilter with a platform view in the scene.
///
/// The stack would look like: picture 1-> filter -> picture 2 -> pv -> picture 3. And picture 1 should be filtered.
///
/// Note it is not testing applying backDropFilter on a platform view.
/// See: https://github.com/flutter/flutter/issues/80766
class PlatformViewWithOtherBackDropFilter extends PlatformViewScenario {
/// Constructs the scenario.
PlatformViewWithOtherBackDropFilter(
super.view, {
super.id = 0,
});
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
// This is just a background picture to make the result more viewable.
canvas.drawRect(
const Rect.fromLTRB(0, 0, 500, 400),
Paint()..color = const Color(0xFFFF0000),
);
// This rect should look blur due to the backdrop filter.
canvas.drawRect(
const Rect.fromLTRB(0, 0, 300, 300),
Paint()..color = const Color(0xFF00FF00),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
final ImageFilter filter = ImageFilter.blur(sigmaX: 8, sigmaY: 8);
builder.pushBackdropFilter(filter);
final PictureRecorder recorder2 = PictureRecorder();
final Canvas canvas2 = Canvas(recorder2);
// This circle should not look blur.
canvas2.drawCircle(
const Offset(200, 100),
50,
Paint()..color = const Color(0xFF0000EF),
);
final Picture picture2 = recorder2.endRecording();
builder.addPicture(const Offset(100, 100), picture2);
builder.pop();
builder.pushOffset(0, 600);
addPlatformView(
id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
);
builder.pop();
final PictureRecorder recorder3 = PictureRecorder();
final Canvas canvas3 = Canvas(recorder3);
// Add another picture layer so an overlay UIView is created, which was
// the root cause of the original issue.
canvas3.drawCircle(
const Offset(300, 200),
50,
Paint()..color = const Color(0xFF0000EF),
);
final Picture picture3 = recorder3.endRecording();
builder.addPicture(const Offset(100, 100), picture3);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// A simple platform view for testing backDropFilter with a platform view in the scene.
///
/// The stack would look like: picture 1 -> pv1 -> picture 2 -> filter -> pv2 - > picture 3.
class TwoPlatformViewsWithOtherBackDropFilter extends Scenario
with _BasePlatformViewScenarioMixin {
/// Constructs the scenario.
TwoPlatformViewsWithOtherBackDropFilter(
super.view, {
required int firstId,
required int secondId,
}) : _firstId = firstId,
_secondId = secondId;
final int _firstId;
final int _secondId;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
// This is just a background picture to make the result more viewable.
canvas.drawRect(
const Rect.fromLTRB(0, 0, 600, 1000),
Paint()..color = const Color(0xFFFF0000),
);
// This rect should look blur due to the backdrop filter.
canvas.drawRect(
const Rect.fromLTRB(0, 0, 300, 300),
Paint()..color = const Color(0xFF00FF00),
);
final Picture picture1 = recorder.endRecording();
builder.addPicture(Offset.zero, picture1);
builder.pushOffset(0, 200);
addPlatformView(
_firstId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
width: 100,
height: 100,
text: 'platform view 1'
);
final PictureRecorder recorder2 = PictureRecorder();
final Canvas canvas2 = Canvas(recorder2);
// This circle should look blur due to the backdrop filter.
canvas2.drawCircle(
const Offset(200, 100),
50,
Paint()..color = const Color(0xFF0000EF),
);
final Picture picture2 = recorder2.endRecording();
builder.addPicture(const Offset(100, 100), picture2);
final ImageFilter filter = ImageFilter.blur(sigmaX: 8, sigmaY: 8);
builder.pushBackdropFilter(filter);
builder.pushOffset(0, 600);
addPlatformView(
_secondId,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view 2',
);
builder.pop();
builder.pop();
final PictureRecorder recorder3 = PictureRecorder();
final Canvas canvas3 = Canvas(recorder3);
// Add another picture layer so an overlay UIView is created, which was
// the root cause of the original issue.
canvas3.drawCircle(
const Offset(300, 200),
50,
Paint()..color = const Color(0xFF0000EF),
);
final Picture picture3 = recorder3.endRecording();
builder.addPicture(const Offset(100, 100), picture3);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// A simple platform view for testing backDropFilter with a platform view in the scene.
///
/// The backdrop filter sigma value is negative, which tries to reproduce a crash, see:
/// https://github.com/flutter/flutter/issues/127095
class PlatformViewWithNegativeBackDropFilter extends Scenario
with _BasePlatformViewScenarioMixin {
/// Constructs the scenario.
PlatformViewWithNegativeBackDropFilter(
super.view, {
required int id,
}) : _id = id;
final int _id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
// This is just a background picture to make the result more viewable.
canvas.drawRect(
const Rect.fromLTRB(0, 0, 600, 1000),
Paint()..color = const Color(0xFFFF0000),
);
canvas.drawRect(
const Rect.fromLTRB(0, 0, 300, 300),
Paint()..color = const Color(0xFF00FF00),
);
final Picture picture1 = recorder.endRecording();
builder.addPicture(Offset.zero, picture1);
builder.pushOffset(0, 200);
addPlatformView(
_id,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
width: 100,
height: 100,
text: 'platform view 1'
);
final PictureRecorder recorder2 = PictureRecorder();
final Canvas canvas2 = Canvas(recorder2);
canvas2.drawCircle(
const Offset(200, 100),
50,
Paint()..color = const Color(0xFF0000EF),
);
final Picture picture2 = recorder2.endRecording();
builder.addPicture(const Offset(100, 100), picture2);
final ImageFilter filter = ImageFilter.blur(sigmaX: -8, sigmaY: 8);
builder.pushBackdropFilter(filter);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Builds a scenario where many platform views are scrolling and pass under a picture.
class PlatformViewScrollingUnderWidget extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewScrollingUnderWidget(
super.view, {
required int firstPlatformViewId,
required int lastPlatformViewId,
}) : _firstPlatformViewId = firstPlatformViewId,
_lastPlatformViewId = lastPlatformViewId;
final int _firstPlatformViewId;
final int _lastPlatformViewId;
double _offset = 0;
bool _movingUp = true;
@override
void onBeginFrame(Duration duration) {
_buildOneFrame(_offset);
}
@override
void onDrawFrame() {
// Scroll up until -1000, then scroll down until -1.
if (_offset < -1000) {
_movingUp = false;
} else if (_offset > -1) {
_movingUp = true;
}
if (_movingUp) {
_offset -= 100;
} else {
_offset += 100;
}
view.platformDispatcher.scheduleFrame();
super.onDrawFrame();
}
Future<void> _buildOneFrame(double offset) async {
const double cellWidth = 1000;
double localOffset = offset;
final SceneBuilder builder = SceneBuilder();
const double cellHeight = 300;
for (int i = _firstPlatformViewId; i <= _lastPlatformViewId; i++) {
// Build a list view with platform views.
builder.pushOffset(0, localOffset);
addPlatformView(
i,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view $i',
width: cellWidth,
height: cellHeight,
);
builder.pop();
localOffset += cellHeight;
}
// Add a "banner" that should display on top of the list view.
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTRB(0, cellHeight, cellWidth, 100),
Paint()..color = const Color(0xFFFF0000),
);
final Picture picture = recorder.endRecording();
builder.addPicture(const Offset(0, 20), picture);
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
/// Builds a scenario where many platform views with clips scrolling.
class PlatformViewsWithClipsScrolling extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
PlatformViewsWithClipsScrolling(
super.view, {
required int firstPlatformViewId,
required int lastPlatformViewId,
}) : _firstPlatformViewId = firstPlatformViewId,
_lastPlatformViewId = lastPlatformViewId;
final int _firstPlatformViewId;
final int _lastPlatformViewId;
double _offset = 0;
bool _movingUp = true;
@override
void onBeginFrame(Duration duration) {
_buildOneFrame(_offset);
}
@override
void onDrawFrame() {
// Scroll up until -1000, then scroll down until -1.
if (_offset < -500) {
_movingUp = false;
} else if (_offset > -1) {
_movingUp = true;
}
if (_movingUp) {
_offset -= 100;
} else {
_offset += 100;
}
view.platformDispatcher.scheduleFrame();
super.onDrawFrame();
}
Future<void> _buildOneFrame(double offset) async {
const double cellWidth = 1000;
double localOffset = offset;
final SceneBuilder builder = SceneBuilder();
const double cellHeight = 300;
for (int i = _firstPlatformViewId; i <= _lastPlatformViewId; i++) {
// Build a list view with platform views.
builder.pushOffset(0, localOffset);
bool addedClipRRect = false;
if (localOffset > -1) {
addedClipRRect = true;
builder.pushClipRRect(
RRect.fromLTRBAndCorners(
100,
100,
400,
400,
topLeft: const Radius.circular(15),
topRight: const Radius.circular(50),
bottomLeft: const Radius.circular(50),
),
);
}
addPlatformView(
i,
dispatcher: view.platformDispatcher,
sceneBuilder: builder,
text: 'platform view $i',
width: cellWidth,
height: cellHeight,
);
if (addedClipRRect) {
builder.pop();
}
builder.pop();
localOffset += cellHeight;
}
final Scene scene = builder.build();
view.render(scene);
scene.dispose();
}
}
final Map<String, int> _createdPlatformViews = <String, int> {};
/// Adds the platform view to the scene.
///
/// First, the platform view is created by calling the corresponding platform channel,
/// then a new frame is scheduled, finally the platform view is added to the scene builder.
void addPlatformView(
int id, {
required PlatformDispatcher dispatcher,
required SceneBuilder sceneBuilder,
String text = 'platform view',
double width = 500,
double height = 500,
String viewType = 'scenarios/textPlatformView',
}) {
if (scenarioParams['view_type'] is String) {
viewType = scenarioParams['view_type'] as String;
}
final String platformViewKey = '$viewType-$id';
if (_createdPlatformViews.containsKey(platformViewKey)) {
addPlatformViewToSceneBuilder(
id,
sceneBuilder: sceneBuilder,
textureId: _createdPlatformViews[platformViewKey]!,
width: width,
height: height,
);
return;
}
final bool usesAndroidHybridComposition = scenarioParams['use_android_view'] as bool? ?? false;
final bool expectAndroidHybridCompositionFallback =
scenarioParams['expect_android_view_fallback'] as bool? ?? false;
const int valueTrue = 1;
const int valueFalse = 2;
const int valueInt32 = 3;
const int valueFloat64 = 6;
const int valueString = 7;
const int valueUint8List = 8;
const int valueMap = 13;
final Uint8List message = Uint8List.fromList(<int>[
valueString,
..._encodeString('create'),
valueMap,
if (Platform.isIOS) 3, // 3 entries in map for iOS.
if (Platform.isAndroid && !usesAndroidHybridComposition)
7, // 7 entries in map for texture on Android.
if (Platform.isAndroid && usesAndroidHybridComposition)
5, // 5 entries in map for hybrid composition on Android.
valueString,
..._encodeString('id'),
valueInt32,
..._to32(id),
valueString,
..._encodeString('viewType'),
valueString,
..._encodeString(viewType),
if (Platform.isAndroid && !usesAndroidHybridComposition) ...<int>[
valueString,
..._encodeString('width'),
// This is missing the 64-bit boundary alignment, making the entire
// message encoding fragile to changes before this point. Do not add new
// variable-length values such as strings before this point.
// TODO(stuartmorgan): Fix this to use the actual encoding logic,
// including alignment: https://github.com/flutter/flutter/issues/111188
valueFloat64,
..._to64(width),
valueString,
..._encodeString('height'),
valueFloat64,
..._to64(height),
valueString,
..._encodeString('direction'),
valueInt32,
..._to32(0), // LTR
valueString,
..._encodeString('hybridFallback'),
if (expectAndroidHybridCompositionFallback) valueTrue
else valueFalse,
],
if (Platform.isAndroid && usesAndroidHybridComposition) ...<int>[
valueString,
..._encodeString('hybrid'),
valueTrue,
valueString,
..._encodeString('direction'),
valueInt32,
..._to32(0), // LTR
],
valueString,
..._encodeString('params'),
valueUint8List,
..._encodeString(text),
]);
dispatcher.sendPlatformMessage(
'flutter/platform_views',
message.buffer.asByteData(),
(ByteData? response) {
late int textureId;
if (response != null &&
Platform.isAndroid &&
!usesAndroidHybridComposition) {
assert(response.getUint8(0) == 0, 'expected envelope');
final int type = response.getUint8(1);
if (expectAndroidHybridCompositionFallback) {
// Fallback is indicated with a null return.
assert(type == 0, 'expected null');
textureId = -1;
} else {
// This is the texture ID.
assert(type == 4, 'expected int64');
textureId = response.getInt64(2, Endian.host);
}
} else {
// There no texture ID.
textureId = -1;
}
_createdPlatformViews[platformViewKey] = textureId;
dispatcher.scheduleFrame();
},
);
}
/// Adds the platform view to the scene builder.
Future<void> addPlatformViewToSceneBuilder(
int id, {
required SceneBuilder sceneBuilder,
required int textureId,
double width = 500,
double height = 500,
}) async {
if (Platform.isIOS) {
sceneBuilder.addPlatformView(id, width: width, height: height);
} else if (Platform.isAndroid) {
final bool expectAndroidHybridCompositionFallback =
scenarioParams['expect_android_view_fallback'] as bool? ?? false;
final bool usesAndroidHybridComposition =
(scenarioParams['use_android_view'] as bool? ?? false) ||
expectAndroidHybridCompositionFallback;
if (usesAndroidHybridComposition) {
sceneBuilder.addPlatformView(id, width: width, height: height);
} else if (textureId != -1) {
sceneBuilder.addTexture(textureId, width: width, height: height);
} else {
throw UnsupportedError('Invalid texture id $textureId');
}
} else {
throw UnsupportedError(
'Platform ${Platform.operatingSystem} is not supported');
}
}
mixin _BasePlatformViewScenarioMixin on Scenario {
// Add a picture and finishes the `sceneBuilder`.
void finishBuilder(
SceneBuilder sceneBuilder, {
Offset? overlayOffset,
}) {
overlayOffset ??= const Offset(50, 50);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawCircle(
overlayOffset,
50,
Paint()..color = const Color(0xFFABCDEF),
);
final Picture picture = recorder.endRecording();
sceneBuilder.addPicture(const Offset(300, 300), picture);
final Scene scene = sceneBuilder.build();
view.render(scene);
scene.dispose();
}
}