diff --git a/example/pubspec.lock b/example/pubspec.lock index 735cb89..351c2ee 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -109,18 +109,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -149,18 +149,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" path: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -222,6 +222,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -250,10 +258,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" vector_math: dependency: transitive description: @@ -266,10 +274,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.1" webdriver: dependency: transitive description: @@ -279,5 +287,5 @@ packages: source: hosted version: "3.0.3" sdks: - dart: ">=3.5.4 <4.0.0" + dart: ">=3.4.4 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 03e0371..2b2e1c6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,7 +5,7 @@ description: "Demonstrates how to use the flutter_cocos_view plugin." publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ^3.5.4 + sdk: ^3.4.4 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/lib/flutter_cocos_view.dart b/lib/flutter_cocos_view.dart index da38c7f..dd0df7b 100644 --- a/lib/flutter_cocos_view.dart +++ b/lib/flutter_cocos_view.dart @@ -5,10 +5,11 @@ // platforms in the `pubspec.yaml` at // https://flutter.dev/to/pubspec-plugin-platforms. -import 'flutter_cocos_view_platform_interface.dart'; +library flutter_Cocos_widget; -class FlutterCocosView { - Future getPlatformVersion() { - return FlutterCocosViewPlatform.instance.getPlatformVersion(); - } -} +export 'src/facade_controller.dart'; +export 'src/facade_widget.dart' +if (dart.library.io) 'src/io/cocos_widget.dart'; +export 'src/helpers/events.dart'; +export 'src/helpers/misc.dart'; +export 'src/helpers/types.dart'; diff --git a/lib/flutter_cocos_view_method_channel.dart b/lib/flutter_cocos_view_method_channel.dart deleted file mode 100644 index 622867f..0000000 --- a/lib/flutter_cocos_view_method_channel.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'flutter_cocos_view_platform_interface.dart'; - -/// An implementation of [FlutterCocosViewPlatform] that uses method channels. -class MethodChannelFlutterCocosView extends FlutterCocosViewPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('flutter_cocos_view'); - - @override - Future getPlatformVersion() async { - final version = await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } -} diff --git a/lib/flutter_cocos_view_platform_interface.dart b/lib/flutter_cocos_view_platform_interface.dart deleted file mode 100644 index 8207822..0000000 --- a/lib/flutter_cocos_view_platform_interface.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'flutter_cocos_view_method_channel.dart'; - -abstract class FlutterCocosViewPlatform extends PlatformInterface { - /// Constructs a FlutterCocosViewPlatform. - FlutterCocosViewPlatform() : super(token: _token); - - static final Object _token = Object(); - - static FlutterCocosViewPlatform _instance = MethodChannelFlutterCocosView(); - - /// The default instance of [FlutterCocosViewPlatform] to use. - /// - /// Defaults to [MethodChannelFlutterCocosView]. - static FlutterCocosViewPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [FlutterCocosViewPlatform] when - /// they register themselves. - static set instance(FlutterCocosViewPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } -} diff --git a/lib/src/facade_controller.dart b/lib/src/facade_controller.dart new file mode 100644 index 0000000..5cb20bb --- /dev/null +++ b/lib/src/facade_controller.dart @@ -0,0 +1,103 @@ +typedef void CocosCreatedCallback(CocosWidgetController controller); + +abstract class CocosWidgetController { + static dynamic webRegistrar; + + /// Method required for web initialization + static void registerWith(dynamic registrar) { + webRegistrar = registrar; + } + + /// Initialize [CocosWidgetController] with [id] + /// Mainly for internal use when instantiating a [CocosWidgetController] passed + /// in [CocosWidget.onCocosCreated] callback. + static Future init( + int id, dynamic CocosWidgetState) async { + throw UnimplementedError('init() has not been implemented.'); + } + + /// Checks to see if Cocos player is ready to be used + /// Returns `true` if Cocos player is ready. + Future? isReady() { + throw UnimplementedError('isReady() has not been implemented.'); + } + + /// Get the current pause state of the Cocos player + /// Returns `true` if Cocos player is paused. + Future? isPaused() { + throw UnimplementedError('isPaused() has not been implemented.'); + } + + /// Get the current load state of the Cocos player + /// Returns `true` if Cocos player is loaded. + Future? isLoaded() { + throw UnimplementedError('isLoaded() has not been implemented.'); + } + + /// Helper method to know if Cocos has been put in background mode (WIP) unstable + /// Returns `true` if Cocos player is in background. + Future? inBackground() { + throw UnimplementedError('inBackground() has not been implemented.'); + } + + /// Creates a Cocos player if it's not already created. Please only call this if Cocos is not ready, + /// or is in unloaded state. Use [isLoaded] to check. + /// Returns `true` if Cocos player was created succesfully. + Future? create() { + throw UnimplementedError('create() has not been implemented.'); + } + + /// Post message to Cocos from flutter. This method takes in a string [message]. + /// The [gameObject] must match the name of an actual Cocos game object in a scene at runtime, and the [methodName], + /// must exist in a `MonoDevelop` `class` and also exposed as a method. [message] is an parameter taken by the method + /// + /// ```dart + /// postMessage("GameManager", "openScene", "ThirdScene") + /// ``` + Future? postMessage(String gameObject, methodName, message) { + throw UnimplementedError('postMessage() has not been implemented.'); + } + + /// Post message to Cocos from flutter. This method takes in a Json or map structure as the [message]. + /// The [gameObject] must match the name of an actual Cocos game object in a scene at runtime, and the [methodName], + /// must exist in a `MonoDevelop` `class` and also exposed as a method. [message] is an parameter taken by the method + /// + /// ```dart + /// postJsonMessage("GameManager", "openScene", {"buildIndex": 3, "name": "ThirdScene"}) + /// ``` + Future? postJsonMessage( + String gameObject, String methodName, Map message) { + throw UnimplementedError('postJsonMessage() has not been implemented.'); + } + + /// Pause the Cocos in-game player with this method + Future? pause() { + throw UnimplementedError('pause() has not been implemented.'); + } + + /// Resume the Cocos in-game player with this method idf it is in a paused state + Future? resume() { + throw UnimplementedError('resume() has not been implemented.'); + } + + /// Sometimes you want to open Cocos in it's own process and openInNativeProcess does just that. + /// It works for Android and iOS is WIP + Future? openInNativeProcess() { + throw UnimplementedError('openInNativeProcess() has not been implemented.'); + } + + /// Unloads Cocos player from th current process (Works on Android only for now) + /// iOS is WIP. For more information please read [Cocos Docs](https://docs.Cocos3d.com/2020.2/Documentation/Manual/CocosasaLibrary.html) + Future? unload() { + throw UnimplementedError('unload() has not been implemented.'); + } + + /// Quits Cocos player. Note that this kills the current flutter process, thus quiting the app + Future? quit() { + throw UnimplementedError('quit() has not been implemented.'); + } + + void dispose() { + throw UnimplementedError('dispose() has not been implemented.'); + } +} diff --git a/lib/src/facade_widget.dart b/lib/src/facade_widget.dart new file mode 100644 index 0000000..22b0859 --- /dev/null +++ b/lib/src/facade_widget.dart @@ -0,0 +1,89 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter_cocos_view/src/facade_controller.dart'; + +import 'helpers/misc.dart'; + +class CocosWidget extends StatefulWidget { + CocosWidget({ + Key? key, + required this.onCocosCreated, + this.onCocosMessage, + this.fullscreen = false, + this.enablePlaceholder = false, + this.runImmediately = false, + this.unloadOnDispose = false, + this.printSetupLog = true, + this.onCocosUnloaded, + this.gestureRecognizers, + this.placeholder, + this.useAndroidViewSurface = false, + this.onCocosSceneLoaded, + this.uiLevel = 1, + this.borderRadius = BorderRadius.zero, + this.layoutDirection, + this.hideStatus = false, + }); + + ///Event fires when the Cocos player is created. + final CocosCreatedCallback onCocosCreated; + + ///Event fires when the [CocosWidget] gets a message from Cocos. + final CocosMessageCallback? onCocosMessage; + + ///Event fires when the [CocosWidget] gets a scene loaded from Cocos. + final CocosSceneChangeCallback? onCocosSceneLoaded; + + ///Event fires when the [CocosWidget] Cocos player gets unloaded. + final CocosUnloadCallback? onCocosUnloaded; + + final Set>? gestureRecognizers; + + /// Set to true to force Cocos to fullscreen + final bool fullscreen; + + /// Set to true to force Cocos to fullscreen + final bool hideStatus; + + /// Controls the layer in which Cocos widget is rendered in flutter (defaults to 1) + final int uiLevel; + + /// This flag tells android to load Cocos as the flutter app starts (Android only) + final bool runImmediately; + + /// This flag tells android to unload Cocos whenever widget is disposed + final bool unloadOnDispose; + + /// This flag enables placeholder widget + final bool enablePlaceholder; + + /// This flag enables placeholder widget + final bool printSetupLog; + + /// This flag allows you use AndroidView instead of PlatformViewLink for android + final bool? useAndroidViewSurface; + + /// This is just a helper to render a placeholder widget + final Widget? placeholder; + + /// Border radius + final BorderRadius borderRadius; + + /// The layout direction to use for the embedded view. + /// + /// If this is null, the ambient [Directionality] is used instead. If there is + /// no ambient [Directionality], [TextDirection.ltr] is used. + final TextDirection? layoutDirection; + + @override + _CocosWidgetState createState() => _CocosWidgetState(); +} + +class _CocosWidgetState extends State { + @override + Widget build(BuildContext context) { + return Text( + '$defaultTargetPlatform is not yet supported by the Cocos player plugin'); + } +} diff --git a/lib/src/helpers/events.dart b/lib/src/helpers/events.dart new file mode 100644 index 0000000..a243558 --- /dev/null +++ b/lib/src/helpers/events.dart @@ -0,0 +1,36 @@ +import 'package:flutter_cocos_view/src/helpers/types.dart'; + +class CocosEvent { + /// The ID of the Cocos this event is associated to. + final int cocosId; + + /// The value wrapped by this event + final T value; + + /// Build a Cocos Event, that relates a mapId with a given value. + /// + /// The `cocosId` is the id of the map that triggered the event. + /// `value` may be `null` in events that don't transport any meaningful data. + CocosEvent(this.cocosId, this.value); +} + +class CocosSceneLoadedEvent extends CocosEvent { + CocosSceneLoadedEvent(int cocosId, SceneLoaded? value) + : super(cocosId, value); +} + +class CocosLoadedEvent extends CocosEvent { + CocosLoadedEvent(int cocosId, void value) : super(cocosId, value); +} + +class CocosUnLoadedEvent extends CocosEvent { + CocosUnLoadedEvent(int cocosId, void value) : super(cocosId, value); +} + +class CocosCreatedEvent extends CocosEvent { + CocosCreatedEvent(int cocosId, void value) : super(cocosId, value); +} + +class CocosMessageEvent extends CocosEvent { + CocosMessageEvent(int cocosId, dynamic value) : super(cocosId, value); +} diff --git a/lib/src/helpers/misc.dart b/lib/src/helpers/misc.dart new file mode 100644 index 0000000..e490268 --- /dev/null +++ b/lib/src/helpers/misc.dart @@ -0,0 +1,27 @@ +import 'package:flutter_cocos_view/src/helpers/types.dart'; + +/// Error thrown when an unknown cocos ID is provided to a method channel API. +class UnknownCocosIDError extends Error { + /// Creates an assertion error with the provided [cocosId] and optional + /// [message]. + UnknownCocosIDError(this.cocosId, [this.message]); + + /// The unknown ID. + final int cocosId; + + /// Message describing the assertion error. + final Object? message; + + String toString() { + if (message != null) { + return "Unknown cocos ID $cocosId: ${Error.safeToString(message)}"; + } + return "Unknown cocos ID $cocosId"; + } +} + +typedef void CocosMessageCallback(dynamic handler); + +typedef void CocosSceneChangeCallback(SceneLoaded? message); + +typedef void CocosUnloadCallback(); diff --git a/lib/src/helpers/types.dart b/lib/src/helpers/types.dart new file mode 100644 index 0000000..153548d --- /dev/null +++ b/lib/src/helpers/types.dart @@ -0,0 +1,31 @@ +class SceneLoaded { + final String? name; + final int? buildIndex; + final bool? isLoaded; + final bool? isValid; + + SceneLoaded({this.name, this.buildIndex, this.isLoaded, this.isValid}); + + /// Mainly for internal use when calling [CameraUpdate.newCameraPosition]. + dynamic toMap() => { + 'name': name, + 'buildIndex': buildIndex, + 'isLoaded': isLoaded, + 'isValid': isValid, + }; + + /// Deserializes [SceneLoaded] from a map. + /// + /// Mainly for internal use. + static SceneLoaded? fromMap(dynamic json) { + if (json == null) { + return null; + } + return SceneLoaded( + name: json['name'], + buildIndex: json['buildIndex'], + isLoaded: json['isLoaded'], + isValid: json['isValid'], + ); + } +} diff --git a/lib/src/io/cocos_widget.dart b/lib/src/io/cocos_widget.dart new file mode 100644 index 0000000..18adec3 --- /dev/null +++ b/lib/src/io/cocos_widget.dart @@ -0,0 +1,201 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../facade_controller.dart'; +import '../helpers/misc.dart'; +import 'device_method.dart'; +import 'mobile_cocos_widget_controller.dart'; +import 'cocos_widget_platform.dart'; + +int _nextCocosCreationId = 0; + +/// Android specific settings for [CocosWidget]. +class AndroidCocosWidgetFlutter { + /// Whether to render [CocosWidget] with a [AndroidViewSurface] to build the Flutter Cocos widget. + /// + /// This implementation uses hybrid composition to render the Flutter Cocos + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// Defaults to true. + static bool get useAndroidViewSurface { + final CocosWidgetPlatform platform = CocosWidgetPlatform.instance; + if (platform is MethodChannelCocosWidget) { + return platform.useAndroidViewSurface; + } + return false; + } + + /// Set whether to render [CocosWidget] with a [AndroidViewSurface] to build the Flutter Cocos widget. + /// + /// This implementation uses hybrid composition to render the Cocos Widget + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// Defaults to true. + static set useAndroidViewSurface(bool useAndroidViewSurface) { + final CocosWidgetPlatform platform = CocosWidgetPlatform.instance; + if (platform is MethodChannelCocosWidget) { + platform.useAndroidViewSurface = useAndroidViewSurface; + } + } +} + +typedef MobileCocosWidgetState = _CocosWidgetState; + +class CocosWidget extends StatefulWidget { + CocosWidget({ + Key? key, + required this.onCocosCreated, + this.onCocosMessage, + this.fullscreen = false, + this.enablePlaceholder = false, + this.runImmediately = false, + this.unloadOnDispose = false, + this.printSetupLog = true, + this.onCocosUnloaded, + this.gestureRecognizers, + this.placeholder, + this.useAndroidViewSurface = false, + this.onCocosSceneLoaded, + this.uiLevel = 1, + this.borderRadius = BorderRadius.zero, + this.layoutDirection, + this.hideStatus = false, + this.webUrl, + }); + + ///Event fires when the Cocos player is created. + final CocosCreatedCallback onCocosCreated; + + /// WebGL url source. + final String? webUrl; + + ///Event fires when the [CocosWidget] gets a message from Cocos. + final CocosMessageCallback? onCocosMessage; + + ///Event fires when the [CocosWidget] gets a scene loaded from Cocos. + final CocosSceneChangeCallback? onCocosSceneLoaded; + + ///Event fires when the [CocosWidget] Cocos player gets unloaded. + final CocosUnloadCallback? onCocosUnloaded; + + final Set>? gestureRecognizers; + + /// Set to true to force Cocos to fullscreen + final bool fullscreen; + + /// Set to true to force Cocos to fullscreen + final bool hideStatus; + + /// Controls the layer in which Cocos widget is rendered in flutter (defaults to 1) + final int uiLevel; + + /// This flag tells android to load Cocos as the flutter app starts (Android only) + final bool runImmediately; + + /// This flag tells android to unload Cocos whenever widget is disposed + final bool unloadOnDispose; + + /// This flag enables placeholder widget + final bool enablePlaceholder; + + /// This flag enables placeholder widget + final bool printSetupLog; + + /// This flag allows you use AndroidView instead of PlatformViewLink for android + final bool? useAndroidViewSurface; + + /// This is just a helper to render a placeholder widget + final Widget? placeholder; + + /// Border radius + final BorderRadius borderRadius; + + /// The layout direction to use for the embedded view. + /// + /// If this is null, the ambient [Directionality] is used instead. If there is + /// no ambient [Directionality], [TextDirection.ltr] is used. + final TextDirection? layoutDirection; + + @override + _CocosWidgetState createState() => _CocosWidgetState(); +} + +class _CocosWidgetState extends State { + late int _CocosId; + + CocosWidgetController? _controller; + + @override + void initState() { + super.initState(); + + if (!kIsWeb) { + _CocosId = _nextCocosCreationId++; + } else { + _CocosId = 0; + } + } + + @override + Future dispose() async { + if (!kIsWeb) { + if (_nextCocosCreationId > 0) --_nextCocosCreationId; + } + try { + _controller?.dispose(); + _controller = null; + } catch (e) { + // todo: implement + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Map CocosOptions = { + 'fullscreen': widget.fullscreen, + 'uiLevel': widget.uiLevel, + 'hideStatus': widget.hideStatus, + 'unloadOnDispose': widget.unloadOnDispose, + 'runImmediately': widget.runImmediately, + }; + + if (widget.enablePlaceholder) { + return widget.placeholder ?? + Text('Placeholder mode enabled, no native code will be called'); + } + + return CocosWidgetPlatform.instance.buildViewWithTextDirection( + _CocosId, + _onPlatformViewCreated, + cocosOptions: CocosOptions, + textDirection: widget.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.ltr, + gestureRecognizers: widget.gestureRecognizers, + useAndroidViewSurf: widget.useAndroidViewSurface, + cocosSrcUrl: widget.webUrl, + ); + } + + Future _onPlatformViewCreated(int id) async { + final controller = await MobileCocosWidgetController.init(id, this); + _controller = controller; + widget.onCocosCreated(controller); + + if (widget.printSetupLog) { + log('*********************************************'); + log('** flutter Cocos controller setup complete **'); + log('*********************************************'); + } + } +} diff --git a/lib/src/io/cocos_widget_platform.dart b/lib/src/io/cocos_widget_platform.dart new file mode 100644 index 0000000..dfefab3 --- /dev/null +++ b/lib/src/io/cocos_widget_platform.dart @@ -0,0 +1,154 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../helpers/events.dart'; +import 'device_method.dart'; + +abstract class CocosWidgetPlatform extends PlatformInterface { + /// Constructs a CocosViewFlutterPlatform. + CocosWidgetPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CocosWidgetPlatform _instance = MethodChannelCocosWidget(); + + /// The default instance of [CocosWidgetPlatform] to use. + /// + /// Defaults to [MethodChannelCocosWidgetFlutter]. + static CocosWidgetPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [CocosWidgetPlatform] when they register themselves. + static set instance(CocosWidgetPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// /// Initializes the platform interface with [id]. + /// + /// This method is called when the plugin is first initialized. + Future init(int cocosId) { + throw UnimplementedError('init() has not been implemented.'); + } + + Future isReady({required int cocosId}) async { + throw UnimplementedError('init() has not been implemented.'); + } + + Future isPaused({required int cocosId}) async { + throw UnimplementedError('isPaused() has not been implemented.'); + } + + Future isLoaded({required int cocosId}) async { + throw UnimplementedError('isLoaded() has not been implemented.'); + } + + Future inBackground({required int cocosId}) async { + throw UnimplementedError('inBackground() has not been implemented.'); + } + + Future createCocosPlayer({required int cocosId}) async { + throw UnimplementedError('createCocosPlayer() has not been implemented.'); + } + + Future postMessage( + {required int cocosId, + required String gameObject, + required String methodName, + required String message}) { + throw UnimplementedError('postMessage() has not been implemented.'); + } + + Future postJsonMessage( + {required int cocosId, + required String gameObject, + required String methodName, + required Map message}) { + throw UnimplementedError('postJsonMessage() has not been implemented.'); + } + + Future pausePlayer({required int cocosId}) async { + throw UnimplementedError('pausePlayer() has not been implemented.'); + } + + Future resumePlayer({required int cocosId}) async { + throw UnimplementedError('resumePlayer() has not been implemented.'); + } + + /// Opens cocos in it's own activity. Android only. + Future openInNativeProcess({required int cocosId}) async { + throw UnimplementedError('openInNativeProcess() has not been implemented.'); + } + + Future unloadPlayer({required int cocosId}) async { + throw UnimplementedError('unloadPlayer() has not been implemented.'); + } + + Future quitPlayer({required int cocosId}) async { + throw UnimplementedError('quitPlayer() has not been implemented.'); + } + + Stream onCocosMessage({required int cocosId}) { + throw UnimplementedError('onCocosMessage() has not been implemented.'); + } + + Stream onCocosUnloaded({required int cocosId}) { + throw UnimplementedError('onCocosUnloaded() has not been implemented.'); + } + + Stream onCocosCreated({required int cocosId}) { + throw UnimplementedError('onCocosUnloaded() has not been implemented.'); + } + + Stream onCocosSceneLoaded({required int cocosId}) { + throw UnimplementedError('onCocosSceneLoaded() has not been implemented.'); + } + + /// Dispose of whatever resources the `cocosId` is holding on to. + void dispose({required int cocosId}) { + throw UnimplementedError('dispose() has not been implemented.'); + } + + /// Returns a widget displaying the cocos view + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + Map cocosOptions = const {}, + Set>? gestureRecognizers, + bool? useAndroidViewSurf, + String? cocosSrcUrl, + }) { + throw UnimplementedError('buildView() has not been implemented.'); + } + + /// Returns a widget displaying the cocos view. + /// + /// This method is similar to [buildView], but contains a parameter for + /// platforms that require a text direction. + /// + /// Default behavior passes all parameters except `textDirection` to + /// [buildView]. This is for backward compatibility with existing + /// implementations. Platforms that use the text direction should override + /// this as the primary implementation, and delegate to it from buildView. + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required TextDirection textDirection, + Set>? gestureRecognizers, + Map cocosOptions = const {}, + bool? useAndroidViewSurf, + String? cocosSrcUrl, + }) { + return buildView( + creationId, + onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + cocosOptions: cocosOptions, + useAndroidViewSurf: useAndroidViewSurf, + cocosSrcUrl: cocosSrcUrl, + ); + } +} diff --git a/lib/src/io/device_method.dart b/lib/src/io/device_method.dart new file mode 100644 index 0000000..3d4c44f --- /dev/null +++ b/lib/src/io/device_method.dart @@ -0,0 +1,299 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import '../helpers/events.dart'; +import '../helpers/misc.dart'; +import '../helpers/types.dart'; +import 'cocos_widget_platform.dart'; +import 'windows_cocos_widget_view.dart'; + +class MethodChannelCocosWidget extends CocosWidgetPlatform { + // Every method call passes the int cocosId + late final Map _channels = {}; + + /// Set [CocosWidgetFlutterPlatform] to use [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Cocos Widget + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// Defaults to false. + bool useAndroidViewSurface = true; + + /// Accesses the MethodChannel associated to the passed cocosId. + MethodChannel channel(int cocosId) { + MethodChannel? channel = _channels[cocosId]; + if (channel == null) { + throw UnknownCocosIDError(cocosId); + } + return channel; + } + + MethodChannel ensureChannelInitialized(int cocosId) { + MethodChannel? channel = _channels[cocosId]; + if (channel == null) { + channel = MethodChannel('plugin.xraph.com/cocos_view_$cocosId'); + + channel.setMethodCallHandler( + (MethodCall call) => _handleMethodCall(call, cocosId)); + _channels[cocosId] = channel; + } + return channel; + } + + /// Initializes the platform interface with [id]. + /// + /// This method is called when the plugin is first initialized. + @override + Future init(int cocosId) { + MethodChannel channel = ensureChannelInitialized(cocosId); + return channel.invokeMethod('cocos#waitForCocos'); + } + + /// Dispose of the native resources. + @override + Future dispose({int? cocosId}) async { + try { + if (cocosId != null) await channel(cocosId).invokeMethod('cocos#dispose'); + } catch (e) { + // ignore + } + } + + // The controller we need to broadcast the different events coming + // from handleMethodCall. + // + // It is a `broadcast` because multiple controllers will connect to + // different stream views of this Controller. + final StreamController _cocosStreamController = + StreamController.broadcast(); + + // Returns a filtered view of the events in the _controller, by cocosId. + Stream _events(int cocosId) => + _cocosStreamController.stream.where((event) => event.cocosId == cocosId); + + Future _handleMethodCall(MethodCall call, int cocosId) async { + switch (call.method) { + case "events#onCocosMessage": + _cocosStreamController.add(CocosMessageEvent(cocosId, call.arguments)); + break; + case "events#onCocosUnloaded": + _cocosStreamController.add(CocosLoadedEvent(cocosId, call.arguments)); + break; + case "events#onCocosSceneLoaded": + _cocosStreamController.add(CocosSceneLoadedEvent( + cocosId, SceneLoaded.fromMap(call.arguments))); + break; + case "events#onCocosCreated": + _cocosStreamController.add(CocosCreatedEvent(cocosId, call.arguments)); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future isPaused({required int cocosId}) async { + return await channel(cocosId).invokeMethod('cocos#isPaused'); + } + + @override + Future isReady({required int cocosId}) async { + return await channel(cocosId).invokeMethod('cocos#isReady'); + } + + @override + Future isLoaded({required int cocosId}) async { + return await channel(cocosId).invokeMethod('cocos#isLoaded'); + } + + @override + Future inBackground({required int cocosId}) async { + return await channel(cocosId).invokeMethod('cocos#inBackground'); + } + + @override + Future createCocosPlayer({required int cocosId}) async { + return await channel(cocosId).invokeMethod('cocos#createPlayer'); + } + + @override + Stream onCocosMessage({required int cocosId}) { + return _events(cocosId).whereType(); + } + + @override + Stream onCocosUnloaded({required int cocosId}) { + return _events(cocosId).whereType(); + } + + @override + Stream onCocosCreated({required int cocosId}) { + return _events(cocosId).whereType(); + } + + @override + Stream onCocosSceneLoaded({required int cocosId}) { + return _events(cocosId).whereType(); + } + + @override + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required TextDirection textDirection, + Set>? gestureRecognizers, + Map cocosOptions = const {}, + bool? useAndroidViewSurf, + bool? height, + bool? width, + bool? cocosWebSource, + String? cocosSrcUrl, + }) { + final String _viewType = 'plugin.xraph.com/cocos_view'; + + if (useAndroidViewSurf != null) useAndroidViewSurface = useAndroidViewSurf; + + final Map creationParams = cocosOptions; + + if (defaultTargetPlatform == TargetPlatform.windows) { + return WindowsCocosWidgetView(); + } + + if (defaultTargetPlatform == TargetPlatform.android) { + if (!useAndroidViewSurface) { + return AndroidView( + viewType: _viewType, + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + layoutDirection: TextDirection.ltr, + ); + } + + return PlatformViewLink( + viewType: _viewType, + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final controller = PlatformViewsService.initExpensiveAndroidView( + id: params.id, + viewType: _viewType, + layoutDirection: TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + + controller + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener(onPlatformViewCreated) + ..create(); + return controller; + }, + ); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + return UiKitView( + viewType: _viewType, + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + return Text( + '$defaultTargetPlatform is not yet supported by the cocos player plugin'); + } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + Map cocosOptions = const {}, + Set>? gestureRecognizers, + bool? useAndroidViewSurf, + String? cocosSrcUrl, + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + textDirection: TextDirection.ltr, + gestureRecognizers: gestureRecognizers, + cocosOptions: cocosOptions, + useAndroidViewSurf: useAndroidViewSurf, + cocosSrcUrl: cocosSrcUrl, + ); + } + + @override + Future postMessage({ + required int cocosId, + required String gameObject, + required String methodName, + required String message, + }) async { + await channel(cocosId).invokeMethod('cocos#postMessage', { + 'gameObject': gameObject, + 'methodName': methodName, + 'message': message, + }); + } + + @override + Future postJsonMessage({ + required int cocosId, + required String gameObject, + required String methodName, + required Map message, + }) async { + await channel(cocosId).invokeMethod('cocos#postMessage', { + 'gameObject': gameObject, + 'methodName': methodName, + 'message': json.encode(message), + }); + } + + @override + Future pausePlayer({required int cocosId}) async { + await channel(cocosId).invokeMethod('cocos#pausePlayer'); + } + + @override + Future resumePlayer({required int cocosId}) async { + await channel(cocosId).invokeMethod('cocos#resumePlayer'); + } + + @override + Future openInNativeProcess({required int cocosId}) async { + await channel(cocosId).invokeMethod('cocos#openInNativeProcess'); + } + + @override + Future unloadPlayer({required int cocosId}) async { + await channel(cocosId).invokeMethod('cocos#unloadPlayer'); + } + + @override + Future quitPlayer({required int cocosId}) async { + await channel(cocosId).invokeMethod('cocos#quitPlayer'); + } +} diff --git a/lib/src/io/io.dart b/lib/src/io/io.dart new file mode 100644 index 0000000..02a5dde --- /dev/null +++ b/lib/src/io/io.dart @@ -0,0 +1,4 @@ +export '../facade_controller.dart'; +export '../helpers/events.dart'; +export '../helpers/misc.dart'; +export '../helpers/types.dart'; diff --git a/lib/src/io/mobile_cocos_widget_controller.dart b/lib/src/io/mobile_cocos_widget_controller.dart new file mode 100644 index 0000000..f0b44a8 --- /dev/null +++ b/lib/src/io/mobile_cocos_widget_controller.dart @@ -0,0 +1,213 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../facade_controller.dart'; +import '../helpers/events.dart'; +import 'device_method.dart'; +import 'cocos_widget.dart'; +import 'cocos_widget_platform.dart'; + +class MobileCocosWidgetController extends CocosWidgetController { + final MobileCocosWidgetState _cocosWidgetState; + + /// The cocosId for this controller + final int cocosId; + + /// used for cancel the subscription + StreamSubscription? _onCocosMessageSub, + _onCocosSceneLoadedSub, + _onCocosUnloadedSub; + + MobileCocosWidgetController._(this._cocosWidgetState, + {required this.cocosId}) { + _connectStreams(cocosId); + } + + /// Initialize [CocosWidgetController] with [id] + /// Mainly for internal use when instantiating a [CocosWidgetController] passed + /// in [CocosWidget.onCocosCreated] callback. + static Future init( + int id, MobileCocosWidgetState cocosWidgetState) async { + await CocosWidgetPlatform.instance.init(id); + return MobileCocosWidgetController._( + cocosWidgetState, + cocosId: id, + ); + } + + @visibleForTesting + MethodChannel? get channel { + if (CocosWidgetPlatform.instance is MethodChannelCocosWidget) { + return (CocosWidgetPlatform.instance as MethodChannelCocosWidget) + .channel(cocosId); + } + return null; + } + + void _connectStreams(int cocosId) { + if (_cocosWidgetState.widget.onCocosMessage != null) { + _onCocosMessageSub = CocosWidgetPlatform.instance + .onCocosMessage(cocosId: cocosId) + .listen((CocosMessageEvent e) => + _cocosWidgetState.widget.onCocosMessage!(e.value)); + } + + if (_cocosWidgetState.widget.onCocosSceneLoaded != null) { + _onCocosSceneLoadedSub = CocosWidgetPlatform.instance + .onCocosSceneLoaded(cocosId: cocosId) + .listen((CocosSceneLoadedEvent e) => + _cocosWidgetState.widget.onCocosSceneLoaded!(e.value)); + } + + if (_cocosWidgetState.widget.onCocosUnloaded != null) { + _onCocosUnloadedSub = CocosWidgetPlatform.instance + .onCocosUnloaded(cocosId: cocosId) + .listen((_) => _cocosWidgetState.widget.onCocosUnloaded!()); + } + } + + /// Checks to see if cocos player is ready to be used + /// Returns `true` if cocos player is ready. + Future? isReady() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.isReady(cocosId: cocosId); + } + return null; + } + + /// Get the current pause state of the cocos player + /// Returns `true` if cocos player is paused. + Future? isPaused() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.isPaused(cocosId: cocosId); + } + return null; + } + + /// Get the current load state of the cocos player + /// Returns `true` if cocos player is loaded. + Future? isLoaded() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.isLoaded(cocosId: cocosId); + } + return null; + } + + /// Helper method to know if Cocos has been put in background mode (WIP) unstable + /// Returns `true` if cocos player is in background. + Future? inBackground() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.inBackground(cocosId: cocosId); + } + return null; + } + + /// Creates a cocos player if it's not already created. Please only call this if cocos is not ready, + /// or is in unloaded state. Use [isLoaded] to check. + /// Returns `true` if cocos player was created succesfully. + Future? create() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.createCocosPlayer(cocosId: cocosId); + } + return null; + } + + /// Post message to cocos from flutter. This method takes in a string [message]. + /// The [gameObject] must match the name of an actual cocos game object in a scene at runtime, and the [methodName], + /// must exist in a `MonoDevelop` `class` and also exposed as a method. [message] is an parameter taken by the method + /// + /// ```dart + /// postMessage("GameManager", "openScene", "ThirdScene") + /// ``` + Future? postMessage(String gameObject, methodName, message) { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.postMessage( + cocosId: cocosId, + gameObject: gameObject, + methodName: methodName, + message: message, + ); + } + return null; + } + + /// Post message to cocos from flutter. This method takes in a Json or map structure as the [message]. + /// The [gameObject] must match the name of an actual cocos game object in a scene at runtime, and the [methodName], + /// must exist in a `MonoDevelop` `class` and also exposed as a method. [message] is an parameter taken by the method + /// + /// ```dart + /// postJsonMessage("GameManager", "openScene", {"buildIndex": 3, "name": "ThirdScene"}) + /// ``` + Future? postJsonMessage( + String gameObject, String methodName, Map message) { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.postJsonMessage( + cocosId: cocosId, + gameObject: gameObject, + methodName: methodName, + message: message, + ); + } + return null; + } + + /// Pause the cocos in-game player with this method + Future? pause() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.pausePlayer(cocosId: cocosId); + } + return null; + } + + /// Resume the cocos in-game player with this method idf it is in a paused state + Future? resume() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.resumePlayer(cocosId: cocosId); + } + return null; + } + + /// Sometimes you want to open cocos in it's own process and openInNativeProcess does just that. + /// It works for Android and iOS is WIP + Future? openInNativeProcess() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.openInNativeProcess(cocosId: cocosId); + } + return null; + } + + /// Unloads cocos player from th current process (Works on Android only for now) + /// iOS is WIP. For more information please read [Cocos Docs](https://docs.cocos3d.com/2020.2/Documentation/Manual/CocosasaLibrary.html) + Future? unload() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.unloadPlayer(cocosId: cocosId); + } + return null; + } + + /// Quits cocos player. Note that this kills the current flutter process, thus quiting the app + Future? quit() { + if (!_cocosWidgetState.widget.enablePlaceholder) { + return CocosWidgetPlatform.instance.quitPlayer(cocosId: cocosId); + } + return null; + } + + /// cancel the subscriptions when dispose called + void _cancelSubscriptions() { + _onCocosMessageSub?.cancel(); + _onCocosSceneLoadedSub?.cancel(); + _onCocosUnloadedSub?.cancel(); + + _onCocosMessageSub = null; + _onCocosSceneLoadedSub = null; + _onCocosUnloadedSub = null; + } + + void dispose() { + _cancelSubscriptions(); + CocosWidgetPlatform.instance.dispose(cocosId: cocosId); + } +} diff --git a/lib/src/io/windows_cocos_widget_view.dart b/lib/src/io/windows_cocos_widget_view.dart new file mode 100644 index 0000000..c0f1ef4 --- /dev/null +++ b/lib/src/io/windows_cocos_widget_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class WindowsCocosWidgetView extends StatefulWidget { + const WindowsCocosWidgetView({super.key}); + + @override + State createState() => _WindowsCocosWidgetViewState(); +} + +class _WindowsCocosWidgetViewState extends State { + @override + Widget build(BuildContext context) { + // TODO: Rex Update windows view + return const MouseRegion( + child: Texture( + textureId: 0, + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 6fbbef5..a1e3972 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,13 +4,14 @@ version: 0.0.1 homepage: environment: - sdk: ^3.5.4 + sdk: ^3.4.4 flutter: '>=3.3.0' dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.0.2 + stream_transform: ^2.0.0 dev_dependencies: flutter_test: diff --git a/test/flutter_cocos_view_method_channel_test.dart b/test/flutter_cocos_view_method_channel_test.dart deleted file mode 100644 index cc1deaf..0000000 --- a/test/flutter_cocos_view_method_channel_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_cocos_view/flutter_cocos_view_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelFlutterCocosView platform = MethodChannelFlutterCocosView(); - const MethodChannel channel = MethodChannel('flutter_cocos_view'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return '42'; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); - }); - - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); - }); -} diff --git a/test/flutter_cocos_view_test.dart b/test/flutter_cocos_view_test.dart deleted file mode 100644 index 2edfd2b..0000000 --- a/test/flutter_cocos_view_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_cocos_view/flutter_cocos_view.dart'; -import 'package:flutter_cocos_view/flutter_cocos_view_platform_interface.dart'; -import 'package:flutter_cocos_view/flutter_cocos_view_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockFlutterCocosViewPlatform - with MockPlatformInterfaceMixin - implements FlutterCocosViewPlatform { - - @override - Future getPlatformVersion() => Future.value('42'); -} - -void main() { - final FlutterCocosViewPlatform initialPlatform = FlutterCocosViewPlatform.instance; - - test('$MethodChannelFlutterCocosView is the default instance', () { - expect(initialPlatform, isInstanceOf()); - }); - - test('getPlatformVersion', () async { - FlutterCocosView flutterCocosViewPlugin = FlutterCocosView(); - MockFlutterCocosViewPlatform fakePlatform = MockFlutterCocosViewPlatform(); - FlutterCocosViewPlatform.instance = fakePlatform; - - expect(await flutterCocosViewPlugin.getPlatformVersion(), '42'); - }); -}