アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Android の ImageView をスクロールさせる 2

この間の Android の ImageView をスクロールさせるで作ったサンプルは移動範囲を一定に留めるという実装にしたのだが、これは一般的な挙動ではないようだ。多くのアプリ、特に iOS 向けの画像ビューアーや電子書籍アプリで採用されている挙動を見るに

  • コンテンツの表示モードがフィット、原寸のいずれでもスワイプ移動を実行できる
  • 画面に指が触れている間はスワイプ移動、離すと移動が終了する
  • 移動が終了した時点でコンテンツが画面に収まるならば、その位置へ移動
  • 移動が終了した時点でコンテンツが画面に収まらないなら、収まるように位置を補正する
  • 移動が終了した時点でコンテンツが画面に収まらないとき、余白のサイズが一定以上なら、前 or 次のコンテンツを選択

というものが多い。そのためこれを再現すべく前回のサンプルを改造してみる。

移動と位置の補正

移動を Android のタッチ操作イベントで表すと MotionEvent.ACTION_DOWN が起点となり ACTION_MOVE は移動中、ACTION_UP で終了となる。これはそれぞれ指の押し下げ、移動、押し上げに対応している。

前回のサンプルでは ACTION_MOVEACTION_UP のどちらでも同じ移動処理を実行していたが、今回は位置の補正を ACTION_UP のみとする。つまり指をグリグリさせている間はどんな位置へも移動できて、指を離したときだけ位置を適正な範囲に直す。

処理としては以下のようになる。

/**
 * View がタッチされた時に発生します。
 *
 * @param v     タッチされた View。
 * @param event イベント データ。
 *
 * @return タッチ操作を他の View へ伝搬しないなら true。する場合は false。
 */
public boolean onTouch( View v, MotionEvent event ) {
    switch( event.getAction() ) {
    case MotionEvent.ACTION_DOWN:
        this.mTouchBeginX = event.getX();
        this.mTouchBeginY = event.getY();
        break;

    case MotionEvent.ACTION_MOVE:
        float x = event.getX(), y = event.getY();
        this.scrollImage( x, y, true );

        this.mTouchBeginX = x;
        this.mTouchBeginY = y;
        break;

    case MotionEvent.ACTION_UP:
        this.scrollImage( event.getX(), event.getY(), false );
        break;
    }

    return true;
}

/**
 * 画像のスクロールを実行します。
 *
 * @param x        スクロール基準となる X 座標。
 * @param y        スクロール基準となる Y 座標。
 * @param isMoving 移動中の場合は true。それ以外は false。
 *
 * @return 画像を切り替えない場合は 0。前に戻すなら -1、次に進めるときは 1。
 */
private void scrollImage( float x, float y, boolean isMoving ) {
    if( isMoving ) {
        int moveX = ( int )( this.mTouchBeginX - x );
        int moveY = ( int )( this.mTouchBeginY - y );
        this.mImageView.scrollBy( moveX, moveY );

    } else if ( this.mImageScaleType == ImageView.ScaleType.FIT_CENTER ) {
        this.scrollFinish( x, y, 0, 0 );

    } else {
        this.scrollFinish( x, y, this.mOverX, this.mOverY );
    }
}

移動中に呼び出された scrollImage メソッドは単純な移動を実行する。ACTION_UP 時は scrollFinish メソッド補正。今回はフィット・原寸に依存せず移動させるため大半の処理は共通化できる。これらの差異は画面からはみ出る量だから、共通メソッドである scrollFinish ではそれをパラメータとして括りだしている。

位置の補正と画像きりかえ

スワイプ移動が完了した時に呼び出される scrollImage メソッド実装は以下。

/**
 * 画像のスクロールが完了した時の処理を実行します。
 *
 * @param x     スクロール基準となる X 座標。
 * @param y     スクロール基準となる Y 座標。
 * @param overX 画面を基準として、画像の表示領域が X 軸にはみ出ている量。
 * @param overY 画面を基準として、画像の表示領域が Y 軸にはみ出ている量。
 */
private void scrollFinish( float x, float y, int overX, int overY ) {
    int moveX = this.calcScrollValue( ( int )( this.mTouchBeginX - x ), this.mImageView.getScrollX(), overX );

    switch( this.mSwitchFlag ) {
    case -1: this.updateImage( this.mPictureManager.prev() ); return;
    case  1: this.updateImage( this.mPictureManager.next() ); return;

    default:
        int moveY = this.calcScrollValue( ( int )( this.mTouchBeginY - y ), this.mImageView.getScrollY(), overY );
        this.mImageView.scrollBy( moveX, moveY );
        break;
    }
}

まず位置の補正だが、これは前回のサンプルと同様に calcScrollValue で処理。このメソッドはタッチ座標、ImageView のスクロール位置、画面からはみ出る量をパラメータとして渡すと適正なスクロール量を返すようになっている。よってこの値を X、Y 軸で算出して ImageView.scrollBy メソッドに指定すれば自動的に適切な位置へ移動される。

ただし今回は一般的な画像ビューアーのように画像の切り替えもおこないたい。そのため画像の端から見て一定以上のスワイプ移動が発生したら前後の画像が選択されるようにしたい。calcScrollValue の中では位置の補正と共に画像の切り替えフラグとなるフィールドも更新し、それを判定している。実装は以下のようになる。

/**
 * スクロール量を算出します。
 *
 * @param move 移動する予定の量。
 * @param pos  現在のスクロール座標
 * @param over 画像が表示領域の一辺からはみ出る量。
 *
 * @return スクロール量。
 */
private int calcScrollValue( int move, int pos, int over ) {
    int newPos = pos + move;
    if( newPos < -over ) {
        move = -( over + pos );
        this.mSwitchFlag = ( newPos < -( over + this.mSwitchSize ) ? -1 : 0 );

    } else if( over < newPos ) {
        move = over - pos;
        this.mSwitchFlag = ( ( over + this.mSwitchSize ) < newPos ? 1 : 0 );

    } else {
        this.mSwitchFlag=0;
    }

    return move;
}

mSwitchFlag フィールドがフラグとなる。右スワイプ時に一定範囲を超えたら前、左なら次の画像を選択するフラグを立てる。もっと上手くやるなら戻り値を long にして上位・下位ワードにフラグとスクロール量の int 値をパックしてもよさそうだ。

mSwitchSize が切り替え許容量。画像の端に到達したら、この値と量を足した幅を移動して切り替えとみなす。この量は画面の短辺の 1/4 としている。480x800 の端末なら短辺が 480 なので 120px となる。このさじ加減は難しいところである。短辺の 1/4 だとタブレットのような大画面では切り替えが大変かもしれない。

サンプル プロジェクト

最後にサンプル アプリのプロジェクトを公開しておく。

Android 2.1 update 1 (API Level 7) でビルド、エミュレータと初代 Xperia ( SO-01B ) にて動作確認した。文章やコードだけで動きを説明するのは難しいのだがサンプルを実際に動かしてみれば合点がゆくと思う。

Comments from WordPress

  • ケイ ケイ 2012-09-17T17:08:00Z

    初めまして。ケイと申します。
    とても丁寧でわかりやすいプログラムで助かりました。

    お伺いしたいことがあります。
    イメージビュウをスクロールにピンチイン/アウトで拡大縮小を行いたいとおもっています。

    いろいろ考えてみたのですがどうもうまくいきません。
    アドバイスお願いできませんでしょうか。

    よろしくお願いいたします。