• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.torus.utils.interaction
18 
19 import android.content.Context
20 import android.hardware.Sensor
21 import android.hardware.SensorEvent
22 import android.hardware.SensorEventListener
23 import android.hardware.SensorManager
24 import android.util.Log
25 import android.view.Surface
26 import com.google.android.torus.math.MathUtils
27 import com.google.android.torus.math.Vector2
28 import kotlin.math.abs
29 import kotlin.math.sign
30 
31 /**
32  * Class that analyzed the gyroscope and generates a rotation out of it.
33  * This class only calculates the gyroscope rotation for two angles/degrees:
34  * - Pitch (rotation around device X axis).
35  * - Yaw (rotation around device Y axis).
36  *
37  * (Check https://developer.android.com/guide/topics/sensors/sensors_motion for more info).
38  */
39 class Gyro2dController(context: Context, config: GyroConfig = GyroConfig()) {
40     companion object {
41         private const val TAG = "Gyro2dController"
42         const val NANOS_TO_S = 1.0f / 1_000_000_000.0f
43         const val RAD_TO_DEG = (180f / Math.PI).toFloat()
44         const val BASE_FPS = 60f
45         const val DEFAULT_EASING = 0.8f
46     }
47 
48     /**
49      * Defines the final rotation.
50      *
51      * - [Vector2.x] represents the Pitch (in degrees).
52      * - [Vector2.y] represents the Yaw (in degrees).
53      */
54     var rotation: Vector2 = Vector2()
55         private set
56 
57     /**
58      * Defines if gyro is considered to be settled.
59      * TODO: remove once clients are switched to the new |isCurrentlySettled(Vector2)| API.
60      */
61     var isSettled: Boolean = false
62         private set
63 
64     /**
65      * Defines whether the gyro animation is almost settled.
66      * TODO: remove once clients are switched to the new |isNearlySettled(Vector2)| API.
67      */
68     var isAlmostSettled: Boolean = false
69         private set
70 
71     /**
72      * The config that defines the behavior of the gyro..
73      */
74     var config: GyroConfig = config
75         set(value) {
76             field = value
77             onNewConfig()
78         }
79 
80     private val angles: FloatArray = FloatArray(3)
81     private val sensorEventListener: SensorEventListener = object : SensorEventListener {
onAccuracyChangednull82         override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
83         }
84 
onSensorChangednull85         override fun onSensorChanged(event: SensorEvent) {
86             updateGyroRotation(event)
87         }
88     }
89     private val displayRotationValues: IntArray =
90         intArrayOf(
91             Surface.ROTATION_0,
92             Surface.ROTATION_90,
93             Surface.ROTATION_180,
94             Surface.ROTATION_270
95         )
96     private val sensorManager: SensorManager =
97         context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
98     private val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
99     private var displayRotation: Int = displayRotationValues[0]
100     private var timestamp: Float = 0f
101     private var recenter: Boolean = false
102     private var recenterMul: Float = 1f
103     private var ease: Boolean = true
104     private var gyroSensorRegistered: Boolean = false
105 
106     // Speed per frame, based on 60FPS.
107     private var easingMul: Float = DEFAULT_EASING * BASE_FPS
108 
109     init {
110         onNewConfig()
111     }
112 
113     /**
114      * Starts listening for gyroscope events.
115      * (the rotation is also reset).
116      */
startnull117     fun start() {
118         gyroSensor?.let {
119             sensorManager.registerListener(
120                 sensorEventListener,
121                 it,
122                 SensorManager.SENSOR_DELAY_GAME
123             )
124 
125             gyroSensorRegistered = true
126         }
127 
128         if (gyroSensor == null) Log.w(
129             TAG,
130             "SensorManager could not find a default TYPE_GYROSCOPE sensor"
131         )
132     }
133 
134     /**
135      * Stops listening for the gyroscope events.
136      * (the rotation is also reset).
137      */
stopnull138     fun stop() {
139         if (gyroSensorRegistered) {
140             sensorManager.unregisterListener(sensorEventListener)
141             gyroSensorRegistered = false
142         }
143     }
144 
145     /**
146      * Resets the rotation values.
147      */
resetValuesnull148     fun resetValues() {
149         rotation = Vector2()
150         angles[0] = 0f
151         angles[1] = 0f
152     }
153 
154     /**
155      * Updates the output rotation (mostly it is used the update and ease the rotation value based
156      * on the [Gyro2dController.GyroConfig.easingSpeed] value).
157      *
158      * @param deltaSeconds the time in seconds elapsed since the last time
159      * [Gyro2dController.update] was called.
160      */
updatenull161     fun update(deltaSeconds: Float) {
162         /*
163          * Ease if needed (specially to reduce movement variation which will allow us to use a
164          * smaller fps).
165          */
166         rotation = if (ease) {
167             Vector2(
168                 MathUtils.lerp(rotation.x, angles[0], easingMul * deltaSeconds),
169                 MathUtils.lerp(rotation.y, angles[1], easingMul * deltaSeconds)
170             )
171         } else {
172             Vector2(angles[0], angles[1])
173         }
174 
175         isSettled = isCurrentlySettled()
176         isAlmostSettled = isNearlySettled()
177     }
178 
179     /**
180      * Call it to change how the gyro sensor is interpreted. This function is specially important
181      * when the display is not being presented in its default orientation (by default
182      * [Gyro2dController] will read the gyro values as if the device is in its default orientation).
183      *
184      * @param displayRotation The current display rotation. It can only be one of the following
185      * values: [Surface.ROTATION_0], [Surface.ROTATION_90], [Surface.ROTATION_180] or
186      * [Surface.ROTATION_270].
187      */
setDisplayRotationnull188     fun setDisplayRotation(displayRotation: Int) {
189         if (displayRotation !in displayRotationValues) {
190             throwDisplayRotationException(displayRotation)
191         }
192 
193         this.displayRotation = displayRotation
194     }
195 
196     /**
197      * Determine whether the gyro orientation is considered to be "settled" and unexpected to change
198      * in the near future. If a non-null [referenceRotation] is provided, then the gyro also won't
199      * be considered "settled" if the current or (expected) future state is too far from the
200      * reference. For example, clients can provide the value of our [rotation] at the time that they
201      * last presented that state to the user, to determine if that reference value is now too far
202      * behind.
203      */
isCurrentlySettlednull204     fun isCurrentlySettled(referenceRotation: Vector2 = rotation): Boolean =
205         (getErrorDistance(referenceRotation) < config.settledThreshold)
206 
207     /** Like [isCurrentlySettled], but with a wider tolerance. */
208     fun isNearlySettled(referenceRotation: Vector2 = rotation): Boolean =
209         (getErrorDistance(referenceRotation) < config.almostSettledThreshold)
210 
211     /**
212      * Determine the amount of recent-or-expected angular rotation given our sensor values and
213      * easing state as documented for [isCurrentlySettled]. This is a signal for how frequently we
214      * should update based on gyro activity.
215      */
216     private fun getErrorDistance(referenceRotation: Vector2 = rotation): Float {
217         val targetOrientation = Vector2(angles[0], angles[1])
218 
219         // Have we now updated to a state far from the last one we presented?
220         val distanceFromReferenceToCurrent = referenceRotation.distanceTo(rotation)
221 
222         // Did our last frame have a long way to go to get to our current target?
223         val distanceFromReferenceToTarget = referenceRotation.distanceTo(targetOrientation)
224 
225         // Are we *currently* far from the target? Note we may often expect the current value to be
226         // somewhere *between* the target and the last-rendered rotation as each frame gets closer
227         // to the target, but it's actually possible for the target to move between updates such
228         // that the "current" value falls outside of the range.
229         val distanceFromCurrentToTarget = rotation.distanceTo(targetOrientation)
230 
231         return maxOf(
232             distanceFromReferenceToCurrent,
233             distanceFromReferenceToTarget,
234             distanceFromCurrentToTarget
235         )
236     }
237 
updateGyroRotationnull238     private fun updateGyroRotation(event: SensorEvent) {
239         if (timestamp != 0f) {
240             val dT = (event.timestamp - timestamp) * NANOS_TO_S
241             // Adjust based on display rotation.
242             var axisX: Float = when (displayRotation) {
243                 Surface.ROTATION_90 -> -event.values[1]
244                 Surface.ROTATION_180 -> -event.values[0]
245                 Surface.ROTATION_270 -> event.values[1]
246                 else -> event.values[0]
247             }
248 
249             var axisY: Float = when (displayRotation) {
250                 Surface.ROTATION_90 -> event.values[0]
251                 Surface.ROTATION_180 -> -event.values[1]
252                 Surface.ROTATION_270 -> -event.values[0]
253                 else -> event.values[1]
254             }
255 
256             axisX *= RAD_TO_DEG * dT * config.intensity
257             axisY *= RAD_TO_DEG * dT * config.intensity
258 
259             angles[0] = updateAngle(angles[0], axisX, config.maxAngleRotation.x)
260             angles[1] = updateAngle(angles[1], axisY, config.maxAngleRotation.y)
261         }
262 
263         timestamp = event.timestamp.toFloat()
264     }
265 
updateAnglenull266     private fun updateAngle(angle: Float, deltaAngle: Float, maxAngle: Float): Float {
267         // Adds incremental value.
268         var angleCombined = angle + deltaAngle
269 
270         // Clamps to maxAngleRotation x and maxAngleRotation y.
271         if (abs(angleCombined) > maxAngle) angleCombined = maxAngle * sign(angleCombined)
272 
273         // Re-centers to origin if needed.
274         if (recenter) angleCombined *= recenterMul
275 
276         return angleCombined
277     }
278 
throwDisplayRotationExceptionnull279     private fun throwDisplayRotationException(displayRotation: Int) {
280         throw IllegalArgumentException(
281             "setDisplayRotation only accepts Surface.ROTATION_0 (0), " +
282                     "Surface.ROTATION_90 (1), Surface.ROTATION_180 (2) or \n" +
283                     "[Surface.ROTATION_270 (3); Instead the value was $displayRotation."
284         )
285     }
286 
onNewConfignull287     private fun onNewConfig() {
288         recenter = config.recenterSpeed > 0f
289         recenterMul = 1f - MathUtils.clamp(config.recenterSpeed, 0f, 1f)
290         ease = config.easingSpeed < 1f
291         easingMul = MathUtils.clamp(config.easingSpeed, 0f, 1f) * BASE_FPS
292     }
293 
294     /**
295      * Class that contains the config attributes for the gyro.
296      */
297     data class GyroConfig(
298         /**
299          * Adjusts the maximum output rotation (in degrees) for both positive and negative angles,
300          * for each direction (x for the rotation around the X axis, y for the rotation
301          * around the Y axis).
302          *
303          * i.e. if [maxAngleRotation] = (2, 4), the output rotation would be inside
304          * ([-2º, 2º], [-4º, 4º]).
305          */
306         val maxAngleRotation: Vector2 = Vector2(2f),
307 
308         /**
309          * Adjusts how much movement we need to apply to the device to make it rotate. This value
310          * multiplies the original rotation values; thus if the value is < 1f, we would need to
311          * rotate more the device than the actual rotation; if it is 1 it would be the default
312          * phone rotation; if it is > 1f it will magnify the rotation.
313          */
314         val intensity: Float = 0.05f,
315 
316         /**
317          * Adjusts how much the end rotation is eased. This value can be from range [0, 1].
318          * - When 0, the eased value won't change.
319          * - when 1, there isn't any easing.
320          */
321         val easingSpeed: Float = 0.8f,
322 
323         /**
324          * How fast we want the rotation to recenter besides the gyro values.
325          * - When 0, it doesn't recenter.
326          * - when 1, it would make the rotation be in the center all the time.
327          */
328         val recenterSpeed: Float = 0f,
329 
330         /**
331          * The minimum frame-over-frame delta required between gyroscope readings
332          * (by L2 distance in the rotation angles) in order to consider the device to be settled
333          * in a given animation frame.
334          */
335         val settledThreshold: Float = 0.0005f,
336 
337         /**
338          * The minimum frame-over-frame delta required between the target orientation and the
339          * current orientation, in order to define if the orientation is almost settled.
340          */
341         val almostSettledThreshold: Float = 0.01f
342     )
343 }
344