Saltar al contenido principal

Los gestos del trackpad pueden activar GestureRecognizer

Los gestos de trackpad en la mayoría de las plataformas ahora envían secuencias de PointerPanZoom y pueden activar callbacks de pan, drag y scale de GestureRecognizer.

Resumen

#

Los gestos de trackpad en la mayoría de las plataformas ahora envían secuencias de PointerPanZoom y pueden activar callbacks de pan, drag y scale de GestureRecognizer.

Contexto

#

El desplazamiento (scrolling) en Flutter Desktop antes de la versión 3.3.0 utilizaba mensajes PointerScrollEvent para representar deltas de desplazamiento discretos. Este sistema funcionaba bien para las ruedas de desplazamiento del ratón, pero no era adecuado para el desplazamiento con trackpad. Se espera que el desplazamiento con trackpad genere inercia (momentum), la cual depende no solo de los deltas de desplazamiento, sino también del momento en que se levantan los dedos del trackpad. Además, el pellizcar para hacer zoom (pinch-to-zoom) en el trackpad no se podía representar.

Se han introducido tres nuevos PointerEvents: PointerPanZoomStartEvent, PointerPanZoomUpdateEvent y PointerPanZoomEndEvent. Los GestureRecognizers correspondientes se han actualizado para registrar interés en las secuencias de gestos de trackpad, y emitirán callbacks onDrag, onPan, y/o onScale en respuesta a los movimientos de dos o más dedos en el trackpad.

Esto significa tanto que el código diseñado únicamente para interacciones táctiles podría activarse con la interacción del trackpad, como que el código diseñado para manejar todo el desplazamiento de escritorio ahora podría activarse únicamente con el desplazamiento del ratón, y no con el del trackpad.

Descripción del cambio

#

El motor (engine) de Flutter se ha actualizado en todas las plataformas posibles para reconocer gestos de trackpad y enviarlos al framework como eventos PointerPanZoom en lugar de eventos PointerScrollSignal. Los eventos PointerScrollSignal se seguirán utilizando para representar el desplazamiento en la rueda de un ratón.

Dependiendo de la plataforma y del modelo de trackpad específico, es posible que no se use el nuevo sistema si las APIs de la plataforma no proporcionan suficientes datos al motor de Flutter. Esto incluye a Windows, donde el soporte de gestos de trackpad depende del controlador (driver) del trackpad, y la plataforma Web, donde las APIs del navegador no proporcionan suficientes datos, por lo que el desplazamiento con trackpad debe seguir utilizando el antiguo sistema PointerScrollSignal.

Los desarrolladores deben estar preparados para recibir ambos tipos de eventos y asegurarse de que sus aplicaciones o paquetes los manejen de la manera adecuada.

Listener ahora tiene tres nuevos callbacks: onPointerPanZoomStart, onPointerPanZoomUpdate y onPointerPanZoomEnd, que se pueden utilizar para observar eventos de desplazamiento y zoom del trackpad.

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('mouse scrolled ${event.scrollDelta}');
        }
      },
      onPointerPanZoomStart: (PointerPanZoomStartEvent event) {
        debugPrint('trackpad scroll started');
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scrolled ${event.panDelta}');
      },
      onPointerPanZoomEnd: (PointerPanZoomEndEvent event) {
        debugPrint('trackpad scroll ended');
      },
      child: Container()
    );
  }
}

PointerPanZoomUpdateEvent contiene un campo pan para representar el desplazamiento acumulado del gesto actual, un campo panDelta para representar la diferencia en el desplazamiento desde el último evento, un evento scale para representar el zoom acumulado del gesto actual y un evento rotation para representar la rotación acumulada (en radianes) del gesto actual.

Los GestureRecognizers ahora tienen métodos para todos los eventos de trackpad de un gesto continuo de trackpad. Llamar al método addPointerPanZoom en un GestureRecognizer con un PointerPanZoomStartEvent hará que el reconocedor registre su interés en esa interacción de trackpad y resuelva conflictos entre múltiples GestureRecognizers que potencialmente podrían responder al gesto.

El siguiente ejemplo muestra el uso adecuado de Listener y GestureRecognizer para responder a las interacciones del trackpad.

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

Cuando se utiliza GestureDetector, esto se hace automáticamente, por lo que el código como el del siguiente ejemplo emitirá sus callbacks de actualización de gestos en respuesta tanto al desplazamiento táctil como al del trackpad.

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

Guía de migración

#

Los pasos de migración dependen de si deseas que cada interacción de gestos en tu aplicación se pueda utilizar mediante un trackpad, o si debe restringirse únicamente al uso táctil y del ratón.

Para interacciones de gestos adecuadas para el uso de trackpad

#

Usar GestureDetector

#

No se necesita ningún cambio, GestureDetector procesa automáticamente los eventos de gestos del trackpad y activa los callbacks si se reconocen.

Usar GestureRecognizer y Listener

#

Asegúrate de que onPointerPanZoomStart se pase a cada reconocedor desde el Listener. Se debe llamar al método addPointerPanZoom de `GestureRecognizer` para que muestre interés y comience a rastrear cada gesto del trackpad.

Código antes de la migración:

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      child: Container()
    );
  }
}

Código después de la migración:

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

Usar Listener puro

#

El siguiente código que utiliza PointerScrollSignal ya no se llamará con todo el desplazamiento de escritorio. Los eventos PointerPanZoomUpdate deben ser capturados para recibir los datos de los gestos del trackpad.

Código antes de la migración:

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      }
      child: Container()
    );
  }
}

Código después de la migración:

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scroll event');
      }
      child: Container()
    );
  }
}

Ten en cuenta: El uso de Listener puro de esta manera podría causar conflictos con otras interacciones de gestos, ya que no participa en el área de desambiguación de gestos.

Para interacciones de gestos no adecuadas para el uso de trackpad

#

Usar GestureDetector

#

Si estás utilizando Flutter 3.3.0, se podría usar RawGestureDetector en lugar de GestureDetector para garantizar que cada GestureRecognizer creado por el GestureDetector tenga supportedDevices configurado para excluir PointerDeviceKind.trackpad. A partir de la versión 3.4.0, hay un parámetro supportedDevices directamente en GestureDetector.

Código antes de la migración:

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

Código después de la migración (Flutter 3.3.0):

dart
// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

Código después de la migración (Flutter 3.4.0):

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      supportedDevices: {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,
        PointerDeviceKind.invertedStylus,
        // Do not include PointerDeviceKind.trackpad
      },
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

Usar RawGestureRecognizer

#

Asegúrate explícitamente de que supportedDevices no incluya PointerDeviceKind.trackpad.

Código antes de la migración:

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

Código después de la migración:

dart
// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

Usar GestureRecognizer y Listener

#

Después de actualizar a Flutter 3.3.0, no habrá un cambio en el comportamiento, ya que se debe llamar a addPointerPanZoom en cada GestureRecognizer para permitirle rastrear gestos. El siguiente código no recibirá callbacks de gestos de deslizamiento (pan) cuando se desplace el trackpad:

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      // recognizer.addPointerPanZoom is not called
      child: Container()
    );
  }
}

Timeline

#

Llegó en la versión: 3.3.0-0.0.pre
En la versión estable: 3.3.0

Referencias

#

Documentación de la API:

Documento de diseño:

Issues relevantes:

PRs relevantes: