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
+
+[](https://pub.dev/packages/flutter_rotation_sensor)
+[](https://github.com/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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ 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
+
+
+ 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));