やりたいこと
- 水平方向で1ページずつ表示する
- スワイプで左右に移動する
- ページの境界線にスナップする
やってみたこと
- LinearLayoutManagerでできるだけシンプルにする
- 1ページの大きさを画面の幅に合わせる
- スクロールに加速度がつくので、速さを抑える
- スナップするようにする
構成
- ImageAdapter: 1枚の画像を画面の幅に合わせて表示するアダプタ
- HorizontalSnapLayoutManager: レイアウトマネージャ
- MainActivity: アクティビティ
ImageAdapter
1ページ毎に表示するためには、このadapterが生成するviewの幅を画面の幅にする必要があるが、RecyclerView自体の幅は大きいので、レイアウトXMLでmatch_parentを指定してもうまくいかない。そのため、生成時に幅に合わせるようにした。
幅をぴったりにすることでスナップの処理も単純になる。
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を計算させて、上限と下限を設定してみた。これで速度が抑えられた。
スクロールが止まったあと、スナップさせるときにはこの上限下限を外す。
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以上進んでいた場合はその方向にスナップするようにしてみた。
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ページに表示するレイアウト。とりあえず画像だけにしたがもちろん何を置いてもよい。
<?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