Unverified Commit c594696f authored by amirh's avatar amirh Committed by GitHub

Make AndroidView participate in gesture arenas. (#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.
parent 0b939116
...@@ -42,7 +42,13 @@ enum _PlatformViewState { ...@@ -42,7 +42,13 @@ enum _PlatformViewState {
/// Android [View](https://developer.android.com/reference/android/view/View). /// Android [View](https://developer.android.com/reference/android/view/View).
/// ///
/// The render object's layout behavior is to fill all available space, the parent of this object must /// The render object's layout behavior is to fill all available space, the parent of this object must
/// provide bounded layout constraints /// provide bounded layout constraints.
///
/// RenderAndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
/// view can be specified in [RenderAndroidView.gestureRecognizers]. If
/// [RenderAndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android
/// view iff it was not claimed by any other gesture recognizer.
/// ///
/// See also: /// See also:
/// * [AndroidView] which is a widget that is used to show an Android view. /// * [AndroidView] which is a widget that is used to show an Android view.
...@@ -53,10 +59,14 @@ class RenderAndroidView extends RenderBox { ...@@ -53,10 +59,14 @@ class RenderAndroidView extends RenderBox {
RenderAndroidView({ RenderAndroidView({
@required AndroidViewController viewController, @required AndroidViewController viewController,
@required this.hitTestBehavior, @required this.hitTestBehavior,
List<OneSequenceGestureRecognizer> gestureRecognizers = const <OneSequenceGestureRecognizer> [],
}) : assert(viewController != null), }) : assert(viewController != null),
assert(hitTestBehavior != null), assert(hitTestBehavior != null),
_viewController = viewController { assert(gestureRecognizers != null),
_viewController = viewController
{
_motionEventsDispatcher = new _MotionEventsDispatcher(globalToLocal, viewController); _motionEventsDispatcher = new _MotionEventsDispatcher(globalToLocal, viewController);
this.gestureRecognizers = gestureRecognizers;
} }
_PlatformViewState _state = _PlatformViewState.uninitialized; _PlatformViewState _state = _PlatformViewState.uninitialized;
...@@ -80,6 +90,18 @@ class RenderAndroidView extends RenderBox { ...@@ -80,6 +90,18 @@ class RenderAndroidView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate. // any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior; PlatformViewHitTestBehavior hitTestBehavior;
/// Which gestures should be forwarded to the Android view.
///
/// The gesture recognizers on this list participate in the gesture arena for each pointer
/// that was put down on the render box. If any of the recognizers on this list wins the
/// gesture arena, the entire pointer event sequence starting from the pointer down event
/// will be dispatched to the Android view.
set gestureRecognizers(List<OneSequenceGestureRecognizer> recognizers) {
assert(recognizers != null);
_gestureRecognizer?.dispose();
_gestureRecognizer = new _AndroidViewGestureRecognizer(_motionEventsDispatcher, recognizers);
}
@override @override
bool get sizedByParent => true; bool get sizedByParent => true;
...@@ -91,6 +113,8 @@ class RenderAndroidView extends RenderBox { ...@@ -91,6 +113,8 @@ class RenderAndroidView extends RenderBox {
_MotionEventsDispatcher _motionEventsDispatcher; _MotionEventsDispatcher _motionEventsDispatcher;
_AndroidViewGestureRecognizer _gestureRecognizer;
@override @override
void performResize() { void performResize() {
size = constraints.biggest; size = constraints.biggest;
...@@ -169,7 +193,109 @@ class RenderAndroidView extends RenderBox { ...@@ -169,7 +193,109 @@ class RenderAndroidView extends RenderBox {
@override @override
void handleEvent(PointerEvent event, HitTestEntry entry) { void handleEvent(PointerEvent event, HitTestEntry entry) {
_motionEventsDispatcher.handlePointerEvent(event); if (event is PointerDownEvent) {
_gestureRecognizer.addPointer(event);
}
}
@override
void detach() {
_gestureRecognizer.reset();
super.detach();
}
}
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
_AndroidViewGestureRecognizer(this.dispatcher, List<OneSequenceGestureRecognizer> gestureRecognizers) {
this.gestureRecognizers = gestureRecognizers;
}
final _MotionEventsDispatcher dispatcher;
// Maps a pointer to a list of its cached pointer events.
// Before the arena for a pointer is resolved all events are cached here, if we win the arena
// the cached events are dispatched to the view, if we lose the arena we clear the cache for
// the pointer.
final Map<int, List<PointerEvent>> cachedEvents = <int, List<PointerEvent>> {};
// Pointer for which we have already won the arena, events for pointers in this set are
// immediately dispatched to the Android view.
final Set<int> forwardedPointers = new Set<int>();
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
List<OneSequenceGestureRecognizer> _gestureRecognizers;
set gestureRecognizers(List<OneSequenceGestureRecognizer> recognizers) {
_gestureRecognizers = recognizers;
team = new GestureArenaTeam();
team.captain = this;
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.team = team;
}
}
@override
void addPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event);
}
}
@override
String get debugDescription => 'Android view';
@override
void didStopTrackingLastPointer(int pointer) {
resolve(GestureDisposition.rejected);
}
@override
void handleEvent(PointerEvent event) {
if (!forwardedPointers.contains(event.pointer)) {
cacheEvent(event);
} else {
dispatcher.handlePointerEvent(event);
}
stopTrackingIfPointerNoLongerDown(event);
}
@override
void acceptGesture(int pointer) {
flushPointerCache(pointer);
forwardedPointers.add(pointer);
}
@override
void rejectGesture(int pointer) {
stopTrackingPointer(pointer);
cachedEvents.remove(pointer);
}
void cacheEvent(PointerEvent event) {
if (!cachedEvents.containsKey(event.pointer)) {
cachedEvents[event.pointer] = <PointerEvent> [];
}
cachedEvents[event.pointer].add(event);
}
void flushPointerCache(int pointer) {
cachedEvents.remove(pointer)?.forEach(dispatcher.handlePointerEvent);
}
@override
void stopTrackingPointer(int pointer) {
super.stopTrackingPointer(pointer);
forwardedPointers.remove(pointer);
}
void reset() {
forwardedPointers.forEach(super.stopTrackingPointer);
forwardedPointers.clear();
cachedEvents.keys.forEach(super.stopTrackingPointer);
cachedEvents.clear();
resolve(GestureDisposition.rejected);
} }
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -21,6 +22,12 @@ import 'framework.dart'; ...@@ -21,6 +22,12 @@ import 'framework.dart';
/// The widget fill all available space, the parent of this object must provide bounded layout /// The widget fill all available space, the parent of this object must provide bounded layout
/// constraints. /// constraints.
/// ///
/// AndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
/// view can be specified in [AndroidView.gestureRecognizers]. If
/// [AndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android
/// view iff it was not claimed by any other gesture recognizer.
///
/// The Android view object is created using a [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html). /// The Android view object is created using a [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html).
/// 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-). /// 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-).
/// ///
...@@ -41,7 +48,7 @@ import 'framework.dart'; ...@@ -41,7 +48,7 @@ import 'framework.dart';
class AndroidView extends StatefulWidget { class AndroidView extends StatefulWidget {
/// Creates a widget that embeds an Android view. /// Creates a widget that embeds an Android view.
/// ///
/// The `viewType` and `hitTestBehavior` parameters must not be null. /// The `viewType`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null.
/// If `creationParams` is not null then `creationParamsCodec` must not be null. /// If `creationParams` is not null then `creationParamsCodec` must not be null.
AndroidView({ AndroidView({
Key key, Key key,
...@@ -49,10 +56,12 @@ class AndroidView extends StatefulWidget { ...@@ -49,10 +56,12 @@ class AndroidView extends StatefulWidget {
this.onPlatformViewCreated, this.onPlatformViewCreated,
this.hitTestBehavior = PlatformViewHitTestBehavior.opaque, this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
this.layoutDirection, this.layoutDirection,
this.gestureRecognizers = const <OneSequenceGestureRecognizer> [],
this.creationParams, this.creationParams,
this.creationParamsCodec this.creationParamsCodec
}) : assert(viewType != null), }) : assert(viewType != null),
assert(hitTestBehavior != null), assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
assert(creationParams == null || creationParamsCodec != null), assert(creationParams == null || creationParamsCodec != null),
super(key: key); super(key: key);
...@@ -78,6 +87,17 @@ class AndroidView extends StatefulWidget { ...@@ -78,6 +87,17 @@ class AndroidView extends StatefulWidget {
/// If this is null, the ambient [Directionality] is used instead. /// If this is null, the ambient [Directionality] is used instead.
final TextDirection layoutDirection; final TextDirection layoutDirection;
/// Which gestures should be forwarded to the Android view.
///
/// The gesture recognizers on this list participate in the gesture arena for each pointer
/// that was put down on the widget. If any of the recognizers on this list wins the
/// gesture arena, the entire pointer event sequence starting from the pointer down event
/// will be dispatched to the Android view.
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
final List<OneSequenceGestureRecognizer> gestureRecognizers;
/// Passed as the args argument of [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-) /// Passed as the args argument of [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-)
/// ///
/// This can be used by plugins to pass constructor parameters to the embedded Android view. /// This can be used by plugins to pass constructor parameters to the embedded Android view.
...@@ -105,7 +125,8 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -105,7 +125,8 @@ class _AndroidViewState extends State<AndroidView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new _AndroidPlatformView( return new _AndroidPlatformView(
controller: _controller, controller: _controller,
hitTestBehavior: widget.hitTestBehavior hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers,
); );
} }
...@@ -182,20 +203,28 @@ class _AndroidPlatformView extends LeafRenderObjectWidget { ...@@ -182,20 +203,28 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
Key key, Key key,
@required this.controller, @required this.controller,
@required this.hitTestBehavior, @required this.hitTestBehavior,
@required this.gestureRecognizers,
}) : assert(controller != null), }) : assert(controller != null),
assert(hitTestBehavior != null), assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
super(key: key); super(key: key);
final AndroidViewController controller; final AndroidViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior; final PlatformViewHitTestBehavior hitTestBehavior;
final List<OneSequenceGestureRecognizer> gestureRecognizers;
@override @override
RenderObject createRenderObject(BuildContext context) => RenderObject createRenderObject(BuildContext context) =>
new RenderAndroidView(viewController: controller, hitTestBehavior: hitTestBehavior); new RenderAndroidView(
viewController: controller,
hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
);
@override @override
void updateRenderObject(BuildContext context, RenderAndroidView renderObject) { void updateRenderObject(BuildContext context, RenderAndroidView renderObject) {
renderObject.viewController = controller; renderObject.viewController = controller;
renderObject.hitTestBehavior = hitTestBehavior; renderObject.hitTestBehavior = hitTestBehavior;
renderObject.gestureRecognizers = gestureRecognizers;
} }
} }
...@@ -513,4 +513,75 @@ void main() { ...@@ -513,4 +513,75 @@ void main() {
]), ]),
); );
}); });
testWidgets('Android view can lose gesture arenas', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android);
viewsController.registerViewType('webview');
bool verticalDragAcceptedByParent = false;
await tester.pumpWidget(
new Align(
alignment: Alignment.topLeft,
child: new Container(
margin: const EdgeInsets.all(10.0),
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) { verticalDragAcceptedByParent = true; },
child: SizedBox(
width: 200.0,
height: 100.0,
child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, true);
expect(
viewsController.motionEvents[currentViewId + 1],
isNull,
);
});
testWidgets('Android view gesture recognizers', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android);
viewsController.registerViewType('webview');
bool verticalDragAcceptedByParent = false;
await tester.pumpWidget(
new Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) { verticalDragAcceptedByParent = true; },
child: SizedBox(
width: 200.0,
height: 100.0,
child: AndroidView(
viewType: 'webview',
gestureRecognizers: <OneSequenceGestureRecognizer> [new VerticalDragGestureRecognizer()],
layoutDirection: TextDirection.ltr,
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, false);
expect(
viewsController.motionEvents[currentViewId + 1],
orderedEquals(<FakeMotionEvent> [
const FakeMotionEvent(AndroidViewController.kActionDown, <int> [0], <Offset> [Offset(50.0, 50.0)]),
const FakeMotionEvent(AndroidViewController.kActionMove, <int> [0], <Offset> [Offset(50.0, 150.0)]),
const FakeMotionEvent(AndroidViewController.kActionUp, <int> [0], <Offset> [Offset(50.0, 150.0)]),
]),
);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment