1 // Copyright 2013 The Flutter Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package io.flutter.embedding.android; 6 7 import android.annotation.TargetApi; 8 import android.annotation.SuppressLint; 9 import android.content.Context; 10 import android.content.res.Configuration; 11 import android.graphics.Insets; 12 import android.graphics.Rect; 13 import android.os.Build; 14 import android.os.LocaleList; 15 import android.support.annotation.NonNull; 16 import android.support.annotation.Nullable; 17 import android.support.annotation.RequiresApi; 18 import android.support.annotation.VisibleForTesting; 19 import android.text.format.DateFormat; 20 import android.util.AttributeSet; 21 import android.view.KeyEvent; 22 import android.view.MotionEvent; 23 import android.view.View; 24 import android.view.WindowInsets; 25 import android.view.accessibility.AccessibilityManager; 26 import android.view.accessibility.AccessibilityNodeProvider; 27 import android.view.inputmethod.EditorInfo; 28 import android.view.inputmethod.InputConnection; 29 import android.widget.FrameLayout; 30 31 import java.util.ArrayList; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Locale; 35 import java.util.Set; 36 37 import io.flutter.Log; 38 import io.flutter.embedding.engine.FlutterEngine; 39 import io.flutter.embedding.engine.renderer.FlutterRenderer; 40 import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; 41 import io.flutter.plugin.editing.TextInputPlugin; 42 import io.flutter.plugin.platform.PlatformViewsController; 43 import io.flutter.view.AccessibilityBridge; 44 45 /** 46 * Displays a Flutter UI on an Android device. 47 * <p> 48 * A {@code FlutterView}'s UI is painted by a corresponding {@link FlutterEngine}. 49 * <p> 50 * A {@code FlutterView} can operate in 2 different {@link RenderMode}s: 51 * <ol> 52 * <li>{@link RenderMode#surface}, which paints a Flutter UI to a {@link android.view.SurfaceView}. 53 * This mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned 54 * between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed. 55 * Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required, 56 * developers should strongly prefer this render mode.</li> 57 * <li>{@link RenderMode#texture}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}. 58 * This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in this 59 * mode can be animated and transformed, as well as positioned in the z-index between 2+ other 60 * Android {@code Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture} 61 * are required, developers should strongly prefer the {@link RenderMode#surface} render mode.</li> 62 * </ol> 63 * See <a>https://source.android.com/devices/graphics/arch-tv#surface_or_texture</a> for more 64 * information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}. 65 */ 66 public class FlutterView extends FrameLayout { 67 private static final String TAG = "FlutterView"; 68 69 // Behavior configuration of this FlutterView. 70 @NonNull 71 private RenderMode renderMode; 72 @Nullable 73 private TransparencyMode transparencyMode; 74 75 // Internal view hierarchy references. 76 @Nullable 77 private FlutterRenderer.RenderSurface renderSurface; 78 private final Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>(); 79 private boolean didRenderFirstFrame; 80 81 // Connections to a Flutter execution context. 82 @Nullable 83 private FlutterEngine flutterEngine; 84 @NonNull 85 private final Set<FlutterEngineAttachmentListener> flutterEngineAttachmentListeners = new HashSet<>(); 86 87 // Components that process various types of Android View input and events, 88 // possibly storing intermediate state, and communicating those events to Flutter. 89 // 90 // These components essentially add some additional behavioral logic on top of 91 // existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc. 92 @Nullable 93 private TextInputPlugin textInputPlugin; 94 @Nullable 95 private AndroidKeyProcessor androidKeyProcessor; 96 @Nullable 97 private AndroidTouchProcessor androidTouchProcessor; 98 @Nullable 99 private AccessibilityBridge accessibilityBridge; 100 101 // Directly implemented View behavior that communicates with Flutter. 102 private final FlutterRenderer.ViewportMetrics viewportMetrics = new FlutterRenderer.ViewportMetrics(); 103 104 private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener = new AccessibilityBridge.OnAccessibilityChangeListener() { 105 @Override 106 public void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) { 107 resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled); 108 } 109 }; 110 111 private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { 112 @Override 113 public void onFirstFrameRendered() { 114 didRenderFirstFrame = true; 115 116 for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) { 117 listener.onFirstFrameRendered(); 118 } 119 } 120 }; 121 122 /** 123 * Constructs a {@code FlutterView} programmatically, without any XML attributes. 124 * <p> 125 * <ul> 126 * <li>{@link #renderMode} defaults to {@link RenderMode#surface}.</li> 127 * <li>{@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.</li> 128 * </ul> 129 * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} 130 * to be compatible with {@link PlatformViewsController}. 131 */ FlutterView(@onNull Context context)132 public FlutterView(@NonNull Context context) { 133 this(context, null, null, null); 134 } 135 136 /** 137 * Constructs a {@code FlutterView} programmatically, without any XML attributes, 138 * and allows selection of a {@link #renderMode}. 139 * <p> 140 * {@link #transparencyMode} defaults to {@link TransparencyMode#opaque}. 141 * <p> 142 * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} 143 * to be compatible with {@link PlatformViewsController}. 144 */ FlutterView(@onNull Context context, @NonNull RenderMode renderMode)145 public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) { 146 this(context, null, renderMode, null); 147 } 148 149 /** 150 * Constructs a {@code FlutterView} programmatically, without any XML attributes, 151 * assumes the use of {@link RenderMode#surface}, and allows selection of a {@link #transparencyMode}. 152 * <p> 153 * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} 154 * to be compatible with {@link PlatformViewsController}. 155 */ FlutterView(@onNull Context context, @NonNull TransparencyMode transparencyMode)156 public FlutterView(@NonNull Context context, @NonNull TransparencyMode transparencyMode) { 157 this(context, null, RenderMode.surface, transparencyMode); 158 } 159 160 /** 161 * Constructs a {@code FlutterView} programmatically, without any XML attributes, and allows 162 * a selection of {@link #renderMode} and {@link #transparencyMode}. 163 * <p> 164 * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} 165 * to be compatible with {@link PlatformViewsController}. 166 */ FlutterView(@onNull Context context, @NonNull RenderMode renderMode, @NonNull TransparencyMode transparencyMode)167 public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode, @NonNull TransparencyMode transparencyMode) { 168 this(context, null, renderMode, transparencyMode); 169 } 170 171 /** 172 * Constructs a {@code FlutterSurfaceView} in an XML-inflation-compliant manner. 173 * <p> 174 * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} 175 * to be compatible with {@link PlatformViewsController}. 176 */ 177 // TODO(mattcarroll): expose renderMode in XML when build system supports R.attr FlutterView(@onNull Context context, @Nullable AttributeSet attrs)178 public FlutterView(@NonNull Context context, @Nullable AttributeSet attrs) { 179 this(context, attrs, null, null); 180 } 181 FlutterView(@onNull Context context, @Nullable AttributeSet attrs, @Nullable RenderMode renderMode, @Nullable TransparencyMode transparencyMode)182 private FlutterView(@NonNull Context context, @Nullable AttributeSet attrs, @Nullable RenderMode renderMode, @Nullable TransparencyMode transparencyMode) { 183 super(context, attrs); 184 185 this.renderMode = renderMode == null ? RenderMode.surface : renderMode; 186 this.transparencyMode = transparencyMode != null ? transparencyMode : TransparencyMode.opaque; 187 188 init(); 189 } 190 init()191 private void init() { 192 Log.v(TAG, "Initializing FlutterView"); 193 194 switch (renderMode) { 195 case surface: 196 Log.v(TAG, "Internally using a FlutterSurfaceView."); 197 FlutterSurfaceView flutterSurfaceView = new FlutterSurfaceView(getContext(), transparencyMode == TransparencyMode.transparent); 198 renderSurface = flutterSurfaceView; 199 addView(flutterSurfaceView); 200 break; 201 case texture: 202 Log.v(TAG, "Internally using a FlutterTextureView."); 203 FlutterTextureView flutterTextureView = new FlutterTextureView(getContext()); 204 renderSurface = flutterTextureView; 205 addView(flutterTextureView); 206 break; 207 } 208 209 // FlutterView needs to be focusable so that the InputMethodManager can interact with it. 210 setFocusable(true); 211 setFocusableInTouchMode(true); 212 } 213 214 /** 215 * Returns true if an attached {@link FlutterEngine} has rendered at least 1 frame to this 216 * {@code FlutterView}. 217 * <p> 218 * Returns false if no {@link FlutterEngine} is attached. 219 * <p> 220 * This flag is specific to a given {@link FlutterEngine}. The following hypothetical timeline 221 * demonstrates how this flag changes over time. 222 * <ol> 223 * <li>{@code flutterEngineA} is attached to this {@code FlutterView}: returns false</li> 224 * <li>{@code flutterEngineA} renders its first frame to this {@code FlutterView}: returns true</li> 225 * <li>{@code flutterEngineA} is detached from this {@code FlutterView}: returns false</li> 226 * <li>{@code flutterEngineB} is attached to this {@code FlutterView}: returns false</li> 227 * <li>{@code flutterEngineB} renders its first frame to this {@code FlutterView}: returns true</li> 228 * </ol> 229 */ hasRenderedFirstFrame()230 public boolean hasRenderedFirstFrame() { 231 return didRenderFirstFrame; 232 } 233 234 /** 235 * Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's 236 * first rendered frame. 237 */ addOnFirstFrameRenderedListener(@onNull OnFirstFrameRenderedListener listener)238 public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { 239 onFirstFrameRenderedListeners.add(listener); 240 } 241 242 /** 243 * Removes the given {@code listener}, which was previously added with 244 * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}. 245 */ removeOnFirstFrameRenderedListener(@onNull OnFirstFrameRenderedListener listener)246 public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { 247 onFirstFrameRenderedListeners.remove(listener); 248 } 249 250 //------- Start: Process View configuration that Flutter cares about. ------ 251 /** 252 * Sends relevant configuration data from Android to Flutter when the Android 253 * {@link Configuration} changes. 254 * 255 * The Android {@link Configuration} might change as a result of device orientation 256 * change, device language change, device text scale factor change, etc. 257 */ 258 @Override onConfigurationChanged(@onNull Configuration newConfig)259 protected void onConfigurationChanged(@NonNull Configuration newConfig) { 260 super.onConfigurationChanged(newConfig); 261 Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter."); 262 sendLocalesToFlutter(newConfig); 263 sendUserSettingsToFlutter(); 264 } 265 266 /** 267 * Invoked when this {@code FlutterView} changes size, including upon initial 268 * measure. 269 * 270 * The initial measure reports an {@code oldWidth} and {@code oldHeight} of zero. 271 * 272 * Flutter cares about the width and height of the view that displays it on the host 273 * platform. Therefore, when this method is invoked, the new width and height are 274 * communicated to Flutter as the "physical size" of the view that displays Flutter's 275 * UI. 276 */ 277 @Override onSizeChanged(int width, int height, int oldWidth, int oldHeight)278 protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { 279 super.onSizeChanged(width, height, oldWidth, oldHeight); 280 Log.v(TAG, "Size changed. Sending Flutter new viewport metrics. FlutterView was " 281 + oldWidth + " x " + oldHeight 282 + ", it is now " + width + " x " + height); 283 viewportMetrics.width = width; 284 viewportMetrics.height = height; 285 sendViewportMetricsToFlutter(); 286 } 287 288 /** 289 * Invoked when Android's desired window insets change, i.e., padding. 290 * 291 * Flutter does not use a standard {@code View} hierarchy and therefore Flutter is 292 * unaware of these insets. Therefore, this method calculates the viewport metrics 293 * that Flutter should use and then sends those metrics to Flutter. 294 * 295 * This callback is not present in API < 20, which means lower API devices will see 296 * the wider than expected padding when the status and navigation bars are hidden. 297 */ 298 @Override 299 @TargetApi(20) 300 @RequiresApi(20) 301 // The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings 302 // caused by usage of Android Q APIs. These calls are safe because they are 303 // guarded. 304 @SuppressLint({"InlinedApi", "NewApi"}) 305 @NonNull onApplyWindowInsets(@onNull WindowInsets insets)306 public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) { 307 WindowInsets newInsets = super.onApplyWindowInsets(insets); 308 309 // Status bar (top) and left/right system insets should partially obscure the content (padding). 310 viewportMetrics.paddingTop = insets.getSystemWindowInsetTop(); 311 viewportMetrics.paddingRight = insets.getSystemWindowInsetRight(); 312 viewportMetrics.paddingBottom = 0; 313 viewportMetrics.paddingLeft = insets.getSystemWindowInsetLeft(); 314 315 // Bottom system inset (keyboard) should adjust scrollable bottom edge (inset). 316 viewportMetrics.viewInsetTop = 0; 317 viewportMetrics.viewInsetRight = 0; 318 viewportMetrics.viewInsetBottom = insets.getSystemWindowInsetBottom(); 319 viewportMetrics.viewInsetLeft = 0; 320 321 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 322 Insets systemGestureInsets = insets.getSystemGestureInsets(); 323 viewportMetrics.systemGestureInsetTop = systemGestureInsets.top; 324 viewportMetrics.systemGestureInsetRight = systemGestureInsets.right; 325 viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom; 326 viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left; 327 } 328 329 Log.v(TAG, "Updating window insets (onApplyWindowInsets()):\n" 330 + "Status bar insets: Top: " + viewportMetrics.paddingTop 331 + ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n" 332 + "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom 333 + ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight 334 + "System Gesture Insets - Left: " + viewportMetrics.systemGestureInsetLeft + ", Top: " + viewportMetrics.systemGestureInsetTop 335 + ", Right: " + viewportMetrics.systemGestureInsetRight + ", Bottom: " + viewportMetrics.viewInsetBottom); 336 337 sendViewportMetricsToFlutter(); 338 339 return newInsets; 340 } 341 342 /** 343 * Invoked when Android's desired window insets change, i.e., padding. 344 * 345 * {@code fitSystemWindows} is an earlier version of 346 * {@link #onApplyWindowInsets(WindowInsets)}. See that method for more details 347 * about how window insets relate to Flutter. 348 */ 349 @Override 350 @SuppressWarnings("deprecation") fitSystemWindows(@onNull Rect insets)351 protected boolean fitSystemWindows(@NonNull Rect insets) { 352 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { 353 // Status bar, left/right system insets partially obscure content (padding). 354 viewportMetrics.paddingTop = insets.top; 355 viewportMetrics.paddingRight = insets.right; 356 viewportMetrics.paddingBottom = 0; 357 viewportMetrics.paddingLeft = insets.left; 358 359 // Bottom system inset (keyboard) should adjust scrollable bottom edge (inset). 360 viewportMetrics.viewInsetTop = 0; 361 viewportMetrics.viewInsetRight = 0; 362 viewportMetrics.viewInsetBottom = insets.bottom; 363 viewportMetrics.viewInsetLeft = 0; 364 365 Log.v(TAG, "Updating window insets (fitSystemWindows()):\n" 366 + "Status bar insets: Top: " + viewportMetrics.paddingTop 367 + ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n" 368 + "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom 369 + ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight); 370 371 sendViewportMetricsToFlutter(); 372 return true; 373 } else { 374 return super.fitSystemWindows(insets); 375 } 376 } 377 //------- End: Process View configuration that Flutter cares about. -------- 378 379 //-------- Start: Process UI I/O that Flutter cares about. ------- 380 /** 381 * Creates an {@link InputConnection} to work with a {@link android.view.inputmethod.InputMethodManager}. 382 * 383 * Any {@code View} that can take focus or process text input must implement this 384 * method by returning a non-null {@code InputConnection}. Flutter may render one or 385 * many focusable and text-input widgets, therefore {@code FlutterView} must support 386 * an {@code InputConnection}. 387 * 388 * The {@code InputConnection} returned from this method comes from a 389 * {@link TextInputPlugin}, which is owned by this {@code FlutterView}. A 390 * {@link TextInputPlugin} exists to encapsulate the nuances of input communication, 391 * rather than spread that logic throughout this {@code FlutterView}. 392 */ 393 @Override 394 @Nullable onCreateInputConnection(@onNull EditorInfo outAttrs)395 public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) { 396 if (!isAttachedToFlutterEngine()) { 397 return super.onCreateInputConnection(outAttrs); 398 } 399 400 return textInputPlugin.createInputConnection(this, outAttrs); 401 } 402 403 /** 404 * Allows a {@code View} that is not currently the input connection target to invoke commands on 405 * the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed. 406 * <p> 407 * Returns true to allow non-input-connection-targets to invoke methods on 408 * {@code InputMethodManager}, or false to exclusively allow the input connection target to invoke 409 * such methods. 410 */ 411 @Override checkInputConnectionProxy(View view)412 public boolean checkInputConnectionProxy(View view) { 413 return flutterEngine != null 414 ? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view) 415 : super.checkInputConnectionProxy(view); 416 } 417 418 /** 419 * Invoked when key is released. 420 * 421 * This method is typically invoked in response to the release of a physical 422 * keyboard key or a D-pad button. It is generally not invoked when a virtual 423 * software keyboard is used, though a software keyboard may choose to invoke 424 * this method in some situations. 425 * 426 * {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} 427 * may do some additional work with the given {@link KeyEvent}, e.g., combine this 428 * {@code keyCode} with the previous {@code keyCode} to generate a unicode combined 429 * character. 430 */ 431 @Override onKeyUp(int keyCode, @NonNull KeyEvent event)432 public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { 433 if (!isAttachedToFlutterEngine()) { 434 return super.onKeyUp(keyCode, event); 435 } 436 437 androidKeyProcessor.onKeyUp(event); 438 return super.onKeyUp(keyCode, event); 439 } 440 441 /** 442 * Invoked when key is pressed. 443 * 444 * This method is typically invoked in response to the press of a physical 445 * keyboard key or a D-pad button. It is generally not invoked when a virtual 446 * software keyboard is used, though a software keyboard may choose to invoke 447 * this method in some situations. 448 * 449 * {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} 450 * may do some additional work with the given {@link KeyEvent}, e.g., combine this 451 * {@code keyCode} with the previous {@code keyCode} to generate a unicode combined 452 * character. 453 */ 454 @Override onKeyDown(int keyCode, @NonNull KeyEvent event)455 public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { 456 if (!isAttachedToFlutterEngine()) { 457 return super.onKeyDown(keyCode, event); 458 } 459 460 androidKeyProcessor.onKeyDown(event); 461 return super.onKeyDown(keyCode, event); 462 } 463 464 /** 465 * Invoked by Android when a user touch event occurs. 466 * 467 * Flutter handles all of its own gesture detection and processing, therefore this 468 * method forwards all {@link MotionEvent} data from Android to Flutter. 469 */ 470 @Override onTouchEvent(@onNull MotionEvent event)471 public boolean onTouchEvent(@NonNull MotionEvent event) { 472 if (!isAttachedToFlutterEngine()) { 473 return super.onTouchEvent(event); 474 } 475 476 // TODO(abarth): This version check might not be effective in some 477 // versions of Android that statically compile code and will be upset 478 // at the lack of |requestUnbufferedDispatch|. Instead, we should factor 479 // version-dependent code into separate classes for each supported 480 // version and dispatch dynamically. 481 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 482 requestUnbufferedDispatch(event); 483 } 484 485 return androidTouchProcessor.onTouchEvent(event); 486 } 487 488 /** 489 * Invoked by Android when a generic motion event occurs, e.g., joystick movement, mouse hover, 490 * track pad touches, scroll wheel movements, etc. 491 * 492 * Flutter handles all of its own gesture detection and processing, therefore this 493 * method forwards all {@link MotionEvent} data from Android to Flutter. 494 */ 495 @Override onGenericMotionEvent(@onNull MotionEvent event)496 public boolean onGenericMotionEvent(@NonNull MotionEvent event) { 497 boolean handled = isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event); 498 return handled ? true : super.onGenericMotionEvent(event); 499 } 500 501 /** 502 * Invoked by Android when a hover-compliant input system causes a hover event. 503 * 504 * An example of hover events is a stylus sitting near an Android screen. As the 505 * stylus moves from outside a {@code View} to hover over a {@code View}, or move 506 * around within a {@code View}, or moves from over a {@code View} to outside a 507 * {@code View}, a corresponding {@link MotionEvent} is reported via this method. 508 * 509 * Hover events can be used for accessibility touch exploration and therefore are 510 * processed here for accessibility purposes. 511 */ 512 @Override onHoverEvent(@onNull MotionEvent event)513 public boolean onHoverEvent(@NonNull MotionEvent event) { 514 if (!isAttachedToFlutterEngine()) { 515 return super.onHoverEvent(event); 516 } 517 518 boolean handled = accessibilityBridge.onAccessibilityHoverEvent(event); 519 if (!handled) { 520 // TODO(ianh): Expose hover events to the platform, 521 // implementing ADD, REMOVE, etc. 522 } 523 return handled; 524 } 525 //-------- End: Process UI I/O that Flutter cares about. --------- 526 527 //-------- Start: Accessibility ------- 528 @Override 529 @Nullable getAccessibilityNodeProvider()530 public AccessibilityNodeProvider getAccessibilityNodeProvider() { 531 if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) { 532 return accessibilityBridge; 533 } else { 534 // TODO(goderbauer): when a11y is off this should return a one-off snapshot of 535 // the a11y 536 // tree. 537 return null; 538 } 539 } 540 541 // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise add comments. resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled)542 private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) { 543 if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) { 544 setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled)); 545 } else { 546 setWillNotDraw(false); 547 } 548 } 549 //-------- End: Accessibility --------- 550 551 /** 552 * Connects this {@code FlutterView} to the given {@link FlutterEngine}. 553 * 554 * This {@code FlutterView} will begin rendering the UI painted by the given {@link FlutterEngine}. 555 * This {@code FlutterView} will also begin forwarding interaction events from this 556 * {@code FlutterView} to the given {@link FlutterEngine}, e.g., user touch events, accessibility 557 * events, keyboard events, and others. 558 * 559 * See {@link #detachFromFlutterEngine()} for information on how to detach from a 560 * {@link FlutterEngine}. 561 */ attachToFlutterEngine( @onNull FlutterEngine flutterEngine )562 public void attachToFlutterEngine( 563 @NonNull FlutterEngine flutterEngine 564 ) { 565 Log.d(TAG, "Attaching to a FlutterEngine: " + flutterEngine); 566 if (isAttachedToFlutterEngine()) { 567 if (flutterEngine == this.flutterEngine) { 568 // We are already attached to this FlutterEngine 569 Log.d(TAG, "Already attached to this engine. Doing nothing."); 570 return; 571 } 572 573 // Detach from a previous FlutterEngine so we can attach to this new one. 574 Log.d(TAG, "Currently attached to a different engine. Detaching and then attaching" 575 + " to new engine."); 576 detachFromFlutterEngine(); 577 } 578 579 this.flutterEngine = flutterEngine; 580 581 // Instruct our FlutterRenderer that we are now its designated RenderSurface. 582 FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer(); 583 didRenderFirstFrame = flutterRenderer.hasRenderedFirstFrame(); 584 flutterRenderer.attachToRenderSurface(renderSurface); 585 flutterRenderer.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener); 586 587 // Initialize various components that know how to process Android View I/O 588 // in a way that Flutter understands. 589 textInputPlugin = new TextInputPlugin( 590 this, 591 this.flutterEngine.getDartExecutor(), 592 this.flutterEngine.getPlatformViewsController() 593 ); 594 androidKeyProcessor = new AndroidKeyProcessor( 595 this.flutterEngine.getKeyEventChannel(), 596 textInputPlugin 597 ); 598 androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer()); 599 accessibilityBridge = new AccessibilityBridge( 600 this, 601 flutterEngine.getAccessibilityChannel(), 602 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE), 603 getContext().getContentResolver(), 604 this.flutterEngine.getPlatformViewsController() 605 ); 606 accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener); 607 resetWillNotDraw( 608 accessibilityBridge.isAccessibilityEnabled(), 609 accessibilityBridge.isTouchExplorationEnabled() 610 ); 611 612 // Connect AccessibilityBridge to the PlatformViewsController within the FlutterEngine. 613 // This allows platform Views to hook into Flutter's overall accessibility system. 614 this.flutterEngine.getPlatformViewsController().attachAccessibilityBridge(accessibilityBridge); 615 616 // Inform the Android framework that it should retrieve a new InputConnection 617 // now that an engine is attached. 618 // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin 619 textInputPlugin.getInputMethodManager().restartInput(this); 620 621 // Push View and Context related information from Android to Flutter. 622 sendUserSettingsToFlutter(); 623 sendLocalesToFlutter(getResources().getConfiguration()); 624 sendViewportMetricsToFlutter(); 625 626 // Notify engine attachment listeners of the attachment. 627 for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) { 628 listener.onFlutterEngineAttachedToFlutterView(flutterEngine); 629 } 630 631 // If the first frame has already been rendered, notify all first frame listeners. 632 // Do this after all other initialization so that listeners don't inadvertently interact 633 // with a FlutterView that is only partially attached to a FlutterEngine. 634 if (didRenderFirstFrame) { 635 onFirstFrameRenderedListener.onFirstFrameRendered(); 636 } 637 } 638 639 /** 640 * Disconnects this {@code FlutterView} from a previously attached {@link FlutterEngine}. 641 * 642 * This {@code FlutterView} will clear its UI and stop forwarding all events to the previously-attached 643 * {@link FlutterEngine}. This includes touch events, accessibility events, keyboard events, 644 * and others. 645 * 646 * See {@link #attachToFlutterEngine(FlutterEngine)} for information on how to attach a 647 * {@link FlutterEngine}. 648 */ detachFromFlutterEngine()649 public void detachFromFlutterEngine() { 650 Log.d(TAG, "Detaching from a FlutterEngine: " + flutterEngine); 651 if (!isAttachedToFlutterEngine()) { 652 Log.d(TAG, "Not attached to an engine. Doing nothing."); 653 return; 654 } 655 656 // Notify engine attachment listeners of the detachment. 657 for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) { 658 listener.onFlutterEngineDetachedFromFlutterView(); 659 } 660 661 // Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge. 662 flutterEngine.getPlatformViewsController().detachAccessibiltyBridge(); 663 664 // Disconnect and clean up the AccessibilityBridge. 665 accessibilityBridge.release(); 666 accessibilityBridge = null; 667 668 // Inform the Android framework that it should retrieve a new InputConnection 669 // now that the engine is detached. The new InputConnection will be null, which 670 // signifies that this View does not process input (until a new engine is attached). 671 // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin 672 textInputPlugin.getInputMethodManager().restartInput(this); 673 textInputPlugin.destroy(); 674 675 // Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface. 676 FlutterRenderer flutterRenderer = flutterEngine.getRenderer(); 677 didRenderFirstFrame = false; 678 flutterRenderer.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener); 679 flutterRenderer.detachFromRenderSurface(); 680 flutterEngine = null; 681 } 682 683 /** 684 * Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}. 685 */ 686 @VisibleForTesting isAttachedToFlutterEngine()687 public boolean isAttachedToFlutterEngine() { 688 return flutterEngine != null && flutterEngine.getRenderer().isAttachedTo(renderSurface); 689 } 690 691 /** 692 * Returns the {@link FlutterEngine} to which this {@code FlutterView} is currently attached, 693 * or null if this {@code FlutterView} is not currently attached to a {@link FlutterEngine}. 694 */ 695 @VisibleForTesting 696 @Nullable getAttachedFlutterEngine()697 public FlutterEngine getAttachedFlutterEngine() { 698 return flutterEngine; 699 } 700 701 /** 702 * Adds a {@link FlutterEngineAttachmentListener}, which is notifed whenever this {@code FlutterView} 703 * attached to/detaches from a {@link FlutterEngine}. 704 */ 705 @VisibleForTesting addFlutterEngineAttachmentListener(@onNull FlutterEngineAttachmentListener listener)706 public void addFlutterEngineAttachmentListener(@NonNull FlutterEngineAttachmentListener listener) { 707 flutterEngineAttachmentListeners.add(listener); 708 } 709 710 /** 711 * Removes a {@link FlutterEngineAttachmentListener} that was previously added with 712 * {@link #addFlutterEngineAttachmentListener(FlutterEngineAttachmentListener)}. 713 */ 714 @VisibleForTesting removeFlutterEngineAttachmentListener(@onNull FlutterEngineAttachmentListener listener)715 public void removeFlutterEngineAttachmentListener(@NonNull FlutterEngineAttachmentListener listener) { 716 flutterEngineAttachmentListeners.remove(listener); 717 } 718 719 /** 720 * Send the current {@link Locale} configuration to Flutter. 721 * 722 * FlutterEngine must be non-null when this method is invoked. 723 */ 724 @SuppressWarnings("deprecation") sendLocalesToFlutter(@onNull Configuration config)725 private void sendLocalesToFlutter(@NonNull Configuration config) { 726 List<Locale> locales = new ArrayList<>(); 727 if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 728 LocaleList localeList = config.getLocales(); 729 int localeCount = localeList.size(); 730 for (int index = 0; index < localeCount; ++index) { 731 Locale locale = localeList.get(index); 732 locales.add(locale); 733 } 734 } else { 735 locales.add(config.locale); 736 } 737 flutterEngine.getLocalizationChannel().sendLocales(locales); 738 } 739 740 /** 741 * Send various user preferences of this Android device to Flutter. 742 * 743 * For example, sends the user's "text scale factor" preferences, as well as the user's clock 744 * format preference. 745 * 746 * FlutterEngine must be non-null when this method is invoked. 747 */ sendUserSettingsToFlutter()748 private void sendUserSettingsToFlutter() { 749 flutterEngine.getSettingsChannel().startMessage() 750 .setTextScaleFactor(getResources().getConfiguration().fontScale) 751 .setUse24HourFormat(DateFormat.is24HourFormat(getContext())) 752 .send(); 753 } 754 755 // TODO(mattcarroll): consider introducing a system channel for this communication instead of JNI sendViewportMetricsToFlutter()756 private void sendViewportMetricsToFlutter() { 757 if (!isAttachedToFlutterEngine()) { 758 Log.w(TAG, "Tried to send viewport metrics from Android to Flutter but this " 759 + "FlutterView was not attached to a FlutterEngine."); 760 return; 761 } 762 763 viewportMetrics.devicePixelRatio = getResources().getDisplayMetrics().density; 764 flutterEngine.getRenderer().setViewportMetrics(viewportMetrics); 765 } 766 767 /** 768 * Render modes for a {@link FlutterView}. 769 */ 770 public enum RenderMode { 771 /** 772 * {@code RenderMode}, which paints a Flutter UI to a {@link android.view.SurfaceView}. 773 * This mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned 774 * between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed. 775 * Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required, 776 * developers should strongly prefer this render mode. 777 */ 778 surface, 779 /** 780 * {@code RenderMode}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}. 781 * This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in this 782 * mode can be animated and transformed, as well as positioned in the z-index between 2+ other 783 * Android {@code Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture} 784 * are required, developers should strongly prefer the {@link RenderMode#surface} render mode. 785 */ 786 texture 787 } 788 789 /** 790 * Transparency mode for a {@code FlutterView}. 791 * <p> 792 * {@code TransparencyMode} impacts the visual behavior and performance of a {@link FlutterSurfaceView}, 793 * which is displayed when a {@code FlutterView} uses {@link RenderMode#surface}. 794 * <p> 795 * {@code TransparencyMode} does not impact {@link FlutterTextureView}, which is displayed when 796 * a {@code FlutterView} uses {@link RenderMode#texture}, because a {@link FlutterTextureView} 797 * automatically comes with transparency. 798 */ 799 public enum TransparencyMode { 800 /** 801 * Renders a {@code FlutterView} without any transparency. This affects {@code FlutterView}s in 802 * {@link RenderMode#surface} by introducing a base color of black, and places the 803 * {@link FlutterSurfaceView}'s {@code Window} behind all other content. 804 * <p> 805 * In {@link RenderMode#surface}, this mode is the most performant and is a good choice for 806 * fullscreen Flutter UIs that will not undergo {@code Fragment} transactions. If this mode is 807 * used within a {@code Fragment}, and that {@code Fragment} is replaced by another one, a 808 * brief black flicker may be visible during the switch. 809 */ 810 opaque, 811 /** 812 * Renders a {@code FlutterView} with transparency. This affects {@code FlutterView}s in 813 * {@link RenderMode#surface} by allowing background transparency, and places the 814 * {@link FlutterSurfaceView}'s {@code Window} on top of all other content. 815 * <p> 816 * In {@link RenderMode#surface}, this mode is less performant than {@link #opaque}, but this 817 * mode avoids the black flicker problem that {@link #opaque} has when going through 818 * {@code Fragment} transactions. Consider using this {@code TransparencyMode} if you intend to 819 * switch {@code Fragment}s at runtime that contain a Flutter UI. 820 */ 821 transparent 822 } 823 824 /** 825 * Listener that is notified when a {@link FlutterEngine} is attached to/detached from 826 * a given {@code FlutterView}. 827 */ 828 @VisibleForTesting 829 public interface FlutterEngineAttachmentListener { 830 /** 831 * The given {@code engine} has been attached to the associated {@code FlutterView}. 832 */ onFlutterEngineAttachedToFlutterView(@onNull FlutterEngine engine)833 void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine); 834 835 /** 836 * A previously attached {@link FlutterEngine} has been detached from the associated 837 * {@code FlutterView}. 838 */ onFlutterEngineDetachedFromFlutterView()839 void onFlutterEngineDetachedFromFlutterView(); 840 } 841 } 842