フリックでページ切り替えをしたい

ということで、GPソフトさんのページを参考にして作成してみました。

GPソフトWiki

LinearLayout ではなく、FrameLayout を継承するように変更しています。横幅を親の View から引き継ぎたかったので、FrameLayout の各層の X 座標をずらして表示するようにしています。

package jp.gr.java_conf.inosix.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.Scroller;

public class HorizontalFlingView extends FrameLayout {

    private static int MIN_FLING_MOVE = 40;

    private GestureDetector mDetector;
    private Scroller mScroller;

    public HorizontalFlingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDetector = new GestureDetector(getContext(), new GestureListener());
        mScroller = new Scroller(getContext(), new DecelerateInterpolator());
    }

    protected void onLayout(
        boolean changed, int left, int top, int right, int bottom) {
        for (int i = 0; i < getChildCount(); i++) {
            getChildAt(i).layout(
                left + i * getWidth(), top, right + i * getWidth(), bottom);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean wasFlinged = mDetector.onTouchEvent(event);
        if (wasFlinged || event.getAction() != MotionEvent.ACTION_UP) {
            return true;
        }
        int currentX = getScrollX();
        scroll(currentX, getWidth() / 2 < currentX % getWidth());
        return true;
    }

    private void scroll(int currentX, boolean toGreater) {
        int targetX = targetPage(currentX, toGreater) * getWidth();
        mScroller.startScroll(currentX, 0, targetX - currentX, 0);
        invalidate();
    }

    private int targetPage(int currentX, boolean toGreater) {
        if (currentX <= 0) {
            return 0;
        }
        if ((getChildCount() - 1) * getWidth() < currentX) {
            return getChildCount() - 1;
        }
        return (currentX / getWidth()) + (toGreater ? 1 : 0);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
        }
    }

    private class GestureListener extends SimpleOnGestureListener {

        @Override
        public boolean onFling(
            MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (!shouldReact(e1, e2, velocityX, velocityY)) {
                super.onFling(e1, e2, velocityX, velocityY);
                return false;
            }
            scroll(getScrollX(), velocityX < 0);
            return true;
        }

        private boolean shouldReact(
            MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            int dx = (int)(e2.getX() - e1.getX());
            return MIN_FLING_MOVE <= Math.abs(dx)
                    && Math.abs(velocityY) < Math.abs(velocityX);
        }

        @Override
        public boolean onScroll(
            MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            scrollBy((int)distanceX, 0);
            return true;
        }
    }
}


レイアウトは次のとおりです。FrameLayout を使ってますが、そこは何でもいいです。

<?xml version="1.0" encoding="utf-8"?>
<jp.gr.java_conf.inosix.view.HorizontalFlingView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        ...
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        ...
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        ...
    </FrameLayout>

</jp.gr.java_conf.inosix.view.HorizontalFlingView>


レイアウトまわりは詳しくないので、使うときは自己責任で。おかしなことしてたら教えてください。