Reland "Keyboard support for embedded Android views. (#9203) (#9257)

#9203 broke the keyboard_resize integration test(see more details in flutter/flutter#34085 (comment)).

This re-lands @9203 and fixes the issue the integration test uncovered by always allowing to hide the keyboard.

The difference from the original change is 07d2598
This commit is contained in:
Amir Hardon 2019-06-10 12:56:35 -07:00 committed by GitHub
parent 36b7123bfc
commit 259d334e05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 295 additions and 28 deletions

View File

@ -477,7 +477,8 @@ public class FlutterView extends FrameLayout {
// in a way that Flutter understands.
textInputPlugin = new TextInputPlugin(
this,
this.flutterEngine.getDartExecutor()
this.flutterEngine.getDartExecutor(),
null
);
androidKeyProcessor = new AndroidKeyProcessor(
this.flutterEngine.getKeyEventChannel(),

View File

@ -26,6 +26,13 @@ public class PlatformViewsChannel {
private final MethodChannel channel;
private PlatformViewsHandler handler;
public void invokeViewFocused(int viewId) {
if (channel == null) {
return;
}
channel.invokeMethod("viewFocused", viewId);
}
private final MethodChannel.MethodCallHandler parsingHandler = new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
@ -51,6 +58,9 @@ public class PlatformViewsChannel {
case "setDirection":
setDirection(call, result);
break;
case "clearFocus":
clearFocus(call, result);
break;
default:
result.notImplemented();
}
@ -172,6 +182,20 @@ public class PlatformViewsChannel {
);
}
}
private void clearFocus(MethodCall call, MethodChannel.Result result) {
int viewId = call.arguments();
try {
handler.clearFocus(viewId);
result.success(null);
} catch (IllegalStateException exception) {
result.error(
"error",
exception.getMessage(),
null
);
}
}
};
/**
@ -241,6 +265,11 @@ public class PlatformViewsChannel {
*/
// TODO(mattcarroll): Introduce an annotation for @TextureId
void setDirection(int viewId, int direction);
/**
* Clears the focus from the platform view with a give id if it is currently focused.
*/
void clearFocus(int viewId);
}
/**

View File

@ -70,6 +70,10 @@ public class TextInputChannel {
result.error("error", exception.getMessage(), null);
}
break;
case "TextInput.setPlatformViewClient":
final int id = (int) args;
textInputMethodHandler.setPlatformViewClient(id);
break;
case "TextInput.setEditingState":
try {
final JSONObject editingState = (JSONObject) args;
@ -218,6 +222,16 @@ public class TextInputChannel {
// TODO(mattcarroll): javadoc
void setClient(int textInputClientId, @NonNull Configuration configuration);
/**
* Sets a platform view as the text input client.
*
* Subsequent calls to createInputConnection will be delegated to the platform view until a
* different client is set.
*
* @param id the ID of the platform view to be set as a text input client.
*/
void setPlatformViewClient(int id);
// TODO(mattcarroll): javadoc
void setEditingState(@NonNull TextEditState editingState);

View File

@ -18,7 +18,7 @@ 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.platform.PlatformViewsController;
/**
* Android implementation of the text input plugin.
@ -30,7 +30,8 @@ public class TextInputPlugin {
private final InputMethodManager mImm;
@NonNull
private final TextInputChannel textInputChannel;
private int mClient = 0;
@NonNull
private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
@Nullable
private TextInputChannel.Configuration configuration;
@Nullable
@ -39,7 +40,13 @@ public class TextInputPlugin {
@Nullable
private InputConnection lastInputConnection;
public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor) {
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 TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, PlatformViewsController platformViewsController) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
@ -61,6 +68,11 @@ public class TextInputPlugin {
setTextInputClient(textInputClientId, configuration);
}
@Override
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
}
@Override
public void setEditingState(TextInputChannel.TextEditState editingState) {
setTextInputEditingState(mView, editingState);
@ -71,6 +83,8 @@ public class TextInputPlugin {
clearTextInputClient();
}
});
this.platformViewsController = platformViewsController;
platformViewsController.attachTextInputPlugin(this);
}
@NonNull
@ -78,6 +92,40 @@ public class TextInputPlugin {
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,
@ -128,8 +176,16 @@ public class TextInputPlugin {
}
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;
}
@ -158,7 +214,7 @@ public class TextInputPlugin {
InputConnectionAdaptor connection = new InputConnectionAdaptor(
view,
mClient,
inputTarget.id,
textInputChannel,
mEditable
);
@ -180,17 +236,28 @@ public class TextInputPlugin {
}
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) {
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
}
private void applyStateToSelection(TextInputChannel.TextEditState state) {
@ -220,6 +287,45 @@ public class TextInputPlugin {
}
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;
}
}

View File

@ -11,7 +11,6 @@ import io.flutter.view.AccessibilityBridge;
* Facilitates interaction between the accessibility bridge and embedded platform views.
*/
public interface PlatformViewsAccessibilityDelegate {
/**
* Returns the root of the view hierarchy for the platform view with the requested id, or null if there is no
* corresponding view.

View File

@ -23,12 +23,15 @@ import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMethodCodec;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.TextureRegistry;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
@ -51,6 +54,8 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// The texture registry maintaining the textures into which the embedded views will be rendered.
private TextureRegistry textureRegistry;
private TextInputPlugin textInputPlugin;
// The system channel used to communicate with the framework about platform views.
private PlatformViewsChannel platformViewsChannel;
@ -59,6 +64,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
private final HashMap<Integer, VirtualDisplayController> vdControllers;
// The set of root views for all active virtual displays managed by this controller.
// This allows an O(1) check whether a view is managed by this controller(by checking if it's root view is in this
// set). This is used by isPlatformView.
private final HashSet<View> vdRootViews;
private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
@ -92,14 +102,19 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
VirtualDisplayController vdController = VirtualDisplayController.create(
context,
accessibilityEventsDelegate,
viewFactory,
textureEntry,
toPhysicalPixels(request.logicalWidth),
toPhysicalPixels(request.logicalHeight),
request.viewId,
createParams
context,
accessibilityEventsDelegate,
viewFactory,
textureEntry,
physicalWidth,
physicalHeight,
request.viewId,
createParams,
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(request.viewId);
}
}
);
if (vdController == null) {
@ -108,7 +123,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
}
vdControllers.put(request.viewId, vdController);
vdController.getView().setLayoutDirection(request.direction);
View platformView = vdController.getView();
platformView.setLayoutDirection(request.direction);
vdRootViews.add(platformView.getRootView());
// TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.
@ -125,6 +142,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
+ viewId);
}
View rootView = vdController.getView().getRootView();
vdRootViews.remove(rootView);
vdController.dispose();
vdControllers.remove(viewId);
}
@ -143,11 +163,28 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
if (textInputPlugin != null) {
// Resizing involved moving the platform view to a new virtual display.
// Doing so potentially results in losing an active input connection.
// To make sure we preserve the input connection when resizing we lock it here
// and unlock after the resize is complete.
textInputPlugin.lockPlatformViewInputConnection();
}
vdRootViews.remove(vdController.getView().getRootView());
vdController.resize(
physicalWidth,
physicalHeight,
onComplete
physicalWidth,
physicalHeight,
new Runnable() {
@Override
public void run() {
if (textInputPlugin != null) {
textInputPlugin.unlockPlatformViewInputConnection();
}
onComplete.run();
}
}
);
vdRootViews.add(vdController.getView().getRootView());
}
@Override
@ -207,6 +244,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
view.setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
View view = vdControllers.get(viewId).getView();
view.clearFocus();
}
private void ensureValidAndroidVersion() {
if (Build.VERSION.SDK_INT < MINIMAL_SDK) {
Log.e(TAG, "Trying to use platform views with API " + Build.VERSION.SDK_INT
@ -221,6 +264,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
registry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
vdRootViews = new HashSet<>();
}
/**
@ -270,6 +314,33 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
accessibilityEventsDelegate.setAccessibilityBridge(null);
}
/**
* Attaches this controller to a text input plugin.
*
* While a text input plugin is available, the platform views controller interacts with it to facilitate
* delegation of text input connections to platform views.
*
* A platform views controller should be attached to a text input plugin whenever it is possible for the Flutter
* framework to receive text input.
*/
public void attachTextInputPlugin(TextInputPlugin textInputPlugin) {
this.textInputPlugin = textInputPlugin;
}
/**
* Detaches this controller from the currently attached text input plugin.
*/
public void detachTextInputPlugin() {
textInputPlugin = null;
}
/**
* Returns true if the view is a platform view managed by this controller.
*/
public boolean isPlatformView(View view) {
return vdRootViews.contains(view.getRootView());
}
public PlatformViewRegistry getRegistry() {
return registry;
}

View File

@ -19,6 +19,7 @@ import android.widget.FrameLayout;
import java.lang.reflect.*;
import static android.content.Context.WINDOW_SERVICE;
import static android.view.View.OnFocusChangeListener;
/*
* A presentation used for hosting a single Android view in a virtual display.
@ -61,6 +62,8 @@ class SingleViewPresentation extends Presentation {
// A reference to the current accessibility bridge to which accessibility events will be delegated.
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final OnFocusChangeListener focusChangeListener;
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
// so when we create the platform view we can tell it its view id.
private int viewId;
@ -78,6 +81,8 @@ class SingleViewPresentation extends Presentation {
private PresentationState state;
private boolean startFocused = false;
/**
* Creates a presentation that will use the view factory to create a new
* platform view in the presentation's onCreate, and attach it.
@ -88,13 +93,15 @@ class SingleViewPresentation extends Presentation {
PlatformViewFactory viewFactory,
AccessibilityEventsDelegate accessibilityEventsDelegate,
int viewId,
Object createParams
Object createParams,
OnFocusChangeListener focusChangeListener
) {
super(outerContext, display);
this.viewFactory = viewFactory;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.viewId = viewId;
this.createParams = createParams;
this.focusChangeListener = focusChangeListener;
state = new PresentationState();
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
@ -113,16 +120,20 @@ class SingleViewPresentation extends Presentation {
Context outerContext,
Display display,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PresentationState state
PresentationState state,
OnFocusChangeListener focusChangeListener,
boolean startFocused
) {
super(outerContext, display);
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
viewFactory = null;
this.state = state;
this.focusChangeListener = focusChangeListener;
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
);
this.startFocused = startFocused;
}
@Override
@ -148,6 +159,14 @@ class SingleViewPresentation extends Presentation {
rootView = new AccessibilityDelegatingFrameLayout(getContext(), accessibilityEventsDelegate, embeddedView);
rootView.addView(container);
rootView.addView(state.fakeWindowViewGroup);
embeddedView.setOnFocusChangeListener(focusChangeListener);
rootView.setFocusableInTouchMode(true);
if (startFocused) {
embeddedView.requestFocus();
} else {
rootView.requestFocus();
}
setContentView(rootView);
}

View File

@ -9,11 +9,14 @@ import android.content.Context;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.ViewTreeObserver;
import io.flutter.view.TextureRegistry;
import static android.view.View.OnFocusChangeListener;
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
class VirtualDisplayController {
@ -25,7 +28,8 @@ class VirtualDisplayController {
int width,
int height,
int viewId,
Object createParams
Object createParams,
OnFocusChangeListener focusChangeListener
) {
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
Surface surface = new Surface(textureEntry.surfaceTexture());
@ -46,13 +50,14 @@ class VirtualDisplayController {
}
return new VirtualDisplayController(
context, accessibilityEventsDelegate, virtualDisplay, viewFactory, surface, textureEntry, viewId, createParams);
context, accessibilityEventsDelegate, virtualDisplay, viewFactory, surface, textureEntry, focusChangeListener, viewId, createParams);
}
private final Context context;
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final int densityDpi;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private final OnFocusChangeListener focusChangeListener;
private VirtualDisplay virtualDisplay;
private SingleViewPresentation presentation;
private Surface surface;
@ -65,21 +70,30 @@ class VirtualDisplayController {
PlatformViewFactory viewFactory,
Surface surface,
TextureRegistry.SurfaceTextureEntry textureEntry,
OnFocusChangeListener focusChangeListener,
int viewId,
Object createParams
) {
this.context = context;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.textureEntry = textureEntry;
this.focusChangeListener = focusChangeListener;
this.surface = surface;
this.virtualDisplay = virtualDisplay;
densityDpi = context.getResources().getDisplayMetrics().densityDpi;
presentation = new SingleViewPresentation(
context, this.virtualDisplay.getDisplay(), viewFactory, accessibilityEventsDelegate, viewId, createParams);
context,
this.virtualDisplay.getDisplay(),
viewFactory,
accessibilityEventsDelegate,
viewId,
createParams,
focusChangeListener);
presentation.show();
}
public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
boolean isFocused = getView().isFocused();
final SingleViewPresentation.PresentationState presentationState = presentation.detachState();
// We detach the surface to prevent it being destroyed when releasing the vd.
//
@ -125,7 +139,13 @@ class VirtualDisplayController {
public void onViewDetachedFromWindow(View v) {}
});
presentation = new SingleViewPresentation(context, virtualDisplay.getDisplay(), accessibilityEventsDelegate, presentationState);
presentation = new SingleViewPresentation(
context,
virtualDisplay.getDisplay(),
accessibilityEventsDelegate,
presentationState,
focusChangeListener,
isFocused);
presentation.show();
}

View File

@ -191,9 +191,11 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture
PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel);
addActivityLifecycleListener(platformPlugin);
mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
mTextInputPlugin = new TextInputPlugin(this, dartExecutor);
PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController();
mTextInputPlugin = new TextInputPlugin(this, dartExecutor, platformViewsController);
androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin);
androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer);
mNativeView.getPluginRegistry().getPlatformViewsController().attachTextInputPlugin(mTextInputPlugin);
// Send initial platform information to Dart
sendLocalesToDart(getResources().getConfiguration());
@ -395,6 +397,12 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture
return mTextInputPlugin.createInputConnection(this, outAttrs);
}
@Override
public boolean checkInputConnectionProxy(View view) {
PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController();
return platformViewsController.isPlatformView(view);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isAttached()) {