Skip to content

Commit

Permalink
Forwards Flutter View to platform views and detaches when needed. (fl…
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-carroll authored Sep 26, 2019
1 parent d8d0d3f commit 5b952f2
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package io.flutter.plugin.platform;

import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.view.View;

/**
Expand All @@ -16,13 +17,50 @@ public interface PlatformView {
*/
View getView();

/**
* Called by the {@link FlutterEngine} that owns this {@code PlatformView} when
* the Android {@link View} responsible for rendering a Flutter UI is associated
* with the {@link FlutterEngine}.
*
* <p>This means that our associated {@link FlutterEngine} can now render a UI and
* interact with the user.
*
* <p>Some platform views may have unusual dependencies on the {@link View} that
* renders Flutter UIs, such as unique keyboard interactions. That {@link View}
* is provided here for those purposes. Use of this {@link View} should be avoided
* if it is not absolutely necessary, because depending on this {@link View} will
* tend to make platform view code more brittle to future changes.
*/
// Default interface methods are supported on all min SDK versions of Android.
@SuppressLint("NewApi")
default void onFlutterViewAttached(@NonNull View flutterView) {}

/**
* Called by the {@link FlutterEngine} that owns this {@code PlatformView} when
* the Android {@link View} responsible for rendering a Flutter UI is detached
* and disassociated from the {@link FlutterEngine}.
*
* <p>This means that our associated {@link FlutterEngine} no longer has a rendering
* surface, or a user interaction surface of any kind.
*
* <p>This platform view must release any references related to the Android {@link View}
* that was provided in {@link #onFlutterViewAttached(View)}.
*/
// Default interface methods are supported on all min SDK versions of Android.
@SuppressLint("NewApi")
default void onFlutterViewDetached() {}

/**
* Dispose this platform view.
*
* <p>The {@link PlatformView} object is unusable after this method is called.
*
* <p>Plugins implementing {@link PlatformView} must clear all references to the View object and the PlatformView
* after this method is called. Failing to do so will result in a memory leak.
*
* <p>References related to the Android {@link View} attached in
* {@link #onFlutterViewAttached(View)} must be released in {@code dispose()} to avoid memory
* leaks.
*/
void dispose();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.content.Context;
import android.os.Build;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.util.DisplayMetrics;
import android.support.annotation.NonNull;
import android.util.Log;
Expand Down Expand Up @@ -44,6 +45,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// The context of the Activity or Fragment hosting the render target for the Flutter engine.
private Context context;

// The View currently rendering the Flutter UI associated with these platform views.
private View flutterView;

// The texture registry maintaining the textures into which the embedded views will be rendered.
private TextureRegistry textureRegistry;

Expand All @@ -55,7 +59,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// The accessibility bridge to which accessibility events form the platform views will be dispatched.
private final AccessibilityEventsDelegate accessibilityEventsDelegate;

private final HashMap<Integer, VirtualDisplayController> vdControllers;
// TODO(mattcarroll): Refactor overall platform views to facilitate testing and then make
// this private. This is visible as a hack to facilitate testing. This was deemed the least
// bad option at the time of writing.
@VisibleForTesting
/* package */ final HashMap<Integer, VirtualDisplayController> vdControllers;

// Maps a virtual display's context to the platform view hosted in this virtual display.
// Since each virtual display has it's unique context this allows associating any view with the platform view that
Expand Down Expand Up @@ -115,6 +123,12 @@ public long createPlatformView(@NonNull PlatformViewsChannel.PlatformViewCreatio
+ request.viewType + " with id: " + request.viewId);
}

// If our FlutterEngine is already attached to a Flutter UI, provide that Android
// View to this new platform view.
if (flutterView != null) {
vdController.onFlutterViewAttached(flutterView);
}

vdControllers.put(request.viewId, vdController);
View platformView = vdController.getView();
platformView.setLayoutDirection(request.direction);
Expand Down Expand Up @@ -290,6 +304,37 @@ public void detach() {
textureRegistry = null;
}

/**
* This {@code PlatformViewsController} and its {@code FlutterEngine} is now attached to
* an Android {@code View} that renders a Flutter UI.
*/
public void attachToView(@NonNull View flutterView) {
this.flutterView = flutterView;

// Inform all existing platform views that they are now associated with
// a Flutter View.
for (VirtualDisplayController controller : vdControllers.values()) {
controller.onFlutterViewAttached(flutterView);
}
}

/**
* This {@code PlatformViewController} and its {@code FlutterEngine} are no longer attached
* to an Android {@code View} that renders a Flutter UI.
* <p>
* All platform views controlled by this {@code PlatformViewController} will be detached
* from the previously attached {@code View}.
*/
public void detachFromView() {
this.flutterView = null;

// Inform all existing platform views that they are no longer associated with
// a Flutter View.
for (VirtualDisplayController controller : vdControllers.values()) {
controller.onFlutterViewDetached();
}
}

@Override
public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) {
accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.Surface;
import android.view.View;
Expand Down Expand Up @@ -159,6 +160,26 @@ public void dispose() {
textureEntry.release();
}

/**
* See {@link PlatformView#onFlutterViewAttached(View)}
*/
/*package*/ void onFlutterViewAttached(@NonNull View flutterView) {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onFlutterViewAttached(flutterView);
}

/**
* See {@link PlatformView#onFlutterViewDetached()}
*/
/*package*/ void onFlutterViewDetached() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onFlutterViewDetached();
}

/*package*/ void onInputConnectionLocked() {
if (presentation == null || presentation.getView() == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.flutter.plugin.platform;

import android.view.View;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@Config(manifest=Config.NONE)
@RunWith(RobolectricTestRunner.class)
public class PlatformViewsControllerTest {
@Test
public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() {
// Setup test structure.
// Create a fake View that represents the View that renders a Flutter UI.
View fakeFlutterView = new View(RuntimeEnvironment.systemContext);

// Create fake VirtualDisplayControllers. This requires internal knowledge of
// PlatformViewsController. We know that all PlatformViewsController does is
// forward view attachment/detachment calls to it's VirtualDisplayControllers.
//
// TODO(mattcarroll): once PlatformViewsController is refactored into testable
// pieces, remove this test and avoid verifying private behavior.
VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class);
VirtualDisplayController fakeVdController2 = mock(VirtualDisplayController.class);

// Create the PlatformViewsController that is under test.
PlatformViewsController platformViewsController = new PlatformViewsController();

// Manually inject fake VirtualDisplayControllers into the PlatformViewsController.
platformViewsController.vdControllers.put(0, fakeVdController1);
platformViewsController.vdControllers.put(1, fakeVdController1);

// Execute test & verify results.
// Attach PlatformViewsController to the fake Flutter View.
platformViewsController.attachToView(fakeFlutterView);

// Verify that all virtual display controllers were notified of View attachment.
verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController1, never()).onFlutterViewDetached();
verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController2, never()).onFlutterViewDetached();

// Detach PlatformViewsController from the fake Flutter View.
platformViewsController.detachFromView();

// Verify that all virtual display controllers were notified of the View detachment.
verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController1, times(1)).onFlutterViewDetached();
verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController2, times(1)).onFlutterViewDetached();
}
}

0 comments on commit 5b952f2

Please sign in to comment.