画像ビューアー系アプリなどでよくみる、画面に収まらないサイズの画像をスクロールする方法について調べてみた。
アクセス解析を見るに、この記事はけっこう多くの方に読まれているようだ。しかし、私的に好ましいスクロール方法は続編記事のほうなので、そちらへのリンクも掲載しておく。
Android の ImageView をスクロールさせる 2
スクロール
Android には ScrollView という標準コントロールがある。これは入れ子にしたコントロールのサイズに応じて、適切なスクロール機能を提供してくれるので、これに ImageView を入れれば、今回の目的を達成できそうに思える。しかし ScrollView は縦スクロール専用なので、横に大きな画像には対応できない。
横方向の場合、HorizontalScrollView が用意されているので、これと ScrollView を入れ子にすれば大丈夫そうだが、その場合、斜め方向のスクロールがおこなえない。縦横の 2 軸を個別に組み合わせているため、連携することができないのだ。
というわけで、自前でスクロールを処理する方法を検討する。
まず、既に実装例があるかもしれないので、「Android ImageView Scroll」といった感じの語句でググってみると、目的に近い記事を見つけた。
ここで cV2 氏が回答している内容を実装してみると、スワイプにより画像のスクロールがおこなえる。
ただし移動範囲をチェックしていないため、どこまでもスクロールできてしまう。これは画面から画像がはみ出た分だけ移動できる方が自然だと思うので、このサンプルをベースに、そのような動きをする処理を実装してみよう。
サンプル アプリ
まず仕様を以下のように定義する。
- ImageView の表示モードは原寸 ( ScaleType.CENTER ) とフィット ( ScaleType.FIT_CENTER ) の 2 種類とする
- 原寸モードの時だけ ImageView をスクロールさせる
- スクロール範囲は画像が画面からはみ出た分とする
- ImageView の上に表示モード切り替え用のコントロールを用意する
- モードが表示されているバーをタップすると表示モードが切り替わる
画面のレイアウトは以下のように定義した。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:background="#0094FF" android:gravity="center" android:layout_width="fill_parent" android:layout_height="fill_parent"> <!-- 画像 --> <ImageView android:id="@+id/image_view" android:src="@drawable/picture" android:scaleType="center" android:layout_centerInParent="true" android:layout_gravity="center" android:layout_width="fill_parent" android:layout_height="fill_parent" /> <!-- 表示モード切り替え --> <TextView android:id="@+id/display_mode" android:background="#88000000" android:textColor="#FFFFFF" android:textStyle="bold" android:textSize="18sp" android:shadowColor="#000000" android:shadowRadius="1.2" android:shadowDx="0" android:shadowDy="2" android:gravity="center" android:padding="4dp" android:layout_alignParentTop="true" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </RelativeLayout>
これに対する実装は以下のようになる。
/** * 画像のスクロールを試すための画面を表します。 */ public class TestImageScrollActivity extends Activity implements OnClickListener, OnTouchListener { /** * 画像の表示モードが CENTER であることを示すテキスト。 */ private static final String DISPLAY_MODE_CENTER = "Center"; /** * 画像の表示モードが FIT_CENTER であることを示すテキスト。 */ private static final String DISPLAY_MODE_FIT_CENTER = "Fit Center"; /** * 画像の表示方法。 */ private ScaleType mImageScaleType = ScaleType.CENTER; /** * 画像が表示領域における、X 軸方向の一辺からはみ出る量。 * 例えば画像の幅が 1080、表示領域は 480 の場合、「( 1080 - 480 ) / 2 = 300」とする。 * 画像より表示領域が大きいならば、この値はゼロとなる。 */ private int mOverX; /** * 画像が表示領域における、Y 軸方向の一辺からはみ出る量。 * 例えば画像の高さが 720、表示領域は 480 の場合、「( 720 - 480 ) / 2 = 120」とする。 * 画像より表示領域が大きいならば、この値はゼロとなる。 */ private int mOverY; /** * 画像を表示するための View。 */ private ImageView mImageView; /** * 画像の表示モードとなるテキスト。 */ private TextView mDisplayModeTextView; /** * タッチの始点となる X 座標。 */ private float mTouchBeginX; /** * タッチの始点となる Y 座標。 */ private float mTouchBeginY; /** * 画面と画像のサイズを元に、一辺からはみ出る量を算出します。 * * @param display 画面のサイズ。 * @param image 画像のサイズ。 * * @return 一辺からはみ出る量。画面に画像が収まる場合はゼロ。 */ private static int calcOverValue( int display, int image ) { return ( display < image ? ( image - display ) / 2 : 0 ); } /** * スクロール量を算出します。 * * @param move 移動する予定の量。 * @param pos 現在のスクロール座標 * @param over 画像が表示領域の一辺からはみ出る量。 * * @return スクロール量。 */ private static int calcScrollValue( int move, int pos, int over ) { int newPos = pos + move; if( newPos < -over ) { move = -( over + pos ); } else if( over < newPos ) { move = over - pos; } return move; } /** * 画面の設定が変更された時に発生します。 * * @param newConfig 新しい設定。 */ @Override public void onConfigurationChanged( Configuration newConfig ) { super.onConfigurationChanged( newConfig ); this.updateOverSize(); this.mImageView.scrollTo( 0, 0 ); } /** * 画面が作成された時に発生します。 * * @param savedInstanceState 保存されたインスタンスの状態。 */ @Override public void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); this.setContentView( R.layout.main ); this.mDisplayModeTextView = ( TextView )this.findViewById( R.id.display_mode ); this.mDisplayModeTextView.setText( DISPLAY_MODE_CENTER ); this.mDisplayModeTextView.setOnClickListener( this ); this.mImageView = ( ImageView )this.findViewById( R.id.image_view ); this.mImageView.setScaleType( this.mImageScaleType ); this.mImageView.setOnTouchListener( this ); this.updateOverSize(); } /** * View がクリックされた時に発生します。 * * @param v クリックされた View。 */ public void onClick( View v ) { if( this.mImageScaleType == ScaleType.CENTER ) { this.mImageScaleType = ScaleType.FIT_CENTER; this.mDisplayModeTextView.setText( DISPLAY_MODE_FIT_CENTER ); } else { this.mImageScaleType = ScaleType.CENTER; this.mDisplayModeTextView.setText( DISPLAY_MODE_CENTER ); } this.mImageView.setScaleType( this.mImageScaleType ); this.mImageView.scrollTo( 0, 0 ); } /** * View がタッチされた時に発生します。 * * @param v タッチされた View。 * @param event イベント データ。 * * @return タッチ操作を他の View へ伝搬しないなら true。する場合は false。 */ public boolean onTouch( View v, MotionEvent event ) { if( this.mImageScaleType == ScaleType.FIT_CENTER ) { return false; } 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 ); this.mTouchBeginX = x; this.mTouchBeginY = y; break; case MotionEvent.ACTION_UP: this.scrollImage( event.getX(), event.getY() ); break; } return true; } /** * 画像をスクロールさせます。 * * @param x 移動先の基準となる画面内の X 軸の座標。 * @param y 移動先の基準となる画面内の Y 軸の座標。 */ private void scrollImage( float x, float y ) { int moveX = ( this.mOverX == 0 ? 0 : calcScrollValue( ( int )( this.mTouchBeginX - x ), this.mImageView.getScrollX(), this.mOverX ) ); int moveY = ( this.mOverY == 0 ? 0 : calcScrollValue( ( int )( this.mTouchBeginY - y ), this.mImageView.getScrollY(), this.mOverY ) ); this.mImageView.scrollBy( moveX, moveY ); } /** * 画像と表示領域を比較し、はみ出る量を算出します。 */ private void updateOverSize() { Display display = ( ( WindowManager )this.getSystemService( Context.WINDOW_SERVICE ) ).getDefaultDisplay(); Drawable image = this.mImageView.getDrawable(); this.mOverX = calcOverValue( display.getWidth(), image.getIntrinsicWidth() ); this.mOverY = calcOverValue( display.getHeight(), image.getIntrinsicHeight() ); } }
まず、画面と画像のサイズを比較してはみ出た分を記録するために updateOverSize メソッドを定義しておく。これを呼び出すことで、縦横にはみ出たサイズが更新される。
次に ImageView のスクロールだが、原寸表示を ScaleType.CENTER としているので、初期状態のスクロール座標は X = 0、Y = 0 となる。この数値を加算したら右・下、減算の場合は左・上へ画像が移動される。つまり、この範囲をはみ出た分量にすれば、仕様どおりの動きとなる。
スクロール操作は画像のスワイプによっておこないたいので、ImageView に関連づけた onTouch イベントでタッチ操作の分量を記録し、その都度、scrollImage メソッドを呼び出している。画面から画像がはみ出ているなら、それを上限した移動量を算出し、ImageView.scrollBy によってスクロールさせる。
scrollBy は View に定義されたメソッドで、現在のスクロール座標から指定された量だけ、位置を動かす。尚、View のスクロールは scrollBy と scrollTo の 2 種類の方法が用意されており、今回使用する前者は相対値、後者は絶対値で位置を指定する。
これらを組み合わせたサンプル アプリを実行すると以下のようになる。
この例では、800×480 の画面に 1200×900 の画像を表示し、スワイプ操作によってスクロール位置をずらしている。
最後に、サンプル アプリのプロジェクトを公開しておく。
TestImageScroll.zip 36.8KB
Android 2.1 update 1 ( API Level 7 ) でビルドし、エミュレータと初代 Xperia ( SO-01B ) にて動作確認をおこなった。
以下、余談。
サンプルに含んでいる画像は、川崎市立日本民家園で私が撮影した写真をリサイズしたもの。この施設には日本各地の貴重な家屋が移設され、それらに触れることができるという、家屋マニアにはたまらない場所である。
近くに岡本太郎美術館もあるので、両方まわると一日、たっぷり過ごせると思う。なお、日本民家園の中には食堂もあり、ここもまた貴重な家屋内となっている。写真を撮影した日はくらくらするような暑さだったため、出されたとろろ蕎麦がやけにおいしく感じたのを思い出した。