[Из песочницы] Очень удобный абстрактный адаптер для RecyclerView своими руками

Когда-то, на заре моей карьеры Android-разработчиком, я просматривал примеры уже имеющихся приложений и в прекрасном, словно солнышко весной, U2020 я нашел пример очень удобного адаптера. Имя его BindableAdapter. Долгое время я использовал его как основу своих адаптеров для ListView и GridView, но их эра приходит к своему закату и наступает эра RecyclerView. Этой эре нужен новый удобный адаптер, и я попытался его сделать.
Для начала я решил спросить у Google «Где ж моя любимая?» «RecyclerBindableAdapter», и ответа он мне не дал. Я честно попытался найти адаптер, с которым мне было бы удобно работать, но все было тщетно. Все найденные мной примеры реализовывали лишь конкретный функционал и не старались быть абстрактными. Да и, честно говоря, многие моменты в них меня смущали, к примеру: во многих из них в конструктор зачем-то передавались Context и LayoutManager. Хотя у RecyclerView.Adapter есть прекрасный метод onAttachedToRecyclerView(RecyclerView recyclerView), а из полученного recyclerView можно без лишнего шума и пыли получить и Context и LayoutManager.

Все это наводило меня на мысли, что они написаны скорее на скорую руку, чем вдумчиво. Так как RecyclerView использую повсеместно и каждый раз плодить кучу похожего кода как-то не комильфо, то я решил исправить ситуацию и написать удобный абстрактный адаптер.

А что вообще собственно нужно?


Портировать тот самый BindableAdapter под RecyclerView, при этом постараться максимально использовать все новые возможности RecyclerView. Как всегда есть одно «но»: RecyclerView по умолчанию не имеет Header'ов и Footer'ов. Следовательно когда будущий пользователь постарается исправить эту проблему создав новый тип элемента, из-за этого номер позиции сместится на количество Header'ов. Это плохо. Значит нужно заранее сделать возможность работы с Header'ами и Footer'ами, а также предусмотреть их наличие в методах работы с данными.

Потом я подумал, что неплохо бы было реализовать и другие возможности RecyclerView которыми я часто пользуюсь, а именно: фильтрация элементов и параллакс Header'а. Тут тоже не все так гладко.

Адаптер предусматривающий фильтрацию вынужден хранить два списка объектов (общий список и список тех элементов, что сейчас отображаются). Следовательно он занимает больше памяти, да и методы для работы с элементами будут работать медленнее. Незачем заранее забивать память лишними данными, да и фильтрация нужна не так уж часто. Поэтому было принято решение сделать его отдельным классом расширяющим наш основной адаптер. Таким образом мы будем использовать этот адаптер только тогда, когда нам необходима фильтрация, а в остальных случаях будем использовать основной адаптер.

Похожая ситуация сложилась и с параллаксом Header'a. Основной адаптер может иметь множество Header'ов и Footer'ов. Реализовать параллакс сразу нескольких элементов синхронно скорее всего будет проблематично, да и выглядеть будет некрасиво, так что не смысла даже пытаться. Делаем его так же отдельным классом.

На этом этапе я подумал, что неплохо было бы иметь некий аналог SimpleAdapter. Такой, чтоб его можно было объявить одной строкой, просто подсунув в него Layout ID и ViewHolder. Ведь зачем плодить кучу кода для простого списка.

В итоге задача сводилась к созданию 4 адаптеров:

  1. RecyclerBindableAdapter — наш основной адаптер
  2. FilterBindableAdapter — адаптер с возможностью фильтрации
  3. ParallaxBindableAdapter — адаптер с возможностью параллакса Header'а
  4. SimpleBindableAdapter — простой адаптер


Приступаем


RecyclerBindableAdapter


Для начала сделаем возможным добавление Header'ов и Footer'ов, подобную реализацию я когда-то встретил на просторах StackOverflow и подкорректировал по нужды нашего адаптера. Я думаю подобную реализацию видели или делали большинство людей работающих с Android, поэтому подробно останавливаться на ней не буду.

Делаем возможным добавление header'ов и footer'ов
//типы для наших Header'ов и Footer'ов
public static final int TYPE_HEADER = 7898;
public static final int TYPE_FOOTER = 7899;
//коллекции где они будут храниться
private List<View> headers = new ArrayList<>();
private List<View> footers = new ArrayList<>();

@Override
public VH onCreateViewHolder(ViewGroup viewGroup, int type) {
    //если это обычный элемент, то создаем обычный ViewHolder
    if (type != TYPE_HEADER && type != TYPE_FOOTER) {
        return (VH) onCreteItemViewHolder(viewGroup, type);
        //иначе это header или footer
    } else {
        //создаем новый FrameLayout в качестве контейнера, это нужно потому, что нам может попасться обычный View 
        FrameLayout frameLayout = new FrameLayout(viewGroup.getContext());
        frameLayout.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        //создаем специальный ViewHolder для header'ов и footer'ов. Он ничем не примечателен, разве что помещает все содержимое в созданный нами FrameLayout
        return (VH) new HeaderFooterViewHolder(frameLayout);
    }
}

@Override
final public void onBindViewHolder(final RecyclerView.ViewHolder vh, int position) {
    //Проверяем какой тип элемента в данной позиции и в зависимости от типа работаем с ним либо как с header/footer, либо как с обычным элементом
    if (isHeader(position)) {
        View v = headers.get(position);
        prepareHeaderFooter((HeaderFooterViewHolder) vh, v);
    } else if (isFooter(position)) {
        View v = footers.get(position - getRealItemCount() - getHeadersCount());
        prepareHeaderFooter((HeaderFooterViewHolder) vh, v);
    } else {
        onBindItemViewHolder((VH) vh, position - headers.size(), getItemType(position));
    }
}

@Override
final public int getItemViewType(int position) {
    //Проверяем какой тип на данной позиции
    if (isHeader(position)) {
        return TYPE_HEADER;
    } else if (isFooter(position)) {
        return TYPE_FOOTER;
    }
    int type = getItemType(position);
    if (type == TYPE_HEADER || type == TYPE_FOOTER) {
        throw new IllegalArgumentException("Item type cannot equal " + TYPE_HEADER + " or " + TYPE_FOOTER);
    }
    return type;
}



Теперь нужно сделать методы для работы с данными, названия для них возьмем из BindableAdapter для ListView/GridView.

Работа с данными
//инициализируем список наших элементов
private List<T> items = new ArrayList<>();

//метод возвращающий кол-во элементов в списке не учитывая header'ы и footer'ы
public int getRealItemCount() {
    return items.size();
}

//возвращаем элемент на выбранной позиции
public T getItem(int position) {
    return items.get(position);
}

//добавляем элемент в выбранную позицию
public void add(int position, T item) {
    //добавляем элемент в список
    items.add(position, item);
    //анимируем вставку элемента
    notifyItemInserted(position);
    //так как возможно данные во ViewHolder основаны на их позиции, то нужно обновить все элементы идущие после вставленного
    int positionStart = position + getHeadersCount();
    int itemCount = items.size() - position;
    notifyItemRangeChanged(positionStart, itemCount);
}

//добавляем элемент в конец списка
public void add(T item) {
    //добавляем элемент в список
    items.add(item);
    //анимируем вставку элемента
    notifyItemInserted(items.size() - 1 + getHeadersCount());
}

//добавляем List элементов в конец списка, этот метод отличается от аналогичного в List, тем что предварительно не отчищает список. Это сделано для удобства, так как по моему опыту простое добавление в конец нужно чаще, чем очистка и добавление
public void addAll(List<? extends T> items) {
    final int size = this.items.size();
    //добавляем элементы в список
    this.items.addAll(items);
    //анимируем вставку элементов
    notifyItemRangeInserted(size + getHeadersCount(), items.size());
}

//заменяем элемент в указанной позиции
public void set(int position, T item) {
    //заменяем элемент в списке
    items.set(position, item);
    //так как возможно данные во ViewHolder основаны на их позиции, то нужно обновить все элементы идущие после замененного
    int positionStart = position + getHeadersCount();
    int itemCount = items.size() - position;
    //анимируем замену
    notifyItemRangeChanged(positionStart, itemCount);
}

//удаление элемента с указанной позиции
public void removeChild(int position) {
    //удаляем элемент из списка
    items.remove(position);
    //анимируем удаление
    notifyItemRemoved(position + getHeadersCount());
    //так как возможно данные во ViewHolder основаны на их позиции, то нужно обновить все элементы идущие после удаленного
    int positionStart = position + getHeadersCount();
    int itemCount = items.size() - position;
    notifyItemRangeChanged(positionStart, itemCount);
}

//очищаем данные в адаптере оставляем только Header'ы и Footer'ы
public void clear() {
    final int size = items.size();
    //очищаем список
    items.clear();
    //анимируем удаление элементов
    notifyItemRangeRemoved(getHeadersCount(), size);
}

//перемещение элемента с одной позиции на другую
public void moveChildTo(int fromPosition, int toPosition) {
    //проверяем можем ли мы переместить элемент в указанную позицию 
    if (toPosition != -1 && toPosition < items.size()) {
        //удаляем элемент с одной позиции...
        final T item = items.remove(fromPosition);
        //и добавляем в другой
        items.add(toPosition, item);
        //анимируем перемещение
        notifyItemMoved(getHeadersCount() + fromPosition, getHeadersCount() + toPosition);
        //так как возможно данные во ViewHolder основаны на их позиции, то нужно обновить все элементы идущие после перемещенного
        int positionStart = fromPosition < toPosition ? fromPosition : toPosition;
        int itemCount = Math.abs(fromPosition - toPosition) + 1;
        notifyItemRangeChanged(positionStart + getHeadersCount(), itemCount);
    }
}

//возвращает позицию элемента
public int indexOf(T object) {
    return items.indexOf(object);
}



Ну вот в принципе мы и закончили наш RecyclerBindableAdapter. Полный текст можно посмотреть тут или под спойлером.

RecyclerBindableAdapter
public abstract class RecyclerBindableAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {

    public static final int TYPE_HEADER = 7898;
    public static final int TYPE_FOOTER = 7899;

    private List<View> headers = new ArrayList<>();
    private List<View> footers = new ArrayList<>();
    private List<T> items = new ArrayList<>();

    private RecyclerView.LayoutManager manager;
    private LayoutInflater inflater;
    private GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            return getGridSpan(position);
        }
    };

    public int getRealItemCount() {
        return items.size();
    }

    public T getItem(int position) {
        return items.get(position);
    }

    public void add(int position, T item) {
        items.add(position, item);
        notifyItemInserted(position);
        int positionStart = position + getHeadersCount();
        int itemCount = items.size() - position;
        notifyItemRangeChanged(positionStart, itemCount);
    }

    public void add(T item) {
        items.add(item);
        notifyItemInserted(items.size() - 1 + getHeadersCount());
    }

    public void addAll(List<? extends T> items) {
        final int size = this.items.size();
        this.items.addAll(items);
        notifyItemRangeInserted(size + getHeadersCount(), items.size());
    }

    public void set(int position, T item) {
        items.set(position, item);
        int positionStart = position + getHeadersCount();
        int itemCount = items.size() - position;
        notifyItemRangeChanged(positionStart, itemCount);
    }

    public void removeChild(int position) {
        items.remove(position);
        notifyItemRemoved(position + getHeadersCount());
        int positionStart = position + getHeadersCount();
        int itemCount = items.size() - position;
        notifyItemRangeChanged(positionStart, itemCount);
    }

    public void clear() {
        final int size = items.size();
        items.clear();
        notifyItemRangeRemoved(getHeadersCount(), size);
    }

    public void moveChildTo(int fromPosition, int toPosition) {
        if (toPosition != -1 && toPosition < items.size()) {
            final T item = items.remove(fromPosition);
            items.add(toPosition, item);
            notifyItemMoved(getHeadersCount() + fromPosition, getHeadersCount() + toPosition);
            int positionStart = fromPosition < toPosition ? fromPosition : toPosition;
            int itemCount = Math.abs(fromPosition - toPosition) + 1;
            notifyItemRangeChanged(positionStart + getHeadersCount(), itemCount);
        }
    }

    //@TODO need test
    public int indexOf(T object) {
        return items.indexOf(object);
    }

    @Override
    public VH onCreateViewHolder(ViewGroup viewGroup, int type) {
        //if our position is one of our items (this comes from getItemViewType(int position) below)
        if (type != TYPE_HEADER && type != TYPE_FOOTER) {
            return (VH) onCreteItemViewHolder(viewGroup, type);
            //else we have a header/footer
        } else {
            //create a new framelayout, or inflate from a resource
            FrameLayout frameLayout = new FrameLayout(viewGroup.getContext());
            //make sure it fills the space
            frameLayout.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            return (VH) new HeaderFooterViewHolder(frameLayout);
        }
    }

    @Override
    final public void onBindViewHolder(final RecyclerView.ViewHolder vh, int position) {
        //check what type of view our position is
        if (isHeader(position)) {
            View v = headers.get(position);
            //add our view to a header view and display it
            prepareHeaderFooter((HeaderFooterViewHolder) vh, v);
        } else if (isFooter(position)) {
            View v = footers.get(position - getRealItemCount() - getHeadersCount());
            //add our view to a footer view and display it
            prepareHeaderFooter((HeaderFooterViewHolder) vh, v);
        } else {
            //it's one of our items, display as required
            onBindItemViewHolder((VH) vh, position - headers.size(), getItemType(position));
        }
    }

    private void prepareHeaderFooter(HeaderFooterViewHolder vh, View view) {
        //if it's a staggered grid, span the whole layout
        if (manager instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            layoutParams.setFullSpan(true);
            vh.itemView.setLayoutParams(layoutParams);
        }
        //if the view already belongs to another layout, remove it
        if (view.getParent() != null) {
            ((ViewGroup) view.getParent()).removeView(view);
        }
        //empty out our FrameLayout and replace with our header/footer
        ((ViewGroup) vh.itemView).removeAllViews();
        ((ViewGroup) vh.itemView).addView(view);
    }

    private boolean isHeader(int position) {
        return (position < headers.size());
    }

    private boolean isFooter(int position) {
        return footers.size() > 0 && (position >= getHeadersCount() + getRealItemCount());
    }

    protected VH onCreteItemViewHolder(ViewGroup parent, int type) {
        return viewHolder(inflater.inflate(layoutId(type), parent, false), type);
    }

    @Override
    public int getItemCount() {
        return headers.size() + getRealItemCount() + footers.size();
    }

    @Override
    final public int getItemViewType(int position) {
        //check what type our position is, based on the assumption that the order is headers > items > footers
        if (isHeader(position)) {
            return TYPE_HEADER;
        } else if (isFooter(position)) {
            return TYPE_FOOTER;
        }
        int type = getItemType(position);
        if (type == TYPE_HEADER || type == TYPE_FOOTER) {
            throw new IllegalArgumentException("Item type cannot equal " + TYPE_HEADER + " or " + TYPE_FOOTER);
        }
        return type;
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        if (manager == null) {
            setManager(recyclerView.getLayoutManager());
        }
        if (inflater == null) {
            this.inflater = LayoutInflater.from(recyclerView.getContext());
        }
    }

    private void setManager(RecyclerView.LayoutManager manager) {
        this.manager = manager;
        if (this.manager instanceof GridLayoutManager) {
            ((GridLayoutManager) this.manager).setSpanSizeLookup(spanSizeLookup);
        } else if (this.manager instanceof StaggeredGridLayoutManager) {
            ((StaggeredGridLayoutManager) this.manager).setGapStrategy(
                    StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
        }
    }

    protected int getGridSpan(int position) {
        if (isHeader(position) || isFooter(position)) {
            return getMaxGridSpan();
        }
        position -= headers.size();
        if (getItem(position) instanceof SpanItemInterface) {
            return ((SpanItemInterface) getItem(position)).getGridSpan();
        }
        return 1;
    }

    protected int getMaxGridSpan() {
        if (manager instanceof GridLayoutManager) {
            return ((GridLayoutManager) manager).getSpanCount();
        } else if (manager instanceof StaggeredGridLayoutManager) {
            return ((StaggeredGridLayoutManager) manager).getSpanCount();
        }
        return 1;
    }

    //add a header to the adapter
    public void addHeader(View header) {
        if (!headers.contains(header)) {
            headers.add(header);
            //animate
            notifyItemInserted(headers.size() - 1);
        }
    }

    //@TODO need test
    public void removeHeader(View header) {
        if (headers.contains(header)) {
            //animate
            notifyItemRemoved(headers.indexOf(header));
            headers.remove(header);
        }
    }

    //add a footer to the adapter
    public void addFooter(View footer) {
        if (!footers.contains(footer)) {
            footers.add(footer);
            //animate
            notifyItemInserted(headers.size() + getItemCount() + footers.size() - 1);
        }
    }

    //@TODO need test
    public void removeFooter(View footer) {
        if (footers.contains(footer)) {
            //animate
            notifyItemRemoved(headers.size() + getItemCount() + footers.indexOf(footer));
            footers.remove(footer);
        }
    }

    public int getHeadersCount() {
        return headers.size();
    }

    protected View getHeader(int location) {
        return headers.get(location);
    }

    public int getFootersCount() {
        return footers.size();
    }

    protected View getFooter(int location) {
        return footers.get(location);
    }

    protected int getItemType(int position) {
        return 0;
    }

    abstract protected void onBindItemViewHolder(VH viewHolder, int position, int type);

    protected abstract VH viewHolder(View view, int type);

    protected abstract
    @LayoutRes
    int layoutId(int type);

    public interface SpanItemInterface {
        int getGridSpan();
    }

    //our header/footer RecyclerView.ViewHolder is just a FrameLayout
    public static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {

        public HeaderFooterViewHolder(View itemView) {
            super(itemView);
        }
    }
}



Теперь создадим какой-нибудь пример:

public class LinearExampleAdapter extends RecyclerBindableAdapter<Integer, LinearViewHolder> {
    private LinearViewHolder.ActionListener actionListener;

    //задаем layout id для нашего элемента
    @Override
    protected int layoutId(int type) {
        return R.layout.linear_example_item;
    }

    //Создаем ViewHolder
    @Override
    protected LinearViewHolder viewHolder(View view, int type) {
        return new LinearViewHolder(view);
    }

    //Изменяем данные внутри элемента
    @Override
    protected void onBindItemViewHolder(LinearViewHolder viewHolder, final int position, int type) {
        viewHolder.bindView(getItem(position), position, actionListener);
    }

    //интерфейс для обработки событий
    public void setActionListener(LinearViewHolder.ActionListener actionListener) {
        this.actionListener = actionListener;
    }
}


ViewHolder (Осторожно Butterknife 7)
public class LinearViewHolder extends RecyclerView.ViewHolder {

    @Bind(R.id.linear_example_item_text)
    TextView text;

    private int position;
    private ActionListener actionListener;

    public LinearViewHolder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
    }


    public void bindView(Integer item, int position, ActionListener listener) {
        actionListener = listener;
        this.position = position;
        text.setText(String.valueOf(item));
    }

    @OnClick(R.id.linear_example_item_move_to_top)
    protected void OnMoveToTopClick() {
        if (actionListener != null) {
            actionListener.onMoveToTop(position);
        }
    }

    @OnClick(R.id.linear_example_item_remove)
    protected void OnRemoveClick() {
        if (actionListener != null) {
            actionListener.OnRemove(position);
        }
    }

    @OnClick(R.id.linear_example_item_up)
    protected void OnUpClick() {
        if (actionListener != null) {
            actionListener.OnUp(position);
        }
    }

    @OnClick(R.id.linear_example_item_down)
    protected void OnDownClick() {
        if (actionListener != null) {
            actionListener.OnDown(position);
        }
    }

    public interface ActionListener {
        void onMoveToTop(int position);

        void OnRemove(int position);

        void OnUp(int position);

        void OnDown(int position);
    }
}



Вот так вот, все теперь просто. Работа с адаптером упростилась в разы. Здесь вы можете найти этот пример, а вот здесь пример для нескольких типов.

FilterBindableAdapter


Давайте теперь создадим адаптер с возможностью фильтрации. Расширяем RecyclerBindableAdapter и первым делом создаем два списка объектов: все объекты и те, что сейчас отображены. Переопределим часть методов, что бы теперь они работали с двумя списками.

Создаем два списка. Переопределяем методы
//инициализация списков
private List<T> originalValues;
private List<T> objects;

protected FilterBindableAdapter() {
    objects = new ArrayList<T>();
    originalValues = new ArrayList<T>();
}

//так как обычно работа с фильтруемым списком предполагает, что элементы загрузятся только один раз и в дальнейшем будут только фильтроваться, то перед добавлением предварительно очищаем список 
@Override
public void addAll(List<? extends T> data) {
    //если точно такие же элементы уже есть в списке их нет смысла добавлять
    if (objects.containsAll(data)) {
        return;
    }
    //очищаем список отображаемых объектов и добавляем новые
    objects.clear();
    objects.addAll(data);
    //очищаем список всех объектов и добавляем новые
    originalValues.clear();
    originalValues.addAll(data);
    //анимируем
    notifyItemRangeInserted(getHeadersCount(), data.size());
}

// подобным образом поступаем с остальными методами для работы с данными



Теперь сделаем фильтрацию.

Делаем фильтрацию
//возвращает ссылку на текущий фильтр
public Filter getFilter() {
    if (filter == null) {
        filter = new ArrayFilter();
    }
    return filter;
}

//интерфейс для отслеживания сколько элементов осталось после фильтрации
public interface OnFilterObjectCallback {
    void handle(int countFilterObject);
}

//собственно класс нашего фильтра
private class ArrayFilter extends Filter {
    //фильтруем наш список
    @Override
    protected FilterResults performFiltering(CharSequence prefix) {
        FilterResults results = new FilterResults();
        //если prefix для фильтрации пустой, то отображаем все элементы
        if (prefix == null || prefix.length() == 0) {
           ArrayList<T> list;
           synchronized (lock) {
               list = new ArrayList<T>(originalValues);
            }
            results.values = list;
            results.count = list.size();
        //если все же не пустой
        } else {
            //для начала сделаем все символы строки для фильтра прописными
            String prefixString = prefix.toString().toLowerCase();
            ArrayList<T> values;
            synchronized (lock) {
                values = new ArrayList<T>(originalValues);
            }
            final int count = values.size();
            final ArrayList<T> newValues = new ArrayList<T>();
            //проверяем каждый наш элемент на соответствие фильтру
            for (int i = 0; i < count; i++) {
                final T value = values.get(i);
                final String valueText = itemToString(value).toLowerCase();
                //если элемент начинается со строки для фильтра то он само собой подходит
                if (valueText.startsWith(prefixString)) {
                    newValues.add(value);
                //если же нет, то вычленяем слова и проверяем на соответствие каждое слово
                } else {
                    final String[] words = valueText.split(" ");
                    //начинаем с первого слова, ведь строка могла начинаться с пробела
                    for (String word : words) {
                        if (word.startsWith(prefixString)) {
                           newValues.add(value);
                           break;
                        }
                    }
                }
            }
            results.values = newValues;
            results.count = newValues.size();
        }
        return results;
    }

    //отображаем результаты фильтрации
    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        objects = (List<T>) results.values;
        //вызываем callback если он есть
        if (onFilterObjectCallback != null) {
            onFilterObjectCallback.handle(results.count);
        }
        notifyDataSetChanged();
    }
}



Пример адаптера полностью аналогичен примеру RecyclerBindableAdapter за исключением одного метода. Этот метод для конвертации элемента в строку, что бы мы потом могли отфильтровать элементы.

@Override
    protected String itemToString(String item) {
        return item;
    }


Полный текст можно посмотреть тут или под спойлером.

FilterBindableAdapter
public abstract class FilterBindableAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerBindableAdapter<T, VH> {

    private final Object lock = new Object();
    private List<T> originalValues;
    private List<T> objects;
    private ArrayFilter filter;
    private OnFilterObjectCallback onFilterObjectCallback;

    protected FilterBindableAdapter() {
        objects = new ArrayList<T>();
        originalValues = new ArrayList<T>();
    }

    @Override
    public void addAll(List<? extends T> data) {
        if (objects.containsAll(data)) {
            return;
        }
        objects.clear();
        objects.addAll(data);
        originalValues.clear();
        originalValues.addAll(data);
        notifyItemRangeInserted(getHeadersCount(), data.size());
    }

    //@TODO need test
    public void addShowed(List<? extends T> data) {
        objects.clear();
        objects.addAll(data);
        notifyDataSetChanged();
    }

    //@TODO need test
    @Override
    public void removeChild(int position) {
        objects.remove(position);
        originalValues.remove(position);
        objects.remove(position);
        notifyItemRemoved(position + getHeadersCount());
        int positionStart = position + getHeadersCount();
        int itemCount = objects.size() - position;
        notifyItemRangeChanged(positionStart, itemCount);
    }

    //@TODO need test
    public void setOnFilterObjectCallback(OnFilterObjectCallback objectCallback) {
        onFilterObjectCallback = objectCallback;
    }

    @Override
    public T getItem(int position) {
        return objects.get(position);
    }

    @Override
    public int getRealItemCount() {
        return objects.size();
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    protected abstract String itemToString(T item);

    public Filter getFilter() {
        if (filter == null) {
            filter = new ArrayFilter();
        }
        return filter;
    }

    public interface OnFilterObjectCallback {
        void handle(int countFilterObject);
    }

    private class ArrayFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence prefix) {
            FilterResults results = new FilterResults();
            if (originalValues == null) {
                synchronized (lock) {
                    originalValues = new ArrayList<T>(objects);
                }
            }
            if (prefix == null || prefix.length() == 0) {
                ArrayList<T> list;
                synchronized (lock) {
                    list = new ArrayList<T>(originalValues);
                }
                results.values = list;
                results.count = list.size();
            } else {
                String prefixString = prefix.toString().toLowerCase();
                ArrayList<T> values;
                synchronized (lock) {
                    values = new ArrayList<T>(originalValues);
                }
                final int count = values.size();
                final ArrayList<T> newValues = new ArrayList<T>();
                for (int i = 0; i < count; i++) {
                    final T value = values.get(i);
                    final String valueText = itemToString(value).toLowerCase();
                    // First match against the whole, non-splitted value
                    if (valueText.startsWith(prefixString)) {
                        newValues.add(value);
                    } else {
                        final String[] words = valueText.split(" ");
                        // Start at index 0, in case valueText starts with space(s)
                        for (String word : words) {
                            if (word.startsWith(prefixString)) {
                                newValues.add(value);
                                break;
                            }
                        }
                    }
                }
                results.values = newValues;
                results.count = newValues.size();
            }
            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            //noinspection unchecked
            objects = (List<T>) results.values;
            if (onFilterObjectCallback != null) {
                onFilterObjectCallback.handle(results.count);
            }
            notifyDataSetChanged();
        }
    }
}



Пример можно посмотреть тут.

ParallaxBindableAdapter


Теперь приступим к созданию эффекта параллакса. Изначально я планировал сделать параллакс только для Header'а, но потом подумал, что сделать параллакс и для Footer'а будет весьма интересным опытом. Для начала переопределим методы для добавления Header'ов и Footer'ов так, чтобы наш адаптер мог иметь только по одному их экземпляру.

Переопределяем addHeader и addFooter
 @Override
    public void addHeader(View header) {
        if (getHeadersCount() == 0) {
            super.addHeader(header);
        } else {
            removeHeader(getHeader(0));
            super.addHeader(header);
        }
    }

    @Override
    public void addFooter(View footer) {
        if (getFootersCount() == 0) {
            super.addFooter(footer);
        } else {
            removeFooter(getFooter(0));
            super.addFooter(footer);
        }
    }



Теперь создадим контейнер который будет ограничивать область отрисовки для того, чтобы header не залазил под другие элементы. Если вы уже делали параллакс, то скорее всего уже встречались с подобной реализацией. Отличие этой реализации, что она поддерживает еще и footer. Этот контейнер мы будем использовать вместо FrameLayout в onCreateViewHolder().

ParallaxContainer
 public class ParallaxContainer extends FrameLayout {

    private final boolean isParallax;
    private final boolean isFooter;
    private int offset;

    public ParallaxContainer(Context context, boolean shouldClick, boolean isFooter) {
        super(context);
        isParallax = shouldClick;
        this.isFooter = isFooter;
    }

    @Override
    protected void dispatchDraw(@NonNull Canvas canvas) {
        if (isParallax) {
            int top = isFooter ? -offset : 0;
            int bottom = isFooter ? getBottom() : getBottom() + offset;
            Rect rect = new Rect(getLeft(), top, getRight(), bottom);
            canvas.clipRect(rect);
        }
        super.dispatchDraw(canvas);
    }

    public void setClipY(int offset) {
        this.offset = offset;
        invalidate();
    }
}



Теперь давайте сделаем методы для сдвигания контейнера при скролле. Для этого в методе onAttachedToRecyclerView() повесим на RecyclerView OnScrollListener. Внутри него будем вызывать метод для сдвигания. Ну и естественно нужно создать методы для того, чтобы включать/отключать эффект параллакса.

Реализуем сдвигание
//переменные для определения стоит ли применять эффект параллакса и setter'ы к ним
private boolean isParallaxHeader = true;
private boolean isParallaxFooter = true;

public void setParallaxHeader(boolean isParallaxHeader) {
    this.isParallaxHeader = isParallaxHeader;
}

public void setParallaxFooter(boolean isParallaxFooter) {
    this.isParallaxFooter = isParallaxFooter;
}

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
    //добавляем ScrollListener к RecyclerView
    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            //если header присутствует и к нему надо применять эффект параллакса то сдвигаем его контейнер
            if (header != null && isParallaxHeader) {
                translateView(recyclerView.computeVerticalScrollOffset(), header, false);
            }
            //если footer присутствует и к нему надо применять эффект параллакса то сдвигаем его контейнер
            if (footer != null && isParallaxFooter) {
                int range = recyclerView.computeVerticalScrollRange();
                int extend = recyclerView.computeVerticalScrollExtent();
                int offset = recyclerView.computeVerticalScrollOffset();
                translateView(range - (extend + offset), footer, true);
            }
        }
    });
}

//сдвигание контейнера
private void translateView(float of, ParallaxContainer view, boolean isFooter) {
    float ofCalculated = of * SCROLL_MULTIPLIER;
    //если это footer то контейнер надо двигать в обратную сторону
    ofCalculated = isFooter ? -ofCalculated : ofCalculated;
    //сдвигаем наш контейнер
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        view.setTranslationY(ofCalculated);
    } else {
        TranslateAnimation anim = new TranslateAnimation(0, 0, ofCalculated, ofCalculated);
        anim.setFillAfter(true);
        anim.setDuration(0);
        view.startAnimation(anim);
    }
    //уменьшаем область отрисовки
    view.setClipY(Math.round(ofCalculated));
    //передаем в callback, то сколько мы проскроллили (вдруг надо скрывать/показывать ActionBar по скроллу)
    if (parallaxScroll != null && !isFooter) {
        float left = Math.min(1, ((ofCalculated) / (view.getHeight() * SCROLL_MULTIPLIER)));
        parallaxScroll.onParallaxScroll(left, of, view);
    }
}



Полный текст можно посмотреть тут или под спойлером.

ParallaxBindableAdapter
public abstract class ParallaxBindableAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerBindableAdapter<T, VH> {

    private static final float SCROLL_MULTIPLIER = 0.5f;
    private ParallaxContainer header;
    private ParallaxContainer footer;
    private OnParallaxScroll parallaxScroll;
    private boolean isParallaxHeader = true;
    private boolean isParallaxFooter = true;

    //parallax adapter may have only one header
    @Override
    public void addHeader(View header) {
        if (getHeadersCount() == 0) {
            super.addHeader(header);
        } else {
            removeHeader(getHeader(0));
            super.addHeader(header);
        }
    }

    //parallax adapter may have only one header
    @Override
    public void addFooter(View footer) {
        if (getFootersCount() == 0) {
            super.addFooter(footer);
        } else {
            removeFooter(getFooter(0));
            super.addFooter(footer);
        }
    }

    private void translateView(float of, ParallaxContainer view, boolean isFooter) {
        float ofCalculated = of * SCROLL_MULTIPLIER;
        ofCalculated = isFooter ? -ofCalculated : ofCalculated;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            view.setTranslationY(ofCalculated);
        } else {
            TranslateAnimation anim = new TranslateAnimation(0, 0, ofCalculated, ofCalculated);
            anim.setFillAfter(true);
            anim.setDuration(0);
            view.startAnimation(anim);
        }
        view.setClipY(Math.round(ofCalculated));
        if (parallaxScroll != null && !isFooter) {
            float left = Math.min(1, ((ofCalculated) / (view.getHeight() * SCROLL_MULTIPLIER)));
            parallaxScroll.onParallaxScroll(left, of, view);
        }
    }

    @Override
    public VH onCreateViewHolder(ViewGroup viewGroup, int type) {
        //if our position is one of our items (this comes from getItemViewType(int position) below)
        if (type != TYPE_HEADER && type != TYPE_FOOTER) {
            return (VH) onCreteItemViewHolder(viewGroup, type);
            //else if we have a header
        } else if (type == TYPE_HEADER) {
            //create a new ParallaxContainer
            header = new ParallaxContainer(viewGroup.getContext(), isParallaxHeader, false);
            //make sure it fills the space
            header.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
            return (VH) new HeaderFooterViewHolder(header);
            //else we have a footer
        } else {
            //create a new ParallaxContainer
            footer = new ParallaxContainer(viewGroup.getContext(), isParallaxFooter, true);
            //make sure it fills the space
            footer.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
            return (VH) new HeaderFooterViewHolder(footer);
        }
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (header != null && isParallaxHeader) {
                    translateView(recyclerView.computeVerticalScrollOffset(), header, false);
                }
                if (footer != null && isParallaxFooter) {
                    int range = recyclerView.computeVerticalScrollRange();
                    int extend = recyclerView.computeVerticalScrollExtent();
                    int offset = recyclerView.computeVerticalScrollOffset();
                    translateView(range - (extend + offset), footer, true);
                }
            }
        });
    }

    public void setParallaxHeader(boolean isParallaxHeader) {
        this.isParallaxHeader = isParallaxHeader;
    }

    public void setParallaxFooter(boolean isParallaxFooter) {
        this.isParallaxFooter = isParallaxFooter;
    }

    public void setOnParallaxScroll(OnParallaxScroll parallaxScroll) {
        this.parallaxScroll = parallaxScroll;
        this.parallaxScroll.onParallaxScroll(0, 0, header);
    }

    public interface OnParallaxScroll {
        /**
         * Event triggered when the parallax is being scrolled.
         *
         * @param percentage
         * @param offset
         * @param parallax
         */
        void onParallaxScroll(float percentage, float offset, View parallax);
    }
}



Пример по сути точно такой же как и для RecyclerBindableAdapter, просто нужно изменить что расширять. Пример смотреть тут.

SimpleBindableAdapter


Для того, чтобы сделать такой адаптер, для начала нам понадобится создать свой ViewHolder. Это делается для того, что бы мы затем точно знали какие методы нам вызывать. Так же нужно сделать свой интерфейс от которого мы в дальнейшем будем наследоваться.

Скрытый текст
public abstract class BindableViewHolder<T> extends RecyclerView.ViewHolder {
    
    public BindableViewHolder(View itemView) {
        super(itemView);
    }

    //в этом методе будет происходить обработка данных, сейчас вешается OnItemClickListener 
    public void bindView(final int position, final T item, final ActionListener actionListener) {
        if (actionListener != null) {
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    actionListener.OnItemClickListener(position, item);
                }
            });
        }
    }

    //это интерфейс который мы будем в дальнейшем расширять
    public interface ActionListener {
        void OnItemClickListener(int position, Object Item);
    }
}



Теперь приступим к созданию нашего адаптера. Просто окончательно переопределим все методы из RecyclerBindableAdapter и будет создавать новый экземпляр нашего ViewHolder через Java Reflection.

Скрытый текст
//делаем класс final, чтоб никто, не дай бог, не вздумал от него наследоваться. Указываем, что VH обязательно должен расширять BindableViewHolder
public final class SimpleBindableAdapter<T, VH extends BindableViewHolder>
        extends RecyclerBindableAdapter<T, VH> {

    //переменная в которой будет храниться layout id нашего элемента
    @LayoutRes
    private final int layoutId;

    //класс ViewHolder эта переменная для того, чтобы можно было создать новый экземпляр ViewHolder
    Class<VH> vhClass;
    //интерфейс для взаимодействия с элементом
    BindableViewHolder.ActionListener actionListener;

    public SimpleBindableAdapter(@LayoutRes int layoutId, Class<VH> vhClass) {
        this.layoutId = layoutId;
        this.vhClass = vhClass;
    }

    @Override
    protected void onBindItemViewHolder(BindableViewHolder viewHolder, int position, int type) {
        //вставляем данные во ViewHolder, ради этого метода мы и создавали BindableViewHolder
        viewHolder.bindView(position, getItem(position), actionListener);
    }

    @Override
    protected VH viewHolder(View view, int type) {
        //через Java Reflection создаем новый экземпляр ViewHolder
        try {
            return (VH) vhClass.getConstructor(View.class).newInstance(view);
        } catch (InstantiationException e) {
            e.printStackTrace();
            return null;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return null;
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return null;
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    protected int layoutId(int type) {
        return layoutId;
    }

    public void setActionListener(BindableViewHolder.ActionListener actionListener) {
        this.actionListener = actionListener;
    }
}



Теперь о том, как им пользоваться. Все очень и очень просто.

private SimpleBindableAdapter<Integer, SimpleViewHolder> simpleExampleAdapter;

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        .............................................................................................
        simpleExampleAdapter = new SimpleBindableAdapter<>(R.layout.simple_example_item, SimpleViewHolder.class);
        simpleExampleAdapter.setActionListener(new SimpleViewHolder.SimpleActionListener() {
        .............................................................................................
        }
        simpleExampleRecycler.setAdapter(simpleExampleAdapter);
        ..............................................................................................
    }


Вот и все, очень просто, не правда ли. Конечно нам еще нужен ViewHolder.

Скрытый текст
public class SimpleViewHolder extends BindableViewHolder<Integer> {

    @Bind(R.id.simple_example_item_tittle)
    TextView tittle;

    private int position;
    private SimpleActionListener simpleActionListener;

    public SimpleViewHolder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
    }

    @Override
    public void bindView(int position, Integer item, ActionListener actionListener) {
        super.bindView(position, item, actionListener);
        this.position = position;
        simpleActionListener = (SimpleActionListener) actionListener;
        tittle.setText(String.valueOf(item));
    }

    @OnClick(R.id.simple_example_item_move_to_top)
    protected void OnMoveToTopClick() {
        if (simpleActionListener != null) {
            simpleActionListener.onMoveToTop(position);
        }
    }

    @OnClick(R.id.simple_example_item_remove)
    protected void OnRemoveClick() {
        if (simpleActionListener != null) {
            simpleActionListener.OnRemove(position);
        }
    }

    @OnClick(R.id.simple_example_item_up)
    protected void OnUpClick() {
        if (simpleActionListener != null) {
            simpleActionListener.OnUp(position);
        }
    }

    @OnClick(R.id.simple_example_item_down)
    protected void OnDownClick() {
        if (simpleActionListener != null) {
            simpleActionListener.OnDown(position);
        }
    }

    public interface SimpleActionListener extends ActionListener {
        void onMoveToTop(int position);

        void OnRemove(int position);

        void OnUp(int position);

        void OnDown(int position);
    }
}



Вот и вся инициализация. Вот здесь можно найти пример.

Вот ссылка на репозиторий, в нем вы найдете все примеры и адаптеры, а также больше документации на кривом английском.

PS. Проект еще не завершен на все 100% поэтому буду признателен за помощь, конструктивную критику и предложения по улучшению. Так же возможно к тому моменту когда вы прочтете эту статью, появится что-то новое.

PSS. На одном из проектов в которых я участвовал, мой коллега начал создание BindableAdapter для RecyclerView, но так и не закончил, сделав реализацию лишь под конкретный проект. В любом случае, если ты это читаешь, большее спасибо за толчок и за идею.

© Habrahabr.ru