Commit 368ae69d authored by yangwu.jia's avatar yangwu.jia

Boost 1.9升级,TextField键盘呼出问题解决

parent c7fe899c
package com.idlefish.flutterboost;
import android.annotation.TargetApi;
import android.app.Activity;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
//import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Build;
import android.os.LocaleList;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewCompat;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeProvider;
......@@ -23,23 +26,43 @@ import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.FlutterSurfaceView;
import io.flutter.embedding.android.FlutterTextureView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.Log;
import io.flutter.embedding.android.*;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.view.AccessibilityBridge;
/**
* Displays a Flutter UI on an Android device.
* <p>
* A {@code FlutterView}'s UI is painted by a corresponding {@link FlutterEngine}.
* <p>
* A {@code FlutterView} can operate in 2 different {@link RenderMode}s:
* <ol>
* <li>{@link RenderMode#surface}, which paints a Flutter UI to a {@link android.view.SurfaceView}.
* This mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned
* between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed.
* Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required,
* developers should strongly prefer this render mode.</li>
* <li>{@link RenderMode#texture}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}.
* This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in this
* mode can be animated and transformed, as well as positioned in the z-index between 2+ other
* Android {@code Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture}
* are required, developers should strongly prefer the {@link RenderMode#surface} render mode.</li>
* </ol>
* See <a>https://source.android.com/devices/graphics/arch-tv#surface_or_texture</a> for more
* information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}.
*/
public class XFlutterView extends FrameLayout {
private static final String TAG = "XFlutterView";
private static final String TAG = "FlutterView";
// Behavior configuration of this FlutterView.
@NonNull
......@@ -50,10 +73,14 @@ public class XFlutterView extends FrameLayout {
// Internal view hierarchy references.
@Nullable
private FlutterRenderer.RenderSurface renderSurface;
private final Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>();
private boolean didRenderFirstFrame;
// Connections to a Flutter execution context.
@Nullable
private FlutterEngine flutterEngine;
@NonNull
private final Set<FlutterView.FlutterEngineAttachmentListener> flutterEngineAttachmentListeners = new HashSet<>();
// Components that process various types of Android View input and events,
// possibly storing intermediate state, and communicating those events to Flutter.
......@@ -61,9 +88,9 @@ public class XFlutterView extends FrameLayout {
// These components essentially add some additional behavioral logic on top of
// existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc.
@Nullable
private XTextInputPlugin textInputPlugin;
private TextInputPlugin textInputPlugin;
@Nullable
private XAndroidKeyProcessor androidKeyProcessor;
private AndroidKeyProcessor androidKeyProcessor;
@Nullable
private AndroidTouchProcessor androidTouchProcessor;
@Nullable
......@@ -79,17 +106,51 @@ public class XFlutterView extends FrameLayout {
}
};
private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
@Override
public void onFirstFrameRendered() {
didRenderFirstFrame = true;
for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) {
listener.onFirstFrameRendered();
}
}
};
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes.
* <p>
* <ul>
* <li>{@link #renderMode} defaults to {@link RenderMode#surface}.</li>
* <li>{@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.</li>
* </ul>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context) {
this(context, null, null, null);
}
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes,
* and allows selection of a {@link #renderMode}.
* <p>
* {@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context, @NonNull FlutterView.RenderMode renderMode) {
this(context, null, renderMode, null);
}
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes,
* assumes the use of {@link RenderMode#surface}, and allows selection of a {@link #transparencyMode}.
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context, @NonNull FlutterView.TransparencyMode transparencyMode) {
this(context, null, FlutterView.RenderMode.surface, transparencyMode);
}
......@@ -97,6 +158,9 @@ public class XFlutterView extends FrameLayout {
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes, and allows
* a selection of {@link #renderMode} and {@link #transparencyMode}.
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context, @NonNull FlutterView.RenderMode renderMode, @NonNull FlutterView.TransparencyMode transparencyMode) {
this(context, null, renderMode, transparencyMode);
......@@ -104,9 +168,11 @@ public class XFlutterView extends FrameLayout {
/**
* Constructs a {@code FlutterSurfaceView} in an XML-inflation-compliant manner.
*
* // TODO(mattcarroll): expose renderMode in XML when build system supports R.attr
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
// TODO(mattcarroll): expose renderMode in XML when build system supports R.attr
public XFlutterView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, null, null);
}
......@@ -121,17 +187,17 @@ public class XFlutterView extends FrameLayout {
}
private void init() {
Log.d(TAG, "Initializing FlutterView");
Log.v(TAG, "Initializing FlutterView");
switch (renderMode) {
case surface:
Log.d(TAG, "Internally creating a FlutterSurfaceView.");
Log.v(TAG, "Internally using a FlutterSurfaceView.");
FlutterSurfaceView flutterSurfaceView = new FlutterSurfaceView(getContext(), transparencyMode == FlutterView.TransparencyMode.transparent);
renderSurface = flutterSurfaceView;
addView(flutterSurfaceView);
break;
case texture:
Log.d(TAG, "Internally creating a FlutterTextureView.");
Log.v(TAG, "Internally using a FlutterTextureView.");
FlutterTextureView flutterTextureView = new FlutterTextureView(getContext());
renderSurface = flutterTextureView;
addView(flutterTextureView);
......@@ -143,12 +209,32 @@ public class XFlutterView extends FrameLayout {
setFocusableInTouchMode(true);
}
/**
* Returns true if an attached {@link FlutterEngine} has rendered at least 1 frame to this
* {@code FlutterView}.
* <p>
* Returns false if no {@link FlutterEngine} is attached.
* <p>
* This flag is specific to a given {@link FlutterEngine}. The following hypothetical timeline
* demonstrates how this flag changes over time.
* <ol>
* <li>{@code flutterEngineA} is attached to this {@code FlutterView}: returns false</li>
* <li>{@code flutterEngineA} renders its first frame to this {@code FlutterView}: returns true</li>
* <li>{@code flutterEngineA} is detached from this {@code FlutterView}: returns false</li>
* <li>{@code flutterEngineB} is attached to this {@code FlutterView}: returns false</li>
* <li>{@code flutterEngineB} renders its first frame to this {@code FlutterView}: returns true</li>
* </ol>
*/
public boolean hasRenderedFirstFrame() {
return didRenderFirstFrame;
}
/**
* Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's
* first rendered frame.
*/
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.addOnFirstFrameRenderedListener(listener);
onFirstFrameRenderedListeners.add(listener);
}
/**
......@@ -156,7 +242,7 @@ public class XFlutterView extends FrameLayout {
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.removeOnFirstFrameRenderedListener(listener);
onFirstFrameRenderedListeners.remove(listener);
}
//------- Start: Process View configuration that Flutter cares about. ------
......@@ -168,16 +254,11 @@ public class XFlutterView extends FrameLayout {
* change, device language change, device text scale factor change, etc.
*/
@Override
protected void onConfigurationChanged(Configuration newConfig) {
protected void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
try {
sendLocalesToFlutter(newConfig);
sendUserSettingsToFlutter();
}catch (Throwable e){
Log.e(TAG, "onConfigurationChanged error ");
}
Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter.");
sendLocalesToFlutter(newConfig);
sendUserSettingsToFlutter();
}
/**
......@@ -194,6 +275,9 @@ public class XFlutterView extends FrameLayout {
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
Log.v(TAG, "Size changed. Sending Flutter new viewport metrics. FlutterView was "
+ oldWidth + " x " + oldHeight
+ ", it is now " + width + " x " + height);
viewportMetrics.width = width;
viewportMetrics.height = height;
sendViewportMetricsToFlutter();
......@@ -212,7 +296,12 @@ public class XFlutterView extends FrameLayout {
@Override
@TargetApi(20)
@RequiresApi(20)
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
// The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings
// caused by usage of Android Q APIs. These calls are safe because they are
// guarded.
@SuppressLint({"InlinedApi", "NewApi"})
@NonNull
public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
WindowInsets newInsets = super.onApplyWindowInsets(insets);
// Status bar (top) and left/right system insets should partially obscure the content (padding).
......@@ -226,6 +315,23 @@ public class XFlutterView extends FrameLayout {
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = insets.getSystemWindowInsetBottom();
viewportMetrics.viewInsetLeft = 0;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Insets systemGestureInsets = insets.getSystemGestureInsets();
// viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
// viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
// viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
// viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
// }
Log.v(TAG, "Updating window insets (onApplyWindowInsets()):\n"
+ "Status bar insets: Top: " + viewportMetrics.paddingTop
+ ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n"
+ "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom
+ ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight
+ "System Gesture Insets - Left: " + viewportMetrics.systemGestureInsetLeft + ", Top: " + viewportMetrics.systemGestureInsetTop
+ ", Right: " + viewportMetrics.systemGestureInsetRight + ", Bottom: " + viewportMetrics.viewInsetBottom);
sendViewportMetricsToFlutter();
return newInsets;
......@@ -240,7 +346,7 @@ public class XFlutterView extends FrameLayout {
*/
@Override
@SuppressWarnings("deprecation")
protected boolean fitSystemWindows(Rect insets) {
protected boolean fitSystemWindows(@NonNull Rect insets) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
// Status bar, left/right system insets partially obscure content (padding).
viewportMetrics.paddingTop = insets.top;
......@@ -253,6 +359,13 @@ public class XFlutterView extends FrameLayout {
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = insets.bottom;
viewportMetrics.viewInsetLeft = 0;
Log.v(TAG, "Updating window insets (fitSystemWindows()):\n"
+ "Status bar insets: Top: " + viewportMetrics.paddingTop
+ ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n"
+ "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom
+ ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight);
sendViewportMetricsToFlutter();
return true;
} else {
......@@ -276,7 +389,8 @@ public class XFlutterView extends FrameLayout {
* rather than spread that logic throughout this {@code FlutterView}.
*/
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
@Nullable
public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
if (!isAttachedToFlutterEngine()) {
return super.onCreateInputConnection(outAttrs);
}
......@@ -284,6 +398,21 @@ public class XFlutterView extends FrameLayout {
return textInputPlugin.createInputConnection(this, outAttrs);
}
/**
* Allows a {@code View} that is not currently the input connection target to invoke commands on
* the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
* <p>
* Returns true to allow non-input-connection-targets to invoke methods on
* {@code InputMethodManager}, or false to exclusively allow the input connection target to invoke
* such methods.
*/
@Override
public boolean checkInputConnectionProxy(View view) {
return flutterEngine != null
? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
: super.checkInputConnectionProxy(view);
}
/**
* Invoked when key is released.
*
......@@ -292,13 +421,13 @@ public class XFlutterView extends FrameLayout {
* software keyboard is used, though a software keyboard may choose to invoke
* this method in some situations.
*
* {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor}
* {@link KeyEvent}s are sent from Android to Flutter. {@link }
* may do some additional work with the given {@link KeyEvent}, e.g., combine this
* {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
* character.
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onKeyUp(keyCode, event);
}
......@@ -315,13 +444,13 @@ public class XFlutterView extends FrameLayout {
* software keyboard is used, though a software keyboard may choose to invoke
* this method in some situations.
*
* {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor}
* {@link KeyEvent}s are sent from Android to Flutter. {@link }
* may do some additional work with the given {@link KeyEvent}, e.g., combine this
* {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
* character.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onKeyDown(keyCode, event);
}
......@@ -337,7 +466,7 @@ public class XFlutterView extends FrameLayout {
* method forwards all {@link MotionEvent} data from Android to Flutter.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onTouchEvent(event);
}
......@@ -362,7 +491,7 @@ public class XFlutterView extends FrameLayout {
* method forwards all {@link MotionEvent} data from Android to Flutter.
*/
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
boolean handled = isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event);
return handled ? true : super.onGenericMotionEvent(event);
}
......@@ -379,7 +508,7 @@ public class XFlutterView extends FrameLayout {
* processed here for accessibility purposes.
*/
@Override
public boolean onHoverEvent(MotionEvent event) {
public boolean onHoverEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onHoverEvent(event);
}
......@@ -395,6 +524,7 @@ public class XFlutterView extends FrameLayout {
//-------- Start: Accessibility -------
@Override
@Nullable
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) {
return accessibilityBridge;
......@@ -406,12 +536,8 @@ public class XFlutterView extends FrameLayout {
}
}
// TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise add comments.
private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
if(flutterEngine==null) return;
if(flutterEngine.getRenderer()==null) return;
if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) {
setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
} else {
......@@ -431,9 +557,10 @@ public class XFlutterView extends FrameLayout {
* See {@link #detachFromFlutterEngine()} for information on how to detach from a
* {@link FlutterEngine}.
*/
public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
Log.d(TAG, "attachToFlutterEngine()");
public void attachToFlutterEngine(
@NonNull FlutterEngine flutterEngine
) {
Log.d(TAG, "Attaching to a FlutterEngine: " + flutterEngine);
if (isAttachedToFlutterEngine()) {
if (flutterEngine == this.flutterEngine) {
// We are already attached to this FlutterEngine
......@@ -442,76 +569,74 @@ public class XFlutterView extends FrameLayout {
}
// Detach from a previous FlutterEngine so we can attach to this new one.
Log.d(TAG, "Currently attached to a different engine. Detaching.");
Log.d(TAG, "Currently attached to a different engine. Detaching and then attaching"
+ " to new engine.");
detachFromFlutterEngine();
}
this.flutterEngine = flutterEngine;
// initialize PlatformViewsController
// this.flutterEngine.getPluginRegistry().getPlatformViewsController().attach(getContext(),flutterEngine.getRenderer(),flutterEngine.getDartExecutor());
// Instruct our FlutterRenderer that we are now its designated RenderSurface.
this.flutterEngine.getRenderer().attachToRenderSurface(renderSurface);
FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer();
didRenderFirstFrame = flutterRenderer.hasRenderedFirstFrame();
flutterRenderer.attachToRenderSurface(renderSurface);
flutterRenderer.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
// Initialize various components that know how to process Android View I/O
// in a way that Flutter understands.
if(textInputPlugin==null){
textInputPlugin = new XTextInputPlugin(
this,
flutterEngine.getTextInputChannel()
);
if(this.textInputPlugin!=null){
this.textInputPlugin.destroy();
}
textInputPlugin.setTextInputMethodHandler();
textInputPlugin.getInputMethodManager().restartInput(this);
androidKeyProcessor = new XAndroidKeyProcessor(
this.flutterEngine.getKeyEventChannel(),
textInputPlugin
);
this.textInputPlugin = new TextInputPlugin(this, this.flutterEngine.getDartExecutor(), this.flutterEngine.getPlatformViewsController());
androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
this.textInputPlugin.getInputMethodManager().restartInput(this);
if(accessibilityBridge==null){
accessibilityBridge = new AccessibilityBridge(
this,
flutterEngine.getAccessibilityChannel(),
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
getContext().getContentResolver(),
// TODO(mattcaroll): plumb the platform views controller to the accessibility bridge.
// https://github.com/flutter/flutter/issues/29618
null
);
accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
resetWillNotDraw(
accessibilityBridge.isAccessibilityEnabled(),
accessibilityBridge.isTouchExplorationEnabled()
);
}
this.androidKeyProcessor = new AndroidKeyProcessor(
this.flutterEngine.getKeyEventChannel(),
textInputPlugin
);
this.androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
this.accessibilityBridge = new AccessibilityBridge(
this,
flutterEngine.getAccessibilityChannel(),
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
getContext().getContentResolver(),
this.flutterEngine.getPlatformViewsController()
);
accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
resetWillNotDraw(
accessibilityBridge.isAccessibilityEnabled(),
accessibilityBridge.isTouchExplorationEnabled()
);
// Connect AccessibilityBridge to the PlatformViewsController within the FlutterEngine.
// This allows platform Views to hook into Flutter's overall accessibility system.
this.flutterEngine.getPlatformViewsController().attachAccessibilityBridge(accessibilityBridge);
// Inform the Android framework that it should retrieve a new InputConnection
// now that an engine is attached.
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
// textInputPlugin.getInputMethodManager().restartInput(this);
// Push View and Context related information from Android to Flutter.
sendUserSettingsToFlutter();
sendLocalesToFlutter(getResources().getConfiguration());
sendViewportMetricsToFlutter();
}
public void release(){
if(accessibilityBridge!=null){
accessibilityBridge.release();
// Notify engine attachment listeners of the attachment.
for (FlutterView.FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
listener.onFlutterEngineAttachedToFlutterView(flutterEngine);
}
textInputPlugin.release();
// If the first frame has already been rendered, notify all first frame listeners.
// Do this after all other initialization so that listeners don't inadvertently interact
// with a FlutterView that is only partially attached to a FlutterEngine.
if (didRenderFirstFrame) {
onFirstFrameRenderedListener.onFirstFrameRendered();
}
}
/**
......@@ -525,46 +650,79 @@ public class XFlutterView extends FrameLayout {
* {@link FlutterEngine}.
*/
public void detachFromFlutterEngine() {
Log.d(TAG, "detachFromFlutterEngine()");
Log.d(TAG, "Detaching from a FlutterEngine: " + flutterEngine);
if (!isAttachedToFlutterEngine()) {
Log.d(TAG, "Not attached to an engine. Doing nothing.");
return;
}
Log.d(TAG, "Detaching from Flutter Engine");
// detach platformviews in page in case memory leak
// flutterEngine.getPluginRegistry().getPlatformViewsController().detach();
// flutterEngine.getPluginRegistry().getPlatformViewsController().onFlutterViewDestroyed();
// Notify engine attachment listeners of the detachment.
for (FlutterView.FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
listener.onFlutterEngineDetachedFromFlutterView();
}
// Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
flutterEngine.getPlatformViewsController().detachAccessibiltyBridge();
// Disconnect and clean up the AccessibilityBridge.
accessibilityBridge.release();
accessibilityBridge = null;
// Inform the Android framework that it should retrieve a new InputConnection
// now that the engine is detached. The new InputConnection will be null, which
// signifies that this View does not process input (until a new engine is attached).
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
// textInputPlugin.getInputMethodManager().restartInput(this);
textInputPlugin.getInputMethodManager().restartInput(this);
// textInputPlugin.destroy();
// Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
// this.textInputPlugin.getInputMethodManager().restartInput(this);
flutterEngine.getRenderer().detachFromRenderSurface();
FlutterRenderer flutterRenderer = flutterEngine.getRenderer();
didRenderFirstFrame = false;
flutterRenderer.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
flutterRenderer.detachFromRenderSurface();
flutterEngine = null;
// TODO(mattcarroll): clear the surface when JNI doesn't blow up
// if (isSurfaceAvailableForRendering) {
// Canvas canvas = surfaceHolder.lockCanvas();
// canvas.drawColor(Color.RED);
// surfaceHolder.unlockCanvasAndPost(canvas);
// }
}
private boolean isAttachedToFlutterEngine() {
/**
* Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}.
*/
@VisibleForTesting
public boolean isAttachedToFlutterEngine() {
return flutterEngine != null;
}
/**
* Returns the {@link FlutterEngine} to which this {@code FlutterView} is currently attached,
* or null if this {@code FlutterView} is not currently attached to a {@link FlutterEngine}.
*/
@VisibleForTesting
@Nullable
public FlutterEngine getAttachedFlutterEngine() {
return flutterEngine;
}
/**
* attached to/detaches from a {@link FlutterEngine}.
*/
@VisibleForTesting
public void addFlutterEngineAttachmentListener(@NonNull FlutterView.FlutterEngineAttachmentListener listener) {
flutterEngineAttachmentListeners.add(listener);
}
/**
*/
@VisibleForTesting
public void removeFlutterEngineAttachmentListener(@NonNull FlutterView.FlutterEngineAttachmentListener listener) {
flutterEngineAttachmentListeners.remove(listener);
}
/**
* Send the current {@link Locale} configuration to Flutter.
*
* FlutterEngine must be non-null when this method is invoked.
*/
@SuppressWarnings("deprecation")
private void sendLocalesToFlutter(Configuration config) {
private void sendLocalesToFlutter(@NonNull Configuration config) {
List<Locale> locales = new ArrayList<>();
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
LocaleList localeList = config.getLocales();
......@@ -576,9 +734,7 @@ public class XFlutterView extends FrameLayout {
} else {
locales.add(config.locale);
}
if(flutterEngine!=null&&flutterEngine.getLocalizationChannel()!=null){
flutterEngine.getLocalizationChannel().sendLocales(locales);
}
flutterEngine.getLocalizationChannel().sendLocales(locales);
}
/**
......@@ -590,20 +746,17 @@ public class XFlutterView extends FrameLayout {
* FlutterEngine must be non-null when this method is invoked.
*/
private void sendUserSettingsToFlutter() {
if(flutterEngine!=null&&flutterEngine.getSettingsChannel()!=null){
flutterEngine.getSettingsChannel().startMessage()
.setTextScaleFactor(getResources().getConfiguration().fontScale)
.setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
.send();
}
flutterEngine.getSettingsChannel().startMessage()
.setTextScaleFactor(getResources().getConfiguration().fontScale)
.setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
.send();
}
// TODO(mattcarroll): consider introducing a system channel for this communication instead of JNI
private void sendViewportMetricsToFlutter() {
Log.d(TAG, "sendViewportMetricsToFlutter()");
if (!isAttachedToFlutterEngine()) {
Log.w(TAG, "Tried to send viewport metrics from Android to Flutter but this FlutterView was not attached to a FlutterEngine.");
Log.w(TAG, "Tried to send viewport metrics from Android to Flutter but this "
+ "FlutterView was not attached to a FlutterEngine.");
return;
}
......
package com.idlefish.flutterboost;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
......@@ -13,20 +11,23 @@ import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.view.FlutterView;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
/**
* Android implementation of the text input plugin.
*/
public class XTextInputPlugin {
public class XTextInputPlugin {
@NonNull
private View mView;
@NonNull
private final InputMethodManager mImm;
@NonNull
private int mClient = 0;
private final TextInputChannel textInputChannel;
@NonNull
private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
@Nullable
private TextInputChannel.Configuration configuration;
@Nullable
......@@ -34,17 +35,27 @@ public class XTextInputPlugin {
private boolean mRestartInputPending;
@Nullable
private InputConnection lastInputConnection;
private TextInputChannel textInputChannel;
public XTextInputPlugin(View view, TextInputChannel mTextInputChannel) {
@NonNull
private PlatformViewsController platformViewsController;
// When true following calls to createInputConnection will return the cached lastInputConnection if the input
// target is a platform view. See the comments on lockPlatformViewInputConnection for more details.
private boolean isInputConnectionLocked;
public XTextInputPlugin(View view,TextInputChannel textInputChannel, PlatformViewsController platformViewsController) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
textInputChannel=mTextInputChannel;
this.textInputChannel=textInputChannel;
this.platformViewsController = platformViewsController;
// this.platformViewsController.attachTextInputPlugin(this);
}
public void release(){
mView=null;
}
public void setTextInputMethodHandler(){
public void setTextInputMethodHandler( ){
textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() {
@Override
public void show() {
......@@ -62,8 +73,8 @@ public class XTextInputPlugin {
}
@Override
public void setPlatformViewClient(int i) {
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
}
@Override
......@@ -76,9 +87,7 @@ public class XTextInputPlugin {
clearTextInputClient();
}
});
}
public void release(){
mView=null;
}
@NonNull
......@@ -86,6 +95,40 @@ public class XTextInputPlugin {
return mImm;
}
/***
* Use the current platform view input connection until unlockPlatformViewInputConnection is called.
*
* The current input connection instance is cached and any following call to @{link createInputConnection} returns
* the cached connection until unlockPlatformViewInputConnection is called.
*
* This is a no-op if the current input target isn't a platform view.
*
* This is used to preserve an input connection when moving a platform view from one virtual display to another.
*/
public void lockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
isInputConnectionLocked = true;
}
}
/**
* Unlocks the input connection.
*
* See also: @{link lockPlatformViewInputConnection}.
*/
public void unlockPlatformViewInputConnection() {
isInputConnectionLocked = false;
}
/**
* Detaches the text input plugin from the platform views controller.
*
* The TextInputPlugin instance should not be used after calling this.
*/
public void destroy() {
platformViewsController.detachTextInputPlugin();
}
private static int inputTypeFromTextInputType(
TextInputChannel.InputType type,
boolean obscureText,
......@@ -114,6 +157,8 @@ public class XTextInputPlugin {
textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
} else if (type.type == TextInputChannel.TextInputType.URL) {
textType |= InputType.TYPE_TEXT_VARIATION_URI;
} else if (type.type == TextInputChannel.TextInputType.VISIBLE_PASSWORD) {
textType |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
}
if (obscureText) {
......@@ -136,8 +181,16 @@ public class XTextInputPlugin {
}
public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
if (mClient == 0) {
if (inputTarget.type == InputTarget.Type.NO_TARGET) {
lastInputConnection = null;
return null;
}
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (isInputConnectionLocked) {
return lastInputConnection;
}
lastInputConnection = platformViewsController.getPlatformViewById(inputTarget.id).onCreateInputConnection(outAttrs);
return lastInputConnection;
}
......@@ -166,7 +219,7 @@ public class XTextInputPlugin {
XInputConnectionAdaptor connection = new XInputConnectionAdaptor(
view,
mClient,
inputTarget.id,
textInputChannel,
mEditable
);
......@@ -182,24 +235,55 @@ public class XTextInputPlugin {
return lastInputConnection;
}
/**
* Clears a platform view text input client if it is the current input target.
*
* This is called when a platform view is disposed to make sure we're not hanging to a stale input
* connection.
*/
public void clearPlatformViewClient(int platformViewId) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
hideTextInput(mView);
mImm.restartInput(mView);
mRestartInputPending = false;
}
}
private void showTextInput(View view) {
if(view==null) return;
view.requestFocus();
mImm.showSoftInput(view, 0);
}
private void hideTextInput(View view) {
// Note: a race condition may lead to us hiding the keyboard here just after a platform view has shown it.
// This can only potentially happen when switching focus from a Flutter text field to a platform view's text
// field(by text field here I mean anything that keeps the keyboard open).
// See: https://github.com/flutter/flutter/issues/34169
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
}
private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
mClient = client;
inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
this.configuration = configuration;
mEditable = Editable.Factory.getInstance().newEditable("");
// setTextInputClient will be followed by a call to setTextInputEditingState.
// Do a restartInput at that time.
mRestartInputPending = true;
unlockPlatformViewInputConnection();
}
private void setPlatformViewTextInputClient(int platformViewId) {
// We need to make sure that the Flutter view is focused so that no imm operations get short circuited.
// Not asking for focus here specifically manifested in a but on API 28 devices where the platform view's
// request to show a keyboard was ignored.
mView.requestFocus();
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
}
private void applyStateToSelection(TextInputChannel.TextEditState state) {
......@@ -213,7 +297,6 @@ public class XTextInputPlugin {
}
}
@RequiresApi(api = Build.VERSION_CODES.CUPCAKE)
private void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
if (!mRestartInputPending && state.text.equals(mEditable.toString())) {
applyStateToSelection(state);
......@@ -230,6 +313,45 @@ public class XTextInputPlugin {
}
private void clearTextInputClient() {
mClient = 0;
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
// Focus changes in the framework tree have no guarantees on the order focus nodes are notified. A node
// that lost focus may be notified before or after a node that gained focus.
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the Flutter text
// field's focus node will be notified that it lost focus after the AndroidView was notified that it gained
// focus. When this happens the text field will send a clearTextInput command which we ignore.
// By doing this we prevent the framework from clearing a platform view input client(the only way to do so
// is to set a new framework text client). I don't see an obvious use case for "clearing" a platform views
// text input client, and it may be error prone as we don't know how the platform view manages the input
// connection and we probably shouldn't interfere.
// If we ever want to allow the framework to clear a platform view text client we should probably consider
// changing the focus manager such that focus nodes that lost focus are notified before focus nodes that
// gained focus as part of the same focus event.
return;
}
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
unlockPlatformViewInputConnection();
}
static private class InputTarget {
enum Type {
NO_TARGET,
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework.
FRAMEWORK_CLIENT,
// InputConnection is managed by an embedded platform view.
PLATFORM_VIEW
}
public InputTarget(@NonNull Type type, int id) {
this.type = type;
this.id = id;
}
@NonNull
Type type;
// The ID of the input target.
//
// For framework clients this is the framework input connection client ID.
// For platform views this is the platform view's ID.
int id;
}
}
\ No newline at end of file
......@@ -19,6 +19,7 @@ import java.util.Map;
import com.idlefish.flutterboost.NewFlutterBoost;
import com.idlefish.flutterboost.XFlutterView;
import com.idlefish.flutterboost.interfaces.IFlutterViewContainer;
import com.idlefish.flutterboost.interfaces.IOperateSyncer;
import io.flutter.Log;
......@@ -44,7 +45,7 @@ public class FlutterActivityAndFragmentDelegate implements IFlutterViewContaine
@Nullable
private FlutterSplashView flutterSplashView;
@Nullable
private FlutterView flutterView;
private XFlutterView flutterView;
@Nullable
private PlatformPlugin platformPlugin;
......@@ -167,7 +168,7 @@ public class FlutterActivityAndFragmentDelegate implements IFlutterViewContaine
mSyncer = NewFlutterBoost.instance().containerManager().generateSyncer(this);
ensureAlive();
flutterView = new FlutterView(host.getActivity(), NewFlutterBoost.instance().platform().renderMode(), host.getTransparencyMode());
flutterView = new XFlutterView(host.getActivity(), NewFlutterBoost.instance().platform().renderMode(), host.getTransparencyMode());
flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
......@@ -229,6 +230,7 @@ public class FlutterActivityAndFragmentDelegate implements IFlutterViewContaine
Log.v(TAG, "onPause()");
ensureAlive();
mSyncer.onDisappear();
flutterEngine.getLifecycleChannel().appIsInactive();
}
......@@ -236,8 +238,6 @@ public class FlutterActivityAndFragmentDelegate implements IFlutterViewContaine
void onStop() {
Log.v(TAG, "onStop()");
ensureAlive();
mSyncer.onDisappear();
flutterEngine.getLifecycleChannel().appIsPaused();
// flutterView.detachFromFlutterEngine();
}
......
......@@ -28,7 +28,7 @@ public class FlutterSplashView extends FrameLayout {
@Nullable
private SplashScreen splashScreen;
@Nullable
private FlutterView flutterView;
private XFlutterView flutterView;
@Nullable
private View splashScreenView;
@Nullable
......@@ -112,7 +112,7 @@ public class FlutterSplashView extends FrameLayout {
* If no {@code splashScreen} is provided, this {@code FlutterSplashView} displays the
* given {@code flutterView} on its own.
*/
public void displayFlutterViewWithSplash(@NonNull FlutterView flutterView, @Nullable SplashScreen splashScreen) {
public void displayFlutterViewWithSplash(@NonNull XFlutterView flutterView, @Nullable SplashScreen splashScreen) {
// If we were displaying a previous FlutterView, remove it.
if (this.flutterView != null) {
this.flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
......
......@@ -35,7 +35,7 @@ class _MyAppState extends State<MyApp> {
return MaterialApp(
title: 'Flutter Boost example',
builder: FlutterBoost.init(postPush: _onRoutePushed),
home: FlutterRouteWidget());
home: Container());
}
void _onRoutePushed(
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment