| # Background | |
| Virtual Display is one of several modes for displaying platform views on Android. See [Android Platform Views](Android-Platform-Views.md) for an overview of modes. | |
| # The approach | |
| `AndroidView` widgets need to be composited within the Flutter UI and | |
| interleaved between Flutter widgets. However the entire Flutter UI is | |
| rendered to a single texture. | |
| In order to solve this problem, Flutter inflates and renders `AndroidView` | |
| widgets inside of | |
| [VirtualDisplays](https://developer.android.com/reference/android/hardware/display/VirtualDisplay#summary) | |
| instead of attempting to add it to the Android View hierarchy alongside | |
| Flutter's texture directly. The `VirtualDisplay` renders its output to a raw | |
| graphical buffer (accessed through | |
| [`getSurface()`](https://developer.android.com/reference/android/hardware/display/VirtualDisplay#getSurface())), | |
| and not to any actual real display(s) of the device. This allows Flutter to | |
| graphically interleave the Android View inside of its own Flutter widget tree | |
| by taking the texture from the `VirtualDisplay` output and treating it as a | |
| texture associated with any other Flutter widget in its internal hierarchy. | |
| Then the `VirtualDisplay's` `Surface` output is composited with the rest of | |
| the Flutter widget hierarchy and rendered as part of Flutter's larger texture | |
| output on Android. | |
| # Associated problems and workarounds | |
| While this approach unblocks the core problem with embedding Android views in | |
| a Flutter UI visually, the `VirtualDisplay` choice causes a long tail of issues | |
| that we've needed to work around. | |
| The heart of the problem with this approach is that the Android View inside of | |
| the `VirtualDisplay` is, as far as Android is concerned, inside of its own View | |
| hierarchy in a completely invisible | |
| [Display](https://developer.android.com/reference/android/view/Display.html) and | |
| completely unconnected and unrelated to the View hierarchy that contains the | |
| Flutter texture output. | |
| Much of the internal functionality to Android depends on walking the View | |
| hierarchy and querying information about the given View in its current hierarchy | |
| and window in order to function. Since the information for the embedded view is | |
| "wrong" here this internal functionality tends to break down. In addition to | |
| that this logic can change based on Android version, so we've needed to case | |
| some of our logic based on the runtime version of the OS. | |
| Some of our issues with this approach and extended workarounds are described | |
| below. | |
| To see all known issues specific to this mode, search for the [`vd-only` label](https://github.com/flutter/flutter/labels/vd-only). | |
| ## Touch events | |
| By default touch events would not work with PlatformViews. The Android View is | |
| inflated inside of a `VirtualDisplay`. Whenever the user taps what they _see_ as | |
| the Android View on their primary display, they're "really" tapping the part of | |
| Flutter's texture output that we're rendering to look like the actual Android | |
| View. The touch event created by their input is getting sent straight to | |
| Flutter's View, not to the Android View they're actually trying to tap on. | |
| ### The workaround | |
| - We detect whether or not the touch from the user should collide with the | |
| `AndroidView` Flutter widget using [hit testing logic within the Flutter | |
| Framework](https://github.com/flutter/flutter/blob/068fa84/packages/flutter/lib/src/rendering/platform_view.dart#L774). | |
| - When the touch is a hit, we [dispatch a message to the Android engine | |
| embedding containing the details of the touch | |
| event](https://github.com/flutter/flutter/blob/068fa84/packages/flutter/lib/src/rendering/platform_view.dart#L595). | |
| - [Within the Android | |
| embedding](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L408), | |
| we convert the coordinates of this event from the ones describing the touch | |
| inside of the larger Flutter texture to ones that would match the real Android | |
| View inside of its `VirtualDisplay`. We then create a new `MotionEvent` | |
| describing the touch and forward it to the real Android View inside of its | |
| `VirtualDisplay`. | |
| ### Limitations | |
| - We're dispatching the new `MotionEvent` directly to the known Android View | |
| embedded by the user. In cases where the View spawns other Views this dispatch | |
| may be sent to the wrong place. | |
| - We're synthesizing a new `MotionEvent` based on the information handed to us | |
| by the Flutter framework. It's possible that there's data being lost and not | |
| totally carried over to our new `MotionEvent`, or some other general problem | |
| with synthesizing `MotionEvent`s. | |
| ## Accessibility | |
| ### Background | |
| Explaining our problems with accessibility (or "a11y") requires a brief detour | |
| into explaining a11y itself on Android, and how a11y typically works on Flutter. | |
| Android itself builds up a tree of a11y nodes for each Android View rendered to | |
| the screen. Each a11y node contains semantic information about what information | |
| is represented by a UI element: for example, whether it's a button, and what the | |
| text on button is. Typically the OS builds this information itself by directly | |
| walking the View hierarchy. A11y services on the device then use this tree of | |
| a11y nodes to deliver the semantic information in an accessible way to the user, | |
| by example drawing highlights over interactable UI elements and reading the | |
| semantic information described by the UI aloud to the user. Users can then use | |
| a11y services to interact with a UI, for example by using it to activate the | |
| button without needing to press once directly on where the button is graphically | |
| rendering on screen. | |
| Some UIs, like Flutter and WebViews, don't have a tree of Android Views to | |
| describe their UIs. For these Android has a concept of a "virtual" a11y node | |
| hierarchy. Instead of walking the View hierarchy, Android Views may implement | |
| an | |
| [AccessibilityNodeProvider](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeProvider) | |
| that returns a tree of a11y nodes to describe whatever is being rendered | |
| within its own View subtree. The Flutter Android embedding uses this concept to | |
| generate an a11y node tree that matches the | |
| [`Semantics`](https://api.flutter.dev/flutter/widgets/Semantics-class.html) | |
| tree created by the Flutter framework. | |
| ### The problem | |
| Theoretically we should have been able to attach the a11y tree from the | |
| embedded Android View to our `AccessibilityNodeProvider` in Flutter, but in | |
| reality we've found that any attempts to do so directly fail. The reality of | |
| the situation appears to be that a11y code in Android frequently relies on | |
| the View hierarchy for state management and querying extra information, so | |
| attempting to parent the child’s hierarchy from the embedded view to our | |
| virtual hierarchy fails to really correctly associate the underlying view | |
| hierarchy in a functional way for a11y. In other words, reparenting the a11y | |
| node trees only "fixes" the a11y node tree itself, but there are still multiple | |
| places where Android a11y code relies on being able to query the "real" view | |
| hierarchy for required info and ends up failing with bugs in our reparented | |
| case. | |
| ### The workaround | |
| - Create a mirror copy of the Android View's a11y nodes as a subtree within [the | |
| Flutter Android | |
| embedding](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java). | |
| This mirror copy consists of virtual nodes and is part of the larger virtual | |
| a11y node tree created by [Flutter's | |
| `AccessibilityNodeProvider`](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L79). | |
| - (P and above only): blacklisted APIs and reflection are usually used to create | |
| a mirror copy of the Android View's a11y nodes. The critical private | |
| information are the node IDs of the parent and children of the a11y hierarchy, | |
| which we need to construct our mirror correctly but which are private data to | |
| the nodes. On newer Android versions [we read the data we need from the | |
| serialized binary form of the | |
| node](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java#L555) | |
| instead. | |
| - [We forward all events sent to mirror a11y nodes to the "real" a11y nodes in | |
| the Android View's subtree as | |
| well](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java#L263). | |
| This forwarding involves translating any coordinates on the event, similar to | |
| forwarding touch events. This means users can use a11y services to still | |
| interact with the embedded Android UI. | |
| ### Limitations | |
| - Only pre-existing virtual hierarchies can be duplicated and mirrored to our | |
| own virtual subtree in this way. That means that embedded views like WebView | |
| are accessible through this mechanism, but things like `Button` are not. | |
| - The use of reflection and serialization to read private a11y node data in | |
| order to make an accurate mirror tree is extremely fragile. This could | |
| easily be broken by Android in a future release. | |
| Tracked in | |
| [flutter/flutter#19418](https://github.com/flutter/flutter/issues/19418). Better | |
| support for reparenting a11y hierarchies is also an [Android SDK issue](https://issuetracker.google.com/issues/138442751). | |
| ## Text input | |
| Normally the embedded Android view wouldn’t be able to get any text input | |
| because it's inside of a `VirtualDisplay` that is always reported as being | |
| unfocused. Android doesn't offer any APIs to dynamically set or change the focus | |
| of a `Window`. The focused `Window` for a Flutter app would normally be the one | |
| actually holding the "real" Flutter texture and UI, and directly visible to the | |
| user. `InputConnections` (how text is entered in Android) to unfocused Views are | |
| normally discarded. | |
| ### Workaround | |
| - We override | |
| [checkInputConnectionProxy](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L941) | |
| so that Android treats the Flutter View as a proxy for the Input Method Editor | |
| (IME) when it tries to talk to the embedded Android View. Basically, Android | |
| asks the Flutter View for an `InputConnection` when the embedded Android View | |
| wants one. | |
| - Q changed the `InputMethodManager` (IMM) to be instantiated per `Window` | |
| instead of being a global singleton. So our naive attempt at setting up a | |
| proxy no longer worked on Q. To work around this further we [created a | |
| subclass of `Context` that returned the same `IMM` as the Flutter View instead | |
| of the real `IMM` for the window when `getSystemService` is | |
| queried](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java#L224). | |
| This means whenever Android tries to use the `IMM` in our `VirtualDisplay` it | |
| still sees and uses the `IMM` from the real display that has been set to use | |
| the Flutter View as a proxy. | |
| - When the Flutter Android embedding is asked for an `InputConnection`, it | |
| checks to see if an embedded Android View is "really" the target of the input. | |
| If so it [internally goes to the embedded Android View, gets the | |
| `InputConnection` from | |
| there](https://github.com/flutter/flutter/blob/e3852df571a4e313bdab85777ac13affcd089d8c/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java#L328), | |
| and returns it to Android as if the embedded Android View's `InputConnection` | |
| is its own. | |
| - Android sees that the Flutter view is focused and available, so it takes the | |
| `InputConnection` that's ultimately from the embedded Android View and uses it | |
| successfully. | |
| ### In embedded WebViews | |
| WebViews running on Android versions pre N have additional complications because | |
| they have their own internal logic for creating and setting up input connections | |
| that don't completely defer to Android. Within the `flutter_webview` plugin | |
| we've also needed to add extra workarounds in order to enable text input there. | |
| - [Set a proxy view that listens for input connections on the same thread as | |
| WebView.](https://github.com/flutter/packages/blob/27f3de3/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java#L113). | |
| Without this WebView would internally consume all `InputConnection` calls | |
| without the Flutter View proxy ever being notified. | |
| - [Within the proxy thread, refer back to the Flutter View for input | |
| creation.](https://github.com/flutter/packages/blob/27f3de3e1e6ed1c0f2cd23b0d1477ff3f0955aaa/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java#L67). | |
| - [Reset the input connection back to the Flutter thread when the WebView is | |
| unfocused](https://github.com/flutter/packages/blob/27f3de3/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java#L128). | |
| This prevents the text input from being "stuck" inside of the WebView. | |
| ### Limitations | |
| - In general this depends on internal Android behavior and is brittle. The Q | |
| workaround was a surprise for us as we were broken by what was supposed to be | |
| an internal Android refactoring. | |
| - Some text functionality is still broken. For example the "Copy" and "Share" | |
| dialogue is currently not usable. |