[sdk] Support updating view properties.

This change allows updating the view's focusable, hitTestable and
the new viewInsets property after the view is created.

Bug: 71338
Change-Id: If840ed8d22bdedfe27ab6f15520de9d63fe57cc4
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/511281
Commit-Queue: Sanjay Chouksey <sanjayc@google.com>
Reviewed-by: David Worsham <dworsham@google.com>
diff --git a/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view.dart b/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view.dart
index c3daa5a..c4a8cfe 100644
--- a/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view.dart
+++ b/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui';
+
 import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
@@ -10,7 +12,7 @@
 import 'fuchsia_view_controller.dart';
 
 /// A widget that is replaced by content from another process.
-class FuchsiaView extends StatelessWidget {
+class FuchsiaView extends StatefulWidget {
   /// The [PlatformViewController] used to control this [FuchsiaView].
   final FuchsiaViewController controller;
 
@@ -24,26 +26,79 @@
   /// Defaults to true.
   final bool focusable;
 
+  /// View insets passed to the child view.
+  ///
+  /// Defaults to [Rect.zero].
+  final Rect viewInsets;
+
   /// Creates a widget that is replaced by content from another process.
   FuchsiaView({
     required this.controller,
     this.hitTestable = true,
     this.focusable = true,
+    this.viewInsets = Rect.zero,
   }) : super(key: GlobalObjectKey(controller));
 
   @override
+  _FuchsiaViewState createState() => _FuchsiaViewState();
+}
+
+class _FuchsiaViewState extends State<FuchsiaView> {
+  bool _needsUpdate = false;
+
+  @override
+  void initState() {
+    super.initState();
+
+    // Apply any pending updates once the platform view is connected.
+    widget.controller.whenConnected.then((_) => _updateView());
+  }
+
+  @override
+  void didUpdateWidget(FuchsiaView oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    if (widget.focusable != oldWidget.focusable ||
+        widget.hitTestable != oldWidget.hitTestable ||
+        widget.viewInsets != oldWidget.viewInsets) {
+      _needsUpdate = true;
+      _updateView();
+    }
+  }
+
+  // Updates the view attributes on the underlying platform view.
+  //
+  // Called when view's [focusable], [hitTestable] or [viewInsets] have changed
+  // or when the underlying platform view is connected.
+  void _updateView() {
+    if (_needsUpdate == false || !widget.controller.connected) {
+      return;
+    }
+
+    widget.controller.update(
+        focusable: widget.focusable,
+        hitTestable: widget.hitTestable,
+        viewInsets: widget.viewInsets);
+    _needsUpdate = false;
+  }
+
+  @override
   Widget build(BuildContext context) {
     return PlatformViewLink(
       viewType: 'fuchsiaView',
-      onCreatePlatformView: (params) => controller
-        ..connect(hitTestable: hitTestable, focusable: focusable).then((_) {
-          params.onPlatformViewCreated(controller.viewId);
+      onCreatePlatformView: (params) => widget.controller
+        ..connect(
+          hitTestable: widget.hitTestable,
+          focusable: widget.focusable,
+          viewInsets: widget.viewInsets,
+        ).then((_) {
+          params.onPlatformViewCreated(widget.controller.viewId);
         }),
       surfaceFactory: (context, controller) {
         return PlatformViewSurface(
           gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
           controller: controller,
-          hitTestBehavior: hitTestable
+          hitTestBehavior: widget.hitTestable
               ? PlatformViewHitTestBehavior.opaque
               : PlatformViewHitTestBehavior.transparent,
         );
diff --git a/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view_controller.dart b/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view_controller.dart
index 90de812..22bd9f9 100644
--- a/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view_controller.dart
+++ b/sdk/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view_controller.dart
@@ -4,6 +4,7 @@
 
 // ignore_for_file: avoid_as, unnecessary_null_comparison
 
+import 'dart:async';
 import 'dart:io';
 import 'dart:ui';
 
@@ -53,8 +54,14 @@
   @visibleForTesting
   MethodChannel get platformViewChannel => _platformViewChannel;
 
-  // Set to true if connected to underlying child view.
-  bool _connected = false;
+  // The [Completer] that is completed when the platform view is connected.
+  var _whenConnected = Completer();
+
+  /// The future that completes when the platform view is connected.
+  Future get whenConnected => _whenConnected.future;
+
+  /// Returns true when platform view is connected.
+  bool get connected => _whenConnected.isCompleted;
 
   /// Constructor.
   FuchsiaViewController({
@@ -69,18 +76,22 @@
   ///
   /// Called by [FuchsiaView] when the platform view is ready to be initialized
   /// and should not be called directly.
-  Future<void> connect({bool hitTestable = true, bool focusable = true}) async {
-    if (_connected) return;
+  Future<void> connect({
+    bool hitTestable = true,
+    bool focusable = true,
+    Rect viewInsets = Rect.zero,
+  }) async {
+    if (_whenConnected.isCompleted) return;
 
     // Setup callbacks for receiving view events.
     platformViewChannel.setMethodCallHandler((call) async {
       switch (call.method) {
         case 'View.viewConnected':
-          _connected = true;
+          _whenConnected.complete();
           onViewConnected?.call(this);
           break;
         case 'View.viewDisconnected':
-          _connected = false;
+          _whenConnected = Completer();
           onViewDisconnected?.call(this);
           break;
         case 'View.viewStateChanged':
@@ -96,6 +107,12 @@
       'viewId': viewId,
       'hitTestable': hitTestable,
       'focusable': focusable,
+      'viewInsetsLTRB': <double>[
+        viewInsets.left,
+        viewInsets.top,
+        viewInsets.right,
+        viewInsets.bottom
+      ],
     };
     return platformViewChannel.invokeMethod('View.create', args);
   }
@@ -114,6 +131,29 @@
     onViewDisconnected?.call(this);
   }
 
+  /// Updates properties on the platform view given it's [viewId].
+  ///
+  /// Called by [FuchsiaView] when the [focusable] or [hitTestable] or
+  /// [viewInsets] properties are changed.
+  Future<void> update({
+    bool focusable = true,
+    bool hitTestable = true,
+    Rect viewInsets = Rect.zero,
+  }) async {
+    final args = <String, dynamic>{
+      'viewId': viewId,
+      'hitTestable': hitTestable,
+      'focusable': focusable,
+      'viewInsetsLTRB': <double>[
+        viewInsets.left,
+        viewInsets.top,
+        viewInsets.right,
+        viewInsets.bottom
+      ],
+    };
+    return platformViewChannel.invokeMethod('View.update', args);
+  }
+
   /// Requests that focus be transferred to the remote Scene represented by
   /// this connection.
   Future<void> requestFocus(int viewRef) async {
diff --git a/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_connection_test.dart b/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_connection_test.dart
index 112c8cf..9407ffd 100644
--- a/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_connection_test.dart
+++ b/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_connection_test.dart
@@ -31,6 +31,7 @@
       'viewId': 42,
       'hitTestable': true,
       'focusable': true,
+      'viewInsetsLTRB': [0, 0, 0, 0],
     }));
 
     final methodCallback =
diff --git a/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_controller_test.dart b/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_controller_test.dart
index f2bdfa3..a130ef8 100644
--- a/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_controller_test.dart
+++ b/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_controller_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui';
+
 import 'package:flutter/gestures.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -27,6 +29,18 @@
       'viewId': 42,
       'hitTestable': true,
       'focusable': true,
+      'viewInsetsLTRB': [0, 0, 0, 0],
+    }));
+
+    await controller.update(
+        focusable: false,
+        hitTestable: false,
+        viewInsets: Rect.fromLTRB(10, 10, 20, 30));
+    verify(controller.platformViewChannel.invokeMethod('View.update', {
+      'viewId': 42,
+      'hitTestable': false,
+      'focusable': false,
+      'viewInsetsLTRB': [10, 10, 20, 30],
     }));
 
     final methodCallback =
diff --git a/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_test.dart b/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_test.dart
index 9325d9f..0cf56ae 100644
--- a/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_test.dart
+++ b/sdk/dart/fuchsia_scenic_flutter/test/fuchsia_view_test.dart
@@ -14,6 +14,7 @@
     final controller = MockFuchsiaViewController();
     final completer = Completer();
     when(controller.viewId).thenReturn(42);
+    when(controller.whenConnected).thenAnswer((_) => Future<bool>.value(false));
     when(controller.connect()).thenAnswer((_) => completer.future);
 
     await tester.pumpWidget(
@@ -28,12 +29,27 @@
     await tester.pumpAndSettle();
 
     verify(controller.connect(hitTestable: true, focusable: true));
+
+    // Change properties on the view.
+    when(controller.connected).thenReturn(true);
+
+    await tester.pumpWidget(
+      Center(
+        child: SizedBox(
+          child: FuchsiaView(controller: controller, hitTestable: false),
+        ),
+      ),
+    );
+    await tester.pumpAndSettle();
+
+    verify(controller.update(hitTestable: false));
   });
 
   testWidgets('FuchsiaView with args', (tester) async {
     final controller = MockFuchsiaViewController();
     final completer = Completer();
     when(controller.viewId).thenReturn(42);
+    when(controller.whenConnected).thenAnswer((_) => Future<bool>.value(false));
     when(controller.connect(hitTestable: false, focusable: false))
         .thenAnswer((_) => completer.future);