diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml new file mode 100644 index 0000000..ec3e3e1 --- /dev/null +++ b/.github/FUNDING.yaml @@ -0,0 +1 @@ +github: tlserver diff --git a/.github/ISSUE_TEMPLATE/Bug.yaml b/.github/ISSUE_TEMPLATE/Bug.yaml new file mode 100644 index 0000000..380830f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug.yaml @@ -0,0 +1,118 @@ +name: Bug Report +description: File a report if you've encountered a bug with FlutterRotationSensor. +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + # 🐛 Bug Report + + Thank you for using FlutterRotationSensor! Before you submit your bug report, please: + + - Review the [documentation](https://pub.dev/documentation/flutter_map_location_marker/latest/). + - Search for similar issues in both [open and closed tickets](https://github.com/tlserver/flutter_rotation_sensor/issues?q=is%3Aissue). + - Discuss non-bug-related questions on [GitHub discussions](https://github.com/tlserver/flutter_rotation_sensor/discussions) or [Stack Overflow](https://stackoverflow.com/). + + If you're certain the issue you're experiencing is a bug, please provide as much detail as possible in this report so we can help you resolve it. + + **Note:** Bug reports not adhering to this template may be closed for incomplete information. + + - type: checkboxes + id: self-checks + attributes: + label: Preliminary Bug Check + description: Ensure you've taken these steps before submitting a bug. + options: + - label: I've searched the issue tracker for similar bug reports. + required: true + - label: I've checked Google and Stack Overflow for solutions. + required: true + - label: I've read the plugin's documentation. + required: true + - label: I'm using the latest plugin version and all dependencies are updated with `flutter pub upgrade`. + required: true + - label: I've executed `flutter clean`. + required: true + - label: I've tried running the example project. + required: true + + - type: input + id: version + attributes: + label: Plugin Version + description: What version of the plugin are you using? + placeholder: e.g., 1.0.0 + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? Please include as much detail as possible. + validations: + required: true + + - type: textarea + id: code + attributes: + label: Code Sample + render: dart + description: | + Provide a minimal code sample or a link to a gist that reproduces the error. Ideally, share a main.dart file that we can run to see the issue. + validations: + required: false + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Detail the exact steps to reproduce the bug. + validations: + required: true + + - type: input + id: platform + attributes: + label: Platform Details + description: Which platform and version did the issue occur on (e.g., Android, iOS)? + placeholder: e.g., Android 12, iOS 14 + validations: + required: true + + - type: input + id: sdk + attributes: + label: Flutter SDK Version + description: Provide the version of the Flutter SDK you are using. + placeholder: e.g., 3.0.0 + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs + render: shell + description: | + Attach the full output of `flutter run --verbose`. If there's an exception, ensure the log includes enough detail to diagnose the issue. + validations: + required: true + + - type: textarea + id: doctor + attributes: + label: Flutter Doctor Output + render: shell + description: What is the output of `flutter doctor -v`? This provides us with your development environment details. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml new file mode 100644 index 0000000..cdfa5a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml @@ -0,0 +1,77 @@ +name: Feature Request +description: Suggest an idea or enhancement for the FlutterRotationSensor plugin. +title: "[Feature Request]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + # ✨ Feature Request + + Thank you for taking the time to suggest a feature for FlutterRotationSensor! + + Before submitting your feature request, please consider the following: + - Check if a similar feature request [has already been submitted](https://github.com/tlserver/flutter_rotation_sensor/issues?q=is%3Aissue+label%3Aenhancement). + - Review the [documentation](https://pub.dev/documentation/flutter_map_location_marker/latest/) to ensure your feature doesn't already exist. + + If you have a specific idea in mind, fill out the template below. Detailed proposals have a higher chance of being considered. + + - type: input + id: feature-summary + attributes: + label: Feature Summary + description: A short, descriptive title for your feature request. + placeholder: e.g., Support for XYZ sensor data + validations: + required: true + + - type: textarea + id: feature-description + attributes: + label: Detailed Description + description: Provide a detailed description of your feature request. What problem does it solve or functionality does it add? + validations: + required: true + + - type: textarea + id: potential-benefits + attributes: + label: Potential Benefits + description: Explain the benefits this feature would provide to users or developers. + validations: + required: true + + - type: textarea + id: possible-solutions + attributes: + label: Possible Solutions + description: If you have ideas on how to implement this feature, please share them here. + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Describe any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Add any other context, links, or screenshots about the feature request here. + validations: + required: false + + - type: checkboxes + id: additional-checks + attributes: + label: Preliminary Checks + description: Ensure you've considered what's necessary before we proceed with this feature request. + options: + - label: I've checked if this feature request does not already exist. + required: true + - label: I've considered and outlined any potential impact on existing functionality. + required: true diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..b3daaaf --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,34 @@ +version: 2 +updates: + + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "weekly" + rebase-strategy: "disabled" + commit-message: + prefix: "chore" + + - package-ecosystem: "pub" + directory: "/example" + schedule: + interval: "weekly" + rebase-strategy: "disabled" + commit-message: + prefix: "chore" + + - package-ecosystem: "gradle" + directory: "/android" + schedule: + interval: "monthly" + rebase-strategy: "disabled" + commit-message: + prefix: "chore" + + - package-ecosystem: "gradle" + directory: "/example/android" + schedule: + interval: "monthly" + rebase-strategy: "disabled" + commit-message: + prefix: "chore" diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..6f85eaf --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,21 @@ +name: 'Stale' +on: + schedule: + - cron: '16 10 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ github.token }} + stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.' + stale-pr-message: 'This PR is stale because it has been open for 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.' + close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' + close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.' + close-issue-reason: 'not_planned' + days-before-issue-stale: 30 + days-before-issue-close: 7 + days-before-pr-stale: 60 + days-before-pr-close: 14 diff --git a/.gitignore b/.gitignore index 330dc47..a553464 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.fvm/flutter_sdk .packages .pub-cache/ .pub/ @@ -17,7 +16,7 @@ lib/generated_plugin_registrant.dart # For library packages, don’t commit the pubspec.lock file. # Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies. # See https://dart.dev/guides/libraries/private-files#pubspeclock -pubspec.lock +/pubspec.lock # Android related **/android/**/gradle-wrapper.jar @@ -178,4 +177,7 @@ fabric.properties # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij .idea/**/azureSettings.xml -# End of https://www.toptal.com/developers/gitignore/api/jetbrains,flutter \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/jetbrains,flutter + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b83df14 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +/caches/deviceStreaming.xml +# Default ignored files +/libraries/ +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..6177abb --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +FlutterRotationSensor \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..6bbe2ae --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..4e1fcc7 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5db477d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..caf8a4b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/dart_format.xml b/.idea/runConfigurations/dart_format.xml new file mode 100644 index 0000000..84d1ae4 --- /dev/null +++ b/.idea/runConfigurations/dart_format.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/example.xml b/.idea/runConfigurations/example.xml new file mode 100644 index 0000000..5650683 --- /dev/null +++ b/.idea/runConfigurations/example.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/flutter_analyze.xml b/.idea/runConfigurations/flutter_analyze.xml new file mode 100644 index 0000000..3933bdd --- /dev/null +++ b/.idea/runConfigurations/flutter_analyze.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/flutter_pub_publish.xml b/.idea/runConfigurations/flutter_pub_publish.xml new file mode 100644 index 0000000..fd7f521 --- /dev/null +++ b/.idea/runConfigurations/flutter_pub_publish.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/pana.xml b/.idea/runConfigurations/pana.xml new file mode 100644 index 0000000..7133d80 --- /dev/null +++ b/.idea/runConfigurations/pana.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/test_all.xml b/.idea/runConfigurations/test_all.xml new file mode 100644 index 0000000..c6602dc --- /dev/null +++ b/.idea/runConfigurations/test_all.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..5fc0921 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..cff4c42 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: android + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: ios + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1a06427 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +## [0.2.0] (2025-10-21) + +**Features** + +* Added an indicator for platform support + +**Bug Fixes** + +* Fixed readme. (#62) + +## [0.1.1] (2024-10-22) + +**Bug Fixes** + +* Fixed an issue during the build process on iOS. + +## [0.1.0] (2024-07-25) + +**Features** + +* Initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68950d0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) 2024 tlserver6y.net + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..22749ce --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# flutter_rotation_sensor + +[![pub package](https://img.shields.io/pub/v/flutter_rotation_sensor)](https://pub.dev/packages/flutter_rotation_sensor) +[![github tag](https://img.shields.io/github/v/tag/tlserver/flutter_rotation_sensor?include_prereleases&sort=semver)](https://github.com/tlserver/flutter_rotation_sensor) +[![license](https://img.shields.io/github/license/tlserver/flutter_rotation_sensor)](https://github.com/tlserver/flutter_rotation_sensor/blob/master/LICENSE) + +The `flutter_rotation_sensor` plugin provides easy access to the device's physical orientation in +three distinct representations: rotation matrix, quaternion, and Euler angles (azimuth, pitch, +roll). This is ideal for applications requiring precise tracking of the device's movement or +orientation in space, such as augmented reality, gaming, navigation, and more. + +## Features + +- **Real-time Rotation Data**: Access to real-time rotation data. +- **Multiple Formats Supported**: Provides rotation matrix, quaternion, and Euler angles (azimuth, + pitch, roll). +- **Customizable Update Intervals**: Set custom intervals for sensor data retrieval. +- **Coordinate System Remapping**: Supports orientation coordinate system remapping. + +## Installation + +To add `flutter_rotation_sensor` to your project, follow these steps: + +1. Add `flutter_rotation_sensor` as a dependency in your `pubspec.yaml` file: + ```yaml + dependencies: + flutter_rotation_sensor: ^latest_version + ``` + +2. Install the plugin by running: + ```sh + flutter pub get + ``` + +3. Import the plugin in your Dart code: + ```dart + import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; + ``` + +## Usage + +To start receiving orientation data from the sensors, simply use the stream in a `StreamBuilder`: + +```dart +@override +Widget build(BuildContext context) { + return StreamBuilder( + stream: RotationSensor.orientationStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + print(data.quaternion); + print(data.rotationMatrix); + print(data.eulerAngles); + // ... + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const CircularProgressIndicator(); + } + }, + ); +} +``` + +For more control, you can subscribe to the stream directly: + +1. Initialize the sensor and specify the desired update interval during `initState`: + ```dart + late final StreamSubscription orientationSubscription; + + @override + void initState() { + super.initState(); + orientationSubscription = RotationSensor.orientationStream.listen((event) { + final azimuth = event.eulerAngles.azimuth; + // Print azimuth: 0 for North, π/2 for East, π for South, -π/2 for West. + // On iOS, an azimuth of 0 only points to north when a north-referenced + // reference frame is set (see the Reference Frame section below). + print(azimuth); + }); + } + ``` + +2. Remember to cancel the subscription in the `dispose` method to prevent memory leaks: + ```dart + @override + void dispose() { + orientationSubscription.cancel(); + super.dispose(); + } + ``` + +## Configuration + +To configure the `flutter_rotation_sensor` plugin, you can set various properties at any time, such +as in your `initState` method. Below is an example demonstrating how to configure these settings: + +```dart +@override +void initState() { + super.initState(); + // Set the sampling period for the rotation sensor + RotationSensor.samplingPeriod = SensorInterval.uiInterval; + + // Set the reference frame the azimuth is measured from + RotationSensor.referenceFrame = ReferenceFrame.magneticNorth; + + // Set the coordinate system for the rotation sensor + RotationSensor.coordinateSystem = CoordinateSystem.transformed(Axis3.X, Axis3.Z); +} +``` + +### Sampling Period + +The [RotationSensor.samplingPeriod](https://pub.dev/documentation/flutter_rotation_sensor/latest/flutter_rotation_sensor/RotationSensor/samplingPeriod.html) +determines how frequently the sensor data is updated. Here are the predefined values you can use: + +- `SensorInterval.normal` (200ms): Default rate, suitable for general use. +- `SensorInterval.ui` (66ms): Suitable for UI updates, balancing update rate and power consumption. +- `SensorInterval.game` (20ms): Suitable for games, updating at a rate to ensure smooth motion. +- `SensorInterval.fastest` (0ms): Updates as fast as possible. + +You can also set a custom [Duration](https://api.dart.dev/stable/dart-core/Duration-class.html), for +example: + +```dart +void config() { + RotationSensor.samplingPeriod = Duration(seconds: 1); +} +``` + +Events may arrive at a rate faster or slower than the sampling period, which is only a hint to the +system. The actual rate depends on the system's event queue and sensor hardware capabilities. + +### Reference Frame + +The [RotationSensor.referenceFrame](https://pub.dev/documentation/flutter_rotation_sensor/latest/flutter_rotation_sensor/RotationSensor/referenceFrame.html) +property controls the world reference the azimuth is measured from. Whatever the value, the +orientation is always returned in the plugin's east-north-up world convention, so an azimuth of 0 +means the device points north. Here are the values you can use: + +- `ReferenceFrame.device`: *(default value)* The platform default, with no guarantee of a north + reference. On iOS the horizontal reference is arbitrary (the direction the device happened to + point when the sensor started, no compass). On Android the rotation vector sensor is already + referenced to magnetic north. +- `ReferenceFrame.magneticNorth`: The azimuth is referenced to magnetic north on both platforms, + without depending on location services. +- `ReferenceFrame.trueNorth`: The azimuth is referenced to true (geographic) north. Requires + location services to be available. + +```dart +void config() { + RotationSensor.referenceFrame = ReferenceFrame.magneticNorth; +} +``` + +> [!NOTE] +> Reference frame selection is currently implemented on iOS only. On Android the rotation vector is +> always referenced to magnetic north, so `device` and `magneticNorth` already behave as expected, +> while `trueNorth` currently behaves like `magneticNorth`. Android support for `trueNorth` is +> tracked in a separate issue. + +### Coordinate System + +The [RotationSensor.coordinateSystem](https://pub.dev/documentation/flutter_rotation_sensor/latest/flutter_rotation_sensor/RotationSensor/coordinateSystem.html) +property allows you to remap the coordinate system used by the sensor data. By default, the +coordinate system follows the display's orientation. You can transform the coordinate system to +match your application's needs. Here are the predefined coordinate systems you can use: + +- `CoordinateSystem.device()`: Defined relative to the device's screen in its default orientation. +- `CoordinateSystem.display()`: *(default value)* Adapts to the device's current orientation. +- `CoordinateSystem.transformed()`: Applies a transformation on top of a base coordinate system. + +For example, a driving navigation application may want a transformed coordinate system where the +y-axis points to the back of the device. This ensures that the plugin can return the azimuth +correctly when the device is mounted in front of the driver. + +```dart +void config() { + // The new x-axis is same as old x-axis and the new y-axis is the old negative-z-axis which points + // to the back of the device. + RotationSensor.coordinateSystem = CoordinateSystem.transformed(Axis3.X, -Axis3.Z); +} +``` + +## License + +This plugin is licensed under the [MIT License](LICENSE). diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..fc5887b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,95 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + - avoid_dynamic_calls + - cancel_subscriptions + - close_sinks + - comment_references + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - literal_only_boolean_expressions + - no_self_assignments + - no_wildcard_variable_uses + - prefer_relative_imports + - prefer_void_to_null + - test_types_in_equals + - throw_in_finally + - unnecessary_statements + + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_double_and_int_checks + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_final_parameters + - avoid_implementing_value_types + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_positional_boolean_parameters + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_setters_without_getters + - avoid_types_on_closure_parameters + - avoid_unused_constructor_parameters + - avoid_void_async + - cascade_invocations + - cast_nullable_to_non_nullable + - combinators_ordering + - conditional_uri_does_not_exist + - deprecated_consistency + - directives_ordering + - do_not_use_environment + - eol_at_end_of_file + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - lines_longer_than_80_chars + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_literal_bool_comparisons + - no_runtimeType_toString + - noop_primitive_operations + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_constructors_over_static_methods + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_foreach + - prefer_if_elements_to_conditional_expressions + - prefer_int_literals + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_single_quotes + - require_trailing_commas + - sized_box_shrink_expand + - sort_unnamed_constructors_first + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_breaks + - unnecessary_lambdas + - unnecessary_library_directive + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_parenthesis + - unnecessary_raw_strings + - unreachable_from_main + - use_colored_box + - use_decorated_box + - use_enums + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_named_constants + - use_raw_strings + - use_setters_to_change_properties + - use_string_buffers + - use_test_throws_matchers + - use_to_and_as_if_applicable + + - sort_pub_dependencies diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..017d899 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,67 @@ +group = "net.tlserver6y.flutter_rotation_sensor" +version = "1.0-SNAPSHOT" + +buildscript { + ext.agp_version = '8.13.0' + ext.kotlin_version = '2.2.20' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:$agp_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace = 'net.tlserver6y.flutter_rotation_sensor' + + compileSdk = 35 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.20.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..e792a4c --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_rotation_sensor' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2bdd789 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/kotlin/net/tlserver6y/flutter_rotation_sensor/FlutterRotationSensorPlugin.kt b/android/src/main/kotlin/net/tlserver6y/flutter_rotation_sensor/FlutterRotationSensorPlugin.kt new file mode 100644 index 0000000..c35fee9 --- /dev/null +++ b/android/src/main/kotlin/net/tlserver6y/flutter_rotation_sensor/FlutterRotationSensorPlugin.kt @@ -0,0 +1,129 @@ +package net.tlserver6y.flutter_rotation_sensor + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import io.flutter.plugin.common.EventChannel.StreamHandler +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class FlutterRotationSensorPlugin : FlutterPlugin, MethodCallHandler, StreamHandler { + private lateinit var methodChannel: MethodChannel + private lateinit var eventChannel: EventChannel + private lateinit var sensorManager: SensorManager + private var sensor: Sensor? = null + private var sensorEventListener: SensorEventListener? = null + private var samplingPeriod = 200000 + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + val context = flutterPluginBinding.applicationContext + sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + setupMethodChannel(flutterPluginBinding.binaryMessenger) + setupEventChannel(flutterPluginBinding.binaryMessenger) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + teardownMethodChannel() + teardownEventChannels() + } + + private fun setupMethodChannel(messenger: BinaryMessenger) { + methodChannel = MethodChannel(messenger, "rotation_sensor/method") + methodChannel.setMethodCallHandler(this) + } + + private fun teardownMethodChannel() { + methodChannel.setMethodCallHandler(null); + } + + private fun setupEventChannel(messenger: BinaryMessenger) { + eventChannel = EventChannel(messenger, "rotation_sensor/orientation") + eventChannel.setStreamHandler(this) + } + + private fun teardownEventChannels() { + eventChannel.setStreamHandler(null) + onCancel(null) + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "getOrientationStream" -> { + if (sensor == null) { + sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + } + if (call.hasArgument("samplingPeriod")) { + val samplingPeriod = call.argument("samplingPeriod") + if ( + samplingPeriod != null && + samplingPeriod != this.samplingPeriod && + sensorEventListener != null + ) { + this.samplingPeriod = samplingPeriod + sensorManager.unregisterListener(sensorEventListener) + sensorManager.registerListener(sensorEventListener, sensor, samplingPeriod) + } + } + result.success(null); + } + else -> { + result.notImplemented() + } + } + } + + override fun onListen(arguments: Any?, events: EventSink) { + if (sensor != null) { + sensorEventListener = createSensorEventListener(events) + sensorManager.registerListener(sensorEventListener, sensor, samplingPeriod) + } else { + events.error( + "NO_SENSOR", + "Sensor not found", + "It seems that your device has no rotation vector sensor" + ) + } + } + + override fun onCancel(arguments: Any?) { + if (sensor != null) { + sensorEventListener?.let { + sensorManager.unregisterListener(it) + sensorEventListener = null + } + } + } + + private fun createSensorEventListener(events: EventSink): SensorEventListener { + return object : SensorEventListener { + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} + + override fun onSensorChanged(event: SensorEvent) { + + val rotationMatrix = FloatArray(9) + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) + + val orientation = FloatArray(3) + SensorManager.getOrientation(rotationMatrix, orientation) + + val sensorValues = arrayListOf( + event.values[0].toDouble(), + event.values[1].toDouble(), + event.values[2].toDouble(), + event.values[3].toDouble(), + event.values[4].toDouble(), + event.timestamp, + ) + events.success(sensorValues) + } + } + } +} diff --git a/android/src/test/kotlin/net/tlserver6y/flutter_rotation_sensor/FlutterRotationSensorPluginTest.kt b/android/src/test/kotlin/net/tlserver6y/flutter_rotation_sensor/FlutterRotationSensorPluginTest.kt new file mode 100644 index 0000000..fd20c36 --- /dev/null +++ b/android/src/test/kotlin/net/tlserver6y/flutter_rotation_sensor/FlutterRotationSensorPluginTest.kt @@ -0,0 +1,4 @@ +package net.tlserver6y.flutter_rotation_sensor + +internal class FlutterRotationSensorPluginTest { +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..cef2e8f --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,26 @@ +# Created by https://www.toptal.com/developers/gitignore/api/jetbrains,flutter +# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains,flutter + +### Flutter ### +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +build/ +coverage/ +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.idea/libraries/Dart_SDK.xml b/example/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..d351807 --- /dev/null +++ b/example/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/.idea/libraries/KotlinJavaRuntime.xml b/example/.idea/libraries/KotlinJavaRuntime.xml new file mode 100644 index 0000000..2b96ac4 --- /dev/null +++ b/example/.idea/libraries/KotlinJavaRuntime.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/example/.idea/modules.xml b/example/.idea/modules.xml new file mode 100644 index 0000000..2cad400 --- /dev/null +++ b/example/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/example/.idea/runConfigurations/main_dart.xml b/example/.idea/runConfigurations/main_dart.xml new file mode 100644 index 0000000..aab7b5c --- /dev/null +++ b/example/.idea/runConfigurations/main_dart.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/example/.idea/workspace.xml b/example/.idea/workspace.xml new file mode 100644 index 0000000..5b3388c --- /dev/null +++ b/example/.idea/workspace.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..58b31eb --- /dev/null +++ b/example/README.md @@ -0,0 +1,3 @@ +# flutter_rotation_sensor_example + +Demonstrates how to use the flutter_rotation_sensor plugin. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +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.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 + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + 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 new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/.idea/.gitignore b/example/android/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/example/android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/example/android/.idea/codeStyles/Project.xml b/example/android/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..4bec4ea --- /dev/null +++ b/example/android/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/example/android/.idea/codeStyles/codeStyleConfig.xml b/example/android/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/example/android/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/example/android/.idea/compiler.xml b/example/android/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/example/android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/example/android/.idea/deploymentTargetDropDown.xml b/example/android/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/example/android/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/example/android/.idea/deploymentTargetSelector.xml b/example/android/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/example/android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/example/android/.idea/gradle.xml b/example/android/.idea/gradle.xml new file mode 100644 index 0000000..0eab322 --- /dev/null +++ b/example/android/.idea/gradle.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/example/android/.idea/kotlinc.xml b/example/android/.idea/kotlinc.xml new file mode 100644 index 0000000..6d0ee1c --- /dev/null +++ b/example/android/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/example/android/.idea/migrations.xml b/example/android/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/example/android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/example/android/.idea/misc.xml b/example/android/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/example/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/example/android/.idea/other.xml b/example/android/.idea/other.xml new file mode 100644 index 0000000..0d3a1fb --- /dev/null +++ b/example/android/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/example/android/.idea/vcs.xml b/example/android/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/example/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..e659b55 --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,41 @@ +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") +} + +android { + namespace = "net.tlserver6y.flutter_rotation_sensor_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + applicationId = "net.tlserver6y.flutter_rotation_sensor_example" + minSdk = flutter.minSdkVersion + 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.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6ebd455 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/net/tlserver6y/flutter_rotation_sensor_example/MainActivity.kt b/example/android/app/src/main/kotlin/net/tlserver6y/flutter_rotation_sensor_example/MainActivity.kt new file mode 100644 index 0000000..d6d9a04 --- /dev/null +++ b/example/android/app/src/main/kotlin/net/tlserver6y/flutter_rotation_sensor_example/MainActivity.kt @@ -0,0 +1,5 @@ +package net.tlserver6y.flutter_rotation_sensor_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/flutter_rotation_sensor_example_android.iml b/example/android/flutter_rotation_sensor_example_android.iml new file mode 100644 index 0000000..1899969 --- /dev/null +++ b/example/android/flutter_rotation_sensor_example_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -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 new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 0000000..19a97ee --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.13.0" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/example/assets/images/other.png b/example/assets/images/other.png new file mode 100644 index 0000000..5e816d4 Binary files /dev/null and b/example/assets/images/other.png differ diff --git a/example/assets/images/top.png b/example/assets/images/top.png new file mode 100644 index 0000000..63bd35a Binary files /dev/null and b/example/assets/images/top.png differ diff --git a/example/flutter_rotation_sensor_example.iml b/example/flutter_rotation_sensor_example.iml new file mode 100644 index 0000000..f66303d --- /dev/null +++ b/example/flutter_rotation_sensor_example.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/.idea/workspace.xml b/example/ios/.idea/workspace.xml new file mode 100644 index 0000000..f6bbb04 --- /dev/null +++ b/example/ios/.idea/workspace.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1704645071516 + + + + + + \ No newline at end of file diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..f48ccee --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,34 @@ +PODS: + - Flutter (1.0.0) + - flutter_rotation_sensor (0.0.1): + - Flutter + - integration_test (0.0.1): + - Flutter + - native_device_orientation (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_rotation_sensor (from `.symlinks/plugins/flutter_rotation_sensor/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_rotation_sensor: + :path: ".symlinks/plugins/flutter_rotation_sensor/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + native_device_orientation: + :path: ".symlinks/plugins/native_device_orientation/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_rotation_sensor: f37d8d24050572029bdd2d55a93e0f334e2eeac6 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + native_device_orientation: 348b10c346a60ebbc62fb235a4fdb5d1b61a8f55 + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..25a96f1 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.tlserver6y.flutterRotationSensorExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.tlserver6y.flutterRotationSensorExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.tlserver6y.flutterRotationSensorExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.tlserver6y.flutterRotationSensorExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.tlserver6y.flutterRotationSensorExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.tlserver6y.flutterRotationSensorExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..15cada4 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..1879ebd --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flutter Rotation Sensor + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_rotation_sensor_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61de4bc --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,26 @@ +import Flutter +import UIKit +import XCTest + +@testable import flutter_rotation_sensor + +// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. +// +// See https://developer.apple.com/documentation/xctest for more information about using XCTest. + +class RunnerTests: XCTestCase { + + func testGetPlatformVersion() { + let plugin = FlutterRotationSensorPlugin() + + let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) + resultExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..9de5112 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,173 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:simple_3d/simple_3d.dart'; +import 'package:simple_3d_renderer/simple_3d_renderer.dart'; +import 'package:util_simple_3d/util_simple_3d.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + late final Sp3dWorld world; + + @override + void initState() { + super.initState(); + const black = Color(0xFF000000); + final obj = UtilSp3dGeometry.cube(60, 200, 40, 1, 1, 1) + ..move(Sp3dV3D(0, 0, -20)) + ..materials = [ + Sp3dMaterial(black, true, 0, black, imageIndex: 0), + Sp3dMaterial(black, true, 0, black, imageIndex: 1), + FSp3dMaterial.red, + FSp3dMaterial.blue, + ] + ..fragments[0].faces[0].materialIndex = 1 + ..fragments[0].faces[2].materialIndex = 2 + ..fragments[0].faces[4].materialIndex = 3; + + world = Sp3dWorld([obj]); + + loadImages(world); + + RotationSensor.samplingPeriod = SensorInterval.uiInterval; + } + + @override + Widget build(BuildContext context) => MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Rotation Sensor Example'), + ), + body: OrientationBuilder( + builder: (context, orientation) => StreamBuilder( + stream: RotationSensor.orientationStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + final axisAngle = data.quaternion.invert().toAxisAngle(); + final axis = axisAngle.axis; + return Center( + child: Flex( + direction: orientation == Orientation.portrait + ? Axis.vertical + : Axis.horizontal, + children: [ + SizedBox( + width: 240, + height: 240, + child: Sp3dRenderer( + const Size(240, 240), + const Sp3dV2D(120, 120), + world, + Sp3dCamera( + Sp3dV3D(0, 0, 3000), + 3000, + rotateAxis: Sp3dV3D(axis.x, axis.y, axis.z), + radian: axisAngle.angle, + ), + Sp3dLight(Sp3dV3D(0, 0, 1)), + useUserGesture: false, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + 'Euler:\n' + '${formatEulerAngles(data.eulerAngles)}', + textAlign: TextAlign.center, + ), + Text( + 'Quaternion:\n' + '${formatQuaternion(data.quaternion)}', + textAlign: TextAlign.center, + ), + Text( + 'Matrix:\n' + '${formatMatrix(data.rotationMatrix)}', + textAlign: TextAlign.center, + ), + Text( + 'Accuracy:\n' + '${formatDouble(data.accuracy)}', + textAlign: TextAlign.center, + ), + Text( + 'Timestamp:\n' + '${data.timestamp}', + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ), + ), + ); + + Future loadImages(Sp3dWorld world) async { + world.objs[0].images = await Future.wait([ + readImageFile('./assets/images/other.png'), + readImageFile('./assets/images/top.png'), + ]); + await world.initImages(); + } + + Future readImageFile(String filePath) async { + final byteData = await rootBundle.load(filePath); + return byteData.buffer.asUint8List(); + } + + String formatQuaternion(Quaternion q) { + final f = formatDouble; + return '(${f(q.x)}, ${f(q.y)}, ${f(q.z)} @ ${f(q.w)})'; + } + + String formatMatrix(Matrix3 m) { + final f = formatDouble; + return '/${f(m[0])}, ${f(m[3])}, ${f(m[6])}\\\n' + '| ${f(m[1])}, ${f(m[4])}, ${f(m[7])} |\n' + '\\${f(m[2])}, ${f(m[5])}, ${f(m[8])}/'; + } + + String formatEulerAngles(EulerAngles e) { + final f = formatDouble; + return '(${f(e.azimuth)}, ${f(e.pitch)}, ${f(e.roll)})'; + } + + String formatDouble(double d) => d.toStringAsFixed(2).padLeft(5); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('world', world)); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..91576b4 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,323 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_state_manager: + dependency: transitive + description: + name: file_state_manager + sha256: a64f7dfe52e45bb1bb1a0e357ab6d38efddc361f33003c0c0a9b78bf9cf0731d + url: "https://pub.dev" + source: hosted + version: "2.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: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_rotation_sensor: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.2.0" + flutter_test: + dependency: "direct dev" + 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: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + native_device_orientation: + dependency: transitive + description: + name: native_device_orientation + sha256: bc0bcccc79752048d2235c10545c5fd554a46035fe0a4a4534d1bb9d8bc85e6c + url: "https://pub.dev" + source: hosted + version: "2.0.4" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + simple_3d: + dependency: "direct main" + description: + name: simple_3d + sha256: f40a14daa88f327ec2807403142aa517aee932fee9d37e98ab71eb42abbf4750 + url: "https://pub.dev" + source: hosted + version: "16.0.2" + simple_3d_renderer: + dependency: "direct main" + description: + name: simple_3d_renderer + sha256: bf94f7e90c9a601072921aa2f27baae09d9a531e41bf849225bb2029845a2e2e + url: "https://pub.dev" + source: hosted + version: "22.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + util_simple_3d: + dependency: "direct main" + description: + name: util_simple_3d + sha256: "2381db1cab8b79e858407a981c3a34016d0514c94bca8f656524d158b970516b" + url: "https://pub.dev" + source: hosted + version: "12.0.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..af264f7 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: flutter_rotation_sensor_example +description: "Demonstrates how to use the flutter_rotation_sensor plugin." +publish_to: 'none' + +environment: + sdk: ^3.8.0 + +dependencies: + flutter: + sdk: flutter + flutter_rotation_sensor: + path: ../ + simple_3d: any + simple_3d_renderer: ^22.0.2 + util_simple_3d: any + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true + + assets: + - 'assets/images/top.png' + - 'assets/images/other.png' diff --git a/flutter_rotation_sensor.iml b/flutter_rotation_sensor.iml new file mode 100644 index 0000000..c0043c2 --- /dev/null +++ b/flutter_rotation_sensor.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..034771f --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ios/Classes/FlutterRotationSensorPlugin.swift b/ios/Classes/FlutterRotationSensorPlugin.swift new file mode 100644 index 0000000..95f328e --- /dev/null +++ b/ios/Classes/FlutterRotationSensorPlugin.swift @@ -0,0 +1,109 @@ +import Flutter +import UIKit +import CoreMotion + +public class FlutterRotationSensorPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { + + private var eventChannel: FlutterEventChannel + private let motionManager: CMMotionManager + private var referenceFrame: CMAttitudeReferenceFrame = .xArbitraryZVertical + private var eventSink: FlutterEventSink? + + // CoreMotion expresses a north reference frame as (north, west, up), while + // this plugin's world convention is (east, north, up) like Android. The two + // differ by a fixed 90° rotation about the vertical axis, applied to the + // attitude quaternion so the azimuth stays 0 = north on every platform. + private let northToEastNorthUp = 0.7071067811865476 // sin(45°) = cos(45°) + + public static func register(with registrar: FlutterPluginRegistrar) { + let methodChannel = FlutterMethodChannel(name: "rotation_sensor/method", binaryMessenger: registrar.messenger()) + let eventChannel = FlutterEventChannel(name: "rotation_sensor/orientation", binaryMessenger: registrar.messenger()) + let motionManager = CMMotionManager() + motionManager.deviceMotionUpdateInterval = 0.2 + let instance = FlutterRotationSensorPlugin(eventChannel: eventChannel, motionManager: motionManager) + registrar.addMethodCallDelegate(instance, channel: methodChannel) + eventChannel.setStreamHandler(instance) + } + + public init(eventChannel: FlutterEventChannel, motionManager: CMMotionManager) { + self.eventChannel = eventChannel + self.motionManager = motionManager + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getOrientationStream": + let args = call.arguments as? [String: Any] + if let samplingPeriod = args?["samplingPeriod"] as? Double { + motionManager.deviceMotionUpdateInterval = samplingPeriod * 0.000001 + } + updateReferenceFrame(args?["referenceFrame"] as? String) + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + + private func updateReferenceFrame(_ name: String?) { + let newFrame = attitudeReferenceFrame(from: name) + guard newFrame != referenceFrame else { return } + referenceFrame = newFrame + if motionManager.isDeviceMotionActive, let sink = eventSink { + motionManager.stopDeviceMotionUpdates() + startUpdates(sink) + } + } + + private func attitudeReferenceFrame(from name: String?) -> CMAttitudeReferenceFrame { + switch name { + case "magneticNorth": return .xMagneticNorthZVertical + case "trueNorth": return .xTrueNorthZVertical + default: return .xArbitraryZVertical + } + } + + private func isNorthReferenced(_ frame: CMAttitudeReferenceFrame) -> Bool { + return frame == .xMagneticNorthZVertical || frame == .xTrueNorthZVertical + } + + private func startUpdates(_ events: @escaping FlutterEventSink) { + let correctToEastNorthUp = isNorthReferenced(referenceFrame) + motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue()) { (motion, error) in + guard let motion = motion, error == nil else { + events(FlutterError(code: "UNAVAILABLE", message: "Device motion updates unavailable", details: nil)) + return + } + + let q = motion.attitude.quaternion + let k = self.northToEastNorthUp + let qx = correctToEastNorthUp ? k * (q.x - q.y) : q.x + let qy = correctToEastNorthUp ? k * (q.x + q.y) : q.y + let qz = correctToEastNorthUp ? k * (q.z + q.w) : q.z + let qw = correctToEastNorthUp ? k * (q.w - q.z) : q.w + + let rotationVector = [qx, qy, qz, qw, -1.0, Int64((motion.timestamp * 1000000000).rounded())] as [Any] + DispatchQueue.main.async { + events(rotationVector) + } + } + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + guard motionManager.isDeviceMotionAvailable else { + return FlutterError(code: "NO_SENSOR", message: "Rotation vector sensor unavailable", details: nil) + } + eventSink = events + startUpdates(events) + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + motionManager.stopDeviceMotionUpdates() + eventSink = nil + return nil + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + eventChannel.setStreamHandler(nil) + } +} diff --git a/ios/Resources/PrivacyInfo.xcprivacy b/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a34b7e2 --- /dev/null +++ b/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/ios/flutter_rotation_sensor.podspec b/ios/flutter_rotation_sensor.podspec new file mode 100644 index 0000000..5c88285 --- /dev/null +++ b/ios/flutter_rotation_sensor.podspec @@ -0,0 +1,30 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_rotation_sensor.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_rotation_sensor' + s.version = '0.0.1' + s.summary = <<-DESC +A package provides a stream of device's orientation in three different representations: a rotation +matrix, a quaternion, and Euler angles (azimuth, pitch, roll). + DESC + s.description = <<-DESC +A package provides a stream of device's orientation in three different representations: a rotation +matrix, a quaternion, and Euler angles (azimuth, pitch, roll). + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + } + s.swift_version = '5.0' +end diff --git a/lib/flutter_rotation_sensor.dart b/lib/flutter_rotation_sensor.dart new file mode 100644 index 0000000..0ad866f --- /dev/null +++ b/lib/flutter_rotation_sensor.dart @@ -0,0 +1,11 @@ +export 'src/coordinate_system.dart'; +export 'src/math/axis3.dart'; +export 'src/math/axis_angle.dart'; +export 'src/math/euler_angles.dart'; +export 'src/math/matrix3.dart'; +export 'src/math/quaternion.dart'; +export 'src/math/vector3.dart'; +export 'src/orientation_event.dart'; +export 'src/reference_frame.dart'; +export 'src/rotation_sensor.dart'; +export 'src/sensor_interval.dart'; diff --git a/lib/src/coordinate_system.dart b/lib/src/coordinate_system.dart new file mode 100644 index 0000000..50fdb2f --- /dev/null +++ b/lib/src/coordinate_system.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:native_device_orientation/native_device_orientation.dart'; + +import 'environment.dart'; +import 'math/axis3.dart'; +import 'orientation_event.dart'; + +/// An abstract class representing a standard 3-axis right-handed Cartesian +/// coordinate system to express orientation data values. +// ignore: one_member_abstracts +abstract class CoordinateSystem { + const CoordinateSystem(); + + factory CoordinateSystem.device() = DeviceCoordinateSystem; + + factory CoordinateSystem.display() = DisplayCoordinateSystem; + + factory CoordinateSystem.transformed( + Axis3 newX, + Axis3 newY, [ + CoordinateSystem? base, + ]) = TransformedCoordinateSystem; + + /// Applies the coordinate system transformation to the given orientation + /// event. + OrientationEvent apply(OrientationEvent event); +} + +/// A coordinate system defined relative to the device's screen when the device +/// is held in its default orientation, which is the orientation that the system +/// first uses for its boot logo, or the orientation in which the hardware logos +/// or markings are upright, or the orientation in which the cameras are at the +/// top. +/// +/// - X axis: Horizontal, points to the right of the screen in its default +/// orientation. +/// - Y axis: Vertical, points up in device's default orientation. +/// - Z axis: Points toward the outside of the screen. Coordinates behind the +/// screen have negative Z values. +class DeviceCoordinateSystem extends CoordinateSystem { + static DeviceCoordinateSystem? _instance; + + factory DeviceCoordinateSystem() => _instance ??= DeviceCoordinateSystem._(); + + const DeviceCoordinateSystem._(); + + @override + OrientationEvent apply(OrientationEvent event) => event; +} + +/// A coordinate system that adapts to the device's current orientation. It +/// adjusts based on the device's rotation. +/// +/// - X axis: Horizontal, points to the right of the current UI orientation. +/// - Y axis: Vertical, points up in current UI orientation. +/// - Z axis: Points toward the outside of the screen. Coordinates behind the +/// screen have negative Z values. +class DisplayCoordinateSystem extends CoordinateSystem { + static DisplayCoordinateSystem? _instance; + + NativeDeviceOrientationCommunicator? _communicator; + + @visibleForTesting + NativeDeviceOrientationCommunicator? get communicator => _communicator; + + @visibleForTesting + set communicator(NativeDeviceOrientationCommunicator? value) { + _communicator = value; + _orientationStreamSubscription?.cancel(); + _orientationStreamSubscription = value?.onOrientationChanged().listen( + (o) => orientation = o, + ); + } + + StreamSubscription? _orientationStreamSubscription; + + @visibleForTesting + NativeDeviceOrientation orientation = NativeDeviceOrientation.portraitUp; + + factory DisplayCoordinateSystem() => + _instance ??= DisplayCoordinateSystem._(); + + DisplayCoordinateSystem._() { + if (!isWeb && + [ + TargetPlatform.android, + TargetPlatform.iOS, + ].contains(defaultTargetPlatform)) { + communicator = NativeDeviceOrientationCommunicator(); + } + } + + @override + OrientationEvent apply(OrientationEvent event) { + switch (orientation) { + case NativeDeviceOrientation.portraitUp: + return event; + case NativeDeviceOrientation.portraitDown: + return event.remapCoordinateSystem(-Axis3.X, -Axis3.Y); + case NativeDeviceOrientation.landscapeLeft: + return event.remapCoordinateSystem(-Axis3.Y, Axis3.X); + case NativeDeviceOrientation.landscapeRight: + return event.remapCoordinateSystem(Axis3.Y, -Axis3.X); + case NativeDeviceOrientation.unknown: + throw StateError('Cannot get display orientation.'); + } + } +} + +/// A coordinate system that applies a transformation on top of an optional base +/// coordinate system. If no base is provided, it defaults to the display +/// coordinate system. +/// +/// This transformation allows you to remap the coordinate axes according to +/// your application's specific requirements. The new coordinate system is +/// defined as: +/// +/// - X axis: Corresponds to the `newX` axis in the `base` coordinate system. +/// - Y axis: Corresponds to the `newY` axis in the `base` coordinate system. +/// - Z axis: Defined by the cross product of `newX` and `newY` axes in the +/// `base` coordinate system, ensuring a right-handed coordinate +/// system. +class TransformedCoordinateSystem extends CoordinateSystem { + final CoordinateSystem base; + final Axis3 newX; + final Axis3 newY; + + TransformedCoordinateSystem(this.newX, this.newY, [CoordinateSystem? base]) + : base = base ?? CoordinateSystem.display(); + + @override + OrientationEvent apply(OrientationEvent event) { + event = base.apply(event); + return event.remapCoordinateSystem(newX, newY); + } +} diff --git a/lib/src/environment.dart b/lib/src/environment.dart new file mode 100644 index 0000000..2e9ee5f --- /dev/null +++ b/lib/src/environment.dart @@ -0,0 +1,5 @@ +import 'package:flutter/foundation.dart'; + +/// A boolean that is true if the application was compiled to run on the web. +/// Can be overridden in tests. +bool isWeb = kIsWeb; diff --git a/lib/src/math/axis3.dart b/lib/src/math/axis3.dart new file mode 100644 index 0000000..6b6cbd9 --- /dev/null +++ b/lib/src/math/axis3.dart @@ -0,0 +1,40 @@ +import 'package:meta/meta.dart'; + +import 'vector3.dart'; + +/// Represents an axis of a 3D coordinate system, internally storing a unit +/// vector that indicates the direction of the axis. +@immutable +class Axis3 extends Vector3 { + /// Represents an invalid axis, often used as a placeholder or error state. + static final invalid = Axis3._(0, 0, 0); + + /// Represents the X-axis of a 3D coordinate system. + static final X = Axis3._(1, 0, 0); + + /// Represents the Y-axis of a 3D coordinate system. + static final Y = Axis3._(0, 1, 0); + + /// Represents the Z-axis of a 3D coordinate system. + static final Z = Axis3._(0, 0, 1); + + Axis3._(super.x, super.y, super.z); + + /// Negates the axis vector, effectively representing the axis in the opposite + /// direction. + @override + Axis3 operator -() => Axis3._(-x, -y, -z); + + /// Converts the Axis to a human-readable string, identifying the Axis by its + /// standard Cartesian coordinate name. + @override + String toString() { + if (this == X) return 'X'; + if (this == Y) return 'Y'; + if (this == Z) return 'Z'; + if (this == -X) return '-X'; + if (this == -Y) return '-Y'; + if (this == -Z) return '-Z'; + return 'Invalid'; + } +} diff --git a/lib/src/math/axis_angle.dart b/lib/src/math/axis_angle.dart new file mode 100644 index 0000000..ec48f35 --- /dev/null +++ b/lib/src/math/axis_angle.dart @@ -0,0 +1,27 @@ +import 'dart:math'; + +import 'package:meta/meta.dart'; + +import 'quaternion.dart'; +import 'vector3.dart'; + +/// A class representing an axis-angle rotation. The rotation is defined by an +/// axis and an angle of rotation around that axis. +@immutable +class AxisAngle { + /// The axis represented by a [Vector3]. + final Vector3 axis; + + /// The angle in radians. + final double angle; + + /// Constructs an AxisAngle. + const AxisAngle(this.axis, this.angle); + + /// Converts this axis-angle representation to a quaternion. + Quaternion toQuaternion() { + final a = angle * 0.5; + final s = sin(a); + return Quaternion(s * axis.x, s * axis.y, s * axis.z, cos(a)); + } +} diff --git a/lib/src/math/euler_angles.dart b/lib/src/math/euler_angles.dart new file mode 100644 index 0000000..7fa984b --- /dev/null +++ b/lib/src/math/euler_angles.dart @@ -0,0 +1,88 @@ +import 'dart:math'; + +import 'matrix3.dart'; +import 'vector3.dart'; + +const twoPi = pi * 2; +const halfPi = pi / 2; + +/// Represents the orientation as +/// [Euler angles](https://en.wikipedia.org/wiki/Euler_angles), which are angles +/// of rotation about each of the three principal axes. The sequence of +/// rotations follows the order of azimuth (yaw), pitch, and roll. All angles +/// are in radians. This plugin utilizes +/// [intrinsic](https://en.wikipedia.org/wiki/Euler_angles#Intrinsic_rotations) +/// [Tait-Bryan angles](https://en.wikipedia.org/wiki/Euler_angles#Tait%E2%80%93Bryan_angles), +/// indicating that rotations are performed relative to the rotating reference +/// frame of the device's coordinate system (XYZ axes). +class EulerAngles extends Vector3 { + /// Angle of rotation about the -Z axis. This value represents the angle + /// between the device's Y axis and the magnetic north pole (y-axis). When + /// facing north, this angle is 0, when facing south, this angle is π. + /// Likewise, when facing east, this angle is π/2, and when facing west, this + /// angle is 3π/2. The range of values is 0(inclusive) to 2π(exclusive). + /// + /// This value is also known as [yaw]. + double get azimuth => -z; + + /// Angle of rotation about the -Z axis. This value represents the angle + /// between the device's Y axis and the magnetic north pole (y-axis). When + /// facing north, this angle is 0, when facing south, this angle is π. + /// Likewise, when facing east, this angle is π/2, and when facing west, this + /// angle is 3π/2. The range of values is 0(inclusive) to 2π(exclusive). + /// + /// This value is also known as [azimuth]. + double get yaw => -z; + + /// Angle of rotation about the X axis. This value represents the angle + /// between a plane parallel to the device's screen and a plane parallel to + /// the ground. Assuming that the bottom edge of the device faces the user and + /// that the screen is face-up, tilting the top edge of the device toward the + /// sky creates a positive pitch angle. The range of values is -π/2(inclusive) + /// to π/2(inclusive). + double get pitch => x; + + /// Angle of rotation about the Y axis. This value represents the angle + /// between a plane perpendicular to the device's screen and a plane + /// perpendicular to the ground. Assuming that the bottom edge of the device + /// faces the user and that the screen is face-up, tilting the left edge of + /// the device toward the sky creates a positive roll angle. The range of + /// values is -π(exclusive) to π(inclusive). + double get roll => y; + + /// Constructs an EulerAngles. + factory EulerAngles(double azimuth, double pitch, double roll) { + azimuth %= twoPi; + if (pitch.abs() > halfPi) { + throw UnsupportedError( + 'The value $pitch is not a valid pitch angle. A valid pitch angle must ' + 'be in the range -π/2 (inclusive) to π/2 (inclusive).', + ); + } + roll = -(-(roll + pi) % twoPi) + pi; + return EulerAngles._(pitch, roll, -azimuth); + } + + EulerAngles._(super.x, super.y, super.z); + + /// Converts this Euler angles to a rotation matrix. + Matrix3 toRotationMatrix() { + final cx = cos(x); + final cy = cos(y); + final cz = cos(z); + final sx = sin(x); + final sy = sin(y); + final sz = sin(z); + return Matrix3( + cz * cy - sz * sx * sy, + -cx * sz, + cz * sy + cy * sz * sx, + cy * sz + cz * sx * sy, + cz * cx, + sz * sy - cz * cy * sx, + -cx * sy, + sx, + cx * cy, + ); + } +} diff --git a/lib/src/math/matrix3.dart b/lib/src/math/matrix3.dart new file mode 100644 index 0000000..d78234b --- /dev/null +++ b/lib/src/math/matrix3.dart @@ -0,0 +1,317 @@ +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +import 'euler_angles.dart'; +import 'quaternion.dart'; +import 'vector3.dart'; + +/// A class representing a 3x3 matrix. +@immutable +class Matrix3 { + static const dimension = 3; + + final Float32List _m3Storage; + + /// Constructs a Matrix3 with the specified elements. + //@formatter:off + Matrix3( + double a, double b, double c, + double d, double e, double f, + double g, double h, double i, + ) : _m3Storage = Float32List.fromList([ + a, b, c, + d, e, f, + g, h, i, + ]); + //@formatter:on + + /// Constructs a Matrix3 from the given row vectors. + Matrix3.rows(Vector3 r0, Vector3 r1, Vector3 r2) + : _m3Storage = Float32List.fromList([ + r0.x, r0.y, r0.z, + r1.x, r1.y, r1.z, + r2.x, r2.y, r2.z, + ]); + + /// Constructs a Matrix3 from the given column vectors. + Matrix3.columns(Vector3 c0, Vector3 c1, Vector3 c2) + : _m3Storage = Float32List.fromList([ + c0.x, c1.x, c2.x, + c0.y, c1.y, c2.y, + c0.z, c1.z, c2.z, + ]); + + /// Constructs a Matrix3 with all elements initialized to zero. + Matrix3.zero() : _m3Storage = Float32List(9); + + /// Constructs an identity Matrix3. + Matrix3.identity() + : _m3Storage = Float32List.fromList([ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + ]); + + /// Constructs a rotation matrix around the X-axis. + factory Matrix3.rotateX(double r) { + final cr = cos(r); + final sr = sin(r); + return Matrix3( + //@formatter:off + 1, 0, 0, + 0, cr, -sr, + 0, sr, cr, + //@formatter:on + ); + } + + /// Constructs a rotation matrix around the Y-axis. + factory Matrix3.rotateY(double r) { + final cr = cos(r); + final sr = sin(r); + return Matrix3( + //@formatter:off + cr, 0, sr, + 0, 1, 0, + -sr, 0, cr, + //@formatter:on + ); + } + + /// Constructs a rotation matrix around the Z-axis. + factory Matrix3.rotateZ(double r) { + final cr = cos(r); + final sr = sin(r); + return Matrix3( + //@formatter:off + cr, -sr, 0, + sr, cr, 0, + 0, 0, 1, + //@formatter:on + ); + } + + /// Determines whether this matrix is equal to another object. Returns true + /// if the other object is an Matrix3 with the same elements. + @override + bool operator ==(Object other) => + identical(this, other) || other is Matrix3 && + a == other.a && b == other.b && c == other.c && + d == other.d && e == other.e && f == other.f && + g == other.g && h == other.h && i == other.i; + + @override + int get hashCode => + Object.hash( + //@formatter:off + a, b, c, + d, e, f, + g, h, i, + //@formatter:on + ); + + @override + String toString() => '' + '⌈$a,$b,$c⌉\n' + '|$d,$e,$f|\n' + '⌊$g,$h,$i⌋\n'; + + /// Returns the element at the first row and first column. + double get a => _m3Storage[0]; + + /// Returns the element at the first row and second column. + double get b => _m3Storage[1]; + + /// Returns the element at the first row and third column. + double get c => _m3Storage[2]; + + /// Returns the element at the second row and first column. + double get d => _m3Storage[3]; + + /// Returns the element at the second row and second column. + double get e => _m3Storage[4]; + + /// Returns the element at the second row and third column. + double get f => _m3Storage[5]; + + /// Returns the element at the third row and first column. + double get g => _m3Storage[6]; + + /// Returns the element at the third row and second column. + double get h => _m3Storage[7]; + + /// Returns the element at the third row and third column. + double get i => _m3Storage[8]; + + /// Returns the element at the given index in row major order. + double operator [](int i) => _m3Storage[i]; + + /// Returns the row at the given index. + Vector3 row(int r) { + final i = r * 3; + return Vector3(this[i + 0], this[i + 1], this[i + 2]); + } + + /// Returns the column at the given index. + Vector3 column(int c) => Vector3(this[0 + c], this[3 + c], this[6 + c]); + + /// Returns the negation of this matrix. + Matrix3 operator -() => + Matrix3( + //@formatter:off + -a, -b, -c, + -d, -e, -f, + -g, -h, -i, + //@formatter:on + ); + + /// Adds the given matrix to this matrix. + Matrix3 operator +(Matrix3 o) => + Matrix3( + //@formatter:off + a + o.a, b + o.b, c + o.c, + d + o.d, e + o.e, f + o.f, + g + o.g, h + o.h, i + o.i, + //@formatter:on + ); + + /// Subtracts the given matrix from this matrix. + Matrix3 operator -(Matrix3 o) => + Matrix3( + //@formatter:off + a - o.a, b - o.b, c - o.c, + d - o.d, e - o.e, f - o.f, + g - o.g, h - o.h, i - o.i, + //@formatter:on + ); + + /// Multiplies this matrix by the given scalar. + Matrix3 operator *(double s) => + Matrix3( + //@formatter:off + a * s, b * s, c * s, + d * s, e * s, f * s, + g * s, h * s, i * s, + //@formatter:on + ); + + /// Divides this matrix by the given scalar. + Matrix3 operator /(double s) => + Matrix3( + //@formatter:off + a / s, b / s, c / s, + d / s, e / s, f / s, + g / s, h / s, i / s, + //@formatter:on + ); + + /// Multiplies this matrix by the given matrix. + Matrix3 multiply(Matrix3 o) => + Matrix3( + a * o.a + b * o.d + c * o.g, + a * o.b + b * o.e + c * o.h, + a * o.c + b * o.f + c * o.i, + + d * o.a + e * o.d + f * o.g, + d * o.b + e * o.e + f * o.h, + d * o.c + e * o.f + f * o.i, + + g * o.a + h * o.d + i * o.g, + g * o.b + h * o.e + i * o.h, + g * o.c + h * o.f + i * o.i, + ); + + /// Returns the trace of this matrix. + double get trace => a + e + i; + + /// Returns the determinant of this matrix. + double get determinant => + a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g; + + /// Returns the transpose of this matrix. + Matrix3 transpose() => + Matrix3( + //@formatter:off + a, d, g, + b, e, h, + c, f, i, + //@formatter:on + ); + + /// Returns the adjoint of this matrix. + Matrix3 adjoint() => + Matrix3( + //@formatter:off + e * i - f * h, c * h - b * i, b * f - c * e, + f * g - d * i, a * i - c * g, c * d - a * f, + d * h - e * g, b * g - a * h, a * e - b * d, + //@formatter:on + ); + + /// Returns the inverse of this matrix. + /// If the determinant is zero, returns this matrix. + Matrix3 invert() { + final t = determinant; + if (t == 0) { + return this; + } else { + return Matrix3( + //@formatter:off + (e * i - f * h) / t, (c * h - b * i) / t, (b * f - c * e) / t, + (f * g - d * i) / t, (a * i - c * g) / t, (c * d - a * f) / t, + (d * h - e * g) / t, (b * g - a * h) / t, (a * e - b * d) / t, + //@formatter:on + ); + } + } + + /// Applies the given function to each element of the matrix. + Matrix3 apply(double Function(double) t) => + Matrix3( + //@formatter:off + t(a), t(b), t(c), + t(d), t(e), t(f), + t(g), t(h), t(i), + //@formatter:on + ); + + /// Converts this matrix to Euler angles. + EulerAngles toEulerAngles() { + final x = asin(clampDouble(h, -1, 1)); + final double y; + final double z; + if (h.abs() < 0.9999999) { + y = atan2(-g, i); + z = atan2(-b, e); + } else { + y = 0; + z = atan2(d, a); + } + return EulerAngles(-z, x, y); + } + + /// Converts this matrix to a quaternion. + Quaternion toQuaternion() { + final t = trace; + if (t > 0) { + final s = sqrt(t + 1); + final r = 0.5 / s; + return Quaternion((h - f) * r, (c - g) * r, (d - b) * r, s * 0.5); + } else { + final u = a < e ? (e < i ? 2 : 1) : (a < i ? 2 : 0); + final v = (u + 1) % 3; + final w = (u + 2) % 3; + final s = sqrt(this[u * 4] - this[v * 4] - this[w * 4] + 1); + final q = Float32List(4); + final r = 0.5 / s; + q[u] = s * 0.5; + q[v] = (this[v * 3 + u] + this[u * 3 + v]) * r; + q[w] = (this[w * 3 + u] + this[u * 3 + w]) * r; + q[3] = (this[w * 3 + v] - this[v * 3 + w]) * r; + return Quaternion(q[0], q[1], q[2], q[3]); + } + } +} diff --git a/lib/src/math/quaternion.dart b/lib/src/math/quaternion.dart new file mode 100644 index 0000000..633678b --- /dev/null +++ b/lib/src/math/quaternion.dart @@ -0,0 +1,158 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import 'axis_angle.dart'; +import 'matrix3.dart'; +import 'vector3.dart'; + +/// A class representing a quaternion. +/// +/// The quaternion number system extends the complex numbers. Quaternions have +/// practical uses in applied mathematics, particularly for calculations +/// involving three-dimensional rotations, such as in three-dimensional computer +/// graphics, computer vision, magnetic resonance imaging and crystallographic +/// texture analysis. They can be used alongside other methods of rotation, such +/// as Euler angles and rotation matrices, or as an alternative to them, +/// depending on the application. +@immutable +class Quaternion { + final Float32List _qStorage; + + /// Constructs a Quaternion with given x, y, z, w components + Quaternion(double x, double y, double z, double w) + : _qStorage = Float32List.fromList([x, y, z, w]); + + /// constructs an identity Quaternion (0, 0, 0, 1) + factory Quaternion.identity() => Quaternion(0, 0, 0, 1); + + /// Determines whether this quaternion is equal to another object. Returns + /// true if the other object is an Quaternion with the same components. + @override + bool operator ==(Object other) => + identical(this, other) || + other is Quaternion && + x == other.x && + y == other.y && + z == other.z && + w == other.w; + + @override + int get hashCode => Object.hash(x, y, z, w); + + @override + String toString() => '$x, $y, $z @ $w'; + + /// The x component of the quaternion. + double get x => _qStorage[0]; + + /// The y component of the quaternion. + double get y => _qStorage[1]; + + /// The z component of the quaternion. + double get z => _qStorage[2]; + + /// The w component of the quaternion. + double get w => _qStorage[3]; + + /// Negates this quaternion. + Quaternion operator -() => Quaternion(-x, -y, -z, -w); + + /// Adds this quaternion with another [Quaternion]. + Quaternion operator +(Quaternion o) => + Quaternion(x + o.x, y + o.y, z + o.z, w + o.w); + + /// Subtracts another [Quaternion] from this quaternion. + Quaternion operator -(Quaternion o) => + Quaternion(x - o.x, y - o.y, z - o.z, w - o.w); + + /// Multiplies this quaternion by a scalar. + Quaternion operator *(double s) => Quaternion(x * s, y * s, z * s, w * s); + + /// Divides this quaternion by a scalar. + Quaternion operator /(double s) => Quaternion(x / s, y / s, z / s, w / s); + + /// Computes the Hamilton product of this quaternion with another + /// [Quaternion]. + Quaternion multiply(Quaternion o) => Quaternion( + w * o.x + x * o.w + y * o.z - z * o.y, + w * o.y + y * o.w + z * o.x - x * o.z, + w * o.z + z * o.w + x * o.y - y * o.x, + w * o.w - x * o.x - y * o.y - z * o.z, + ); + + /// The squared length of this quaternion. + double get length2 => x * x + y * y + z * z + w * w; + + /// The length (magnitude) of this quaternion. + double get length => sqrt(length2); + + /// Normalizes this quaternion. + Quaternion normalize() { + final l = length; + if (l == 0) { + return this; + } else { + return this / l; + } + } + + /// Returns the conjugate of this quaternion. + Quaternion conjugate() => Quaternion(-x, -y, -z, w); + + /// Inverts the quaternion. + Quaternion invert() { + final l = length2; + return Quaternion(-x / l, -y / l, -z / l, w / l); + } + + /// Applies a function [f] to each component of this quaternion and returns a + /// new [Quaternion]. + Quaternion apply(double Function(double) f) => + Quaternion(f(x), f(y), f(z), f(w)); + + /// Converts quaternion to axis-angle representation. + AxisAngle toAxisAngle() { + final d = 1 - (w * w); + if (d < 0.00001) { + return AxisAngle(Vector3.zero(), 0); + } else { + final s = sqrt(d); + return AxisAngle(Vector3(x / s, y / s, z / s), 2 * acos(w)); + } + } + + /// Converts quaternion to rotation matrix. + Matrix3 toRotationMatrix() { + final l = length2; + assert(l != 0.0, 'Cannot convert a zero quaternion to rotation matrix.'); + final s = 2.0 / l; + + final xs = x * s; + final ys = y * s; + final zs = z * s; + + final wx = w * xs; + final wy = w * ys; + final wz = w * zs; + final xx = x * xs; + final xy = x * ys; + final xz = x * zs; + final yy = y * ys; + final yz = y * zs; + final zz = z * zs; + + return Matrix3( + 1 - yy - zz, + xy - wz, + xz + wy, + xy + wz, + 1 - xx - zz, + yz - wx, + xz - wy, + yz + wx, + 1 - xx - yy, + ); + } +} diff --git a/lib/src/math/vector3.dart b/lib/src/math/vector3.dart new file mode 100644 index 0000000..7ed770a --- /dev/null +++ b/lib/src/math/vector3.dart @@ -0,0 +1,82 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +/// A 3D vector class for representing and manipulating vectors in +/// three-dimensional space. +@immutable +class Vector3 { + final Float32List _v3Storage; + + /// Constructs a [Vector3] with the given [x], [y], and [z] components. + Vector3(double x, double y, double z) + : _v3Storage = Float32List.fromList([x, y, z]); + + /// Constructs a [Vector3] initialized to zero (0, 0, 0). + Vector3.zero() : _v3Storage = Float32List(3); + + /// Determines whether this vector is equal to another object. Returns true if + /// the other object is an Vector3 with the same components. + @override + bool operator ==(Object other) => + identical(this, other) || + other is Vector3 && x == other.x && y == other.y && z == other.z; + + @override + int get hashCode => Object.hash(x, y, z); + + @override + String toString() => '[$x,$y,$z]'; + + /// The x component of the vector. + double get x => _v3Storage[0]; + + /// The y component of the vector. + double get y => _v3Storage[1]; + + /// The z component of the vector. + double get z => _v3Storage[2]; + + /// Negates this vector. + Vector3 operator -() => Vector3(-x, -y, -z); + + /// Adds this vector with another [Vector3]. + Vector3 operator +(Vector3 o) => Vector3(x + o.x, y + o.y, z + o.z); + + /// Subtracts another [Vector3] from this vector. + Vector3 operator -(Vector3 o) => Vector3(x - o.x, y - o.y, z - o.z); + + /// Multiplies this vector by a scalar. + Vector3 operator *(double s) => Vector3(x * s, y * s, z * s); + + /// Divides this vector by a scalar. + Vector3 operator /(double s) => Vector3(x / s, y / s, z / s); + + /// Computes the dot product of this vector with another [Vector3]. + double dot(Vector3 o) => x * o.x + y * o.y + z * o.z; + + /// Computes the cross product of this vector with another [Vector3]. + Vector3 cross(Vector3 o) => + Vector3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x); + + /// The squared length of this vector. + double get length2 => x * x + y * y + z * z; + + /// The length (magnitude) of this vector. + double get length => sqrt(length2); + + /// Normalizes this vector. + Vector3 normalize() { + final l = length; + if (l == 0) { + return this; + } else { + return this / l; + } + } + + /// Applies a function [f] to each component of this vector and returns a new + /// [Vector3]. + Vector3 apply(double Function(double) f) => Vector3(f(x), f(y), f(z)); +} diff --git a/lib/src/orientation_event.dart b/lib/src/orientation_event.dart new file mode 100644 index 0000000..225e09b --- /dev/null +++ b/lib/src/orientation_event.dart @@ -0,0 +1,111 @@ +import 'math/axis3.dart'; +import 'math/euler_angles.dart'; +import 'math/matrix3.dart'; +import 'math/quaternion.dart'; + +/// Represents an orientation event detected by sensors, providing information +/// about the orientation of the device. +/// +/// The device's coordinate system is defined relative to the screen in its +/// default orientation. It remains unchanged when the device's screen +/// orientation changes. +/// - X axis: Horizontal and points to the right. +/// - Y axis: Vertical and points up. +/// - Z axis: Points towards the outside of the front face of the screen. +/// Coordinates behind the screen have negative Z values. +/// +/// The world coordinate system is defined as a direct orthonormal basis: +/// - x axis: Defined as the vector cross product y⨯z. It is tangential to the +/// ground at the device's current location and roughly points East. +/// - y axis: Tangential to the ground at the device's current location and +/// points towards magnetic north. +/// - z axis: Points towards the sky and is perpendicular to the ground. +/// +/// A +/// [right-handed reference frame](https://en.wikipedia.org/wiki/Right-hand_rule) +/// is adopted, with the +/// [right-hand rule](https://en.wikipedia.org/wiki/Right-hand_rule) used to +/// determine the sign of the angles. +class OrientationEvent { + /// The orientation of the device represented as a quaternion. + final Quaternion quaternion; + + /// An estimated accuracy of the sensor data (in radians). The actual device + /// orientation is expected to be within this margin of error. If the accuracy + /// is unavailable, this value is -1. Accuracy information was introduced in + /// Android SDK level 18, but not all devices support it. For iOS devices, + /// this is always -1. + final double accuracy; + + /// The timestamp at which the event was recorded, in microseconds since + /// some arbitrary point in time, usually the time of system boot. + final int timestamp; + + /// The coordinate system in which the orientation is expressed, represented + /// as a 3x3 matrix. + final Matrix3 coordinateSystem; + + /// Constructs an [OrientationEvent] with the given [quaternion], [accuracy], + /// and [timestamp]. The [coordinateSystem] is initialized to the identity + /// matrix, representing the device's default coordinate system. + OrientationEvent({ + required this.quaternion, + required this.accuracy, + required this.timestamp, + }) : coordinateSystem = Matrix3.identity(); + + /// Constructs an [OrientationEvent] with a specific coordinate system. + OrientationEvent._( + this.quaternion, + this.accuracy, + this.timestamp, + this.coordinateSystem, + ); + + @override + String toString() => + 'OrientationEvent(\n' + 'quaternion: $quaternion,\n' + 'accuracy: $accuracy,\n' + 'timestamp: $timestamp,\n' + 'coordinateSystem:\n' + '$coordinateSystem' + ')'; + + /// The orientation of the device represented as a rotation matrix. + Matrix3 get rotationMatrix => quaternion.toRotationMatrix(); + + /// The orientation of the device represented as euler angles (azimuth, pitch, + /// roll). + EulerAngles get eulerAngles => rotationMatrix.toEulerAngles(); + + /// Remaps the device coordinate system of this [OrientationEvent] to a new + /// coordinate system defined by the specified 'newX' and 'newY' axes. + /// + /// The 'newZ' axis is calculated as the cross product of 'newX' and 'newY'. + /// + /// Throws an [UnsupportedError] if 'newX' and 'newY' are not orthogonal or if + /// they are identical. + /// + /// Returns a new [OrientationEvent] instance with the remapped coordinate + /// system and updated quaternion representing the orientation in the new + /// system. + OrientationEvent remapCoordinateSystem(Axis3 newX, Axis3 newY) { + final newZ = newX.cross(newY); + if (newZ.length2 != 1) { + throw UnsupportedError( + 'The specified axes for newX and newY are not orthogonal or are ' + 'identical. Please specify two different, non-parallel axes that are ' + 'orthogonal to each other.', + ); + } + final transformMatrix = Matrix3.columns(newX, newY, newZ); + + return OrientationEvent._( + quaternion.multiply(transformMatrix.toQuaternion()), + accuracy, + timestamp, + coordinateSystem.multiply(transformMatrix), + ); + } +} diff --git a/lib/src/reference_frame.dart b/lib/src/reference_frame.dart new file mode 100644 index 0000000..a159663 --- /dev/null +++ b/lib/src/reference_frame.dart @@ -0,0 +1,22 @@ +/// The world reference frame the device orientation is expressed against. +/// +/// This controls what the azimuth is measured from. Whatever the value, the +/// orientation is always returned in the package's east-north-up world +/// convention, so an azimuth of 0 means the device points north. +enum ReferenceFrame { + /// The platform default, with no guarantee of a north reference. + /// + /// On iOS the horizontal reference is arbitrary (the direction the device + /// happened to point when the sensor started, no compass). On Android the + /// rotation vector sensor is already referenced to magnetic north. + device, + + /// The azimuth is referenced to magnetic north on both platforms. No + /// dependency on location services. + magneticNorth, + + /// The azimuth is referenced to true (geographic) north. Requires location + /// services to be available. On Android, where the rotation vector is only + /// magnetic, this currently behaves like [magneticNorth]. + trueNorth; +} diff --git a/lib/src/rotation_sensor.dart b/lib/src/rotation_sensor.dart new file mode 100644 index 0000000..37bcc84 --- /dev/null +++ b/lib/src/rotation_sensor.dart @@ -0,0 +1,60 @@ +import 'package:meta/meta.dart'; + +import 'coordinate_system.dart'; +import 'orientation_event.dart'; +import 'reference_frame.dart'; +import 'rotation_sensor_method_channel.dart'; +import 'rotation_sensor_platform.dart'; +import 'sensor_interval.dart'; + +/// Provides access to the device's rotation sensor, offering a real-time stream +/// of the device's orientation. +/// +/// This class allows applications to retrieve a stream of [OrientationEvent]s +/// which include the device's orientation represented as a rotation matrix, +/// quaternion, and Euler angles (azimuth, pitch, roll). +@sealed +class RotationSensor { + /// Determines whether the current platform is supported. + static bool get isPlatformSupported => + RotationSensorMethodChannel.isPlatformSupported; + + /// A broadcast [Stream] of [OrientationEvent]s which emits events containing + /// the orientation of the device from the device's rotation sensor. + static Stream get orientationStream => + RotationSensorPlatform.instance.orientationStream; + + /// The [samplingPeriod] for the device's rotation sensor. The events may + /// arrive at a rate faster or slower than the [samplingPeriod], which is only + /// a hint to the system. The actual rate depends on the system's event queue + /// and sensor hardware capabilities. + /// + /// Defaults to [SensorInterval.normalInterval]. It can be set to other + /// predefined [SensorInterval] values or any [Duration] as needed to suit + /// different use cases such as gaming or UI responsiveness. When changing + /// this value, all existing listeners will be affected. + static Duration get samplingPeriod => + RotationSensorPlatform.instance.samplingPeriod; + + static set samplingPeriod(Duration value) => + RotationSensorPlatform.instance.samplingPeriod = value; + + /// The [coordinateSystem] used for upcoming [OrientationEvent]. + /// + /// Defaults to [DisplayCoordinateSystem]. When changing this value, all + /// existing listeners will receive [OrientationEvent] in the new coordinate + /// system. + static CoordinateSystem coordinateSystem = DisplayCoordinateSystem(); + + /// The world [ReferenceFrame] the azimuth is measured from. + /// + /// Defaults to [ReferenceFrame.device]. Set it to + /// [ReferenceFrame.magneticNorth] or [ReferenceFrame.trueNorth] to get a real + /// compass heading (azimuth 0 = north). When changing this value, all + /// existing listeners will be affected. + static ReferenceFrame get referenceFrame => + RotationSensorPlatform.instance.referenceFrame; + + static set referenceFrame(ReferenceFrame value) => + RotationSensorPlatform.instance.referenceFrame = value; +} diff --git a/lib/src/rotation_sensor_method_channel.dart b/lib/src/rotation_sensor_method_channel.dart new file mode 100644 index 0000000..b69a303 --- /dev/null +++ b/lib/src/rotation_sensor_method_channel.dart @@ -0,0 +1,73 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'environment.dart'; +import 'math/quaternion.dart'; +import 'orientation_event.dart'; +import 'reference_frame.dart'; +import 'rotation_sensor.dart'; +import 'rotation_sensor_platform.dart'; + +/// An implementation of [RotationSensorPlatform] that uses method channels. +class RotationSensorMethodChannel extends RotationSensorPlatform { + + /// The method channel used to interact with the native platform. + @visibleForTesting + static const methodChannel = MethodChannel('rotation_sensor/method'); + + /// The event channel used to receive orientation events from the native + /// platform. + @visibleForTesting + static const eventChannel = EventChannel('rotation_sensor/orientation'); + + /// Determines whether the current platform is supported. + static bool get isPlatformSupported => + !isWeb && + [ + TargetPlatform.android, + TargetPlatform.iOS, + ].contains(defaultTargetPlatform); + + Stream? _orientationStream; + + /// A broadcast [Stream] of [OrientationEvent]s which emits events containing + /// the orientation of the device from the device's rotation sensor. + @override + Stream get orientationStream { + if (_orientationStream != null) { + return _orientationStream!; + } + methodChannel.invokeMethod('getOrientationStream', { + 'samplingPeriod': samplingMicroseconds, + 'referenceFrame': referenceFrameValue.name, + }); + final broadcastStream = eventChannel.receiveBroadcastStream(); + return _orientationStream = broadcastStream.map((event) { + final data = event as List; + final orientationEvent = OrientationEvent( + quaternion: Quaternion(data[0], data[1], data[2], data[3]), + accuracy: data[4], + timestamp: data[5], + ); + return RotationSensor.coordinateSystem.apply(orientationEvent); + }); + } + + @override + @protected + void updateSamplingPeriod(int value) { + methodChannel.invokeMethod('getOrientationStream', { + 'samplingPeriod': value, + 'referenceFrame': referenceFrameValue.name, + }); + } + + @override + @protected + void updateReferenceFrame(ReferenceFrame value) { + methodChannel.invokeMethod('getOrientationStream', { + 'samplingPeriod': samplingMicroseconds, + 'referenceFrame': value.name, + }); + } +} diff --git a/lib/src/rotation_sensor_platform.dart b/lib/src/rotation_sensor_platform.dart new file mode 100644 index 0000000..4267ff5 --- /dev/null +++ b/lib/src/rotation_sensor_platform.dart @@ -0,0 +1,102 @@ +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'orientation_event.dart'; +import 'reference_frame.dart'; +import 'rotation_sensor_method_channel.dart'; +import 'rotation_sensor_unsupported.dart'; +import 'sensor_interval.dart'; + +/// The interface that implementations of rotation_sensor must implement. +abstract class RotationSensorPlatform extends PlatformInterface { + /// Constructs a RotationSensorPlatform. + RotationSensorPlatform() : super(token: _token) { + logger = Logger(runtimeType.toString()); + } + + static final Object _token = Object(); + + /// The [RotationSensorPlatform] for current platform. + static RotationSensorPlatform? _instance; + + static RotationSensorPlatform get instance { + if (_instance != null) return _instance!; + return createPlatformInstance(); + } + + static set instance(RotationSensorPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + @visibleForTesting + static RotationSensorPlatform createPlatformInstance() { + if (RotationSensorMethodChannel.isPlatformSupported) { + return instance = RotationSensorMethodChannel(); + } + return instance = RotationSensorUnsupported(); + } + + @protected + late final Logger logger; + + /// A broadcast [Stream] of [OrientationEvent]s which emits events containing + /// the orientation of the device from the device's rotation sensor. + Stream get orientationStream; + + @protected + int samplingMicroseconds = SensorInterval.normalInterval.inMicroseconds; + + /// The [samplingPeriod] for the device's rotation sensor. The events may + /// arrive at a rate faster or slower than the [samplingPeriod], which is only + /// a hint to the system. The actual rate depends on the system's event queue + /// and sensor hardware capabilities. + /// + /// Defaults to [SensorInterval.normalInterval]. It can be set to other + /// predefined [SensorInterval] values or any [Duration] as needed to suit + /// different use cases such as gaming or UI responsiveness. When changing + /// this value, all existing listeners will be affected. + Duration get samplingPeriod => Duration(microseconds: samplingMicroseconds); + + set samplingPeriod(Duration value) { + samplingMicroseconds = value.inMicroseconds; + if (samplingMicroseconds >= 1 && samplingMicroseconds <= 3) { + logger.warning( + 'The sampling period is currently set to $samplingMicrosecondsμs, ' + 'which is a reserved value in Android. Please consider changing it to ' + // ignore: missing_whitespace_between_adjacent_strings + 'either 0 or 4μs. See https://developer.android.com/reference/android/' + 'hardware/SensorManager#registerListener(android.hardware.' + 'SensorEventListener,%20android.hardware.Sensor,%20int) for more ' + 'information.', + ); + samplingMicroseconds = 0; + } + updateSamplingPeriod(samplingMicroseconds); + } + + @protected + void updateSamplingPeriod(int value) { + // no-op + } + + @protected + ReferenceFrame referenceFrameValue = ReferenceFrame.device; + + /// The world [ReferenceFrame] the azimuth is measured from. + /// + /// Defaults to [ReferenceFrame.device]. When changing this value, all + /// existing listeners will be affected. + ReferenceFrame get referenceFrame => referenceFrameValue; + + set referenceFrame(ReferenceFrame value) { + referenceFrameValue = value; + updateReferenceFrame(value); + } + + @protected + void updateReferenceFrame(ReferenceFrame value) { + // no-op + } +} diff --git a/lib/src/rotation_sensor_unsupported.dart b/lib/src/rotation_sensor_unsupported.dart new file mode 100644 index 0000000..d0ee2f6 --- /dev/null +++ b/lib/src/rotation_sensor_unsupported.dart @@ -0,0 +1,21 @@ +import 'package:flutter/foundation.dart'; + +import 'environment.dart'; +import 'orientation_event.dart'; +import 'rotation_sensor_platform.dart'; + +/// A placeholder implementation of [RotationSensorPlatform] for unsupported +/// platforms. Used as a fallback to indicate that device rotation sensing is +/// not available. +class RotationSensorUnsupported extends RotationSensorPlatform { + /// Throws an [UnsupportedError] indicating that the rotation sensor is not + /// supported on the current platform. This getter does not return a + /// functional stream. + @override + Stream get orientationStream { + final platform = isWeb ? 'web' : defaultTargetPlatform.name; + throw UnsupportedError( + 'FlutterRotationSensor does not support the $platform platform.', + ); + } +} diff --git a/lib/src/sensor_interval.dart b/lib/src/sensor_interval.dart new file mode 100644 index 0000000..51f39de --- /dev/null +++ b/lib/src/sensor_interval.dart @@ -0,0 +1,25 @@ +import 'package:meta/meta.dart'; + +/// Defines some common intervals for sensor data updates. +@sealed +class SensorInterval { + /// Default update interval suitable for tracking screen orientation changes. + /// This is a balanced rate that does not demand high processing power and is + /// sufficient for most applications that react to orientation changes. + static const normalInterval = Duration(milliseconds: 200); + + /// Update interval optimised for user interface responsiveness. This faster + /// rate is appropriate when the UI needs to update smoothly in response to + /// sensor data, such as in animations or transitions. + static const uiInterval = Duration(milliseconds: 66, microseconds: 667); + + /// High-frequency update interval suitable for gaming applications. Provides + /// more frequent updates to ensure game mechanics based on sensor data are + /// responsive and provide a fluid experience. + static const gameInterval = Duration(milliseconds: 20); + + /// The fastest possible update interval for sensor data. This setting is for + /// applications that require real-time updates from the sensor, such as those + /// needed for precise scientific measurements or advanced simulation. + static const fastestInterval = Duration.zero; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..3b8f051 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,43 @@ +name: flutter_rotation_sensor +description: > + A package provides a stream of device's orientation in three different representations: a rotation + matrix, a quaternion, and Euler angles (azimuth, pitch, roll). +version: 0.2.0 +repository: https://github.com/tlserver/flutter_rotation_sensor + +environment: + sdk: ^3.7.2 + flutter: '>=3.3.0' + +topics: + - rotation + - orientation + - heading + - sensor + - compass + +dependencies: + flutter: + sdk: flutter + logging: ^1.2.0 + # flutter_test depends on meta, so use any + meta: any + native_device_orientation: ^2.0.4 + plugin_platform_interface: ^2.1.8 + +dev_dependencies: + build_runner: ^2.4.11 + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + lint: ^2.3.0 + mockito: ^5.4.4 + +flutter: + plugin: + platforms: + android: + package: net.tlserver6y.flutter_rotation_sensor + pluginClass: FlutterRotationSensorPlugin + ios: + pluginClass: FlutterRotationSensorPlugin diff --git a/test/coordinate_system_test.dart b/test/coordinate_system_test.dart new file mode 100644 index 0000000..339dc97 --- /dev/null +++ b/test/coordinate_system_test.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_rotation_sensor/src/rotation_sensor_method_channel.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:native_device_orientation/native_device_orientation.dart'; + +@GenerateNiceMocks([MockSpec()]) +import 'coordinate_system_test.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final platform = RotationSensorMethodChannel(); + final binaryMessenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + const methodChannel = RotationSensorMethodChannel.methodChannel; + const orientationChannel = RotationSensorMethodChannel.eventChannel; + // ignore: close_sinks + late StreamController oeStreamController; + late int expectedSamplingPeriod; + + setUp(() { + oeStreamController = StreamController(); + expectedSamplingPeriod = platform.samplingPeriod.inMicroseconds; + binaryMessenger + ..setMockMethodCallHandler(methodChannel, (methodCall) async { + switch (methodCall.method) { + case 'getOrientationStream': + final arguments = methodCall.arguments as Map; + final samplingPeriod = arguments['samplingPeriod'] as int; + expect(samplingPeriod, expectedSamplingPeriod); + return null; + default: + throw UnsupportedError(methodCall.method); + } + }) + ..setMockStreamHandler( + orientationChannel, + MockStreamHandler.inline( + onListen: + (args, sink) => oeStreamController.stream.listen( + (_) => sink.success([ + // Quaternion + 0.0, 0.0, 0.0, 1.0, + // Accuracy + -1.0, + // Timestamp + 123456789, + ]), + onDone: () => sink.endOfStream(), + ), + ), + ); + }); + + tearDown(() async { + await oeStreamController.sink.close(); + binaryMessenger + ..setMockMethodCallHandler(methodChannel, null) + ..setMockStreamHandler(orientationChannel, null); + }); + + test( + 'orientationStream emit OrientationEvent in device coordinate system', + () async { + RotationSensor.coordinateSystem = CoordinateSystem.device(); + oeStreamController.sink.add(null); + final orientationEvent = await platform.orientationStream.first; + expect(orientationEvent.coordinateSystem, equals(Matrix3.identity())); + }, + ); + + test( + 'orientationStream emit OrientationEvent in display coordinate system', + () async { + final displayCoordinateSystem = DisplayCoordinateSystem(); + RotationSensor.coordinateSystem = displayCoordinateSystem; + final ndoStreamController = + StreamController.broadcast(); + final mockCommunicator = MockNativeDeviceOrientationCommunicator(); + when( + mockCommunicator.onOrientationChanged(), + ).thenAnswer((_) => ndoStreamController.stream); + displayCoordinateSystem.communicator = mockCommunicator; + expect(displayCoordinateSystem.communicator, equals(mockCommunicator)); + + final orientations = [ + NativeDeviceOrientation.portraitUp, + NativeDeviceOrientation.landscapeRight, + NativeDeviceOrientation.portraitDown, + NativeDeviceOrientation.landscapeLeft, + ]; + + final orientationEventsFuture = + platform.orientationStream.take(orientations.length).toList(); + for (final orientation in orientations) { + ndoStreamController.sink.add(orientation); + await Future.delayed(const Duration(microseconds: 1), () {}); + oeStreamController.sink.add(null); + await Future.delayed(const Duration(microseconds: 1), () {}); + } + final orientationEvents = await orientationEventsFuture; + + for ( + var t = 0, e = Matrix3.identity(); + t < orientationEvents.length; + t++, e = e.multiply(Matrix3(0, -1, 0, 1, 0, 0, 0, 0, 1)) + ) { + final orientationEvent = orientationEvents[t]; + expect( + orientationEvent.coordinateSystem, + equals(e), + reason: 'orientationEvents[$t]', + ); + } + + ndoStreamController.sink.add(NativeDeviceOrientation.portraitUp); + await ndoStreamController.close(); + }, + ); + + test('orientationStream emit error in display coordinate system', () async { + final displayCoordinateSystem = DisplayCoordinateSystem(); + RotationSensor.coordinateSystem = displayCoordinateSystem; + final ndoStreamController = + StreamController.broadcast(); + final mockCommunicator = MockNativeDeviceOrientationCommunicator(); + when( + mockCommunicator.onOrientationChanged(), + ).thenAnswer((_) => ndoStreamController.stream); + displayCoordinateSystem.communicator = mockCommunicator; + expect(displayCoordinateSystem.communicator, equals(mockCommunicator)); + + ndoStreamController.sink.add(NativeDeviceOrientation.unknown); + await Future.delayed(const Duration(microseconds: 1), () {}); + oeStreamController.sink.add(null); + await Future.delayed(const Duration(microseconds: 1), () {}); + await expectLater(() => platform.orientationStream.first, throwsStateError); + + ndoStreamController.sink.add(NativeDeviceOrientation.portraitUp); + await ndoStreamController.close(); + }); + + test( + 'orientationStream emit OrientationEvent in transformed coordinate system', + () async { + RotationSensor.coordinateSystem = CoordinateSystem.transformed( + Axis3.X, + Axis3.Z, + CoordinateSystem.transformed(Axis3.X, Axis3.Y), + ); + oeStreamController.sink.add(null); + final orientationEvent = await platform.orientationStream.first; + expect( + orientationEvent.coordinateSystem, + equals(Matrix3(1, 0, 0, 0, 0, -1, 0, 1, 0)), + ); + }, + ); +} diff --git a/test/coordinate_system_test.mocks.dart b/test/coordinate_system_test.mocks.dart new file mode 100644 index 0000000..0499cc8 --- /dev/null +++ b/test/coordinate_system_test.mocks.dart @@ -0,0 +1,93 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in rotation_sensor/test/coordinate_system_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:native_device_orientation/src/native_device_orientation.dart' + as _i4; +import 'package:native_device_orientation/src/native_device_orientation_communicator.dart' + as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [NativeDeviceOrientationCommunicator]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNativeDeviceOrientationCommunicator extends _i1.Mock + implements _i2.NativeDeviceOrientationCommunicator { + @override + _i3.Future<_i4.NativeDeviceOrientation> orientation({ + bool? useSensor = false, + _i4.NativeDeviceOrientation? defaultOrientation = + _i4.NativeDeviceOrientation.portraitUp, + }) => + (super.noSuchMethod( + Invocation.method( + #orientation, + [], + { + #useSensor: useSensor, + #defaultOrientation: defaultOrientation, + }, + ), + returnValue: _i3.Future<_i4.NativeDeviceOrientation>.value( + _i4.NativeDeviceOrientation.portraitUp), + returnValueForMissingStub: + _i3.Future<_i4.NativeDeviceOrientation>.value( + _i4.NativeDeviceOrientation.portraitUp), + ) as _i3.Future<_i4.NativeDeviceOrientation>); + + @override + _i3.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Stream<_i4.NativeDeviceOrientation> onOrientationChanged({ + bool? useSensor = false, + _i4.NativeDeviceOrientation? defaultOrientation = + _i4.NativeDeviceOrientation.portraitUp, + }) => + (super.noSuchMethod( + Invocation.method( + #onOrientationChanged, + [], + { + #useSensor: useSensor, + #defaultOrientation: defaultOrientation, + }, + ), + returnValue: _i3.Stream<_i4.NativeDeviceOrientation>.empty(), + returnValueForMissingStub: + _i3.Stream<_i4.NativeDeviceOrientation>.empty(), + ) as _i3.Stream<_i4.NativeDeviceOrientation>); +} diff --git a/test/math/axis3_test.dart b/test/math/axis3_test.dart new file mode 100644 index 0000000..2eb6414 --- /dev/null +++ b/test/math/axis3_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('predefined axes returns correct unit vectors', () { + expect(Axis3.X, equals(Vector3(1, 0, 0))); + expect(Axis3.Y, equals(Vector3(0, 1, 0))); + expect(Axis3.Z, equals(Vector3(0, 0, 1))); + expect(Axis3.invalid, equals(Vector3(0, 0, 0))); + }); + + test('negating axes reverse their direction', () { + final negX = -Axis3.X; + final negY = -Axis3.Y; + final negZ = -Axis3.Z; + + expect(negX, equals(Vector3(-1, 0, 0))); + expect(negY, equals(Vector3(0, -1, 0))); + expect(negZ, equals(Vector3(0, 0, -1))); + }); + + test('cross product of axes produce correct orthogonal axis', () { + final crossXY = Axis3.X.cross(Axis3.Y); + final crossYZ = Axis3.Y.cross(Axis3.Z); + final crossZX = Axis3.Z.cross(Axis3.X); + + expect(crossXY, equals(Axis3.Z)); + expect(crossYZ, equals(Axis3.X)); + expect(crossZX, equals(Axis3.Y)); + }); + + test('axis instances with the same vector are equal', () { + final axis1 = Axis3.X; + final axis2 = Axis3.Y.cross(Axis3.Z); + + expect(axis1 == axis2, isTrue); + expect(axis1.hashCode, equals(axis2.hashCode)); + }); + + test('toString return the correct representation', () { + expect(Axis3.X.toString(), equals('X')); + expect(Axis3.Y.toString(), equals('Y')); + expect(Axis3.Z.toString(), equals('Z')); + expect((-Axis3.X).toString(), equals('-X')); + expect((-Axis3.Y).toString(), equals('-Y')); + expect((-Axis3.Z).toString(), equals('-Z')); + expect(Axis3.invalid.toString(), equals('Invalid')); + }); +} diff --git a/test/math/axis_angle_test.dart b/test/math/axis_angle_test.dart new file mode 100644 index 0000000..db268de --- /dev/null +++ b/test/math/axis_angle_test.dart @@ -0,0 +1,27 @@ +import 'dart:math'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils.dart'; + +void main() { + test('constructor returns an axis-angle with correct component', () { + final a = AxisAngle(Vector3(1, 0, 0), 1); + expect(a.axis.x, equals(1)); + expect(a.axis.y, equals(0)); + expect(a.axis.z, equals(0)); + expect(a.angle, equals(1)); + }); + + test('toQuaternion converts this axis-angle to quaternion', () { + expect( + AxisAngle(Vector3(0, 0, 1), pi / 2).toQuaternion(), + closeToQuaternion(Quaternion(0, 0, sin(pi / 4), cos(pi / 4))), + ); + expect( + AxisAngle(Vector3(0, 0, 0), 0).toQuaternion(), + closeToQuaternion(Quaternion(0, 0, 0, 1)), + ); + }); +} diff --git a/test/math/euler_angles_test.dart b/test/math/euler_angles_test.dart new file mode 100644 index 0000000..6273441 --- /dev/null +++ b/test/math/euler_angles_test.dart @@ -0,0 +1,97 @@ +import 'dart:math'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils.dart'; + +void main() { + test('constructor sets azimuth, pitch, and roll correctly', () { + const azimuth = pi / 2; // 90 degrees + const pitch = pi / 4; // 45 degrees + const roll = -pi / 6; // -30 degrees + final eulerAngles = EulerAngles(azimuth, pitch, roll); + + expect(eulerAngles.azimuth, closeTo(azimuth, delta)); + expect(eulerAngles.pitch, closeTo(pitch, delta)); + expect(eulerAngles.roll, closeTo(roll, delta)); + }); + + test('azimuth returns the correct value', () { + const azimuth = pi / 3; // 60 degrees + final eulerAngles = EulerAngles(azimuth, 0, 0); + + expect(eulerAngles.azimuth, closeTo(azimuth, delta)); + }); + + test('pitch returns the correct value', () { + const pitch = -pi / 4; // -45 degrees + final eulerAngles = EulerAngles(0, pitch, 0); + + expect(eulerAngles.pitch, closeTo(pitch, delta)); + }); + + test('roll returns the correct value', () { + const roll = pi; // 180 degrees + final eulerAngles = EulerAngles(0, 0, roll); + + expect(eulerAngles.roll, closeTo(roll, delta)); + }); + + test('yaw is equivalent to azimuth', () { + const azimuth = pi / 2; // 90 degrees + final eulerAngles = EulerAngles(azimuth, 0, 0); + + expect(eulerAngles.yaw, closeTo(eulerAngles.azimuth, delta)); + }); + + test('azimuth is normalized to the range 0 to 2π', () { + final eulerAngles = EulerAngles(-2 * pi, 0, 0); + + expect(eulerAngles.azimuth, closeTo(0, delta)); + }); + + test('pitch is within the range -π/2 to π/2', () { + const pitchAngles = [ + -pi / 2, // -90 degrees + pi / 4, // 45 degrees + pi / 2, // 90 degrees + ]; + for (final pitchAngle in pitchAngles) { + final eulerAngles1 = EulerAngles(0, pitchAngle, 0); + expect(eulerAngles1.pitch, closeTo(pitchAngle, delta)); + } + }); + + test('pitch outside -π/2 to π/2 throws InvalidPitchException', () { + expect( + () => EulerAngles(0, pi, 0), + throwsA(isA()), + ); + }); + + test('roll is normalized to the range -π to π', () { + final eulerAngles = EulerAngles(0, 0, -3 * pi); + + expect(eulerAngles.roll, closeTo(pi, delta)); + }); + + test('toRotationMatrix converts to matrix', () { + expect( + EulerAngles(0.1, 0.2, 0.3).toRotationMatrix(), + closeToMatrix3( + Matrix3( + 00.9564251, + 00.0978434, + 00.2750958, + -0.0369570, + 00.9751703, + -0.2183507, + -0.2896295, + 00.1986693, + 00.9362934, + ), + ), + ); + }); +} diff --git a/test/math/matrix3_test.dart b/test/math/matrix3_test.dart new file mode 100644 index 0000000..0355be3 --- /dev/null +++ b/test/math/matrix3_test.dart @@ -0,0 +1,248 @@ +// ignore_for_file: prefer_int_literals + +import 'dart:math'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils.dart'; + +void main() { + test('constructor returns an matrix with correct elements', () { + final m = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); + expect(m.a, equals(1)); + expect(m.b, equals(2)); + expect(m.c, equals(3)); + expect(m.d, equals(4)); + expect(m.e, equals(5)); + expect(m.f, equals(6)); + expect(m.g, equals(7)); + expect(m.h, equals(8)); + expect(m.i, equals(9)); + }); + + test('identity constructor returns an identity matrix', () { + expect( + Matrix3.identity(), + equals(Matrix3(1, 0, 0, 0, 1, 0, 0, 0, 1)), + ); + }); + + test('rows constructor returns an matrix with given row vectors', () { + final v = Vector3(1, 2, 3); + expect( + Matrix3.rows(v, v, v), + equals(Matrix3(1, 2, 3, 1, 2, 3, 1, 2, 3)), + ); + }); + + test('columns constructor returns an matrix with given columns vectors', () { + final v = Vector3(1, 2, 3); + expect( + Matrix3.columns(v, v, v), + equals(Matrix3(1, 1, 1, 2, 2, 2, 3, 3, 3)), + ); + }); + + test('zero constructor returns a zero matrix', () { + expect( + Matrix3.zero(), + equals(Matrix3(0, 0, 0, 0, 0, 0, 0, 0, 0)), + ); + }); + + test('rotateX constructor returns a zero matrix', () { + expect( + Matrix3.rotateX(1), + closeToMatrix3( + Matrix3( + 01.0000000, + 00.0000000, + 00.0000000, + 00.0000000, + 00.5403023, + -0.8414710, + 00.0000000, + 00.8414710, + 00.5403023, + ), + ), + ); + }); + + test('rotateY constructor returns a zero matrix', () { + expect( + Matrix3.rotateY(2), + closeToMatrix3( + Matrix3( + -0.4161468, + 00.0000000, + 00.9092974, + 00.0000000, + 01.0000000, + 00.0000000, + -0.9092974, + 00.0000000, + -0.4161468, + ), + ), + ); + }); + + test('rotateZ constructor returns a zero matrix', () { + expect( + Matrix3.rotateZ(3), + closeToMatrix3( + Matrix3( + -0.9899925, + -0.1411200, + 00.0000000, + 00.1411200, + -0.9899925, + 00.0000000, + 00.0000000, + 00.0000000, + 01.0000000, + ), + ), + ); + }); + + test('equality and hashCode', () { + final m1 = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); + final m2 = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); + final m3 = Matrix3(9, 8, 7, 6, 5, 4, 3, 2, 1); + expect(m1 == m2, isTrue); + expect(m1 == m3, isFalse); + expect(m1.hashCode == m2.hashCode, isTrue); + expect(m1.hashCode == m3.hashCode, isFalse); + }); + + test('toString returns the correct representation', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9).toString(), + equals('⌈1.0,2.0,3.0⌉\n|4.0,5.0,6.0|\n⌊7.0,8.0,9.0⌋\n'), + ); + }); + + test('row returns the corresponding elements at index', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9).row(1), + equals(Vector3(4, 5, 6)), + ); + }); + + test('column returns the corresponding elements at index', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9).column(1), + equals(Vector3(2, 5, 8)), + ); + }); + + test('negation changes sign of each element', () { + expect( + -Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9), + equals(Matrix3(-1, -2, -3, -4, -5, -6, -7, -8, -9)), + ); + }); + + test('addition sums corresponding elements', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9) + Matrix3(9, 8, 7, 6, 5, 4, 3, 2, 1), + equals(Matrix3(10, 10, 10, 10, 10, 10, 10, 10, 10)), + ); + }); + + test('subtraction subtracts corresponding elements', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9) - Matrix3(9, 8, 7, 6, 5, 4, 3, 2, 1), + equals(Matrix3(-8, -6, -4, -2, 0, 2, 4, 6, 8)), + ); + }); + + test('multiplication scales each element by a scalar', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9) * 2, + equals(Matrix3(2, 4, 6, 8, 10, 12, 14, 16, 18)), + ); + }); + + test('division scales each element by a scalar', () { + expect( + Matrix3(2, 4, 6, 8, 10, 12, 14, 16, 18) / 2, + equals(Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9)), + ); + }); + + test('multiplication matrix calculates product of two matrices', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9) + .multiply(Matrix3(9, 8, 7, 6, 5, 4, 3, 2, 1)), + equals(Matrix3(30, 24, 18, 84, 69, 54, 138, 114, 90)), + ); + }); + + test('trace calculates the sum of main diagonal', () { + expect(Matrix3(1, 2, 3, 0, 1, 4, 5, 6, 0).trace, equals(2)); + }); + + test('determinant calculates the determinant value', () { + expect(Matrix3(1, 2, 3, 0, 1, 4, 5, 6, 0).determinant, equals(1)); + }); + + test('transpose swaps rows and columns', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9).transpose(), + equals(Matrix3(1, 4, 7, 2, 5, 8, 3, 6, 9)), + ); + }); + + test('adjoint calculates the adjugate matrix', () { + expect( + Matrix3(1, 2, 3, 0, 1, 4, 5, 6, 0).adjoint(), + equals(Matrix3(-24, 18, 5, 20, -15, -4, -5, 4, 1)), + ); + }); + + test('invert calculates the inverse matrix', () { + expect( + Matrix3(1, 2, 3, 0, 1, 4, 5, 6, 0).invert(), + equals(Matrix3(-24, 18, 5, 20, -15, -4, -5, 4, 1)), + ); + }); + + test('apply function applies function to each element', () { + expect( + Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9).apply((x) => min(x * 2, 9)), + equals(Matrix3(2, 4, 6, 8, 9, 9, 9, 9, 9)), + ); + }); + + test('toQuaternion converts this rotation matrix to quaternion', () { + expect( + Matrix3(1, 0, 0, 0, -1, 0, 0, 0, -1).toQuaternion(), + equals(Quaternion(1, 0, 0, 0)), + ); + }); + + test('toEulerAngles converts this rotation matrix to Euler-angles', () { + expect( + Matrix3(1, 0, 0, 0, -1, 0, 0, 0, -1).toEulerAngles(), + closeToEulerAngles(EulerAngles(pi, 0, pi)), + ); + expect( + Matrix3( + 00.5403023, + 00.0000000, + 00.8414710, + 00.8414710, + 00.0000000, + -0.5403023, + 00.0000000, + 01.0000000, + 00.0000000, + ).toEulerAngles(), + closeToEulerAngles(EulerAngles(pi * 2 - 1, pi / 2, 0)), + ); + }); +} diff --git a/test/math/quaternion_test.dart b/test/math/quaternion_test.dart new file mode 100644 index 0000000..ba7aa52 --- /dev/null +++ b/test/math/quaternion_test.dart @@ -0,0 +1,127 @@ +import 'dart:math'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils.dart'; + +void main() { + test('constructor returns a quaternion with correct components', () { + final q = Quaternion(1, 2, 3, 4); + expect(q.x, equals(1)); + expect(q.y, equals(2)); + expect(q.z, equals(3)); + expect(q.w, equals(4)); + }); + + test('identity constructor returns a identity quaternion', () { + expect(Quaternion.identity(), equals(Quaternion(0, 0, 0, 1))); + }); + + test('equality and hashCode', () { + final q1 = Quaternion(1, 2, 3, 4); + final q2 = Quaternion(1, 2, 3, 4); + final q3 = Quaternion(2, 3, 4, 5); + expect(q1 == q2, isTrue); + expect(q1 == q3, isFalse); + expect(q1.hashCode == q2.hashCode, isTrue); + expect(q1.hashCode == q3.hashCode, isFalse); + }); + + test('toString returns the correct representation', () { + expect(Quaternion(1, 2, 3, 4).toString(), equals('1.0, 2.0, 3.0 @ 4.0')); + }); + + test('negation changes sign of each component', () { + expect(-Quaternion(1, 2, 3, 4), equals(Quaternion(-1, -2, -3, -4))); + }); + + test('addition sums corresponding components', () { + expect( + Quaternion(1, 2, 3, 4) + Quaternion(5, 6, 7, 8), + equals(Quaternion(6, 8, 10, 12)), + ); + }); + + test('subtraction subtracts corresponding components', () { + expect( + Quaternion(5, 6, 7, 8) - Quaternion(1, 2, 3, 4), + equals(Quaternion(4, 4, 4, 4)), + ); + }); + + test('multiplication scales each component by a scalar', () { + expect( + Quaternion(1, 2, 3, 4) * 2, + equals(Quaternion(2, 4, 6, 8)), + ); + }); + + test('division scales each component by a scalar', () { + expect( + Quaternion(2, 4, 6, 8) / 2, + equals(Quaternion(1, 2, 3, 4)), + ); + }); + + test('multiplication quaternion calculates product of two quaternions', () { + expect( + Quaternion(1, 2, 3, 4).multiply(Quaternion(5, 6, 7, 8)), + equals(Quaternion(24, 48, 48, -6)), + ); + }); + + test('length and length2 calculate quaternion magnitude and its square', () { + final q = Quaternion(1, 2, 3, 4); + expect(q.length2, closeTo(30, delta)); + expect(q.length, closeTo(5.4772256, delta)); + }); + + test('normalize scales quaternion to unit length', () { + expect( + Quaternion(1, 2, 3, 4).normalize(), + closeToQuaternion(Quaternion(0.1825742, 0.3651484, 0.5477226, 0.7302967)), + ); + }); + + test('conjugate', () { + expect( + Quaternion(1, 2, 3, 4).conjugate(), + equals(Quaternion(-1, -2, -3, 4)), + ); + }); + + test('invert calculates the inverse quaternion', () { + expect( + Quaternion(1, 2, 3, 4).invert(), + closeToQuaternion( + Quaternion(-0.0333333, -0.0666667, -0.1000000, 0.1333333), + ), + ); + }); + + test('apply function applies function to each component', () { + expect( + Quaternion(1, 2, 3, 4).apply((x) => min(x * 2, 5)), + equals(Quaternion(2, 4, 5, 5)), + ); + }); + + test('toAxisAngle converts quaternion to axis angle representation', () { + expect( + Quaternion(0, 0, sin(pi / 4), cos(pi / 4)).toAxisAngle(), + closeToAxisAngle(AxisAngle(Vector3(0, 0, 1), pi / 2)), + ); + expect( + Quaternion(0, 0, 0, 1).toAxisAngle(), + closeToAxisAngle(AxisAngle(Vector3(0, 0, 0), 0)), + ); + }); + + test('toRotationMatrix converts quaternion to rotation matrix', () { + expect( + Quaternion(1, 0, 0, 0).toRotationMatrix(), + equals(Matrix3(1, 0, 0, 0, -1, 0, 0, 0, -1)), + ); + }); +} diff --git a/test/math/vector3_test.dart b/test/math/vector3_test.dart new file mode 100644 index 0000000..0e71aca --- /dev/null +++ b/test/math/vector3_test.dart @@ -0,0 +1,80 @@ +import 'dart:math'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('constructor returns a vector with correct components', () { + final v = Vector3(1, 2, 3); + expect(v.x, 1); + expect(v.y, 2); + expect(v.z, 3); + }); + + test('zero constructor returns a zero vector', () { + expect(Vector3.zero(), equals(Vector3(0, 0, 0))); + }); + + test('equality and hashCode', () { + final v1 = Vector3(1, 2, 3); + final v2 = Vector3(1, 2, 3); + final v3 = Vector3(4, 5, 6); + expect(v1 == v2, isTrue); + expect(v1 == v3, isFalse); + expect(v1.hashCode == v2.hashCode, isTrue); + expect(v1.hashCode == v3.hashCode, isFalse); + }); + + test('toString returns the correct representation', () { + expect(Vector3(1, 2, 3).toString(), equals('[1.0,2.0,3.0]')); + }); + + test('negation changes sign of each component', () { + expect(-Vector3(1, 2, 3), equals(Vector3(-1, -2, -3))); + }); + + test('addition sums corresponding components', () { + expect(Vector3(1, 2, 3) + Vector3(4, 5, 6), equals(Vector3(5, 7, 9))); + }); + + test('subtraction subtracts corresponding components', () { + expect(Vector3(4, 5, 6) - Vector3(1, 2, 3), equals(Vector3(3, 3, 3))); + }); + + test('multiplication scales each component by a scalar', () { + expect(Vector3(1, 2, 3) * 2, equals(Vector3(2, 4, 6))); + }); + + test('division scales each component by a scalar', () { + expect(Vector3(4, 6, 8) / 2, equals(Vector3(2, 3, 4))); + }); + + test('dot product calculates scalar product of two vectors', () { + expect(Vector3(1, 2, 3).dot(Vector3(4, 5, 6)), equals(32)); + }); + + test('cross product calculates perpendicular vector', () { + expect(Vector3(1, 0, 0).cross(Vector3(0, 1, 0)), equals(Vector3(0, 0, 1))); + }); + + test('length and length2 calculate vector magnitude and its square', () { + final v = Vector3(3, 4, 0); + expect(v.length2, equals(25)); + expect(v.length, equals(5)); + }); + + test('normalize scales vector to unit length', () { + expect( + Vector3(3, 4, 0).normalize(), + equals( + // ignore: prefer_int_literals + Vector3(0.6, 0.8, 0.0), + ), + ); + expect(Vector3(0, 0, 0).normalize(), equals(Vector3(0, 0, 0))); + }); + + test('apply function applies function to each component', () { + expect(Vector3(1, 2, 3).apply((x) => min(x * 2, 5)), Vector3(2, 4, 5)); + }); +} diff --git a/test/orientation_event_test.dart b/test/orientation_event_test.dart new file mode 100644 index 0000000..7f640b2 --- /dev/null +++ b/test/orientation_event_test.dart @@ -0,0 +1,271 @@ +// ignore_for_file: prefer_int_literals + +import 'dart:math'; + +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'utils.dart'; + +const threshold = 0.000001; + +void main() { + final event1 = eventOf(00.0000000, 00.0000000, 00.0000000, 01.0000000); + final event2 = eventOf(00.4299807, 00.4374203, -0.5786997, 00.5374818); + + test( + 'rotationMatrix returns correct matrix for some known quaternions', + () { + expect( + event1.rotationMatrix, + closeToMatrix3( + matrix( + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, 01.0000000, 00.0000000], + [00.0000000, 00.0000000, 01.0000000], + ), + ), + ); + expect( + event2.rotationMatrix, + closeToMatrix3( + matrix( + [-0.0524597, 00.9982457, -0.0274485], + [-0.2459166, -0.0395536, -0.9684836], + [-0.9678704, -0.0440563, 00.2475601], + ), + ), + ); + }, + ); + + test('eulerAngles returns correct angles for some known quaternions', () { + expect( + event1.eulerAngles, + closeToEulerAngles(EulerAngles(00.0000000, 00.0000000, 00.0000000)), + ); + expect( + event2.eulerAngles, + closeToEulerAngles(EulerAngles(01.6103987, -0.0440705, 01.3203868)), + ); + final event = eventOf(00.7071068, 00.0000000, 00.0000000, 00.7071068); + final eulerAngles = event.eulerAngles; + expect(eulerAngles.pitch, closeTo(pi / 2, threshold)); + expect(eulerAngles.azimuth, closeTo(eulerAngles.roll, threshold)); + }); + + test('remapCoordinateSystem throws error with invalid axes', () { + expect( + () => event1.remapCoordinateSystem(Axis3.X, Axis3.X), + throwsUnsupportedError, + ); + expect( + () => event1.remapCoordinateSystem(Axis3.X, -Axis3.X), + throwsUnsupportedError, + ); + }); + + test( + 'remapCoordinateSystem returns a new OrientationEvent with transformed ' + 'coordinate system', + () { + final event1xy = event1.remapCoordinateSystem(Axis3.X, Axis3.Y); + expect( + event1xy.rotationMatrix, + closeToMatrix3( + matrix( + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, 01.0000000, 00.0000000], + [00.0000000, 00.0000000, 01.0000000], + ), + ), + ); + expect( + event1xy.coordinateSystem, + closeToMatrix3( + matrix( + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, 01.0000000, 00.0000000], + [00.0000000, 00.0000000, 01.0000000], + ), + ), + ); + + final event1xz = event1.remapCoordinateSystem(Axis3.X, Axis3.Z); + expect( + event1xz.rotationMatrix, + closeToMatrix3( + matrix( + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, 00.0000000, -1.0000000], + [00.0000000, 01.0000000, 00.0000000], + ), + ), + ); + expect( + event1xz.coordinateSystem, + closeToMatrix3( + matrix( + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, 00.0000000, -1.0000000], + [00.0000000, 01.0000000, 00.0000000], + ), + ), + ); + + final event1yZ = event1.remapCoordinateSystem(Axis3.Y, -Axis3.Z); + expect( + event1yZ.rotationMatrix, + closeToMatrix3( + matrix( + [00.0000000, 00.0000000, -1.0000000], + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, -1.0000000, 00.0000000], + ), + ), + ); + expect( + event1yZ.coordinateSystem, + closeToMatrix3( + matrix( + [00.0000000, 00.0000000, -1.0000000], + [01.0000000, 00.0000000, 00.0000000], + [00.0000000, -1.0000000, 00.0000000], + ), + ), + ); + + final event1YX = event1.remapCoordinateSystem(-Axis3.Y, -Axis3.X); + expect( + event1YX.rotationMatrix, + closeToMatrix3( + matrix( + [00.0000000, -1.0000000, 00.0000000], + [-1.0000000, 00.0000000, 00.0000000], + [00.0000000, 00.0000000, -1.0000000], + ), + ), + ); + expect( + event1YX.coordinateSystem, + closeToMatrix3( + matrix( + [00.0000000, -1.0000000, 00.0000000], + [-1.0000000, 00.0000000, 00.0000000], + [00.0000000, 00.0000000, -1.0000000], + ), + ), + ); + + expect( + event2.remapCoordinateSystem(Axis3.X, Axis3.Z).rotationMatrix, + closeToMatrix3( + matrix( + [-0.0524597, -0.0274485, -0.9982457], + [-0.2459166, -0.9684836, 00.0395536], + [-0.9678704, 00.2475601, 00.0440563], + ), + ), + ); + + expect( + event2.remapCoordinateSystem(Axis3.Y, -Axis3.Z).rotationMatrix, + closeToMatrix3( + matrix( + [00.9982457, 00.0274485, 00.0524597], + [-0.0395536, 00.9684836, 00.2459166], + [-0.0440563, -0.2475601, 00.9678704], + ), + ), + ); + + expect( + event2.remapCoordinateSystem(-Axis3.Y, -Axis3.X).rotationMatrix, + closeToMatrix3( + matrix( + [-0.9982457, 00.0524597, 00.0274485], + [00.0395536, 00.2459166, 00.9684836], + [00.0440563, 00.9678704, -0.2475601], + ), + ), + ); + }, + ); + + test( + 'should be equivalent to single remapping with different axes when ' + 'remapped twice consecutively', + () { + expect( + event2 + .remapCoordinateSystem(-Axis3.Y, Axis3.Z) + .remapCoordinateSystem(-Axis3.Y, Axis3.Z), + closeToOrientationEvent( + event2.remapCoordinateSystem(-Axis3.Z, -Axis3.X), + ), + ); + }, + ); + + test( + 'should result in a matrix close to the original rotation matrix ' + 'transposed after remapping and multiplying by inverted coordinate system', + () { + final remapped = event2.remapCoordinateSystem(-Axis3.Y, Axis3.Z); + expect( + remapped.rotationMatrix.multiply(remapped.coordinateSystem.invert()), + closeToMatrix3( + event2.rotationMatrix, + ), + ); + }, + ); + + test('toString return the correct representation', () { + expect( + event1.toString(), + equals(''' +OrientationEvent( +quaternion: 0.0, 0.0, 0.0 @ 1.0, +accuracy: -1.0, +timestamp: 0, +coordinateSystem: +⌈1.0,0.0,0.0⌉ +|0.0,1.0,0.0| +⌊0.0,0.0,1.0⌋ +)'''), + ); + expect( + event2.remapCoordinateSystem(-Axis3.Y, -Axis3.X).toString(), + equals(''' +OrientationEvent( +quaternion: -0.02914547175168991, -0.7892594933509827, -0.6133451461791992 @ 0.005260601174086332, +accuracy: -1.0, +timestamp: 0, +coordinateSystem: +⌈-0.0,-1.0,0.0⌉ +|-1.0,-0.0,0.0| +⌊-0.0,-0.0,-1.0⌋ +)'''), + ); + }); +} + +OrientationEvent eventOf(double x, double y, double z, double w) => + OrientationEvent( + quaternion: Quaternion(x, y, z, w).normalize(), + accuracy: -1, + timestamp: 0, + ); + +Matrix3 matrix(List row0, List row1, List row2) => + Matrix3( + row0[0], + row0[1], + row0[2], + row1[0], + row1[1], + row1[2], + row2[0], + row2[1], + row2[2], + ); diff --git a/test/rotation_sensor_method_channel_test.dart b/test/rotation_sensor_method_channel_test.dart new file mode 100644 index 0000000..f3d2a32 --- /dev/null +++ b/test/rotation_sensor_method_channel_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_rotation_sensor/src/rotation_sensor_method_channel.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final platform = RotationSensorMethodChannel(); + const methodChannel = RotationSensorMethodChannel.methodChannel; + const orientationChannel = RotationSensorMethodChannel.eventChannel; + late int expectedSamplingPeriod; + late ReferenceFrame expectedReferenceFrame; + + setUp(() { + expectedSamplingPeriod = platform.samplingPeriod.inMicroseconds; + expectedReferenceFrame = platform.referenceFrame; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (methodCall) async { + switch (methodCall.method) { + case 'getOrientationStream': + final arguments = methodCall.arguments as Map; + final samplingPeriod = arguments['samplingPeriod'] as int; + expect(samplingPeriod, expectedSamplingPeriod); + final referenceFrame = arguments['referenceFrame'] as String; + expect(referenceFrame, expectedReferenceFrame.name); + return null; + default: + throw UnsupportedError(methodCall.method); + } + }); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockStreamHandler( + orientationChannel, + MockStreamHandler.inline( + onListen: (args, sink) { + sink.success([ + // Quaternion + 0.0, 0.0, 0.0, 1.0, + // Accuracy + -1.0, + // Timestamp + 123456789, + ]); + }, + ), + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockStreamHandler(orientationChannel, null); + }); + + test( + 'orientationStream emits OrientationEvent with default sampling period', + () async { + expect(await platform.orientationStream.first, isA()); + }, + ); + + test( + 'orientationStream emits OrientationEvent with a replaced sampling period ' + 'when a reserved value is provided', + () async { + // samplingPeriod should be replaced with 0 since 1-3 is a reserved value + // for Android. + expectedSamplingPeriod = 0; + platform.samplingPeriod = const Duration(microseconds: 1); + expect(platform.samplingPeriod, equals(Duration.zero)); + await Future.microtask(() => null); + expect(await platform.orientationStream.first, isA()); + }, + ); + + test( + 'orientationStream forwards the configured reference frame to the platform', + () async { + expectedReferenceFrame = ReferenceFrame.magneticNorth; + platform.referenceFrame = ReferenceFrame.magneticNorth; + expect(platform.referenceFrame, equals(ReferenceFrame.magneticNorth)); + await Future.microtask(() => null); + expect(await platform.orientationStream.first, isA()); + }, + ); +} diff --git a/test/rotation_sensor_platform_test.dart b/test/rotation_sensor_platform_test.dart new file mode 100644 index 0000000..263a4ff --- /dev/null +++ b/test/rotation_sensor_platform_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_rotation_sensor/src/environment.dart'; +import 'package:flutter_rotation_sensor/src/rotation_sensor_method_channel.dart'; +import 'package:flutter_rotation_sensor/src/rotation_sensor_platform.dart'; +import 'package:flutter_rotation_sensor/src/rotation_sensor_unsupported.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const webImplementation = RotationSensorUnsupported; + +const implementations = { + TargetPlatform.android: RotationSensorMethodChannel, + TargetPlatform.iOS: RotationSensorMethodChannel, + TargetPlatform.fuchsia: RotationSensorUnsupported, + TargetPlatform.linux: RotationSensorUnsupported, + TargetPlatform.macOS: RotationSensorUnsupported, + TargetPlatform.windows: RotationSensorUnsupported, +}; + +void main() { + test('instance returns the implementation for current platform', () { + expect( + RotationSensorPlatform.instance.runtimeType, + equals(implementations[defaultTargetPlatform]), + ); + }); + + test('$webImplementation is used on web platform', () { + isWeb = true; + expect( + RotationSensorPlatform.createPlatformInstance().runtimeType, + equals(webImplementation), + ); + }); + + implementations.forEach((platform, implementation) { + test('$implementation is used on ${platform.name} platform', () { + isWeb = false; + debugDefaultTargetPlatformOverride = platform; + expect( + RotationSensorPlatform.createPlatformInstance().runtimeType, + equals(implementation), + ); + }); + }); +} diff --git a/test/rotation_sensor_test.dart b/test/rotation_sensor_test.dart new file mode 100644 index 0000000..3fb76c0 --- /dev/null +++ b/test/rotation_sensor_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_rotation_sensor/src/environment.dart'; +import 'package:flutter_rotation_sensor/src/rotation_sensor_platform.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockRotationSensorPlatform extends RotationSensorPlatform + with MockPlatformInterfaceMixin { + @override + Stream get orientationStream => + Stream.fromIterable([ + OrientationEvent( + quaternion: Quaternion.identity(), + accuracy: -1, + timestamp: 0, + ), + ]); +} + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + test('isPlatformSupported returns true only for Android and iOS platforms', () { + isWeb = false; + for (final platform in TargetPlatform.values) { + debugDefaultTargetPlatformOverride = platform; + expect(RotationSensor.isPlatformSupported, equals([ + TargetPlatform.android, + TargetPlatform.iOS, + ].contains(platform))); + } + }); + + test('isPlatformSupported returns false for web platform', () { + isWeb = true; + expect(RotationSensor.isPlatformSupported, isFalse); + }); + + test('orientationStream returns a stream of orientation events', () async { + var fakePlatform = MockRotationSensorPlatform(); + RotationSensorPlatform.instance = fakePlatform; + + expect( + await RotationSensor.orientationStream.first, + isA(), + ); + }); + + test('samplingPeriod return zero duration for reserved value', () { + for (var t = 0; t < 4; t++) { + RotationSensor.samplingPeriod = Duration(microseconds: t); + expect(RotationSensor.samplingPeriod, equals(Duration.zero)); + } + RotationSensor.samplingPeriod = SensorInterval.uiInterval; + expect(RotationSensor.samplingPeriod, equals(SensorInterval.uiInterval)); + }); + + test('coordinateSystem can be set and retrieved correctly', () { + RotationSensor.coordinateSystem = CoordinateSystem.display(); + expect(RotationSensor.coordinateSystem, same(CoordinateSystem.display())); + }); + + test('referenceFrame defaults to device', () { + RotationSensorPlatform.instance = MockRotationSensorPlatform(); + expect(RotationSensor.referenceFrame, equals(ReferenceFrame.device)); + }); + + test('referenceFrame can be set and retrieved correctly', () { + RotationSensor.referenceFrame = ReferenceFrame.magneticNorth; + expect(RotationSensor.referenceFrame, equals(ReferenceFrame.magneticNorth)); + RotationSensor.referenceFrame = ReferenceFrame.trueNorth; + expect(RotationSensor.referenceFrame, equals(ReferenceFrame.trueNorth)); + }); +} diff --git a/test/rotation_sensor_unsupported_test.dart b/test/rotation_sensor_unsupported_test.dart new file mode 100644 index 0000000..2a9354f --- /dev/null +++ b/test/rotation_sensor_unsupported_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_rotation_sensor/src/rotation_sensor_unsupported.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final platform = RotationSensorUnsupported(); + + test('orientationStream', () async { + expect(() => platform.orientationStream.first, throwsUnsupportedError); + }); +} diff --git a/test/utils.dart b/test/utils.dart new file mode 100644 index 0000000..f66efd8 --- /dev/null +++ b/test/utils.dart @@ -0,0 +1,59 @@ +import 'package:flutter_rotation_sensor/flutter_rotation_sensor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const delta = 1e-6; + +Matcher closeToVector3(Vector3 o, [num delta = delta]) => isA() + .having((v) => v.x, 'x', closeTo(o.x, delta)) + .having((v) => v.y, 'y', closeTo(o.y, delta)) + .having((v) => v.z, 'z', closeTo(o.z, delta)); + +Matcher closeToMatrix3(Matrix3 o, [num delta = delta]) => isA() + .having((m) => m.a, 'a', closeTo(o.a, delta)) + .having((m) => m.b, 'b', closeTo(o.b, delta)) + .having((m) => m.c, 'c', closeTo(o.c, delta)) + .having((m) => m.d, 'd', closeTo(o.d, delta)) + .having((m) => m.e, 'e', closeTo(o.e, delta)) + .having((m) => m.f, 'f', closeTo(o.f, delta)) + .having((m) => m.g, 'g', closeTo(o.g, delta)) + .having((m) => m.h, 'h', closeTo(o.h, delta)) + .having((m) => m.i, 'i', closeTo(o.i, delta)); + +Matcher closeToQuaternion(Quaternion o, [num delta = delta]) => + isA() + .having((q) => q.x, 'x', closeTo(o.x, delta)) + .having((q) => q.y, 'y', closeTo(o.y, delta)) + .having((q) => q.z, 'z', closeTo(o.z, delta)) + .having((q) => q.w, 'w', closeTo(o.w, delta)); + +Matcher closeToAxisAngle(AxisAngle o, [num delta = delta]) => isA() + .having((a) => a.axis, 'axis', closeToVector3(o.axis, delta)) + .having((a) => a.angle, 'angle', closeTo(o.angle, delta)); + +Matcher closeToEulerAngles(EulerAngles expected, [num delta = delta]) => + isA() + .having((ea) => ea.azimuth, 'azimuth', closeTo(expected.azimuth, delta)) + .having((ea) => ea.pitch, 'pitch', closeTo(expected.pitch, delta)) + .having((ea) => ea.roll, 'roll', closeTo(expected.roll, delta)); + +Matcher closeToOrientationEvent( + OrientationEvent expected, [ + num delta = delta, +]) => + isA() + .having( + (e) => e.quaternion, + 'quaternion', + closeToQuaternion(expected.quaternion, delta), + ) + .having( + (e) => e.accuracy, + 'accuracy', + closeTo(expected.accuracy, delta), + ) + .having( + (e) => e.coordinateSystem, + 'coordinateSystem', + equals(expected.coordinateSystem), + ) + .having((e) => e.timestamp, 'timestamp', equals(expected.timestamp));