mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Re-land "Make AndroidView participate in gesture arenas." (#21097)
This re-lands commit c594696. The original PR was breaking the integration test as while the gesture was active the AndroidView's parent widget got rebuilt which disposed the _AndroidViewGestureRecognizer and dropped pointer events from that point on. This change adds a unit test that reproduces the failure case, and fixes it by not rebuilding the _AndroidViewGestureRecognizer if the list of gesture recognizers did not change.
This commit is contained in:
parent
2b14ac1efc
commit
c5b7766cbb
@ -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,21 @@ 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);
|
||||
if (recognizers == _gestureRecognizer?.gestureRecognizers) {
|
||||
return;
|
||||
}
|
||||
_gestureRecognizer?.dispose();
|
||||
_gestureRecognizer = new _AndroidViewGestureRecognizer(_motionEventsDispatcher, recognizers);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@ -91,6 +116,8 @@ class RenderAndroidView extends RenderBox {
|
||||
|
||||
_MotionEventsDispatcher _motionEventsDispatcher;
|
||||
|
||||
_AndroidViewGestureRecognizer _gestureRecognizer;
|
||||
|
||||
@override
|
||||
void performResize() {
|
||||
size = constraints.biggest;
|
||||
@ -169,7 +196,110 @@ 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;
|
||||
List<OneSequenceGestureRecognizer> get gestureRecognizers => _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,122 @@ 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)]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Android view rebuilt during gesture', (WidgetTester tester) async {
|
||||
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
|
||||
final FakePlatformViewsController viewsController = new FakePlatformViewsController(TargetPlatform.android);
|
||||
viewsController.registerViewType('webview');
|
||||
await tester.pumpWidget(
|
||||
new Align(
|
||||
alignment: Alignment.topLeft,
|
||||
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 tester.pumpWidget(
|
||||
new Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 200.0,
|
||||
height: 100.0,
|
||||
child: AndroidView(
|
||||
viewType: 'webview',
|
||||
layoutDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await gesture.up();
|
||||
|
||||
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)]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user