-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
(This is similar to another issue that happens with the Canvas.transform() method filed under #82955)
The following example demonstrates a problem with the implementation of the Transform widget where it accepts 4x4 matrices in its transform property, but it does not compose the matrix appropriately if there are multiple nested Transform widgets that all use 4x4 matrices.
(Note that when there are no children that require compositing, this issue would be another example of #82955 but a RepaintBoundary is used here to force the Transform to use layers to implement the nested transforms so that this example is testing the Transform layers, not the Canvas transform method.)
This limitation is due to relying on the SkMatrix to maintain the matrix storage when SkMatrix is limited to a 3x3 matrix. As a result, the Transform Layer implementation will reduce all 4x4 matrices down to 3x3 matrices in the constructor. That may work OK if there is only ever a single layer with a 4x4 matrix, but it does not work if there are multiple such layers.
In this example, a 3D "wobble" effect is rendered by rotating a matrix around the X and Y axes. In the upper example, these operations are applied using several nested Transform widgets each incrementally applying a single element of the transform and the result fails to render correctly. In the bottom example, all transform operations are composed in a Matrix4 object and then only added to the Widget hierarchy in a single Transform widget/layer and it works.
Tested on:
- iOS, Android, MacOS - fails
- web with canvaskit and HTML renderer - fails
Flutter 2.3.0-13.0.pre.46 • channel unknown • unknown source
Framework • revision f54dc5bf95 (2 weeks ago) • 2021-05-05 00:20:19 -0700
Engine • revision 0a372173ff
Tools • Dart 2.14.0 (build 2.14.0-119.0.dev)
Details
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: '3D Transform Compositing Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin<MyHomePage> {
late AnimationController _controller;
double _rotateAroundX = 0.0;
double _rotateAroundY = 0.0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 3));
_controller.addListener(() { setState(() {
final double angle = _controller.value * 2 * pi;
_rotateAroundX = sin(angle) * pi / 4.0;
_rotateAroundY = cos(angle) * pi / 4.0;
}); });
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget transformIncrementally(Widget child) {
child = RepaintBoundary(child: child);
child = Transform(
transform: Matrix4.rotationY(_rotateAroundY),
child: child,
);
child = Transform(
transform: Matrix4.rotationX(_rotateAroundX),
child: child,
);
child = Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..setEntry(3, 2, 0.001),
child: child,
);
return child;
}
Widget transformAtOnce(Widget child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(_rotateAroundX)
..rotateY(_rotateAroundY),
child: RepaintBoundary(child: child),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
transformIncrementally(CustomPaint(
painter: MyPainter(Colors.red),
child: const SizedBox(width: 150, height: 150),
)),
transformAtOnce(CustomPaint(
painter: MyPainter(Colors.green),
child: const SizedBox(width: 150, height: 150),
)),
],
),
),
);
}
}
class MyPainter extends CustomPainter {
MyPainter(this.color);
final Color color;
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width * 0.5, size.height * 0.5);
canvas.drawRect(const Rect.fromLTRB(-50, -50, 50, 50), Paint()..color = color);
canvas.drawRect(const Rect.fromLTRB(-30, -30, -20, 30), Paint()..color = Colors.black);
canvas.drawRect(const Rect.fromLTRB(-30, -30, 30, -20), Paint()..color = Colors.black);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}