Androidアプリ開発 RecyclerView フッターを作成する

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

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実装

RecyclerViewを利用しているFragmentにフッター処理を実装します。

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

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

    /**
     * Create object with singleton.
     *
     * @return object
     */
    public static FooterRecyclerViewFragment newInstance() {
        FooterRecyclerViewFragment fragment = new FooterRecyclerViewFragment();
        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 FooterRecyclerViewFragmentAdapter(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 (autoScrollSize == 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;
        autoScrollSize = 0;
    }

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

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

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

        return true;
    }

    private void load() {
        // sample data 1,000 count
        int size = DEFAULT_OFFSET;
        if (list.size() > 200) {
            size = 7;
        }

        List<Item> items = new ArrayList();
        for (int i = 0; i < size; i++) {
            Item item = new Item();
            item.name = "Item " + (list.size() + i);
            items.add(item);
        }

        setList(items);
    }

    private void setList(@Nullable List<Item> items) {
        // データがない
        if ((items == null || items.size() == 0) && list.size() == 0) {
            return;
        }

       +// delete old footer
        deleteOldFooter();
        // add new footer
        addNewFooter(items);

        // リストにアイテムを追加
        int positionStart = list.size();
        for (Item item : items) {
            list.add(item);
        }
        recyclerViewFragmentAdapter.notifyItemRangeInserted(positionStart, items.size());

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

   +/**
     * 古いfooterを削除
     */
    private void deleteOldFooter() {
        // 古いfooterを削除する
        if (list.size() > 0) {
            Item item = list.get(list.size() - 1);
            if (item.isFooter() || item.isLoadingFooter()) {
                list.remove(item);
            }
        }
    }

    /**
     * 新しいfooterを追加
     *
     * @param items
     */
    private void addNewFooter(@Nullable List<Item> items) {
        // 追加項目から表示するfooterの種類を決定する
        int add_item_size = items.size();
        // offset以下
        Item item_footer = new Item();
        if (add_item_size < DEFAULT_OFFSET) {
            item_footer.type = FooterRecyclerViewFragmentAdapter.TYPE_FOOTER;
        } else if (add_item_size == DEFAULT_OFFSET) {
            item_footer.type = FooterRecyclerViewFragmentAdapter.TYPE_LOADING_FOOTER;
        }
        items.add(items.size(), item_footer);
    }

    public interface OnFragmentInteractionListener {
        void onClickItem(Item item);

        void onClickDelete(Item item);
    }
}
        

フッターは2種類実装しています。一つはデータローディング時のフッターで、もう一つは全てのデータ表示後のフッターです。

■ 実装の解説

1. フッター追加処理

フッター追加処理を実装します。
リストデータを取得後に以下の処理を追加します。

private void addNewFooter(@Nullable List<Item> items) {
    // 追加項目から表示するfooterの種類を決定する
    int add_item_size = items.size();
    // offset以下
    Item item_footer = new Item();
    if (add_item_size < DEFAULT_OFFSET) {
        item_footer.type = FooterRecyclerViewFragmentAdapter.TYPE_FOOTER;
    } else if (add_item_size == DEFAULT_OFFSET) {
        item_footer.type = FooterRecyclerViewFragmentAdapter.TYPE_LOADING_FOOTER;
    }
    items.add(items.size(), item_footer);
}
        

RecyclerView自身は、フッター処理がありません。なので、フッター処理はRecyclerViewに実装するのではなく、リストデータにフッターデータを追加して対応します。このサンプルでは、最後のデータの次にフッター用のデータを追加します。

index object 説明
0 item 最初のデータ
1 item 2番目のデータ
... item
19 item 最後(20番目)のデータ
20 footer フッター用のデータ

フッターの種類は2種類用意します。一つはデータLoading時のフッターで、もう一つは全てのデータ表示後のフッターです。

if (add_item_size < DEFAULT_OFFSET) {
    item_footer.type = FooterRecyclerViewFragmentAdapter.TYPE_FOOTER;
} else if (add_item_size == DEFAULT_OFFSET) {
    item_footer.type = FooterRecyclerViewFragmentAdapter.TYPE_LOADING_FOOTER;
}
        

追加データ件数が、一度に取得する最大件数と同じ場合は、さらに追加データがあるので、progress Barを利用するフッター(TYPE_LOADING_FOOTER)を使います。

例:
全データ : 100件

最初のロード : 1..20 (20件取得)

次のロード条件 (取得した件数 == 1度に取得できるデータ件数)

2回目のロード load : 21..40
        
LOADING FOOTER

追加データ件数が、一度に取得する最大件数より少ない場合は、追加データは存在しないので、progress Barのない通常フッター(TYPE_FOOTER)を使います。

例:
全データ : 38件

2回目のロード load : 21..38 (18件取得)

次のロード条件 (取得した件数 < 1度に取得できるデータ件数)

3回目のロード なし
        
FOOTER

2. フッター削除処理

フッター削除処理を実装します。

新しいリストデータを取得後に、以前のリストデータに含まれるフッターデータを削除します。

private void deleteOldFooter() {
    // delete old footer
    if (list.size() > 0) {
        Item item = list.get(list.size() - 1);
        if (item.isFooter() || item.isLoadingFooter()) {
            list.remove(item);
        }
    }
}
        

リストのフッターはindexの最後の位置になります。

index object 説明
0 item 最初のデータ
1 item 2番目のデータ
... item
19 item 最後(20番目)のデータ
20 footer フッター用のデータ
Item item = list.get(list.size() - 1);
if (item.isFooter() || item.isLoadingFooter()) {
    list.remove(item);
}
        
例:
list : 0,1...19,footer
delete footer
list : 0,1...19
add new list 
list : 0,1...20,21...39
add new footer
list : 0,1...20,21...39,footer
        

RecyclerView.Adapter実装

RecyclerView.Adapterにフッター処理を実装します。
RecyclerView.Adapterは、データ単位でviewオブジェクトを管理します。

{project_folder}/ui/FooterRecyclerViewFragmentAdapter.java
public class FooterRecyclerViewFragmentAdapter extends RecyclerView.Adapter<FooterRecyclerViewFragmentAdapter.ViewHolder> {

   +public final static int TYPE_LIST = 1;
    public final static int TYPE_LOADING_FOOTER = 2;
    public final static int TYPE_FOOTER = 3;

    private final List<Item> items;
    private final FooterRecyclerViewFragment.OnFragmentInteractionListener listener;

    public FooterRecyclerViewFragmentAdapter(List<Item> items, FooterRecyclerViewFragment.OnFragmentInteractionListener listener) {
        this.items = items;
        this.listener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view;
       +if (viewType == TYPE_FOOTER) {
            view = LayoutInflater.from(parent.getContext())
                    .inflate(layout_item_footer, parent, false);
        } else if (viewType == TYPE_LOADING_FOOTER) {
            view = LayoutInflater.from(parent.getContext())
                    .inflate(layout_item_loading_footer, parent, false);
        } else {
            view = LayoutInflater.from(parent.getContext())
                    .inflate(fragment_item, parent, false);
        }
        return new ViewHolder(view, viewType);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
       +if (TYPE_FOOTER == holder.viewType || TYPE_LOADING_FOOTER == holder.viewType) {
            return;
        }

        holder.item = items.get(position);
        holder.content.setText(items.get(position).name);

        holder.view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != listener) {
                    listener.onClickItem(holder.item);
                }
            }
        });
    }

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

   +@Override
    public int getItemViewType(int position) {
        if (items.get(position).isLoadingFooter()) {
            return TYPE_LOADING_FOOTER;
        } else if (items.get(position).isFooter()) {
            return TYPE_FOOTER;
        } else {
            return TYPE_LIST;
        }
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        public final View view;
        public final TextView content;
        public Item item;
        public int viewType;

       +public ViewHolder(View view, int viewType) {
            super(view);
            this.view = view;
            this.viewType = viewType;
            content = (TextView) view.findViewById(R.id.content);
        }
    }
}
        

■ 実装の解説

1. 定数追加

定数を追加します。定数はviewの種類です。

public final static int TYPE_LIST = 1;
public final static int TYPE_LOADING_FOOTER = 2;
public final static int TYPE_FOOTER = 3;
        

TYPE_LISTは、画面に表示するデータ項目です。

TYPE_LOADING_FOOTERは、画面下で表示するローディングフッターです。

TYPE_FOOTERは、全てのデータを取得後の画面下に表示するフッターです。

2. viewの種類の決定

一覧表示は、リストに含まれているデータによって表示するviewを変更する必要があります。こういった場合、getItemViewTypeメソッドをオーバーライドします。

@Override
public int getItemViewType(int position) {
    if (items.get(position).isLoadingFooter()) {
        return TYPE_LOADING_FOOTER;
    } else if (items.get(position).isFooter()) {
        return TYPE_FOOTER;
    } else {
        return TYPE_LIST;
    }
}
        

isLoadingFooterメソッドやisFooterメソッドを呼び出してデータの種類を判断します。

これらのメソッドはアイテムオブジェクトで実装します。

/**
 * if footer is true, else false.
 *
 * @return
 */
public boolean isFooter() {
    if (type == UI.TYPE_FOOTER) {
        return true;
    }
    return false;
}

/**
 * if loading footer is true, else false.
 *
 * @return
 */
public boolean isLoadingFooter() {
    if (type == UI.TYPE_LOADING_FOOTER) {
        return true;
    }
    return false;
}
        

3. viewの生成

viewの生成は、onCreateViewHolderメソッドを使います。
引数viewTypeで生成するオブジェクトを切り替えます。

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view;
    if (viewType == TYPE_FOOTER) {
        view = LayoutInflater.from(parent.getContext())
                .inflate(layout_item_footer, parent, false);
    } else if (viewType == TYPE_LOADING_FOOTER) {
        view = LayoutInflater.from(parent.getContext())
                .inflate(layout_item_loading_footer, parent, false);
    } else {
        view = LayoutInflater.from(parent.getContext())
                .inflate(fragment_item, parent, false);
    }
    return new ViewHolder(view, viewType);
}
        

4. viewのデータ設定

viewのデータ設定は、onBindViewHolderメソッドを使います。
フッターの場合は、何もしません。

if (TYPE_FOOTER == holder.viewType || TYPE_LOADING_FOOTER == holder.viewType) {
    return;
}
        

ビルド

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

sample app

二種類のフッターが実装できました。

実装のポイント

実装のポイントは、データで表示オブジェクトを決めることです。

RecyclerViewは一覧データを表示するために使います。しかし、画面にヘッダーやフッターが必要になると、ヘッダーとフッターを画面の一部と認識し、データと分離して考えてしまいがちです。

しかし、RecyclerViewでは、ヘッダーやフッターもデータの一部と捉えます。なので、リストに含まれているデータによってviewを切り替えると考えると実装しやすいと思います。

結論

ListViewに慣れていると、RecyclerViewでフッターを実装する場合に、考え方を変える必要があります。
しかし、RecyclerViewは柔軟なので、慣れると色々なUIを作成することができます。
色々と実装して、実装可能なパターンを増やしていってください。

Related Service

RecyclerView Generator

関連記事

タグ検索で調べてみよう

Android7.0 UI