Skip to content

Commit c594696

Browse files
authored
Make AndroidView participate in gesture arenas. (flutter#20917)
Pointer events are dispatched to the Android view only if it won Flutter's gesture arena for the pointer. Specific gestures that should be dispatched to the android view can be specified with the gestureRecognizers parameter.
1 parent 0b93911 commit c594696

File tree

3 files changed

+232
-6
lines changed

3 files changed

+232
-6
lines changed

packages/flutter/lib/src/rendering/platform_view.dart

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ enum _PlatformViewState {
4242
/// Android [View](https://developer.android.com/reference/android/view/View).
4343
///
4444
/// The render object's layout behavior is to fill all available space, the parent of this object must
45-
/// provide bounded layout constraints
45+
/// provide bounded layout constraints.
46+
///
47+
/// RenderAndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
48+
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
49+
/// view can be specified in [RenderAndroidView.gestureRecognizers]. If
50+
/// [RenderAndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android
51+
/// view iff it was not claimed by any other gesture recognizer.
4652
///
4753
/// See also:
4854
/// * [AndroidView] which is a widget that is used to show an Android view.
@@ -53,10 +59,14 @@ class RenderAndroidView extends RenderBox {
5359
RenderAndroidView({
5460
@required AndroidViewController viewController,
5561
@required this.hitTestBehavior,
62+
List<OneSequenceGestureRecognizer> gestureRecognizers = const <OneSequenceGestureRecognizer> [],
5663
}) : assert(viewController != null),
5764
assert(hitTestBehavior != null),
58-
_viewController = viewController {
65+
assert(gestureRecognizers != null),
66+
_viewController = viewController
67+
{
5968
_motionEventsDispatcher = new _MotionEventsDispatcher(globalToLocal, viewController);
69+
this.gestureRecognizers = gestureRecognizers;
6070
}
6171

6272
_PlatformViewState _state = _PlatformViewState.uninitialized;
@@ -80,6 +90,18 @@ class RenderAndroidView extends RenderBox {
8090
// any newly arriving events there's nothing we need to invalidate.
8191
PlatformViewHitTestBehavior hitTestBehavior;
8292

93+
/// Which gestures should be forwarded to the Android view.
94+
///
95+
/// The gesture recognizers on this list participate in the gesture arena for each pointer
96+
/// that was put down on the render box. If any of the recognizers on this list wins the
97+
/// gesture arena, the entire pointer event sequence starting from the pointer down event
98+
/// will be dispatched to the Android view.
99+
set gestureRecognizers(List<OneSequenceGestureRecognizer> recognizers) {
100+
assert(recognizers != null);
101+
_gestureRecognizer?.dispose();
102+
_gestureRecognizer = new _AndroidViewGestureRecognizer(_motionEventsDispatcher, recognizers);
103+
}
104+
83105
@override
84106
bool get sizedByParent => true;
85107

@@ -91,6 +113,8 @@ class RenderAndroidView extends RenderBox {
91113

92114
_MotionEventsDispatcher _motionEventsDispatcher;
93115

116+
_AndroidViewGestureRecognizer _gestureRecognizer;
117+
94118
@override
95119
void performResize() {
96120
size = constraints.biggest;
@@ -169,7 +193,109 @@ class RenderAndroidView extends RenderBox {
169193

170194
@override
171195
void handleEvent(PointerEvent event, HitTestEntry entry) {
172-
_motionEventsDispatcher.handlePointerEvent(event);
196+
if (event is PointerDownEvent) {
197+
_gestureRecognizer.addPointer(event);
198+
}
199+
}
200+
201+
@override
202+
void detach() {
203+
_gestureRecognizer.reset();
204+
super.detach();
205+
}
206+
}
207+
208+
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
209+
_AndroidViewGestureRecognizer(this.dispatcher, List<OneSequenceGestureRecognizer> gestureRecognizers) {
210+
this.gestureRecognizers = gestureRecognizers;
211+
}
212+
213+
final _MotionEventsDispatcher dispatcher;
214+
215+
// Maps a pointer to a list of its cached pointer events.
216+
// Before the arena for a pointer is resolved all events are cached here, if we win the arena
217+
// the cached events are dispatched to the view, if we lose the arena we clear the cache for
218+
// the pointer.
219+
final Map<int, List<PointerEvent>> cachedEvents = <int, List<PointerEvent>> {};
220+
221+
// Pointer for which we have already won the arena, events for pointers in this set are
222+
// immediately dispatched to the Android view.
223+
final Set<int> forwardedPointers = new Set<int>();
224+
225+
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
226+
// TODO(amirh): get a list of GestureRecognizers here.
227+
// https://github.com/flutter/flutter/issues/20953
228+
List<OneSequenceGestureRecognizer> _gestureRecognizers;
229+
set gestureRecognizers(List<OneSequenceGestureRecognizer> recognizers) {
230+
_gestureRecognizers = recognizers;
231+
team = new GestureArenaTeam();
232+
team.captain = this;
233+
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
234+
recognizer.team = team;
235+
}
236+
}
237+
238+
@override
239+
void addPointer(PointerDownEvent event) {
240+
startTrackingPointer(event.pointer);
241+
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
242+
recognizer.addPointer(event);
243+
}
244+
}
245+
246+
@override
247+
String get debugDescription => 'Android view';
248+
249+
@override
250+
void didStopTrackingLastPointer(int pointer) {
251+
resolve(GestureDisposition.rejected);
252+
}
253+
254+
@override
255+
void handleEvent(PointerEvent event) {
256+
if (!forwardedPointers.contains(event.pointer)) {
257+
cacheEvent(event);
258+
} else {
259+
dispatcher.handlePointerEvent(event);
260+
}
261+
stopTrackingIfPointerNoLongerDown(event);
262+
}
263+
264+
@override
265+
void acceptGesture(int pointer) {
266+
flushPointerCache(pointer);
267+
forwardedPointers.add(pointer);
268+
}
269+
270+
@override
271+
void rejectGesture(int pointer) {
272+
stopTrackingPointer(pointer);
273+
cachedEvents.remove(pointer);
274+
}
275+
276+
void cacheEvent(PointerEvent event) {
277+
if (!cachedEvents.containsKey(event.pointer)) {
278+
cachedEvents[event.pointer] = <PointerEvent> [];
279+
}
280+
cachedEvents[event.pointer].add(event);
281+
}
282+
283+
void flushPointerCache(int pointer) {
284+
cachedEvents.remove(pointer)?.forEach(dispatcher.handlePointerEvent);
285+
}
286+
287+
@override
288+
void stopTrackingPointer(int pointer) {
289+
super.stopTrackingPointer(pointer);
290+
forwardedPointers.remove(pointer);
291+
}
292+
293+
void reset() {
294+
forwardedPointers.forEach(super.stopTrackingPointer);
295+
forwardedPointers.clear();
296+
cachedEvents.keys.forEach(super.stopTrackingPointer);
297+
cachedEvents.clear();
298+
resolve(GestureDisposition.rejected);
173299
}
174300
}
175301

packages/flutter/lib/src/widgets/platform_view.dart

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/gestures.dart';
67
import 'package:flutter/rendering.dart';
78
import 'package:flutter/services.dart';
89

@@ -21,6 +22,12 @@ import 'framework.dart';
2122
/// The widget fill all available space, the parent of this object must provide bounded layout
2223
/// constraints.
2324
///
25+
/// AndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
26+
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
27+
/// view can be specified in [AndroidView.gestureRecognizers]. If
28+
/// [AndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android
29+
/// view iff it was not claimed by any other gesture recognizer.
30+
///
2431
/// The Android view object is created using a [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html).
2532
/// Plugins can register platform view factories with [PlatformViewRegistry#registerViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewRegistry.html#registerViewFactory-java.lang.String-io.flutter.plugin.platform.PlatformViewFactory-).
2633
///
@@ -41,18 +48,20 @@ import 'framework.dart';
4148
class AndroidView extends StatefulWidget {
4249
/// Creates a widget that embeds an Android view.
4350
///
44-
/// The `viewType` and `hitTestBehavior` parameters must not be null.
51+
/// The `viewType`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null.
4552
/// If `creationParams` is not null then `creationParamsCodec` must not be null.
4653
AndroidView({
4754
Key key,
4855
@required this.viewType,
4956
this.onPlatformViewCreated,
5057
this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
5158
this.layoutDirection,
59+
this.gestureRecognizers = const <OneSequenceGestureRecognizer> [],
5260
this.creationParams,
5361
this.creationParamsCodec
5462
}) : assert(viewType != null),
5563
assert(hitTestBehavior != null),
64+
assert(gestureRecognizers != null),
5665
assert(creationParams == null || creationParamsCodec != null),
5766
super(key: key);
5867

@@ -78,6 +87,17 @@ class AndroidView extends StatefulWidget {
7887
/// If this is null, the ambient [Directionality] is used instead.
7988
final TextDirection layoutDirection;
8089

90+
/// Which gestures should be forwarded to the Android view.
91+
///
92+
/// The gesture recognizers on this list participate in the gesture arena for each pointer
93+
/// that was put down on the widget. If any of the recognizers on this list wins the
94+
/// gesture arena, the entire pointer event sequence starting from the pointer down event
95+
/// will be dispatched to the Android view.
96+
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
97+
// TODO(amirh): get a list of GestureRecognizers here.
98+
// https://github.com/flutter/flutter/issues/20953
99+
final List<OneSequenceGestureRecognizer> gestureRecognizers;
100+
81101
/// Passed as the args argument of [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-)
82102
///
83103
/// This can be used by plugins to pass constructor parameters to the embedded Android view.
@@ -105,7 +125,8 @@ class _AndroidViewState extends State<AndroidView> {
105125
Widget build(BuildContext context) {
106126
return new _AndroidPlatformView(
107127
controller: _controller,
108-
hitTestBehavior: widget.hitTestBehavior
128+
hitTestBehavior: widget.hitTestBehavior,
129+
gestureRecognizers: widget.gestureRecognizers,
109130
);
110131
}
111132

@@ -182,20 +203,28 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
182203
Key key,
183204
@required this.controller,
184205
@required this.hitTestBehavior,
206+
@required this.gestureRecognizers,
185207
}) : assert(controller != null),
186208
assert(hitTestBehavior != null),
209+
assert(gestureRecognizers != null),
187210
super(key: key);
188211

189212
final AndroidViewController controller;
190213
final PlatformViewHitTestBehavior hitTestBehavior;
214+
final List<OneSequenceGestureRecognizer> gestureRecognizers;
191215

192216
@override
193217
RenderObject createRenderObject(BuildContext context) =>
194-
new RenderAndroidView(viewController: controller, hitTestBehavior: hitTestBehavior);
218+
new RenderAndroidView(
219+
viewController: controller,
220+
hitTestBehavior: hitTestBehavior,
221+
gestureRecognizers: gestureRecognizers,
222+
);
195223

196224
@override
197225
void updateRenderObject(BuildContext context, RenderAndroidView renderObject) {
198226
renderObject.viewController = controller;
199227
renderObject.hitTestBehavior = hitTestBehavior;
228+
renderObject.gestureRecognizers = gestureRecognizers;
200229
}
201230
}

packages/flutter/test/widgets/platform_view_test.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,4 +513,75 @@ void main() {
513513
]),
514514
);
515515
});
516+
517+
testWidgets('Android view can lose gesture arenas', (WidgetTester tester) async {
518+
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
519+
final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android);
520+
viewsController.registerViewType('webview');
521+
bool verticalDragAcceptedByParent = false;
522+
await tester.pumpWidget(
523+
new Align(
524+
alignment: Alignment.topLeft,
525+
child: new Container(
526+
margin: const EdgeInsets.all(10.0),
527+
child: GestureDetector(
528+
onVerticalDragStart: (DragStartDetails d) { verticalDragAcceptedByParent = true; },
529+
child: SizedBox(
530+
width: 200.0,
531+
height: 100.0,
532+
child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr),
533+
),
534+
),
535+
),
536+
),
537+
);
538+
539+
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
540+
await gesture.moveBy(const Offset(0.0, 100.0));
541+
await gesture.up();
542+
543+
expect(verticalDragAcceptedByParent, true);
544+
expect(
545+
viewsController.motionEvents[currentViewId + 1],
546+
isNull,
547+
);
548+
});
549+
550+
testWidgets('Android view gesture recognizers', (WidgetTester tester) async {
551+
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
552+
final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android);
553+
viewsController.registerViewType('webview');
554+
bool verticalDragAcceptedByParent = false;
555+
await tester.pumpWidget(
556+
new Align(
557+
alignment: Alignment.topLeft,
558+
child: GestureDetector(
559+
onVerticalDragStart: (DragStartDetails d) { verticalDragAcceptedByParent = true; },
560+
child: SizedBox(
561+
width: 200.0,
562+
height: 100.0,
563+
child: AndroidView(
564+
viewType: 'webview',
565+
gestureRecognizers: <OneSequenceGestureRecognizer> [new VerticalDragGestureRecognizer()],
566+
layoutDirection: TextDirection.ltr,
567+
),
568+
),
569+
),
570+
),
571+
);
572+
573+
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
574+
await gesture.moveBy(const Offset(0.0, 100.0));
575+
await gesture.up();
576+
577+
expect(verticalDragAcceptedByParent, false);
578+
expect(
579+
viewsController.motionEvents[currentViewId + 1],
580+
orderedEquals(<FakeMotionEvent> [
581+
const FakeMotionEvent(AndroidViewController.kActionDown, <int> [0], <Offset> [Offset(50.0, 50.0)]),
582+
const FakeMotionEvent(AndroidViewController.kActionMove, <int> [0], <Offset> [Offset(50.0, 150.0)]),
583+
const FakeMotionEvent(AndroidViewController.kActionUp, <int> [0], <Offset> [Offset(50.0, 150.0)]),
584+
]),
585+
);
586+
});
516587
}

0 commit comments

Comments
 (0)