ViewFlipperによるビューの切替えとアニメーション

iPhoneAndroid端末は静電容量タイプのタッチスクリーンを装備しており、メインの入力デバイスとしているのが特徴だ。
これらのタッチスクリーンは旧来の感圧タイプとは違い、非常に繊細な操作を可能にしておりスクリーンをそっと触りながら任意の方向に撫で滑らす操作、いわゆる「フィンガータッチモード」に対応しており、様々な用途に利用される。(マウスなどの補助的なデバイスを使用する旧来からのPCと最も違う部分だ)

モバイルプラットホームOSのユーザインタフェースではこのタッチモードをアフォードするために様々なエフェクト、アニメーションを駆使しているが、その中でも特徴的なのがタッチしてから特定の方向に指を動かした時に見られる、画面が滑らかにスライドしていく(場合によっては加速度まで表現する)アニメーションだろう。

そんなユーザインタフェースとアニメーションは非常に高度なプログラミングが要求されそうに感じるが、Androidでは比較的簡単に実装できる。
技術的には、遷移すると思われるビュー(GUI)をそれ用のコンテナに予め乗せておき、タッチの状況によって表示するビューを切り替えることで実現する。


以下、簡単なサンプルコードを例にするが、同様のコードはいろいろな所に散見される。私は以下のサイトにあるスニペットを参考させて頂いた。
Android: ViewFlipper Touch Animation like News & Weather
Code ProjectにAndroidのセクションがあるのにはびっくりした。

  • レイアウト(layout.xml)の作成

通常のレイアウトリソースと同様だが、切換える対象のビューは"ViewFlipper"要素の子要素として記述する(ViewFlipperの子要素は他のレイアウトである必要は無いが、レイアウトを対象にしておくとなにかと便利だろう)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/LinearLayout01" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    xmlns:android="http://schemas.android.com/apk/res/android">
    <ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layoutswitcher"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
            <TextView
                android:id="@+id/firstpanel"
                android:paddingTop="10dip"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:layout_weight="1"
                android:textStyle="bold"  
                android:text=" 一枚目">
            </TextView>
        </LinearLayout>
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
            <TextView
                android:id="@+id/secondpanel"
                android:paddingTop="10dip"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:layout_weight="1"
                android:textStyle="bold"
                android:text=" 二枚目">
            </TextView>
        </LinearLayout>
    </ViewFlipper>
</LinearLayout>

このレイアウトで言えば、id:firstpanelid:secondpanelを入れ替えるのがViewFlipperの機能、ということになる。

  • OnTouchEventイベントにビューを切り換えるコードを書く

ViewFlipperは基本的にはViewGroupであり、Viewのコンテナである。上記レイアウトではInflateFactoryによるインフレート時に内部には二つのビューが格納されるが、格納されたViewを切り換えるためには、

//前のビューを表示
ViewFlipper#showPrevious()

//次のビューを表示
ViewFlipper#showNext()

これらのメソッドをタッチ操作の方向(X軸)により切り換えながら、上記メソッド実行すれば良い。

public class FlipperTest extends Activity {
    private ViewFlipper viewflipper;  
    private float lastTouchX;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        viewflipper = (ViewFlipper)this.findViewById(R.id.layoutswitcher);
        
        this.viewflipper.setOnTouchListener(new OnTouchListener() {
            
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN: //A
                        lastTouchX = event.getX();
                        break;
                    case MotionEvent.ACTION_UP: //B
                        float currentX = event.getX();
                        if (lastTouchX < currentX) {
                            viewflipper.showNext();
                        }
                        if (lastTouchX > currentX) {
                            viewflipper.showPrevious();
                        }
                        break;
                }
                return true; //C
            }
        });
    }
}

ポイントは event.getAction()の戻り値によって処理を変えている部分。

    • MotionEvent.ACTION_DOWN

タッチモードが有効なGUI上にて指をスクリーンにタッチした際に検出。以降指を動かした後で離すまでがモーションイベント中となる。(タッチイベントは指によるタッチに限定している訳では無いが、静電容量タイプのタッチスクリーンを装備しているAndroidスマートフォンが対象のため、便宜上タッチしているのは指であることを前提にしている)
コードではタッチされた際のX座標の値をlastTouchXに退避している。

    • MotionEvent.ACTION_UP

タッチ中の指を離した際に検出。モーションの終了となる。
コードではタッチされた際のX軸座標の値と、モーション開始時に退避されたlastTouchXの値を比較して、指タッチしたままどちらに滑らせたかを判定している。

( lastTouchX < currentX ) が真ならば、ViewFlipperの次のView要素に切り換える(タッチしたまま指を右に滑らせた)
( lastTouchX > currentX ) が真ならば、ViewFlipperの前のView要素に切り換える(タッチしたまま指を左に滑らせた)

また、onTouchメソッドは戻り値が必要であり、trueを返している。これをしないとタッチイベントがこのビューで完了したことにならず、ビューの構成によってはACTION_UPが検出されなくなるので注意が必要。

単にビューを切り換えるだけであれば、ここまででコードは終了だ。


  • アニメーションを適用する

冒頭に書いたようにスマートフォンらしい、モーションをアフォードする動きにするためには、ビューの切り換え時にアニメーションを適用することが必要になる。
ViewFlipperのスーパクラスはViewAnimatorクラスであり、元々View要素の切り換え時にアニメーションを挿入することを前提にしており、以下のメソッドを提供する。

//表示領域に入ってくる際に再生されるアニメーションをセットする
ViewAnimator#setInAnimation(Animation inAnimation) 
ViewAnimator#setInAnimation(Context context, int resourceID) 

//表示領域から出ていく際に再生されるアニメーションをセットする
ViewAnimator#setOutAnimation(Animation outAnimation) 
ViewAnimator#setOutAnimation(Context context, int resourceID) 

それぞれAnimationクラスのインスタンスか、コンテキスト+リソースIDの組合せをパラメタとして指定できるが、これはコードでアニメーションを生成するか、XMLリソースからアニメーションをインフレートするかの違いである。

今回はネットでもよく紹介しているようにコードでアニメーションオブジェクトを用意した。(ほぼ同じことがXMLリソースでもできる)

    • AnimationHelper.java
public class AnimationHelper {
    /**
     * 右から描画領域に入ってくる動作を表現するアニメーションを生成します
     * @return Animation 生成したアニメーションが戻ります
     */
    public static Animation inFromRightAnimation() {

        Animation inFromRight = 
            new TranslateAnimation(
                    Animation.RELATIVE_TO_PARENT
                    , +1.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f);
        inFromRight.setDuration(350);
        inFromRight.setInterpolator(new AccelerateInterpolator());
        return inFromRight;
    }
    /**
     * 描画領域の左へ出ていく動作を表現するアニメーションを生成します
     * @return Animation 生成したアニメーションが戻ります
     */
    public static Animation outToLeftAnimation() {
        Animation outtoLeft = 
            new TranslateAnimation(
                    Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , -1.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f);
        outtoLeft.setDuration(350);
        outtoLeft.setInterpolator(new AccelerateInterpolator());
        return outtoLeft;
    }    
    /**
     * 左から描画領域に入ってくる動作を表現するアニメーションを生成します
     * @return Animation 生成したアニメーションが戻ります
     */
    public static Animation inFromLeftAnimation() {
        Animation inFromLeft = 
            new TranslateAnimation(
                    Animation.RELATIVE_TO_PARENT
                    , -1.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f);
        inFromLeft.setDuration(350);
        inFromLeft.setInterpolator(new AccelerateInterpolator());
        return inFromLeft;
    }
    /**
     * 描画領域の右へ出ていく動作を表現するアニメーションを生成します
     * @return Animation 生成したアニメーションが戻ります
     */
    public static Animation outToRightAnimation() {
        Animation outtoRight = 
            new TranslateAnimation(
                    Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , +1.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f
                    , Animation.RELATIVE_TO_PARENT
                    , 0.0f );
        outtoRight.setDuration(350);
        outtoRight.setInterpolator(new AccelerateInterpolator());
        return outtoRight;
    }           
}

毎回生成するのは処理コストとGCが嫌なので、finalなフィールドに設定しておく。

private static final Animation inFromLeft = AnimationHelper.inFromLeftAnimation();
private static final Animation outToRight = AnimationHelper.outToRightAnimation();
private static final Animation inFromRight = AnimationHelper.inFromRightAnimation();
private static final Animation outToLeft = AnimationHelper.outToLeftAnimation();


あとは、適宜必要に応じてViewHelperのアニメーションとして設定すれば自動的に再生される。
アニメーションを再生したいのはビューの切り換え時なので、適用するのはモーションイベントが完了したと判定されたタイミングである。

    case MotionEvent.ACTION_UP: {
        float currentX = event.getX();
        if (lastTouchX < currentX) {
            viewflipper.setInAnimation(inFromLeft);
            viewflipper.setOutAnimation(outToRight);
            viewflipper.showNext();
        }
        if (lastTouchX > currentX) {
            viewflipper.setInAnimation(inFromRight);
            viewflipper.setOutAnimation(outToLeft);
            viewflipper.showPrevious();
        }
        break;
    }

以上で「それっぽい」タッチモーションによるビューの切り換えが実現できる。

ただし、実際に動かしてみると解るがこの実装でできるのは現在描画中のビューとこれから描画するビューとの切り替えを滑らかに描画することであり、Androidのホーム画面の切り替えのように、タッチモーション途中の切り替え中のビューを描画することはできない。※



[追記]
この例だと、タッチイベント開始時のX座標から1画素ずれても左右どちらかに動かしたと判定されるため、人間の指だとほぼ間違いなくタッチしただけでビューが切り替わってしまう。
上記サンプルのコードで言えば、タッチ開始時のX座標値lastTouchXとタッチ完了時のcurrentXの差、つまり移動量がある程度の値を超えたか否かを判定することで実用的になるはずだ。

例えば、拙作のCalendarVierwであれば、モーションによる移動量がカレンダを描画するマトリクスの中の一つのセルの幅を超えたか否かが判定基準となるだろう。


※MotionEvent.ACTION_MOVEを処理することでできそうなのだが、出来たのは現在表示中のビューがスライド中イメージだけであり、これから表示されるであろう、他のビューを描画することができなかった。良いアイディアをお持ちの方にはご教示頂きたい。