@@ -11,71 +11,88 @@ import androidx.lifecycle.ViewModel
1111import dagger.hilt.android.lifecycle.HiltViewModel
1212import dagger.hilt.android.qualifiers.ApplicationContext
1313import javax.inject.Inject
14- import kotlin.math.exp
15- import kotlin.math.ln
14+ import kotlin.math.PI
1615
17- // This factor is used to convert the raw values emitted from device sensor to an appropriate scale for the consuming
18- // composables.
19- private const val ACCELERATION_FACTOR = - 0.1f
16+ /* *
17+ * This factor is used to convert the raw values emitted from device sensor to an appropriate scale for the consuming
18+ * composables. The range for pitch values is -π/2 to π/2, representing an upright pose and an upside-down pose,
19+ * respectively. E.g. Setting this factor to 0.2 / (π/2) will scale this output range to -0.2 to 0.2. The resulting
20+ * product is interpreted in units proportional to the repeating child composable's height per second, i.e. at a
21+ * velocity of 0.1, it will take 10 seconds for the looping animation to repeat. When applied, the product can also be
22+ * thought of as a frequency value in Hz.
23+ */
24+ private const val VELOCITY_FACTOR = (0.2 / (PI / 2 )).toFloat()
2025
21- // The maximum velocity (in either direction)
22- private const val MAXIMUM_VELOCITY = 0.125f
23-
24- // The velocity decay factor (i.e. 1/4th velocity after 1 second)
25- private val VELOCITY_DECAY = - ln(4f )
26-
27- // An additional acceleration applied to make the text scroll when the device is flat on a table
28- private const val DRIFT = - 0.05f
26+ /* *
27+ * This is the default pitch provided for devices lacking support for the sensors used. This is consumed by the model
28+ * as a value in radians, but is specified here as -30 degrees for convenience. This represents a device pose that is
29+ * slightly upright from flat, approximating a typical usage pattern, and will ensure that the text is animated at
30+ * an appropriate velocity when sensors are unavailable.
31+ */
32+ private const val DEFAULT_PITCH = (- 30 * PI / 180 ).toFloat()
2933
3034@HiltViewModel
3135class LoginPrologueRevampedViewModel @Inject constructor(
3236 @ApplicationContext appContext : Context ,
3337) : ViewModel() {
34- private var acceleration = - 0.3f // Default value when sensor is off
35- private var velocity = 0f
38+ private val accelerometerData = FloatArray (3 )
39+ private val magnetometerData = FloatArray (3 )
40+ private val rotationMatrix = FloatArray (9 )
41+ private val orientationAngles = floatArrayOf(0f , DEFAULT_PITCH , 0f )
3642 private var position = 0f
3743
3844 /* *
3945 * This function updates the physics model for the interactive animation by applying the elapsed time (in seconds)
4046 * to update the velocity and position.
4147 *
42- * * Velocity is constrained so that it does not fall below -MAXIMUM_VELOCITY and does not exceed MAXIMUM_VELOCITY.
48+ * * Velocity is calculated as proportional to the pitch angle
4349 * * Position is constrained so that it always falls between 0 and 1, and represents the relative vertical offset
4450 * in terms of the height of the repeated child composable.
4551 *
4652 * @param elapsed the elapsed time (in seconds) since the last frame
4753 */
4854 fun updateForFrame (elapsed : Float ) {
49- // Update the velocity, (decayed and clamped to the maximum)
50- velocity = (velocity * exp(elapsed * VELOCITY_DECAY ) + elapsed * acceleration)
51- .coerceIn(- MAXIMUM_VELOCITY , MAXIMUM_VELOCITY )
52- // Update the position, modulo 1 (ensuring a value greater or equal to 0, and less than 1)
53- position = ((position + elapsed * velocity) % 1 + 1 ) % 1
55+ orientationAngles.let { (_, pitch) ->
56+ val velocity = pitch * VELOCITY_FACTOR
5457
55- _positionData .postValue(position)
58+ // Update the position, modulo 1 (ensuring a value greater or equal to 0, and less than 1)
59+ position = ((position + elapsed * velocity) % 1 + 1 ) % 1
60+
61+ _positionData .postValue(position)
62+ }
5663 }
5764
58- /* * This LiveData responds to accelerometer data from the y-axis of the device and emits updated position data. */
65+ /* *
66+ * This LiveData responds to orientation data to calculate the pitch of the device. This is then used to update the
67+ * velocity and position for each frame.
68+ */
5969 private val _positionData = object : MutableLiveData <Float >(), SensorEventListener {
6070 private val sensorManager
6171 get() = appContext.getSystemService(Context .SENSOR_SERVICE ) as SensorManager
6272
6373 override fun onActive () {
6474 super .onActive()
65- val sensor = sensorManager.getDefaultSensor(Sensor .TYPE_ACCELEROMETER )
66- sensorManager.registerListener(this , sensor, SensorManager .SENSOR_DELAY_GAME )
75+ sensorManager.getDefaultSensor(Sensor .TYPE_ACCELEROMETER )?.also {
76+ sensorManager.registerListener( this , it, SensorManager .SENSOR_DELAY_GAME )
77+ }
78+ sensorManager.getDefaultSensor(Sensor .TYPE_MAGNETIC_FIELD )?.also {
79+ sensorManager.registerListener( this , it, SensorManager .SENSOR_DELAY_GAME )
80+ }
6781 }
6882
6983 override fun onInactive () {
7084 super .onInactive()
7185 sensorManager.unregisterListener(this )
7286 }
7387
74- override fun onSensorChanged (event : SensorEvent ? ) {
75- event?.values?. let { (_, yAxisAcceleration, _) ->
76- acceleration = yAxisAcceleration * ACCELERATION_FACTOR + DRIFT
77- postValue(position )
88+ override fun onSensorChanged (event : SensorEvent ) {
89+ when ( event.sensor.type) {
90+ Sensor . TYPE_ACCELEROMETER -> event.values.copyInto(accelerometerData)
91+ Sensor . TYPE_MAGNETIC_FIELD -> event.values.copyInto(magnetometerData )
7892 }
93+ // Update the orientation angles when sensor data is updated
94+ SensorManager .getRotationMatrix(rotationMatrix, null , accelerometerData, magnetometerData)
95+ SensorManager .getOrientation(rotationMatrix, orientationAngles)
7996 }
8097
8198 override fun onAccuracyChanged (sensor : Sensor ? , accuracy : Int ) = Unit
0 commit comments