Edited at

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

More than 3 years have passed since last update.


1 目的

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


2 目標

(1) 横スクロール

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

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

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

(5) AdapterViewで実装する

今回、(1)〜(4)までは出来ました。

(5)は保留中です。


3 アプローチ

(1)(4) ⇒HorizontalScrollViewを使う


HorizontalListViewなるものがあるらしいので、利用可能な状況ならそちらに置き換えたら(5)も達成なのかも。

MITライセンスっぽいので今回は使わない方向で。


(2) ⇒画面幅とアイテムのViewサイズから表示indexを計算する

先頭と最後のアイテムを中央に表示するためには、両端に必要なサイズを計算した「空」のViewを置く

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


4 実装


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サンプル


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>


左端イメージ

中央にスナップ

右端イメージ