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

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

上级 91b1460d
package io.flutter.embedding.engine.mutatorsstack; package io.flutter.embedding.engine.mutatorsstack;
import static android.view.View.OnFocusChangeListener;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Path; import android.graphics.Path;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.android.AndroidTouchProcessor; import io.flutter.embedding.android.AndroidTouchProcessor;
/** /**
...@@ -31,7 +38,7 @@ public class FlutterMutatorView extends FrameLayout { ...@@ -31,7 +38,7 @@ public class FlutterMutatorView extends FrameLayout {
public FlutterMutatorView( public FlutterMutatorView(
@NonNull Context context, @NonNull Context context,
float screenDensity, float screenDensity,
@NonNull AndroidTouchProcessor androidTouchProcessor) { @Nullable AndroidTouchProcessor androidTouchProcessor) {
super(context, null); super(context, null);
this.screenDensity = screenDensity; this.screenDensity = screenDensity;
this.androidTouchProcessor = androidTouchProcessor; this.androidTouchProcessor = androidTouchProcessor;
...@@ -39,9 +46,52 @@ public class FlutterMutatorView extends FrameLayout { ...@@ -39,9 +46,52 @@ public class FlutterMutatorView extends FrameLayout {
/** Initialize the FlutterMutatorView. */ /** Initialize the FlutterMutatorView. */
public FlutterMutatorView(@NonNull Context context) { public FlutterMutatorView(@NonNull Context context) {
super(context, null); this(context, 1, /* androidTouchProcessor=*/ null);
this.screenDensity = 1; }
this.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 { ...@@ -82,8 +82,16 @@ public class TextInputChannel {
result.success(null); result.success(null);
break; break;
case "TextInput.setPlatformViewClient": case "TextInput.setPlatformViewClient":
final int id = (int) args; try {
textInputMethodHandler.setPlatformViewClient(id); 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; break;
case "TextInput.setEditingState": case "TextInput.setEditingState":
try { try {
...@@ -360,8 +368,10 @@ public class TextInputChannel { ...@@ -360,8 +368,10 @@ public class TextInputChannel {
* different client is set. * different client is set.
* *
* @param id the ID of the platform view to be set as a text input client. * @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. * Sets the size and the transform matrix of the current text input client.
......
...@@ -102,7 +102,11 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -102,7 +102,11 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@Override @Override
public void hide() { public void hide() {
hideTextInput(mView); if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
notifyViewExited();
} else {
hideTextInput(mView);
}
} }
@Override @Override
...@@ -129,8 +133,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -129,8 +133,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
} }
@Override @Override
public void setPlatformViewClient(int platformViewId) { public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
setPlatformViewTextInputClient(platformViewId); setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
} }
@Override @Override
...@@ -189,7 +193,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -189,7 +193,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
* display to another. * display to another.
*/ */
public void lockPlatformViewInputConnection() { public void lockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
isInputConnectionLocked = true; isInputConnectionLocked = true;
} }
} }
...@@ -284,7 +288,11 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -284,7 +288,11 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return null; 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) { if (isInputConnectionLocked) {
return lastInputConnection; return lastInputConnection;
} }
...@@ -342,9 +350,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -342,9 +350,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
* input connection. * input connection.
*/ */
public void clearPlatformViewClient(int platformViewId) { 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); inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
hideTextInput(mView); notifyViewExited();
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
mImm.restartInput(mView); mImm.restartInput(mView);
mRestartInputPending = false; mRestartInputPending = false;
} }
...@@ -361,8 +372,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -361,8 +372,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
private void hideTextInput(View view) { private void hideTextInput(View view) {
notifyViewExited(); notifyViewExited();
// Note: a race condition may lead to us hiding the keyboard here just after a platform view has // Note: when a virtual display is used, a race condition may lead to us hiding the keyboard
// shown it. // 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 // This can only potentially happen when switching focus from a Flutter text field to a platform
// view's text // view's text
// field(by text field here I mean anything that keeps the keyboard open). // field(by text field here I mean anything that keeps the keyboard open).
...@@ -393,16 +404,20 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -393,16 +404,20 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
mEditable.addEditingStateListener(this); mEditable.addEditingStateListener(this);
} }
private void setPlatformViewTextInputClient(int platformViewId) { private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
// We need to make sure that the Flutter view is focused so that no imm operations get short if (usesVirtualDisplay) {
// circuited. // We need to make sure that the Flutter view is focused so that no imm operations get short
// Not asking for focus here specifically manifested in a but on API 28 devices where the // circuited.
// platform view's // Not asking for focus here specifically manifested in a but on API 28 devices where the
// request to show a keyboard was ignored. // platform view's request to show a keyboard was ignored.
mView.requestFocus(); mView.requestFocus();
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId); inputTarget = new InputTarget(InputTarget.Type.VD_PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView); mImm.restartInput(mView);
mRestartInputPending = false; mRestartInputPending = false;
} else {
inputTarget = new InputTarget(InputTarget.Type.HC_PLATFORM_VIEW, platformViewId);
lastInputConnection = null;
}
} }
private static boolean composingChanged( private static boolean composingChanged(
...@@ -493,7 +508,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -493,7 +508,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@VisibleForTesting @VisibleForTesting
void clearTextInputClient() { 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 // Focus changes in the framework tree have no guarantees on the order focus nodes are
// notified. A node // notified. A node
// that lost focus may be notified before or after a node that gained focus. // that lost focus may be notified before or after a node that gained focus.
...@@ -530,8 +546,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch ...@@ -530,8 +546,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter // InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter
// framework. // framework.
FRAMEWORK_CLIENT, FRAMEWORK_CLIENT,
// InputConnection is managed by an embedded platform view. // InputConnection is managed by an embedded platform view that is backed by a virtual
PLATFORM_VIEW // 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) { public InputTarget(@NonNull Type type, int id) {
......
...@@ -337,6 +337,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega ...@@ -337,6 +337,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
@Override @Override
public void clearFocus(int viewId) { public void clearFocus(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView != null) {
platformView.getView().clearFocus();
return;
}
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
View view = vdControllers.get(viewId).getView(); View view = vdControllers.get(viewId).getView();
view.clearFocus(); view.clearFocus();
...@@ -732,6 +737,16 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega ...@@ -732,6 +737,16 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
final FlutterMutatorView parentView = final FlutterMutatorView parentView =
new FlutterMutatorView( new FlutterMutatorView(
context, context.getResources().getDisplayMetrics().density, androidTouchProcessor); context, context.getResources().getDisplayMetrics().density, androidTouchProcessor);
parentView.addOnFocusChangeListener(
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(viewId);
} else {
textInputPlugin.clearPlatformViewClient(viewId);
}
});
platformViewParent.put(viewId, parentView); platformViewParent.put(viewId, parentView);
parentView.addView(platformView.getView()); parentView.addView(platformView.getView());
((FlutterView) flutterView).addView(parentView); ((FlutterView) flutterView).addView(parentView);
......
package io.flutter.embedding.engine.mutatorsstack; package io.flutter.embedding.engine.mutatorsstack;
import static android.view.View.OnFocusChangeListener;
import static junit.framework.TestCase.*; import static junit.framework.TestCase.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import io.flutter.embedding.android.AndroidTouchProcessor; import io.flutter.embedding.android.AndroidTouchProcessor;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -76,4 +80,119 @@ public class FlutterMutatorViewTest { ...@@ -76,4 +80,119 @@ public class FlutterMutatorViewTest {
assertTrue(matrixCaptor.getValue().equals(screenMatrix)); 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.
先完成此消息的编辑!
想要评论请 注册