Android(X06HT Desire) ページをめくる動作を実装してみるサンプル

あの魅力的な iPad の本のページをめくる様なアニメーション。

そこまでとは行かないまでも、ちょっとしたページをめくるような動きは、ちょっとした設定でできるんだろうAndroid。位に思っていたが、そうでもない感じ。

どうするのが定石なのかちと分からないが、それっぽく動くようになったので、メモしておく。

ViewFlipper とアニメーションを組み合わせる

ViewFlipperっていうくらいだから、こいつを使えば万事OKと思っていた。

確かに、簡単にできる。

以下のソースは、3つの実装を切り分けるような形式になっているので無駄に長いが、この実装をおこなっているのは、pageFlipWithSimpleAnimation() メソッド。

ページ遷移は、ViewFlipperに任せて、動きはアニメーションで行う。

アニメーションサンプルの読み込みと作成

アニメーションって急に言われても取っつきにくいが、サンプルがSDKにある。 自分の環境だと、以下

{android-sdk}\samples\android-7\ApiDemos\res\anim

適宜読み替えて。

Eclipse から、/res/anim フォルダをつくって、コンテキストメニューから上記フォルダのアニメーションファイルを適宜インポート。

android_anim01

ここでは、push_left_in.xml、push_left_out.xml をそのまま、使う。

これは、View を 左に向けて 入ってくるのと、左に向けて出て行く アニメーション。

逆に右に向けて入ってくるのと、右に向けて出ていくのが必要になるので、上記をコピーして、push_right_in.xml、push_right_out.xml を作成。

<translate android:fromXDelta="100%p" 
              android:toXDelta="0" 
              android:duration="300"/>
<alpha     android:fromAlpha="1.0"
              android:toAlpha="1.0"
              android:duration="300" />

中身を見るとこんな記述がされている。

見慣れないのでとまどうが、要するに、translate(移動する)、X軸上で、fromXDelta (どこから) toXDelta(どこまで) を duration(どれだけの時間をかけて)と、alpah (透明度) を fromAlpa(1.0 なら不透明) から toAlpha(0.0 なら透明)  という設定だけなので、基本 left の 反対を設定するように right の設定ファイルを作成する。

基本以上。あとは、動きに合わせて、前ページか次ページか判定し、それに合わせたアニメーションでページ遷移する。

ごりごりページめくりを実装する

で、ViewFlipperを使えば、ページめくりを途中で止めたりとかできるかと思いきやそうでもないようだ。(できるのかな?)

なので、ごりごり実装してみる。

基本、同じレイアウトファイルのViewFlipper 配下に、ページ設定のレイアウトを配置するようだが、外に出してみる。

LayoutInflater.inflate を利用すると、動的にレイアウトを読み込むことができる。

あと、はまったのが、ViewFlipper の addView メソッドでインデックスを引数にとるが、インデックスとビューとの対応が、なぜか動かすと変わってしまって、たとえば、インデックス=1 で add したつもりの ビューが 2  getChildAt(2) で取れてきたり、setDisplayedChild(1) で、インデックス=0 で設定したつもりのビューがアクティブになってしまったり。。。そもそも使い方が悪いのか知らん。

なので、動的にViewの内容を入れ替えることを想定して、前、今、後 の3つのレイアウトを用意し、それぞれのID を int[] viewOrder に外だしして管理するようにした。

あとは、View.layout で、それぞれのビューの表示、非表示を切り替えながら、位置を変えていくことで、それなりに動くようにはなった。

課題

一応、

  1. ViewFlipper + アニメーションをつかった単純なページめくり
  2. 今のページをずらして、次や前のページがあわられる
  3. 次や前のページがあらわれて、今のページを隠す

3パターンを作ってみた。layout の位置の設定だけで、Desire の ホーム画面のような連続して切り替わるような形にもできそうだ。

3の「次や前のページがあらわれて、今のページを隠す」 場合、一瞬画面がちらつく。これは何とかならんかな。

 

package info.typea.viewflipperapp;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.ViewFlipper;

public class ViewFilpperAppActivity extends Activity implements OnTouchListener {
    private static final String LOG_TAG = "MyApp";
    
    /* コンテキストメニューID */
    private static final int MENU_VIEWFLIP_ANIM  = Menu.FIRST; 
    private static final int MENU_MOVE_NEXT      = Menu.FIRST + 1; 
    private static final int MENU_MOVE_CURRENT   = Menu.FIRST + 2; 
    
    /* ページ切り替え動作モード */
    private int procMode = MENU_VIEWFLIP_ANIM;
    
    /* ページを切り替える移動量閾値 */
    private final int SWITCH_THRESHOLD = 10;
    
    /* ページ切り替えモード */
    private final int FLIPMODE_NOMOVE = 0;
    private final int FLIPMODE_NEXT =  1;
    private final int FLIPMODE_PREV = -1;
    private int flipMode = FLIPMODE_NOMOVE;
         
    private ViewFlipper vf    = null;
    private View currentView  = null;
    private View nextView     = null;
    private View prevView     = null;
    
    /* ページのIDと順序を管理 */
    private int viewOrder[]   = null;
    private int curIdx = -1;
    private int preIdx = -1;
    private int nxtIdx = -1;
    
    private int movePageThreshold = 0;
    private float startX;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        LinearLayout layout = (LinearLayout) findViewById(R.id.layout_main);
        layout.setOnTouchListener(this);
    
        vf = (ViewFlipper) findViewById(R.id.details);

        /* 
         * レイアウトを実行時にインスタンス化し ViewFlipperに格納 
         * 同時に順序をIDで管理する
         * addView(v,idx) で管理できると思いきや、実行時に View と Index の対応が変わってしまうようだ
         */
        LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        int layouts[] = new int[] {R.layout.layout_page1,
                                    R.layout.layout_page2,
                                    R.layout.layout_page3};
        viewOrder = new int[layouts.length];
        for (int i=0; i<layouts.length; i++) {
            View vw = vi.inflate(layouts[i], null);
            // ViewFlipper に格納
            vf.addView(vw);
            // ID管理用配列に保持
            viewOrder[i] = vw.getId();
        }
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // コンテキストメニューで、ページめくりの実装を切り替える
        super.onCreateOptionsMenu(menu);
        menu.add(0, MENU_VIEWFLIP_ANIM, 0, R.string.vewflipper_anim);
        menu.add(0, MENU_MOVE_CURRENT,  0, R.string.current_page_move);
        menu.add(0, MENU_MOVE_NEXT,     0, R.string.next_prev_page_move);
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        procMode = item.getItemId();
        
        // ViewFlipper に設定したアニメーションを解除
        vf.setInAnimation(null);
        vf.setOutAnimation(null);
        
        return true;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        boolean ret = false;
        
        // ページめくりの実装を切り替える
        switch(procMode) {
        case MENU_VIEWFLIP_ANIM:
            ret =  pageFlipWithSimpleAnimation(v, event);
            break;
        case MENU_MOVE_CURRENT:
            ret = pageFlipWithFingerMoveCurrent(v, event);
            break;
        case MENU_MOVE_NEXT:
            ret = pageFlipWithFingerMoveNext(v, event);
            break;
        }
        return ret;
    }

    /**
     * ViewFlipper と アニメーションを利用して単純にページをめくる
     * @param v
     * @param event
     * @return
     */
    public boolean pageFlipWithSimpleAnimation(View v, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            float currentX = event.getX();
            if (this.startX > currentX ) {
                vf.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_left_in));
                vf.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_left_out));
                vf.showNext();
            }
            if (this.startX  < currentX) {
                vf.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_right_out));
                vf.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_right_in));
                vf.showPrevious();
            }
        default:
            break;
        }
        return true;
    }

    /**
     * 前のページおよび次のページを移動することによってページをめくる
     * @param v
     * @param event
     * @return
     */
    public boolean pageFlipWithFingerMoveNext(View v, MotionEvent event) {
        float currentX = event.getX();
        
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();

            currentView = vf.getCurrentView();
            movePageThreshold = (currentView.getWidth() / 5);
            
            int viewCount = viewOrder.length;
            for (int i=0; i<viewCount; i++) {
                //Log.i("MyApp", "ord=id:" + viewOrder[i] + "==" + cv.getId());
                if (viewOrder[i] == currentView.getId()) {
                    curIdx = i;
                    break;
                }
            }
            
            if (curIdx >= 0) {
                preIdx = curIdx - 1;
                nxtIdx = curIdx + 1;
                preIdx = (preIdx < 0         )?viewCount - 1:preIdx;
                nxtIdx = (nxtIdx >= viewCount)?0            :nxtIdx;
                
                prevView = vf.findViewById(viewOrder[preIdx]);
                nextView = vf.findViewById(viewOrder[nxtIdx]);
            }
            
            Log.i(LOG_TAG, 
                    String.format("Pre=%d(%d),Cur=%d(%d),Nxt=%d(%d)" 
                                    ,preIdx,prevView.getId()
                                    ,curIdx,currentView.getId()
                                    ,nxtIdx,nextView.getId()));
            
            break;
        case MotionEvent.ACTION_MOVE:
            int travelDistanceX = (int)(currentX - this.startX);
            int fingerPosX = (int)currentX;
            
            if (flipMode == FLIPMODE_NOMOVE) {
                if (travelDistanceX > SWITCH_THRESHOLD) {
                    flipMode = FLIPMODE_PREV;
                } else if
                   ( travelDistanceX < (SWITCH_THRESHOLD * -1)  ) {
                    flipMode = FLIPMODE_NEXT;
                } else {
                    flipMode = FLIPMODE_NOMOVE;
                }
            }
            
            if (flipMode == FLIPMODE_PREV) {
                prevView.layout(fingerPosX - prevView.getWidth(), 
                          prevView.getTop(), 
                          fingerPosX , 
                          prevView.getBottom());
                
                vf.bringChildToFront(prevView);
                prevView.setVisibility(View.VISIBLE);
                    
            }
            if ( flipMode == FLIPMODE_NEXT)  {
                nextView.layout(fingerPosX , 
                          nextView.getTop(), 
                          fingerPosX + currentView.getWidth() + nextView.getWidth(), 
                          nextView.getBottom());
                
                vf.bringChildToFront(nextView);
                nextView.setVisibility(View.VISIBLE);
            }
            break;
            
        case MotionEvent.ACTION_UP:
            
            int activeIdx = -1;
            if ((this.startX - currentX) > movePageThreshold ) {
                activeIdx = nxtIdx;
            }else if 
                ((this.startX - currentX) < (movePageThreshold * -1) ) {
                activeIdx = preIdx;
            } else {
                activeIdx = curIdx;
            }
            int activeId = viewOrder[activeIdx];
            for(int i=0; i<vf.getChildCount(); i++) {
                // Log.i("MyApp",String.format("vf_id:%d,sel_id:%d",vf.getChildAt(i).getId(),activeId));
                if (vf.getChildAt(i).getId() == activeId) {
                    vf.setDisplayedChild(i);
                    break;
                }
            }
            flipMode = 0;
        default:
            break;
        }
        return true;
    }
    
    /**
     * 現在のページを移動することによってページをめくる
     * @param v
     * @param event
     * @return
     */
    public boolean pageFlipWithFingerMoveCurrent(View v, MotionEvent event) {
        float currentX = event.getX();
        
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();

            currentView = vf.getCurrentView();
            movePageThreshold = (currentView.getWidth() / 5);
            
            int viewCount = viewOrder.length;
            for (int i=0; i<viewCount; i++) {
                //Log.i("MyApp", "ord=id:" + viewOrder[i] + "==" + cv.getId());
                if (viewOrder[i] == currentView.getId()) {
                    curIdx = i;
                    break;
                }
            }
            
            if (curIdx >= 0) {
                preIdx = curIdx - 1;
                nxtIdx = curIdx + 1;
                preIdx = (preIdx < 0         )?viewCount - 1:preIdx;
                nxtIdx = (nxtIdx >= viewCount)?0            :nxtIdx;
                
                prevView = vf.findViewById(viewOrder[preIdx]);
                nextView = vf.findViewById(viewOrder[nxtIdx]);
            }
            
            Log.i(LOG_TAG, 
                    String.format("Pre=%d(%d),Cur=%d(%d),Nxt=%d(%d)" 
                                    ,preIdx,prevView.getId()
                                    ,curIdx,currentView.getId()
                                    ,nxtIdx,nextView.getId()));
            
            break;
        case MotionEvent.ACTION_MOVE:
            int travelDistanceX = (int)(currentX - this.startX);
            int fingerPosX = (int)currentX;
            
            if (flipMode == FLIPMODE_NOMOVE) {
                if (travelDistanceX > SWITCH_THRESHOLD) {
                    flipMode = FLIPMODE_PREV;
                } else if
                   ( travelDistanceX < (SWITCH_THRESHOLD * -1)  ) {
                    flipMode = FLIPMODE_NEXT;
                } else {
                    flipMode = FLIPMODE_NOMOVE;
                }
            }
            
            if (flipMode == FLIPMODE_PREV) {
                currentView.layout(fingerPosX, 
                            currentView.getTop(), 
                            fingerPosX + currentView.getWidth() , 
                            currentView.getBottom());
                
                vf.bringChildToFront(currentView);
                prevView.setVisibility(View.VISIBLE);
                    
            }
            if ( flipMode == FLIPMODE_NEXT)  {
                currentView.layout(fingerPosX - currentView.getWidth() , 
                          currentView.getTop(), 
                          fingerPosX, 
                          currentView.getBottom());
                
                vf.bringChildToFront(currentView);
                nextView.setVisibility(View.VISIBLE);
            }
            break;
            
        case MotionEvent.ACTION_UP:
            
            int activeIdx = -1;
            if ((this.startX - currentX) > movePageThreshold ) {
                activeIdx = nxtIdx;
            }else if 
                ((this.startX - currentX) < (movePageThreshold * -1) ) {
                activeIdx = preIdx;
            } else {
                activeIdx = curIdx;
            }
            int activeId = viewOrder[activeIdx];
            for(int i=0; i<vf.getChildCount(); i++) {
                // Log.i("MyApp",String.format("vf_id:%d,sel_id:%d",vf.getChildAt(i).getId(),activeId));
                if (vf.getChildAt(i).getId() == activeId) {
                    vf.setDisplayedChild(i);
                    break;
                }
            }
            flipMode = 0;
        default:
            break;
        }
        return true;
    }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#ffffff"
    android:id="@+id/layout_main"
    >
    <ViewFlipper android:id="@+id/details"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" android:animationCache="true">  
    </ViewFlipper>
</LinearLayout>

push_right_in.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" 
               android:toXDelta="100%p"
               android:duration="300"/>
    <alpha android:fromAlpha="1.0" 
           android:toAlpha="1.0" 
           android:duration="300" />
</set>

push_right_out.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%p" 
               android:toXDelta="0" 
               android:duration="300"/>
    <alpha android:fromAlpha="1.0" 
           android:toAlpha="1.0" 
           android:duration="300" />
</set>