Skip to content

Conversation

@justinmc
Copy link
Contributor

@justinmc justinmc commented Mar 5, 2021

This PR adds a .builder constructor to InteractiveViewer, similar to ListView.builder.

InteractiveViewer(
  builder: (BuildContext context, Rect viewport) {
    // Build the child based on the viewport.
    // E.g. maybe only render the visible parts of the child?
  },
),

Closes #58603
Potential fix for #49109

Why not just use a Builder as the child of InteractiveViewer?

The main value added by the builder constructor approach is the pre-calculated viewport Rect parameter. It's possible but fairly difficult to calculate this without access to the private state and methods of the InteractiveViewer instance.

Example

This example shows building a Table whose cell content only renders for visible cells. In the screenshot, off-screen cells are empty until they are panned onto the screen.

Screen Shot 2021-03-05 at 1 33 37 PM

Code
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class IVBuilderPage extends StatefulWidget {
  const IVBuilderPage({ Key key }) : super(key: key); 

  @override _IVBuilderPageState createState() => _IVBuilderPageState();
}

class _IVBuilderPageState extends State<IVBuilderPage> {
  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.
  Rect _cachedViewport;
  int _firstVisibleColumn;
  int _firstVisibleRow;
  int _lastVisibleColumn;
  int _lastVisibleRow;
  bool _isCellVisible(int row, int column, Rect viewport) {
    if (viewport != _cachedViewport) {
      _cachedViewport = viewport;
      _firstVisibleRow = (viewport.top / _cellHeight).floor();
      _firstVisibleColumn = (viewport.left / _cellWidth).floor();
      _lastVisibleRow = (viewport.bottom / _cellHeight).floor();
      _lastVisibleColumn = (viewport.right / _cellWidth).floor();
    }
    return row >= _firstVisibleRow && row <= _lastVisibleRow
        && column >= _firstVisibleColumn && column <= _lastVisibleColumn;
  }

  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 Scaffold(
      appBar: AppBar(
        title: const Text('IV Builder'),
        actions: <Widget>[
        ],
      ),
      body: Center(
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            return InteractiveViewer.builder(
              alignPanAxis: true,
              scaleEnabled: false,
              constrained: false,
              transformationController: _transformationController,
              builder: (BuildContext context, Rect viewport) {
                // This is a quick widget I wrote to generate a Table
                return TableBuilder(
                  rowCount: 60,
                  columnCount: 6,
                  cellWidth: _cellWidth,
                  builder: (BuildContext context, int row, int column) {
                    if (!_isCellVisible(row, column, viewport)) {
                      return Container(height: _cellHeight);
                    }
                    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'),
                      ),
                    );
                  }
                );
              },
            );
          },
        ),
      ),
    );
  }
}

@justinmc justinmc self-assigned this Mar 5, 2021
@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label Mar 5, 2021
@google-cla google-cla bot added the cla: yes label Mar 5, 2021
@justinmc justinmc changed the title POC for a builder that gets the transformed viewport as an argument InteractiveViewer.builder Mar 5, 2021
return Offset.zero & parentRenderBox.size;
}
}
return Offset.zero & _maxConstraints;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was previously using _parentKey to get the size of the viewport, but now I need it before the first render. It seems like using LayoutBuilder's max constraints should work just as well... Is there any case where that won't work?

By "viewport" I mean the size of the InteractiveViewer widget.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable. Instead of storing it in a field, can you just turn this into a method and pass it in from the build method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I've realized that the constraints-based calculation will be wrong when InteractiveViewer is sized by its child, i.e. when the child is smaller than the constraints. This is the typical problem of knowing the size one frame too late.

I think I've worked around it by requiring InteractiveViewer.builder to be used with constrained: false. That will make this constraints-based viewport calculation correct, and I don't think there's a use case for needing constrained to be true with builder.

If you've got another idea let me know, though.

child: child,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
_maxConstraints = Size(constraints.maxWidth, constraints.maxHeight);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmc justinmc force-pushed the iv-builder branch 4 times, most recently from 2cd0cf5 to c382c11 Compare March 10, 2021 23:47
@justinmc justinmc marked this pull request as ready for review March 16, 2021 19:57
@justinmc
Copy link
Contributor Author

@goderbauer Ready for re-review. I'm using this in my talk and it seems to work well.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

/// Create an InteractiveViewer.
///
/// The [child] parameter must not be null.
/// The child parameter must not be null.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
/// The child parameter must not be null.
/// The `child` parameter must not be null.

///
/// * [InteractiveViewer.builder], whose builder is of this type.
/// * [WidgetBuilder], which is similar, but takes no Rect.
typedef TransformedWidgetBuilder = Widget Function(BuildContext context, Rect viewport);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name's a little strange. What about this is "transformed"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of this is transformed using a Transform widget by the InteractiveViewer. Maybe it could be InteractiveViewerWidgetBuilder if that's better?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Giving this a more specific name sounds good.


// Convert an axis aligned Quad to a Rect.
//
// All Rects must axis aligned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which "rects" is this referring to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of them. I'll change it to this:

"All instances of Rect are axis aligned by definition."

Meanwhile, Quads are not necessarily axis aligned.


/// Returns true iff the given Quad is axis aligned.
@visibleForTesting
static bool isAxisAligned(Quad quad) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is only called from asserts, should we name this debugIsAxisAligned and be a no-op in release builds?

Copy link
Contributor Author

@justinmc justinmc Mar 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. The asserts are automatically removed in release mode, right? I'm assuming you don't want me to check for kReleaseMode and return true and just want me to change the name.

@justinmc justinmc force-pushed the iv-builder branch 2 times, most recently from 0f8268d to 036171f Compare March 20, 2021 19:50
@justinmc
Copy link
Contributor Author

@goderbauer FYI I changed the builder to take a Quad instead of a Rect so that it can support rotated viewports in the future.

@fluttergithubbot fluttergithubbot merged commit 2c2c8a7 into flutter:master Mar 22, 2021
@justinmc justinmc deleted the iv-builder branch March 22, 2021 23:23
renyou added a commit that referenced this pull request Mar 23, 2021
renyou added a commit that referenced this pull request Mar 23, 2021
justinmc added a commit that referenced this pull request Mar 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InteractiveViewer builder constructor

3 participants