blob: 132a33f7787bae09115a0998daa7dcc79d033e63 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:flutter/gestures.dart';
import 'package:fidl_fuchsia_ui_pointerinjector/fidl_async.dart';
/// Defines a singleton [PlatformViewChannel] used to create and control
/// Fuchsia specific platform views.
class FuchsiaViewsService {
// FuchsiaViewsService is a singleton because there is only ever one
// entry-point into the native code for a given platform channel.
static final FuchsiaViewsService instance = FuchsiaViewsService._();
// The platform view channel used to communicate with flutter engine.
final _platformViewChannel = MethodChannel(
'flutter/platform_views',
JSONMethodCodec(),
);
/// The [MethodChannel] used to communicate with Flutter Embedder.
@internal
MethodChannel get platformViewChannel => _platformViewChannel;
/// Holds the method call handlers registered by the view id.
final _callHandlers = <int, Future<dynamic> Function(MethodCall call)?>{};
// Private constructor. Registers a method call handler with the platform
// view.
FuchsiaViewsService._() {
platformViewChannel.setMethodCallHandler((call) async {
// Guard against invalid or missing arguments.
try {
// Call the method call handler registered for viewId.
int? viewId = (call.arguments as Map)['viewId'];
return _callHandlers[viewId]?.call(call);
// ignore: avoid_catches_without_on_clauses
} catch (e) {
// If viewId is missing, call the last registered handler.
return _callHandlers.values.last?.call(call);
}
});
}
/// Register a [MethodCall] handler for a given [viewId].
void register(
int viewId, Future<dynamic> Function(MethodCall call)? handler) =>
_callHandlers[viewId] = handler;
/// Deregister existing [MethodCall] handler for a given [viewId].
void deregister(int viewId) => _callHandlers.remove(viewId);
/// Creates a platform view with [viewId] and given properties.
Future<void> createView(
int viewId, {
bool hitTestable = true,
bool focusable = true,
Rect viewOcclusionHint = Rect.zero,
}) async {
final Map<String, dynamic> args = <String, dynamic>{
'viewId': viewId,
'hitTestable': hitTestable,
'focusable': focusable,
'viewOcclusionHintLTRB': <double>[
viewOcclusionHint.left,
viewOcclusionHint.top,
viewOcclusionHint.right,
viewOcclusionHint.bottom
],
};
return platformViewChannel.invokeMethod('View.create', args);
}
/// Updates view properties of the platform view associated with [viewId].
Future<void> updateView(
int viewId, {
bool hitTestable = true,
bool focusable = true,
Rect viewOcclusionHint = Rect.zero,
}) async {
final Map<String, dynamic> args = <String, dynamic>{
'viewId': viewId,
'hitTestable': hitTestable,
'focusable': focusable,
'viewOcclusionHintLTRB': <double>[
viewOcclusionHint.left,
viewOcclusionHint.top,
viewOcclusionHint.right,
viewOcclusionHint.bottom
],
};
return platformViewChannel.invokeMethod('View.update', args);
}
/// Destroys the platform view associated with [viewId].
Future<void> destroyView(int viewId) async {
final Map<String, dynamic> args = <String, dynamic>{
'viewId': viewId,
};
return platformViewChannel.invokeMethod('View.dispose', args);
}
/// Dispatch [PointerEvent] event to [viewId].
Future<void> dispatchPointerEvent(
{required int viewId,
required PointerEvent pointer,
int? viewRef}) async {
if (!_hasFuchsiaEventPhase(pointer)) {
return;
}
// We use the global position in the parent's local coordinate system.
// The injection viewport is set up to coincide with the local coordinate
// system, and all child-specific transforms are handled on the server.
final x = pointer.position.dx;
final y = pointer.position.dy;
// Use the logical space of the parent view as the injection viewport.
// It means that pointer coordinates, in the parent's view, can be used
// verbatim for injecting into a child view. The fuchsia.ui.pointerinjector
// server handles the pointer coordinate transforms for the child view.
//
// Note that the Flutter instance's logical space can change size, but since
// the logical space *always* has its origin at Offset.zero, a size change
// does not need a new viewport, since the viewport merely anchors pointer
// coordinates received by the Flutter instance.
ui.Size window = ui.window.physicalSize / ui.window.devicePixelRatio;
final phase = pointer is PointerDownEvent
? EventPhase.add
: pointer is PointerUpEvent
? EventPhase.remove
: pointer is PointerMoveEvent
? EventPhase.change
: EventPhase.cancel;
final args = _makeInjectArgs(
viewId: viewId,
x: x,
y: y,
phase: _getEventPhaseValue(phase),
pointerId: ((pointer.device << 32) >> 32),
// `traceFlowId` and `pointer.pointer` are program specific nonces.
traceFlowId: pointer.pointer,
logicalWidth: window.width,
logicalHeight: window.height,
timestamp: pointer.timeStamp.inMicroseconds * 1000,
viewRef: viewRef);
return platformViewChannel.invokeMethod(
'View.pointerinjector.inject', args);
}
Map<String, dynamic> _makeInjectArgs(
{required int viewId,
required double x,
required double y,
required int phase,
required int pointerId,
required int traceFlowId,
required double logicalWidth,
required double logicalHeight,
required int timestamp,
int? viewRef}) {
final args = <String, dynamic>{
'viewId': viewId,
'x': x,
'y': y,
'phase': phase,
'pointerId': pointerId,
'traceFlowId': traceFlowId,
'logicalWidth': logicalWidth,
'logicalHeight': logicalHeight,
'timestamp': timestamp
};
// In Flatland, the Flutter engine holds the ViewRef and does not provide it
// to the dart code, so we do not include it in the platform message.
if (viewRef != null) {
args['viewRef'] = viewRef;
}
return args;
}
// Check if [PointerEvent] is one of supported events.
bool _hasFuchsiaEventPhase(PointerEvent? pointer) {
// TODO(fxbug.dev/84030) - Implement stream consistency checking: inject
// only valid streams, and reject broken streams.
return pointer != null &&
(pointer is PointerDownEvent ||
pointer is PointerUpEvent ||
pointer is PointerMoveEvent ||
pointer is PointerCancelEvent);
}
int _getEventPhaseValue(EventPhase phase) {
switch (phase) {
case EventPhase.add:
return 1;
case EventPhase.change:
return 2;
case EventPhase.remove:
return 3;
case EventPhase.cancel:
return 4;
default:
return 0;
}
}
}