中央にスナップしつつ横スクロールもフリックもできるView

#1 目的
中央にスナップしつつ横スクロールもフリックもできるViewを作成する

#2 目標
(1) 横スクロール

(2) 中央にスナップする（スクロールから指を離したとき、最も中央に近いアイテムのViewを中央に表示し直す）

(3) フリックでは１つずつアイテムViewを移動する

(4) 隣のアイテムViewは見えている(ViewPagerではダメ)

(5) AdapterViewで実装する

今回、(1)〜(4)までは出来ました。
(5)は保留中です。

#3 アプローチ

(1)(4) ⇒HorizontalScrollViewを使う
>HorizontalListViewなるものがあるらしいので、利用可能な状況ならそちらに置き換えたら(5)も達成なのかも。
MITライセンスっぽいので今回は使わない方向で。

(2) ⇒画面幅とアイテムのViewサイズから表示indexを計算する
先頭と最後のアイテムを中央に表示するためには、両端に必要なサイズを計算した「空」のViewを置く

(3) ⇒Gestureでなんとかする

#4 実装

```java:SwipeView.java
public class SwipeView extends HorizontalScrollView {

	int currentIndex = 0;

	private LinearLayout linearLayout;
	private int dpWidth; // ディスプレイの横サイズ
	private int ivWidth; // ImageViewの横サイズ(隙間込み)
	private int pad; // 画像を真ん中に表示させるためのpadding
	private int scrollX; // ScrollViewの座標(現在)
	/** ジェスチャー用 */
	GestureDetector gestureDetector = null;

	int itemCount; // アイテム数

	public SwipeView(Context context) {
		super(context);
	}

	public SwipeView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public SwipeView(Context context, AttributeSet attrs, int defs) {
		super(context, attrs, defs);
	}

	/** スクロールViewで制御するLinearLayoutをセットする */
	public void setChildLayout(LinearLayout linearLayout) {
		this.linearLayout = linearLayout;
		if (this.linearLayout == null) {
			this.linearLayout = (LinearLayout) getChildAt(0);
		}
		// ウィンドウマネージャのインスタンス取得
		WindowManager wm = (WindowManager) getContext().getSystemService(
				Context.WINDOW_SERVICE);
		// ディスプレイのインスタンス生成
		Display disp = wm.getDefaultDisplay();
		Point p = new Point();
		disp.getSize(p);
		dpWidth = p.x;

		// ジェスチャーの設定
		gestureDetector = new GestureDetector(getContext(), gestureListener);
	}

	/** gesture リスナー */
	private final SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {

		@Override
		public boolean onFling(MotionEvent event1, MotionEvent event2,
				float velocityX, float velocityY) {
			if (velocityX < 0) {

				// 右へフリック
				moveNextItem(1);
				return true;

			} else if (velocityX > 0) {
				// 左へフリック
				moveNextItem(-1);
				return true;
			}

			return super.onFling(event1, event2, velocityX, velocityY);
		}

		@Override
		public boolean onSingleTapUp(MotionEvent e) {
			return true;
		}
	};

	/** カレントアイテムindex */
	public int getCurrentIndex() {
		return currentIndex;
	}

	/** カレントインデックスをセット */
	public void setCurrentIndex(int index) {
		currentIndex = index;
		// その位置までスクロール
		int to = getTargetPosition(currentIndex);
		smoothScrollTo(to, 0);
	}

	@Override
	public void onWindowFocusChanged(boolean hasFocus) {
		if (this.linearLayout == null) {
			setChildLayout(null);
		}
		if (ivWidth == 0) {
			// ディスプレイサイズとImageViewの横サイズから両端の空白を計算し挿入
			itemCount = this.linearLayout.getChildCount();
			if (itemCount == 0)
				return;

			View item = linearLayout.getChildAt(0);
			ivWidth = item.getWidth();
			if (ivWidth != 0) {
				TextView view_0 = new TextView(getContext());
				TextView view_1 = new TextView(getContext());
				pad = (int) ((dpWidth - ivWidth) / 2);
				view_0.setWidth(pad);
				view_1.setWidth(pad);
				linearLayout.addView(view_0, 0);
				linearLayout.addView(view_1, linearLayout.getChildCount());
			}
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (gestureDetector.onTouchEvent(event)) {
			return true;
		}
		int action = event.getAction();
		switch (action) {
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:

			postDelayed(new Runnable() {

				@Override
				public void run() {
					
					// スクロール終了なので位置を調整
					setDisplayPosition();
				}
			}, 50);
		}
		return super.onTouchEvent(event);
	}

	private int getTargetPosition(int index) {
		int to = index == 0 ? 0
				: (pad + index * ivWidth - (dpWidth - ivWidth) / 2);
		return to;
	}

	private void setDisplayPosition() {
		scrollX = getScrollX();
		currentIndex = toIndex(); // 真ん中に表示させるべき画像のIndex
		int to = getTargetPosition(currentIndex);
		smoothScrollTo(to, 0);
	}

	private int toIndex() {
		int index = 0;
		for (int i = 0; i < itemCount; i++) {
			if (i == 0) {
				if (scrollX < pad) {
					index = i;
					break;
				}
			} else if (scrollX < pad + i * ivWidth) {
				index = i;
				break;
			}
		}
		return index;
	}

	/** 次のアイテムをカレントにセットする */
	public void moveNextItem(int move) {
		currentIndex += move;
		if (currentIndex < 0)
			currentIndex = 0;
		if (currentIndex >= itemCount)
			currentIndex = itemCount - 1;
		// その位置までスクロール
		int to = getTargetPosition(currentIndex);
		smoothScrollTo(to, 0);
	}
}
```

大量の表示ならAdapterViewにするのが必須ですが、固定メニューなど数に限りがある場合にはこんなので十分ではないかと。
なお、上記のままだと、onTouchEventで以下のようなワーニングが出ます。
気になる人はググって直して下さい。

```
Custom view XXXXXX/SwipeView overrides onTouchEvent but not performClick
```

#4 使い方

レイアウトxmlで配置して、子にアイテムViewを含んだLinearLayout(orientation="vertical")を配置しておくか(HorizontalScrollViewと同様)、プログラム内でLinearLayoutを作成、アイテムViewを追加してsetChildLayoutでセットして下さい。

xmlサンプル

```xml:activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="${relativePackage}.${activityClass}" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <com.example.swipeviewsample.SwipeView
        android:id="@+id/swipe"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/textView1" >

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:orientation="horizontal" >

            <ImageView
                android:layout_width="250dp"
                android:layout_height="250dp"
                android:src="@drawable/ic_launcher" />

            <ImageView
                android:layout_width="250dp"
                android:layout_height="250dp"
                android:src="@drawable/ic_launcher" />

            <ImageView
                android:layout_width="250dp"
                android:layout_height="250dp"
                android:src="@drawable/ic_launcher" />

            <ImageView
                android:layout_width="250dp"
                android:layout_height="250dp"
                android:src="@drawable/ic_launcher" />

            <ImageView
                android:layout_width="250dp"
                android:layout_height="250dp"
                android:src="@drawable/ic_launcher" />

        </LinearLayout>
    </com.example.swipeviewsample.SwipeView>

</RelativeLayout>
```
左端イメージ
![device-2014-11-21-212039.png](https://qiita-image-store.s3.amazonaws.com/0/4731/b31ea88f-c1ba-d240-6b00-97b67eeeb7e2.png)

中央にスナップ
![device-2014-11-21-212102.png](https://qiita-image-store.s3.amazonaws.com/0/4731/d35d6624-3c3f-89d7-c166-c61466f3c56c.png)

右端イメージ
![device-2014-11-21-212111.png](https://qiita-image-store.s3.amazonaws.com/0/4731/1ab68e49-3453-edac-37ea-e8053118d49f.png)