未验证 提交 5c59a1d1 编写于 作者: E Emmanuel Garcia 提交者: GitHub

Allow Flutter focus to interop with Android view hierarchies (#26602)

上级 91b1460d
package io.flutter.embedding.engine.mutatorsstack;
import static android.view.View.OnFocusChangeListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.android.AndroidTouchProcessor;
/**
......@@ -31,7 +38,7 @@ public class FlutterMutatorView extends FrameLayout {
public FlutterMutatorView(
@NonNull Context context,
float screenDensity,
@NonNull AndroidTouchProcessor androidTouchProcessor) {
@Nullable AndroidTouchProcessor androidTouchProcessor) {
super(context, null);
this.screenDensity = screenDensity;
this.androidTouchProcessor = androidTouchProcessor;
......@@ -39,9 +46,52 @@ public class FlutterMutatorView extends FrameLayout {
/** Initialize the FlutterMutatorView. */
public FlutterMutatorView(@NonNull Context context) {
super(context, null);
this.screenDensity = 1;
this.androidTouchProcessor = null;
this(context, 1, /* androidTouchProcessor=*/ null);
}
/**
* Determines if the current view or any descendant view has focus.
*
* @param root The root view.
* @return True if the current view or any descendant view has focus.
*/
@VisibleForTesting
public static boolean childHasFocus(@Nullable View root) {
if (root == null) {
return false;
}
if (root.hasFocus()) {
return true;
}
if (root instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) root;
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
if (childHasFocus(viewGroup.getChildAt(idx))) {
return true;
}
}
}
return false;
}
/**
* Adds a focus change listener that notifies when the current view or any of its descendant views
* have received focus.
*
* @param focusListener The focus listener.
*/
public void addOnFocusChangeListener(@NonNull OnFocusChangeListener focusListener) {
final View mutatorView = this;
final ViewTreeObserver observer = this.getViewTreeObserver();
if (observer.isAlive()) {
observer.addOnGlobalFocusChangeListener(
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
focusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
}
});
}
}
/**
......
......@@ -82,8 +82,16 @@ public class TextInputChannel {
result.success(null);
break;
case "TextInput.setPlatformViewClient":
final int id = (int) args;
textInputMethodHandler.setPlatformViewClient(id);
try {
final JSONObject arguments = (JSONObject) args;
final int platformViewId = arguments.getInt("platformViewId");
final boolean usesVirtualDisplay =
arguments.optBoolean("usesVirtualDisplay", false);
textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay);
result.success(null);
} catch (JSONException exception) {
result.error("error", exception.getMessage(), null);
}
break;
case "TextInput.setEditingState":
try {
......@@ -360,8 +368,10 @@ public class TextInputChannel {
* different client is set.
*
* @param id the ID of the platform view to be set as a text input client.
* @param usesVirtualDisplay True if the platform view uses a virtual display, false if it uses
* hybrid composition.
*/
void setPlatformViewClient(int id);
void setPlatformViewClient(int id, boolean usesVirtualDisplay);
/**
* Sets the size and the transform matrix of the current text input client.
......
......@@ -102,7 +102,11 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@Override
public void hide() {
hideTextInput(mView);
if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
notifyViewExited();
} else {
hideTextInput(mView);
}
}
@Override
......@@ -129,8 +133,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
}
@Override
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
}
@Override
......@@ -189,7 +193,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
* display to another.
*/
public void lockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
isInputConnectionLocked = true;
}
}
......@@ -284,7 +288,11 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return null;
}
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
return null;
}
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
if (isInputConnectionLocked) {
return lastInputConnection;
}
......@@ -342,9 +350,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
* input connection.
*/
public void clearPlatformViewClient(int platformViewId) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
if ((inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW
|| inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW)
&& inputTarget.id == platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
hideTextInput(mView);
notifyViewExited();
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
mImm.restartInput(mView);
mRestartInputPending = false;
}
......@@ -361,8 +372,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
private void hideTextInput(View view) {
notifyViewExited();
// Note: a race condition may lead to us hiding the keyboard here just after a platform view has
// shown it.
// Note: when a virtual display is used, 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).
......@@ -393,16 +404,20 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
mEditable.addEditingStateListener(this);
}
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 setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
if (usesVirtualDisplay) {
// 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.VD_PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
} else {
inputTarget = new InputTarget(InputTarget.Type.HC_PLATFORM_VIEW, platformViewId);
lastInputConnection = null;
}
}
private static boolean composingChanged(
......@@ -493,7 +508,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@VisibleForTesting
void clearTextInputClient() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
// This only applies to platform views that use a virtual display.
// 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.
......@@ -530,8 +546,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
// 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
// InputConnection is managed by an embedded platform view that is backed by a virtual
// display (VD).
VD_PLATFORM_VIEW,
// InputConnection is managed by an embedded platform view that is embeded in the Android view
// hierarchy, and uses hybrid composition (HC).
HC_PLATFORM_VIEW,
}
public InputTarget(@NonNull Type type, int id) {
......
......@@ -337,6 +337,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
@Override
public void clearFocus(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView != null) {
platformView.getView().clearFocus();
return;
}
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
View view = vdControllers.get(viewId).getView();
view.clearFocus();
......@@ -732,6 +737,16 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
final FlutterMutatorView parentView =
new FlutterMutatorView(
context, context.getResources().getDisplayMetrics().density, androidTouchProcessor);
parentView.addOnFocusChangeListener(
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(viewId);
} else {
textInputPlugin.clearPlatformViewClient(viewId);
}
});
platformViewParent.put(viewId, parentView);
parentView.addView(platformView.getView());
((FlutterView) flutterView).addView(parentView);
......
package io.flutter.embedding.engine.mutatorsstack;
import static android.view.View.OnFocusChangeListener;
import static junit.framework.TestCase.*;
import static org.mockito.Mockito.*;
import android.graphics.Matrix;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import io.flutter.embedding.android.AndroidTouchProcessor;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -76,4 +80,119 @@ public class FlutterMutatorViewTest {
assertTrue(matrixCaptor.getValue().equals(screenMatrix));
}
}
@Test
public void childHasFocus_rootHasFocus() {
final View rootView = mock(View.class);
when(rootView.hasFocus()).thenReturn(true);
assertTrue(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void childHasFocus_rootDoesNotHaveFocus() {
final View rootView = mock(View.class);
when(rootView.hasFocus()).thenReturn(false);
assertFalse(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void childHasFocus_rootIsNull() {
assertFalse(FlutterMutatorView.childHasFocus(null));
}
@Test
public void childHasFocus_childHasFocus() {
final View childView = mock(View.class);
when(childView.hasFocus()).thenReturn(true);
final ViewGroup rootView = mock(ViewGroup.class);
when(rootView.getChildCount()).thenReturn(1);
when(rootView.getChildAt(0)).thenReturn(childView);
assertTrue(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void childHasFocus_childDoesNotHaveFocus() {
final View childView = mock(View.class);
when(childView.hasFocus()).thenReturn(false);
final ViewGroup rootView = mock(ViewGroup.class);
when(rootView.getChildCount()).thenReturn(1);
when(rootView.getChildAt(0)).thenReturn(childView);
assertFalse(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void focusChangeListener_hasFocus() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(true);
final FlutterMutatorView view =
new FlutterMutatorView(RuntimeEnvironment.systemContext) {
@Override
public ViewTreeObserver getViewTreeObserver() {
return viewTreeObserver;
}
@Override
public boolean hasFocus() {
return true;
}
};
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
view.addOnFocusChangeListener(focusListener);
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture());
focusListenerCaptor.getValue().onGlobalFocusChanged(null, null);
verify(focusListener).onFocusChange(view, true);
}
@Test
public void focusChangeListener_doesNotHaveFocus() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(true);
final FlutterMutatorView view =
new FlutterMutatorView(RuntimeEnvironment.systemContext) {
@Override
public ViewTreeObserver getViewTreeObserver() {
return viewTreeObserver;
}
@Override
public boolean hasFocus() {
return false;
}
};
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
view.addOnFocusChangeListener(focusListener);
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture());
focusListenerCaptor.getValue().onGlobalFocusChanged(null, null);
verify(focusListener).onFocusChange(view, false);
}
@Test
public void focusChangeListener_viewTreeObserverIsAliveFalseDoesNotThrow() {
final FlutterMutatorView view =
new FlutterMutatorView(RuntimeEnvironment.systemContext) {
@Override
public ViewTreeObserver getViewTreeObserver() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(false);
return viewTreeObserver;
}
};
view.addOnFocusChangeListener(mock(OnFocusChangeListener.class));
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册