Tap it! Shake it! Fling it! Sheep it! The Gesture
Animations Dance! @KwakEuiJin KotlinConf’24 Korea
• 안드로이드 3년차 개발자 • SOPT IT 벤처창업 동아리 Android
파트장 곽의진(KEZ) github.com/KwakEuiJin
오늘의 발표는?
오늘의 발표는?
오늘의 발표는? Compose Multiplatform Animation With Gesture And Sensor
Jetpack Compose 란?
Jetpack Compose 란? 제 생일도 7월 28일 !!
간단하게 Compose Mulitplatform에 대해 알아보자!
우리는 오늘 무엇을 중점적으로 알아볼 것인가? Gestures Sensors Animations Data
애니메이션을 어떻게 만들 것 인가?
1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을
발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가?
Scale Translation Rotation Alpha 1. What: 무엇을 애니메이션화 할 것인가?
1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을
발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가?
2. When: 언제 애니메이션을 발생시킬 것인가? https://developer.android.com/develop/ui/compose/touch-input/pointer-input/drag-swipe-fling Drag Swife
1. What: 무엇을 애니메이션화 할 것인가? 2. When: 언제 애니메이션을
발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가?
3. How: 어떻게 애니메이션을 구현할 것인가? Animation Spec
Compose Animation APIs! 어떻게 애니메이션을 구현할 것인가?
애니메이션을 어떻게 만들 것 인가? 1. What: 무엇을 애니메이션화 할
것인가? 2. When: 언제 애니메이션을 발생시킬 것인가? 3. How: 어떻게 애니메이션을 구현할 것인가? Animation을 만들기 위한 3원칙
Gestures!
Tab To Scale! Step #1 1. What? 크기 2. When?
컴포저블이 터치되었을 때 3. How? 크기 1.0 <-> 1.2 Screen_recording_20240625_191219.gif
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 }
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 }
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?
잠깐!! graphicsLayer 만 짚고 넘어가보자 https://developer.android.com/develop/ui/compose/graphics/draw/modifiers#graphics-modifiers Composition Layout Drawing Drawing
명령어 변환
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 }
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!
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 }
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 }
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 }
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) } }
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) } }
Tab To Scale! // 3. How? onClick = { coroutineScope.launch
{ val newScale = if (scale.value == 1f) 1.2f else 1f scale.animateTo(newScale) } }
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 ) . . .
Animation Spec Spring Stiff KeyFrame Spring Nonstiff
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, ) ) . . .
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, ) ) . . .
Reposition (Drag) ! Step #1 1. What? • 위치(translation) 2.
When? • 컴포저블을 드래그 했을 때 3. How? • 새로운 위치로 이동
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)) } }
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)) } }
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)) } }
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)) } }
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)) } }
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)) } }
Fling and Back Step #1 Screen_recording_20240625_210341-ezgif.com- video-to-gif-converter.gif 1. What? •
위치(translation) 2. When? • 컴포저블을 던지는 드래그 모션이 발생했을 때 3. How? • 컴포저블이 위치를 되돌리거나 멈추기
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)) } }
Fling and Back // 2. When? ComposableKodee( modifier = Modifier
.draggable2D( state = draggableState, onDragStopped = { velocity -> doFlingMove(velocity) } ) )
Fling and Back // 3. How? val decay = rememberSplineBasedDecay<Offset>()
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) } }
Fling and Back // 3. How? val decay = rememberSplineBasedDecay<Offset>()
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
Fling and Back // 3. How? val decay = rememberSplineBasedDecay<Offset>()
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 ৈӝࢲ ݥԅ
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 } } }
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 } } }
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 } } }
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 } } }
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 } } }
Fling and Back // 3. How? val decay = rememberSplineBasedDecay<Offset>()
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 } } }
우리는 오늘 무엇을 중점적으로 알아볼 것인가? Gestures Sensors Animations Data
Sensors!
센서 활용하기 3. Sensor Event 처리하기 1.필요한 Sensor를 선택하기 2.
Sensor Data 구독하기
센서..??
센서 Android Sensor Manager iOS Sensor Manager Multiplatform Sensor Manager
MultiPlatform Sensor Manager interface MultiplatformSensorManager { fun registerListener( sensorType: MultiplatformSensorType,
onSensorChanged: (MultiplatformSensorEvent) -> Unit, ) fun unregisterAll() } @Composable expect fun rememberSensorManager(): MultiplatformSensorManager
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
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() { } }
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) } } }
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) } } }
Multiplatform Sensor Manager // iOS class iOSSensorManager : MultiplatformSensorManager {
override fun registerListener(. . .) { } override fun unregisterAll() { }
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() { }
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() { }
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 -> . . . } } }
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() ... } }
Rotation Shift 1. What? 회전 시키기 2. When? 디바이스의 방향이
변경 될 경우 3. How? 회전 Sensor의 value 0. Sensor Data이해하기 Orientation
Orientation - Android - Roll • Y 축 회전 •
-180°에서 180° - Azimuth • Z 축 회전 • 0°에서 360° - Pitch • X 축 회전 • -90°에서 90°
Orientation iOS Yaw • Z축 회전 • -180º에서 180º
Pitch • X축 회전 • -180º에서 180º Roll • Y축 회전 • -180º에서 180º
• -180º에서 180º - Pitch • X축 회전 • -180º에서 180º - Roll • Y축 회전 • -180º에서 180º - Azimuth • Z 축 회전 • 0°에서 360° - Pitch • X 축 회전 • -90°에서 90° - Roll • Y 축 회전 • -180°에서 180°
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() }
Orientation Shared Code data class DeviceOrientation() // Main interface
MultiplatformSensorManager { fun registerListener( sensorType: MultiplatformSensorType, onSensorChanged: (MultiplatformSensorEvent) -> Unit ) fun observeOrientationChanges( onOrientationChanged: (DeviceOrientation) -> Unit ) fun unregisterAll() }
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?
Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager()
sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }
Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager()
sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }
Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager()
sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }
Rotation Shift // 3. How? val sensorManager: MultiplatformSensorManager = rememberSensorManager()
sensorManager.observeOrientationChanges { orientation -> coroutineScope.launch { rotationX.snapTo() } coroutineScope.launch { rotationY.snapTo() } coroutineScope.launch { rotationZ.snapTo() } }
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) } ... }
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) } ... }
우리는 오늘 무엇을 중점적으로 알아볼 것인가? Gestures Sensors Animations Data
더 많은 것을 배우고 싶다면? • 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