diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index 89e199fccfd56..9735854f41842 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -377,12 +377,6 @@ RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) { TRACE_EVENT0("flutter", "Rasterizer::DrawToSurface"); FML_DCHECK(surface_); - auto frame = surface_->AcquireFrame(layer_tree.frame_size()); - - if (frame == nullptr) { - return RasterStatus::kFailed; - } - // There is no way for the compositor to know how long the layer tree // construction took. Fortunately, the layer tree does. Grab that time // for instrumentation. @@ -398,6 +392,16 @@ RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) { embedder_root_canvas = external_view_embedder->GetRootCanvas(); } + // On Android, the external view embedder deletes surfaces in `BeginFrame`. + // + // Deleting a surface also clears the GL context. Therefore, acquire the + // frame after calling `BeginFrame` as this operation resets the GL context. + auto frame = surface_->AcquireFrame(layer_tree.frame_size()); + + if (frame == nullptr) { + return RasterStatus::kFailed; + } + // If the external view embedder has specified an optional root surface, the // root surface transformation is set by the embedder instead of // having to apply it here. diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc index e6301898ff581..e94b70318c922 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -85,18 +85,20 @@ bool AndroidExternalViewEmbedder::SubmitFrame( std::unordered_map> overlay_layers; std::unordered_map> pictures; SkCanvas* background_canvas = frame->SkiaCanvas(); + auto current_frame_view_count = composition_order_.size(); // Restore the clip context after exiting this method since it's changed // below. SkAutoCanvasRestore save(background_canvas, /*doSave=*/true); - for (size_t i = 0; i < composition_order_.size(); i++) { + for (size_t i = 0; i < current_frame_view_count; i++) { int64_t view_id = composition_order_[i]; sk_sp picture = picture_recorders_.at(view_id)->finishRecordingAsPicture(); FML_CHECK(picture); pictures.insert({view_id, picture}); + overlay_layers.insert({view_id, {}}); sk_sp rtree = view_rtrees_.at(view_id); @@ -142,8 +144,14 @@ bool AndroidExternalViewEmbedder::SubmitFrame( background_canvas->drawPicture(pictures.at(view_id)); } // Submit the background canvas frame before switching the GL context to - // the surfaces above. - frame->Submit(); + // the overlay surfaces. + // + // Skip a frame if the embedding is switching surfaces. + auto should_submit_current_frame = + previous_frame_view_count_ > 0 || current_frame_view_count == 0; + if (should_submit_current_frame) { + frame->Submit(); + } for (int64_t view_id : composition_order_) { SkRect view_rect = GetViewRect(view_id); @@ -161,12 +169,15 @@ bool AndroidExternalViewEmbedder::SubmitFrame( params.mutatorsStack() // ); for (const SkRect& overlay_rect : overlay_layers.at(view_id)) { - CreateSurfaceIfNeeded(context, // - view_id, // - pictures.at(view_id), // - overlay_rect // - ) - ->Submit(); + std::unique_ptr frame = + CreateSurfaceIfNeeded(context, // + view_id, // + pictures.at(view_id), // + overlay_rect // + ); + if (should_submit_current_frame) { + frame->Submit(); + } } } return true; @@ -233,6 +244,8 @@ SkCanvas* AndroidExternalViewEmbedder::GetRootCanvas() { } void AndroidExternalViewEmbedder::Reset() { + previous_frame_view_count_ = composition_order_.size(); + composition_order_.clear(); picture_recorders_.clear(); } @@ -244,6 +257,9 @@ void AndroidExternalViewEmbedder::BeginFrame( double device_pixel_ratio, fml::RefPtr raster_thread_merger) { Reset(); + + // The surface size changed. Therefore, destroy existing surfaces as + // the existing surfaces in the pool can't be recycled. if (frame_size_ != frame_size) { surface_pool_->DestroyLayers(jni_facade_); } diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.h b/shell/platform/android/external_view_embedder/external_view_embedder.h index d1ef3821e281f..7395c13055d91 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.h +++ b/shell/platform/android/external_view_embedder/external_view_embedder.h @@ -124,6 +124,9 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { // The r-tree that captures the operations for the picture recorders. std::unordered_map> view_rtrees_; + // The number of platform views in the previous frame. + int64_t previous_frame_view_count_; + // Resets the state. void Reset(); diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index cf745b53e7b1a..1ad2be5a077a7 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -250,7 +250,7 @@ TEST(AndroidExternalViewEmbedder, PlatformViewRect__ChangedParams) { ASSERT_EQ(SkRect::MakeXYWH(75, 90, 105, 120), embedder->GetViewRect(view_id)); } -TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { +TEST(AndroidExternalViewEmbedder, SubmitFrame) { auto jni_mock = std::make_shared(); auto android_context = std::make_shared(AndroidRenderingAPI::kSoftware); @@ -294,6 +294,27 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { auto raster_thread_merger = GetThreadMergerFromPlatformThread(); // ------------------ First frame ------------------ // + { + auto did_submit_frame = false; + auto surface_frame = std::make_unique( + SkSurface::MakeNull(1000, 1000), false, + [&did_submit_frame](const SurfaceFrame& surface_frame, + SkCanvas* canvas) mutable { + if (canvas != nullptr) { + did_submit_frame = true; + } + return true; + }); + + embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); + // Submits frame if no Android view in the current frame. + EXPECT_TRUE(did_submit_frame); + + EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); + embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); + } + + // ------------------ Second frame ------------------ // { EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()); embedder->BeginFrame(frame_size, nullptr, 1.5, raster_thread_merger); @@ -338,18 +359,26 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { EXPECT_CALL(*jni_mock, FlutterViewDisplayOverlaySurface(0, 50, 50, 200, 200)); - auto surface_frame = - std::make_unique(SkSurface::MakeNull(1000, 1000), false, - [](const SurfaceFrame& surface_frame, - SkCanvas* canvas) { return true; }); + auto did_submit_frame = false; + auto surface_frame = std::make_unique( + SkSurface::MakeNull(1000, 1000), false, + [&did_submit_frame](const SurfaceFrame& surface_frame, + SkCanvas* canvas) mutable { + if (canvas != nullptr) { + did_submit_frame = true; + } + return true; + }); embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); + // Doesn't submit frame if there aren't Android views in the previous frame. + EXPECT_FALSE(did_submit_frame); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); } - // ------------------ Second frame ------------------ // + // ------------------ Third frame ------------------ // { EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()); embedder->BeginFrame(frame_size, nullptr, 1.5, raster_thread_merger); @@ -392,11 +421,19 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { EXPECT_CALL(*jni_mock, FlutterViewDisplayOverlaySurface(0, 50, 50, 200, 200)); - auto surface_frame = - std::make_unique(SkSurface::MakeNull(1000, 1000), false, - [](const SurfaceFrame& surface_frame, - SkCanvas* canvas) { return true; }); + auto did_submit_frame = false; + auto surface_frame = std::make_unique( + SkSurface::MakeNull(1000, 1000), false, + [&did_submit_frame](const SurfaceFrame& surface_frame, + SkCanvas* canvas) mutable { + if (canvas != nullptr) { + did_submit_frame = true; + } + return true; + }); embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); + // Submits frame if there are Android views in the previous frame. + EXPECT_TRUE(did_submit_frame); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); @@ -423,15 +460,83 @@ TEST(AndroidExternalViewEmbedder, DoesNotCallJNIPlatformThreadOnlyMethods) { TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { auto jni_mock = std::make_shared(); - auto embedder = - std::make_unique(nullptr, jni_mock, nullptr); + auto android_context = + std::make_shared(AndroidRenderingAPI::kSoftware); + auto window = fml::MakeRefCounted(nullptr); + auto gr_context = GrContext::MakeMock(nullptr); + auto frame_size = SkISize::Make(1000, 1000); + auto surface_factory = + [gr_context, window, frame_size]( + std::shared_ptr android_context, + std::shared_ptr jni_facade) { + auto surface_frame_1 = std::make_unique( + SkSurface::MakeNull(1000, 1000), false, + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + return true; + }); + + auto surface_mock = std::make_unique(); + EXPECT_CALL(*surface_mock, AcquireFrame(frame_size)) + .WillOnce(Return(ByMove(std::move(surface_frame_1)))); + + auto android_surface_mock = std::make_unique(); + EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); + + EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())) + .WillOnce(Return(ByMove(std::move(surface_mock)))); + + EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); + + return android_surface_mock; + }; + + auto embedder = std::make_unique( + android_context, jni_mock, surface_factory); auto raster_thread_merger = GetThreadMergerFromPlatformThread(); - ASSERT_FALSE(raster_thread_merger->IsMerged()); - embedder->BeginFrame(SkISize::Make(10, 20), nullptr, 1.0, - raster_thread_merger); + // ------------------ First frame ------------------ // + { + EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()); + embedder->BeginFrame(frame_size, nullptr, 1.5, raster_thread_merger); + + // Add an Android view. + MutatorsStack stack1; + // TODO(egarciad): Investigate why Flow applies the device pixel ratio to + // the offsetPixels, but not the sizePoints. + auto view_params_1 = std::make_unique( + SkMatrix(), SkSize::Make(200, 200), stack1); + + embedder->PrerollCompositeEmbeddedView(0, std::move(view_params_1)); + + // This simulates Flutter UI that intersects with the Android view. + embedder->CompositeEmbeddedView(0)->drawRect( + SkRect::MakeXYWH(50, 50, 200, 200), SkPaint()); + + // Create a new overlay surface. + EXPECT_CALL(*jni_mock, FlutterViewCreateOverlaySurface()) + .WillOnce(Return( + ByMove(std::make_unique( + 0, window)))); + // The JNI call to display the Android view. + EXPECT_CALL(*jni_mock, FlutterViewOnDisplayPlatformView(0, 0, 0, 200, 200, + 300, 300, stack1)); + EXPECT_CALL(*jni_mock, + FlutterViewDisplayOverlaySurface(0, 50, 50, 200, 200)); + + auto surface_frame = + std::make_unique(SkSurface::MakeNull(1000, 1000), false, + [](const SurfaceFrame& surface_frame, + SkCanvas* canvas) { return true; }); + embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); + + EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); + embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); + } + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()); + EXPECT_CALL(*jni_mock, FlutterViewBeginFrame()); + // Change the frame size. embedder->BeginFrame(SkISize::Make(30, 40), nullptr, 1.0, raster_thread_merger); } diff --git a/shell/platform/android/external_view_embedder/surface_pool.cc b/shell/platform/android/external_view_embedder/surface_pool.cc index 934b153fadb1d..cefd5e668a40f 100644 --- a/shell/platform/android/external_view_embedder/surface_pool.cc +++ b/shell/platform/android/external_view_embedder/surface_pool.cc @@ -72,9 +72,11 @@ void SurfacePool::RecycleLayers() { void SurfacePool::DestroyLayers( std::shared_ptr jni_facade) { + if (layers_.size() > 0) { + jni_facade->FlutterViewDestroyOverlaySurfaces(); + } layers_.clear(); available_layer_index_ = 0; - jni_facade->FlutterViewDestroyOverlaySurfaces(); } std::vector> SurfacePool::GetUnusedLayers() { diff --git a/shell/platform/android/external_view_embedder/surface_pool_unittests.cc b/shell/platform/android/external_view_embedder/surface_pool_unittests.cc index 72488c9cb03aa..f2fae1228de80 100644 --- a/shell/platform/android/external_view_embedder/surface_pool_unittests.cc +++ b/shell/platform/android/external_view_embedder/surface_pool_unittests.cc @@ -164,5 +164,41 @@ TEST(SurfacePool, GetLayer__AllocateTwoLayers) { ASSERT_EQ(1, layer_2->id); } +TEST(SurfacePool, DestroyLayers) { + auto pool = std::make_unique(); + auto jni_mock = std::make_shared(); + + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()).Times(0); + pool->DestroyLayers(jni_mock); + + auto gr_context = GrContext::MakeMock(nullptr); + auto android_context = + std::make_shared(AndroidRenderingAPI::kSoftware); + + auto window = fml::MakeRefCounted(nullptr); + EXPECT_CALL(*jni_mock, FlutterViewCreateOverlaySurface()) + .Times(1) + .WillOnce(Return( + ByMove(std::make_unique( + 0, window)))); + + auto surface_factory = + [gr_context, window](std::shared_ptr android_context, + std::shared_ptr jni_facade) { + auto android_surface_mock = std::make_unique(); + EXPECT_CALL(*android_surface_mock, CreateGPUSurface(gr_context.get())); + EXPECT_CALL(*android_surface_mock, SetNativeWindow(window)); + EXPECT_CALL(*android_surface_mock, IsValid()).WillOnce(Return(true)); + return android_surface_mock; + }; + pool->GetLayer(gr_context.get(), android_context, jni_mock, surface_factory); + + EXPECT_CALL(*jni_mock, FlutterViewDestroyOverlaySurfaces()); + pool->DestroyLayers(jni_mock); + + pool->RecycleLayers(); + ASSERT_TRUE(pool->GetUnusedLayers().empty()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java index dfc5fae151a0d..46079d429f56c 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java @@ -62,6 +62,9 @@ public enum SurfaceKind { */ private int pendingImages = 0; + /** Whether the view is attached to the Flutter render. */ + private boolean isAttachedToFlutterRenderer = false; + /** * Constructs a {@code FlutterImageView} with an {@link android.media.ImageReader} that provides * the Flutter UI. @@ -70,6 +73,7 @@ public FlutterImageView(@NonNull Context context, int width, int height, Surface super(context, null); this.imageReader = createImageReader(width, height); this.kind = kind; + init(); } @VisibleForTesting @@ -77,6 +81,11 @@ public FlutterImageView(@NonNull Context context, int width, int height, Surface super(context, null); this.imageReader = imageReader; this.kind = kind; + init(); + } + + private void init() { + setAlpha(0.0f); } @TargetApi(19) @@ -111,16 +120,21 @@ public FlutterRenderer getAttachedRenderer() { */ @Override public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { - this.flutterRenderer = flutterRenderer; + if (isAttachedToFlutterRenderer) { + return; + } switch (kind) { case background: flutterRenderer.swapSurface(imageReader.getSurface()); break; case overlay: - // Don't do anything as this is done by the handler of + // Do nothing since the attachment is done by the handler of // `FlutterJNI#createOverlaySurface()` in the native side. break; } + setAlpha(1.0f); + this.flutterRenderer = flutterRenderer; + isAttachedToFlutterRenderer = true; } /** @@ -128,16 +142,26 @@ public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { * Flutter UI to this {@code FlutterImageView}. */ public void detachFromRenderer() { - switch (kind) { - case background: - // TODO: Swap the surface back to the original one. - // https://github.com/flutter/flutter/issues/58291 - break; - case overlay: - // TODO: Handle this in the native side. - // https://github.com/flutter/flutter/issues/59904 - break; + if (!isAttachedToFlutterRenderer) { + return; + } + setAlpha(0.0f); + // Drop the lastest image as it shouldn't render this image if this view is + // attached to the renderer again. + acquireLatestImage(); + // Clear drawings. + pendingImages = 0; + currentBitmap = null; + if (nextImage != null) { + nextImage.close(); + nextImage = null; + } + if (currentImage != null) { + currentImage.close(); + currentImage = null; } + invalidate(); + isAttachedToFlutterRenderer = false; } public void pause() { @@ -146,7 +170,10 @@ public void pause() { /** Acquires the next image to be drawn to the {@link android.graphics.Canvas}. */ @TargetApi(19) - public void acquireLatestImage() { + public boolean acquireLatestImage() { + if (!isAttachedToFlutterRenderer) { + return false; + } // There's no guarantee that the image will be closed before the next call to // `acquireLatestImage()`. For example, the device may not produce new frames if // it's in sleep mode, so the calls to `invalidate()` will be queued up @@ -162,6 +189,30 @@ public void acquireLatestImage() { } } invalidate(); + return nextImage != null; + } + + /** Creates a new image reader with the provided size. */ + public void resizeIfNeeded(int width, int height) { + if (flutterRenderer == null) { + return; + } + if (width == imageReader.getWidth() && height == imageReader.getHeight()) { + return; + } + // Close resources. + if (nextImage != null) { + nextImage.close(); + nextImage = null; + } + if (currentImage != null) { + currentImage.close(); + currentImage = null; + } + imageReader.close(); + // Image readers cannot be resized once created. + imageReader = createImageReader(width, height); + pendingImages = 0; } @Override @@ -214,8 +265,13 @@ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) if (width == imageReader.getWidth() && height == imageReader.getHeight()) { return; } - if (kind == SurfaceKind.background && flutterRenderer != null) { - imageReader = createImageReader(width, height); + // `SurfaceKind.overlay` isn't resized. Instead, the `FlutterImageView` instance + // is destroyed. As a result, an instance with the new size is created by the surface + // pool in the native side. + if (kind == SurfaceKind.background && isAttachedToFlutterRenderer) { + resizeIfNeeded(width, height); + // Bind native window to the new surface, and create a new onscreen surface + // with the new size in the native side. flutterRenderer.swapSurface(imageReader.getSurface()); } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 64e1886596983..52b337978aed5 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -81,6 +81,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC @Nullable private FlutterTextureView flutterTextureView; @Nullable private FlutterImageView flutterImageView; @Nullable private RenderSurface renderSurface; + @Nullable private RenderSurface previousRenderSurface; private final Set flutterUiDisplayListeners = new HashSet<>(); private boolean isFlutterUiDisplayed; @@ -943,26 +944,63 @@ public void detachFromFlutterEngine() { flutterRenderer.stopRenderingToSurface(); flutterRenderer.setSemanticsEnabled(false); renderSurface.detachFromRenderer(); + + flutterImageView = null; + previousRenderSurface = null; flutterEngine = null; } public void convertToImageView() { renderSurface.pause(); - flutterImageView = - new FlutterImageView( - getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background); + if (flutterImageView == null) { + flutterImageView = + new FlutterImageView( + getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background); + addView(flutterImageView); + } else { + flutterImageView.resizeIfNeeded(getWidth(), getHeight()); + } + + previousRenderSurface = renderSurface; renderSurface = flutterImageView; if (flutterEngine != null) { renderSurface.attachToRenderer(flutterEngine.getRenderer()); } - addView(flutterImageView); } - public void acquireLatestImageViewFrame() { + /** + * If the surface is rendered by a {@code FlutterImageView}. Then, calling this method will stop + * rendering to a {@code FlutterImageView}, and use the previous surface instead. + */ + public void revertImageView() { + if (flutterImageView == null) { + Log.v(TAG, "Tried to revert the image view, but no image view is used."); + return; + } + if (previousRenderSurface == null) { + Log.v(TAG, "Tried to revert the image view, but no previous surface was used."); + return; + } + flutterImageView.detachFromRenderer(); + renderSurface = previousRenderSurface; + previousRenderSurface = null; + if (flutterEngine != null) { + renderSurface.attachToRenderer(flutterEngine.getRenderer()); + } + } + + public void attachOverlaySurfaceToRender(FlutterImageView view) { + if (flutterEngine != null) { + view.attachToRenderer(flutterEngine.getRenderer()); + } + } + + public boolean acquireLatestImageViewFrame() { if (flutterImageView != null) { - flutterImageView.acquireLatestImage(); + return flutterImageView.acquireLatestImage(); } + return false; } /** Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}. */ diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 91ca791bdee94..61ceab005a147 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -663,7 +663,6 @@ public void onDisplayPlatformView( FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, ViewHeight); View platformView = platformViews.get(viewId); platformView.setLayoutParams(layoutParams); - platformView.setVisibility(View.VISIBLE); platformView.bringToFront(); currentFrameUsedPlatformViewIds.add(viewId); } @@ -691,42 +690,86 @@ public void onBeginFrame() { } public void onEndFrame() { - // Hide overlay surfaces that aren't rendered in the current frame. + // Whether the current frame was rendered using ImageReaders. + // + // Since the image readers may not have images available at this point, + // this becomes true if all the required surfaces have images available. + // + // This is used to decide if the platform views can be rendered in the current frame. + // If one of the surfaces doesn't have an image, the frame may be incomplete and must be + // dropped. + // For example, a toolbar widget painted by Flutter may not be rendered. + boolean isFrameRenderedUsingImageReaders = false; + + if (flutterViewConvertedToImageView) { + FlutterView view = (FlutterView) flutterView; + // If there are no platform views in the current frame, + // then revert the image view surface and use the previous surface. + // + // Otherwise, acquire the latest image. + if (currentFrameUsedPlatformViewIds.isEmpty()) { + view.revertImageView(); + flutterViewConvertedToImageView = false; + } else { + isFrameRenderedUsingImageReaders = view.acquireLatestImageViewFrame(); + } + } + for (int i = 0; i < overlayLayerViews.size(); i++) { int overlayId = overlayLayerViews.keyAt(i); FlutterImageView overlayView = overlayLayerViews.valueAt(i); + if (currentFrameUsedOverlayLayerIds.contains(overlayId)) { - overlayView.acquireLatestImage(); + ((FlutterView) flutterView).attachOverlaySurfaceToRender(overlayView); + boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage(); + isFrameRenderedUsingImageReaders &= didAcquireOverlaySurfaceImage; } else { + // If the background surface isn't rendered by the image view, then the + // overlay surfaces can be detached from the rendered. + // This releases resources used by the ImageReader. + if (!flutterViewConvertedToImageView) { + overlayView.detachFromRenderer(); + } + // Hide overlay surfaces that aren't rendered in the current frame. overlayView.setVisibility(View.GONE); } } - // Hide platform views that aren't rendered in the current frame. - // The platform view is destroyed by the framework after the widget is disposed. - // - // The framework diposes the platform view, when its `State` object will never - // build again. + for (int i = 0; i < platformViews.size(); i++) { int viewId = platformViews.keyAt(i); - if (!currentFrameUsedPlatformViewIds.contains(viewId)) { - platformViews.get(viewId).setVisibility(View.GONE); - mutatorViews.get(viewId).setVisibility(View.GONE); + View platformView = platformViews.get(viewId); + View mutatorView = mutatorViews.get(viewId); + + // Show platform views only if the surfaces have images available in this frame, + // and if the platform view is rendered in this frame. + // + // Otherwise, hide the platform view, but don't remove it from the view hierarchy yet as + // they are removed when the framework diposes the platform view widget. + if (isFrameRenderedUsingImageReaders && currentFrameUsedPlatformViewIds.contains(viewId)) { + platformView.setVisibility(View.VISIBLE); + mutatorView.setVisibility(View.VISIBLE); + } else { + platformView.setVisibility(View.GONE); + mutatorView.setVisibility(View.GONE); } } - // If the background surface is still an image, then acquire the latest image. - if (flutterViewConvertedToImageView) { - ((FlutterView) flutterView).acquireLatestImageViewFrame(); - } } @TargetApi(19) public FlutterOverlaySurface createOverlaySurface() { + // Overlay surfaces have the same size as the background surface. + // + // This allows to reuse these surfaces in consecutive frames even + // if the drawings they contain have a different tight bound. + // + // The final view size is determined when its frame is set. FlutterImageView imageView = new FlutterImageView( flutterView.getContext(), flutterView.getWidth(), flutterView.getHeight(), FlutterImageView.SurfaceKind.overlay); + int id = nextOverlayLayerId++; overlayLayerViews.put(id, imageView); @@ -734,6 +777,12 @@ public FlutterOverlaySurface createOverlaySurface() { } public void destroyOverlaySurfaces() { + for (int i = 0; i < overlayLayerViews.size(); i++) { + int overlayId = overlayLayerViews.keyAt(i); + FlutterImageView overlayView = overlayLayerViews.valueAt(i); + overlayView.detachFromRenderer(); + ((FlutterView) flutterView).removeView(overlayView); + } overlayLayerViews.clear(); } } 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 e41c5225d9cf3..80b368242809e 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -1,6 +1,8 @@ package io.flutter.embedding.android; import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; @@ -385,11 +387,38 @@ public void flutterImageView_acquiresImageAndInvalidates() { mockReader, FlutterImageView.SurfaceKind.background)); - imageView.acquireLatestImage(); + final FlutterJNI jni = mock(FlutterJNI.class); + imageView.attachToRenderer(new FlutterRenderer(jni)); + + final Image mockImage = mock(Image.class); + when(mockReader.acquireLatestImage()).thenReturn(mockImage); + + assertTrue(imageView.acquireLatestImage()); verify(mockReader, times(1)).acquireLatestImage(); verify(imageView, times(1)).invalidate(); } + @Test + public void flutterImageView_acquireLatestImageReturnsFalse() { + final ImageReader mockReader = mock(ImageReader.class); + when(mockReader.getMaxImages()).thenReturn(2); + + final FlutterImageView imageView = + spy( + new FlutterImageView( + RuntimeEnvironment.application, + mockReader, + FlutterImageView.SurfaceKind.background)); + + assertFalse(imageView.acquireLatestImage()); + + final FlutterJNI jni = mock(FlutterJNI.class); + imageView.attachToRenderer(new FlutterRenderer(jni)); + + when(mockReader.acquireLatestImage()).thenReturn(null); + assertFalse(imageView.acquireLatestImage()); + } + @Test public void flutterImageView_acquiresMaxImagesAtMost() { final ImageReader mockReader = mock(ImageReader.class); @@ -405,10 +434,13 @@ public void flutterImageView_acquiresMaxImagesAtMost() { mockReader, FlutterImageView.SurfaceKind.background)); + final FlutterJNI jni = mock(FlutterJNI.class); + imageView.attachToRenderer(new FlutterRenderer(jni)); + doNothing().when(imageView).invalidate(); - imageView.acquireLatestImage(); - imageView.acquireLatestImage(); - imageView.acquireLatestImage(); + assertTrue(imageView.acquireLatestImage()); + assertTrue(imageView.acquireLatestImage()); + assertTrue(imageView.acquireLatestImage()); verify(mockReader, times(2)).acquireLatestImage(); }