Introducing InteractiveViewer Widget

Introducing InteractiveViewer Widget in Flutter 1.20 Release

Flutter, Google’s cross-platform UI toolkit, has reached model 1.20 secure. In the previous stable release, Google launched substantial efficiency enhancements, improved help for Metal on iOS, and new Material widgets. Today’s Flutter 1.20 secure launch consists of extra efficiency enhancements, a number of UI enhancements, a replacement to the Visual Studio Code extension, autofill for cellular textual content fields, and extra.

To enable you to build Flutter apps that are ever more beautiful, 1.20  release has several UI enhancements, including the long-awaited features like listed below:

  • Support for autofill
  • Jank Improvements
  • A new way to layer your widgets to support pan and zoom
  • New mouse cursor support
  • Updates to old favorite Material widgets such as the TimePicker and DateRangePicker.
  • A whole new responsive license page for the About box in your desktop and mobile form-factor Flutter apps.
  • Null safety has been added to dart.dev

In this article, we will discuss InteractiveViewer Widget in detail.

  • What is InteractiveViewer Widget?

This release of Flutter 1.20  introduces a new widget, the InteractiveViewer. The InteractiveViewer Widget is designed for building common kinds of interactivity into the app, like pan, zoom, and drag ’n’ drop, even in the face of resizing.

Kindly check out the API documentation to understand how to integrate InteractiveViewer Widget into your own app.

If you are more curious about how the InteractiveViewer was designed and developed, you can see a presentation by the author for Chicago Flutter on YouTube.

Default Constructor of it will look like below:

InteractiveViewer({
    Key key,
    bool alignPanAxis: false,
    EdgeInsets boundaryMargin: EdgeInsets.zero,
    bool constrained: true,
    double maxScale: 2.5,
    double minScale: 0.8,
    GestureScaleEndCallback onInteractionEnd,
    GestureScaleStartCallback onInteractionStart,
    GestureScaleUpdateCallback onInteractionUpdate,
    bool panEnabled: true,
    bool scaleEnabled: true,
    TransformationController transformationController,
    @required Widget child,
  });

In the Above constructor, all fields marked with @required must not be empty.

Properties:

  • bool alignPanAxis: If true, panning is only allowed in the direction of the horizontal axis or the vertical axis. In other words, when this is true, diagonal panning is not allowed. A single gesture begun along one axis cannot also cause panning along the other axis without stopping and beginning a new gesture. This is a common pattern in tables where data is displayed in columns and rows.
  • EdgeInsets boundaryMargin: A margin for the visible boundaries of the child. Any transformation that results in the viewport being able to view outside of the boundaries will be stopped at the boundary. The boundaries do not rotate with the rest of the scene, so they are always aligned with the viewport.
  • bool constrained: Whether the normal size constraints at this point in the widget tree are applied to the child. If set to false, then the child will be given infinite constraints. This is often useful when a child should be bigger than InteractiveViewer Widget. Defaults to true.
  • double maxScale: The maximum allowed scale. The scale will be clamped between this and minScale inclusively. Defaults to 2.5. It can not be null and must be greater than zero and greater than minScale.
  • double minScale: The minimum allowed scale. The scale will be clamped between this and maxScale inclusively. The default value will be 0.8.Cannot be null, and must be a finite number greater than zero and less than maxScale.
  • GestureScaleStartCallback onInteractionStart: onInteractionStart will be called when the user begins a pan or scale gesture on the widget. It will be called even if the interaction is disabled with panEnabled or scaleEnabled. A GestureDetector wrapping the InteractiveViewer will not respond to GestureDetector.onScaleStart,GestureDetector.onScaleUpdate, and GestureDetector.onScaleEnd. Use onInteractionStart, onInteractionUpdate, and onInteractionEnd to respond to those gestures.
  • GestureScaleEndCallback onInteractionEnd: onInteractionEnd will be called when the user ends a pan or scale gesture on the widget. It will be called even if the interaction is disabled with panEnabled or scaleEnabled. A GestureDetector wrapping the InteractiveViewer will not respond to GestureDetector.onScaleStart, GestureDetector.onScaleUpdate, and GestureDetector.onScaleEnd.Use onInteractionStart, onInteractionUpdate, and onInteractionEnd to respond to those gestures.
  • GestureScaleUpdateCallback onInteractionUpdate: onInteractionUpdate will be called when the user updates a pan or scale gesture on the widget.It will be called even if the interaction is disabled with panEnabled or scaleEnabled. A GestureDetector wrapping the InteractiveViewer will not respond to GestureDetector.onScaleStart, GestureDetector.onScaleUpdate, and GestureDetector.onScaleEnd. Use onInteractionStart, onInteractionUpdate, and onInteractionEnd to respond to those gestures.
  • bool panEnabled: If false, the user will be prevented from panning. Defaults to true.

Code Snippet will look like below:

import 'dart:ui' as ui;

import 'package:flutter/material.dart';

const _strokeWidth = 8.0;

class DragTargetDetailsExample extends StatefulWidget {
  @override
  _DragTargetDetailsExampleState createState() =>
      _DragTargetDetailsExampleState();
}

class _DragTargetDetailsExampleState extends State<DragTargetDetailsExample> {
  static const _feedbackSize = Size(100.0, 100.0);
  static const _padding = 16.0;

  static final _decoration = BoxDecoration(
    border: Border.all(
      color: Colors.blue,
      width: _strokeWidth,
    ),
    borderRadius: BorderRadius.circular(12),
  );

  Offset _lastDropOffset;
  int _lastDropIndex;

  Widget _buildSourceRowChild(int index) => Expanded(
        child: Padding(
          padding: EdgeInsets.all(_padding),
          child: Draggable<int>(
            data: index,
            dragAnchor: DragAnchor.pointer,
            feedback: Transform.translate(
                offset: Offset(
                    -_feedbackSize.width / 2.0, -_feedbackSize.height / 2.0),
                child: Container(
                    decoration: _decoration,
                    width: _feedbackSize.width,
                    height: _feedbackSize.height)),
            child: Container(
              decoration: _decoration,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('drag me'),
                  Text(
                    '$index',
                    style: TextStyle(
                      fontSize: 32.0,
                    ),
                  )
                ],
              ),
            ),
          ),
        ),
      );

  void _handleAcceptWithDetails(
      BuildContext dragTargetContext, DragTargetDetails details) {
    RenderBox renderBox = dragTargetContext.findRenderObject();
    final localOffset = renderBox.globalToLocal(details.offset);
    setState(() {
      _lastDropOffset = localOffset;
      _lastDropIndex = details.data;
    });
  }

  Widget _buildDragTargetChild() => Padding(
        padding: EdgeInsets.all(_padding),
        child: Container(
          decoration: _decoration,
          child: Builder(
            builder: (targetContext) => DragTarget<int>(
              builder: (_, candidateData, __) => Container(
                color: candidateData.isNotEmpty
                    ? Color(0x2200FF00)
                    : Color(0x00000000),
                child: CustomPaint(
                  painter: _Painter(_lastDropOffset, _lastDropIndex),
                ),
              ),
              onAcceptWithDetails: (details) =>
                  _handleAcceptWithDetails(targetContext, details),
            ),
          ),
        ),
      );

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            flex: 1,
            child: Row(
              children: List<Widget>.generate(
                3,
                _buildSourceRowChild,
              ),
            ),
          ),
          Expanded(
            flex: 2,
            child: _buildDragTargetChild(),
          )
        ],
      ),
    );
  }
}

class _Painter extends CustomPainter {
  static final _diameter = 24.0;

  static final _linePaint = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = _strokeWidth
    ..color = Colors.blue;

  static final _whiteFillPaint = Paint()
    ..style = PaintingStyle.fill
    ..color = Colors.white;

  static final _blueFillPaint = Paint()
    ..style = PaintingStyle.fill
    ..color = Colors.blue;

  final Offset _offset;
  final int _index;

  _Painter(this._offset, this._index);

  @override
  void paint(Canvas canvas, Size size) {
    if (_offset == null || _index == null) return;
    canvas.drawLine(
        Offset(_offset.dx, 0.0), Offset(_offset.dx, size.height), _linePaint);
    canvas.drawLine(
        Offset(0.0, _offset.dy), Offset(size.width, _offset.dy), _linePaint);

    canvas.drawCircle(_offset, _diameter + _strokeWidth, _blueFillPaint);
    canvas.drawCircle(_offset, _diameter, _whiteFillPaint);

    final paragraphBuilder = ui.ParagraphBuilder(
      ui.ParagraphStyle(textAlign: TextAlign.center),
    )
      ..pushStyle(ui.TextStyle(
          fontStyle: FontStyle.normal, color: Colors.blue, fontSize: _diameter))
      ..addText('$_index');
    final paragraph = paragraphBuilder.build();
    paragraph.layout(
      ui.ParagraphConstraints(width: _diameter),
    );
    canvas.drawParagraph(
      paragraph,
      _offset - Offset(_diameter / 2.0, _diameter / 2.0),
    );
  }

  @override
  bool shouldRepaint(_Painter oldPainter) => false;
}

We will get output like below:

Interactive Widget - Flutter Release 1.20

Interactive Widget – Flutter Release 1.20

Thanks for reading !!!
Do let us know your valuable feedback to serve you better.

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *


ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields

✓ Valid number ✕ Invalid number
our share of the limelight

as seen on