Androidアプリ開発 RecyclerView 画面スクロールしながらデータをロードする

2016年11月27日(編集2016年11月27日)
このエントリーをはてなブックマークに追加

Androidアプリで一覧データのUI(ユーザーインターフェース)を作成する場合は、ListViewかRecyclerViewを使います。
この記事は、RecyclerViewで画面スクロール中にデータをロードする方法を記載した記事です。

環境はAndroid 7.1 (API level 25) です。

環境

  • OS X El Capitan
  • android sdk 25
  • Oracle jdk version 1.8.0_72
  • Android Studio 2.2.2

難易度

中級者向け

サンプルコード

Android-RecyclerView-Demo

RecyclerView

RecyclerViewは一覧表示の実装に利用するクラスです。 ListViewを使うより、表現力のある、柔軟なユーザーインターフェースを作成できます。
今回の記事で説明をする画面スクロールしながらのデータロードは、一覧表示の実装を理解している必要があります。理解不足の場合、まずはこちらの記事を読んで理解してください。

sample app

実装

RecyclerViewの一覧実装にスクロール処理を実装します。

{project_folder}/ui/AutoScrollRecyclerViewFragment.java
public class AutoScrollRecyclerViewFragment extends Fragment {

    private AutoScrollRecyclerViewFragmentAdapter recyclerViewFragmentAdapter;
    private OnFragmentInteractionListener listener;
    private List<Item> list;
   +private long offset;
    private long autoScrollPosition;
    private boolean isLoading;
    private static final int DEFAULT_OFFSET = 20;

    /**
     * Create object with singleton.
     *
     * @return object
     */
    public static AutoScrollRecyclerViewFragment newInstance() {
        AutoScrollRecyclerViewFragment fragment = new AutoScrollRecyclerViewFragment();
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_item_list, container, false);
        initParam();
        list = new ArrayList<Item>();

        // Set the adapter
        if (view instanceof RecyclerView) {
            Context context = view.getContext();
            RecyclerView recyclerView = (RecyclerView) view;
            recyclerView.setLayoutManager(new LinearLayoutManager(context));
            recyclerViewFragmentAdapter = new AutoScrollRecyclerViewFragmentAdapter(list, listener);
            recyclerView.setAdapter(recyclerViewFragmentAdapter);
           +recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);

                    int visibleItemCount = recyclerView.getChildCount();
                    LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
                    int firstVisibleItem = manager.findFirstVisibleItemPosition();
                    int lastInScreen = firstVisibleItem + visibleItemCount;

                    if (isAutoScroll(lastInScreen)) {
                        isLoading = true;
                        load();
                    }
                }

                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                }
            });
        }
        return view;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (activity instanceof OnFragmentInteractionListener) {
            listener = (OnFragmentInteractionListener) activity;
        } else {
            throw new RuntimeException(activity.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            listener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onStart() {
        super.onStart();
       +if (autoScrollPosition == 0) {
            load();
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        list = null;
        recyclerViewFragmentAdapter = null;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    /**
     * Insert object to RecyclerView
     *
     * @param item
     */
    public void insertToRecyclerView(Item item) {
        if (list != null) {
            int index = list.indexOf(item);
            // check whether object has or not
            if (-1 == index) {
                list.add(0, item);
                recyclerViewFragmentAdapter.notifyItemInserted(0);
            }
        }
    }

    /**
     * Update object to RecyclerView
     *
     * @param item
     */
    public void updateToRecyclerView(Item item) {
        if (list != null) {
            int index = list.indexOf(item);
            // check whether object has or not
            if (-1 != index) {
                recyclerViewFragmentAdapter.notifyItemChanged(index, item);
            }
        }
    }

    /**
     * Delete object from RecyclerView
     *
     * @param item
     */
    public void deleteFromRecyclerView(Item item) {
        if (list != null) {
            int index = list.indexOf(item);
            if (-1 != index) {
                boolean isDelete = list.remove(item);
                if (isDelete) {
                    recyclerViewFragmentAdapter.notifyItemRemoved(index);
                }
            }
        }
    }

   +public void initParam() {
        list = null;
        offset = 0;
        isLoading = false;
        autoScrollPosition = 0;
    }

    private boolean isAutoScroll(int lastInScreen) {
        // If you have never loaded,  Auto Scroll do not do
        if (autoScrollPosition == 0) {
            return false;
        }

        // loading中はAutoScrollしない
        if (isLoading) {
            return false;
        }

        // 画面下でない場合は、AutoScrollしない
        if (autoScrollPosition != lastInScreen) {
            return false;
        }

        return true;
    }

    private void load() {
        if (list.size() >= 1000) {
            return;
        }
        List<Item> items = new ArrayList();
        for (int i = 0; i < DEFAULT_OFFSET; i++) {
            Item item = new Item();
            item.name = "product " + (list.size() + i);
            items.add(item);
        }
        setList(items);
    }

    private void setList(@Nullable  List<Item> items)  {
        if (items == null || items.size() == 0) {
            return;
        }

        int positionStart = list.size();
        for (Item item : items) {
            list.add(item);
        }
        recyclerViewFragmentAdapter.notifyItemRangeInserted(positionStart, items.size());

        offset = list.size();
        isLoading = false;
        autoScrollPosition += DEFAULT_OFFSET;
    }

    public interface OnFragmentInteractionListener {
        void onClickItem(Item item);

        void onClickDelete(Item item);
    }
}
        

実装は以上です。
ActivityやRecyclerView.Adapterの変更は不要です。
詳しく内容を見ていきましょう。

■ 実装の解説

1. 変数

画面スクロールロードに必要な変数を定義します。

private long offset;
private long autoScrollPosition;
private boolean isLoading;
private static final int DEFAULT_OFFSET = 20;
        

offsetは現在リストに保持しているデータの総数です。データロードごとに増加していきます。sqlのoffsetと同じ意味です。

autoScrollPositionはデータロードを実行するデータの位置です。なので、スクロール下部のデータの位置になります。

isLoadingはデータロード中はtrueに変更します。それ以外はfalseです。

DEFAULT_OFFSETは1回のスクロールごとに追加するデータの数です。sqlのlimitと同じです。

2. スクロール実装

RecyclerViewでスクロールの挙動を実装する場合は、addOnScrollListenerメソッドを使用します。

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        // ex. 2.1
        int visibleItemCount = recyclerView.getChildCount();
        LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
        // ex. 2.2
        int firstVisibleItem = manager.findFirstVisibleItemPosition();
        // ex. 2.3.
        int lastInScreen = firstVisibleItem + visibleItemCount;

        if (isAutoScroll(lastInScreen)) {
            isLoading = true;
            load();
        }
    }

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
    }
});
        

addOnScrollListenerメソッドの引数にOnScrollListenerの匿名クラスを設定します。

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    // something
});
        

2.1. LinearLayoutManagerクラスのrecyclerView.getChildCount()は現在画面に見えているアイテムの数を取得します。

sample app

画面にデータが10件表示されています。recyclerView.getChildCount()の値をconsoleに出力してみます。

recyclerView.getChildCount() : 10
        

画面のデータ表示件数が取得できました。

2.2. manager.findFirstVisibleItemPosition()は、画面表示項目の最初のindex値を返します。consoleに出力して確認します。

manager.findFirstVisibleItemPosition() : 3
        

画面表示項目の最初のindex値が取得できました。

2.3. この2つの値を加算することで、現在一番画面下に表示されているデータのindex情報が取得できます。

int lastInScreen = firstVisibleItem + visibleItemCount;
        

lastInScreenの値が、データロードを実行するデータの位置のautoScrollPositionと同じ値になった時に、次のデータ取得処理を行います。

private boolean isAutoScroll(int lastInScreen) {
    // If you have never loaded,  Auto Scroll do not do
    if (autoScrollPosition == 0) {
        return false;
    }

    // loading中はAutoScrollしない
    if (isLoading) {
        return false;
    }

    // 画面下でない場合は、AutoScrollしない
    if (autoScrollPosition != lastInScreen) {
        return false;
    }

    return true;
}
        

autoScrollPositionは、データロードを実行するデータの位置です。つまり、20件ごとにデータを取得するロード処理で、40件のリストデータを持っていれば、40 + 20 = 60 がautoScrollPositionになります。

また、ロード処理が1度だけ呼ばれるように、ガード条件が必要です。

if (isAutoScroll(lastInScreen)) {
    isLoading = true;
    load();
}
        

isLoadingがガード条件になります。isLoadingがtrueの間は、load処理は呼び出されません。

3. データ追加

RecyclerViewで追加処理を実装する場合は、RecyclerView.AdapterクラスのnotifyItemRangeInsertedメソッドを使用します。

private void setList(@Nullable  List<Item> items)  {
    if (items == null || items.size() == 0) {
        return;
    }

    int positionStart = list.size();
    for (Item item : items) {
        list.add(item);
    }
    recyclerViewFragmentAdapter.notifyItemRangeInserted(positionStart, items.size());

    offset = list.size();
    autoScrollPosition += DEFAULT_OFFSET;
    isLoading = false;
}
        

notifyItemRangeInsertedの第1引数は開始ポジションです。list.size()で取得する値が次のindexの開始位置になります。

int positionStart = list.size();
recyclerViewFragmentAdapter.notifyItemRangeInserted(positionStart, items.size());
        

第2引数は、追加するアイテム数です。これはsqlやネットワークから取得した追加のリストの数を設定します。

最後に、次のスクロールロードのために変数を更新します。
offsetに現在のアイテム数を設定します。当然、list.size()の数を設定します。
autoScrollPositionは次のスクロールポジションです。ロードで取得するデータ件数を追加します。
isLoadingはガード条件です。falseにしてロード可能の状態にします。

ビルド

上記のコードの動作を確認してみましょう。

sample app

スクロールでデータが取得可能になりました。

実装のポイント

実装のポイントは、onScrolledメソッドの処理です。

onScrolledメソッドは、画面を少しでも動かすと呼び出されるので、ガード条件の変数を使って1度だけ呼び出されるようにする必要があります。

結論

ListViewで、スクロールロードを実装したことがある人なら、それほど苦も無く実装できるはずです。
一方で、初心者の場合は相当苦労すると思うので、開発工数には余力を持ちましょう。

関連サービス

RecyclerView Generator

関連記事

タグ検索で調べてみよう

Android7.0 UI