크리스마스 이브
크리스마스 이브 날 아침입니다.
아침에 밖을 보니 눈이 내리는 것을 보았습니다...
이를 기념하여 SnowFall
effect
를 구현해보도록 하겠습니다.
SnowFall Effect
SnowFall Effect
는 화면에서 눈이 내리는 애니메이션 입니다.
이를 위해, 가장 필요한 것은 바로 눈
입니다.
눈
을 많이 생성하고, 화면 위에서 아래까지 떨어지도록 애니메이션을 만들어주면, 눈 내리는 모션을 제작할 수 있습니다.
완성 미리보기
snow Data Class
data class
Snow
를 생성해주겠습니다.
Snow
의 경우에는, 눈의 시작 위치 x,y 좌표
그리고 눈의 크기인 radius
마지막으로 떨어지는 속도를 담은 snow
가 정의된 data class
입니다.
data class Snow(
var x : Float,
var y : Float,
var radius : Float,
var speed : Float
)
변하는 Y값 설정하기
만들어진 Snow
객체 값에 변하는 Y
값과 Speed
를 더해주어 움직이는 모션을 만들어줄 수 있습니다.
애니메이션은 반복하여 재생할 것이므로, infinitiTransition
을 통해 무한한 애니메이션을 만들어주었습니다.
val offsetY by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = screenHeight,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 10000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
)
초기값은 0이며, targetValue
는 화면의 높이로 설정하였습니다.offsetY
는 durationMillis
동안 0f
에서 screenHeight
까지 이동하게 됩니다.
만약 빠른 애니메이션을 원한다면, durationMillis
값을 낮게 주면 더 빠른 애니메이션을 보여줄 수 있습니다.
repeatMode
의 경우에 Restart
와 Reverse
두 가지로, 원래 방향으로 반복과 반대로 움직이는 애니메이션 두 가지가 있습니다.
눈은 반대로 내리지 않으므로 ,Restart
로 설정하였습니다.
눈의 위치 랜덤 생성
그런데 Snow
의 각 값은 어떻게 설정을 해야할까요?
바로, Random
함수를 이용하여 초기 값을 모두 다르게 설정해줍니다.
눈의 x
좌표 그리고 y
좌표는 모두, Random.nextFloat()
를 통하여 0~1
사이의 값을 랜덤으로 생성해줍니다.
이는 나중에 눈을 그릴 때, Canvas
를 사용해서 그리게 될 예정인데, Canvas
의 DrawScope
의 경우에 Canvas
의 너비 그리고 높이 값을 제공해주므로, 0~1
사이의 값을 곱해주면, 화면의 랜덤
위치로 설정이 가능합니다.
radius
와 speed
의 경우에도 코드를 계속 실행해보면서 제 기준에 맞는 값을 설정해주었습니다.
private fun makeRandomSnow(screenHeight: Float) : Snow{
return Snow(
x = Random.nextFloat(),
y = Random.nextFloat() * screenHeight * -1f,
radius = Random.nextFloat() * 3f + 2f,
speed = Random.nextFloat() * 1.2f + 1f
)
}
스크린 높이 구하기
화면의 높이를 구하는 방법은
LocalConfiguration.current
를 통해 구할 수 있습니다.
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenHeight = with(density) { configuration.screenHeightDp.dp.toPx() }
LocalDensity
를 이용하여 화면의 밀도 정보를 가져오고,
가져온 스크린 높이의 Dp
단위를 Px
단위로 변경할 수 있습니다.
Canvas의 Draw 확장 함수 만들기
위에서 잠깐 간단하게 설명한 Canvas
의 확장 함수를 만들어보도록 하겠습니다.
Canvas
의 경우에는, DrawScope
를 통해 화면에 물체를 그릴 수 있습니다.
새로운 offsetY
의 위치는 기존 snow
의 y
값과, 기존 offsetY
값에 눈의 속도를 곱한 값으로 바꾸어 줍니다.
또한 눈이 땅에 닿으면 다시 눈을 다시 랜덤 위치로 배치해야 하기 때문에 size.height
보다 크면, snow.y
값을 랜덤으로 재생성 해주었습니다.
fun DrawScope.drawSnow(snow: Snow, offsetY: Float, screenHeight: Float) {
var newOffsetY = snow.y + offsetY * snow.speed
if (newOffsetY > size.height) {
snow.y = Random.nextFloat() * - screenHeight
newOffsetY = snow.y
}
drawCircle(Color.White, radius = snow.radius, center = Offset(snow.x * size.width, newOffsetY))
}
다음과 같이 drawCircle
을 통해 원 형태의 그림을 그려줍니다.
그리기
snow-effect
를 만드는 함수를 작성해줍니다.
가장 먼저 눈의 개수는 100개로 설정하고 처음에 작성한 랜덤 snow
객체를 생성하는 함수를 통해 100개의 눈을 만들어주겠습니다.
remember
를 통해 Compose
가 재구성 되어도 재생성되지 않도록 하였습니다
val snows = remember {
List(100) { makeRandomSnow(screenHeight) }
}
다음은 무한한 애니메이션을 관리하는 객체를 생성해줍니다.val infiniteTransition = rememberInfiniteTransition()
Canvas
를 통해 화면에 그려주었습니다.
Canvas(
modifier = Modifier
.fillMaxSize()
.background(gradient)
){
snows.forEach{ snow ->
drawSnow(snow,offsetY,screenHeight)
}
}
Canvas
의 배경의 경우에는 gradient
를 통해 화사한 이미지를 넣어주었습니다
val gradient = androidx.compose.ui.graphics.Brush.Companion.linearGradient(
colors = listOf(Color.White, Color.Blue.copy(alpha = 0.3f), Color.Red.copy(alpha = 0.3f)),
start = Offset.Zero,
end = Offset.Infinite
)
해결해야할 점
애니메이션이 처음 시작하고 다시 시작할 때 초기화되는 모션이 주어지는 데, 이는 나중에 시간이 남으면 해결해보고자 합니다.
전체 코드
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SnowfalleffectTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
androidx.compose.foundation.layout.Box(
modifier = Modifier
.fillMaxSize()
) {
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenHeight = with(density) { configuration.screenHeightDp.dp.toPx() }
com.shino72.snowfall_effect.SnowFallEffect(screenHeight = screenHeight)
}
} } } }
}
data class Snow(
var x : Float,
var y : Float,
var radius : Float,
var speed : Float
)
private fun makeRandomSnow(screenHeight: Float) : Snow{
return Snow(
x = Random.nextFloat(),
y = Random.nextFloat() * screenHeight * -1f,
radius = Random.nextFloat() * 3f + 2f,
speed = Random.nextFloat() * 1.2f + 1f
)
}
@Composable
private fun SnowFallEffect(modifier: Modifier = Modifier , screenHeight : Float) {
val snows = remember {
List(100) { makeRandomSnow(screenHeight) }
} val infiniteTransition = rememberInfiniteTransition()
val offsetY by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = screenHeight,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 10000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
)
val gradient = androidx.compose.ui.graphics.Brush.Companion.linearGradient(
colors = listOf(Color.White, Color.Blue.copy(alpha = 0.3f), Color.Red.copy(alpha = 0.3f)),
start = Offset.Zero,
end = Offset.Infinite
)
Canvas(
modifier = Modifier
.fillMaxSize()
.background(gradient)
){
snows.forEach{ snow ->
drawSnow(snow,offsetY,screenHeight)
}
}
}
fun DrawScope.drawSnow(snow: Snow, offsetY: Float, screenHeight: Float) {
var newOffsetY = snow.y + offsetY * snow.speed
if (newOffsetY > size.height) {
snow.y = Random.nextFloat() * - screenHeight
newOffsetY = snow.y
}
drawCircle(Color.White, radius = snow.radius, center = Offset(snow.x * size.width, newOffsetY))
}
깃허브
https://github.com/Myeongcheol-shin/snowfall-effect
'안드로이드 > Compose' 카테고리의 다른 글
[Kotlin/Compose] 슬라이드를 이용한 공 던지기 모션 만들기 (0) | 2023.12.23 |
---|---|
[Kotlin/Compose] 반응형 이미지 만들기 (2) | 2023.12.22 |
[Kotlin/Compose] Scroll Fade-In/Out animation (0) | 2023.12.22 |
[Kotlin/Compose] Slide-Unlock(밀어서 잠금해제) 기능 개발하기 (2) (0) | 2023.12.15 |
[Kotlin/Compose] Slide-Unlock(밀어서 잠금해제) 기능 개발하기 (1) (0) | 2023.12.15 |