さいきん書いた、Android の ImageView をスクロールさせるで作ったサンプルは、移動範囲を一定に留めるという実装にしたのだが、これは一般的な挙動ではないようだ。
多くのアプリ、特に iOS 向けの画像ビューアーや電子書籍アプリで採用されている挙動を見ると、だいたい以下のようになっていた。
- コンテンツの表示モードがフィット、原寸のいずれでもスワイプ移動を実行できる
- 画面に指が触れている間はスワイプ移動、離すと移動が終了する
- 移動が終了した時点でコンテンツが画面に収まるならば、その位置へ移動
- 移動が終了した時点でコンテンツが画面に収まらないなら、収まるように位置を補正する
- 移動が終了した時点でコンテンツが画面に収まらないとき、余白のサイズが一定以上なら、前 or 次のコンテンツを選択
この動きを再現すべく、前回のサンプルを改造してみる。
移動と位置の補正
移動を Android のタッチ操作イベントで表すと、MotionEvent.ACTION_DOWN が起点となり、ACTION_MOVE は移動中、ACTION_UP で終了となる。これは指の押し下げ、移動、押し上げに対応している。
前回のサンプルでは、これらの内、ACTION_MOVE と ACTION_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 メソッドに指定すれば、自動的に適切な位置へ移動される。
ただし、今回は一般的な画像ビューアーのように画像の切り替えもおこないたいので、画像の端から見て一定以上のスワイプ移動が発生したら、前後の画像が選択されるようにする。上記コードでいうと、232、233 行目の処理がそれにあたる。
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 としている。480×800 の端末なら、短辺が 480 なので 120px となる。このさじ加減は難しいところである。短辺の 1/4 だと、タブレットのような大画面では切り替えが大変かもしれない。
サンプル プロジェクト
最後に、サンプル アプリのプロジェクトを公開しておく。
TestImageScroll2.zip 366KB
Android 2.1 update 1 ( API Level 7 ) でビルドし、エミュレータと初代 Xperia ( SO-01B ) にて動作確認をおこなった。
文章やコードだけで動きを説明するのは難しいのだが、サンプルを実際に動かしてみれば、ああ、あの動きのことをいっているのだな、と合点がゆくと思う。