diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index eb2713c557b90..d07a4edc56d6f 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -690,6 +690,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index fc33b1ce4b474..7bd8b22322684 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -138,6 +138,7 @@ android_java_sources = [ "io/flutter/embedding/android/FlutterEngineProvider.java", "io/flutter/embedding/android/FlutterFragment.java", "io/flutter/embedding/android/FlutterFragmentActivity.java", + "io/flutter/embedding/android/FlutterImageView.java", "io/flutter/embedding/android/FlutterSplashView.java", "io/flutter/embedding/android/FlutterSurfaceView.java", "io/flutter/embedding/android/FlutterTextureView.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java new file mode 100644 index 0000000000000..48863f0d42766 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java @@ -0,0 +1,106 @@ +// 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.embedding.android; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorSpace; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.Image.Plane; +import android.media.ImageReader; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Paints a Flutter UI provided by an {@link android.media.ImageReader} onto a {@link + * android.graphics.Canvas}. + * + *

A {@code FlutterImageView} is intended for situations where a developer needs to render a + * Flutter UI, but also needs to render an interactive {@link + * io.flutter.plugin.platform.PlatformView}. + * + *

This {@code View} takes an {@link android.media.ImageReader} that provides the Flutter UI in + * an {@link android.media.Image} and renders it to the {@link android.graphics.Canvas} in {@code + * onDraw}. + */ +@SuppressLint("ViewConstructor") +@TargetApi(19) +public class FlutterImageView extends View { + private final ImageReader imageReader; + @Nullable private Image nextImage; + @Nullable private Image currentImage; + + /** + * Constructs a {@code FlutterImageView} with an {@link android.media.ImageReader} that provides + * the Flutter UI. + */ + public FlutterImageView(@NonNull Context context, @NonNull ImageReader imageReader) { + super(context, null); + this.imageReader = imageReader; + } + + /** Acquires the next image to be drawn to the {@link android.graphics.Canvas}. */ + @TargetApi(19) + public void acquireLatestImage() { + nextImage = imageReader.acquireLatestImage(); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (nextImage == null) { + return; + } + + if (currentImage != null) { + currentImage.close(); + } + currentImage = nextImage; + nextImage = null; + + if (android.os.Build.VERSION.SDK_INT >= 29) { + drawImageBuffer(canvas); + return; + } + + drawImagePlane(canvas); + } + + @TargetApi(29) + private void drawImageBuffer(@NonNull Canvas canvas) { + final HardwareBuffer buffer = currentImage.getHardwareBuffer(); + + final Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB)); + canvas.drawBitmap(bitmap, 0, 0, null); + } + + private void drawImagePlane(@NonNull Canvas canvas) { + if (currentImage == null) { + return; + } + + final Plane[] imagePlanes = currentImage.getPlanes(); + if (imagePlanes.length != 1) { + return; + } + + final Plane imagePlane = imagePlanes[0]; + final int desiredWidth = imagePlane.getRowStride() / imagePlane.getPixelStride(); + final int desiredHeight = currentImage.getHeight(); + + final Bitmap bitmap = + android.graphics.Bitmap.createBitmap( + desiredWidth, desiredHeight, android.graphics.Bitmap.Config.ARGB_8888); + + bitmap.copyPixelsFromBuffer(imagePlane.getBuffer()); + canvas.drawBitmap(bitmap, 0, 0, null); + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index c05b37bdb4e67..2fd22da61d4e1 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -79,6 +79,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC // Internal view hierarchy references. @Nullable private FlutterSurfaceView flutterSurfaceView; @Nullable private FlutterTextureView flutterTextureView; + @Nullable private FlutterImageView flutterImageView; @Nullable private RenderSurface renderSurface; private final Set flutterUiDisplayListeners = new HashSet<>(); private boolean isFlutterUiDisplayed; @@ -155,7 +156,8 @@ public FlutterView(@NonNull Context context) { /** * Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link - * #FlutterView(Context, FlutterTextureView)} instead. + * #FlutterView(Context, FlutterTextureView)} or {@link #FlutterView(Context, FlutterImageView)} + * instead. */ @Deprecated public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) { @@ -164,9 +166,12 @@ public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) { if (renderMode == RenderMode.surface) { flutterSurfaceView = new FlutterSurfaceView(context); renderSurface = flutterSurfaceView; - } else { + } else if (renderMode == RenderMode.texture) { flutterTextureView = new FlutterTextureView(context); renderSurface = flutterTextureView; + } else { + throw new IllegalArgumentException( + String.format("RenderMode not supported with this constructor: ", renderMode)); } init(); @@ -216,6 +221,18 @@ public FlutterView(@NonNull Context context, @NonNull FlutterTextureView flutter this(context, null, flutterTextureView); } + /** + * Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given + * {@link FlutterImageView} to render the Flutter UI. + * + *

{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be + * compatible with {@link PlatformViewsController}. + */ + @TargetApi(19) + public FlutterView(@NonNull Context context, @NonNull FlutterImageView flutterImageView) { + this(context, null, flutterImageView); + } + /** * Constructs a {@code FlutterView} in an XML-inflation-compliant manner. * @@ -243,9 +260,12 @@ public FlutterView( flutterSurfaceView = new FlutterSurfaceView(context, transparencyMode == TransparencyMode.transparent); renderSurface = flutterSurfaceView; - } else { + } else if (renderMode == RenderMode.texture) { flutterTextureView = new FlutterTextureView(context); renderSurface = flutterTextureView; + } else { + throw new IllegalArgumentException( + String.format("RenderMode not supported with this constructor: ", renderMode)); } init(); @@ -275,15 +295,30 @@ private FlutterView( init(); } + @TargetApi(19) + private FlutterView( + @NonNull Context context, + @Nullable AttributeSet attrs, + @NonNull FlutterImageView flutterImageView) { + super(context, attrs); + + this.flutterImageView = flutterImageView; + + init(); + } + private void init() { Log.v(TAG, "Initializing FlutterView"); if (flutterSurfaceView != null) { Log.v(TAG, "Internally using a FlutterSurfaceView."); addView(flutterSurfaceView); - } else { + } else if (flutterTextureView != null) { Log.v(TAG, "Internally using a FlutterTextureView."); addView(flutterTextureView); + } else { + Log.v(TAG, "Internally using a FlutterImageView."); + addView(flutterImageView); } // FlutterView needs to be focusable so that the InputMethodManager can interact with it. @@ -1018,7 +1053,16 @@ public enum RenderMode { * android.graphics.SurfaceTexture} are required, developers should strongly prefer the {@link * RenderMode#surface} render mode. */ - texture + texture, + /** + * {@code RenderMode}, which paints Paints a Flutter UI provided by an {@link + * android.media.ImageReader} onto a {@link android.graphics.Canvas}. This mode is not as + * performant as {@link RenderMode#surface}, but a {@code FlutterView} in this mode can handle + * full interactivity with a {@link io.flutter.plugin.platform.PlatformView}. Unless {@link + * io.flutter.plugin.platform.PlatformView}s are required developers should strongly prefer the + * {@link RenderMode#surface} render mode. + */ + image } /** diff --git a/shell/platform/android/io/flutter/embedding/android/RenderMode.java b/shell/platform/android/io/flutter/embedding/android/RenderMode.java index 3c0907bb850a3..2471396178aec 100644 --- a/shell/platform/android/io/flutter/embedding/android/RenderMode.java +++ b/shell/platform/android/io/flutter/embedding/android/RenderMode.java @@ -21,5 +21,14 @@ public enum RenderMode { * Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are * required, developers should strongly prefer the {@link #surface} render mode. */ - texture + texture, + /** + * {@code RenderMode}, which paints Paints a Flutter UI provided by an {@link + * android.media.ImageReader} onto a {@link android.graphics.Canvas}. This mode is not as + * performant as {@link RenderMode#surface}, but a {@code FlutterView} in this mode can handle + * full interactivity with a {@link io.flutter.plugin.platform.PlatformView}. Unless {@link + * io.flutter.plugin.platform.PlatformView}s are required developers should strongly prefer the + * {@link RenderMode#surface} render mode. + */ + image } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 7461674f243b0..ad29381ef273d 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -12,6 +12,7 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.media.ImageReader; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -370,6 +371,17 @@ public void systemInsetHandlesFullscreenNavbarLeft() { assertEquals(100, viewportMetricsCaptor.getValue().paddingRight); } + @Test + public void flutterImageView_acquiresImageAndInvalidates() { + final ImageReader mockReader = mock(ImageReader.class); + final FlutterImageView imageView = + spy(new FlutterImageView(RuntimeEnvironment.application, mockReader)); + + imageView.acquireLatestImage(); + verify(mockReader, times(1)).acquireLatestImage(); + verify(imageView, times(1)).invalidate(); + } + /* * A custom shadow that reports fullscreen flag for system UI visibility */