Update example code and docs for InteractiveViewer.builder (#98623)
This commit is contained in:
parent
eb83181041
commit
ac1fc234ea
@ -5,7 +5,6 @@
|
|||||||
// Flutter code sample for InteractiveViewer.builder
|
// Flutter code sample for InteractiveViewer.builder
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3;
|
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3;
|
||||||
|
|
||||||
void main() => runApp(const IVBuilderExampleApp());
|
void main() => runApp(const IVBuilderExampleApp());
|
||||||
@ -32,77 +31,34 @@ class _IVBuilderExample extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _IVBuilderExampleState extends State<_IVBuilderExample> {
|
class _IVBuilderExampleState extends State<_IVBuilderExample> {
|
||||||
final TransformationController _transformationController =
|
static const double _cellWidth = 160.0;
|
||||||
TransformationController();
|
static const double _cellHeight = 80.0;
|
||||||
|
|
||||||
static const double _cellWidth = 200.0;
|
// Returns the axis aligned bounding box for the given Quad, which might not be axis aligned.
|
||||||
static const double _cellHeight = 26.0;
|
Rect axisAlignedBoundingBox(Quad quad) {
|
||||||
|
double xMin = quad.point0.x;
|
||||||
// Returns true iff the given cell is currently visible. Caches viewport
|
double xMax = quad.point0.x;
|
||||||
// calculations.
|
double yMin = quad.point0.y;
|
||||||
Quad? _cachedViewport;
|
double yMax = quad.point0.y;
|
||||||
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>[
|
for (final Vector3 point in <Vector3>[
|
||||||
quad.point0,
|
|
||||||
quad.point1,
|
quad.point1,
|
||||||
quad.point2,
|
quad.point2,
|
||||||
quad.point3
|
quad.point3
|
||||||
]) {
|
]) {
|
||||||
if (xMin == null || point.x < xMin) {
|
if (point.x < xMin) {
|
||||||
xMin = point.x;
|
xMin = point.x;
|
||||||
}
|
} else if (point.x > xMax) {
|
||||||
if (xMax == null || point.x > xMax) {
|
|
||||||
xMax = point.x;
|
xMax = point.x;
|
||||||
}
|
}
|
||||||
if (yMin == null || point.y < yMin) {
|
|
||||||
|
if (point.y < yMin) {
|
||||||
yMin = point.y;
|
yMin = point.y;
|
||||||
}
|
} else if (point.y > yMax) {
|
||||||
if (yMax == null || point.y > yMax) {
|
|
||||||
yMax = point.y;
|
yMax = point.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Rect.fromLTRB(xMin!, yMin!, xMax!, yMax!);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onChangeTransformation() {
|
return Rect.fromLTRB(xMin, yMin, xMax, yMax);
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_transformationController.addListener(_onChangeTransformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_transformationController.removeListener(_onChangeTransformation);
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -111,32 +67,25 @@ class _IVBuilderExampleState extends State<_IVBuilderExample> {
|
|||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
return InteractiveViewer.builder(
|
return InteractiveViewer.builder(
|
||||||
alignPanAxis: true,
|
boundaryMargin: const EdgeInsets.all(double.infinity),
|
||||||
scaleEnabled: false,
|
|
||||||
transformationController: _transformationController,
|
|
||||||
builder: (BuildContext context, Quad viewport) {
|
builder: (BuildContext context, Quad viewport) {
|
||||||
// A simple extension of Table that builds cells.
|
|
||||||
return _TableBuilder(
|
return _TableBuilder(
|
||||||
rowCount: 60,
|
cellWidth: _cellWidth,
|
||||||
columnCount: 6,
|
cellHeight: _cellHeight,
|
||||||
cellWidth: _cellWidth,
|
viewport: axisAlignedBoundingBox(viewport),
|
||||||
builder: (BuildContext context, int row, int column) {
|
builder: (BuildContext context, int row, int column) {
|
||||||
if (!_isCellVisible(row, column, viewport)) {
|
return Container(
|
||||||
debugPrint('removing cell ($row, $column)');
|
height: _cellHeight,
|
||||||
return Container(height: _cellHeight);
|
width: _cellWidth,
|
||||||
}
|
color: row % 2 + column % 2 == 1
|
||||||
debugPrint('building cell ($row, $column)');
|
? Colors.white
|
||||||
return Container(
|
: Colors.grey.withOpacity(0.1),
|
||||||
height: _cellHeight,
|
child: Align(
|
||||||
color: row % 2 + column % 2 == 1
|
child: Text('$row x $column'),
|
||||||
? Colors.white
|
),
|
||||||
: Colors.grey.withOpacity(0.1),
|
);
|
||||||
child: Align(
|
},
|
||||||
alignment: Alignment.centerLeft,
|
);
|
||||||
child: Text('$row x $column'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -150,38 +99,44 @@ typedef _CellBuilder = Widget Function(
|
|||||||
|
|
||||||
class _TableBuilder extends StatelessWidget {
|
class _TableBuilder extends StatelessWidget {
|
||||||
const _TableBuilder({
|
const _TableBuilder({
|
||||||
required this.rowCount,
|
|
||||||
required this.columnCount,
|
|
||||||
required this.cellWidth,
|
required this.cellWidth,
|
||||||
|
required this.cellHeight,
|
||||||
|
required this.viewport,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
}) : assert(rowCount > 0),
|
});
|
||||||
assert(columnCount > 0);
|
|
||||||
|
|
||||||
final int rowCount;
|
|
||||||
final int columnCount;
|
|
||||||
final double cellWidth;
|
final double cellWidth;
|
||||||
|
final double cellHeight;
|
||||||
|
final Rect viewport;
|
||||||
final _CellBuilder builder;
|
final _CellBuilder builder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Table(
|
final int firstRow = (viewport.top / cellHeight).floor();
|
||||||
// ignore: prefer_const_literals_to_create_immutables
|
final int lastRow = (viewport.bottom / cellHeight).ceil();
|
||||||
columnWidths: <int, TableColumnWidth>{
|
final int firstCol = (viewport.left / cellWidth).floor();
|
||||||
for (int column = 0; column < columnCount; column++)
|
final int lastCol = (viewport.right / cellWidth).ceil();
|
||||||
column: FixedColumnWidth(cellWidth),
|
|
||||||
},
|
// This will create and render exactly (lastRow - firstRow) * (lastCol - firstCol) cells
|
||||||
// ignore: prefer_const_literals_to_create_immutables
|
|
||||||
children: <TableRow>[
|
return SizedBox(
|
||||||
for (int row = 0; row < rowCount; row++)
|
// Stack needs constraints, even though we then Clip.none outside of them.
|
||||||
// ignore: prefer_const_constructors
|
// InteractiveViewer.builder always sets constrained to false, giving infinite constraints to the child.
|
||||||
TableRow(
|
// See: https://master-api.flutter.dev/flutter/widgets/InteractiveViewer/constrained.html
|
||||||
// ignore: prefer_const_literals_to_create_immutables
|
width: 1,
|
||||||
children: <Widget>[
|
height: 1,
|
||||||
for (int column = 0; column < columnCount; column++)
|
child: Stack(
|
||||||
builder(context, row, column),
|
clipBehavior: Clip.none,
|
||||||
],
|
children: <Widget>[
|
||||||
),
|
for (int row = firstRow; row < lastRow; row++)
|
||||||
],
|
for (int col = firstCol; col < lastCol; col++)
|
||||||
|
Positioned(
|
||||||
|
left: col * cellWidth,
|
||||||
|
top: row * cellHeight,
|
||||||
|
child: builder(context, row, col),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2014 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 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_api_samples/widgets/interactive_viewer/interactive_viewer.builder.0.dart'
|
||||||
|
as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('has correct items on screen', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.IVBuilderExampleApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder positionedFinder = find.byType(Positioned);
|
||||||
|
final Finder zeroFinder = find.text('0 x 0');
|
||||||
|
final Finder nineFinder = find.text('0 x 9');
|
||||||
|
|
||||||
|
expect(positionedFinder, findsNWidgets(35));
|
||||||
|
expect(zeroFinder, findsOneWidget);
|
||||||
|
expect(nineFinder, findsNothing);
|
||||||
|
|
||||||
|
const Offset firstLocation = Offset(750.0, 100.0);
|
||||||
|
final TestGesture gesture = await tester.startGesture(firstLocation);
|
||||||
|
|
||||||
|
const Offset secondLocation = Offset(50.0, 100.0);
|
||||||
|
await gesture.moveTo(secondLocation);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(positionedFinder, findsNWidgets(42));
|
||||||
|
expect(nineFinder, findsOneWidget);
|
||||||
|
expect(zeroFinder, findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -37,15 +37,15 @@ typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Q
|
|||||||
/// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the
|
/// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the
|
||||||
/// size of the area that should be interactive.
|
/// size of the area that should be interactive.
|
||||||
///
|
///
|
||||||
/// See [flutter-go](https://github.com/justinmc/flutter-go) for an example of
|
|
||||||
/// robust positioning of an InteractiveViewer child that works for all screen
|
|
||||||
/// sizes and child sizes.
|
|
||||||
///
|
|
||||||
/// The [child] must not be null.
|
/// The [child] must not be null.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
/// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/master/lib/demos/reference/transformations_demo.dart),
|
/// * 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.
|
/// which includes the use of InteractiveViewer.
|
||||||
|
/// * The [flutter-go demo](https://github.com/justinmc/flutter-go), which includes robust positioning of an InteractiveViewer child
|
||||||
|
/// that works for all screen sizes and child sizes.
|
||||||
|
/// * The [Lazy Flutter Performance Session](https://www.youtube.com/watch?v=qax_nOpgz7E), which includes the use of an InteractiveViewer to
|
||||||
|
/// performantly view subsets of a large set of widgets using the builder constructor.
|
||||||
///
|
///
|
||||||
/// {@tool dartpad}
|
/// {@tool dartpad}
|
||||||
/// This example shows a simple Container that can be panned and zoomed.
|
/// This example shows a simple Container that can be panned and zoomed.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user