Add a PlatformViewRenderTarget abstraction (flutter/engine#43813)

- Introduce PlatformViewRenderTarget interface.
- Refactor VirtualDisplayController and PlatformViewWrapper to extract
SurfaceTexturePlatformViewRenderTarget into a separate class.

In a future CL I will add an ImageReaderPlatformViewRenderTarget which
will enable Platform Views on Impeller/Vulkan.

Tracking issue: https://github.com/flutter/flutter/issues/130892
This commit is contained in:
John McCutchan 2023-07-19 20:26:21 -07:00 committed by GitHub
parent 4cb94272fd
commit b3945f7706
9 changed files with 460 additions and 338 deletions

View File

@ -2435,6 +2435,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platf
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java + ../../../flutter/LICENSE
@ -5136,10 +5137,12 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java

View File

@ -297,10 +297,12 @@ android_java_sources = [
"io/flutter/plugin/platform/PlatformViewFactory.java",
"io/flutter/plugin/platform/PlatformViewRegistry.java",
"io/flutter/plugin/platform/PlatformViewRegistryImpl.java",
"io/flutter/plugin/platform/PlatformViewRenderTarget.java",
"io/flutter/plugin/platform/PlatformViewWrapper.java",
"io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java",
"io/flutter/plugin/platform/PlatformViewsController.java",
"io/flutter/plugin/platform/SingleViewPresentation.java",
"io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java",
"io/flutter/plugin/platform/VirtualDisplayController.java",
"io/flutter/util/HandlerCompat.java",
"io/flutter/util/PathUtils.java",

View File

@ -0,0 +1,45 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugin.platform;
import android.graphics.Canvas;
import android.view.Surface;
/**
* A PlatformViewRenderTarget interface allows an Android Platform View to be rendered into an
* offscreen buffer (usually a texture is involved) that the engine can compose into the
* FlutterView.
*/
public interface PlatformViewRenderTarget {
// Called when the render target should be resized.
public void resize(int width, int height);
// Returns the currently specified width.
public int getWidth();
// Returns the currently specified height.
public int getHeight();
// Forwards call to Surface returned by getSurface.
// NOTE: If this returns null the RenderTarget is "full" and has no room for a
// new frame.
Canvas lockHardwareCanvas();
// Forwards call to Surface returned by getSurface.
// NOTE: Must be called if lockHardwareCanvas returns a non-null Canvas.
void unlockCanvasAndPost(Canvas canvas);
// The id of this render target.
public long getId();
// Releases backing resources.
public void release();
// Returns true in the case that backing resource have been released.
public boolean isReleased();
// Returns the Surface to be rendered on to.
public Surface getSurface();
}

View File

@ -4,8 +4,6 @@
package io.flutter.plugin.platform;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
@ -14,10 +12,7 @@ import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
@ -30,85 +25,30 @@ import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.util.ViewUtils;
import io.flutter.view.TextureRegistry;
import java.util.concurrent.atomic.AtomicLong;
/**
* Wraps a platform view to intercept gestures and project this view onto a {@link SurfaceTexture}.
* Wraps a platform view to intercept gestures and project this view onto a {@link
* PlatformViewRenderTarget}.
*
* <p>An Android platform view is composed by the engine using a {@code TextureLayer}. The view is
* embeded to the Android view hierarchy like a normal view, but it's projected onto a {@link
* SurfaceTexture}, so it can be efficiently composed by the engine.
* PlatformViewRenderTarget}, so it can be efficiently composed by the engine.
*
* <p>Since the view is in the Android view hierarchy, keyboard and accessibility interactions
* behave normally.
*/
@TargetApi(23)
class PlatformViewWrapper extends FrameLayout {
public class PlatformViewWrapper extends FrameLayout {
private static final String TAG = "PlatformViewWrapper";
private int prevLeft;
private int prevTop;
private int left;
private int top;
private int bufferWidth;
private int bufferHeight;
private SurfaceTexture tx;
private Surface surface;
private AndroidTouchProcessor touchProcessor;
private PlatformViewRenderTarget renderTarget;
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
private final AtomicLong pendingFramesCount = new AtomicLong(0L);
private final TextureRegistry.OnFrameConsumedListener frameConsumedListener =
new TextureRegistry.OnFrameConsumedListener() {
@Override
public void onFrameConsumed() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.decrementAndGet();
}
}
};
private boolean shouldRecreateSurfaceForLowMemory = false;
private final TextureRegistry.OnTrimMemoryListener trimMemoryListener =
new TextureRegistry.OnTrimMemoryListener() {
@Override
public void onTrimMemory(int level) {
// When a memory pressure warning is received and the level equal {@code
// ComponentCallbacks2.TRIM_MEMORY_COMPLETE}, the Android system releases the underlying
// surface. If we continue to use the surface (e.g., call lockHardwareCanvas), a crash
// occurs, and we found that this crash appeared on Android10 and above.
// See https://github.com/flutter/flutter/issues/103870 for more details.
//
// Here our workaround is to recreate the surface before using it.
if (level == TRIM_MEMORY_COMPLETE && Build.VERSION.SDK_INT >= 29) {
shouldRecreateSurfaceForLowMemory = true;
}
}
};
private void onFrameProduced() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.incrementAndGet();
}
}
private void recreateSurfaceIfNeeded() {
if (shouldRecreateSurfaceForLowMemory) {
if (surface != null) {
surface.release();
}
surface = createSurface(tx);
shouldRecreateSurfaceForLowMemory = false;
}
}
private boolean shouldDrawToSurfaceNow() {
if (Build.VERSION.SDK_INT == 29) {
return pendingFramesCount.get() <= 0L;
}
return true;
}
private ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
public PlatformViewWrapper(@NonNull Context context) {
super(context);
@ -118,9 +58,13 @@ class PlatformViewWrapper extends FrameLayout {
public PlatformViewWrapper(
@NonNull Context context, @NonNull TextureRegistry.SurfaceTextureEntry textureEntry) {
this(context);
textureEntry.setOnFrameConsumedListener(frameConsumedListener);
textureEntry.setOnTrimMemoryListener(trimMemoryListener);
setTexture(textureEntry.surfaceTexture());
this.renderTarget = new SurfaceTexturePlatformViewRenderTarget(textureEntry);
}
public PlatformViewWrapper(
@NonNull Context context, @NonNull PlatformViewRenderTarget renderTarget) {
this(context);
this.renderTarget = renderTarget;
}
/**
@ -132,62 +76,6 @@ class PlatformViewWrapper extends FrameLayout {
touchProcessor = newTouchProcessor;
}
/**
* Sets the texture where the view is projected onto.
*
* <p>{@link PlatformViewWrapper} doesn't take ownership of the {@link SurfaceTexture}. As a
* result, the caller is responsible for releasing the texture.
*
* <p>{@link io.flutter.view.TextureRegistry} is responsible for creating and registering textures
* in the engine. Therefore, the engine is responsible for also releasing the texture.
*
* @param newTx The texture where the view is projected onto.
*/
@SuppressLint("NewApi")
public void setTexture(@Nullable SurfaceTexture newTx) {
if (Build.VERSION.SDK_INT < 23) {
Log.e(
TAG,
"Platform views cannot be displayed below API level 23. "
+ "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle.");
return;
}
tx = newTx;
if (bufferWidth > 0 && bufferHeight > 0) {
tx.setDefaultBufferSize(bufferWidth, bufferHeight);
}
if (surface != null) {
surface.release();
}
surface = createSurface(newTx);
// Fill the entire canvas with a transparent color.
// As a result, the background color of the platform view container is displayed
// to the user until the platform view draws its first frame.
final Canvas canvas = surface.lockHardwareCanvas();
try {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
onFrameProduced();
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
@NonNull
@VisibleForTesting
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return new Surface(tx);
}
/** Returns the texture where the view is projected. */
@Nullable
public SurfaceTexture getTexture() {
return tx;
}
/**
* Sets the layout parameters for this view.
*
@ -200,37 +88,31 @@ class PlatformViewWrapper extends FrameLayout {
top = params.topMargin;
}
/**
* Sets the size of the image buffer.
*
* @param width The width of the screen buffer.
* @param height The height of the screen buffer.
*/
public void setBufferSize(int width, int height) {
bufferWidth = width;
bufferHeight = height;
if (tx != null) {
tx.setDefaultBufferSize(width, height);
public void resizeRenderTarget(int width, int height) {
if (renderTarget != null) {
renderTarget.resize(width, height);
}
}
/** Returns the image buffer width. */
public int getBufferWidth() {
return bufferWidth;
public int getRenderTargetWidth() {
if (renderTarget != null) {
return renderTarget.getWidth();
}
return 0;
}
/** Returns the image buffer height. */
public int getBufferHeight() {
return bufferHeight;
public int getRenderTargetHeight() {
if (renderTarget != null) {
return renderTarget.getHeight();
}
return 0;
}
/** Releases the surface. */
/** Releases resources. */
public void release() {
// Don't release the texture.
tx = null;
if (surface != null) {
surface.release();
surface = null;
if (renderTarget != null) {
renderTarget.release();
renderTarget = null;
}
}
@ -271,42 +153,26 @@ class PlatformViewWrapper extends FrameLayout {
@Override
@SuppressLint("NewApi")
public void draw(Canvas canvas) {
if (surface == null) {
if (renderTarget == null) {
super.draw(canvas);
Log.e(TAG, "Platform view cannot be composed without a surface.");
Log.e(TAG, "Platform view cannot be composed without a RenderTarget.");
return;
}
if (!surface.isValid()) {
Log.e(TAG, "Invalid surface. The platform view cannot be displayed.");
return;
}
if (tx == null || tx.isReleased()) {
Log.e(TAG, "Invalid texture. The platform view cannot be displayed.");
return;
}
// We've observed on Android Q that we have to wait for the consumer of {@link SurfaceTexture}
// to consume the last image before continuing to draw, otherwise subsequent calls to
// {@code dequeueBuffer} to request a free buffer from the {@link BufferQueue} will fail.
// See https://github.com/flutter/flutter/issues/98722
if (!shouldDrawToSurfaceNow()) {
// If there are still frames that are not consumed, we will draw them next time.
final Canvas targetCanvas = renderTarget.lockHardwareCanvas();
if (targetCanvas == null) {
// Cannot render right now.
invalidate();
} else {
// We try to recreate the surface before using it to avoid the crash:
// https://github.com/flutter/flutter/issues/103870
recreateSurfaceIfNeeded();
return;
}
try {
// Fill the render target with transparent pixels. This is needed for platform views that
// expect a transparent background.
targetCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Override the canvas that this subtree of views will use to draw.
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
try {
// Clear the current pixels in the canvas.
// This helps when a WebView renders an HTML document with transparent background.
surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
super.draw(surfaceCanvas);
onFrameProduced();
} finally {
surface.unlockCanvasAndPost(surfaceCanvas);
}
super.draw(targetCanvas);
} finally {
renderTarget.unlockCanvasAndPost(targetCanvas);
}
}
@ -338,6 +204,11 @@ class PlatformViewWrapper extends FrameLayout {
return touchProcessor.onTouchEvent(event, screenMatrix);
}
@VisibleForTesting
public ViewTreeObserver.OnGlobalFocusChangeListener getActiveFocusListener() {
return this.activeFocusListener;
}
public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) {
unsetOnDescendantFocusChangeListener();
final ViewTreeObserver observer = getViewTreeObserver();

View File

@ -341,8 +341,8 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
context == null ? originalDisplayDensity : getDisplayDensity();
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(vdController.getBufferWidth(), displayDensity),
toLogicalPixels(vdController.getBufferHeight(), displayDensity)));
toLogicalPixels(vdController.getRenderTargetWidth(), displayDensity),
toLogicalPixels(vdController.getRenderTargetHeight(), displayDensity)));
});
return;
}
@ -361,9 +361,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// Resizing the texture causes pixel stretching since the size of the GL texture used in
// the engine
// is set by the framework, but the texture buffer size is set by the platform down below.
if (physicalWidth > viewWrapper.getBufferWidth()
|| physicalHeight > viewWrapper.getBufferHeight()) {
viewWrapper.setBufferSize(physicalWidth, physicalHeight);
if (physicalWidth > viewWrapper.getRenderTargetWidth()
|| physicalHeight > viewWrapper.getRenderTargetHeight()) {
viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);
}
final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams();
@ -380,8 +380,8 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
}
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(viewWrapper.getBufferWidth()),
toLogicalPixels(viewWrapper.getBufferHeight())));
toLogicalPixels(viewWrapper.getRenderTargetWidth()),
toLogicalPixels(viewWrapper.getRenderTargetHeight())));
}
@Override
@ -615,7 +615,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
textureId = textureEntry.id();
}
viewWrapper.setTouchProcessor(androidTouchProcessor);
viewWrapper.setBufferSize(physicalWidth, physicalHeight);
viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);
final FrameLayout.LayoutParams viewWrapperLayoutParams =
new FrameLayout.LayoutParams(physicalWidth, physicalHeight);

View File

@ -0,0 +1,179 @@
package io.flutter.plugin.platform;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.view.Surface;
import io.flutter.Log;
import io.flutter.view.TextureRegistry;
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
import java.util.concurrent.atomic.AtomicLong;
@TargetApi(26)
public class SurfaceTexturePlatformViewRenderTarget implements PlatformViewRenderTarget {
private static final String TAG = "SurfaceTexturePlatformViewRenderTarget";
private final AtomicLong pendingFramesCount = new AtomicLong(0L);
private void onFrameProduced() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.incrementAndGet();
}
}
private final SurfaceTextureEntry surfaceTextureEntry;
private SurfaceTexture surfaceTexture;
private Surface surface;
private int bufferWidth = 0;
private int bufferHeight = 0;
private final TextureRegistry.OnFrameConsumedListener frameConsumedListener =
new TextureRegistry.OnFrameConsumedListener() {
@Override
public void onFrameConsumed() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.decrementAndGet();
}
}
};
private boolean shouldRecreateSurfaceForLowMemory = false;
private final TextureRegistry.OnTrimMemoryListener trimMemoryListener =
new TextureRegistry.OnTrimMemoryListener() {
@Override
public void onTrimMemory(int level) {
// When a memory pressure warning is received and the level equal {@code
// ComponentCallbacks2.TRIM_MEMORY_COMPLETE}, the Android system releases the
// underlying
// surface. If we continue to use the surface (e.g., call lockHardwareCanvas), a
// crash
// occurs, and we found that this crash appeared on Android10 and above.
// See https://github.com/flutter/flutter/issues/103870 for more details.
//
// Here our workaround is to recreate the surface before using it.
if (level == TRIM_MEMORY_COMPLETE && Build.VERSION.SDK_INT >= 29) {
shouldRecreateSurfaceForLowMemory = true;
}
}
};
private void recreateSurfaceIfNeeded() {
if (!shouldRecreateSurfaceForLowMemory) {
return;
}
if (surface != null) {
surface.release();
surface = null;
}
surface = createSurface();
shouldRecreateSurfaceForLowMemory = false;
}
protected Surface createSurface() {
return new Surface(surfaceTexture);
}
private void init() {
if (bufferWidth > 0 && bufferHeight > 0) {
surfaceTexture.setDefaultBufferSize(bufferWidth, bufferHeight);
}
if (surface != null) {
surface.release();
surface = null;
}
surface = createSurface();
// Fill the entire canvas with a transparent color.
// As a result, the background color of the platform view container is displayed
// to the user until the platform view draws its first frame.
final Canvas canvas = lockHardwareCanvas();
try {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
} finally {
unlockCanvasAndPost(canvas);
}
}
/** Implementation of PlatformViewRenderTarget */
public SurfaceTexturePlatformViewRenderTarget(SurfaceTextureEntry surfaceTextureEntry) {
if (Build.VERSION.SDK_INT < 23) {
throw new UnsupportedOperationException(
"Platform views cannot be displayed below API level 23"
+ "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle.");
}
this.surfaceTextureEntry = surfaceTextureEntry;
this.surfaceTexture = surfaceTextureEntry.surfaceTexture();
surfaceTextureEntry.setOnFrameConsumedListener(frameConsumedListener);
surfaceTextureEntry.setOnTrimMemoryListener(trimMemoryListener);
init();
}
public Canvas lockHardwareCanvas() {
recreateSurfaceIfNeeded();
// We've observed on Android Q that we have to wait for the consumer of {@link
// SurfaceTexture}
// to consume the last image before continuing to draw, otherwise subsequent
// calls to
// {@code dequeueBuffer} to request a free buffer from the {@link BufferQueue}
// will fail.
// See https://github.com/flutter/flutter/issues/98722
if (Build.VERSION.SDK_INT == 29 && pendingFramesCount.get() > 0L) {
return null;
}
if (surfaceTexture == null || surfaceTexture.isReleased()) {
Log.e(TAG, "Invalid RenderTarget: null or already released SurfaceTexture");
return null;
}
onFrameProduced();
return surface.lockHardwareCanvas();
}
public void unlockCanvasAndPost(Canvas canvas) {
surface.unlockCanvasAndPost(canvas);
}
public void resize(int width, int height) {
bufferWidth = width;
bufferHeight = height;
if (surfaceTexture != null) {
surfaceTexture.setDefaultBufferSize(bufferWidth, bufferHeight);
}
}
public int getWidth() {
return bufferWidth;
}
public int getHeight() {
return bufferHeight;
}
public long getId() {
return this.surfaceTextureEntry.id();
}
public boolean isReleased() {
return surfaceTexture == null;
}
public void release() {
// Don't release the texture.
surfaceTexture = null;
if (surface != null) {
surface.release();
surface = null;
}
}
public Surface getSurface() {
recreateSurfaceIfNeeded();
return surface;
}
}

View File

@ -12,8 +12,8 @@ import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@ -33,12 +33,16 @@ class VirtualDisplayController {
int viewId,
Object createParams,
OnFocusChangeListener focusChangeListener) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
if (width == 0 || height == 0) {
return null;
}
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final PlatformViewRenderTarget renderTarget =
new SurfaceTexturePlatformViewRenderTarget(textureEntry);
// Virtual Display crashes for some PlatformViews if the width or height is bigger
// than the physical screen size. We have tried to clamp or scale down the size to prevent
// the crash, but both solutions lead to unwanted behavior because the
@ -49,14 +53,15 @@ class VirtualDisplayController {
// TODO(cyanglaz): find a way to prevent the crash without introducing size mistach betewen
// virtual display and AndroidPlatformView widget.
// https://github.com/flutter/flutter/issues/93115
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
Surface surface = new Surface(textureEntry.surfaceTexture());
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
int densityDpi = context.getResources().getDisplayMetrics().densityDpi;
renderTarget.resize(width, height);
VirtualDisplay virtualDisplay =
displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0);
displayManager.createVirtualDisplay(
"flutter-vd#" + viewId,
width,
height,
metrics.densityDpi,
renderTarget.getSurface(),
0);
if (virtualDisplay == null) {
return null;
@ -67,13 +72,11 @@ class VirtualDisplayController {
accessibilityEventsDelegate,
virtualDisplay,
view,
surface,
renderTarget,
textureEntry,
focusChangeListener,
viewId,
createParams);
controller.bufferWidth = width;
controller.bufferHeight = height;
return controller;
}
@ -82,31 +85,31 @@ class VirtualDisplayController {
private final Context context;
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final int densityDpi;
private final int viewId;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private final PlatformViewRenderTarget renderTarget;
private final OnFocusChangeListener focusChangeListener;
private final Surface surface;
private VirtualDisplay virtualDisplay;
private int bufferWidth;
private int bufferHeight;
private VirtualDisplayController(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
VirtualDisplay virtualDisplay,
PlatformView view,
Surface surface,
PlatformViewRenderTarget renderTarget,
TextureRegistry.SurfaceTextureEntry textureEntry,
OnFocusChangeListener focusChangeListener,
int viewId,
Object createParams) {
this.context = context;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.renderTarget = renderTarget;
this.textureEntry = textureEntry;
this.focusChangeListener = focusChangeListener;
this.surface = surface;
this.viewId = viewId;
this.virtualDisplay = virtualDisplay;
densityDpi = context.getResources().getDisplayMetrics().densityDpi;
this.densityDpi = context.getResources().getDisplayMetrics().densityDpi;
presentation =
new SingleViewPresentation(
context,
@ -118,33 +121,33 @@ class VirtualDisplayController {
presentation.show();
}
public int getBufferWidth() {
return bufferWidth;
public int getRenderTargetWidth() {
if (renderTarget != null) {
return renderTarget.getWidth();
}
return 0;
}
public int getBufferHeight() {
return bufferHeight;
public int getRenderTargetHeight() {
if (renderTarget != null) {
return renderTarget.getHeight();
}
return 0;
}
public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
boolean isFocused = getView().isFocused();
final SingleViewPresentation.PresentationState presentationState = presentation.detachState();
// We detach the surface to prevent it being destroyed when releasing the vd.
//
// setSurface is only available starting API 20. We could support API 19 by re-creating a new
// SurfaceTexture here. This will require refactoring the TextureRegistry to allow recycling
// texture
// entry IDs.
virtualDisplay.setSurface(null);
virtualDisplay.release();
bufferWidth = width;
bufferHeight = height;
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
DisplayManager displayManager =
final DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
renderTarget.resize(width, height);
virtualDisplay =
displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0);
displayManager.createVirtualDisplay(
"flutter-vd#" + viewId, width, height, densityDpi, renderTarget.getSurface(), 0);
final View embeddedView = getView();
// There's a bug in Android version older than O where view tree observer onDrawListeners don't

View File

@ -4,19 +4,17 @@ import static android.view.View.OnFocusChangeListener;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.spy;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
@ -42,83 +40,6 @@ public class PlatformViewWrapperTest {
verify(wrapper, times(1)).invalidate();
}
@Test
public void setTexture_writesToBuffer() {
final Surface surface = mock(Surface.class);
final PlatformViewWrapper wrapper =
new PlatformViewWrapper(ctx) {
@Override
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return surface;
}
};
final SurfaceTexture tx = mock(SurfaceTexture.class);
when(tx.isReleased()).thenReturn(false);
final Canvas canvas = mock(Canvas.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
// Test.
wrapper.setTexture(tx);
// Verify.
verify(surface, times(1)).lockHardwareCanvas();
verify(surface, times(1)).unlockCanvasAndPost(canvas);
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(canvas);
}
@Test
public void draw_writesToBuffer() {
final Surface surface = mock(Surface.class);
final PlatformViewWrapper wrapper =
new PlatformViewWrapper(ctx) {
@Override
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return surface;
}
};
wrapper.addView(
new View(ctx) {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawColor(Color.RED);
}
});
final int size = 100;
wrapper.measure(size, size);
wrapper.layout(0, 0, size, size);
final SurfaceTexture tx = mock(SurfaceTexture.class);
when(tx.isReleased()).thenReturn(false);
when(surface.lockHardwareCanvas()).thenReturn(mock(Canvas.class));
wrapper.setTexture(tx);
reset(surface);
final Canvas canvas = mock(Canvas.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
when(surface.isValid()).thenReturn(true);
// Test.
wrapper.invalidate();
wrapper.draw(new Canvas());
// Verify.
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
verify(surface, times(1)).isValid();
verify(surface, times(1)).lockHardwareCanvas();
verify(surface, times(1)).unlockCanvasAndPost(canvas);
verifyNoMoreInteractions(surface);
}
@Test
@Config(
shadows = {
@ -140,36 +61,6 @@ public class PlatformViewWrapperTest {
verify(canvas, times(1)).drawColor(Color.RED);
}
@Test
public void release() {
final Surface surface = mock(Surface.class);
final PlatformViewWrapper wrapper =
new PlatformViewWrapper(ctx) {
@Override
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return surface;
}
};
final SurfaceTexture tx = mock(SurfaceTexture.class);
when(tx.isReleased()).thenReturn(false);
final Canvas canvas = mock(Canvas.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
wrapper.setTexture(tx);
reset(surface);
reset(tx);
// Test.
wrapper.release();
// Verify.
verify(surface, times(1)).release();
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(tx);
}
@Test
public void focusChangeListener_hasFocus() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
@ -255,16 +146,16 @@ public class PlatformViewWrapperTest {
}
};
assertNull(view.activeFocusListener);
assertNull(view.getActiveFocusListener());
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
assertNotNull(view.activeFocusListener);
assertNotNull(view.getActiveFocusListener());
final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
view.activeFocusListener;
view.getActiveFocusListener();
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
assertNotNull(view.activeFocusListener);
assertNotNull(view.getActiveFocusListener());
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
}
@ -282,16 +173,16 @@ public class PlatformViewWrapperTest {
}
};
assertNull(view.activeFocusListener);
assertNull(view.getActiveFocusListener());
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
assertNotNull(view.activeFocusListener);
assertNotNull(view.getActiveFocusListener());
final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
view.activeFocusListener;
view.getActiveFocusListener();
view.unsetOnDescendantFocusChangeListener();
assertNull(view.activeFocusListener);
assertNull(view.getActiveFocusListener());
view.unsetOnDescendantFocusChangeListener();
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);

View File

@ -0,0 +1,128 @@
package io.flutter.plugin.platform;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.View;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
import org.junit.Test;
import org.junit.runner.RunWith;
@TargetApi(31)
@RunWith(AndroidJUnit4.class)
public class SurfaceTexturePlatformViewRenderTargetTest {
private final Context ctx = ApplicationProvider.getApplicationContext();
@Test
public void create_clearsTexture() {
final Canvas canvas = mock(Canvas.class);
final Surface surface = mock(Surface.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
when(surface.isValid()).thenReturn(true);
final SurfaceTexture surfaceTexture = mock(SurfaceTexture.class);
final SurfaceTextureEntry surfaceTextureEntry = mock(SurfaceTextureEntry.class);
when(surfaceTextureEntry.surfaceTexture()).thenReturn(surfaceTexture);
when(surfaceTexture.isReleased()).thenReturn(false);
// Test.
final SurfaceTexturePlatformViewRenderTarget renderTarget =
new SurfaceTexturePlatformViewRenderTarget(surfaceTextureEntry) {
@Override
protected Surface createSurface() {
return surface;
}
};
// Verify.
verify(surface, times(1)).lockHardwareCanvas();
verify(surface, times(1)).unlockCanvasAndPost(canvas);
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(canvas);
}
@Test
public void viewDraw_writesToBuffer() {
final Canvas canvas = mock(Canvas.class);
final Surface surface = mock(Surface.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
when(surface.isValid()).thenReturn(true);
final SurfaceTexture surfaceTexture = mock(SurfaceTexture.class);
final SurfaceTextureEntry surfaceTextureEntry = mock(SurfaceTextureEntry.class);
when(surfaceTextureEntry.surfaceTexture()).thenReturn(surfaceTexture);
when(surfaceTexture.isReleased()).thenReturn(false);
final SurfaceTexturePlatformViewRenderTarget renderTarget =
new SurfaceTexturePlatformViewRenderTarget(surfaceTextureEntry) {
@Override
protected Surface createSurface() {
return surface;
}
};
// Custom view.
final View platformView =
new View(ctx) {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawColor(Color.RED);
}
};
final int size = 100;
platformView.measure(size, size);
platformView.layout(0, 0, size, size);
// Test.
final Canvas c = renderTarget.lockHardwareCanvas();
platformView.draw(c);
renderTarget.unlockCanvasAndPost(c);
// Verify.
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
verify(canvas, times(1)).drawColor(Color.RED);
verify(surface, times(2)).lockHardwareCanvas();
verify(surface, times(2)).unlockCanvasAndPost(canvas);
verifyNoMoreInteractions(surface);
}
@Test
public void release() {
final Canvas canvas = mock(Canvas.class);
final Surface surface = mock(Surface.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
when(surface.isValid()).thenReturn(true);
final SurfaceTexture surfaceTexture = mock(SurfaceTexture.class);
final SurfaceTextureEntry surfaceTextureEntry = mock(SurfaceTextureEntry.class);
when(surfaceTextureEntry.surfaceTexture()).thenReturn(surfaceTexture);
when(surfaceTexture.isReleased()).thenReturn(false);
final SurfaceTexturePlatformViewRenderTarget renderTarget =
new SurfaceTexturePlatformViewRenderTarget(surfaceTextureEntry) {
@Override
protected Surface createSurface() {
return surface;
}
};
reset(surface);
reset(surfaceTexture);
// Test.
renderTarget.release();
// Verify.
verify(surface, times(1)).release();
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(surfaceTexture);
}
}