앱 사용 시 오래된 정보가 삭제되는 동안 새 정보가 화면에 표시되어야 합니다. 표시 내용이 빠르게 전환되면 부자연스럽게 보이거나 사용자가 화면에서 새 콘텐츠를 쉽게 놓칠 수 있습니다. 애니메이션을 사용하면 변화 속도가 느리고 움직임으로 사용자의 시선을 끌 수 있으므로 변경 사항을 더 분명하게 알 수 있습니다.
뷰를 표시하거나 숨길 때 흔히 사용하는 애니메이션에는 3가지가 있습니다. 회전 표시 애니메이션과 크로스페이드 애니메이션, 카드플립 애니메이션이 이에 해당합니다.
크로스페이드 애니메이션 만들기
크로스페이드 애니메이션(디졸브 애니메이션이라고도 함)은 하나의 View
또는 ViewGroup
을 점진적으로 페이드 아웃하는 동시에 다른 View 또는 ViewGroup을 페이드 인합니다. 이 애니메이션은 앱에서 콘텐츠 또는 뷰를 전환하려는 경우 유용합니다. 여기에 표시된 크로스페이드 애니메이션에서는 Android 3.1(API 수준 12) 이상에 사용 가능한 ViewPropertyAnimator
를 사용합니다.
다음은 진행률 표시기에서 텍스트 콘텐츠로 바뀌는 크로스페이드의 예입니다.
뷰 만들기
먼저, 크로스페이드할 뷰를 두 개 만들어야 합니다. 다음 예제에서는 진행률 표시기와 스크롤 가능한 텍스트 뷰를 만듭니다.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView style="?android:textAppearanceMedium"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lorem_ipsum"
android:padding="16dp" />
</ScrollView>
<ProgressBar android:id="@+id/loading_spinner"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
크로스페이드 애니메이션 설정
크로스페이드 애니메이션을 설정하는 방법은 다음과 같습니다.
- 크로스페이드할 뷰의 멤버 변수를 만들어야 합니다. 이러한 참조는 나중에 애니메이션 중에 뷰를 수정할 때 필요합니다.
- 페이드 인되는 뷰의 가시성을
GONE
으로 설정합니다. 이렇게 하면 뷰가 레이아웃 공간을 차지하지 않고 레이아웃 계산에서 생략되어 처리 속도가 빨라집니다. - 멤버 변수에서
시스템 속성을 캐시합니다. 이 속성은 애니메이션의 '짧은' 표준 재생 시간을 정의합니다. 이러한 재생 시간은 섬세한 애니메이션 또는 자주 발생하는 애니메이션에 이상적입니다. 원한다면config_shortAnimTime
config_longAnimTime
및config_mediumAnimTime
도 사용할 수 있습니다.
다음은 이전 코드 스니펫에서 레이아웃을 활동 콘텐츠 뷰로 사용하는 예제입니다.
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_crossfade) contentView = findViewById(R.id.content) loadingView = findViewById(R.id.loading_spinner) // Initially hide the content view. contentView.visibility = View.GONE // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) } ... }
자바
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crossfade); contentView = findViewById(R.id.content); loadingView = findViewById(R.id.loading_spinner); // Initially hide the content view. contentView.setVisibility(View.GONE); // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime); } ... }
뷰 크로스페이드
이제 뷰가 적절히 설정되었으므로 다음 단계를 통해 뷰를 크로스페이드합니다.
- 페이드 인되는 뷰의 알파 값을
0
으로 설정하고 가시성을VISIBLE
로 설정합니다. (처음에는GONE
으로 설정되었습니다.) 이렇게 하면 뷰가 표시되기는 하지만 완전히 투명한 상태입니다. - 페이드 인되는 뷰는 알파 값을
0
에서1
로 애니메이션합니다. 페이드 아웃되는 뷰는 알파 값을1
에서0
으로 애니메이션합니다. Animator.AnimatorListener
에서onAnimationEnd()
를 사용하여 페이드 아웃된 뷰의 가시성을GONE
으로 설정합니다. 알파 값이0
이더라도 뷰의 가시성을GONE
으로 설정하면 뷰가 레이아웃 공간을 차지하지 않고 레이아웃 계산에서 생략되어 처리 속도가 빨라집니다.
다음은 이 작업을 어떻게 실행하는지 보여주는 예제입니다.
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... private fun crossfade() { contentView.apply { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. alpha = 0f visibility = View.VISIBLE // Animate the content view to 100% opacity, and clear any animation // listener set on the view. animate() .alpha(1f) .setDuration(shortAnimationDuration.toLong()) .setListener(null) } // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step (it won't // participate in layout passes, etc.) loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration.toLong()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loadingView.visibility = View.GONE } }) } }
자바
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... private void crossfade() { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. contentView.setAlpha(0f); contentView.setVisibility(View.VISIBLE); // Animate the content view to 100% opacity, and clear any animation // listener set on the view. contentView.animate() .alpha(1f) .setDuration(shortAnimationDuration) .setListener(null); // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step (it won't // participate in layout passes, etc.) loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { loadingView.setVisibility(View.GONE); } }); } }
카드플립 애니메이션 만들기
카드플립은 뒤집히는 카드를 에뮬레이션하는 애니메이션을 표시하여 콘텐츠 뷰 간에 애니메이션합니다. 여기에 표시된 카드플립 애니메이션에서는 Android 3.0(API 수준 11) 이상에서 사용할 수 있는 FragmentTransaction
을 사용합니다.
다음은 카드플립 애니메이션입니다.
Animator 개체 만들기
카드플립 애니메이션을 만들려면 총 4개의 애니메이터가 필요합니다. 두 개의 애니메이터는 카드 앞면이 애니메이션되어 왼쪽으로 뒤집혔다가 왼쪽에서 넘어오도록 하는 데 필요합니다. 카드 뒷면이 애니메이션되어 오른쪽에서 넘어왔다가 오른쪽으로 뒤집히도록 하는 데에도 두 개의 애니메이터가 필요합니다.
card_flip_left_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="-180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_left_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="-180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
뷰 만들기
'카드'의 각 면은 원하는 콘텐츠를 포함할 수 있는 별개의 레이아웃입니다. 예를 들면 두 개의 텍스트 뷰, 두 개의 이미지 등 서로 전환할 수 있는 다양한 뷰 조합 같은 콘텐츠를 포함할 수 있습니다. 두 개의 레이아웃은 나중에 애니메이션할 프래그먼트에 사용됩니다. 다음 레이아웃은 텍스트를 보여주는 카드의 한 면을 만듭니다.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#a6c"
android:padding="16dp"
android:gravity="bottom">
<TextView android:id="@android:id/text1"
style="?android:textAppearanceLarge"
android:textStyle="bold"
android:textColor="#fff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_title" />
<TextView style="?android:textAppearanceSmall"
android:textAllCaps="true"
android:textColor="#80ffffff"
android:textStyle="bold"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_description" />
</LinearLayout>
다음은 ImageView
를 보여주는 카드의 다른 한 면을 만듭니다.
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/image1"
android:scaleType="centerCrop"
android:contentDescription="@string/description_image_1" />
프래그먼트 만들기
카드의 앞면과 뒷면을 위한 프래그먼트 클래스를 만듭니다. 이러한 클래스는 각 프래그먼트의 onCreateView()
메서드에서 이전에 만든 레이아웃을 반환합니다. 그러면 카드를 표시하고자 하는 상위 활동에서 이 프래그먼트의 인스턴스를 만들 수 있습니다. 다음 예제에서는 상위 활동 내에 중첩된 프래그먼트 클래스를 보여줍니다.
Kotlin
class CardFlipActivity : FragmentActivity() { ... /** * A fragment representing the front of the card. */ class CardFrontFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_front, container, false) } /** * A fragment representing the back of the card. */ class CardBackFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_back, container, false) } }
자바
public class CardFlipActivity extends FragmentActivity { ... /** * A fragment representing the front of the card. */ public class CardFrontFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_front, container, false); } } /** * A fragment representing the back of the card. */ public class CardBackFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_back, container, false); } } }
카드 플립 애니메이션
이제 상위 활동 내에 있는 프래그먼트를 표시해야 합니다.
이를 위해 활동에 관한 레이아웃을 먼저 만듭니다. 다음 예제에서는 런타임에 프래그먼트를 추가할 수 있는 FrameLayout
을 만듭니다.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
활동 코드에서 콘텐츠 뷰를 방금 만든 레이아웃으로 설정합니다. 또한 활동이 생성되면 기본 프래그먼트를 표시하는 것이 좋습니다. 이런 이유로 다음 예제 활동에서는 기본적으로 카드 앞면을 표시하는 방법을 보여줍니다.
Kotlin
class CardFlipActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_activity_card_flip) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .add(R.id.container, CardFrontFragment()) .commit() } } ... }
자바
public class CardFlipActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_activity_card_flip); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() .add(R.id.container, new CardFrontFragment()) .commit(); } } ... }
이제 카드 앞면이 표시되었으므로 적절한 시간에 플립 애니메이션으로 카드 뒷면을 표시할 수 있습니다. 카드의 다른 면을 보여주는 메서드를 만듭니다. 이 메서드는 다음 작업을 합니다.
- 프래그먼트 전환을 위해 이전에 만든 맞춤 애니메이션을 설정합니다.
- 현재 표시된 프래그먼트를 새 프래그먼트로 바꾸고 이 이벤트를 이전에 만든 맞춤 애니메이션으로 애니메이션합니다.
- 이전에 표시된 프래그먼트를 프래그먼트 백 스택에 추가합니다. 그러면 사용자가 뒤로 단추를 누르면 카드가 다시 뒤집힙니다.
Kotlin
class CardFlipActivity : FragmentActivity() { ... private fun flipCard() { if (showingBack) { supportFragmentManager.popBackStack() return } // Flip to the back. showingBack = true // Create and commit a new fragment transaction that adds the fragment for // the back of the card, uses custom animations, and is part of the fragment // manager's back stack. supportFragmentManager.beginTransaction() // Replace the default fragment animations with animator resources // representing rotations when switching to the back of the card, as // well as animator resources representing rotations when flipping // back to the front (e.g. when the system Back button is pressed). .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out ) // Replace any fragments currently in the container view with a // fragment representing the next page (indicated by the // just-incremented currentPage variable). .replace(R.id.container, CardBackFragment()) // Add this transaction to the back stack, allowing users to press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit() } }
자바
public class CardFlipActivity extends FragmentActivity { ... private void flipCard() { if (showingBack) { getSupportFragmentManager().popBackStack(); return; } // Flip to the back. showingBack = true; // Create and commit a new fragment transaction that adds the fragment for // the back of the card, uses custom animations, and is part of the fragment // manager's back stack. getSupportFragmentManager() .beginTransaction() // Replace the default fragment animations with animator resources // representing rotations when switching to the back of the card, as // well as animator resources representing rotations when flipping // back to the front (e.g. when the system Back button is pressed). .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out) // Replace any fragments currently in the container view with a // fragment representing the next page (indicated by the // just-incremented currentPage variable). .replace(R.id.container, new CardBackFragment()) // Add this transaction to the back stack, allowing users to press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit(); } }
회전 표시 애니메이션 만들기
표시 애니메이션은 UI 요소 그룹을 표시하거나 숨길 때 사용자에게 시각적 연속성을 제공합니다. ViewAnimationUtils.createCircularReveal()
메서드를 사용하면 뷰를 표시하거나 숨기도록 클리핑 서클을 애니메이션할 수 있습니다. 이 애니메이션은 Android 5.0(API 수준 21) 이상에서 사용할 수 있는 ViewAnimationUtils
클래스에 제공되어 있습니다.
다음은 이전에 보이지 않았던 뷰를 표시하는 방법을 보여주는 예제입니다.
Kotlin
// previously invisible view val myView: View = findViewById(R.id.my_view) // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle val cx = myView.width / 2 val cy = myView.height / 2 // get the final radius for the clipping circle val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // create the animator for this view (the start radius is zero) val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius) // make the view visible and start the animation myView.visibility = View.VISIBLE anim.start() } else { // set the view to invisible without a circular reveal animation below Lollipop myView.visibility = View.INVISIBLE }
자바
// previously invisible view View myView = findViewById(R.id.my_view); // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // get the final radius for the clipping circle float finalRadius = (float) Math.hypot(cx, cy); // create the animator for this view (the start radius is zero) Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius); // make the view visible and start the animation myView.setVisibility(View.VISIBLE); anim.start(); } else { // set the view to invisible without a circular reveal animation below Lollipop myView.setVisibility(View.INVISIBLE); }
ViewAnimationUtils.createCircularReveal()
애니메이션에는 5개의 매개변수가 사용됩니다. 첫 번째 매개변수는 화면에서 숨기거나 표시하려는 뷰입니다. 그다음 두 개의 매개변수는 클리핑 서클 중심에 대한 x, y 좌표입니다. 일반적으로 이것이 뷰의 중심이 되지만, 사용자가 터치한 지점도 사용할 수 있습니다. 이 경우 사용자가 선택한 위치에서 애니메이션이 시작됩니다. 네 번째 매개변수는 클리핑 서클의 시작 반지름입니다.
위 예제에서 초기 반지름이 0으로 설정되었기 때문에 뷰가 서클에 숨겨집니다. 마지막 매개변수는 서클의 최종 반지름입니다. 뷰를 표시할 때 최종 반지름이 뷰 자체보다 커야 합니다. 그래야 애니메이션이 끝나기 전에 뷰가 완전히 표시됩니다.
이전에 표시된 뷰를 숨기려면 다음을 수행합니다.
Kotlin
// previously visible view val myView: View = findViewById(R.id.my_view) // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle val cx = myView.width / 2 val cy = myView.height / 2 // get the initial radius for the clipping circle val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // create the animation (the final radius is zero) val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f) // make the view invisible when the animation is done anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) myView.visibility = View.INVISIBLE } }) // start the animation anim.start() } else { // set the view to visible without a circular reveal animation below Lollipop myView.visibility = View.VISIBLE }
자바
// previously visible view final View myView = findViewById(R.id.my_view); // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // get the initial radius for the clipping circle float initialRadius = (float) Math.hypot(cx, cy); // create the animation (the final radius is zero) Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f); // make the view invisible when the animation is done anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); myView.setVisibility(View.INVISIBLE); } }); // start the animation anim.start(); } else { // set the view to visible without a circular reveal animation below Lollipop myView.setVisibility(View.VISIBLE); }
이 경우 클리핑 서클의 초기 반지름 크기가 뷰와 같도록 설정되었기 때문에 애니메이션이 시작되기 전에 뷰가 표시됩니다. 최종 반지름이 0으로 설정되어 애니메이션이 끝나면 뷰가 숨겨집니다. 애니메이션이 완료되면 뷰의 가시성을 INVISIBLE
로 설정할 수 있도록 애니메이션에 리스너를 추가하는 것이 중요합니다.