This method guarantees to return a non-null object.
+ */
+ private PointerIcon resolveSystemCursor(@NonNull String kind) {
+ if (MouseCursorPlugin.systemCursorConstants == null) {
+ // Initialize the map when first used, because the map can grow big in the future (~70)
+ // and most mobile devices will not use them.
+ MouseCursorPlugin.systemCursorConstants =
+ new HashMap The MouseCursorPlugin instance should not be used after calling this.
+ */
+ public void destroy() {
+ mouseCursorChannel.setMethodHandler(null);
+ }
+
+ /**
+ * A map from Flutter's system cursor {@code kind} to Android's pointer icon constants.
+ *
+ * It is null until the first time a system cursor is requested, at which time it is filled
+ * with the entire mapping.
+ */
+ @NonNull private static HashMap Typically implemented by an {@link android.view.View}, such as a {@code FlutterView}.
+ */
+ public interface MouseCursorViewDelegate {
+ /**
+ * Gets a system pointer icon object for the given {@code type}.
+ *
+ * If typeis not recognized, returns the default pointer icon.
+ *
+ * This is typically implemented by calling {@link android.view.PointerIcon.getSystemIcon}
+ * with the context associated with this view.
+ */
+ public PointerIcon getSystemPointerIcon(int type);
+
+ /**
+ * Request the pointer to display the specified icon object.
+ *
+ * If the delegate is implemented by a {@link android.view.View}, then this method is
+ * automatically implemented by View.
+ */
+ public void setPointerIcon(@NonNull PointerIcon icon);
+ }
+}
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index 739e58054b0..d5c79c9eb54 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -24,6 +24,7 @@ import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
+import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -49,6 +50,7 @@ import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
+import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
@@ -57,6 +59,7 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.editing.TextInputPlugin;
+import io.flutter.plugin.mouse.MouseCursorPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import java.nio.ByteBuffer;
@@ -72,7 +75,8 @@ import java.util.concurrent.atomic.AtomicLong;
* Deprecation: {@link io.flutter.embedding.android.FlutterView} is the new API that now replaces
* this class. See https://flutter.dev/go/android-project-migration for more migration details.
*/
-public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry {
+public class FlutterView extends SurfaceView
+ implements BinaryMessenger, TextureRegistry, MouseCursorPlugin.MouseCursorViewDelegate {
/**
* Interface for those objects that maintain and expose a reference to a {@code FlutterView} (such
* as a full-screen Flutter activity).
@@ -120,6 +124,7 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture
private final SystemChannel systemChannel;
private final InputMethodManager mImm;
private final TextInputPlugin mTextInputPlugin;
+ private final MouseCursorPlugin mMouseCursorPlugin;
private final AndroidKeyProcessor androidKeyProcessor;
private final AndroidTouchProcessor androidTouchProcessor;
private AccessibilityBridge mAccessibilityNodeProvider;
@@ -221,6 +226,11 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture
mNativeView.getPluginRegistry().getPlatformViewsController();
mTextInputPlugin =
new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor));
+ } else {
+ mMouseCursorPlugin = null;
+ }
androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin);
androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer);
mNativeView
@@ -793,6 +803,14 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture
}
}
+ @Override
+ @TargetApi(Build.VERSION_CODES.N)
+ @RequiresApi(Build.VERSION_CODES.N)
+ @NonNull
+ public PointerIcon getSystemPointerIcon(int type) {
+ return PointerIcon.getSystemIcon(getContext(), type);
+ }
+
@Override
@UiThread
public void send(String channel, ByteBuffer message) {
diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
index 820e428fce7..7d0d84ac1f8 100644
--- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java
+++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
@@ -20,6 +20,7 @@ import io.flutter.plugin.common.StandardMessageCodecTest;
import io.flutter.plugin.common.StandardMethodCodecTest;
import io.flutter.plugin.editing.InputConnectionAdaptorTest;
import io.flutter.plugin.editing.TextInputPluginTest;
+import io.flutter.plugin.mouse.MouseCursorPluginTest;
import io.flutter.plugin.platform.PlatformPluginTest;
import io.flutter.plugin.platform.SingleViewPresentationTest;
import io.flutter.util.PreconditionsTest;
@@ -58,6 +59,7 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
SingleViewPresentationTest.class,
SmokeTest.class,
TextInputPluginTest.class,
+ MouseCursorPluginTest.class,
AccessibilityBridgeTest.class,
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index b6c97d56fc3..e024f2530ab 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -27,6 +27,7 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
+import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
@@ -615,6 +616,7 @@ public class FlutterActivityAndFragmentDelegateTest {
when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class));
when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class));
when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
+ when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class));
when(engine.getActivityControlSurface()).thenReturn(mock(ActivityControlSurface.class));
return engine;
diff --git a/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java b/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java
new file mode 100644
index 00000000000..60c275bd1ec
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java
@@ -0,0 +1,71 @@
+package io.flutter.plugin.mouse;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.TargetApi;
+import android.view.PointerIcon;
+import io.flutter.embedding.android.FlutterView;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import org.json.JSONException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(
+ manifest = Config.NONE,
+ shadows = {})
+@RunWith(RobolectricTestRunner.class)
+@TargetApi(24)
+public class MouseCursorPluginTest {
+ @Test
+ public void mouseCursorPlugin_SetsSystemCursorOnRequest() throws JSONException {
+ // Initialize a general MouseCursorPlugin.
+ FlutterView testView = spy(new FlutterView(RuntimeEnvironment.application));
+ MouseCursorChannel mouseCursorChannel = new MouseCursorChannel(mock(DartExecutor.class));
+
+ MouseCursorPlugin mouseCursorPlugin = new MouseCursorPlugin(testView, mouseCursorChannel);
+
+ final StoredResult methodResult = new StoredResult();
+ mouseCursorChannel.synthesizeMethodCall(
+ new MethodCall(
+ "activateSystemCursor",
+ new HashMap