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