arena.dart 9.76 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6 7
import 'dart:async';

8 9 10 11
import 'package:flutter/foundation.dart';

import 'debug.dart';

12
/// Whether the gesture was accepted or rejected.
Adam Barth's avatar
Adam Barth committed
13
enum GestureDisposition {
14
  /// This gesture was accepted as the interpretation of the user's input.
Adam Barth's avatar
Adam Barth committed
15
  accepted,
16

17
  /// This gesture was rejected as the interpretation of the user's input.
18
  rejected,
Adam Barth's avatar
Adam Barth committed
19 20
}

21 22 23 24
/// Represents an object participating in an arena.
///
/// Receives callbacks from the GestureArena to notify the object when it wins
/// or loses a gesture negotiation. Exactly one of [acceptGesture] or
25
/// [rejectGesture] will be called for each arena this member was added to,
26 27 28
/// regardless of what caused the arena to be resolved. For example, if a
/// member resolves the arena itself, that member still receives an
/// [acceptGesture] callback.
Adam Barth's avatar
Adam Barth committed
29
abstract class GestureArenaMember {
30 31
  /// Called when this member wins the arena for the given pointer id.
  void acceptGesture(int pointer);
Adam Barth's avatar
Adam Barth committed
32

33 34
  /// Called when this member loses the arena for the given pointer id.
  void rejectGesture(int pointer);
Adam Barth's avatar
Adam Barth committed
35 36
}

37
/// An interface to pass information to an arena.
38 39
///
/// A given [GestureArenaMember] can have multiple entries in multiple arenas
40
/// with different pointer ids.
Adam Barth's avatar
Adam Barth committed
41
class GestureArenaEntry {
42
  GestureArenaEntry._(this._arena, this._pointer, this._member);
Adam Barth's avatar
Adam Barth committed
43

44 45
  final GestureArenaManager _arena;
  final int _pointer;
Adam Barth's avatar
Adam Barth committed
46 47 48
  final GestureArenaMember _member;

  /// Call this member to claim victory (with accepted) or admit defeat (with rejected).
49
  ///
50 51
  /// It's fine to attempt to resolve a gesture recognizer for an arena that is
  /// already resolved.
Adam Barth's avatar
Adam Barth committed
52
  void resolve(GestureDisposition disposition) {
53
    _arena._resolve(_pointer, _member, disposition);
Adam Barth's avatar
Adam Barth committed
54 55 56
  }
}

57
class _GestureArena {
58
  final List<GestureArenaMember> members = <GestureArenaMember>[];
59
  bool isOpen = true;
60
  bool isHeld = false;
61
  bool hasPendingSweep = false;
62

63
  /// If a member attempts to win while the arena is still open, it becomes the
64
  /// "eager winner". We look for an eager winner when closing the arena to new
65
  /// participants, and if there is one, we resolve the arena in its favor at
Hixie's avatar
Hixie committed
66
  /// that time.
67
  GestureArenaMember? eagerWinner;
Hixie's avatar
Hixie committed
68

69 70 71 72
  void add(GestureArenaMember member) {
    assert(isOpen);
    members.add(member);
  }
73 74 75

  @override
  String toString() {
76
    final StringBuffer buffer = StringBuffer();
77 78 79 80
    if (members.isEmpty) {
      buffer.write('<empty>');
    } else {
      buffer.write(members.map<String>((GestureArenaMember member) {
81
        if (member == eagerWinner) {
82
          return '$member (eager winner)';
83
        }
84 85 86
        return '$member';
      }).join(', '));
    }
87
    if (isOpen) {
88
      buffer.write(' [open]');
89 90
    }
    if (isHeld) {
91
      buffer.write(' [held]');
92 93
    }
    if (hasPendingSweep) {
94
      buffer.write(' [hasPendingSweep]');
95
    }
96 97
    return buffer.toString();
  }
98 99
}

100
/// The first member to accept or the last member to not reject wins.
101
///
102
/// See <https://flutter.dev/gestures/#gesture-disambiguation> for more
103
/// information about the role this class plays in the gesture system.
104 105 106
///
/// To debug problems with gestures, consider using
/// [debugPrintGestureArenaDiagnostics].
107
class GestureArenaManager {
108
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
Adam Barth's avatar
Adam Barth committed
109

110
  /// Adds a new member (e.g., gesture recognizer) to the arena.
111
  GestureArenaEntry add(int pointer, GestureArenaMember member) {
112 113
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
114
      return _GestureArena();
115
    });
116
    state.add(member);
117
    assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
118
    return GestureArenaEntry._(this, pointer, member);
Adam Barth's avatar
Adam Barth committed
119 120
  }

121 122 123
  /// Prevents new members from entering the arena.
  ///
  /// Called after the framework has finished dispatching the pointer down event.
124
  void close(int pointer) {
125
    final _GestureArena? state = _arenas[pointer];
126
    if (state == null) {
127
      return; // This arena either never existed or has been resolved.
128
    }
129
    state.isOpen = false;
130
    assert(_debugLogDiagnostic(pointer, 'Closing', state));
131
    _tryToResolveArena(pointer, state);
132 133
  }

134
  /// Forces resolution of the arena, giving the win to the first member.
135 136 137 138 139 140 141 142 143 144 145 146
  ///
  /// Sweep is typically after all the other processing for a [PointerUpEvent]
  /// have taken place. It ensures that multiple passive gestures do not cause a
  /// stalemate that prevents the user from interacting with the app.
  ///
  /// Recognizers that wish to delay resolving an arena past [PointerUpEvent]
  /// should call [hold] to delay sweep until [release] is called.
  ///
  /// See also:
  ///
  ///  * [hold]
  ///  * [release]
147
  void sweep(int pointer) {
148
    final _GestureArena? state = _arenas[pointer];
149
    if (state == null) {
150
      return; // This arena either never existed or has been resolved.
151
    }
152
    assert(!state.isOpen);
153 154
    if (state.isHeld) {
      state.hasPendingSweep = true;
155
      assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
156
      return; // This arena is being held for a long-lived member.
157
    }
158
    assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
159
    _arenas.remove(pointer);
Ian Hickson's avatar
Ian Hickson committed
160
    if (state.members.isNotEmpty) {
161 162
      // First member wins.
      assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
163
      state.members.first.acceptGesture(pointer);
164
      // Give all the other members the bad news.
165
      for (int i = 1; i < state.members.length; i++) {
166
        state.members[i].rejectGesture(pointer);
167
      }
168 169 170
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
171
  /// Prevents the arena from being swept.
172 173 174 175 176 177 178 179 180 181 182
  ///
  /// Typically, a winner is chosen in an arena after all the other
  /// [PointerUpEvent] processing by [sweep]. If a recognizer wishes to delay
  /// resolving an arena past [PointerUpEvent], the recognizer can [hold] the
  /// arena open using this function. To release such a hold and let the arena
  /// resolve, call [release].
  ///
  /// See also:
  ///
  ///  * [sweep]
  ///  * [release]
183
  void hold(int pointer) {
184
    final _GestureArena? state = _arenas[pointer];
185
    if (state == null) {
186
      return; // This arena either never existed or has been resolved.
187
    }
188
    state.isHeld = true;
189
    assert(_debugLogDiagnostic(pointer, 'Holding', state));
190 191
  }

Florian Loitsch's avatar
Florian Loitsch committed
192 193
  /// Releases a hold, allowing the arena to be swept.
  ///
194
  /// If a sweep was attempted on a held arena, the sweep will be done
Florian Loitsch's avatar
Florian Loitsch committed
195
  /// on release.
196 197 198 199 200
  ///
  /// See also:
  ///
  ///  * [sweep]
  ///  * [hold]
201
  void release(int pointer) {
202
    final _GestureArena? state = _arenas[pointer];
203
    if (state == null) {
204
      return; // This arena either never existed or has been resolved.
205
    }
206
    state.isHeld = false;
207
    assert(_debugLogDiagnostic(pointer, 'Releasing', state));
208
    if (state.hasPendingSweep) {
209
      sweep(pointer);
210
    }
211 212
  }

213 214 215
  /// Reject or accept a gesture recognizer.
  ///
  /// This is called by calling [GestureArenaEntry.resolve] on the object returned from [add].
216
  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
217
    final _GestureArena? state = _arenas[pointer];
218
    if (state == null) {
219
      return; // This arena has already resolved.
220
    }
221
    assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
222
    assert(state.members.contains(member));
Adam Barth's avatar
Adam Barth committed
223
    if (disposition == GestureDisposition.rejected) {
224
      state.members.remove(member);
225
      member.rejectGesture(pointer);
226
      if (!state.isOpen) {
227
        _tryToResolveArena(pointer, state);
228
      }
Adam Barth's avatar
Adam Barth committed
229 230
    } else {
      assert(disposition == GestureDisposition.accepted);
Hixie's avatar
Hixie committed
231
      if (state.isOpen) {
Ian Hickson's avatar
Ian Hickson committed
232
        state.eagerWinner ??= member;
Hixie's avatar
Hixie committed
233
      } else {
234
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
235
        _resolveInFavorOf(pointer, state, member);
Adam Barth's avatar
Adam Barth committed
236 237 238
      }
    }
  }
Hixie's avatar
Hixie committed
239

240 241 242 243 244 245 246 247 248 249
  void _tryToResolveArena(int pointer, _GestureArena state) {
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    if (state.members.length == 1) {
      scheduleMicrotask(() => _resolveByDefault(pointer, state));
    } else if (state.members.isEmpty) {
      _arenas.remove(pointer);
      assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
    } else if (state.eagerWinner != null) {
      assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
250
      _resolveInFavorOf(pointer, state, state.eagerWinner!);
251 252 253 254
    }
  }

  void _resolveByDefault(int pointer, _GestureArena state) {
255 256 257
    if (!_arenas.containsKey(pointer)) {
      return; // This arena has already resolved.
    }
258 259 260 261 262 263 264 265 266
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    final List<GestureArenaMember> members = state.members;
    assert(members.length == 1);
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
    state.members.first.acceptGesture(pointer);
  }

267 268
  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    assert(state == _arenas[pointer]);
Hixie's avatar
Hixie committed
269 270 271
    assert(state != null);
    assert(state.eagerWinner == null || state.eagerWinner == member);
    assert(!state.isOpen);
272
    _arenas.remove(pointer);
273
    for (final GestureArenaMember rejectedMember in state.members) {
274
      if (rejectedMember != member) {
275
        rejectedMember.rejectGesture(pointer);
276
      }
Hixie's avatar
Hixie committed
277
    }
278
    member.acceptGesture(pointer);
Hixie's avatar
Hixie committed
279
  }
280

281
  bool _debugLogDiagnostic(int pointer, String message, [ _GestureArena? state ]) {
282 283
    assert(() {
      if (debugPrintGestureArenaDiagnostics) {
284
        final int? count = state?.members.length;
285 286 287 288
        final String s = count != 1 ? 's' : '';
        debugPrint('Gesture arena ${pointer.toString().padRight(4)}$message${ count != null ? " with $count member$s." : ""}');
      }
      return true;
289
    }());
290 291
    return true;
  }
Ian Hickson's avatar
Ian Hickson committed
292
}