diff --git a/.gitignore b/.gitignore index 09808ae..95f0716 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -26,6 +28,7 @@ migrate_working_dir/ /pubspec.lock **/doc/api/ .dart_tool/ -.packages +.flutter-plugins +.flutter-plugins-dependencies build/ -.fvm +.vscode/ \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..5bcd91a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,3 @@ include: package:flutter_lints/flutter.yaml - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/android/build.gradle b/android/build.gradle index 930ffed..662b929 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,53 +1,77 @@ -group 'io.github.sceneview.sceneview_flutter' -version '1.0-SNAPSHOT' +group = "io.github.sceneview.sceneview_flutter" +version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = '1.9.0' + ext.kotlin_version = "1.8.22" repositories { - mavenLocal() google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("com.android.tools.build:gradle:8.1.4") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } allprojects { repositories { - mavenLocal() google() mavenCentral() } } -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: "com.android.library" +apply plugin: "kotlin-android" android { - compileSdk 34 - namespace 'io.github.sceneview.sceneview_flutter' + namespace = "io.github.sceneview.sceneview_flutter" + + compileSdk = 35 + compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = JavaVersion.VERSION_11 } sourceSets { - main.java.srcDirs += 'src/main/kotlin' + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" } defaultConfig { - minSdkVersion 28 + minSdk = 28 + consumerProguardFiles 'proguard-rules.pro' + } + + dependencies { + implementation("io.github.sceneview:arsceneview:2.2.1") + + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.1.1") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen { false } + showStandardStreams = true + } + } + } + buildFeatures { + viewBinding true } } dependencies { - implementation 'io.github.sceneview:arsceneview:1.2.3' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' -} \ No newline at end of file + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' +} diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 8049c68..0000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew deleted file mode 100644 index 1b6c787..0000000 --- a/android/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 107acd3..0000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000..b29c697 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,13 @@ +-keep class com.google.android.gms.location.** { *; } +-keep class com.google.android.gms.common.** { *; } +-keep class com.google.android.gms.maps.** { *; } +-keep class com.google.ar.core.** { *; } +-keep class com.google.ar.sceneform.** { *; } + +-keep class com.google.android.gms.location.FusedLocationProviderClient { *; } + +-keep class com.google.ar.core.Config { *; } +-keep class com.google.ar.core.Session { *; } + +-keepnames class com.google.ar.** { *; } +-dontwarn com.google.ar.** diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index f856e75..27096df 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,17 @@ + + + + + + + + + diff --git a/android/src/main/kotlin/io/github/sceneview/sceneview_flutter/SceneViewWrapper.kt b/android/src/main/kotlin/io/github/sceneview/sceneview_flutter/SceneViewWrapper.kt index d10935e..6b767db 100644 --- a/android/src/main/kotlin/io/github/sceneview/sceneview_flutter/SceneViewWrapper.kt +++ b/android/src/main/kotlin/io/github/sceneview/sceneview_flutter/SceneViewWrapper.kt @@ -7,7 +7,9 @@ import android.view.View import android.widget.FrameLayout import androidx.lifecycle.Lifecycle import com.google.ar.core.Config +import com.google.ar.core.Session import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -27,92 +29,119 @@ class SceneViewWrapper( id: Int, ) : PlatformView, MethodCallHandler { private val TAG = "SceneViewWrapper" - private var sceneView: ARSceneView - private val _mainScope = CoroutineScope(Dispatchers.Main) - private val _channel = MethodChannel(messenger, "scene_view_$id") + private val methodChannelIdentifier = "sceneview_methods"; + private val eventChannelIdentifier = "sceneview_events"; - override fun getView(): View { - Log.i(TAG, "getView:") - return sceneView - } + private var sceneView: ARSceneView? = null + private val _mainScope = CoroutineScope(Dispatchers.Main) + private val _methodChannel = MethodChannel(messenger, "$methodChannelIdentifier-$id") + private val _eventChannel = EventChannel(messenger, "$eventChannelIdentifier-$id") + private var eventSink: EventChannel.EventSink? = null - override fun dispose() { - Log.i(TAG, "dispose") - } + private val container: FrameLayout = FrameLayout(context) + private var disposed: Boolean = false init { Log.i(TAG, "init") - sceneView = ARSceneView(context, sharedLifecycle = lifecycle) - sceneView.apply { - configureSession { session, config -> - config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR - config.depthMode = when (session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)) { - true -> Config.DepthMode.AUTOMATIC - else -> Config.DepthMode.DISABLED - } - config.instantPlacementMode = Config.InstantPlacementMode.DISABLED - } - onSessionResumed = { session -> + sceneView = ARSceneView( + context, + sharedLifecycle = lifecycle, + sessionConfiguration = ::configureSession, + onSessionCreated = { session -> Log.i(TAG, "onSessionCreated") - } + }, + onSessionResumed = { session -> + Log.i(TAG, "onSessionResumed") + val event = mapOf("type" to "onSessionResumed", "data" to true) + eventSink?.success(event) + }, onSessionFailed = { exception -> Log.e(TAG, "onSessionFailed : $exception") - } - onSessionCreated = { session -> - Log.i(TAG, "onSessionCreated") - } + }, onTrackingFailureChanged = { reason -> Log.i(TAG, "onTrackingFailureChanged: $reason"); } + ).apply { + keepScreenOn = true + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + } + initializeChannels() + disposed = false + container.addView(sceneView) + } + + override fun dispose() { + if (disposed) return + + sceneView?.session?.close() + sceneView = null + container.removeAllViews() + val emptyView = View(container.context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) } - sceneView.layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) - sceneView.keepScreenOn = true - _channel.setMethodCallHandler(this) + container.addView(emptyView) + disposed = true + Log.i(TAG, "dispose") + } + + override fun getView(): View { + return container + } + + private fun configureSession(session: Session, config: Config) { + config.focusMode = Config.FocusMode.AUTO + config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE + config.planeFindingMode = Config.PlaneFindingMode.DISABLED + config.lightEstimationMode = Config.LightEstimationMode.DISABLED + config.textureUpdateMode = Config.TextureUpdateMode.BIND_TO_TEXTURE_EXTERNAL_OES + + config.depthMode = Config.DepthMode.DISABLED + config.semanticMode = Config.SemanticMode.DISABLED + config.geospatialMode = Config.GeospatialMode.DISABLED + config.cloudAnchorMode = Config.CloudAnchorMode.DISABLED + config.augmentedFaceMode = Config.AugmentedFaceMode.DISABLED + config.imageStabilizationMode = Config.ImageStabilizationMode.OFF + config.instantPlacementMode = Config.InstantPlacementMode.DISABLED + config.streetscapeGeometryMode = Config.StreetscapeGeometryMode.DISABLED + + Log.i(TAG, "Session Configured") + } + + private fun initializeChannels() { + _methodChannel.setMethodCallHandler(this) + _eventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + Log.i(TAG, "Events initialized") + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + }) + Log.i(TAG, "Channels initialized") } private suspend fun addNode(flutterNode: FlutterSceneViewNode) { val node = buildNode(flutterNode) ?: return - sceneView.addChildNode(node) - //AnchorNode(sceneView.engine, anchor).apply {} - Log.d("Done", "Done") + sceneView?.addChildNode(node) + Log.d(TAG, "Model placed") } private suspend fun buildNode(flutterNode: FlutterSceneViewNode): ModelNode? { var model: ModelInstance? = null - - /* - AnchorNode(sceneView.engine, anchor) - .apply { - isEditable = true - //isLoading = true - sceneView.modelLoader.loadModelInstance( - "https://sceneview.github.io/assets/models/DamagedHelmet.glb" - )?.let { modelInstance -> - addChildNode( - ModelNode( - modelInstance = modelInstance, - // Scale to fit in a 0.5 meters cube - scaleToUnits = 0.5f, - // Bottom origin instead of center so the model base is on floor - centerOrigin = Position(y = -0.5f) - ).apply { - isEditable = true - } - ) - } - //isLoading = false - anchorNode = this - } - */ when (flutterNode) { is FlutterReferenceNode -> { val fileLocation = Utils.getFlutterAssetKey(activity, flutterNode.fileLocation) - Log.d("SceneViewWrapper", fileLocation) + Log.d(TAG, fileLocation) model = - sceneView.modelLoader.loadModelInstance(fileLocation) + sceneView?.modelLoader?.loadModelInstance(fileLocation) } } if (model != null) { @@ -120,12 +149,7 @@ class SceneViewWrapper( transform( position = flutterNode.position, rotation = flutterNode.rotation, - //scale = flutterNode.scale, ) - //scaleToUnitsCube(flutterNode.scaleUnits) - // TODO: Fix centerOrigin - // centerOrigin(Position(x=-1.0f, y=-1.0f)) - //playAnimation() } return modelNode } @@ -135,7 +159,15 @@ class SceneViewWrapper( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "init" -> { - result.success(null) + result.success(true) + } + + "dispose" -> { + _mainScope.launch { + dispose() + result.success(true) + _methodChannel.setMethodCallHandler(null) + } } "addNode" -> { @@ -143,9 +175,8 @@ class SceneViewWrapper( val flutterNode = FlutterSceneViewNode.from(call.arguments as Map) _mainScope.launch { addNode(flutterNode) + result.success(true) } - result.success(null) - return } else -> result.notImplemented() diff --git a/example/.gitignore b/example/.gitignore index d2406ad..79c113f 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -27,7 +29,6 @@ migrate_working_dir/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.packages .pub-cache/ .pub/ /build/ @@ -42,5 +43,3 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release - -.fvm diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4d..0439848 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -13,8 +13,7 @@ linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. + # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code @@ -24,6 +23,5 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore index 6f56801..55afd91 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -7,7 +7,7 @@ gradle-wrapper.jar GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 6080d95..f66390d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,73 +1,44 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdk 34 - ndkVersion flutter.ndkVersion - namespace 'io.github.sceneview.sceneview_flutter_example' + namespace = "io.github.sceneview.sceneview_flutter_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + jvmTarget = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.github.sceneview.sceneview_flutter_example" + applicationId = "io.github.sceneview.sceneview_flutter_example" // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 28 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 28 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + source = "../.." } diff --git a/example/android/build.gradle b/example/android/build.gradle index 0f650bb..d2ffbff 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,31 +1,16 @@ -buildscript { - ext.kotlin_version = '1.9.0' - repositories { - mavenLocal() - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { - mavenLocal() google() mavenCentral() } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 94adc3a..2597170 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3b919e9..7bb2df6 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Oct 24 11:34:16 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bc..b9e43bd 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" diff --git a/example/lib/main.dart b/example/lib/main.dart index d3ee098..3b5327b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,43 +1,72 @@ import 'package:flutter/material.dart'; -import 'dart:async'; - -import 'package:sceneview_flutter/sceneview_flutter.dart'; -import 'package:sceneview_flutter/sceneview_node.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:sceneview_flutter_example/sceneview_screen.dart'; void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { const MyApp({super.key}); @override - State createState() => _MyAppState(); + Widget build(BuildContext context) { + return MaterialApp( + home: HomeScreen(), + ); + } } -class _MyAppState extends State { +class HomeScreen extends StatelessWidget { + HomeScreen({super.key}) { + checkCameraPermission(); + } + @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Scene view example app'), - ), - body: Stack( - children: [ - SceneView( - onViewCreated: (controller) { - print('flutter: onViewCreated'); - controller.addNode(SceneViewNode( - fileLocation: 'assets/models/MaterialSuite.glb', - position: KotlinFloat3(z: -1.0), - rotation: KotlinFloat3(x: 15), - )); - }, - ), - ], + return Scaffold( + appBar: AppBar( + title: const Text('Scene view example app'), + ), + body: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Center( + child: ElevatedButton( + onPressed: () => _openSceneViewScreen(context), + child: Text("Open SceneView Screen"), + ), ), ), ); } + + void _openSceneViewScreen(BuildContext context) { + Navigator.push( + context, + PageRouteBuilder( + settings: RouteSettings(name: "sceneview_screen"), + pageBuilder: (context, animation, secondaryAnimation) { + return SceneViewScreen(); + }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ), + ); + } + + void checkCameraPermission() async { + var cameraStatus = await Permission.camera.status; + if (!cameraStatus.isGranted) { + await Permission.camera.request(); + } + var geolocationStatus = await Permission.location.status; + if (!geolocationStatus.isGranted) { + await Permission.location.request(); + } + } } diff --git a/example/lib/sceneview_screen.dart b/example/lib/sceneview_screen.dart new file mode 100644 index 0000000..51c95c2 --- /dev/null +++ b/example/lib/sceneview_screen.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:sceneview_flutter/sceneview_flutter.dart'; +import 'package:sceneview_flutter/sceneview_node.dart'; + +class SceneViewScreen extends StatefulWidget { + const SceneViewScreen({super.key}); + + @override + State createState() => _SceneViewScreenState(); +} + +class _SceneViewScreenState extends State { + SceneViewController? _controller; + StreamSubscription? _onSessionResumed; + + @override + void dispose() { + _onSessionResumed?.cancel(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + _controller?.dispose().then(_closeView); + }, + child: Scaffold( + appBar: AppBar( + title: const Text('AR Scene view'), + ), + body: Stack( + children: [ + SceneView( + onViewCreated: (controller) { + _controller = controller; + _onSessionResumed = _controller?.on(SceneViewEvent.onSessionResumed).listen((data) { + debugPrint("Flutter: onSessionResumed $data"); + }); + }, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Center( + child: ElevatedButton( + onPressed: _placeModel, + child: Text("Place model"), + ), + ), + ) + ], + ), + ), + ); + } + + void _closeView(bool dispose) { + if (dispose) { + if (mounted) { + Navigator.pop(context); + } + } + } + + void _placeModel() { + _controller?.addNode(SceneViewNode( + fileLocation: 'assets/models/MaterialSuite.glb', + position: KotlinFloat3(z: -1.0), + rotation: KotlinFloat3(x: 15), + )); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index bd05899..01435de 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.19.0" fake_async: dependency: transitive description: @@ -49,48 +49,172 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.dev" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.15.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + url: "https://pub.dev" + source: hosted + version: "9.4.6" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -99,6 +223,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" sceneview_flutter: dependency: "direct main" description: @@ -110,7 +242,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -123,26 +255,34 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "0.3.1" term_glyph: dependency: transitive description: @@ -155,10 +295,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.3" vector_math: dependency: transitive description: @@ -167,14 +307,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.dev" + source: hosted + version: "14.3.0" web: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "3.0.4" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=2.5.0" + dart: ">=3.6.1 <4.0.0" + flutter: ">=3.24.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0acb0f0..6f57539 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,21 +1,26 @@ name: sceneview_flutter_example description: Demonstrates how to use the sceneview_flutter plugin. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=2.18.5 <3.0.0' + sdk: ^3.6.1 dependencies: flutter: sdk: flutter + permission_handler: ^11.4.0 sceneview_flutter: path: ../ dev_dependencies: + integration_test: + sdk: flutter flutter_test: sdk: flutter + flutter_lints: ^5.0.0 flutter: + uses-material-design: true assets: - assets/models/MaterialSuite.glb diff --git a/lib/scene_view.dart b/lib/scene_view.dart index cdc4931..e0d3d5b 100644 --- a/lib/scene_view.dart +++ b/lib/scene_view.dart @@ -8,25 +8,24 @@ import 'package:flutter/services.dart'; import 'package:sceneview_flutter/sceneview_controller.dart'; class SceneView extends StatefulWidget { + final void Function(SceneViewController)? onViewCreated; + const SceneView({ super.key, this.onViewCreated, }); - final Function(SceneViewController)? onViewCreated; - @override State createState() => _SceneViewState(); } class _SceneViewState extends State { - final Completer _controller = - Completer(); + final Completer _controller = Completer(); @override Widget build(BuildContext context) { // This is used in the platform side to register the view. - const String viewType = 'SceneView'; + const String viewType = "SceneView"; // Pass parameters to the platform side. const Map creationParams = {}; @@ -51,9 +50,7 @@ class _SceneViewState extends State { }, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((id) { - onPlatformViewCreated(id); - }); + ..addOnPlatformViewCreatedListener((id) => onPlatformViewCreated(id)); }, ); } diff --git a/lib/sceneview_controller.dart b/lib/sceneview_controller.dart index 1cb78c8..83b4cb9 100644 --- a/lib/sceneview_controller.dart +++ b/lib/sceneview_controller.dart @@ -1,27 +1,30 @@ -import 'package:flutter/services.dart'; +import 'dart:async'; + +import 'package:sceneview_flutter/sceneview_flutter_events.dart'; import 'package:sceneview_flutter/sceneview_flutter_platform_interface.dart'; import 'package:sceneview_flutter/sceneview_node.dart'; +/// When creating new functions call SceneviewFlutterPlatform.instance.invokeMethod() class SceneViewController { - SceneViewController._({ - required this.sceneId, - }); - final int sceneId; - static Future init( - int sceneId, - ) async { + SceneViewController._(this.sceneId); + + static Future init(int sceneId) async { await SceneviewFlutterPlatform.instance.init(sceneId); - return SceneViewController._(sceneId: sceneId); + return SceneViewController._(sceneId); } - void addNode(SceneViewNode node) { - SceneviewFlutterPlatform.instance.addNode(node); + Future dispose() async { + return await SceneviewFlutterPlatform.instance.dispose(); } - void dispose() { - SceneviewFlutterPlatform.instance.dispose(sceneId); + void addNode(SceneViewNode node) { + SceneviewFlutterPlatform.instance.invokeMethod("addNode", node.toMap()); } + /// All events available in native code through the eventChannel must be declared in SceneViewEvent + Stream on(SceneViewEvent event) { + return SceneviewFlutterPlatform.instance.on(event); + } } diff --git a/lib/sceneview_flutter.dart b/lib/sceneview_flutter.dart index 8266837..27bfb11 100644 --- a/lib/sceneview_flutter.dart +++ b/lib/sceneview_flutter.dart @@ -1,6 +1,5 @@ -import 'sceneview_flutter_platform_interface.dart'; - -export 'sceneview_controller.dart'; export 'scene_view.dart'; +export 'sceneview_controller.dart'; +export 'sceneview_flutter_events.dart'; class SceneviewFlutter {} diff --git a/lib/sceneview_flutter_events.dart b/lib/sceneview_flutter_events.dart new file mode 100644 index 0000000..a2a8fa7 --- /dev/null +++ b/lib/sceneview_flutter_events.dart @@ -0,0 +1,32 @@ +/// Define here all the event types +enum SceneViewEvent { + onSessionResumed, +} + +class SceneViewEventData { + final SceneViewEvent event; + final dynamic data; + + SceneViewEventData({ + required this.event, + required this.data, + }); +} + +bool isValidSceneViewEvent(String value) { + for (var type in SceneViewEvent.values) { + if (type.name.toLowerCase() == value.toLowerCase()) { + return true; + } + } + return false; +} + +SceneViewEvent? stringToSceneViewEvent(String value) { + for (var type in SceneViewEvent.values) { + if (type.name.toLowerCase() == value.toLowerCase()) { + return type; + } + } + return null; +} diff --git a/lib/sceneview_flutter_method_channel.dart b/lib/sceneview_flutter_method_channel.dart index f4219c8..da55eac 100644 --- a/lib/sceneview_flutter_method_channel.dart +++ b/lib/sceneview_flutter_method_channel.dart @@ -1,11 +1,16 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:sceneview_flutter/sceneview_node.dart'; +import 'package:sceneview_flutter/sceneview_flutter_events.dart'; import 'sceneview_flutter_platform_interface.dart'; /// An implementation of [SceneviewFlutterPlatform] that uses method channels. class MethodChannelSceneViewFlutter extends SceneviewFlutterPlatform { + static const String methodChannelIdentifier = "sceneview_methods"; + static const String eventChannelIdentifier = "sceneview_events"; + /// Registers the Android implementation of SceneviewFlutterPlatform. static void registerWith() { SceneviewFlutterPlatform.instance = MethodChannelSceneViewFlutter(); @@ -13,33 +18,55 @@ class MethodChannelSceneViewFlutter extends SceneviewFlutterPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - final methodChannel = const MethodChannel('sceneview_flutter'); + final methodChannel = const MethodChannel(methodChannelIdentifier); + final eventChannel = const EventChannel(eventChannelIdentifier); - MethodChannel? _channel; + MethodChannel? _methodChannel; + EventChannel? _eventChannel; + StreamSubscription? _eventChannelSubscription; + final StreamController _eventController = StreamController.broadcast(); + Stream get eventStream => _eventController.stream; - MethodChannel ensureChannelInitialized(int sceneId) { - MethodChannel? channel = _channel; - if (channel == null) { - channel = MethodChannel('scene_view_$sceneId'); - channel.setMethodCallHandler( - (MethodCall call) => _handleMethodCall(call, sceneId)); - _channel = channel; - } - return channel; + @override + Future init(int sceneId) async { + _ensureMethodChannelInitialized(sceneId); + _ensureEventChannelInitialized(sceneId); + return invokeMethod("init"); } + @override + Future dispose() async { + final result = await invokeMethod("dispose") ?? false; + if (result) { + _methodChannel?.setMethodCallHandler(null); + _methodChannel = null; + _eventChannel = null; + await _eventChannelSubscription?.cancel(); + _eventChannelSubscription = null; + _eventController.close(); + } + return result; + } @override - Future init(int sceneId) async { - final channel = ensureChannelInitialized(sceneId); - return channel.invokeMethod('init'); + Future invokeMethod(String method, [dynamic arguments]) async { + return await _methodChannel?.invokeMethod(method, arguments); } @override - void addNode(SceneViewNode node) { - _channel?.invokeMethod('addNode', node.toMap()); + Stream on(SceneViewEvent event) => eventStream.where((e) => e.event == event).map((e) => e.data as T); + + MethodChannel _ensureMethodChannelInitialized(int sceneId) { + MethodChannel? channel = _methodChannel; + if (channel == null) { + channel = MethodChannel("$methodChannelIdentifier-$sceneId"); + channel.setMethodCallHandler((MethodCall call) => _handleMethodCall(call, sceneId)); + _methodChannel = channel; + } + return channel; } + /// Add here flutter functions you want to call from native Future _handleMethodCall(MethodCall call, int mapId) async { switch (call.method) { default: @@ -47,6 +74,42 @@ class MethodChannelSceneViewFlutter extends SceneviewFlutterPlatform { } } - @override - void dispose(int sceneId) {} + EventChannel _ensureEventChannelInitialized(int sceneId) { + EventChannel? channel = _eventChannel; + if (channel == null) { + channel = EventChannel("$eventChannelIdentifier-$sceneId"); + _eventChannelSubscription = channel.receiveBroadcastStream().listen( + _handleEventCall, + onError: _handleEventError, + ); + _eventChannel = channel; + } + return channel; + } + + void _handleEventCall(dynamic map) { + if (map is! Map) { + _handleEventError("Event is not a Map"); + return; + } + + final String event = map["type"] ?? ""; + final dynamic data = map["data"]; + + if (!isValidSceneViewEvent(event) || event.isEmpty) { + _handleEventError("Event type is empty or not valid"); + return; + } + + _eventController.add( + SceneViewEventData( + event: stringToSceneViewEvent(event)!, + data: data, + ), + ); + } + + void _handleEventError(dynamic e) { + debugPrint("Flutter: Error in EventChannel: $e"); + } } diff --git a/lib/sceneview_flutter_platform_interface.dart b/lib/sceneview_flutter_platform_interface.dart index fdbfa19..7cc67b8 100644 --- a/lib/sceneview_flutter_platform_interface.dart +++ b/lib/sceneview_flutter_platform_interface.dart @@ -1,5 +1,7 @@ +import 'dart:async'; + import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:sceneview_flutter/sceneview_node.dart'; +import 'package:sceneview_flutter/sceneview_flutter_events.dart'; import 'sceneview_flutter_method_channel.dart'; @@ -25,14 +27,18 @@ abstract class SceneviewFlutterPlatform extends PlatformInterface { } Future init(int sceneId) { - throw UnimplementedError('init() has not been implemented.'); + throw UnimplementedError("init() has not been implemented."); + } + + Future dispose() async { + throw UnimplementedError("dispose() has not been implemented."); } - void addNode(SceneViewNode node) { - throw UnimplementedError('addNode() has not been implemented.'); + Future invokeMethod(String method, [dynamic arguments]) { + throw UnimplementedError("invokeMethod() has not been implemented."); } - void dispose(int sceneId){ - throw UnimplementedError('dispose() has not been implemented.'); + Stream on(SceneViewEvent event) { + throw UnimplementedError("on() has not been implemented."); } } diff --git a/pubspec.yaml b/pubspec.yaml index 2e987b1..09de7fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,25 +1,25 @@ name: sceneview_flutter description: A demonstration of how to integrate SceneView in flutter. -version: 0.0.1 +version: 0.1.0 homepage: https://github.com/SceneView environment: - sdk: '>=2.18.5 <3.0.0' - flutter: ">=2.5.0" + sdk: ^3.6.1 + flutter: ">=3.3.0" dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.1.3 + plugin_platform_interface: ^2.0.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^5.0.0 flutter: plugin: platforms: android: package: io.github.sceneview.sceneview_flutter - pluginClass: SceneviewFlutterPlugin \ No newline at end of file + pluginClass: SceneviewFlutterPlugin