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
2c2c8a75
Unverified
Commit
2c2c8a75
authored
Mar 22, 2021
by
Justin McCandless
Committed by
GitHub
Mar 22, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
InteractiveViewer.builder (#77414)
parent
e58ee0fb
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
408 additions
and
41 deletions
+408
-41
interactive_viewer.dart
packages/flutter/lib/src/widgets/interactive_viewer.dart
+301
-41
interactive_viewer_test.dart
packages/flutter/test/widgets/interactive_viewer_test.dart
+107
-0
No files found.
packages/flutter/lib/src/widgets/interactive_viewer.dart
View file @
2c2c8a75
...
...
@@ -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 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}
...
...
@@ -32,8 +41,6 @@ import 'ticker_provider.dart';
/// 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.
...
...
@@ -67,7 +74,7 @@ import 'ticker_provider.dart';
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
,
...
...
@@ -84,7 +91,7 @@ class InteractiveViewer extends StatefulWidget {
this
.
panEnabled
=
true
,
this
.
scaleEnabled
=
true
,
this
.
transformationController
,
required
this
.
child
,
required
Widget
child
,
})
:
assert
(
alignPanAxis
!=
null
),
assert
(
child
!=
null
),
assert
(
constrained
!=
null
),
...
...
@@ -103,6 +110,51 @@ class InteractiveViewer extends StatefulWidget {
&&
boundaryMargin
.
vertical
.
isInfinite
)
||
(
boundaryMargin
.
top
.
isFinite
&&
boundaryMargin
.
right
.
isFinite
&&
boundaryMargin
.
bottom
.
isFinite
&&
boundaryMargin
.
left
.
isFinite
)),
builder
=
_getBuilderForChild
(
child
),
super
(
key:
key
);
// TODO(justinmc): Example. Or put it in the builder param docs.
/// 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,
...
...
@@ -138,13 +190,206 @@ 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
;
/// The Widget to perform the transformations on.
/// Builds the child of this widget.
///
/// 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.
///
/// Cannot be null.
final
Widget
child
;
/// ```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.
/// * [InteractiveViewer.builder], which has an example of building the
/// child on demand.
final
InteractiveViewerWidgetBuilder
builder
;
/// Whether the normal size constraints at this point in the widget tree are
/// applied to the child.
...
...
@@ -422,6 +667,13 @@ 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
)
{
...
...
@@ -1062,11 +1314,22 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
@override
Widget
build
(
BuildContext
context
)
{
// 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:
_transformationController
!.
value
,
transform:
matrix
,
child:
KeyedSubtree
(
key:
_childKey
,
child:
widget
.
child
,
child:
widget
.
builder
(
context
,
_transformViewport
(
matrix
,
Offset
.
zero
&
constraints
.
biggest
))
,
),
);
...
...
@@ -1088,18 +1351,15 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
);
}
// A GestureDetector allows the detection of panning and zooming gestures on
// the child.
return
Listener
(
key:
_parentKey
,
onPointerSignal:
_receivedPointerSignal
,
child:
GestureDetector
(
return
GestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
// Necessary when panning off screen.
dragStartBehavior:
DragStartBehavior
.
start
,
onScaleEnd:
_onScaleEnd
,
onScaleStart:
_onScaleStart
,
onScaleUpdate:
_onScaleUpdate
,
child:
child
,
);
},
),
);
}
...
...
packages/flutter/test/widgets/interactive_viewer_test.dart
View file @
2c2c8a75
...
...
@@ -1026,6 +1026,89 @@ 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
++;
}
});
});
group
(
'getNearestPointOnLine'
,
()
{
...
...
@@ -1235,3 +1318,27 @@ 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
<
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