Spring Physics로 움직임 애니메이션 처리

Compose 방법 사용해 보기
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 애니메이션을 사용하는 방법을 알아보세요.

물리학 기반 모션은 강제로 구동됩니다. 스프링력은 상호작용과 모션을 안내하는 힘입니다. 스프링력에는 감쇠 및 강성과 같은 속성이 있습니다. 스프링 기반 애니메이션에서 값과 속도는 각 프레임에 적용되는 스프링력을 기준으로 계산됩니다.

앱 애니메이션이 한 방향으로만 느려지게 하려면 마찰 기반 플링 애니메이션을 대신 사용해 보세요.

스프링 애니메이션의 수명주기

스프링 기반 애니메이션에서 SpringForce 클래스를 사용하면 스프링의 강성, 감쇠비 및 최종 위치를 맞춤설정할 수 있습니다. 애니메이션이 시작되는 즉시 스프링은 각 프레임의 애니메이션 값과 속도를 강제로 업데이트합니다. 애니메이션은 스프링력이 평형에 도달할 때까지 계속됩니다.

예를 들어 화면에서 앱 아이콘을 드래그한 후 나중에 아이콘에서 손가락을 떼면 보이지는 않지만 익숙한 힘에 아이콘이 원래 위치로 돌아갑니다.

그림 1에서는 유사한 스프링 효과를 보여줍니다. 원 중앙에 있는 더하기 기호 (+)는 터치 동작을 통해 적용되는 힘을 나타냅니다.

스프링 해제
그림 1. 스프링 해제 효과

스프링 애니메이션 빌드

애플리케이션의 스프링 애니메이션을 빌드하는 일반적인 단계는 다음과 같습니다.

다음 섹션에서는 스프링 애니메이션을 빌드하는 일반적인 단계를 자세히 설명합니다.

지원 라이브러리 추가

물리학 기반 지원 라이브러리를 사용하려면 다음과 같이 프로젝트에 지원 라이브러리를 추가해야 합니다.

  1. 앱 모듈의 build.gradle 파일 열기
  2. 지원 라이브러리를 dependencies 섹션에 추가합니다.

    Groovy

            dependencies {
                def dynamicanimation_version = '1.0.0'
                implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"
            }
            

    Kotlin

            dependencies {
                val dynamicanimation_version = "1.0.0"
                implementation("androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version")
            }
            

    이 라이브러리의 현재 버전을 보려면 버전 페이지에서 동적 애니메이션에 관한 정보를 확인하세요.

스프링 애니메이션 만들기

SpringAnimation 클래스를 사용하면 객체의 스프링 애니메이션을 만들 수 있습니다. 스프링 애니메이션을 빌드하려면 SpringAnimation 클래스의 인스턴스를 만들고 객체, 애니메이션을 적용할 객체의 속성, 애니메이션을 배치할 최종 스프링 위치(선택사항)를 제공해야 합니다.

참고: 스프링 애니메이션을 만들 때 스프링의 최종 위치는 선택사항입니다. 그러나 애니메이션을 시작하기 전에 정의해야 합니다.

Kotlin

val springAnim = findViewById<View>(R.id.imageView).let { img ->
    // Setting up a spring animation to animate the view’s translationY property with the final
    // spring position at 0.
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0f)
}

Java

final View img = findViewById(R.id.imageView);
// Setting up a spring animation to animate the view’s translationY property with the final
// spring position at 0.
final SpringAnimation springAnim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0);

스프링 기반 애니메이션은 뷰 객체의 실제 속성을 변경하여 화면의 뷰에 애니메이션을 적용할 수 있습니다. 시스템에서 사용할 수 있는 뷰는 다음과 같습니다.

  • ALPHA: 뷰의 알파 투명도를 나타냅니다. 값은 기본적으로 1 (불투명)이며 값 0은 완전 투명 (표시되지 않음)을 나타냅니다.
  • TRANSLATION_X, TRANSLATION_YTRANSLATION_Z: 이 속성은 레이아웃 컨테이너에 의해 설정된 왼쪽 좌표, 상단 좌표 및 고도로부터의 델타 값으로 뷰의 위치를 제어합니다.
  • ROTATION, ROTATION_XROTATION_Y: 이 속성은 중심점을 기준으로 2D (rotation 속성) 및 3D의 회전을 제어합니다.
  • SCROLL_XSCROLL_Y: 이 속성은 소스 왼쪽 및 상단 가장자리의 스크롤 오프셋을 픽셀 단위로 나타냅니다. 또한 페이지가 스크롤되는 정도 측면에서 위치를 나타냅니다.
  • SCALE_XSCALE_Y: 이 속성은 중심점을 기준으로 뷰의 2D 크기 조정을 제어합니다.
  • X, YZ: 컨테이너에서 뷰의 최종 위치를 설명하는 기본 유틸리티 속성입니다.

리스너 등록

DynamicAnimation 클래스는 OnAnimationUpdateListenerOnAnimationEndListener, 이렇게 두 개의 리스너를 제공합니다. 이러한 리스너는 애니메이션 값이 변경되거나 애니메이션이 끝날 때 애니메이션의 업데이트를 수신 대기합니다.

OnAnimationUpdateListener

여러 뷰에 애니메이션을 적용하여 체인으로 연결된 애니메이션을 만들려면 현재 뷰의 속성이 변경될 때마다 콜백을 수신하도록 OnAnimationUpdateListener를 설정하면 됩니다. 이 콜백은 현재 뷰의 속성에서 발생하는 변경에 따라 스프링 위치를 업데이트하도록 다른 뷰에 알립니다. 리스너를 등록하려면 다음 단계를 따르세요.

  1. addUpdateListener() 메서드를 호출하고 리스너를 애니메이션에 연결합니다.

    참고: 애니메이션이 시작되기 전에 업데이트 리스너를 등록해야 합니다. 그러나 업데이트 리스너는 애니메이션 값 변경 시 프레임당 업데이트가 필요한 경우에만 등록해야 합니다. 업데이트 리스너는 애니메이션이 별도의 스레드에서 실행되지 않도록 합니다.

  2. 호출자에게 현재 객체의 변경사항을 알리도록 onAnimationUpdate() 메서드를 재정의합니다. 다음 샘플 코드는 OnAnimationUpdateListener의 전반적인 사용을 보여줍니다.

Kotlin

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
val (anim1X, anim1Y) = findViewById<View>(R.id.view1).let { view1 ->
    SpringAnimation(view1, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view1, DynamicAnimation.TRANSLATION_Y)
}
val (anim2X, anim2Y) = findViewById<View>(R.id.view2).let { view2 ->
    SpringAnimation(view2, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view2, DynamicAnimation.TRANSLATION_Y)
}

// Registering the update listener
anim1X.addUpdateListener { _, value, _ ->
    // Overriding the method to notify view2 about the change in the view1’s property.
    anim2X.animateToFinalPosition(value)
}

anim1Y.addUpdateListener { _, value, _ -> anim2Y.animateToFinalPosition(value) }

Java

// Creating two views to demonstrate the registration of the update listener.
final View view1 = findViewById(R.id.view1);
final View view2 = findViewById(R.id.view2);

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
final SpringAnimation anim1X = new SpringAnimation(view1,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim1Y = new SpringAnimation(view1,
    DynamicAnimation.TRANSLATION_Y);
final SpringAnimation anim2X = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim2Y = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_Y);

// Registering the update listener
anim1X.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

// Overriding the method to notify view2 about the change in the view1’s property.
    @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2X.animateToFinalPosition(value);
    }
});

anim1Y.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

  @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2Y.animateToFinalPosition(value);
    }
});

OnAnimationEndListener

OnAnimationEndListener는 애니메이션의 끝을 알립니다. 애니메이션이 평형 상태가 되거나 취소될 때마다 콜백을 수신하도록 리스너를 설정할 수 있습니다. 리스너를 등록하려면 다음 단계를 따르세요.

  1. addEndListener() 메서드를 호출하고 리스너를 애니메이션에 연결합니다.
  2. 애니메이션이 평형 상태가 되거나 취소될 때마다 알림을 받도록 onAnimationEnd() 메서드를 재정의합니다.

리스너 삭제

애니메이션 업데이트 콜백과 애니메이션 종료 콜백 수신을 중지하려면 각각 removeUpdateListener()removeEndListener() 메서드를 호출합니다.

애니메이션 시작 값 설정

애니메이션의 시작 값을 설정하려면 setStartValue() 메서드를 호출하고 애니메이션의 시작 값을 전달합니다. 시작 값을 설정하지 않으면 애니메이션은 객체 속성의 현재 값을 시작 값으로 사용합니다.

애니메이션 값 범위 설정

속성 값을 특정 범위로 제한하려면 최소 및 최대 애니메이션 값을 설정할 수 있습니다. 또한 알파 (0에서 1)와 같은 고유 범위가 있는 속성에 애니메이션을 적용하는 경우 범위를 제어하는 데도 도움이 됩니다.

  • 최솟값을 설정하려면 setMinValue() 메서드를 호출하고 속성의 최솟값을 전달합니다.
  • 최댓값을 설정하려면 setMaxValue() 메서드를 호출하고 속성의 최댓값을 전달합니다.

두 메서드 모두 값이 설정되는 애니메이션을 반환합니다.

참고: 시작 값을 설정하고 애니메이션 값 범위를 정의한 경우 시작 값이 최솟값 및 최댓값 범위 내에 있는지 확인합니다.

시작 속도 설정

시작 속도는 애니메이션 시작 시 애니메이션 속성이 변경되는 속도를 정의합니다. 기본 시작 속도는 초당 0픽셀로 설정됩니다. 터치 동작의 속도를 사용하거나 고정 값을 시작 속도로 사용하여 속도를 설정할 수 있습니다. 고정 값을 제공하기로 선택한 경우 초당 dp 값을 정의한 다음 이를 초당 픽셀로 변환하는 것이 좋습니다. 초당 dp로 값을 정의하면 속도가 밀도 및 폼 팩터에 독립적일 수 있습니다. 값을 초당 픽셀로 변환하는 방법에 관한 자세한 내용은 초당 dp를 초당 픽셀로 변환 섹션을 참조하세요.

속도를 설정하려면 setStartVelocity() 메서드를 호출하고 속도를 초당 픽셀 단위로 전달합니다. 이 메서드는 속도가 설정된 스프링력 객체를 반환합니다.

참고: 터치 동작의 속도를 검색하고 계산하려면 GestureDetector.OnGestureListener 또는 VelocityTracker 클래스 메서드를 사용하세요.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Compute velocity in the unit pixel/second
        vt.computeCurrentVelocity(1000)
        val velocity = vt.yVelocity
        setStartVelocity(velocity)
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Compute velocity in the unit pixel/second
vt.computeCurrentVelocity(1000);
float velocity = vt.getYVelocity();
anim.setStartVelocity(velocity);

초당 dp를 초당 픽셀로 변환

스프링의 속도는 초당 픽셀 단위여야 합니다. 속도의 시작으로 고정 값을 제공하는 경우에는 초당 dp 값을 제공한 다음 초당 픽셀로 변환합니다. 변환하려면 TypedValue 클래스의 applyDimension() 메서드를 사용합니다. 다음 샘플 코드를 참조하세요.

Kotlin

val pixelPerSecond: Float =
    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, resources.displayMetrics)

Java

float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics());

스프링 속성 설정

SpringForce 클래스는 감쇠비 및 강성과 같은 각 스프링 속성의 getter 및 setter 메서드를 정의합니다. 스프링 속성을 설정하려면 스프링력 객체를 검색하거나 속성을 설정할 수 있는 맞춤 스프링력을 만드는 것이 중요합니다. 맞춤 스프링력 만들기에 관한 자세한 내용은 맞춤 스프링력 만들기 섹션을 참고하세요.

팁: 모든 setter 메서드가 스프링력 객체를 반환하므로, setter 메서드를 사용하는 동안 메서드 체인을 만들 수 있습니다.

감쇠비

감쇠비는 스프링 진동이 점진적으로 감소하는 것을 나타냅니다. 감쇠비를 사용하여 한 탄성에서 다음 탄성으로 진동이 얼마나 빠르게 감소하는지 정의할 수 있습니다. 스프링을 감쇠시키는 방법에는 네 가지가 있습니다.

  • 감쇠비가 1보다 크면 과도 감쇠가 발생합니다. 그러면 객체가 부드럽게 정지 위치로 돌아갑니다.
  • 임계 감쇠는 감쇠비가 1일 때 발생합니다. 이를 통해 최단 시간 내에 객체가 정지 위치로 돌아갈 수 있습니다.
  • 부족 감쇠는 감쇠비가 1 미만일 때 발생합니다. 그러면 객체가 여러 번 정지 위치를 지나친 다음 점진적으로 정지 위치에 도달합니다.
  • 비감쇠는 감쇠비가 0일 때 발생합니다. 그러면 객체가 영구적으로 진동할 수 있습니다.

스프링에 감쇠비를 추가하려면 다음 단계를 따르세요.

  1. getSpring() 메서드를 호출하여 감쇠비를 추가할 스프링을 가져옵니다.
  2. setDampingRatio() 메서드를 호출하고 스프링에 추가하려는 감쇠비를 전달합니다. 이 메서드는 감쇠비가 설정된 스프링력 객체를 반환합니다.

    참고: 감쇠비는 음수가 아닌 숫자여야 합니다. 감쇠비를 0으로 설정하면 스프링이 정지 위치에 도달하지 않습니다. 즉, 영구적으로 진동합니다.

시스템에서 사용할 수 있는 감쇠비 상수는 다음과 같습니다.

그림 2: 높은 탄성

그림 3: 중간 탄성

그림 4: 낮은 탄성

그림 5: 반동력 없음

기본 감쇠비는 DAMPING_RATIO_MEDIUM_BOUNCY로 설정됩니다.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the damping ratio to create a low bouncing effect.
        spring.dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the damping ratio to create a low bouncing effect.
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
…

강성

강성은 스프링의 강도를 측정하는 스프링 상수를 정의합니다. 고정 스프링은 스프링이 정지 위치에 있지 않을 때 부착된 객체에 더 많은 힘을 가합니다. 스프링에 강성을 추가하려면 다음 단계를 완료합니다.

  1. getSpring() 메서드를 호출하여 강성을 추가할 스프링을 검색합니다.
  2. setStiffness() 메서드를 호출하고 스프링에 추가하려는 강성 값을 전달합니다. 이 메서드는 강성이 설정된 스프링력 객체를 반환합니다.

    참고: 강성은 양수여야 합니다.

시스템에서 사용할 수 있는 강성 상수는 다음과 같습니다.

그림 6: 높은 강성

그림 7: 중간 강성

그림 8: 낮은 강성

그림 9: 매우 낮은 강성

기본 강성은 STIFFNESS_MEDIUM로 설정됩니다.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the spring with a low stiffness.
        spring.stiffness = SpringForce.STIFFNESS_LOW
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the spring with a low stiffness.
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW);
…

맞춤 스프링력 만들기

기본 스프링력을 사용하는 대신 맞춤 스프링력을 만들 수 있습니다. 맞춤 스프링력을 사용하면 여러 스프링 애니메이션에서 동일한 스프링력 인스턴스를 공유할 수 있습니다. 스프링력을 만든 후에는 감쇠비 및 강성과 같은 속성을 설정할 수 있습니다.

  1. SpringForce 객체를 만듭니다.

    SpringForce force = new SpringForce();

  2. 각 메서드를 호출하여 속성을 할당합니다. 메서드 체인을 만들 수도 있습니다.

    force.setDampingRatio(DAMPING_RATIO_LOW_BOUNCY).setStiffness(STIFFNESS_LOW);

  3. setSpring() 메서드를 호출하여 스프링을 애니메이션으로 설정합니다.

    setSpring(force);

애니메이션 시작

스프링 애니메이션을 시작하는 두 가지 방법이 있습니다. start()를 호출하는 방법과 animateToFinalPosition() 메서드를 호출하는 방법입니다. 두 메서드 모두 기본 스레드에서 호출해야 합니다.

animateToFinalPosition() 메서드는 다음 두 가지 작업을 실행합니다.

  • 스프링의 최종 위치를 설정합니다.
  • 애니메이션이 시작되지 않았으면 애니메이션을 시작합니다.

이 메서드는 스프링의 최종 위치를 업데이트하고 필요한 경우 애니메이션을 시작하므로 언제든지 이 메서드를 호출하여 애니메이션 과정을 변경할 수 있습니다. 예를 들어 연쇄 스프링 애니메이션에서 한 뷰의 애니메이션은 다른 뷰에 종속됩니다. 이러한 애니메이션의 경우 animateToFinalPosition() 메서드를 사용하는 것이 더 편리합니다. 연쇄 스프링 애니메이션에서 이 메서드를 사용하면 다음에 업데이트하려는 애니메이션이 현재 실행 중인지 걱정할 필요가 없습니다.

그림 10은 한 뷰의 애니메이션이 다른 뷰에 종속되는 연쇄 스프링 애니메이션을 보여줍니다.

연쇄 스프링 데모
그림 10. 연쇄 스프링 데모

animateToFinalPosition() 메서드를 사용하려면 animateToFinalPosition() 메서드를 호출하고 스프링의 정지 위치를 전달합니다. setFinalPosition() 메서드를 호출하여 스프링의 정지 위치를 설정할 수도 있습니다.

start() 메서드는 속성 값을 즉시 시작 값으로 설정하지 않습니다. 속성 값은 애니메이션이 깜빡일 때마다 변경되며 그리기 단계 전에 발생합니다. 따라서 값이 즉시 설정되는 것처럼 변경사항이 다음 프레임에 반영됩니다.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Starting the animation
        start()
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Starting the animation
anim.start();
…

애니메이션 취소

애니메이션을 취소하거나 애니메이션의 끝으로 건너뛸 수 있습니다. 애니메이션을 취소하거나 애니메이션의 끝으로 건너뛰어야 하는 이상적인 상황은 사용자 상호작용으로 애니메이션을 즉시 종료해야 하는 경우입니다. 주로 사용자가 앱을 갑자기 종료하거나 뷰가 보이지 않게 되는 경우입니다.

애니메이션을 종료하는 데 사용할 수 있는 메서드는 두 가지가 있습니다. cancel() 메서드는 현재 값에서 애니메이션을 종료합니다. skipToEnd() 메서드는 애니메이션을 최종 값으로 건너뛴 다음 종료합니다.

애니메이션을 종료하려면 먼저 스프링의 상태를 확인하는 것이 중요합니다. 상태가 감쇠되지 않으면 애니메이션은 정지 위치에 도달할 수 없습니다. 스프링의 상태를 확인하려면 canSkipToEnd() 메서드를 호출합니다. 스프링이 감쇠되면 메서드는 true를 반환하고 감쇠되지 않으면 false를 반환합니다.

스프링의 상태를 알면 skipToEnd() 메서드나 cancel() 메서드를 사용하여 애니메이션을 종료할 수 있습니다. cancel() 메서드는 기본 스레드에서만 호출해야 합니다.

참고: 일반적으로 skipToEnd() 메서드를 사용하면 시각적으로 점프합니다.