| // 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 static android.view.MotionEvent.PointerCoords; |
| import static android.view.MotionEvent.PointerProperties; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.MutableContextWrapper; |
| import android.os.Build; |
| import android.util.SparseArray; |
| import android.view.MotionEvent; |
| import android.view.SurfaceView; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.UiThread; |
| import androidx.annotation.VisibleForTesting; |
| import io.flutter.Log; |
| import io.flutter.embedding.android.AndroidTouchProcessor; |
| import io.flutter.embedding.android.FlutterView; |
| import io.flutter.embedding.android.MotionEventTracker; |
| import io.flutter.embedding.engine.FlutterOverlaySurface; |
| import io.flutter.embedding.engine.dart.DartExecutor; |
| import io.flutter.embedding.engine.mutatorsstack.*; |
| import io.flutter.embedding.engine.renderer.FlutterRenderer; |
| import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel; |
| import io.flutter.plugin.editing.TextInputPlugin; |
| import io.flutter.util.ViewUtils; |
| import io.flutter.view.AccessibilityBridge; |
| import io.flutter.view.TextureRegistry; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| |
| /** |
| * Manages platform views. |
| * |
| * <p>Each {@link io.flutter.embedding.engine.FlutterEngine} or {@link |
| * io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A platform views |
| * controller can be attached to at most one Flutter view. |
| */ |
| public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { |
| private static final String TAG = "PlatformViewsController"; |
| |
| // These view types allow out-of-band drawing commands that don't notify the Android view |
| // hierarchy. |
| // To support these cases, Flutter hosts the embedded view in a VirtualDisplay, |
| // and binds the VirtualDisplay to a GL texture that is then composed by the engine. |
| // However, there are a few issues with Virtual Displays. For example, they don't fully support |
| // accessibility due to https://github.com/flutter/flutter/issues/29717, |
| // and keyboard interactions may have non-deterministic behavior. |
| // Views that issue out-of-band drawing commands that aren't included in this array are |
| // required to call `View#invalidate()` to notify Flutter about the update. |
| // This isn't ideal, but given all the other limitations it's a reasonable tradeoff. |
| // Related issue: https://github.com/flutter/flutter/issues/103630 |
| private static Class[] VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY = {SurfaceView.class}; |
| |
| private final PlatformViewRegistryImpl registry; |
| |
| private AndroidTouchProcessor androidTouchProcessor; |
| |
| // 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 FlutterView flutterView; |
| |
| // The texture registry maintaining the textures into which the embedded views will be rendered. |
| @Nullable private TextureRegistry textureRegistry; |
| |
| @Nullable private TextInputPlugin textInputPlugin; |
| |
| // The system channel used to communicate with the framework about platform views. |
| private PlatformViewsChannel platformViewsChannel; |
| |
| // The accessibility bridge to which accessibility events form the platform views will be |
| // dispatched. |
| private final AccessibilityEventsDelegate accessibilityEventsDelegate; |
| |
| // 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 embedded 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 |
| // it is associated with(e.g if a platform view creates other views in the same virtual display. |
| @VisibleForTesting /* package */ final HashMap<Context, View> contextToEmbeddedView; |
| |
| // The platform views. |
| private final SparseArray<PlatformView> platformViews; |
| |
| // The platform view wrappers that are appended to FlutterView. |
| // |
| // These platform views use a PlatformViewLayer in the framework. This is different than |
| // the platform views that use a TextureLayer. |
| // |
| // This distinction is necessary because a PlatformViewLayer allows to embed Android's |
| // SurfaceViews in a Flutter app whereas the texture layer is unable to support such native views. |
| // |
| // If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't |
| // in the view hierarchy. |
| // |
| // This view provides a wrapper that applies scene builder operations to the platform view. |
| // For example, a transform matrix, or setting opacity on the platform view layer. |
| private final SparseArray<FlutterMutatorView> platformViewParent; |
| |
| // Map of unique IDs to views that render overlay layers. |
| private final SparseArray<PlatformOverlayView> overlayLayerViews; |
| |
| // The platform view wrappers that are appended to FlutterView. |
| // |
| // These platform views use a TextureLayer in the framework. This is different than |
| // the platform views that use a PlatformViewLayer. |
| // |
| // This is the default mode, and recommended for better performance. |
| private final SparseArray<PlatformViewWrapper> viewWrappers; |
| |
| // Next available unique ID for use in overlayLayerViews. |
| private int nextOverlayLayerId = 0; |
| |
| // Tracks whether the flutterView has been converted to use a FlutterImageView. |
| private boolean flutterViewConvertedToImageView = false; |
| |
| // When adding platform views using Hybrid Composition, the engine converts the render surface |
| // to a FlutterImageView to help improve animation synchronization on Android. This flag allows |
| // disabling this conversion through the PlatformView platform channel. |
| private boolean synchronizeToNativeViewHierarchy = true; |
| |
| // Overlay layer IDs that were displayed since the start of the current frame. |
| private final HashSet<Integer> currentFrameUsedOverlayLayerIds; |
| |
| // Platform view IDs that were displayed since the start of the current frame. |
| private final HashSet<Integer> currentFrameUsedPlatformViewIds; |
| |
| // Used to acquire the original motion events using the motionEventIds. |
| private final MotionEventTracker motionEventTracker; |
| |
| // Whether software rendering is used. |
| private boolean usesSoftwareRendering = false; |
| |
| private final PlatformViewsChannel.PlatformViewsHandler channelHandler = |
| new PlatformViewsChannel.PlatformViewsHandler() { |
| |
| @TargetApi(19) |
| @Override |
| // TODO(egarciad): Remove the need for this. |
| // https://github.com/flutter/flutter/issues/96679 |
| public void createForPlatformViewLayer( |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { |
| // API level 19 is required for `android.graphics.ImageReader`. |
| ensureValidAndroidVersion(19); |
| ensureValidRequest(request); |
| |
| final PlatformView platformView = createPlatformView(request, false); |
| |
| configureForHybridComposition(platformView, request); |
| // New code should be added to configureForHybridComposition, not here, unless it is |
| // not applicable to fallback from TLHC to HC. |
| } |
| |
| @TargetApi(20) |
| @Override |
| public long createForTextureLayer( |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { |
| ensureValidRequest(request); |
| final int viewId = request.viewId; |
| if (viewWrappers.get(viewId) != null) { |
| throw new IllegalStateException( |
| "Trying to create an already created platform view, view id: " + viewId); |
| } |
| if (textureRegistry == null) { |
| throw new IllegalStateException( |
| "Texture registry is null. This means that platform views controller was detached, view id: " |
| + viewId); |
| } |
| if (flutterView == null) { |
| throw new IllegalStateException( |
| "Flutter view is null. This means the platform views controller doesn't have an attached view, view id: " |
| + viewId); |
| } |
| |
| final PlatformView platformView = createPlatformView(request, true); |
| |
| final View embeddedView = platformView.getView(); |
| if (embeddedView.getParent() != null) { |
| throw new IllegalStateException( |
| "The Android view returned from PlatformView#getView() was already added to a parent view."); |
| } |
| |
| // The newer Texture Layer Hybrid Composition mode isn't suppported if any of the |
| // following are true: |
| // - The embedded view contains any of the VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY view types. |
| // These views allow out-of-band graphics operations that aren't notified to the Android |
| // view hierarchy via callbacks such as ViewParent#onDescendantInvalidated(). |
| // - The API level is <23, due to TLHC implementation API requirements. |
| final boolean supportsTextureLayerMode = |
| Build.VERSION.SDK_INT >= 23 |
| && !ViewUtils.hasChildViewOfType( |
| embeddedView, VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY); |
| |
| // Fall back to Hybrid Composition or Virtual Display when necessary, depending on which |
| // fallback mode is requested. |
| if (!supportsTextureLayerMode) { |
| if (request.displayMode |
| == PlatformViewsChannel.PlatformViewCreationRequest.RequestedDisplayMode |
| .TEXTURE_WITH_HYBRID_FALLBACK) { |
| configureForHybridComposition(platformView, request); |
| return PlatformViewsChannel.PlatformViewsHandler.NON_TEXTURE_FALLBACK; |
| } else if (!usesSoftwareRendering) { // Virtual Display doesn't support software mode. |
| return configureForVirtualDisplay(platformView, request); |
| } |
| // TODO(stuartmorgan): Consider throwing a specific exception here as a breaking change. |
| // For now, preserve the 3.0 behavior of falling through to Texture Layer mode even |
| // though it won't work correctly. |
| } |
| return configureForTextureLayerComposition(platformView, request); |
| } |
| |
| @Override |
| public void dispose(int viewId) { |
| final PlatformView platformView = platformViews.get(viewId); |
| if (platformView == null) { |
| Log.e(TAG, "Disposing unknown platform view with id: " + viewId); |
| return; |
| } |
| platformViews.remove(viewId); |
| |
| try { |
| platformView.dispose(); |
| } catch (RuntimeException exception) { |
| Log.e(TAG, "Disposing platform view threw an exception", exception); |
| } |
| |
| if (usesVirtualDisplay(viewId)) { |
| final VirtualDisplayController vdController = vdControllers.get(viewId); |
| final View embeddedView = vdController.getView(); |
| if (embeddedView != null) { |
| contextToEmbeddedView.remove(embeddedView.getContext()); |
| } |
| vdControllers.remove(viewId); |
| return; |
| } |
| // The platform view is displayed using a TextureLayer and is inserted in the view |
| // hierarchy. |
| final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); |
| if (viewWrapper != null) { |
| viewWrapper.removeAllViews(); |
| viewWrapper.release(); |
| viewWrapper.unsetOnDescendantFocusChangeListener(); |
| |
| final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent(); |
| if (wrapperParent != null) { |
| wrapperParent.removeView(viewWrapper); |
| } |
| viewWrappers.remove(viewId); |
| return; |
| } |
| // The platform view is displayed using a PlatformViewLayer. |
| final FlutterMutatorView parentView = platformViewParent.get(viewId); |
| if (parentView != null) { |
| parentView.removeAllViews(); |
| parentView.unsetOnDescendantFocusChangeListener(); |
| |
| final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent(); |
| if (mutatorViewParent != null) { |
| mutatorViewParent.removeView(parentView); |
| } |
| platformViewParent.remove(viewId); |
| } |
| } |
| |
| @Override |
| public void offset(int viewId, double top, double left) { |
| if (usesVirtualDisplay(viewId)) { |
| // Virtual displays don't need an accessibility offset. |
| return; |
| } |
| // For platform views that use TextureView and are in the view hierarchy, set |
| // an offset to the wrapper view. |
| // This ensures that the accessibility highlights are drawn in the expected position on |
| // screen. |
| // This offset doesn't affect the position of the embeded view by itself since the GL |
| // texture is positioned by the Flutter engine, which knows where to position different |
| // types of layers. |
| final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); |
| if (viewWrapper == null) { |
| Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId); |
| return; |
| } |
| final int physicalTop = toPhysicalPixels(top); |
| final int physicalLeft = toPhysicalPixels(left); |
| final FrameLayout.LayoutParams layoutParams = |
| (FrameLayout.LayoutParams) viewWrapper.getLayoutParams(); |
| layoutParams.topMargin = physicalTop; |
| layoutParams.leftMargin = physicalLeft; |
| viewWrapper.setLayoutParams(layoutParams); |
| } |
| |
| @Override |
| public void resize( |
| @NonNull PlatformViewsChannel.PlatformViewResizeRequest request, |
| @NonNull PlatformViewsChannel.PlatformViewBufferResized onComplete) { |
| final int physicalWidth = toPhysicalPixels(request.newLogicalWidth); |
| final int physicalHeight = toPhysicalPixels(request.newLogicalHeight); |
| final int viewId = request.viewId; |
| |
| if (usesVirtualDisplay(viewId)) { |
| final float originalDisplayDensity = getDisplayDensity(); |
| final VirtualDisplayController vdController = vdControllers.get(viewId); |
| // Resizing involved moving the platform view to a new virtual display. Doing so |
| // potentially results in losing an active input connection. To make sure we preserve |
| // the input connection when resizing we lock it here and unlock after the resize is |
| // complete. |
| lockInputConnection(vdController); |
| vdController.resize( |
| physicalWidth, |
| physicalHeight, |
| () -> { |
| unlockInputConnection(vdController); |
| // Converting back to logic pixels requires a context, which may no longer be |
| // available. If that happens, assume the same logic/physical relationship as |
| // was present when the request arrived. |
| final float displayDensity = |
| context == null ? originalDisplayDensity : getDisplayDensity(); |
| onComplete.run( |
| new PlatformViewsChannel.PlatformViewBufferSize( |
| toLogicalPixels(vdController.getBufferWidth(), displayDensity), |
| toLogicalPixels(vdController.getBufferHeight(), displayDensity))); |
| }); |
| return; |
| } |
| |
| final PlatformView platformView = platformViews.get(viewId); |
| final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); |
| if (platformView == null || viewWrapper == null) { |
| Log.e(TAG, "Resizing unknown platform view with id: " + viewId); |
| return; |
| } |
| // Resize the buffer only when the current buffer size is smaller than the new size. |
| // This is required to prevent a situation when smooth keyboard animation |
| // resizes the texture too often, such that the GPU and the platform thread don't agree on |
| // the |
| // timing of the new size. |
| // 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); |
| } |
| |
| final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams(); |
| viewWrapperLayoutParams.width = physicalWidth; |
| viewWrapperLayoutParams.height = physicalHeight; |
| viewWrapper.setLayoutParams(viewWrapperLayoutParams); |
| |
| final View embeddedView = platformView.getView(); |
| if (embeddedView != null) { |
| final ViewGroup.LayoutParams embeddedViewLayoutParams = embeddedView.getLayoutParams(); |
| embeddedViewLayoutParams.width = physicalWidth; |
| embeddedViewLayoutParams.height = physicalHeight; |
| embeddedView.setLayoutParams(embeddedViewLayoutParams); |
| } |
| onComplete.run( |
| new PlatformViewsChannel.PlatformViewBufferSize( |
| toLogicalPixels(viewWrapper.getBufferWidth()), |
| toLogicalPixels(viewWrapper.getBufferHeight()))); |
| } |
| |
| @Override |
| public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { |
| final int viewId = touch.viewId; |
| final float density = context.getResources().getDisplayMetrics().density; |
| |
| if (usesVirtualDisplay(viewId)) { |
| final VirtualDisplayController vdController = vdControllers.get(viewId); |
| final MotionEvent event = toMotionEvent(density, touch, true); |
| vdController.dispatchTouchEvent(event); |
| return; |
| } |
| |
| final PlatformView platformView = platformViews.get(viewId); |
| if (platformView == null) { |
| Log.e(TAG, "Sending touch to an unknown view with id: " + viewId); |
| return; |
| } |
| final View view = platformView.getView(); |
| if (view == null) { |
| Log.e(TAG, "Sending touch to a null view with id: " + viewId); |
| return; |
| } |
| final MotionEvent event = toMotionEvent(density, touch, false); |
| view.dispatchTouchEvent(event); |
| } |
| |
| @TargetApi(17) |
| @Override |
| public void setDirection(int viewId, int direction) { |
| if (!validateDirection(direction)) { |
| throw new IllegalStateException( |
| "Trying to set unknown direction value: " |
| + direction |
| + "(view id: " |
| + viewId |
| + ")"); |
| } |
| |
| View embeddedView; |
| |
| if (usesVirtualDisplay(viewId)) { |
| final VirtualDisplayController controller = vdControllers.get(viewId); |
| embeddedView = controller.getView(); |
| } else { |
| final PlatformView platformView = platformViews.get(viewId); |
| if (platformView == null) { |
| Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); |
| return; |
| } |
| embeddedView = platformView.getView(); |
| } |
| if (embeddedView == null) { |
| Log.e(TAG, "Setting direction to a null view with id: " + viewId); |
| return; |
| } |
| embeddedView.setLayoutDirection(direction); |
| } |
| |
| @Override |
| public void clearFocus(int viewId) { |
| View embeddedView; |
| |
| if (usesVirtualDisplay(viewId)) { |
| final VirtualDisplayController controller = vdControllers.get(viewId); |
| embeddedView = controller.getView(); |
| } else { |
| final PlatformView platformView = platformViews.get(viewId); |
| if (platformView == null) { |
| Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId); |
| return; |
| } |
| embeddedView = platformView.getView(); |
| } |
| if (embeddedView == null) { |
| Log.e(TAG, "Clearing focus on a null view with id: " + viewId); |
| return; |
| } |
| embeddedView.clearFocus(); |
| } |
| |
| private void ensureValidAndroidVersion(int minSdkVersion) { |
| if (Build.VERSION.SDK_INT < minSdkVersion) { |
| throw new IllegalStateException( |
| "Trying to use platform views with API " |
| + Build.VERSION.SDK_INT |
| + ", required API level is: " |
| + minSdkVersion); |
| } |
| } |
| |
| private void ensureValidRequest( |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { |
| if (!validateDirection(request.direction)) { |
| throw new IllegalStateException( |
| "Trying to create a view with unknown direction value: " |
| + request.direction |
| + "(view id: " |
| + request.viewId |
| + ")"); |
| } |
| } |
| |
| // Creates a platform view based on `request`, performs configuration that's common to |
| // all display modes, and adds it to `platformViews`. |
| @TargetApi(19) |
| private PlatformView createPlatformView( |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request, |
| boolean wrapContext) { |
| final PlatformViewFactory viewFactory = registry.getFactory(request.viewType); |
| if (viewFactory == null) { |
| throw new IllegalStateException( |
| "Trying to create a platform view of unregistered type: " + request.viewType); |
| } |
| |
| Object createParams = null; |
| if (request.params != null) { |
| createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params); |
| } |
| |
| // In some display modes, the context needs to be modified during display. |
| // TODO(stuartmorgan): Make this wrapping unconditional if possible; for context see |
| // https://github.com/flutter/flutter/issues/113449 |
| final Context mutableContext = wrapContext ? new MutableContextWrapper(context) : context; |
| final PlatformView platformView = |
| viewFactory.create(mutableContext, request.viewId, createParams); |
| |
| // Configure the view to match the requested layout direction. |
| final View embeddedView = platformView.getView(); |
| if (embeddedView == null) { |
| throw new IllegalStateException( |
| "PlatformView#getView() returned null, but an Android view reference was expected."); |
| } |
| embeddedView.setLayoutDirection(request.direction); |
| |
| platformViews.put(request.viewId, platformView); |
| return platformView; |
| } |
| |
| // Configures the view for Hybrid Composition mode. |
| private void configureForHybridComposition( |
| @NonNull PlatformView platformView, |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { |
| Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId); |
| } |
| |
| // Configures the view for Virtual Display mode, returning the associated texture ID. |
| private long configureForVirtualDisplay( |
| @NonNull PlatformView platformView, |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { |
| // This mode adds the view to a virtual display, which is wired up to a GL texture that |
| // is composed by the Flutter engine. |
| |
| // API level 20 is required to use VirtualDisplay#setSurface. |
| ensureValidAndroidVersion(20); |
| |
| Log.i(TAG, "Hosting view in a virtual display for platform view: " + request.viewId); |
| |
| final TextureRegistry.SurfaceTextureEntry textureEntry = |
| textureRegistry.createSurfaceTexture(); |
| final int physicalWidth = toPhysicalPixels(request.logicalWidth); |
| final int physicalHeight = toPhysicalPixels(request.logicalHeight); |
| final VirtualDisplayController vdController = |
| VirtualDisplayController.create( |
| context, |
| accessibilityEventsDelegate, |
| platformView, |
| textureEntry, |
| physicalWidth, |
| physicalHeight, |
| request.viewId, |
| null, |
| (view, hasFocus) -> { |
| if (hasFocus) { |
| platformViewsChannel.invokeViewFocused(request.viewId); |
| } |
| }); |
| |
| if (vdController == null) { |
| throw new IllegalStateException( |
| "Failed creating virtual display for a " |
| + 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); |
| } |
| |
| // The embedded view doesn't need to be sized in Virtual Display mode because the |
| // virtual display itself is sized. |
| |
| vdControllers.put(request.viewId, vdController); |
| final View embeddedView = platformView.getView(); |
| contextToEmbeddedView.put(embeddedView.getContext(), embeddedView); |
| |
| return textureEntry.id(); |
| } |
| |
| // Configures the view for Texture Layer Hybrid Composition mode, returning the associated |
| // texture ID. |
| @TargetApi(23) |
| private long configureForTextureLayerComposition( |
| @NonNull PlatformView platformView, |
| @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { |
| // This mode attaches the view to the Android view hierarchy and record its drawing |
| // operations, so they can be forwarded to a GL texture that is composed by the |
| // Flutter engine. |
| |
| // API level 23 is required to use Surface#lockHardwareCanvas(). |
| ensureValidAndroidVersion(23); |
| Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId); |
| |
| final int physicalWidth = toPhysicalPixels(request.logicalWidth); |
| final int physicalHeight = toPhysicalPixels(request.logicalHeight); |
| PlatformViewWrapper viewWrapper; |
| long textureId; |
| if (usesSoftwareRendering) { |
| viewWrapper = new PlatformViewWrapper(context); |
| textureId = -1; |
| } else { |
| final TextureRegistry.SurfaceTextureEntry textureEntry = |
| textureRegistry.createSurfaceTexture(); |
| viewWrapper = new PlatformViewWrapper(context, textureEntry); |
| textureId = textureEntry.id(); |
| } |
| viewWrapper.setTouchProcessor(androidTouchProcessor); |
| viewWrapper.setBufferSize(physicalWidth, physicalHeight); |
| |
| final FrameLayout.LayoutParams viewWrapperLayoutParams = |
| new FrameLayout.LayoutParams(physicalWidth, physicalHeight); |
| |
| // Size and position the view wrapper. |
| final int physicalTop = toPhysicalPixels(request.logicalTop); |
| final int physicalLeft = toPhysicalPixels(request.logicalLeft); |
| viewWrapperLayoutParams.topMargin = physicalTop; |
| viewWrapperLayoutParams.leftMargin = physicalLeft; |
| viewWrapper.setLayoutParams(viewWrapperLayoutParams); |
| |
| // Size the embedded view. |
| final View embeddedView = platformView.getView(); |
| embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight)); |
| |
| // Accessibility in the embedded view is initially disabled because if a Flutter app |
| // disabled accessibility in the first frame, the embedding won't receive an update to |
| // disable accessibility since the embedding never received an update to enable it. |
| // The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas |
| // when the framework sends a new a11y tree to the embedding. |
| // To prevent races, the framework populate the SemanticsNode after the platform view has |
| // been created. |
| embeddedView.setImportantForAccessibility( |
| View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); |
| |
| // Add the embedded view to the wrapper. |
| viewWrapper.addView(embeddedView); |
| |
| // Listen for focus changed in any subview, so the framework is notified when the platform |
| // view is focused. |
| viewWrapper.setOnDescendantFocusChangeListener( |
| (v, hasFocus) -> { |
| if (hasFocus) { |
| platformViewsChannel.invokeViewFocused(request.viewId); |
| } else if (textInputPlugin != null) { |
| textInputPlugin.clearPlatformViewClient(request.viewId); |
| } |
| }); |
| flutterView.addView(viewWrapper); |
| viewWrappers.append(request.viewId, viewWrapper); |
| return textureId; |
| } |
| |
| @Override |
| public void synchronizeToNativeViewHierarchy(boolean yes) { |
| synchronizeToNativeViewHierarchy = yes; |
| } |
| }; |
| |
| @VisibleForTesting |
| public MotionEvent toMotionEvent( |
| float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) { |
| MotionEventTracker.MotionEventId motionEventId = |
| MotionEventTracker.MotionEventId.from(touch.motionEventId); |
| MotionEvent trackedEvent = motionEventTracker.pop(motionEventId); |
| |
| // Pointer coordinates in the tracked events are global to FlutterView |
| // framework converts them to be local to a widget, given that |
| // motion events operate on local coords, we need to replace these in the tracked |
| // event with their local counterparts. |
| PointerProperties[] pointerProperties = |
| parsePointerPropertiesList(touch.rawPointerPropertiesList) |
| .toArray(new PointerProperties[touch.pointerCount]); |
| PointerCoords[] pointerCoords = |
| parsePointerCoordsList(touch.rawPointerCoords, density) |
| .toArray(new PointerCoords[touch.pointerCount]); |
| |
| if (!usingVirtualDiplay && trackedEvent != null) { |
| return MotionEvent.obtain( |
| trackedEvent.getDownTime(), |
| trackedEvent.getEventTime(), |
| touch.action, |
| touch.pointerCount, |
| pointerProperties, |
| pointerCoords, |
| trackedEvent.getMetaState(), |
| trackedEvent.getButtonState(), |
| trackedEvent.getXPrecision(), |
| trackedEvent.getYPrecision(), |
| trackedEvent.getDeviceId(), |
| trackedEvent.getEdgeFlags(), |
| trackedEvent.getSource(), |
| trackedEvent.getFlags()); |
| } |
| |
| // TODO (kaushikiska) : warn that we are potentially using an untracked |
| // event in the platform views. |
| return MotionEvent.obtain( |
| touch.downTime.longValue(), |
| touch.eventTime.longValue(), |
| touch.action, |
| touch.pointerCount, |
| pointerProperties, |
| pointerCoords, |
| touch.metaState, |
| touch.buttonState, |
| touch.xPrecision, |
| touch.yPrecision, |
| touch.deviceId, |
| touch.edgeFlags, |
| touch.source, |
| touch.flags); |
| } |
| |
| public PlatformViewsController() { |
| registry = new PlatformViewRegistryImpl(); |
| vdControllers = new HashMap<>(); |
| accessibilityEventsDelegate = new AccessibilityEventsDelegate(); |
| contextToEmbeddedView = new HashMap<>(); |
| overlayLayerViews = new SparseArray<>(); |
| currentFrameUsedOverlayLayerIds = new HashSet<>(); |
| currentFrameUsedPlatformViewIds = new HashSet<>(); |
| viewWrappers = new SparseArray<>(); |
| platformViews = new SparseArray<>(); |
| platformViewParent = new SparseArray<>(); |
| |
| motionEventTracker = MotionEventTracker.getInstance(); |
| } |
| |
| /** |
| * Attaches this platform views controller to its input and output channels. |
| * |
| * @param context The base context that will be passed to embedded views created by this |
| * controller. This should be the context of the Activity hosting the Flutter application. |
| * @param textureRegistry The texture registry which provides the output textures into which the |
| * embedded views will be rendered. |
| * @param dartExecutor The dart execution context, which is used to set up a system channel. |
| */ |
| public void attach( |
| @Nullable Context context, |
| @NonNull TextureRegistry textureRegistry, |
| @NonNull DartExecutor dartExecutor) { |
| if (this.context != null) { |
| throw new AssertionError( |
| "A PlatformViewsController can only be attached to a single output target.\n" |
| + "attach was called while the PlatformViewsController was already attached."); |
| } |
| this.context = context; |
| this.textureRegistry = textureRegistry; |
| platformViewsChannel = new PlatformViewsChannel(dartExecutor); |
| platformViewsChannel.setPlatformViewsHandler(channelHandler); |
| } |
| |
| /** |
| * Sets whether Flutter uses software rendering. |
| * |
| * <p>When software rendering is used, no GL context is available on the raster thread. When this |
| * is set to true, there's no Flutter composition of Android views and Flutter widgets since GL |
| * textures cannot be used. |
| * |
| * <p>Software rendering is only used for testing in emulators, and it should never be set to true |
| * in a production environment. |
| * |
| * @param useSoftwareRendering Whether software rendering is used. |
| */ |
| public void setSoftwareRendering(boolean useSoftwareRendering) { |
| usesSoftwareRendering = useSoftwareRendering; |
| } |
| |
| /** |
| * Detaches this platform views controller. |
| * |
| * <p>This is typically called when a Flutter applications moves to run in the background, or is |
| * destroyed. After calling this the platform views controller will no longer listen to it's |
| * previous messenger, and will not maintain references to the texture registry, context, and |
| * messenger passed to the previous attach call. |
| */ |
| @UiThread |
| public void detach() { |
| if (platformViewsChannel != null) { |
| platformViewsChannel.setPlatformViewsHandler(null); |
| } |
| destroyOverlaySurfaces(); |
| platformViewsChannel = null; |
| context = null; |
| textureRegistry = null; |
| } |
| |
| /** |
| * Attaches the controller to a {@link FlutterView}. |
| * |
| * <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called |
| * after the device rotates since the FlutterView is recreated after a rotation. |
| */ |
| public void attachToView(@NonNull FlutterView newFlutterView) { |
| flutterView = newFlutterView; |
| // Add wrapper for platform views that use GL texture. |
| for (int index = 0; index < viewWrappers.size(); index++) { |
| final PlatformViewWrapper view = viewWrappers.valueAt(index); |
| flutterView.addView(view); |
| } |
| // Add wrapper for platform views that are composed at the view hierarchy level. |
| for (int index = 0; index < platformViewParent.size(); index++) { |
| final FlutterMutatorView view = platformViewParent.valueAt(index); |
| flutterView.addView(view); |
| } |
| // Notify platform views that they are now attached to a FlutterView. |
| for (int index = 0; index < platformViews.size(); index++) { |
| final PlatformView view = platformViews.valueAt(index); |
| view.onFlutterViewAttached(flutterView); |
| } |
| } |
| |
| /** |
| * Detaches the controller from {@link FlutterView}. |
| * |
| * <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called |
| * when the device rotates since the FlutterView is detached from the fragment. The next time the |
| * fragment needs to be displayed, a new Flutter view is created, so attachToView is called again. |
| */ |
| public void detachFromView() { |
| // Remove wrapper for platform views that use GL texture. |
| for (int index = 0; index < viewWrappers.size(); index++) { |
| final PlatformViewWrapper view = viewWrappers.valueAt(index); |
| flutterView.removeView(view); |
| } |
| // Remove wrapper for platform views that are composed at the view hierarchy level. |
| for (int index = 0; index < platformViewParent.size(); index++) { |
| final FlutterMutatorView view = platformViewParent.valueAt(index); |
| flutterView.removeView(view); |
| } |
| |
| destroyOverlaySurfaces(); |
| removeOverlaySurfaces(); |
| flutterView = null; |
| flutterViewConvertedToImageView = false; |
| |
| // Notify that the platform view have been detached from FlutterView. |
| for (int index = 0; index < platformViews.size(); index++) { |
| final PlatformView view = platformViews.valueAt(index); |
| view.onFlutterViewDetached(); |
| } |
| } |
| |
| @Override |
| public void attachAccessibilityBridge(@NonNull AccessibilityBridge accessibilityBridge) { |
| accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge); |
| } |
| |
| @Override |
| public void detachAccessibilityBridge() { |
| accessibilityEventsDelegate.setAccessibilityBridge(null); |
| } |
| |
| /** |
| * Attaches this controller to a text input plugin. |
| * |
| * <p>While a text input plugin is available, the platform views controller interacts with it to |
| * facilitate delegation of text input connections to platform views. |
| * |
| * <p>A platform views controller should be attached to a text input plugin whenever it is |
| * possible for the Flutter framework to receive text input. |
| */ |
| public void attachTextInputPlugin(@NonNull TextInputPlugin textInputPlugin) { |
| this.textInputPlugin = textInputPlugin; |
| } |
| |
| /** Detaches this controller from the currently attached text input plugin. */ |
| public void detachTextInputPlugin() { |
| textInputPlugin = null; |
| } |
| |
| /** |
| * Returns true if Flutter should perform input connection proxying for the view. |
| * |
| * <p>If the view is a platform view managed by this platform views controller returns true. Else |
| * if the view was created in a platform view's VD, delegates the decision to the platform view's |
| * {@link View#checkInputConnectionProxy(View)} method. Else returns false. |
| */ |
| public boolean checkInputConnectionProxy(@Nullable View view) { |
| // View can be null on some devices |
| // See: https://github.com/flutter/flutter/issues/36517 |
| if (view == null) { |
| return false; |
| } |
| if (!contextToEmbeddedView.containsKey(view.getContext())) { |
| return false; |
| } |
| View platformView = contextToEmbeddedView.get(view.getContext()); |
| if (platformView == view) { |
| return true; |
| } |
| return platformView.checkInputConnectionProxy(view); |
| } |
| |
| public PlatformViewRegistry getRegistry() { |
| return registry; |
| } |
| |
| /** |
| * Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link |
| * PlatformViewsController} attaches to JNI. |
| */ |
| public void onAttachedToJNI() { |
| // Currently no action needs to be taken after JNI attachment. |
| } |
| |
| /** |
| * Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link |
| * PlatformViewsController} detaches from JNI. |
| */ |
| public void onDetachedFromJNI() { |
| diposeAllViews(); |
| } |
| |
| public void onPreEngineRestart() { |
| diposeAllViews(); |
| } |
| |
| @Override |
| @Nullable |
| public View getPlatformViewById(int viewId) { |
| if (usesVirtualDisplay(viewId)) { |
| final VirtualDisplayController controller = vdControllers.get(viewId); |
| return controller.getView(); |
| } |
| |
| final PlatformView platformView = platformViews.get(viewId); |
| if (platformView == null) { |
| return null; |
| } |
| return platformView.getView(); |
| } |
| |
| @Override |
| public boolean usesVirtualDisplay(int id) { |
| return vdControllers.containsKey(id); |
| } |
| |
| private void lockInputConnection(@NonNull VirtualDisplayController controller) { |
| if (textInputPlugin == null) { |
| return; |
| } |
| textInputPlugin.lockPlatformViewInputConnection(); |
| controller.onInputConnectionLocked(); |
| } |
| |
| private void unlockInputConnection(@NonNull VirtualDisplayController controller) { |
| if (textInputPlugin == null) { |
| return; |
| } |
| textInputPlugin.unlockPlatformViewInputConnection(); |
| controller.onInputConnectionUnlocked(); |
| } |
| |
| private static boolean validateDirection(int direction) { |
| return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) { |
| List<Object> rawProperties = (List<Object>) rawPropertiesList; |
| List<PointerProperties> pointerProperties = new ArrayList<>(); |
| for (Object o : rawProperties) { |
| pointerProperties.add(parsePointerProperties(o)); |
| } |
| return pointerProperties; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static PointerProperties parsePointerProperties(Object rawProperties) { |
| List<Object> propertiesList = (List<Object>) rawProperties; |
| PointerProperties properties = new MotionEvent.PointerProperties(); |
| properties.id = (int) propertiesList.get(0); |
| properties.toolType = (int) propertiesList.get(1); |
| return properties; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static List<PointerCoords> parsePointerCoordsList(Object rawCoordsList, float density) { |
| List<Object> rawCoords = (List<Object>) rawCoordsList; |
| List<PointerCoords> pointerCoords = new ArrayList<>(); |
| for (Object o : rawCoords) { |
| pointerCoords.add(parsePointerCoords(o, density)); |
| } |
| return pointerCoords; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static PointerCoords parsePointerCoords(Object rawCoords, float density) { |
| List<Object> coordsList = (List<Object>) rawCoords; |
| PointerCoords coords = new MotionEvent.PointerCoords(); |
| coords.orientation = (float) (double) coordsList.get(0); |
| coords.pressure = (float) (double) coordsList.get(1); |
| coords.size = (float) (double) coordsList.get(2); |
| coords.toolMajor = (float) (double) coordsList.get(3) * density; |
| coords.toolMinor = (float) (double) coordsList.get(4) * density; |
| coords.touchMajor = (float) (double) coordsList.get(5) * density; |
| coords.touchMinor = (float) (double) coordsList.get(6) * density; |
| coords.x = (float) (double) coordsList.get(7) * density; |
| coords.y = (float) (double) coordsList.get(8) * density; |
| return coords; |
| } |
| |
| private float getDisplayDensity() { |
| return context.getResources().getDisplayMetrics().density; |
| } |
| |
| private int toPhysicalPixels(double logicalPixels) { |
| return (int) Math.round(logicalPixels * getDisplayDensity()); |
| } |
| |
| private int toLogicalPixels(double physicalPixels, float displayDensity) { |
| return (int) Math.round(physicalPixels / displayDensity); |
| } |
| |
| private int toLogicalPixels(double physicalPixels) { |
| return toLogicalPixels(physicalPixels, getDisplayDensity()); |
| } |
| |
| private void diposeAllViews() { |
| while (platformViews.size() > 0) { |
| final int viewId = platformViews.keyAt(0); |
| // Dispose deletes the entry from platformViews and clears associated resources. |
| channelHandler.dispose(viewId); |
| } |
| } |
| |
| private void initializeRootImageViewIfNeeded() { |
| if (synchronizeToNativeViewHierarchy && !flutterViewConvertedToImageView) { |
| flutterView.convertToImageView(); |
| flutterViewConvertedToImageView = true; |
| } |
| } |
| |
| /** |
| * Initializes a platform view and adds it to the view hierarchy. |
| * |
| * @param viewId The view ID. This member is not intended for public use, and is only visible for |
| * testing. |
| */ |
| @VisibleForTesting |
| @TargetApi(Build.VERSION_CODES.KITKAT) |
| void initializePlatformViewIfNeeded(int viewId) { |
| final PlatformView platformView = platformViews.get(viewId); |
| if (platformView == null) { |
| throw new IllegalStateException( |
| "Platform view hasn't been initialized from the platform view channel."); |
| } |
| if (platformViewParent.get(viewId) != null) { |
| return; |
| } |
| final View embeddedView = platformView.getView(); |
| if (embeddedView == null) { |
| throw new IllegalStateException( |
| "PlatformView#getView() returned null, but an Android view reference was expected."); |
| } |
| if (embeddedView.getParent() != null) { |
| throw new IllegalStateException( |
| "The Android view returned from PlatformView#getView() was already added to a parent view."); |
| } |
| final FlutterMutatorView parentView = |
| new FlutterMutatorView( |
| context, context.getResources().getDisplayMetrics().density, androidTouchProcessor); |
| |
| parentView.setOnDescendantFocusChangeListener( |
| (view, hasFocus) -> { |
| if (hasFocus) { |
| platformViewsChannel.invokeViewFocused(viewId); |
| } else if (textInputPlugin != null) { |
| textInputPlugin.clearPlatformViewClient(viewId); |
| } |
| }); |
| |
| platformViewParent.put(viewId, parentView); |
| |
| // Accessibility in the embedded view is initially disabled because if a Flutter app disabled |
| // accessibility in the first frame, the embedding won't receive an update to disable |
| // accessibility since the embedding never received an update to enable it. |
| // The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas when |
| // the framework sends a new a11y tree to the embedding. |
| // To prevent races, the framework populate the SemanticsNode after the platform view has been |
| // created. |
| embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); |
| |
| parentView.addView(embeddedView); |
| flutterView.addView(parentView); |
| } |
| |
| public void attachToFlutterRenderer(@NonNull FlutterRenderer flutterRenderer) { |
| androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ true); |
| } |
| |
| /** |
| * Called when a platform view id displayed in the current frame. |
| * |
| * @param viewId The ID of the platform view. |
| * @param x The left position relative to {@code FlutterView}. |
| * @param y The top position relative to {@code FlutterView}. |
| * @param width The width of the platform view. |
| * @param height The height of the platform view. |
| * @param viewWidth The original width of the platform view before applying the mutator stack. |
| * @param viewHeight The original height of the platform view before applying the mutator stack. |
| * @param mutatorsStack The mutator stack. This member is not intended for public use, and is only |
| * visible for testing. |
| */ |
| public void onDisplayPlatformView( |
| int viewId, |
| int x, |
| int y, |
| int width, |
| int height, |
| int viewWidth, |
| int viewHeight, |
| @NonNull FlutterMutatorsStack mutatorsStack) { |
| initializeRootImageViewIfNeeded(); |
| initializePlatformViewIfNeeded(viewId); |
| |
| final FlutterMutatorView parentView = platformViewParent.get(viewId); |
| parentView.readyToDisplay(mutatorsStack, x, y, width, height); |
| parentView.setVisibility(View.VISIBLE); |
| parentView.bringToFront(); |
| |
| final FrameLayout.LayoutParams layoutParams = |
| new FrameLayout.LayoutParams(viewWidth, viewHeight); |
| final View view = platformViews.get(viewId).getView(); |
| if (view != null) { |
| view.setLayoutParams(layoutParams); |
| view.bringToFront(); |
| } |
| currentFrameUsedPlatformViewIds.add(viewId); |
| } |
| |
| /** |
| * Called when an overlay surface is displayed in the current frame. |
| * |
| * @param id The ID of the surface. |
| * @param x The left position relative to {@code FlutterView}. |
| * @param y The top position relative to {@code FlutterView}. |
| * @param width The width of the surface. |
| * @param height The height of the surface. This member is not intended for public use, and is |
| * only visible for testing. |
| */ |
| public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) { |
| if (overlayLayerViews.get(id) == null) { |
| throw new IllegalStateException("The overlay surface (id:" + id + ") doesn't exist"); |
| } |
| initializeRootImageViewIfNeeded(); |
| |
| final PlatformOverlayView overlayView = overlayLayerViews.get(id); |
| if (overlayView.getParent() == null) { |
| flutterView.addView(overlayView); |
| } |
| |
| FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams((int) width, (int) height); |
| layoutParams.leftMargin = (int) x; |
| layoutParams.topMargin = (int) y; |
| overlayView.setLayoutParams(layoutParams); |
| overlayView.setVisibility(View.VISIBLE); |
| overlayView.bringToFront(); |
| currentFrameUsedOverlayLayerIds.add(id); |
| } |
| |
| public void onBeginFrame() { |
| currentFrameUsedOverlayLayerIds.clear(); |
| currentFrameUsedPlatformViewIds.clear(); |
| } |
| |
| /** |
| * Called by {@code FlutterJNI} when the Flutter frame was submitted. |
| * |
| * <p>This member is not intended for public use, and is only visible for testing. |
| */ |
| public void onEndFrame() { |
| // 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 (flutterViewConvertedToImageView && currentFrameUsedPlatformViewIds.isEmpty()) { |
| flutterViewConvertedToImageView = false; |
| flutterView.revertImageView( |
| () -> { |
| // Destroy overlay surfaces once the surface reversion is completed. |
| finishFrame(false); |
| }); |
| return; |
| } |
| // 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. |
| final boolean isFrameRenderedUsingImageReaders = |
| flutterViewConvertedToImageView && flutterView.acquireLatestImageViewFrame(); |
| finishFrame(isFrameRenderedUsingImageReaders); |
| } |
| |
| private void finishFrame(boolean isFrameRenderedUsingImageReaders) { |
| for (int i = 0; i < overlayLayerViews.size(); i++) { |
| final int overlayId = overlayLayerViews.keyAt(i); |
| final PlatformOverlayView overlayView = overlayLayerViews.valueAt(i); |
| |
| if (currentFrameUsedOverlayLayerIds.contains(overlayId)) { |
| flutterView.attachOverlaySurfaceToRender(overlayView); |
| final 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); |
| flutterView.removeView(overlayView); |
| } |
| } |
| |
| for (int i = 0; i < platformViewParent.size(); i++) { |
| final int viewId = platformViewParent.keyAt(i); |
| final View parentView = platformViewParent.get(viewId); |
| |
| // This should only show platform views that are rendered in this frame and either: |
| // 1. Surface has images available in this frame or, |
| // 2. Surface does not have images available in this frame because the render surface should |
| // not be an ImageView. |
| // |
| // The platform view is appended to a mutator view. |
| // |
| // Otherwise, hide the platform view, but don't remove it from the view hierarchy yet as |
| // they are removed when the framework disposes the platform view widget. |
| if (currentFrameUsedPlatformViewIds.contains(viewId) |
| && (isFrameRenderedUsingImageReaders || !synchronizeToNativeViewHierarchy)) { |
| parentView.setVisibility(View.VISIBLE); |
| } else { |
| parentView.setVisibility(View.GONE); |
| } |
| } |
| } |
| |
| /** |
| * Creates and tracks the overlay surface. |
| * |
| * @param imageView The surface that displays the overlay. |
| * @return Wrapper object that provides the layer id and the surface. This member is not intended |
| * for public use, and is only visible for testing. |
| */ |
| @VisibleForTesting |
| @TargetApi(19) |
| @NonNull |
| public FlutterOverlaySurface createOverlaySurface(@NonNull PlatformOverlayView imageView) { |
| final int id = nextOverlayLayerId++; |
| overlayLayerViews.put(id, imageView); |
| return new FlutterOverlaySurface(id, imageView.getSurface()); |
| } |
| |
| /** |
| * Creates an overlay surface while the Flutter view is rendered by {@code PlatformOverlayView}. |
| * |
| * <p>This method is invoked by {@code FlutterJNI} only. |
| * |
| * <p>This member is not intended for public use, and is only visible for testing. |
| */ |
| @TargetApi(19) |
| @NonNull |
| 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. |
| return createOverlaySurface( |
| new PlatformOverlayView( |
| flutterView.getContext(), |
| flutterView.getWidth(), |
| flutterView.getHeight(), |
| accessibilityEventsDelegate)); |
| } |
| |
| /** |
| * Destroys the overlay surfaces and removes them from the view hierarchy. |
| * |
| * <p>This method is used only internally by {@code FlutterJNI}. |
| */ |
| public void destroyOverlaySurfaces() { |
| for (int viewId = 0; viewId < overlayLayerViews.size(); viewId++) { |
| final PlatformOverlayView overlayView = overlayLayerViews.valueAt(viewId); |
| overlayView.detachFromRenderer(); |
| overlayView.closeImageReader(); |
| // Don't remove overlayView from the view hierarchy since this method can |
| // be called while the Android framework is iterating over the array of views. |
| // See ViewGroup#dispatchDetachedFromWindow(), and |
| // https://github.com/flutter/flutter/issues/97679. |
| } |
| } |
| |
| private void removeOverlaySurfaces() { |
| if (flutterView == null) { |
| Log.e(TAG, "removeOverlaySurfaces called while flutter view is null"); |
| return; |
| } |
| for (int viewId = 0; viewId < overlayLayerViews.size(); viewId++) { |
| flutterView.removeView(overlayLayerViews.valueAt(viewId)); |
| } |
| overlayLayerViews.clear(); |
| } |
| |
| @VisibleForTesting |
| public SparseArray<PlatformOverlayView> getOverlayLayerViews() { |
| return overlayLayerViews; |
| } |
| } |