Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
a8e41f82
Unverified
Commit
a8e41f82
authored
Apr 27, 2021
by
Justin McCandless
Committed by
GitHub
Apr 27, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix InteractiveViewer.builder for custom RenderBox parents (#80166)
parent
a27815c1
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
464 additions
and
24 deletions
+464
-24
interactive_viewer.dart
packages/flutter/lib/src/widgets/interactive_viewer.dart
+324
-24
interactive_viewer_test.dart
packages/flutter/test/widgets/interactive_viewer_test.dart
+140
-0
No files found.
packages/flutter/lib/src/widgets/interactive_viewer.dart
View file @
a8e41f82
...
...
@@ -11,8 +11,17 @@ 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 signature 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}
...
...
@@ -84,7 +93,7 @@ class InteractiveViewer extends StatefulWidget {
this
.
panEnabled
=
true
,
this
.
scaleEnabled
=
true
,
this
.
transformationController
,
required
this
.
child
,
required
Widget
this
.
child
,
})
:
assert
(
alignPanAxis
!=
null
),
assert
(
child
!=
null
),
assert
(
constrained
!=
null
),
...
...
@@ -105,6 +114,51 @@ class InteractiveViewer extends StatefulWidget {
&&
boundaryMargin
.
right
.
isFinite
&&
boundaryMargin
.
bottom
.
isFinite
&&
boundaryMargin
.
left
.
isFinite
),
),
builder
=
null
,
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
InteractiveViewerWidgetBuilder
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
,
child
=
null
,
super
(
key:
key
);
/// If set to [Clip.none], the child may extend beyond the size of the InteractiveViewer,
...
...
@@ -143,10 +197,203 @@ class InteractiveViewer extends StatefulWidget {
/// exact same size and position as the [child].
final
EdgeInsets
boundaryMargin
;
///
The Widget to perform the transformations on
.
///
Builds the child of this widget
.
///
/// Cannot be null.
final
Widget
child
;
/// Passed with the [InteractiveViewer.builder] constructor. Otherwise, the
/// [child] parameter must be passed directly, and this is null.
///
/// {@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 <Vector3>[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: <int, TableColumnWidth>{
/// for (int column = 0; column < columnCount; column++)
/// column: FixedColumnWidth(cellWidth),
/// },
/// // ignore: prefer_const_literals_to_create_immutables
/// children: <TableRow>[
/// for (int row = 0; row < rowCount; row++)
/// // ignore: prefer_const_constructors
/// TableRow(
/// // ignore: prefer_const_literals_to_create_immutables
/// children: <Widget>[
/// for (int column = 0; column < columnCount; column++)
/// builder(context, row, column),
/// ],
/// ),
/// ],
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [ListView.builder], which follows a similar pattern.
final
InteractiveViewerWidgetBuilder
?
builder
;
/// The child [Widget] that is transformed by InteractiveViewer.
///
/// If the [InteractiveViewer.builder] constructor is used, then this will be
/// null, otherwise it is required.
final
Widget
?
child
;
/// Whether the normal size constraints at this point in the widget tree are
/// applied to the child.
...
...
@@ -1076,17 +1323,83 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
Widget
child
;
if
(
widget
.
child
!=
null
)
{
child
=
_InteractiveViewerBuilt
(
childKey:
_childKey
,
clipBehavior:
widget
.
clipBehavior
,
constrained:
widget
.
constrained
,
matrix:
_transformationController
!.
value
,
child:
widget
.
child
!,
);
}
else
{
// When using InteractiveViewer.builder, then constrained is false and the
// viewport is the size of the constraints.
assert
(
widget
.
builder
!=
null
);
assert
(!
widget
.
constrained
);
child
=
LayoutBuilder
(
builder:
(
BuildContext
context
,
BoxConstraints
constraints
)
{
final
Matrix4
matrix
=
_transformationController
!.
value
;
return
_InteractiveViewerBuilt
(
childKey:
_childKey
,
clipBehavior:
widget
.
clipBehavior
,
constrained:
widget
.
constrained
,
matrix:
matrix
,
child:
widget
.
builder
!(
context
,
_transformViewport
(
matrix
,
Offset
.
zero
&
constraints
.
biggest
),
),
);
},
);
}
return
Listener
(
key:
_parentKey
,
onPointerSignal:
_receivedPointerSignal
,
child:
GestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
// Necessary when panning off screen.
dragStartBehavior:
DragStartBehavior
.
start
,
onScaleEnd:
_onScaleEnd
,
onScaleStart:
_onScaleStart
,
onScaleUpdate:
_onScaleUpdate
,
child:
child
,
),
);
}
}
// This widget simply allows us to easily swap in and out the LayoutBuilder in
// InteractiveViewer's depending on if it's using a builder or a child.
class
_InteractiveViewerBuilt
extends
StatelessWidget
{
const
_InteractiveViewerBuilt
({
Key
?
key
,
required
this
.
child
,
required
this
.
childKey
,
required
this
.
clipBehavior
,
required
this
.
constrained
,
required
this
.
matrix
,
})
:
super
(
key:
key
);
final
Widget
child
;
final
GlobalKey
childKey
;
final
Clip
clipBehavior
;
final
bool
constrained
;
final
Matrix4
matrix
;
@override
Widget
build
(
BuildContext
context
)
{
Widget
child
=
Transform
(
transform:
_transformationController
!.
value
,
transform:
matrix
,
child:
KeyedSubtree
(
key:
_
childKey
,
child:
widget
.
child
,
key:
childKey
,
child:
this
.
child
,
),
);
if
(!
widget
.
constrained
)
{
if
(!
constrained
)
{
child
=
OverflowBox
(
alignment:
Alignment
.
topLeft
,
minWidth:
0.0
,
...
...
@@ -1097,27 +1410,14 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
);
}
if
(
widget
.
clipBehavior
!=
Clip
.
none
)
{
if
(
clipBehavior
!=
Clip
.
none
)
{
child
=
ClipRect
(
clipBehavior:
widget
.
clipBehavior
,
clipBehavior:
clipBehavior
,
child:
child
,
);
}
// A GestureDetector allows the detection of panning and zooming gestures on
// the child.
return
Listener
(
key:
_parentKey
,
onPointerSignal:
_receivedPointerSignal
,
child:
GestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
// Necessary when panning off screen.
dragStartBehavior:
DragStartBehavior
.
start
,
onScaleEnd:
_onScaleEnd
,
onScaleStart:
_onScaleStart
,
onScaleUpdate:
_onScaleUpdate
,
child:
child
,
),
);
return
child
;
}
}
...
...
packages/flutter/test/widgets/interactive_viewer_test.dart
View file @
a8e41f82
...
...
@@ -1161,6 +1161,124 @@ 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
<
Container
>
children
=
<
Container
>[];
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
++;
}
});
// Accessing the intrinsic size of a LayoutBuilder throws an error, so
// InteractiveViewer only uses a LayoutBuilder when it's needed by
// InteractiveViewer.builder.
testWidgets
(
'LayoutBuilder is only used for InteractiveViewer.builder'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
child:
const
SizedBox
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
expect
(
find
.
byType
(
LayoutBuilder
),
findsNothing
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
.
builder
(
builder:
(
BuildContext
context
,
Quad
viewport
)
{
return
const
SizedBox
(
width:
200.0
,
height:
200.0
);
},
),
),
),
),
);
expect
(
find
.
byType
(
LayoutBuilder
),
findsOneWidget
);
});
});
group
(
'getNearestPointOnLine'
,
()
{
...
...
@@ -1370,3 +1488,25 @@ void main() {
});
});
}
Rect
_axisAlignedBoundingBox
(
Quad
quad
)
{
double
?
xMin
;
double
?
xMax
;
double
?
yMin
;
double
?
yMax
;
for
(
final
Vector3
point
in
<
Vector3
>[
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
!);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment