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.
This commit is contained in:
@@ -42,7 +42,13 @@ enum _PlatformViewState {
|
||||
/// 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
|
||||
/// 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:
|
||||
/// * [AndroidView] which is a widget that is used to show an Android view.
|
||||
@@ -53,10 +59,14 @@ class RenderAndroidView extends RenderBox {
|
||||
RenderAndroidView({
|
||||
@required AndroidViewController viewController,
|
||||
@required this.hitTestBehavior,
|
||||
List<OneSequenceGestureRecognizer> gestureRecognizers = const <OneSequenceGestureRecognizer> [],
|
||||
}) : assert(viewController != null),
|
||||
assert(hitTestBehavior != null),
|
||||
_viewController = viewController {
|
||||
assert(gestureRecognizers != null),
|
||||
_viewController = viewController
|
||||
{
|
||||
_motionEventsDispatcher = new _MotionEventsDispatcher(globalToLocal, viewController);
|
||||
this.gestureRecognizers = gestureRecognizers;
|
||||
}
|
||||
|
||||
_PlatformViewState _state = _PlatformViewState.uninitialized;
|
||||
@@ -80,6 +90,18 @@ class RenderAndroidView extends RenderBox {
|
||||
// any newly arriving events there's nothing we need to invalidate.
|
||||
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
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@@ -91,6 +113,8 @@ class RenderAndroidView extends RenderBox {
|
||||
|
||||
_MotionEventsDispatcher _motionEventsDispatcher;
|
||||
|
||||
_AndroidViewGestureRecognizer _gestureRecognizer;
|
||||
|
||||
@override
|
||||
void performResize() {
|
||||
size = constraints.biggest;
|
||||
@@ -169,7 +193,109 @@ class RenderAndroidView extends RenderBox {
|
||||
|
||||
@override
|
||||
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 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -21,6 +22,12 @@ import 'framework.dart';
|
||||
/// The widget fill all available space, the parent of this object must provide bounded layout
|
||||
/// 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).
|
||||
/// 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';
|
||||
class AndroidView extends StatefulWidget {
|
||||
/// 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.
|
||||
AndroidView({
|
||||
Key key,
|
||||
@@ -49,10 +56,12 @@ class AndroidView extends StatefulWidget {
|
||||
this.onPlatformViewCreated,
|
||||
this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
|
||||
this.layoutDirection,
|
||||
this.gestureRecognizers = const <OneSequenceGestureRecognizer> [],
|
||||
this.creationParams,
|
||||
this.creationParamsCodec
|
||||
}) : assert(viewType != null),
|
||||
assert(hitTestBehavior != null),
|
||||
assert(gestureRecognizers != null),
|
||||
assert(creationParams == null || creationParamsCodec != null),
|
||||
super(key: key);
|
||||
|
||||
@@ -78,6 +87,17 @@ class AndroidView extends StatefulWidget {
|
||||
/// If this is null, the ambient [Directionality] is used instead.
|
||||
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-)
|
||||
///
|
||||
/// This can be used by plugins to pass constructor parameters to the embedded Android view.
|
||||
@@ -105,7 +125,8 @@ class _AndroidViewState extends State<AndroidView> {
|
||||
Widget build(BuildContext context) {
|
||||
return new _AndroidPlatformView(
|
||||
controller: _controller,
|
||||
hitTestBehavior: widget.hitTestBehavior
|
||||
hitTestBehavior: widget.hitTestBehavior,
|
||||
gestureRecognizers: widget.gestureRecognizers,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,20 +203,28 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
|
||||
Key key,
|
||||
@required this.controller,
|
||||
@required this.hitTestBehavior,
|
||||
@required this.gestureRecognizers,
|
||||
}) : assert(controller != null),
|
||||
assert(hitTestBehavior != null),
|
||||
assert(gestureRecognizers != null),
|
||||
super(key: key);
|
||||
|
||||
final AndroidViewController controller;
|
||||
final PlatformViewHitTestBehavior hitTestBehavior;
|
||||
final List<OneSequenceGestureRecognizer> gestureRecognizers;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) =>
|
||||
new RenderAndroidView(viewController: controller, hitTestBehavior: hitTestBehavior);
|
||||
new RenderAndroidView(
|
||||
viewController: controller,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
gestureRecognizers: gestureRecognizers,
|
||||
);
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderAndroidView renderObject) {
|
||||
renderObject.viewController = controller;
|
||||
renderObject.hitTestBehavior = hitTestBehavior;
|
||||
renderObject.gestureRecognizers = gestureRecognizers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user