2016年の11月に droid girls というAndroidの技術に特化した女性コミュニティを立ち上げました。
第2回 Meetup では私が講師を担当してRecyclerViewを取り上げたのですが、その時に SnapHelper というものを発見してしまいました。
今日はこの SnapHelper についての話です。
https://t.co/5sXrCQb7yX いつのまにか増えてた
— Yuki Anzai (@yanzm) 2016年12月21日
ちなみに第3回 Meetupでは vector drawable を取り上げます。開催は2017年1月下旬を予定しています。
本題
以下の検証は v25.1.0 で行っています。RecyclerView.OnFlingListener および関連する SnapHelper などは v24.2.0 で追加されました。
- RecyclerView.setOnFlingListener(android.support.v7.widget.RecyclerView.OnFlingListener)
- RecyclerView.OnFlingListener
- abstract class
- fling 時の挙動を定義
- つまり、このクラスのサブクラスを用意することで fling 時の挙動をカスタマイズ可能
- SnapHelper
- RecyclerView.OnFlingListener を継承した abstract class
- fling したときに子ビューに snap させる
- LayoutManager が ScrollVectorProvider を実装している前提
- つまり、独自の LayoutManager で利用する場合は ScrollVectorProvider を実装する必要がある
- LinearSnapHelper
- SnapHelperを継承したクラス
- snap 対象の View の中心が RecyclerView の中心に来るように snap する
- PagerSnapHelper
- SnapHelperを継承したクラス
- ViewPagerのような挙動を実現する
SnapHelper の abstract メソッド
- public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY)
- snap 対象の Adapter での位置を返す
- public abstract View findSnapView(LayoutManager layoutManager)
- snap 対象の View を返す
- scroll 後に scroll 状態が idle になったときと、SnapHelper.attachToRecyclerView(android.support.v7.widget.RecyclerView) を呼んだときに呼ばれる
- public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView)
- snap する位置までの距離を返す
PagerSnapHelper
PagerSnapHelper は ViewPager みたいな挙動を実現するのに使います。そのため、RecyclerView も RecyclerView.Adapter が提供する子 View も height と width が MATCH_PARENT である必要があります。使用例
- public class PagerSnapHelperActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_snap_helper);
- final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
- recyclerView.setHasFixedSize(true);
- recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
- recyclerView.setAdapter(new MyPagerAdapter());
- final PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
- pagerSnapHelper.attachToRecyclerView(recyclerView);
- }
- private static class MyPagerAdapter extends RecyclerView.Adapter<ViewHolder> {
- private static final int[] colors = {
- Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
- Color.CYAN, Color.BLUE, Color.MAGENTA, Color.LTGRAY
- };
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return ViewHolder.create(parent);
- }
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- holder.textView.setText(String.valueOf(position));
- holder.textView.setBackgroundColor(colors[position]);
- }
- @Override
- public int getItemCount() {
- return colors.length;
- }
- }
- private static class ViewHolder extends RecyclerView.ViewHolder {
- static ViewHolder create(@NonNull ViewGroup parent) {
- final TextView textView = new TextView(parent.getContext());
- textView.setTextSize(32);
- textView.setGravity(Gravity.CENTER);
- textView.setLayoutParams(new RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- return new ViewHolder(textView);
- }
- final TextView textView;
- private ViewHolder(@NonNull TextView itemView) {
- super(itemView);
- this.textView = itemView;
- }
- }
- }
findSnapView() では、RecyclerView の中心位置と各子 View の中心位置との距離を比較して、一番近い子 View を snap 対象としています。
findTargetSnapPosition() では、上端または左端にある子 View を基準に、fling 時の velocity の正負に応じて隣の子 View の位置を返しています。velocity の絶対値は使っていないので、弱く fling しても強く fling しても隣のページに移動するだけです。
calculateDistanceToFinalSnap() では RecyclerView の中心と snap 対象の View の中心との差を返しています。
LinearSnapHelper
LinearSnapHelper は snap 対象の View の中心が RecyclerView の中心に来るように snap します。使用例
- public class LinearSnapHelperActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_snap_helper);
- final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
- recyclerView.setHasFixedSize(true);
- recyclerView.setLayoutManager(new LinearLayoutManager(this));
- recyclerView.setAdapter(new MyPagerAdapter());
- final LinearSnapHelper linearSnapHelper = new LinearSnapHelper();
- linearSnapHelper.attachToRecyclerView(recyclerView);
- }
- private static class MyPagerAdapter extends RecyclerView.Adapter<ViewHolder> {
- private static final int[] colors = {
- Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
- Color.CYAN, Color.BLUE, Color.MAGENTA, Color.LTGRAY,
- Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
- Color.CYAN, Color.BLUE, Color.MAGENTA, Color.LTGRAY
- };
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return ViewHolder.create(parent);
- }
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- holder.textView.setText(String.valueOf(position));
- holder.textView.setBackgroundColor(colors[position]);
- }
- @Override
- public int getItemCount() {
- return colors.length;
- }
- }
- private static class ViewHolder extends RecyclerView.ViewHolder {
- static ViewHolder create(@NonNull ViewGroup parent) {
- final TextView textView = new TextView(parent.getContext());
- textView.setTextSize(32);
- textView.setGravity(Gravity.CENTER);
- int height = parent.getMeasuredHeight() / 4;
- textView.setLayoutParams(new RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, height));
- return new ViewHolder(textView);
- }
- final TextView textView;
- private ViewHolder(@NonNull TextView itemView) {
- super(itemView);
- this.textView = itemView;
- }
- }
- }
findSnapView() では、RecyclerView の中心位置と各子 View の中心位置との距離を比較して、一番近い子 View を snap 対象としています。
findTargetSnapPosition() では、velocity の大きさから対応するスクロール量を計算し、1子ビューあたりの大きさからスクロールで移動する子ビューの数を計算し、現在の位置から移動する子ビューの数だけ離れた位置を返しています。
calculateDistanceToFinalSnap() では RecyclerView の中心と snap 対象の View の中心との差を返しています。
上端に snap する SnapHelper
LinearSnapHelper が中心に snap するので、そのコードを参考に center を計算する部分を top に変えれば、上端(start)に snap するようにできます。- public class MyLinearSnapHelper extends SnapHelper {
- ...
- @Override
- public int[] calculateDistanceToFinalSnap(
- @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
- int[] out = new int[2];
- if (layoutManager.canScrollHorizontally()) {
- out[0] = distanceToTop(layoutManager, targetView,
- getHorizontalHelper(layoutManager));
- } else {
- out[0] = 0;
- }
- if (layoutManager.canScrollVertically()) {
- out[1] = distanceToTop(layoutManager, targetView,
- getVerticalHelper(layoutManager));
- } else {
- out[1] = 0;
- }
- return out;
- }
- ...
- @Override
- public View findSnapView(RecyclerView.LayoutManager layoutManager) {
- if (layoutManager.canScrollVertically()) {
- return findTopView(layoutManager, getVerticalHelper(layoutManager));
- } else if (layoutManager.canScrollHorizontally()) {
- return findTopView(layoutManager, getHorizontalHelper(layoutManager));
- }
- return null;
- }
- private int distanceToTop(@NonNull RecyclerView.LayoutManager layoutManager,
- @NonNull View targetView, OrientationHelper helper) {
- final int childTop = helper.getDecoratedStart(targetView);
- final int containerTop;
- if (layoutManager.getClipToPadding()) {
- containerTop = helper.getStartAfterPadding();
- } else {
- containerTop = 0;
- }
- return childTop - containerTop;
- }
- ...
- @Nullable
- private View findTopView(RecyclerView.LayoutManager layoutManager,
- OrientationHelper helper) {
- int childCount = layoutManager.getChildCount();
- if (childCount == 0) {
- return null;
- }
- View closestChild = null;
- final int top;
- if (layoutManager.getClipToPadding()) {
- top = helper.getStartAfterPadding();
- } else {
- top = 0;
- }
- int absClosest = Integer.MAX_VALUE;
- for (int i = 0; i < childCount; i++) {
- final View child = layoutManager.getChildAt(i);
- int childTop = helper.getDecoratedStart(child);
- int absDistance = Math.abs(childTop - top);
- /** if child top is closer than previous closest, set it as closest **/
- if (absDistance < absClosest) {
- absClosest = absDistance;
- closestChild = child;
- }
- }
- return closestChild;
- }
- ...
- }