Flutter Gestures: Conflict of panable view and dragable item

197 views Asked by At

TDLR

How can I setup a scrollable view (two-finger) with a draggable item (one-finger) in Flutter in way that the item doesn't eat up the two-finger gesture.

Goal

I am trying to setup an application in Flutter that behaves as follows:

  • It provides a view that can be panned and zoomed with two finger gestures
  • Inside view resides an item (e.g. box) that can be dragged with one finger

At the moment I am using GestureDetectors for that. I am aware of the GestureArena handling that Flutter internally does.

enter image description here

Problem

When panning the view with two fingers, the item interprets this as a drag operation once at least one finger touches the item. Technically spoken "onHorizontalDragUpdate" is called although I am using two fingers on the screen.

The GestureArea debug output states that the Drag-GestureDetector of the item wins:

Accepting: HorizontalDragGestureRecognizer#b044d(debugOwner: GestureDetector-[GlobalKey#ebeb1 Grüne Box], start behavior: start)

What I'd like to have is that the Scale-GestureDetector of the view wins.

What I would expect for that is that either the HorizontalDrag-GestureDetector on the item surrenders in the gesture arena once a second finger comes into play. Or that the Scale-GestureDetector on the view declares victory in the arena once two fingers are used.

Question

How can this use case be solved in Flutter?

  • Is it possible to configure the Drag-GestureDetector to only accept one finger drags?
  • Or is it possible to give the Scale-GestureDetector priority for two finger touches?
  • Or is there any other solution?

This use case sounds so common that I probably just didn't find the answer yet, although it is probably there. Thus I really appreciate any help.

Example

The following video demonstrates the problem. The two finger pan should always pan the view and not scale the green box. The video is made in the Android emulator:

https://www.screencast.com/t/Y6nwpioFb

Here is the full source code of that sample:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

void main() {
  debugPrintGestureArenaDiagnostics = true;
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _scale = 1.0;
  var _translation = const Offset(0, 0);
  double _scaleStart = 1.0;
  double _boxScale = 1.0;
  final _color = Colors.green;
  final _boxGesture = GlobalKey(debugLabel: "Grüne Box");
  final _viewGesture = GlobalKey(debugLabel: "Blaue View");

  void _handleDragUpdate(DragUpdateDetails details) {
    setState(() {
      if (details.delta.distance > 0) {
        _boxScale = _boxScale + (details.delta.dx / 100);
      }
    });
  }

  Widget buildContent(BuildContext context) {
    Matrix4 matrix = Matrix4.identity();
    matrix.scale(_scale);
    matrix.translate(_translation.dx, _translation.dy);

    // Scalable item
    Widget child = Transform(
      transform: matrix,
      alignment: Alignment.topLeft,
      child: GestureDetector(
        key: _boxGesture,
        onHorizontalDragUpdate: _handleDragUpdate,
        child: SizedBox(
          width: 500 * _boxScale,
          height: 500,
          child: Card(
            color: _color,
          ),
        ),
      ),
    );

    child = OverflowBox(
      alignment: Alignment.topLeft,
      minWidth: 0.0,
      minHeight: 0.0,
      maxWidth: double.infinity,
      maxHeight: double.infinity,
      child: child,
    );

    child = Container(
      color: Colors.blue,
      child: ClipRect(
        clipBehavior: Clip.hardEdge,
        child: child,
      ),
    );

    // View pan/zoom handling
    child = GestureDetector(
      key: _viewGesture,
      onScaleStart: (details) {
        _scaleStart = _scale;
      },
      onScaleUpdate: (details) {
        if (details.pointerCount == 2) {
          setState(() {
            _scale = _scaleStart * details.scale;
            _translation += details.focalPointDelta;
          });
        }
      },
      onScaleEnd: (details) {},
      child: child,
    );

    child = Center(
      child: child,
    );

    return child;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: buildContent(context),
        ));
  }
}
0

There are 0 answers