Slide 1

Slide 1 text

Tap it! Shake it! Fling it! Sheep it! The Gesture Animations Dance! @KwakEuiJin KotlinConf’24 Korea

Slide 2

Slide 2 text

• 안드로이드 3년차 개발자 • SOPT IT 벤처창업 동아리 Android 파트장 곽의진(KEZ) github.com/KwakEuiJin

Slide 3

Slide 3 text

오늘의 발표는?

Slide 4

Slide 4 text

오늘의 발표는?

Slide 5

Slide 5 text

오늘의 발표는? Compose Multiplatform Animation With Gesture And Sensor

Slide 6

Slide 6 text

Jetpack Compose 란?

Slide 7

Slide 7 text

Jetpack Compose 란? 제 생일도 7월 28일 !!

Slide 8

Slide 8 text

간단하게 Compose Mulitplatform에 대해 알아보자!

Slide 9

Slide 9 text

우리는 오늘 무엇을 중점적으로 알아볼 것인가? Gestures Sensors Animations Data

Slide 10

Slide 10 text

애니메이션을 어떻게 만들 것 인가?

Slide 11

Slide 11 text

1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을 발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가?

Slide 12

Slide 12 text

Scale Translation Rotation Alpha 1. What: 무엇을 애니메이션화 할 것인가?

Slide 13

Slide 13 text

1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을 발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가?

Slide 14

Slide 14 text

2. When: 언제 애니메이션을 발생시킬 것인가? https://developer.android.com/develop/ui/compose/touch-input/pointer-input/drag-swipe-fling Drag Swife

Slide 15

Slide 15 text

1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을 발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가?

Slide 16

Slide 16 text

3. How: 어떻게 애니메이션을 구현할 것인가? Animation Spec

Slide 17

Slide 17 text

Compose Animation APIs! 어떻게 애니메이션을 구현할 것인가?

Slide 18

Slide 18 text

애니메이션을 어떻게 만들 것 인가? 1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을 발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가? Animation을 만들기 위한 3원칙

Slide 19

Slide 19 text

Gestures!

Slide 20

Slide 20 text

Tab To Scale! Step #1 1. What? 크기 2. When? 컴포저블이 터치되었을 때 3. How? 크기 1.0 <-> 1.2 Screen_recording_20240625_191219.gif

Slide 21

Slide 21 text

Tab To Scale! // 1. What? val scale = 1f // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale scaleY = scale } .clickable( onClick = { . . . }) . . . // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale }

Slide 22

Slide 22 text

Tab To Scale! // 1. What? val scale = 1f // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale scaleY = scale } .clickable( onClick = { . . . }) . . . // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale }

Slide 23

Slide 23 text

Tab To Scale! // 1. What? val scale = 1f // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale scaleY = scale } .clickable( onClick = { . . . }) . . . // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale } graphicsLayer?

Slide 24

Slide 24 text

잠깐!! graphicsLayer 만 짚고 넘어가보자 https://developer.android.com/develop/ui/compose/graphics/draw/modifiers#graphics-modifiers Composition Layout Drawing Drawing 명령어 변환

Slide 25

Slide 25 text

Tab To Scale! // 1. What? val scale = 1f // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale scaleY = scale } .clickable( onClick = { . . . }) . . . // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale }

Slide 26

Slide 26 text

Tab To Scale! // 1. What? val scale = 1f // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale scaleY = scale } .clickable( onClick = { . . . }) . . . // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale } No Animations!

Slide 27

Slide 27 text

Tab To Scale! // 1. What? val scale = 1f // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale.value scaleY = scale.value } .clickable(onClick = { . . . }) // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale }

Slide 28

Slide 28 text

Tab To Scale! // 1. What? val scale = remember { Animatable(1f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale.value scaleY = scale.value } .clickable(onClick = { . . . }) // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale }

Slide 29

Slide 29 text

Tab To Scale! // 1. What? val scale = remember { Animatable(1f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale.value scaleY = scale.value } .clickable(onClick = { . . . }) // 3. How? onClick = { val newScale = if (scale == 1f) 1.2f else 1f scale = newScale }

Slide 30

Slide 30 text

Tab To Scale! // 1. What? val scale = remember { Animatable(1f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale.value scaleY = scale.value } .clickable(onClick = { . . . }) // 3. How? onClick = { coroutineScope.launch { val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo(newScale) } }

Slide 31

Slide 31 text

Tab To Scale! // 1. What? val scale = remember { Animatable(1f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { // Important: set scale in graphicsLayer(!) scaleX = scale.value scaleY = scale.value } .clickable(onClick = { . . . }) // 3. How? onClick = { coroutineScope.launch { val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo(newScale) } }

Slide 32

Slide 32 text

Tab To Scale! // 3. How? onClick = { coroutineScope.launch { val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo(newScale) } }

Slide 33

Slide 33 text

Tab To Scale! // 3. How? onClick = { coroutineScope.launch { val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo( targetValue = newScale, animationSpec = // animation spec ) . . .

Slide 34

Slide 34 text

Animation Spec Spring ­ Stiff KeyFrame Spring ­ Nonstiff

Slide 35

Slide 35 text

Tab To Scale! // 3. How? onClick = { coroutineScope.launch { val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo( targetValue = newScale, animationSpec = spring( dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh, ) ) . . .

Slide 36

Slide 36 text

Tab To Scale! // 3. How? onClick = { coroutineScope.launch { val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo( targetValue = newScale, animationSpec = spring( dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh, ) ) . . .

Slide 37

Slide 37 text

Reposition (Drag) ! Step #1 1. What? • 위치(translation) 2. When? • 컴포저블을 드래그 했을 때 3. How? • 새로운 위치로 이동

Slide 38

Slide 38 text

Reposition (Drag) ! // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 39

Slide 39 text

Reposition (Drag) ! // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 40

Slide 40 text

Reposition (Drag) ! // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 41

Slide 41 text

Reposition (Drag) ! // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 42

Slide 42 text

Reposition (Drag) ! // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 43

Slide 43 text

Reposition (Drag) ! // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 44

Slide 44 text

Fling and Back Step #1 Screen_recording_20240625_210341-ezgif.com- video-to-gif-converter.gif 1. What? • 위치(translation) 2. When? • 컴포저블을 던지는 드래그 모션이 발생했을 때 3. How? • 컴포저블이 위치를 되돌리거나 멈추기

Slide 45

Slide 45 text

Fling and Back // 1. What? val translation = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { translationX = translation.value.x translationY = translation.value.y } .draggable2D(state = draggableState) ... ) // 3. How? val draggableState = rememberDraggable2DState { delta -> coroutineScope.launch { translation.snapTo(translation.value.plus(delta)) } }

Slide 46

Slide 46 text

Fling and Back // 2. When? ComposableKodee( modifier = Modifier .draggable2D( state = draggableState, onDragStopped = { velocity -> doFlingMove(velocity) } ) )

Slide 47

Slide 47 text

Fling and Back // 3. How? val decay = rememberSplineBasedDecay() fun doFlingMove(velocity: Velocity) { // 1. Calculate target offset based on velocity val velocityOffset = Offset(velocity.x / 2f, velocity.y / 2f) val targetOffset = decay.calculateTargetValue( typeConverter = Offset.VectorConverter, initialValue = translation.value, initialVelocity = velocityOffset, ) // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } }

Slide 48

Slide 48 text

Fling and Back // 3. How? val decay = rememberSplineBasedDecay() fun doFlingMove(velocity: Velocity) { // 1. Calculate target offset based on velocity val velocityOffset = Offset(velocity.x / 2f, velocity.y / 2f) val targetOffset = decay.calculateTargetValue( typeConverter = Offset.VectorConverter, initialValue = translation.value, initialVelocity = velocityOffset, ) // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } Decay Animation Spec

Slide 49

Slide 49 text

Fling and Back // 3. How? val decay = rememberSplineBasedDecay() fun doFlingMove(velocity: Velocity) { // 1. Calculate target offset based on velocity val velocityOffset = Offset(velocity.x / 2f, velocity.y / 2f) val targetOffset = decay.calculateTargetValue( typeConverter = Offset.VectorConverter, initialValue = translation.value, initialVelocity = velocityOffset, ) // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } Decay Animation Spec DBMDVMBUF5BSHFU7BMVF ৈӝࢲ ݥ୹ԅ

Slide 50

Slide 50 text

Fling and Back // 3. How? // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } else { coroutineScope.launch { val adjustedOffset = calculateBoundedOffset(targetOffset, screenSize) // Get farthest Offset translation.animateTo(adjustedOffset) // Animate to farthest point translation.animateTo(Offset(0f, 0f), ..) // Animate back to center } } }

Slide 51

Slide 51 text

Fling and Back // 3. How? // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } else { coroutineScope.launch { val adjustedOffset = calculateBoundedOffset(targetOffset, screenSize) // Get farthest Offset translation.animateTo(adjustedOffset) // Animate to farthest point translation.animateTo(Offset(0f, 0f), ..) // Animate back to center } } }

Slide 52

Slide 52 text

Fling and Back // 3. How? // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } else { coroutineScope.launch { val adjustedOffset = calculateBoundedOffset(targetOffset, screenSize) translation.animateTo(adjustedOffset) // Animate to farthest point translation.animateTo(Offset(0f, 0f), ..) // Animate back to center } } }

Slide 53

Slide 53 text

Fling and Back // 3. How? // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } else { coroutineScope.launch { val adjustedOffset = calculateBoundedOffset(targetOffset, screenSize) translation.animateTo(adjustedOffset) // Animate to farthest point translation.animateTo(Offset(0f, 0f), ..) // Animate back to center } } }

Slide 54

Slide 54 text

Fling and Back // 3. How? // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } else { // 3. If not, animate to farthest point within bounds and then animate back to center coroutineScope.launch { val adjustedOffset = calculateBoundedOffset(targetOffset, screenSize) translation.animateTo(adjustedOffset) // Animate to farthest point translation.animateTo(Offset(0f, 0f), ..) // Animate back to center } } }

Slide 55

Slide 55 text

Fling and Back // 3. How? val decay = rememberSplineBasedDecay() fun doFlingMove(velocity: Velocity) { // 1. Calculate target offset based on velocity val velocityOffset = Offset(velocity.x / 2f, velocity.y / 2f) val targetOffset = decay.calculateTargetValue( typeConverter = Offset.VectorConverter, initialValue = translation.value, initialVelocity = velocityOffset, ) // 2. If the target offset is within bounds, animate to it if (isTargetInBounds(targetOffset, screenSize)) { coroutineScope.launch { translation.animateDecay(velocityOffset, decay) } } else { // 3. If not, animate to farthest point within bounds and then animate back to center coroutineScope.launch { val adjustedOffset = calculateBoundedOffset(targetOffset, screenSize) translation.animateTo(adjustedOffset) // Animate to farthest point translation.animateTo(Offset(0f, 0f), ..) // Animate back to center } } }

Slide 56

Slide 56 text

우리는 오늘 무엇을 중점적으로 알아볼 것인가? Gestures Sensors Animations Data

Slide 57

Slide 57 text

Sensors!

Slide 58

Slide 58 text

센서 활용하기 3. Sensor Event 처리하기 1.필요한 Sensor를 선택하기 2. Sensor Data 구독하기

Slide 59

Slide 59 text

센서..??

Slide 60

Slide 60 text

센서 Android Sensor Manager iOS Sensor Manager Multiplatform Sensor Manager

Slide 61

Slide 61 text

MultiPlatform Sensor Manager interface MultiplatformSensorManager { fun registerListener( sensorType: MultiplatformSensorType, onSensorChanged: (MultiplatformSensorEvent) -> Unit, ) fun unregisterAll() } @Composable expect fun rememberSensorManager(): MultiplatformSensorManager

Slide 62

Slide 62 text

MultiPlatform Sensor Manager // Android @Composable actual fun rememberSensorManager(): MultiplatformSensorManager { val context = LocalContext.current return remember(context) { AndroidSensorManager(context) } } // iOS @Composable actual fun rememberSensorManager(): MultiplatformSensorManager { return remember { iOSSensorManager() } } @Composable expect fun rememberSensorManager(): MultiplatformSensorManager

Slide 63

Slide 63 text

Multiplatform Sensor Manager // Android class AndroidSensorManager(private val context: Context) : MultiplatformSensorManager { private val sensorManager by lazy { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager } override fun registerListener(. . .) { } override fun unregisterAll() { } }

Slide 64

Slide 64 text

Multiplatform Sensor Manager // Android class AndroidSensorManager(private val context: Context) : MultiplatformSensorManager { private val sensorManager by lazy { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager } override fun registerListener(...) { sensorManager.getDefaultSensor(sensorType.toSensorType())?.let { sensor -> val sensorEventListener = object : SensorEventListener {...} sensorManager.registerListener(sensorEventListener, sensor) } } override fun unregisterAll() { listeners.forEach { (_, listener) -> sensorManager.unregisterListener(listener) } } }

Slide 65

Slide 65 text

Multiplatform Sensor Manager // Android class AndroidSensorManager(private val context: Context) : MultiplatformSensorManager { private val sensorManager by lazy { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager } override fun registerListener(...) { sensorManager.getDefaultSensor(sensorType.toSensorType())?.let { sensor -> val sensorEventListener = object : SensorEventListener {...} sensorManager.registerListener(sensorEventListener, sensor) } } override fun unregisterAll() { listeners.forEach { (_, listener) -> sensorManager.unregisterListener(listener) } } }

Slide 66

Slide 66 text

Multiplatform Sensor Manager // iOS class iOSSensorManager : MultiplatformSensorManager { override fun registerListener(. . .) { } override fun unregisterAll() { }

Slide 67

Slide 67 text

Multiplatform Sensor Manager // iOS class iOSSensorManager : MultiplatformSensorManager { private val motionManager = CMMotionManager() private val activityManager = CMMotionActivityManager() private val pedometerManager = CMPedometer() override fun registerListener(. . .) { } override fun unregisterAll() { }

Slide 68

Slide 68 text

Multiplatform Sensor Manager // iOS class iOSSensorManager : MultiplatformSensorManager { private val motionManager = CMMotionManager() . . . override fun registerListener(. . .) { when (sensorType) { MultiplatformSensorType.ACCELEROMETER -> startAccelerometerUpdates(onSensorChanged) MultiplatformSensorType.STEP_COUNTER -> startPedometerUpdates(onSensorChanged) MultiplatformSensorType.STEP_DETECTOR -> startStepDetection(onSensorChanged) MultiplatformSensorType.LIGHT -> startLightUpdates(onSensorChanged) } } override fun unregisterAll() { }

Slide 69

Slide 69 text

Multiplatform Sensor Manager // iOS class iOSSensorManager : MultiplatformSensorManager { private val motionManager = CMMotionManager() . . . override fun registerListener(. . .) { when (sensorType) { MultiplatformSensorType.ACCELEROMETER -> startAccelerometerUpdates(onSensorChanged) . . . } } private fun startAccelerometerUpdates(onSensorChanged: (MultiplatformSensorEvent) -> Unit, ) { if (motionManager.isAccelerometerAvailable()) { motionManager.startAccelerometerUpdatesToQueue(. . .) { data, error -> . . . } } }

Slide 70

Slide 70 text

Multiplatform Sensor Manager class iOSSensorManager : MultiplatformSensorManager { private val motionManager = CMMotionManager() // For position related sensors private val activityManager = CMMotionActivityManager() // Detect activity (walking, driving, ...) private val pedometerManager = CMPedometer() // Step counter ... override fun registerListener(...) { when (sensorType) { MultiplatformSensorType.ACCELEROMETER -> startAccelerometerUpdates(onSensorChanged) ... } } override fun unregisterAll() { motionManager.stopDeviceMotionUpdates() motionManager.stopDeviceMotionUpdates() motionManager.stopGyroUpdates() motionManager.stopMagnetometerUpdates() motionManager.stopAccelerometerUpdates() pedometerManager.stopPedometerUpdates() pedometerManager.stopPedometerEventUpdates() activityManager.stopActivityUpdates() ... } }

Slide 71

Slide 71 text

Rotation Shift 1. What? 회전 시키기 2. When? 디바이스의 방향이 변경 될 경우 3. How? 회전 Sensor의 value 0. Sensor Data이해하기 Orientation

Slide 72

Slide 72 text

Orientation - Android - Roll • Y 축 회전 • -180°에서 180° - Azimuth • Z 축 회전 • 0°에서 360° - Pitch • X 축 회전 • -90°에서 90°

Slide 73

Slide 73 text

Orientation ­ iOS Yaw • Z축 회전 • -180º에서 180º Pitch • X축 회전 • -180º에서 180º Roll • Y축 회전 • -180º에서 180º

Slide 74

Slide 74 text

Orientation ­ Android && iOS - Yaw • Z축 회전 • -180º에서 180º - Pitch • X축 회전 • -180º에서 180º - Roll • Y축 회전 • -180º에서 180º - Azimuth • Z 축 회전 • 0°에서 360° - Pitch • X 축 회전 • -90°에서 90° - Roll • Y 축 회전 • -180°에서 180°

Slide 75

Slide 75 text

Orientation ­ Shared Code data class DeviceOrientation( val azimuth: Float, // Orientation[0] 0 to 360 val pitch: Float, // Orientation[1] -90 to 90 val roll: Float, // Orientation[2] -180 to 180 ) { val azimuthDegrees = azimuth.toDegrees() val pitchDegrees = pitch.toDegrees() val rollDegrees = roll.toDegrees() }

Slide 76

Slide 76 text

Orientation ­ Shared Code data class DeviceOrientation() // Main interface MultiplatformSensorManager { fun registerListener( sensorType: MultiplatformSensorType, onSensorChanged: (MultiplatformSensorEvent) -> Unit ) fun observeOrientationChanges( onOrientationChanged: (DeviceOrientation) -> Unit ) fun unregisterAll() }

Slide 77

Slide 77 text

Rotation Shift // 1. What? val rotationX = remember { Animatable(0f) } val rotationY = remember { Animatable(0f) } val rotationZ = remember { Animatable(0f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { this.rotationX = rotationX.value this.rotationY = rotationY.value this.rotationZ = rotationZ.value } ... ) // 3. How?

Slide 78

Slide 78 text

Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager() sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }

Slide 79

Slide 79 text

Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager() sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }

Slide 80

Slide 80 text

Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager() sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }

Slide 81

Slide 81 text

Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager() sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }

Slide 82

Slide 82 text

Rotation Shift // 1. What? val rotationX = remember { Animatable(0f) } val rotationY = remember { Animatable(0f) } val rotationZ = remember { Animatable(0f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { this.rotationX = rotationX.value this.rotationY = rotationY.value this.rotationZ = rotationZ.value } ... ) // 3. How? sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotation.snapTo(orientation.degrees) } ... }

Slide 83

Slide 83 text

Rotation Shift // 1. What? val rotationX = remember { Animatable(0f) } val rotationY = remember { Animatable(0f) } val rotationZ = remember { Animatable(0f) } // 2. When? ComposableKodee( modifier = Modifier .graphicsLayer { this.rotationX = rotationX.value this.rotationY = rotationY.value this.rotationZ = rotationZ.value } ... ) // 3. How? sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotation.snapTo(orientation.degrees) } ... }

Slide 84

Slide 84 text

우리는 오늘 무엇을 중점적으로 알아볼 것인가? Gestures Sensors Animations Data

Slide 85

Slide 85 text

더 많은 것을 배우고 싶다면? • Play around with the code! https://github.com/nicole-terc/sheepit-sensors- multiplatform • Rebecca Franks - Practical magic with animations in Jetpack Compose https://www.youtube.com/watch?v=HNSKJIQtb4c • Compose Animation Documentation https://developer.android.com/develop/ui/compose/animat ion/introduction • Android Sensor Documentation https://developer.android.com/develop/sensors-and- location/sensors/sensors_overview • iOS Sensor Documentation https://developer.apple.com/documentation/coremotion https://developer.apple.com/documentation/sensorkit https://github.com/nicole-terc/sheepit-sensors-multiplatform

Slide 86

Slide 86 text

Thank you