О RecyclerView и выделении элементов

    Привет, %username%!
    В этой статье я хочу рассказать немного о новом виджете в Android SDK — RecyclerView, о реализации выделения элементов и нескольких полезных «рецептах» при работе с ним.


    Содержание


    1. Немного о ViewHolder'ах
    2. Вкратце о RecyclerView
    3. Выделяем элементы
    4. Заключение + Бонус
    5. Полезные ссылки

    1. Немного о ViewHolder'ах


    До выхода в свет Android SDK 5.0 Lollipop для отображения списков и таблиц использовались виджеты ListView и GridView. Общей рекомендацией при работе с этим виджетом было использование паттерна ViewHolder. Суть паттерна заключается в том, что для каждого элемента списка создаётся объект, хранящий ссылки на отдельные вьюхи внутри элемента. Таким образом, приходится выполнять достаточно дорогой вызов findViewById(int) только один раз при создании элемента.

    Пример типичного ViewHolder'а прямиком из руководств гугла:
    static class ViewHolder {
      TextView text;
      TextView timestamp;
      ImageView icon;
      ProgressBar progress;
      int position;
    }

    Cсылка на такой холдер для каждого элемента сохраняется в корневом layout'е, используя метод setTag(int, Object) (с моей точки зрения тот ещё костыль).

    2. Вкратце о RecyclerView


    К выходу Android SDK 5.0 Lollipop разработчиков Google наконец-то озарило, что два вышеперечисленных виджета морально устарели и нужно бы заменить их на нечто более стильное, модное и молодёжное. Было принято решение не переделывать старые виджеты, а написать новый. Так и появился на свет RecyclerView. Так в чём же его отличия, спросите вы?

    Я приведу вкратце основные, а для более полного их раскрытия советую к ознакомлению вот эту статью на хабре. Итак:
    1. Сам виджет больше не берёт на себя обязанность по размещению элементов. Для этого появились LayoutManager'ы.
    2. Паттерн ViewHolder стал обязательным. Причём виджет научился заново использовать уже созданные ViewHolder'ы и удалять уже не используемые (отсюда и название), что благоприятно сказывается на быстродействии и размере используемой памяти.
    3. Новый, удобный способ работы с анимацией.

    Я попробовал его и виджет оставил у меня противоречивые впечатления. С одной стороны, да, здорово, что теперь использование ViewHolder'а является обязательным, работает вроде тоже быстрей, памяти жрёт меньше. С другой стороны, есть проблемы со сложностью и недоделанностью виджета.

    Что я понимаю под сложностью? Если что-то не работало в ListView (или работало не так как задумано) всегда можно было залезть в исходники, разобраться, в чём ошибка, исправить её, подоткнуть костылей тут и там и всё начинало работать. RecyclerView гораздо сложнее в плане логики работы, мозг сломаешь, пока разберёшься. Я пытался, но забросил, уж слишком много времени и сил для этого нужно.

    Вторая проблема — банальное отсутствие функционала, присутствовавшего в ListView и GridView. За примерами далеко ходить не надо — стандартный функционал выделения элементов (дальнейшая тема этой статьи), отступы между элементами. Раньше, чтобы добавить всё это, нужно было написать буквально пару строчек кода, теперь на это уйдут уже десятки строк. Есть анимации, но только для добавления/удаления/редактирования элемента. Если же вы хотите, например, анимировать частичное изменение элемента, то к вам в дверь уже стучится птица обломинго. Виджет не поддерживает анимацию части элемента, и если анимировать элемент извне (из адаптера, например), то лучше этого не делать — подобные манипуляции оставляют элементы виджета (те самые ViewHolder'ы) в неопределённом состоянии, что приводит к совершенно фантастическому поведению вашего списка.

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

    3. Выделяем элементы


    Итак, перейдём к главному — к технической части статьи. Поговорим о том, как выделять элементы в RecyclerView. Сразу оговорюсь — идея реализации почерпнута из замечательной серии статей Билла Филлипса про RecyclerView (ссылки в конце), так что всё нижеследующее можно считать вольным кратким пересказом.
    В ListView для выделения элементов использовался метод setChoiceMode(int), RecyclerView же понятия не имеет, что элементы могут выделяться, поэтому мы должны научить этому наш адаптер.

    Схема такая:
    На диаграмме я схематично обозначил связи между объектами. Пунктирные стрелки — ссылки, остальные — вызовы методов. Зелёным я обозначил объекты, которые непосредственно реализуют логику выделения.

    Принцип работы получается следующий:
    1. ViewHolderWrapper устанавливает себя в качестве ClickListener'а для корневой вьюхи ViewHolder'а и начинает получать события onClick и onLongClick. В зависимости от реализации он может просто проксировать эти события в HolderClickObservable (ViewHolderClickWrapper), либо, исходя из текущего статуса SelectionHelper'а выделять элемент вызовом setItemSelected(ViewHolder, boolean) (ViewHolderMultiSelectionWrapper).
    2. SelectionHelper сохраняет информацию о выделенных элементах и оповещает слушателей (SelectionObserver) об изменении выделения.
    3. Слушатель (в нашем случае адаптер) отвечает за визуальное отображение выделения элемента, а также взаимодействия с ним (на диаграмме вызов startActionMode у Activity).

    В самом адаптере необходимо сделать следующие изменения:

    1. Создать SelectionHelper и зарегистрировать слушателей (в данном случае сам адаптер, но это может быть и Activity, например)
    mSelectionHelper = new SelectionHelper(mHolderTracker);
    mSelectionHelper.registerSelectionObserver(this);

    2. Обернуть создаваемые ViewHolder'ы во ViewHolderWrapper нужного типа.
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position)
    {
        LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
        ImageViewHolder holder = new ImageViewHolder(
                inflater.inflate(R.layout.gallery_item, viewGroup, false));    
        return mSelectionHelper.wrapSelectable(holder);
    }
    Метод wrapSelectable(ViewHolder) у SelectionHelper'а:
    public <H extends RecyclerView.ViewHolder> H wrapSelectable(H holder)
    {
        new ViewHolderMultiSelectionWrapper(holder);
        return holder;
    }

    3. Не забывать прицеплять наши ViewHolder'ы к SelectionHelper'у в методе onBindViewHolder(ViewHolder, int) нашего адаптера!
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position)
    {
        Image image = mDataSet.getItem(position);
        ImageViewHolder imageViewHolder = (ImageViewHolder) viewHolder;
        imageViewHolder.bindInfo(image);
    
        Checkable view = (Checkable) viewHolder.itemView;
        view.setChecked(mSelectionHelper.isItemSelected(position));
        mHolderTracker.bindHolder(imageViewHolder, position);
    }

    Это нужно по причине того, что пока нет другого способа получить от RecyclerView список используемых в настоящий момент ViewHolder'ов. Если не вести их учёт, при необходимости обновить отрисовку выделения у всех выбранных элементов (пользователь закрыл ActionMode, например), SelectionHelper просто не сможет этого сделать. Вьюхи останутся выглядеть выделенными, когда по факту таковыми не будут.

    Вы спросите — «А почему бы просто не запоминать выделяемые ViewHolder'ы в методе setItemSelected(ViewHolder, boolean)?». Тут как раз сказывается особенность RecyclerView — он использует заново уже созданные ViewHolder'ы.

    Выглядит это примерно так:
    1. Открываем приложение. На экране 10 элементов — 10 ViewHolder'ов создано для них.
    2. Запускаем ActionMode, начинаем выделять элементы — 1,2,3.
    3. Прокручиваем вьюху вниз, видим элементы с 10 по 20. Думаете, что теперь в памяти висит 20 ViewHolder'ов? Как бы ни так! Для части данных RecyclerView создаст новые ViewHolder’ы, а для другой заново использует уже имеющиеся. Причём неизвестно в каком порядке.
    4. Теперь если мы прокрутим вьюху обратно вверх, часть из наших 10 ViewHolder'ов будет уничтожена, вместо них будут созданы новые. Оставшаяся часть будет использована заново и совершенно не обязательно для тех же позиций.
    5. Отменяем ActionMode. SelectionHelper должен раскидать слушателям уведомления о сменившемся выделении на элементах с указанием ViewHolder'а для каждого элемента, но он уже не владеет актуальными данными, все Holder'ы поменялись!

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

    И здесь становится очевидным ещё один важный момент — нельзя сохранять строгие ссылки (strong reference) на ViewHolder'ы! Они могут быть удалены из RecyclerView в зависимости от фазы Луны и желания левой пятки Ларри Пейджа. В этом случае, если мы будем хранить строгие ссылки на них, случится утечка памяти. Поэтому для хранения ссылок в ViewHolderWrapper и WeakHolderTracker используются только WeakReference.
    private abstract class ViewHolderWrapper implements android.view.View.OnClickListener
    {
        protected final WeakReference<RecyclerView.ViewHolder> mWrappedHolderRef;
    
        protected ViewHolderWrapper(RecyclerView.ViewHolder holder)
        {
            mWrappedHolderRef = new WeakReference<>(holder);
        }
    }

    WeakHolderTracker
    public class WeakHolderTracker
    {
        private final SparseArray<WeakReference<RecyclerView.ViewHolder>> mHoldersByPosition =
                new SparseArray<>();
    
        public void bindHolder(RecyclerView.ViewHolder holder, int position)
        {
            mHoldersByPosition.put(position, new WeakReference<>(holder));
        }
    
        @Nullable
        private RecyclerView.ViewHolder getHolder(int position)
        {
            WeakReference<RecyclerView.ViewHolder> holderRef = mHoldersByPosition.get(position);
            if (holderRef == null)
            {
                mHoldersByPosition.remove(position);
                return null;
            }
    
            RecyclerView.ViewHolder holder = holderRef.get();
            if (holder == null || (holder.getAdapterPosition() != position && holder.getAdapterPosition() != RecyclerView.NO_POSITION))
            {
                mHoldersByPosition.remove(position);
                return null;
            }
    
            return holder;
        }
    
        public List<RecyclerView.ViewHolder> getTrackedHolders()
        {
            List<RecyclerView.ViewHolder> holders = new ArrayList<>();
    
            for (int i = 0; i < mHoldersByPosition.size(); i++)
            {
                int key = mHoldersByPosition.keyAt(i);
                RecyclerView.ViewHolder holder = getHolder(key);
    
                if (holder != null)
                {
                    holders.add(holder);
                }
            }
    
            return holders;
        }
    }

    4. Также важно не забыть в onBindViewHolder(ViewHolder, int) визуально отобразить выделение если оно есть (если нет — не забыть убрать!). Вы же помните, что для не выделенного элемента может быть использован ViewHolder, ранее использовавшийся для не выделенного и наоборот?
    У меня это реализовано следующим образом:

    4.1. SelectableRecyclerViewAdapter.onBindViewHolder(ViewHolder, int)
    Checkable view = (Checkable) viewHolder.itemView;
    view.setChecked(mSelectionHelper.isItemSelected(position));

    4.2. layout-файл для элемента
    <com.bejibx.android.recyclerview.example.ui.widget.CheckableAutofitHeightFrameLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#AAA"
        android:foreground="@drawable/gallery_item_foreground"
        tools:ignore="ContentDescription,RtlHardcoded">
    
        <ImageView
            android:id="@+id/image"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </com.bejibx.android.recyclerview.example.ui.widget.CheckableAutofitHeightFrameLayout>

    4.3. Виджет CheckableAutofitHeightFrameLayout
    public class CheckableAutofitHeightFrameLayout extends FrameLayout implements Checkable
    {
        private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
    
        private boolean mIsChecked;
        private boolean mIsCheckable;
    
        public CheckableAutofitHeightFrameLayout(Context context)
        {
            super(context);
        }
    
        public CheckableAutofitHeightFrameLayout(Context context, AttributeSet attrs)
        {
            super(context, attrs);
        }
    
        public CheckableAutofitHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr)
        {
            super(context, attrs, defStyleAttr);
        }
    
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        public CheckableAutofitHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
        {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    
        @Override
        protected int[] onCreateDrawableState(int extraSpace)
        {
            final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
            if (isChecked())
            {
                mergeDrawableStates(drawableState, CHECKED_STATE_SET);
            }
            return drawableState;
        }
    
        @Override
        public boolean isCheckable()
        {
            return mIsCheckable;
        }
    
        @Override
        public void setCheckable(boolean isCheckable)
        {
            boolean wasCheckable = isCheckable();
            mIsCheckable = isCheckable;
            if (!isCheckable && isChecked())
            {
                setChecked(false);
            }
            else if (wasCheckable ^ mIsCheckable)
            {
                refreshDrawableState();
            }
    
        }
    
        @Override
        public void setChecked(boolean isChecked)
        {
            boolean wasChecked = isChecked();
            mIsChecked = isCheckable() && isChecked;
    
            if (wasChecked ^ mIsChecked)
            {
                refreshDrawableState();
            }
        }
    
        @Override
        public boolean isChecked()
        {
            return mIsChecked;
        }
    
        @Override
        public void toggle()
        {
            setChecked(!mIsChecked);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
        {
            //noinspection SuspiciousNameCombination
            super.onMeasure(widthMeasureSpec, widthMeasureSpec);
        }
    }

    CheckableAutofitHeightFrameLayout добавляет к FrameLayout всего 2 вещи: во-первых, он всегда квадратный (смотри onMeasure(int, int)) и, во-вторых, добавляет к DrawableStates (те самые, которые используются в xml) состояние state_checked. В результате, для отображения выделения у такого layout'а можно использовать StateListDrawable на вроде этого:
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item android:state_pressed="true">
            <shape xmlns:android="http://schemas.android.com/apk/res/android">
                <stroke android:color="@color/accent" android:width="1dp" />
            </shape>
        </item>
    
        <item android:state_checked="true">
            <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
                <stroke android:color="@color/accent" android:width="1dp" />
                <solid android:color="@color/accent_alpha" />
            </shape>
        </item>
    
        <item android:drawable="@android:color/transparent" />
    
    </selector>
    и все детали отображения уползают в xml-ки, в Java только нужно установить соответствующие состояния.

    5. Передать событие onSelectableChanged(boolean) в Activity и запустить ActionMode:

    В Адаптере
    @Override
    public void onSelectableChanged(boolean isSelectable)
    {
        if (isSelectable)
        {
            mActivity.startActionMode();
        }
    }

    Запуск ActionMode в Activity
    public class GalleryActivity extends Activity
    {
        private final ActionModeCallback mActionModeCallback = new ActionModeCallback();
    
        private SelectableRecyclerViewAdapter mAdapter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            RecyclerView recyclerView = (RecyclerView) findViewById(R.id.gallery);
            int columnWidth = getResources().getDimensionPixelSize(R.dimen.column_width);
            int vSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_vertical);
            int hSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_horizontal);
            recyclerView.setLayoutManager(new GridAutofitLayoutManager(this, columnWidth));
            recyclerView.addItemDecoration(new GridSimpleSpacingDecoration(hSpacing, vSpacing));
            DataSet<Image> dataSet = new DummyImagesDataSet();
            mAdapter = new SelectableRecyclerViewAdapter(this, dataSet);
            recyclerView.setAdapter(mAdapter);
        }
    
        public void startActionMode()
        {
            startActionMode(mActionModeCallback);
        }
    
        private class ActionModeCallback implements ActionMode.Callback, SelectionObserver
        {
            private ActionMode mActionMode;
    
            @Override
            public boolean onPrepareActionMode(ActionMode actionMode, Menu menu)
            {
                return false;
            }
    
            @Override
            public void onDestroyActionMode(ActionMode actionMode)
            {
                SelectionHelper selectionHelper = mAdapter.getSelectionHelper();
                selectionHelper.unregisterSelectionObserver(this);
                mActionMode = null;
                selectionHelper.setSelectable(false);
            }
    
            @Override
            public boolean onCreateActionMode(ActionMode actionMode, Menu menu)
            {
                mActionMode = actionMode;
                mActionMode.getMenuInflater().inflate(R.menu.gallery_selection, menu);
                mAdapter.getSelectionHelper().registerSelectionObserver(this);
                return true;
            }
    
            @Override
            public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem)
            {
                switch (menuItem.getItemId())
                {
                    case R.id.menu_toast:
                        Toast.makeText(GalleryActivity.this,
                                R.string.text_simple_toast, Toast.LENGTH_SHORT).show();
                        break;
                }
                return true;
            }
    
            @Override
            public void onSelectedChanged(RecyclerView.ViewHolder holder, boolean isSelected)
            {
                if (mActionMode != null)
                {
                    int checkedImagesCount = mAdapter.getSelectionHelper().getSelectedItemsCount();
                    mActionMode.setTitle(String.valueOf(checkedImagesCount));
                }
            }
    
            @Override
            public void onSelectableChanged(boolean isSelectable)
            {
                if (!isSelectable)
                {
                   mActionMode.finish();
                }
            }
        }
    }

    Как вы видите, при запуске ActionMode, она регистрирует себя как SelectionObserver. Таким образом, можно обновлять количество выделенных элементов в заголовке. Не забудьте вызвать unregisterSelectionObserver(SelectionObserver) при закрытии!

    4. Заключение + Бонус


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

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

    1. Если не нужно выделять элементы, а нужно просто обрабатывать нажатия, вместо ViewHolderMultiSelectionWrapper оборачивайте элементы в ViewHolderClickWrapper методом wrapClickable(ViewHolder). Сам адаптер в таком случае будет выглядеть примерно так:
    Скрытый текст
    public class SelectableRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements SelectionObserver
    {
        private final SelectionHelper mSelectionHelper;
    
        public SelectableRecyclerViewAdapter()
        {
            mSelectionHelper = new SelectionHelper();
            mSelectionHelper.registerSelectionObserver(this);
        }
    
        public SelectionHelper getSelectionHelper()
        {
            return mSelectionHelper;
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position)
        {
            LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
            ImageViewHolder holder = new ImageViewHolder(
                    inflater.inflate(R.layout.gallery_item, viewGroup, false));
            return mSelectionHelper.wrapClickable(holder);
        }
    
        @Override
        public void onHolderClick(RecyclerView.ViewHolder holder)
        {
    		// perform item click
        }
    
        @Override
        public boolean onHolderLongClick(RecyclerView.ViewHolder holder)
        {
    	    // perform item long click
            return false;
        }
    
        //...
    }

    2. GridLayoutManager не умеет автоматически подбирать количество столбцов в зависимости от ширины контента. Я добавил этот функционал в
    GridAutofitLayoutManager
    public class GridAutofitLayoutManager extends GridLayoutManager
    {
        private int mColumnWidth;
        private boolean mColumnWidthChanged = true;
    
        public GridAutofitLayoutManager(Context context, int columnWidth)
        {
            /* Initially set spanCount to 1, will be changed automatically later. */
            super(context, 1);
            setColumnWidth(checkedColumnWidth(context, columnWidth));
        }
    
        public GridAutofitLayoutManager(Context context, int columnWidth, int orientation, boolean reverseLayout)
        {
            /* Initially set spanCount to 1, will be changed automatically later. */
            super(context, 1, orientation, reverseLayout);
            setColumnWidth(checkedColumnWidth(context, columnWidth));
        }
    
        private int checkedColumnWidth(Context context, int columnWidth)
        {
            if (columnWidth <= 0)
            {
                context.getResources().getDimensionPixelSize(R.dimen.rv_def_column_width);
            }
            return columnWidth;
        }
    
        public void setColumnWidth(int newColumnWidth)
        {
            if (newColumnWidth > 0 && newColumnWidth != mColumnWidth)
            {
                mColumnWidth = newColumnWidth;
                mColumnWidthChanged = true;
            }
        }
    
        /* I don't actually remember why I choose to set span count in onLayoutChildren, I wrote this
        class some time ago. But the point is we need to do so after view get measured,
        so we can get its height and width. */
        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
        {
            if (mColumnWidthChanged && mColumnWidth > 0)
            {
                int totalSpace;
                if (getOrientation() == VERTICAL)
                {
                    totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
                }
                else
                {
                    totalSpace = getHeight() - getPaddingTop() - getPaddingBottom();
                }
                int spanCount = Math.max(1, totalSpace / mColumnWidth);
                setSpanCount(spanCount);
                mColumnWidthChanged = false;
            }
            super.onLayoutChildren(recycler, state);
        }
    }

    Виджет подбирает ширину столбцов в зависимости от параметра columnWidth. Важный момент: если доступная ширина 330 пикселей, а мы передадим желаемую ширину 100, в итоге в таблице будет 3 столбца по 110 пикселей и элементы будут этой ширины. Именно поэтому я также сделал CheckableAutofitHeightFrameLayout автоматически изменяющим свою высоту в зависимости от ширины.

    3. Для добавления отступов между элементами можно выставить paddingTop/Left у RecyclerView и marginRight/Bottom у элементов, однако это выглядит как костыль. Рекомендуемым способом является добавление ItemDecoration к RecyclerView. В примере можно найти несколько. Для добавления отступов к обычному GridLayoutManager (под «обычным» я имею ввиду GridLayoutManager со стандартным SpanSizeLookup, в нём каждый элемент занимает 1 span) можно использовать
    GridSimpleSpacingDecoration
    
    public class GalleryActivity extends Activity
    {
        private final ActionModeCallback mActionModeCallback = new ActionModeCallback();
    
        private SelectableRecyclerViewAdapter mAdapter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            RecyclerView recyclerView = (RecyclerView) findViewById(R.id.gallery);
            int columnWidth = getResources().getDimensionPixelSize(R.dimen.column_width);
            int vSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_vertical);
            int hSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_horizontal);
            recyclerView.setLayoutManager(new GridAutofitLayoutManager(this, columnWidth));
            recyclerView.addItemDecoration(new GridSimpleSpacingDecoration(hSpacing, vSpacing));
            DataSet<Image> dataSet = new DummyImagesDataSet();
            mAdapter = new SelectableRecyclerViewAdapter(this, dataSet);
            recyclerView.setAdapter(mAdapter);
        }
    	
    	//...

    Вот, кажется, и всё. Спасибо за внимание и чтобы ваши списки никогда не тормозили!

    5. Полезные ссылки


    1. Первая часть статьи Билла Филлипса о RecyclerView. Базовая информация (на английском)
    2. Вторая часть статьи Билла Филлипса о RecyclerView. О выделении элементов (на английском)
    3. Обзорная статья про RecyclerView (на английском)
    4. Информация о паттерне ViewHolder для ListView (на английском)
    5. Делаем parallax header в RecyclerView
    6. Базовая информация о RecyclerView и CardView
    7. О переходе на RecyclerView
    8. Типы ссылок в Java
    9. Полезная библиотека для работы с RecyclerView (спасибо пользователю artemgapchenko)

    PS. Выражаю огромную благодарность пользователю Qayatri за помощь в подготовке публикации! Также ещё раз благодарю пользователя artemgapchenko за ценное замечание!
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 11

      0
      Вопрос следующий: почему вы не воспользовались просто дополнительным полем во ViewHolder для position. можно туда записывать позицию последней вьюхи? — это полезно для обработки кликов.
      Для обработки выделения элементов достаточно завести удобную сущность
      SparseArray<Boolean> seletedArray = new SparseArray<>();
      
      .
      выделение/снятие выделения записать достаточно просто будет
      seletedArray.put(position, isSelected);
      

      получить аналогично легко
      isSelected = seletedArray.get(position,false);
      

      если нужно очистить выделение, то просто вызовем
      seletedArray.clear();
      adapter.notifyDataSetChanged();
      

      Мне кажется кода меньше будет, нет?
      Это ради интереса. Я хочу узнать другую сторону :)
        0
        Вопрос следующий: почему вы не воспользовались просто дополнительным полем во ViewHolder для position. можно туда записывать позицию последней вьюхи? — это полезно для обработки кликов.

        Потому, что у ViewHolder'а уже есть метод для получения позиции. Единственная проблема, полагаться полностью на эту позицию нельзя, потому что прямо сейчас, когда вы выполняете тот или иной код, ViewHolder уже может находиться в состоянии Scrapped или Dirty (см. документацию к RecyclerView) и в этом случае могут возникнуть проблемы с отображением.

        По поводу остальной части комментария. Да, кода меньше, но подход на мой взгляд неверный. Обработка выделения элементов это ведь не только сохранить сам факт выделения — это как минимум:
        1. Правильно инициализировать ViewHolder'ы для обработки нажатий.
        2. Добавить логику когда какое нажатие что делает.
        3. Правильно визуально отобразить выделение.

        Метод notifyDataSetChanged() должен вызываться если у нас изменились данные, а в случае выделения, данные остались те же самые, изменилось только их визуальное представление. Опять же, взгляните на официальную документацию:
        If you are writing an adapter it will always be more efficient to use the more specific change events if you can. Rely on notifyDataSetChanged() as a last resort.
        0
        Просто добавлю для информации:

        А я пользовался вот этим методом для получения позиции клика: stackoverflow.com/questions/28296708/get-clicked-item-and-its-position-in-recyclerview
          0
          По сути моё решение ничем не отличается, я просто вынес всю логику выделения в отдельный класс, а адаптеру оставил ответственность по визуальному отображению.
          Мой совет — при работе с RecyclerView постарайтесь как можно меньше полагаться на результат метода getAdapterPosition(), как я уже упомянул в комментарии выше, нельзя точно сказать в каком состоянии находится ViewHolder во время вызова, так что вместо ожидаемой позиции вы легко можете получить NO_POSITION в ответ.

          PS: считается хорошим тоном заворачивать ссылки в тэг
          <a href=""></>
            0
            Так используйте getLayoutPosition ()!
            developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder.html#getLayoutPosition()
            В доке же написано что он выдает позицию всегда, даже если уже идет обновление!

            Тэги использовать не могу — карма слита вхлам.
              0
              Ага, вот только метод вернёт вам (как понятно из названия) позицию во вьюхе, которая, в определённых условиях может отличаться от его позиции в адаптере. Собственно это и есть самая большая сложность в работе с RecyclerView — никогда не знаешь что она сделает с твоими ViewHolder'ми в следующий момент. У меня, например, бывали ситуации когда для одной и той же позиции при инициализации по-очереди зачем-то создавались и байндились два разных ViewHolder'а. Поверьте, я пробовал всё, что вы предлагаете. В итоге остановился на том, что на 100% можно доверять только позиции, полученной из метода onBindVIewHolder(ViewHolder, int).
                0
                Спорить не буду — опыта явно меньше чем у вас.
          0
          Cсылка на такой холдер для каждого элемента сохраняется в корневом layout'е, используя метод setTag(int, Object) (с моей точки зрения тот ещё костыль).

          Есть еще View#setTag(Object), для случаев, когда с View нужно проассоциировать только один тэг.

          Всё вроде здорово, но при отображении больших списков таких вот ViewHolder'ов создаётся достаточно много, что плохо влияет на размер используемой памяти.

          Почему? Наследники AdapterView, т.е. ListView или GridView, создают ровно столько элементов, сколько помещается на экране, при скроллинге ранее созданные элементы списков/таблиц переиспользуются. То есть помещается у меня в ListView 8 элементов на экране, значит и всего будет создано 8 элементов, пусть даже в адаптере этого ListView находится две-три сотни объектов. На каждый из них будет прицеплен свой собственный ViewHolder, то есть их тоже будет 8 штук. Не такие уж и большие затраты по памяти.

          Упомяну еще twoway-view — библиотека, которая решает некоторые из рассмотренных вами проблем с RecyclerView (отсутствие выделения элементов, OnItemClickListeners и прочего).
            0
            при скроллинге ранее созданные элементы списков/таблиц переиспользуются

            Да, но ранее созданные элементы для данной позиции адаптера, а не вообще ранее созданные. Здесь как раз и кроется ключевое различие между List/GridView и RecyclerView, — он умеет использовать одну и ту же вьюху+холдер для других позиций, тогда как List/GridView ассоциирует конкретную вьюху с конкретной позицией в адаптере. То-есть когда вы скроллите список вниз у вас не используются те самые, первые 8 вьюх, а для каждой новой позиции создаётся своя. А вот когда начинаете скроллить в обратную сторону — тут они могут использоваться заново, если ещё не удалены.

            twoway-view действительно классная библиотека, подглядывал там некоторые решения, добавлю ссылку в статью.
              0
              Специально создал сейчас простенький проект с ListActivity. Каждый элемент — TextView, в адаптере лежат сто чисел, от 0 до 99. Переопределил метод ArrayAdapter#getView(int, View, ViewGroup) следующим образом:

              @Override
              public View getView(int position, View convertView, ViewGroup parent) {
                  Log.e(getClass().getName(), "position: " + position + "; " + ((convertView == null) ? "new one" : "reused"));
                  return super.getView(position, convertView, parent);
              }
              


              Итог: для позиций 0-11 вывелось «new one», для всех остальных «reused». Проскроллил обратно вверх — для всех позиций вывелось «reused». По-моему это подтверждает мою точку зрения о том, что AdapterView с наследниками точно так же используют ранее созданные View для новых позиций.
                0
                Вы только что пошатнули одну из ключевых причин для меня по переходу на новый виджет. Тем более странно, что я был уверен в обратном, поскольку в одном своем древнем проекте наблюдал прямо противоположную картину. Как только доберусь до компьютера обязательно обновлю статью. Спасибо.

          Only users with full accounts can post comments. Log in, please.