アプリの使用中は、新しい情報が画面に表示され、古い情報は削除されます。画面にすぐに表示される内容を変更すると、ユーザーに不快感を与える可能性があります。また、突然表示される新しいコンテンツを見逃す可能性もあります。アニメーションは変化を遅らせて、動きでユーザーの目を引き、更新が明確になるようにします。
ビューの表示 / 非表示に使用できる一般的なアニメーションは、表示アニメーション、クロスフェード アニメーション、カードフリップ アニメーションの 3 つです。
クロスフェード アニメーションを作成する
クロスフェード アニメーション(ディゾルブとも呼ばれます)は、1 つの View
または ViewGroup
が徐々にフェードアウトするのと同時に別のフェードインを行います。このアニメーションは、アプリ内のコンテンツやビューを切り替える場合に便利です。ここに示すクロスフェード アニメーションは、Android 3.1(API レベル 12)以降で利用可能な ViewPropertyAnimator
を使用します。
進行状況インジケーターからテキスト コンテンツへのクロスフェードの例を次に示します。
ビューを作成する
クロスフェードする 2 つのビューを作成します。次の例では、進行状況インジケーターとスクロール可能なテキストビューを作成します。
<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) } ... }
Java
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 に設定し、公開設定を
GONE
の初期設定からVISIBLE
に設定します。これにより、ビューは表示されますが、透明になります。 - フェードインするビューのアルファ値を 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 so it doesn't // participate in layout passes. loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration.toLong()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loadingView.visibility = View.GONE } }) } }
Java
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 so it doesn't // participate in layout passes. loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { loadingView.setVisibility(View.GONE); } }); } }
カードフリップ アニメーションを作成する
カード切り替えでは、カードの反転をエミュレートするアニメーションが表示され、コンテンツの表示が切り替わります。ここに示すカードフリップ アニメーションでは、FragmentTransaction
を使用しています。
カードフリップは次のようになります。
アニメーター オブジェクトを作成する
カードフリップ アニメーションを作成するには、4 つのアニメーターが必要です。2 つのアニメーターは、カードの前面が左にアニメーション表示される場合と、左に内外でアニメーション化するためのものです。他の 2 つのアニメーターは、カードの裏面が右からイン / 右に動くタイミングと、カードの裏側が右に動くタイミングに使用します。
<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" />
<!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
<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" />
<!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
<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" />
<!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
<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" />
<!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
ビューを作成する
カードの両側は個別のレイアウトで、2 つのテキストビュー、2 つの画像、切り替えるビューの組み合わせなど、任意のコンテンツを含めることができます。後でアニメーション化するフラグメントで 2 つのレイアウトを使用します。次のレイアウトでは、テキストを表示するカードの片側が作成されます。
<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) } }
Java
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() } } ... }
Java
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, such as when the // system Back button is tapped. .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 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, letting users press // the Back button to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit() } }
Java
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, such as 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 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, letting users 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
// A previously invisible view. val myView: View = findViewById(R.id.my_view) // Check whether the runtime version is at least Android 5.0. 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 0. 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 // Android 5.0. myView.visibility = View.INVISIBLE }
Java
// A previously invisible view. View myView = findViewById(R.id.my_view); // Check whether the runtime version is at least Android 5.0. 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 0. 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 // Android 5.0. myView.setVisibility(View.INVISIBLE); }
ViewAnimationUtils.createCircularReveal()
アニメーションは 5 つのパラメータを取ります。最初のパラメータは、画面上で非表示にする、または表示するビューです。次の 2 つのパラメータは、切り取り円の中心の X 座標と Y 座標です。通常、これはビューの中心ですが、ユーザーがタップしたポイントを使用して、選択した位置からアニメーションを開始することもできます。4 番目のパラメータは、クリッピング円の開始半径です。
上記の例では、初期半径が 0 に設定されているため、表示されているビューは円で非表示になっています。最後のパラメータは円の最終半径ですビューを表示するときは、アニメーションが終了する前にビューが完全に表示されるように、最終半径をビューよりも大きくします。
以前に表示されていたビューを非表示にするには、次の手順を行います。
Kotlin
// A previously visible view. val myView: View = findViewById(R.id.my_view) // Check whether the runtime version is at least Android 5.0. 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 0. 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 // Android 5.0. myView.visibility = View.VISIBLE }
Java
// A previously visible view. final View myView = findViewById(R.id.my_view); // Check whether the runtime version is at least Android 5.0. 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 0. 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 Android // 5.0. myView.setVisibility(View.VISIBLE); }
この場合、アニメーションの開始前にビューが表示されるように、クリップ円の初期半径がビューと同じ大きさに設定されます。最後の半径は 0 に設定されるため、アニメーションが終了するとビューは非表示になります。アニメーションにリスナーを追加して、アニメーションの完了時にビューの表示を INVISIBLE
に設定できるようにします。