Jetpack Compose를 사용한 간단한 애니메이션

1. 시작하기 전에

이 Codelab에서는 Android 앱에 간단한 애니메이션을 추가하는 방법을 알아봅니다. 애니메이션을 통해 보다 흥미롭고 사용자가 더 쉽게 해석할 수 있는 대화형 앱을 만들 수 있습니다. 정보가 가득 찬 화면에 개별 업데이트를 애니메이션으로 표시하면 사용자가 변경된 내용을 확인하는 데 도움이 됩니다.

앱 사용자 인터페이스에 사용할 수 있는 다양한 유형의 애니메이션이 있습니다. 항목은 나타났다가 사라지거나, 화면 안팎으로 이동하거나, 흥미로운 방식으로 변환할 수 있습니다. 이렇게 하면 앱의 UI를 표현력이 뛰어나고 사용하기 쉽게 만들 수 있습니다.

또한 애니메이션은 앱에 세련된 느낌을 더해주어 우아한 디자인과 분위기를 주면서 동시에 사용자에게 도움을 줍니다.

사용자에게 작업에 대한 보상을 제공하는 애니메이션은 사용자 경험의 중요한 부분을 더 의미 있게 만들 수 있습니다.

키패드 입력에 응답하는 애니메이션 요소는 피드백을 제공하여 작업의 성공 여부를 표시합니다.

애니메이션 목록 항목은 콘텐츠가 로드 중임을 전달하는 자리표시자입니다.

스와이프하여 열기 작업을 보여주는 애니메이션을 통해 필요한 동작을 유도합니다.

애니메이션 아이콘을 사용하면 아이콘을 재미있게 보완하거나 아이콘의 의미를 추가할 수 있습니다.

기본 요건

  • 함수, 람다, 스테이트리스(Stateless) 컴포저블을 비롯한 Kotlin 지식
  • Jetpack Compose에서 레이아웃을 빌드하는 방법에 관한 기본 지식
  • Jetpack Compose에서 목록을 만드는 방법에 관한 기본 지식
  • Material Design 관련 기본 지식

학습할 내용

  • Jetpack Compose로 간단한 스프링 애니메이션을 빌드하는 방법

빌드할 항목

  • 'Jetpack Compose를 사용한 Material Theming' Codelab의 Woof 앱을 기반으로 빌드하고 간단한 애니메이션을 추가해 사용자의 작업을 확인합니다.

필요한 항목

  • Android 스튜디오 최신 버전
  • 시작 코드를 다운로드하기 위한 인터넷 연결

2. 앱 개요

'Jetpack Compose를 사용한 Material Theming' Codelab에서 Material Design을 사용하여 반려견과 그 정보를 목록으로 표시하는 Woof 앱을 만들었습니다.

7252aa244a54ad90.png

이 Codelab에서는 Woof 앱에 애니메이션을 추가합니다. 취미 정보를 추가하면 목록 항목을 펼칠 때 표시됩니다. 또한 확장되는 목록 항목에 애니메이션을 적용하는 스프링 애니메이션을 추가합니다.

1e9cf1dbc490924a.gif

시작 코드 가져오기

시작하려면 시작 코드를 다운로드하세요.

또는 코드에 관한 GitHub 저장소를 클론해도 됩니다.

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

Woof app GitHub 저장소에서 코드를 찾아볼 수 있습니다.

3. 펼치기 아이콘 추가

스프링 애니메이션을 빌드하는 첫 번째 단계는 더 펼치기 3387908f9031c112.png 아이콘을 추가하는 것입니다. 펼치기 버튼 아이콘은 사용자가 목록 항목을 펼칠 수 있는 버튼을 제공합니다.

9fbd3fb0daf35fd3.png

아이콘

아이콘은 의도한 기능을 시각적으로 전달하여 사용자가 사용자 인터페이스를 이해하는 데 도움을 주는 기호입니다. 사용자가 경험했을 것으로 기대되는 실제 세상의 사물에서 아이콘의 아이디어를 얻는 경우가 많습니다. 아이콘 디자인은 종종 필요한 최소한의 수준으로 세부 표현을 줄여서 사용자에게 인식되도록 만듭니다. 예를 들어 실제 세상의 연필은 쓰기에 사용되므로 그 아이콘은 일반적으로 만들기 또는 수정을 나타냅니다.

사진: 안젤리나 리트빈(Unsplash 제공)

흑백 연필 아이콘

Material Design은 대부분의 요구에 부합하는 다수의 아이콘을 일반적인 카테고리로 정리하여 제공합니다.

BFDB896506790c69.png

Gradle 종속 항목 추가

프로젝트에 material-icons-extended 라이브러리 종속 항목을 추가합니다. 이 라이브러리의 Icons.Filled.ExpandLess 30c384f00846e69b.pngIcons.Filled.ExpandMore 3387908f9031c112.png 아이콘을 사용합니다.

  1. 프로젝트 창에서 Gradle Scripts > build.gradle (Module: Woof.app)을 엽니다.

f7fe58e936bbad3e.png

  1. build.gradle (Module: Woof.app) 파일 끝까지 스크롤합니다. dependencies{} 블록에 다음 줄을 추가합니다.
implementation "androidx.compose.material:material-icons-extended:$compose_version"

아이콘 컴포저블 추가

머티리얼 아이콘 라이브러리에서 펼치기 아이콘을 표시하고 버튼으로 사용할 함수를 추가합니다.

  1. MainActivity.kt에서 DogItem() 함수 뒤에 DogItemButton()이라는 구성 가능한 새 함수를 만듭니다.
  2. 펼쳐진 상태의 Boolean, 버튼 클릭 이벤트의 람다 표현식 및 다음과 같이 선택적 Modifier을 전달합니다.
@Composable
private fun DogItemButton(
    expanded: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {

}
  1. DogItemButton() 함수에서 이 아이콘을 누르면 호출되는 onClick이라는 매개변수(후행 람다 구문을 사용하는 람다)를 허용하는 IconButton() 컴포저블을 추가합니다. onClick 인수에 전달되었습니다.
@Composable
private fun DogItemButton(
   // ...
) {
   IconButton(onClick = onClick) {

   }
}
  1. IconButton() 람다 블록 내에서 imageVector라는 이름이 지정된 매개변수를 사용하여 Icon 컴포저블을 추가하고 Icons.Filled.ExpandMore로 설정합니다. 목록 항목 끝에 표시되는 아이콘 버튼 3387908f9031c112.png입니다. Android 스튜디오에서는 구성 가능한 Icon() 매개변수에 관한 경고를 표시하며, 이후 단계에서 수정합니다.
  2. 이름이 지정된 매개변수 tint를 추가하고 아이콘의 색상을 MaterialTheme.colors.secondary로 설정합니다. 이름이 지정된 매개변수 contentDescription을 추가하고 문자열 리소스 R.string.expand_button_content_description으로 설정합니다.
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore

IconButton(onClick = onClick) {
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       tint = MaterialTheme.colors.secondary,
       contentDescription = stringResource(R.string.expand_button_content_description)
   )
}

아이콘 표시

DogItemButton() 컴포저블을 레이아웃에 추가하여 표시합니다.

  1. DogItem() 구성 가능한 함수의 시작 부분에 var를 추가하여 목록 항목의 확장된 상태를 저장합니다. 초깃값을 false으로 설정합니다.
var expanded by remember { mutableStateOf(false) }
  1. DogItem() 구성 가능한 함수의 Row 블록 끝에서 DogItemButton() 함수를 호출한 다음 확장된 상태와 콜백에 빈 람다를 전달합니다. 이 코드는 목록 항목에 아이콘 버튼을 표시합니다.
  2. 목록 항목 내에 아이콘 버튼을 표시하려면 Row 블록 끝에 있는 구성 가능한 DogItem() 함수에서 DogInformation() 호출 후 DogItemButton()을 호출합니다. expanded 상태와 콜백의 빈 람다를 전달합니다. 이 람다 함수는 이후 단계에서 정의합니다.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. Design 창에서 미리보기를 빌드하고 새로고침합니다.

a49643f08701a8d.png

'펼치기' 버튼은 목록 항목의 끝에 정렬되지 않습니다. 다음 단계에서 이 문제를 해결합니다.

더보기 버튼 정렬

목록 항목의 끝부분에 펼치기 버튼을 정렬하려면 레이아웃에서 Modifier.weight() 속성을 사용하여 스페이서를 추가해야 합니다.

Woof 앱에서 각 목록 항목 행에는 반려견 이미지, 반려견 정보, 펼치기 버튼이 포함되어 있습니다. 가중치 1f를 사용하여 펼치기 버튼 앞에 스페이서 컴포저블을 추가하여 버튼 아이콘을 올바르게 정렬합니다. 스페이서는 행에서 가중치가 적용된 유일한 하위 요소이므로 가중치가 없는 다른 하위 요소의 길이를 측정한 후 행에 남아 있는 공간을 채웁니다.

412c34212b644f4f.png

목록 항목 행에 스페이서 추가하기

  1. DogItem() 구성 가능한 함수의 Row 블록 끝에 Spacer를 추가합니다. weight(1f)를 사용하여 Modifier를 전달합니다. Modifier.weight()를 사용하면 스페이서가 행의 나머지 공간을 채웁니다.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(Modifier.weight(1f))
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. Design 창에서 미리보기를 빌드하고 새로고침합니다. 이제 펼치기 버튼이 목록 항목의 끝에 정렬되는 것을 확인할 수 있습니다.

f6a140413de9ad54.png

4. 취미를 표시하기 위해 컴포저블 추가

이 작업에서는 반려견 취미 정보를 표시하는 Text 컴포저블을 추가합니다.

66ea5cc5c7253d55.png

  1. 새 구성 가능한 함수 만들기DogHobby() 반려견 취미 문자열 리소스 ID(선택사항)를 사용하는Modifier 가 있는지 진단합니다.
  2. DogHobby() 함수 내에서 다음 패딩 속성이 있는 열을 만들어 열과 하위 컴포저블 사이에 공간을 추가합니다.
import androidx.annotation.StringRes

@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
            top = 8.dp,
            bottom = 16.dp,
            end = 16.dp
       )
   ) { }
}
  1. 열 블록 내부에 Text 컴포저블을 추가합니다. 하나는 취미 정보 위에 About 텍스트를 표시하고 다른 하나는 취미 정보를 표시합니다.

6c26142b52c51285.png

  1. 정보 텍스트의 경우 스타일을 h3(제목 3)으로 설정하고 색상을 onBackground로 설정합니다. 취미 정보의 경우 스타일을 body1로 설정합니다.
Column(
   modifier = modifier.padding(
       //..
   )
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.h3,
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.body1,
   )
}
  1. 완성된 DogHobby() 구성 가능한 함수는 다음과 같습니다.
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
           top = 8.dp,
           bottom = 16.dp,
           end = 16.dp
       )
   ) {
       Text(
           text = stringResource(R.string.about),
           style = MaterialTheme.typography.h3
       )
       Text(
           text = stringResource(dogHobby),
           style = MaterialTheme.typography.body1
       )
   }
}
  1. DogHobby() 컴포저블을 표시하려면 DogItem()에서 RowColumn로 래핑합니다. DogHobby() 함수를 호출하여 dog.hobbies를 매개변수로 전달하고 Row 뒤에 두 번째 하위 요소로 전달합니다.
Column() {
   Row(
       //..
   ) {
       //..
   }
   DogHobby(dog.hobbies)
}

전체 DogItem() 함수는 다음과 같습니다.

@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
        elevation = 4.dp,
       modifier = modifier.padding(8.dp)
   ) {
       Column() {
           Row(
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { expanded = !expanded },
               )
           }
           DogHobby(dog.hobbies)
       }
   }
}
  1. Design 창에서 미리보기를 빌드하고 새로고침합니다. 반려견 취미가 표시됩니다.

9e2e68a4bc4a8ae1.png

5. 버튼 클릭 시 취미 표시 또는 숨기기

앱에는 모든 목록 항목에 펼치기 버튼이 있지만 이 버튼은 아직 아무런 기능을 하지 않습니다. 이 섹션에서는 사용자가 펼치기 버튼을 클릭할 때 취미 정보를 숨기거나 표시하는 옵션을 추가합니다.

  1. 구성 가능한 DogItem() 함수의 DogItemButton() 함수 호출에서 onClick() 람다 표현식을 정의하고 버튼을 클릭할 때 expanded 부울 상태 값을 true로 변경합니다. 버튼을 다시 클릭하면 false로 다시 변경합니다.
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. DogItemButton() 함수에서 expanded 부울을 확인하는 if 검사로 DogHobby() 함수 호출을 래핑합니다.
// No need to copy over
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       //..
   ) {
       Column() {
           Row(
               //..
           ) {
               //..
           }
           if (expanded) {
               DogHobby(dog.hobbies)
           }
       }
   }
}

위 코드에서 반려견의 취미 정보는 expanded 값이 true인 경우에만 표시됩니다.

  1. 미리보기를 통해 UI의 모양을 확인할 수 있으며 UI와 상호작용할 수도 있습니다. UI 미리보기와 상호작용하려면 Design 창의 오른쪽 상단에 있는 대화형 모드 버튼 42379dbe94a7a497.png을 클릭합니다. 그러면 미리보기가 대화형 모드로 시작됩니다.

2a4ad1f3d2d0bff7.png

  1. '펼치기' 버튼을 클릭하여 미리보기와 상호작용하기 '더보기' 버튼을 클릭하면 반려견 취미 정보가 숨겨지고 표시됩니다.

6ee6774b5b14c7e1.gif

목록 항목을 펼치면 더 펼치기 버튼 아이콘은 동일하게 유지됩니다. 더 나은 사용자 환경을 위해 ExpandMore에 아래쪽 화살표 C761ef298c2aea5a.png를 표시하고 ExpandLess에 위쪽 화살표 b380f933be0b6ff4.png를 표시하도록 아이콘을 변경합니다.

  1. DogItemButton() 함수에서 다음과 같이 expanded 상태를 기반으로 imageVector 값을 업데이트합니다.
import androidx.compose.material.icons.filled.ExpandLess

@Composable
private fun DogItemButton(
   //..
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           //..
       )
   }
}
  1. 기기 또는 에뮬레이터에서 앱을 실행하거나 미리보기에서 대화형 모드를 다시 사용합니다. 아이콘은 ExpandMore C761ef298c2aea5a.pngExpandLess b380f933be0b6ff4.png 간에 번갈아 표시됩니다.

CANNOT TRANSLATE

아이콘을 업데이트했습니다.

목록 항목을 펼치면 급격한 높이 변화를 확인하나요? 갑작스러운 높이 변경이 세련된 앱처럼 보이지는 않습니다. 이 문제를 해결하려면 다음으로 애니메이션을 추가하세요.

6. 애니메이션 추가

애니메이션을 사용하면 앱에 일어나고 있는 일을 사용자에게 알려주는 시각적 단서를 추가할 수 있습니다. 새 콘텐츠가 로드되거나 새 작업이 제공되는 경우와 같이 UI에서 상태가 변경되는 경우 특히 유용합니다. 또한 애니메이션을 사용하여 앱에 세련된 느낌을 줄 수도 있습니다.

이 섹션에서는 목록 항목의 높이 변화에 애니메이션을 적용하는 스프링 애니메이션을 추가합니다.

스프링 애니메이션

스프링 애니메이션스프링력에 기반한 물리학 기반 애니메이션입니다. 스프링 애니메이션에서는 적용된 스프링 포력을 기준으로 이동의 값과 속도가 계산됩니다.

예를 들어 화면 주위에서 앱 아이콘을 드래그한 다음 손가락을 떼면 아이콘이 보이지 않는 힘으로 원래 위치로 돌아갑니다.

다음 애니메이션은 스프링 효과를 보여줍니다. 아이콘에서 손가락을 떼면 아이콘이 스프링을 모방하며 뒤로 이동합니다.

7b52f63dc639c28d.gif

스프링 효과

스프링력은 다음 두 가지 속성을 기준으로 합니다.

  • 감쇠비: 스프링의 탄성입니다.
  • 강도 수준: 스프링의 강수 즉, 스프링이 끝까지 이동하는 속도입니다.

다음은 감쇠비와 강도 수준이 다른 애니메이션의 예입니다.

스프링 효과높은 반송율

스프링 효과이탈 없음

높은 강도

매우 낮은 강도

이제 앱에 스프링 애니메이션을 추가합니다.

  1. MainActivity.ktDogItem()에서 Column 레이아웃에 modifier 매개변수를 추가합니다.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
          modifier = Modifier
       ){
           //..
       }
   }
}

DogItem() 구성 가능한 함수에서 DogHobby() 함수 호출을 관찰합니다. 반려견 취미 정보는 expanded 부울 값에 따라 컴포지션에 포함됩니다. 목록 항목의 높이는 취미 정보의 표시 여부에 따라 달라집니다. animateContentSize 수정자를 사용하여 새 높이와 이전 높이 사이에 전환을 추가합니다.

// No need to copy over
@Composable
fun DogItem(...) {

        //..
           if (expanded) {
               DogHobby(dog.hobbies)
           }
}
  1. 수정자를 animateContentSize 수정자로 연결하여 크기 (목록 항목 높이) 변경을 애니메이션 처리합니다.
import androidx.compose.animation.animateContentSize

Column(
           modifier = Modifier
               .animateContentSize()
       ) {
            //..
       }

현재 구현에서는 앱의 목록 항목 높이를 애니메이션으로 보여줍니다. 그러나 애니메이션이 매우 미묘하여 앱을 실행할 때 파악하기 어렵습니다. 이 문제를 해결하려면 애니메이션 맞춤설정이 가능한 animationSpec 매개변수(선택사항)를 사용합니다.

  1. animationSpec 매개변수를 animateContentSize() 함수 호출에 추가합니다. DampingRatioMediumBouncyStiffnessLow 매개변수를 사용하여 스프링 애니메이션으로 설정합니다.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioMediumBouncy,
               stiffness = Spring.StiffnessLow
           )
       )
)
  1. Design 창에서 미리보기를 빌드 및 새로고침하고, 대화형 모드를 사용하거나 에뮬레이터 또는 기기에서 앱을 실행하여 스프링 애니메이션을 실제로 확인합니다.

8cf711b8821b4696.gif

에뮬레이터나 기기에서 앱을 다시 실행하고 애니메이션으로 멋진 앱을 즐기세요.

1e9cf1dbc490924a.gif

7. (선택사항) 다른 애니메이션 실험

animate*AsState

animate*AsState() 함수는 Compose에서 단일 값을 애니메이션 처리하는 가장 간단한 애니메이션 API 중 하나입니다. 최종 값(또는 타겟 값)만 제공하면 API가 현재 값에서 지정된 값으로 애니메이션을 시작합니다.

Compose는 Float, Color, Dp, Size, Offset, Intanimate*AsState() 함수를 제공합니다. 일반 유형을 취하는 animateValueAsState()를 사용하여 다른 데이터 유형의 지원 기능을 쉽게 추가할 수 있습니다.

목록 항목을 펼칠 때 animateColorAsState() 함수를 사용하여 색상에 애니메이션을 적용합니다.

힌트:

  1. 색상을 선언하고 초기화를 animateColorAsState() 함수에 위임합니다.
  2. expanded 부울 값에 따라 이름이 지정된 targetValue 매개변수를 설정합니다.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   val color by animateColorAsState(
       targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
   )
   Card(
       //..
   ) {...}
}
  1. 위에서 백그라운드 수정자로 선언한 colorColumn로 설정합니다.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   //..
                   )
               )
               .background(color = color)
       ) {...}
}

8. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 이 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git

또는, ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.

9. 결론

축하합니다. 반려견에 관한 정보를 숨기고 표시하는 버튼을 추가했습니다. 스프링 애니메이션을 사용하여 사용자 환경을 개선했습니다. Design 창에서 대화형 모드를 사용하는 방법도 배웠습니다.

다른 유형의 Jetpack Compose 애니메이션을 사용해 볼 수도 있습니다. #AndroidBasics를 사용해 작업한 결과물을 소셜 미디어로 공유해 보세요.

자세히 알아보기