Irbisa · Шпаргалка

Жесты и ввод указателя

Арена жестов, hit-test, drag и scale, свои распознаватели, клавиатура, hover, drag-and-drop

Middle-разработчик · 20 вопросов

  1. 01

    Чем отличаются GestureDetector, InkWell и Listener, и когда что использовать?

    Listener отдаёт сырые события указателя и ни с кем за них не конкурирует; GestureDetector превращает события указателя в жесты (тап, драг, масштабирование) через распознаватели, которые соревнуются в арене жестов; InkWell обрабатывает тапы и рисует Material-рябь, подсветку наведения и фокуса.

  2. 02

    Чем отличаются IgnorePointer и AbsorbPointer?

    Оба не дают поддереву реагировать на касания.

    // A decorative layer on top: taps fall through to the list below.Stack(  children: [    const ProductList(),    IgnorePointer(child: ConfettiLayer(controller: _confetti)),  ],…
  3. 03

    Как сделать свайп для удаления через Dismissible, и почему его key должен браться из элемента, а не из индекса?

    Оберните каждую строку в Dismissible, удалите элемент из данных в onDismissed и задайте ключ по идентичности элемента, например ValueKey(item.id).

    ListView.builder(  itemCount: tasks.length,  itemBuilder: (context, i) {    final task = tasks[i];    return Dismissible(      key: ValueKey(task.id), // identity, not position…
  4. 04

    Как арена жестов во Flutter решает, какой жест победил, и почему onTap срабатывает только когда палец отрывается?

    Каждый распознаватель под пальцем входит в арену этого указателя; любой может принять или отклонить жест, побеждает последний оставшийся.

  5. 05

    Что делают HitTestBehavior.deferToChild, opaque и translucent, и почему теряются тапы по пустому месту внутри GestureDetector?

    HitTestBehavior решает, считается ли детектор попавшим под касание там, где его потомок ничего не рисует, и проверяются ли виджеты позади него.

    // Dead zone: the gap between the icon and the text paints nothing.GestureDetector(  onTap: _open, // behavior: deferToChild by default  child: const Row(    mainAxisAlignment: MainAxisAlignment.spaceBetween,    children: [Icon(Icons.folder), Text('Reports')],…
  6. 06

    Что выведет этот код, когда пользователь один раз тапнет по звезде, и что выведет, когда тапнет по отступу вокруг неё?

    GestureDetector(  onTap: () => print('card'),  onDoubleTap: () => print('card: double'),  child: Padding(    padding: const EdgeInsets.all(24),    child: GestureDetector(      onTap: () => print('star'),      child: const Icon(Icons.star),    ),  ),)

    Тап по звезде печатает star, но примерно через 300 мс после отпускания пальца; тап по отступу не печатает ничего.

  7. 07

    Поверх карточки лежит оверлей с HitTestBehavior.translucent, чтобы на тап реагировали оба. Что выведет код при тапе по карточке и почему?

    Stack(  children: [    GestureDetector(      onTap: () => print('card'),      child: const ColoredBox(color: Colors.white, child: SizedBox.expand()),    ),    Positioned.fill(      child: GestureDetector(        behavior: HitTestBehavior.translucent,        onTap: () => print('overlay'),      ),    ),  ],)

    Только overlay.

  8. 08

    Найдите баг: leak_tracker сообщает о неосвобождённом TapGestureRecognizer в этой ссылке на условия использования, и их число растёт с каждой перестройкой.

    class TermsNotice extends StatelessWidget {  const TermsNotice({super.key, required this.onOpenTerms});  final VoidCallback onOpenTerms;   @override  Widget build(BuildContext context) => Text.rich(        TextSpan(text: 'By continuing you accept the ', children: [          TextSpan(            text: 'Terms of Service',            style: const TextStyle(decoration: TextDecoration.underline),            recognizer: TapGestureRecognizer()..onTap = onOpenTerms,          ),        ]),      );}

    Каждый build создаёт новый TapGestureRecognizer, и никто его не уничтожает, потому что TextSpan не владеет своим распознавателем.

  9. 09

    Вы добавили onDoubleTap строке списка, у которой уже был onTap, и весь экран стал казаться тормозным: чего стоила эта одна строка?

    Каждый одиночный тап по этой строке теперь срабатывает примерно на 300 мс позже: арена больше не может разрешиться на отпускании пальца, потому что распознаватель двойного тапа удерживает её kDoubleTapTimeout в ожидании второго тапа.

  10. 10

    Строки со свайпом в архив лежат в вертикальном ListView, и диагональный свайп то прокручивает список, то двигает строку: что это решает?

    Оба драга находятся в одной арене, и каждый принимает жест, как только палец пройдёт kTouchSlop (по умолчанию 18 логических px) вдоль своей оси, поэтому на диагонали побеждает ось, порог по которой пересечён первым: крутой свайп прокручивает список, пологий двигает строку.

    // Pan needs 36 px in any direction before it claims, so the row starts late and you also get dy.GestureDetector(  onPanUpdate: (d) => setState(() => _dx += d.delta.dx),  child: const MessageRow(),);…
  11. 11

    Этот код pinch-to-zoom падает в debug, а после исправления масштаб через пару кадров улетает. Что с ним не так?

    GestureDetector(  onPanUpdate: (d) => setState(() => _offset += d.delta),  onScaleUpdate: (d) => setState(() => _scale *= d.scale),  child: Transform.translate(    offset: _offset,    child: Transform.scale(scale: _scale, child: const MapSurface()),  ),)

    Две ошибки. GestureDetector срабатывает assert, когда заданы и pan-, и scale-колбэки, потому что scale является надмножеством pan; а ScaleUpdateDetails.scale отсчитывается от начала жеста, а не от предыдущего кадра, поэтому умножение на него при каждом обновлении накапливается.

  12. 12

    Как сделать drag-and-drop через Draggable и DragTarget, и почему перетаскиваемая копия часто рисуется без стилей?

    Draggable<T> несёт типизированный data и показывает feedback под пальцем; DragTarget<T> принимает его в onAcceptWithDetails.

    LongPressDraggable<Task>( // waits, so the ListView keeps its scroll  data: task,  feedback: Material( // the Overlay has no Material above it    elevation: 8,    borderRadius: BorderRadius.circular(12),    child: SizedBox(width: 260, child: TaskCard(task: task)),…
  13. 13

    Найдите баг: стикер в начале перетаскивания прыгает вниз и вбок, а потом едет за пальцем со смещением.

    class _StickerBoardState extends State<StickerBoard> {  Offset _pos = const Offset(40, 40);  @override  Widget build(BuildContext context) => Stack(        children: [          Positioned(            left: _pos.dx,            top: _pos.dy,            child: GestureDetector(              onPanUpdate: (d) => setState(() => _pos = d.globalPosition),              child: const StickerCard(),            ),          ),        ],      );}

    d.globalPosition даёт положение пальца на экране, а Positioned ждёт левый верхний угол карточки в координатах самого Stack, поэтому карточка прыгает на смещение Stack (app bar, отступы) плюс точку, за которую её взяли.

  14. 14

    Свайп для удаления срабатывает на резкий взмах, но не когда пользователь протащил далеко, замер и отпустил: что сообщает DragEndDetails.velocity?

    Velocity.zero.

    // Inside the State of a swipe row; _c is an AnimationController (0..1 = row travel).static const double _width = 320;static const _spring = SpringDescription(mass: 1, stiffness: 500, damping: 30); void _onUpdate(DragUpdateDetails d) => _c.value -= d.primaryDelta! / _width;…
  15. 15

    Как добавить зум по двойному тапу в InteractiveViewer, и почему изображение прыгает, если пользователь делает pinch во время анимации зума?

    В InteractiveViewer нет зума по двойному тапу, поэтому его TransformationController анимируют вручную; прыжок возникает из-за двух источников записи в одну Matrix4 (вашей анимации и жеста пользователя).

    class _ZoomableState extends State<Zoomable> with SingleTickerProviderStateMixin {  final _transform = TransformationController();  late final _anim = AnimationController(    vsync: this,    duration: const Duration(milliseconds: 250),  )..addListener(() => _transform.value = _zoom!.value);…
  16. 16

    Как добавить горячие клавиши через Shortcuts, Actions и Focus, и почему Ctrl+S иногда вообще ничего не делает?

    Shortcuts сопоставляет сочетание клавиш с Intent, Actions сопоставляет тип интента с кодом, который нужно выполнить, а поиск начинается от виджета в фокусе, поэтому без фокуса внутри поддерева горячая клавиша не срабатывает.

    class SaveIntent extends Intent {  const SaveIntent();} Widget buildEditor() {  return Shortcuts(…
  17. 17

    Кнопка в углу карточки не реагирует: ни ряби, ни колбэка, ничего в логах. Как выяснить, куда на самом деле уходит тап?

    Пройдите путь указателя шаг за шагом и заставьте фреймворк печатать каждый шаг: дошёл ли hit test до кнопки, перехватил ли событие кто-то выше, проиграл ли её распознаватель в арене.

    import 'package:flutter/gestures.dart';import 'package:flutter/rendering.dart'; void main() {  debugPaintPointersEnabled = true; // highlights every box the hit test reaches  debugPrintHitTestResults = true; // prints the hit path, deepest first…
  18. 18

    Что даёт RawGestureDetector по сравнению с GestureDetector, и как написать свой распознаватель, например драг только двумя пальцами?

    RawGestureDetector принимает фабрики распознавателей, поэтому стандартные можно настроить шире, чем позволяет GestureDetector (supportedDevices, allowedButtonsFilter, своя duration долгого нажатия), или подключить свой; сам GestureDetector служит тонкой обёрткой над ним.

    class TwoFingerDragRecognizer extends OneSequenceGestureRecognizer {  ValueChanged<Offset>? onDrag;  final Set<int> _pointers = {};   @override  void addAllowedPointer(PointerDownEvent event) {…
  19. 19

    Как колесо мыши, тачпад и стилус доходят до холста во Flutter, и чего лишается на десктопе холст, построенный только на сенсорных жестах?

    Не всё это проходит через арену жестов: щелчок колеса приходит как PointerScrollEvent в канале pointer signal, pinch на тачпаде приходит как PointerPanZoom*, а перо приходит как указатель вида stylus с давлением; холст только для касаний теряет зум колесом, pinch на тачпаде, наведение и давление пера.

    Listener(  onPointerSignal: (event) {    if (event is! PointerScrollEvent) return;    // Claim the tick, or a scroll view behind the canvas scrolls on it too.    GestureBinding.instance.pointerSignalResolver.register(event, (e) {      final dy = (e as PointerScrollEvent).scrollDelta.dy;…
  20. 20

    С включённым VoiceOver или TalkBack ваш регулятор яркости, который задаётся драгом, вообще не двигается. Что происходит с вводом указателя, когда работает скринридер?

    Скринридер забирает сенсорный экран себе: ваши распознаватели больше не видят свайпов пользователя, а платформа вместо этого отправляет SemanticsAction узлу семантики в фокусе.

    // Pointer-only: with a screen reader on, the user's swipes never arrive here.Listener(  onPointerMove: (e) => _setBrightness(e.localPosition.dx / _width),  child: const BrightnessBar(),);…