LoginSignup
32

More than 5 years have passed since last update.

RecyclerViewをFragmentPagerAdapter風にする

Last updated at Posted at 2015-07-12

やりたいこと

  • 水平方向で1ページずつ表示する
  • スワイプで左右に移動する
  • ページの境界線にスナップする

やってみたこと

  • LinearLayoutManagerでできるだけシンプルにする
  • 1ページの大きさを画面の幅に合わせる
  • スクロールに加速度がつくので、速さを抑える
  • スナップするようにする

構成

  • ImageAdapter: 1枚の画像を画面の幅に合わせて表示するアダプタ
  • HorizontalSnapLayoutManager: レイアウトマネージャ
  • MainActivity: アクティビティ

ImageAdapter

1ページ毎に表示するためには、このadapterが生成するviewの幅を画面の幅にする必要があるが、RecyclerView自体の幅は大きいので、レイアウトXMLでmatch_parentを指定してもうまくいかない。そのため、生成時に幅に合わせるようにした。
幅をぴったりにすることでスナップの処理も単純になる。

ImageAdapter.java
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
    private LayoutInflater mLayoutInflater;
    private List<Integer> mDataList;

    public ImageAdapter(Context context, List<Integer> dataList) {
        super();
        mDataList = dataList;
        mLayoutInflater = LayoutInflater.from(context);
    }

    @Override
    public ImageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = mLayoutInflater.inflate(R.layout.list_image, parent, false);

        // resize
        ViewGroup.LayoutParams params = v.getLayoutParams();
        params.width = parent.getMeasuredWidth();
        params.height = parent.getMeasuredHeight();
        v.setLayoutParams(params);

        return new ViewHolder(v);
    }

    @Override
    public int getItemCount() {
        return mDataList.size();
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.image.setImageResource(mDataList.get(position));
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView image;

        public ViewHolder(View v) {
            super(v);
            image = (ImageView) v.findViewById(R.id.imageView);
        }
    }
}

HorizontalSnapLayoutManager

何もいじらないとスクロール量が大きくすべっていくので、抑えるようにしてみた。
scrollHorizontallyByイベントでスクロール量を決める仕組みになっている。
与えられた移動係数dxから移動量travelを計算する必要がある。まじめに計算するのは大変そうなので、親クラスにtravalを計算させて、上限と下限を設定してみた。これで速度が抑えられた。
スクロールが止まったあと、スナップさせるときにはこの上限下限を外す。

HorizontalSnapLayoutManager.java
public class HorizontalSnapLayoutManager extends LinearLayoutManager {
    final static int MAX_VELOCITY = 100;
    private boolean mSnapping;

    public boolean isSnapping() { return mSnapping; }
    public void setSnapping(boolean snapping) { mSnapping = snapping; }

    public HorizontalSnapLayoutManager(Context context) {
        super(context, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    public int scrollHorizontallyBy(int dx, final RecyclerView.Recycler recycler, RecyclerView.State state) {
        int travel = super.scrollHorizontallyBy(dx, recycler, state);
        if (!isSnapping()) {
           travel = Math.max(-MAX_VELOCITY, travel);
           travel = Math.min(MAX_VELOCITY, travel);
        }
        return travel;
    }
}

MainActivity

RecyclerViewのスクロール完了でスナップする。直前の方向を保存しておき、スクロールする方向に1/4以上進んでいた場合はその方向にスナップするようにしてみた。

MainActivity.java
public class MainActivity extends ActionBarActivity {
    final static Integer ResourceIds[] = { R.drawable.earth, R.drawable.lunar_module, R.drawable.columbia };
    final static int SNAP_THRESHOLD = 4;

    private HorizontalSnapLayoutManager mLayoutManager;
    private RecyclerView mRecyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLayoutManager = new HorizontalSnapLayoutManager(this);
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setAdapter(new ImageAdapter(this, Arrays.asList(ResourceIds)));
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            private int mDirection;

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                mDirection = dx;
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    onScrollStopped(mDirection);
                }
            }
        });
    }

    void onScrollStopped(int direction) {
        int latter_pos = mLayoutManager.findLastVisibleItemPosition();
        if (latter_pos == RecyclerView.NO_POSITION) {
            return;
        }

        if (mLayoutManager.isSnapping()) {
            // snapping is finished
            mLayoutManager.setSnapping(false);
            return;
        }

        View a = mLayoutManager.getChildAt(0);      // former
        View b = mLayoutManager.getChildAt(1);      // latter

        int diff_a = a != null ? Math.abs(mLayoutManager.getDecoratedLeft(a)) : 0;
        int diff_b = b != null ? Math.abs(mLayoutManager.getDecoratedLeft(b)) : 0;
        if (diff_a > 0 && diff_b > 0) {
            int snap_to;
            if (direction < 0) {
                // left
                snap_to = (latter_pos > 0 && diff_a < diff_b * SNAP_THRESHOLD) ? (latter_pos - 1) : latter_pos;
            } else {
                snap_to = (latter_pos > 0 && diff_a * SNAP_THRESHOLD < diff_b) ? (latter_pos - 1) : latter_pos;
            }

            mLayoutManager.setSnapping(true);
            mRecyclerView.smoothScrollToPosition(snap_to);
        }
    }
}

list_image.xml

1ページに表示するレイアウト。とりあえず画像だけにしたがもちろん何を置いてもよい。

list_image.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="fill_parent">
    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/imageView"
        android:scaleType="centerCrop" />
</FrameLayout>

ソースコード

一式ここに置きました。
https://github.com/firewood/horizontal_snappy

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32