From c0c231be3b0782c7cc237a304e29dd97398c271c Mon Sep 17 00:00:00 2001 From: Angjie Li <56002538+angjieli@users.noreply.github.com> Date: Tue, 13 Apr 2021 10:58:50 -0700 Subject: [PATCH] Revert "Reland InteractiveViewer.builder (#79287)" (#80305) This reverts commit 0fd75528de3f8d5225ce721e8be27041b8f6e2b6. --- .../lib/src/widgets/interactive_viewer.dart | 341 +++--------------- .../test/widgets/interactive_viewer_test.dart | 107 ------ 2 files changed, 41 insertions(+), 407 deletions(-) diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index a5949b6e09..2f293ab82a 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -11,17 +11,8 @@ import 'package:vector_math/vector_math_64.dart' show Quad, Vector3, Matrix4; import 'basic.dart'; import 'framework.dart'; import 'gesture_detector.dart'; -import 'layout_builder.dart'; import 'ticker_provider.dart'; -/// A type for widget builders that take a [Quad] of the current viewport. -/// -/// See also: -/// -/// * [InteractiveViewer.builder], whose builder is of this type. -/// * [WidgetBuilder], which is similar, but takes no viewport. -typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Quad viewport); - /// A widget that enables pan and zoom interactions with its child. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=zrn7V3bMJvg} @@ -41,6 +32,8 @@ typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Q /// robust positioning of an InteractiveViewer child that works for all screen /// sizes and child sizes. /// +/// The [child] must not be null. +/// /// See also: /// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/master/lib/demos/reference/transformations_demo.dart), /// which includes the use of InteractiveViewer. @@ -74,7 +67,7 @@ typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Q class InteractiveViewer extends StatefulWidget { /// Create an InteractiveViewer. /// - /// The `child` parameter must not be null. + /// The [child] parameter must not be null. InteractiveViewer({ Key? key, this.clipBehavior = Clip.hardEdge, @@ -91,7 +84,7 @@ class InteractiveViewer extends StatefulWidget { this.panEnabled = true, this.scaleEnabled = true, this.transformationController, - required Widget child, + required this.child, }) : assert(alignPanAxis != null), assert(child != null), assert(constrained != null), @@ -112,50 +105,6 @@ class InteractiveViewer extends StatefulWidget { && boundaryMargin.right.isFinite && boundaryMargin.bottom.isFinite && boundaryMargin.left.isFinite), ), - builder = _getBuilderForChild(child), - super(key: key); - - /// Creates an InteractiveViewer for a child that is created on demand. - /// - /// Can be used to render a child that changes in response to the current - /// transformation. - /// - /// The [builder] parameter must not be null. See its docs for an example of - /// using it to optimize a large child. - InteractiveViewer.builder({ - Key? key, - this.clipBehavior = Clip.hardEdge, - this.alignPanAxis = false, - this.boundaryMargin = EdgeInsets.zero, - // These default scale values were eyeballed as reasonable limits for common - // use cases. - this.maxScale = 2.5, - this.minScale = 0.8, - this.onInteractionEnd, - this.onInteractionStart, - this.onInteractionUpdate, - this.panEnabled = true, - this.scaleEnabled = true, - this.transformationController, - required this.builder, - }) : assert(alignPanAxis != null), - assert(builder != null), - assert(minScale != null), - assert(minScale > 0), - assert(minScale.isFinite), - assert(maxScale != null), - assert(maxScale > 0), - assert(!maxScale.isNaN), - assert(maxScale >= minScale), - assert(panEnabled != null), - assert(scaleEnabled != null), - // boundaryMargin must be either fully infinite or fully finite, but not - // a mix of both. - assert((boundaryMargin.horizontal.isInfinite - && boundaryMargin.vertical.isInfinite) || (boundaryMargin.top.isFinite - && boundaryMargin.right.isFinite && boundaryMargin.bottom.isFinite - && boundaryMargin.left.isFinite)), - constrained = false, super(key: key); /// If set to [Clip.none], the child may extend beyond the size of the InteractiveViewer, @@ -191,206 +140,13 @@ class InteractiveViewer extends StatefulWidget { /// No edge can be NaN. /// /// Defaults to [EdgeInsets.zero], which results in boundaries that are the - /// exact same size and position as the child. + /// exact same size and position as the [child]. final EdgeInsets boundaryMargin; - /// Builds the child of this widget. + /// The Widget to perform the transformations on. /// - /// If a child is passed directly, then this is simply a function that returns - /// that child. - /// - /// If using the [InteractiveViewer.builder] constructor, this can be passed - /// directly. This allows the child to be built in response to the current - /// transformation. - /// - /// {@tool dartpad --template=freeform} - /// - /// This example shows how to use builder to create a [Table] whose cell - /// contents are only built when they are visible. Built and remove cells are - /// logged in the console for illustration. - /// - /// ```dart main - /// import 'package:vector_math/vector_math_64.dart' show Quad, Vector3; - /// - /// import 'package:flutter/material.dart'; - /// import 'package:flutter/widgets.dart'; - /// - /// void main() => runApp(const IVBuilderExampleApp()); - /// - /// class IVBuilderExampleApp extends StatelessWidget { - /// const IVBuilderExampleApp({Key? key}) : super(key: key); - /// - /// @override - /// Widget build(BuildContext context) { - /// return MaterialApp( - /// home: Scaffold( - /// appBar: AppBar( - /// title: const Text('IV Builder Example'), - /// ), - /// body: _IVBuilderExample(), - /// ), - /// ); - /// } - /// } - /// - /// class _IVBuilderExample extends StatefulWidget { - /// @override - /// _IVBuilderExampleState createState() => _IVBuilderExampleState(); - /// } - /// - /// class _IVBuilderExampleState extends State<_IVBuilderExample> { - /// final TransformationController _transformationController = TransformationController(); - /// - /// static const double _cellWidth = 200.0; - /// static const double _cellHeight = 26.0; - /// - /// // Returns true iff the given cell is currently visible. Caches viewport - /// // calculations. - /// late Quad _cachedViewport; - /// late int _firstVisibleRow; - /// late int _firstVisibleColumn; - /// late int _lastVisibleRow; - /// late int _lastVisibleColumn; - /// bool _isCellVisible(int row, int column, Quad viewport) { - /// if (viewport != _cachedViewport) { - /// final Rect aabb = _axisAlignedBoundingBox(viewport); - /// _cachedViewport = viewport; - /// _firstVisibleRow = (aabb.top / _cellHeight).floor(); - /// _firstVisibleColumn = (aabb.left / _cellWidth).floor(); - /// _lastVisibleRow = (aabb.bottom / _cellHeight).floor(); - /// _lastVisibleColumn = (aabb.right / _cellWidth).floor(); - /// } - /// return row >= _firstVisibleRow && row <= _lastVisibleRow - /// && column >= _firstVisibleColumn && column <= _lastVisibleColumn; - /// } - /// - /// // Returns the axis aligned bounding box for the given Quad, which might not - /// // be axis aligned. - /// Rect _axisAlignedBoundingBox(Quad quad) { - /// double? xMin; - /// double? xMax; - /// double? yMin; - /// double? yMax; - /// for (final Vector3 point in [quad.point0, quad.point1, quad.point2, quad.point3]) { - /// if (xMin == null || point.x < xMin) { - /// xMin = point.x; - /// } - /// if (xMax == null || point.x > xMax) { - /// xMax = point.x; - /// } - /// if (yMin == null || point.y < yMin) { - /// yMin = point.y; - /// } - /// if (yMax == null || point.y > yMax) { - /// yMax = point.y; - /// } - /// } - /// return Rect.fromLTRB(xMin!, yMin!, xMax!, yMax!); - /// } - /// - /// void _onChangeTransformation() { - /// setState(() {}); - /// } - /// - /// @override - /// void initState() { - /// super.initState(); - /// _transformationController.addListener(_onChangeTransformation); - /// } - /// - /// @override - /// void dispose() { - /// _transformationController.removeListener(_onChangeTransformation); - /// super.dispose(); - /// } - /// - /// @override - /// Widget build(BuildContext context) { - /// return Center( - /// child: LayoutBuilder( - /// builder: (BuildContext context, BoxConstraints constraints) { - /// return InteractiveViewer.builder( - /// alignPanAxis: true, - /// scaleEnabled: false, - /// transformationController: _transformationController, - /// builder: (BuildContext context, Quad viewport) { - /// // A simple extension of Table that builds cells. - /// return _TableBuilder( - /// rowCount: 60, - /// columnCount: 6, - /// cellWidth: _cellWidth, - /// builder: (BuildContext context, int row, int column) { - /// if (!_isCellVisible(row, column, viewport)) { - /// print('removing cell ($row, $column)'); - /// return Container(height: _cellHeight); - /// } - /// print('building cell ($row, $column)'); - /// return Container( - /// height: _cellHeight, - /// color: row % 2 + column % 2 == 1 ? Colors.white : Colors.grey.withOpacity(0.1), - /// child: Align( - /// alignment: Alignment.centerLeft, - /// child: Text('$row x $column'), - /// ), - /// ); - /// } - /// ); - /// }, - /// ); - /// }, - /// ), - /// ); - /// } - /// } - /// - /// typedef _CellBuilder = Widget Function(BuildContext context, int row, int column); - /// - /// class _TableBuilder extends StatelessWidget { - /// const _TableBuilder({ - /// required this.rowCount, - /// required this.columnCount, - /// required this.cellWidth, - /// required this.builder, - /// }) : assert(rowCount > 0), - /// assert(columnCount > 0); - /// - /// final int rowCount; - /// final int columnCount; - /// final double cellWidth; - /// final _CellBuilder builder; - /// - /// @override - /// Widget build(BuildContext context) { - /// return Table( - /// // ignore: prefer_const_literals_to_create_immutables - /// columnWidths: { - /// for (int column = 0; column < columnCount; column++) - /// column: FixedColumnWidth(cellWidth), - /// }, - /// // ignore: prefer_const_literals_to_create_immutables - /// children: [ - /// for (int row = 0; row < rowCount; row++) - /// // ignore: prefer_const_constructors - /// TableRow( - /// // ignore: prefer_const_literals_to_create_immutables - /// children: [ - /// for (int column = 0; column < columnCount; column++) - /// builder(context, row, column), - /// ], - /// ), - /// ], - /// ); - /// } - /// } - /// ``` - /// {@end-tool} - /// - /// See also: - /// - /// * [ListView.builder], which follows a similar pattern. - /// * [InteractiveViewer.builder], which has an example of building the - /// child on demand. - final InteractiveViewerWidgetBuilder builder; + /// Cannot be null. + final Widget child; /// Whether the normal size constraints at this point in the widget tree are /// applied to the child. @@ -670,13 +426,6 @@ class InteractiveViewer extends StatefulWidget { /// * [TextEditingController] for an example of another similar pattern. final TransformationController? transformationController; - // Get a InteractiveViewerWidgetBuilder that simply returns the given child. - static InteractiveViewerWidgetBuilder _getBuilderForChild(Widget child) { - return (BuildContext context, Quad viewport) { - return child; - }; - } - /// Returns the closest point to the given point on the given line segment. @visibleForTesting static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { @@ -1329,52 +1078,44 @@ class _InteractiveViewerState extends State with TickerProvid @override Widget build(BuildContext context) { + Widget child = Transform( + transform: _transformationController!.value, + child: KeyedSubtree( + key: _childKey, + child: widget.child, + ), + ); + + if (!widget.constrained) { + child = OverflowBox( + alignment: Alignment.topLeft, + minWidth: 0.0, + minHeight: 0.0, + maxWidth: double.infinity, + maxHeight: double.infinity, + child: child, + ); + } + + if (widget.clipBehavior != Clip.none) { + child = ClipRect( + clipBehavior: widget.clipBehavior, + child: child, + ); + } + // A GestureDetector allows the detection of panning and zooming gestures on // the child. return Listener( key: _parentKey, onPointerSignal: _receivedPointerSignal, - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final Matrix4 matrix = _transformationController!.value; - // When constrained is false, such as when using - // InteractiveViewer.builder, then the viewport is the size of the - // constraints. - Widget child = Transform( - transform: matrix, - child: KeyedSubtree( - key: _childKey, - child: widget.builder(context, _transformViewport(matrix, Offset.zero & constraints.biggest)), - ), - ); - - if (!widget.constrained) { - child = OverflowBox( - alignment: Alignment.topLeft, - minWidth: 0.0, - minHeight: 0.0, - maxWidth: double.infinity, - maxHeight: double.infinity, - child: child, - ); - } - - if (widget.clipBehavior != Clip.none) { - child = ClipRect( - clipBehavior: widget.clipBehavior, - child: child, - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, // Necessary when panning off screen. - dragStartBehavior: DragStartBehavior.start, - onScaleEnd: _onScaleEnd, - onScaleStart: _onScaleStart, - onScaleUpdate: _onScaleUpdate, - child: child, - ); - }, + child: GestureDetector( + behavior: HitTestBehavior.opaque, // Necessary when panning off screen. + dragStartBehavior: DragStartBehavior.start, + onScaleEnd: _onScaleEnd, + onScaleStart: _onScaleStart, + onScaleUpdate: _onScaleUpdate, + child: child, ), ); } diff --git a/packages/flutter/test/widgets/interactive_viewer_test.dart b/packages/flutter/test/widgets/interactive_viewer_test.dart index e1735ae04e..ed62e2e4c2 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -1161,89 +1161,6 @@ void main() { findsOneWidget, ); }); - - testWidgets('builder can change widgets that are off-screen', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); - const double childHeight = 10.0; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Center( - child: SizedBox( - height: 50.0, - child: InteractiveViewer.builder( - transformationController: transformationController, - scaleEnabled: false, - boundaryMargin: const EdgeInsets.all(double.infinity), - // Build visible children green, off-screen children red. - builder: (BuildContext context, Quad viewportQuad) { - final Rect viewport = _axisAlignedBoundingBox(viewportQuad); - final List children = []; - for (int i = 0; i < 10; i++) { - final double childTop = i * childHeight; - final double childBottom = childTop + childHeight; - final bool visible = (childBottom >= viewport.top && childBottom <= viewport.bottom) - || (childTop >= viewport.top && childTop <= viewport.bottom); - children.add(Container( - height: childHeight, - color: visible ? Colors.green : Colors.red, - )); - } - return Column( - children: children, - ); - }, - ), - ), - ), - ), - ), - ); - - expect(transformationController.value, equals(Matrix4.identity())); - - // The first six are partially visible and therefore green. - int i = 0; - for (final Element element in find.byType(Container, skipOffstage: false).evaluate()) { - final Container container = element.widget as Container; - if (i < 6) { - expect(container.color, Colors.green); - } else { - expect(container.color, Colors.red); - } - i++; - } - - // Drag to pan down past the first child. - final Offset childOffset = tester.getTopLeft(find.byType(SizedBox)); - const double translationY = 15.0; - final Offset childInterior = Offset( - childOffset.dx, - childOffset.dy + translationY, - ); - final TestGesture gesture = await tester.startGesture(childInterior); - addTearDown(gesture.removePointer); - await tester.pump(); - await gesture.moveTo(childOffset); - await tester.pump(); - await gesture.up(); - await tester.pumpAndSettle(); - expect(transformationController.value, isNot(Matrix4.identity())); - expect(transformationController.value.getTranslation().y, -translationY); - - // After scrolling down a bit, the first child is not visible, the next - // six are, and the final three are not. - i = 0; - for (final Element element in find.byType(Container, skipOffstage: false).evaluate()) { - final Container container = element.widget as Container; - if (i > 0 && i < 7) { - expect(container.color, Colors.green); - } else { - expect(container.color, Colors.red); - } - i++; - } - }); }); group('getNearestPointOnLine', () { @@ -1453,27 +1370,3 @@ void main() { }); }); } - -// Returns the axis aligned bounding box for the given Quad, which might not -// be axis aligned. -Rect _axisAlignedBoundingBox(Quad quad) { - double? xMin; - double? xMax; - double? yMin; - double? yMax; - for (final Vector3 point in [quad.point0, quad.point1, quad.point2, quad.point3]) { - if (xMin == null || point.x < xMin) { - xMin = point.x; - } - if (xMax == null || point.x > xMax) { - xMax = point.x; - } - if (yMin == null || point.y < yMin) { - yMin = point.y; - } - if (yMax == null || point.y > yMax) { - yMax = point.y; - } - } - return Rect.fromLTRB(xMin!, yMin!, xMax!, yMax!); -}