Вложенный RecyclerView создает все ViewHolder сразу

У меня довольно сложный список с вложенными RecyclerViews. Я понимаю, что вложенные RecyclerView не лучшее решение, но в моем случае это одно из немногих решений, которые создают структурированный код и отвечают требованиям. Прилагаю изображение конструкции. Вы можете взять Telegram в качестве примера, чтобы лучше понять структуру. В основном у меня есть внешний RecyclerView RV-1 с элементами RV-1-Item и внутренний RecyclerView RV-2 с элементами RV-2-Item. Пока все хорошо, моя проблема заключается в том, что внешний RecyclerView перерабатывает представления, как и предполагалось, но если один из элементов RV-1 попадает в поле зрения, создаются все ViewHolder RV-2 (это означает, что иногда создается более 100 ViewHolder). ). Подводя итог, мой вопрос заключается в том, как заставить внутренний RecyclerView RV-2 также перерабатывать ViewHolder.

Структура RecyclerView

Я знаю, что внутренний RecyclerView RV-2 должен иметь высоту wrap_content, потому что это зависит от количества внутренних элементов, также я не могу установить setHasFixedHeigth(true) (и я не знаю, поможет ли это), потому что во время выполнения новые предметы RV-2 могут быть добавлены в RV-2. Я также пытался установить setNestedScrollingEnabled(false) на RV-2, потому что много читал об этом в Интернете, но мне это тоже не помогло.

Так что в основном я так настраиваю

RV-1

layoutManager = LinearLayoutManager(context)      
isNestedScrollingEnabled = false

RV-2

setHasFixedSize(true)
layoutManager = LinearLayoutManager(context).apply {
    reverseLayout = true
}

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

Подводя итог: внешний RV-1 перерабатывает свои ViewHolder, как и предполагалось, но внутренний RV-2 создает все ViewHolder сразу, даже если они не на экране. Я предполагаю, что это так, потому что RV-2 имеет высоту wrap_content, и когда нужно измерить layout_height, ему нужно создать все представления. ВОПРОС: Есть ли способ заставить RV-2 повторно использовать свои представления?

РЕДАКТИРОВАТЬ: Также я использую общий RecycledViewPool между всеми RV-2 RecyclerViews, но это на самом деле не связано с проблемой, потому что даже если ViewHolders совместно используются RecyclerViews, RV-2 RecyclerView не должен создавать ViewHolders, которые не t виден при инициализации.

РЕДАКТИРОВАТЬ 2: во многих комментариях и связанных вопросах говорится, что два вертикальных вложенных RecyclerViews невозможны в Android, на случай, если все посетители этого вопроса думают одинаково, мой вопрос: как бы вы реализовали такую ​​структуру. Очевидно, что я мог бы сделать один вид, который имеет IM (Round Image View) и RV-2-Item и просто сделать IM невидимым, когда он не нужен. На мой взгляд, это как-то усложняет структуру. Кроме того, требуется, чтобы IM слева от RV-1-Item имел возможность перемещаться вверх и вниз по RV-1-Item, что, очевидно, проще с моей нынешней структурой.

РЕДАКТИРОВАТЬ 3: (Мой последний, который я обещаю). Проблема, которую я показал, может быть решена с помощью подхода, который я объясняю в своем РЕДАКТИРОВАТЬ 2, даже если это не лучшее решение, оно сработает. Но проблема в том, что у меня есть еще более сложный экран, где этот подход больше не будет работать, потому что у меня есть три вложенных RecyclerView. Я мог бы уменьшить это число до двух с помощью подхода EDIT 2, но у меня все равно остались бы два вложенных RecyclerView, и я не могу придумать обходной путь, который мог бы решить проблему оставшихся двух вложенных RecyclerView. Я приложил изображение еще более сложного экрана, который содержит интерфейс приложения с отмеченными разделами, чтобы помочь вам понять структуру.

Структура RecyclerView 2


person Max Gierlachowski    schedule 12.08.2019    source источник
comment
Когда представление повторного использования находится внутри другого прокручиваемого содержимого, оно прекращает повторное использование. Это легко проверить, поместив recyclerview внутрь NestedScrollView и наблюдая за эффектом. Решение: не используйте представления вложенной прокрутки.   -  person Martin Marconcini    schedule 12.08.2019
comment
Чтобы понять, почему это так: для глаз вашего RV-2 размер фиксирован, поэтому он знает, сколько места ему нужно для отображения своих элементов (размер 1 * количество элементов). Внешний (RV-1) раздувает представление (ваш объект RV-1) и просит вычислить его размер. Мы не видели ваш XML/код, но, естественно, это заставляет RV-2 продолжать создавать представления, пока они не закончатся, потому что никто не говорит ему остановиться. Это побочный эффект такого вложенного динамического прокручиваемого контента. Я бы пересмотрел решение (плюс это кошмар для пользователей)   -  person Martin Marconcini    schedule 12.08.2019
comment
@user2836202 user2836202 Я понимаю, почему вы пометили его как дубликат, но вопрос, который вы связали, был решен с помощью GridLayout, что просто невозможно в моем сценарии. Поэтому я думаю, что эти две проблемы не совсем похожи и имеют разные решения. Вскоре я обновлю свой вопрос, добавив дополнительную информацию, которая сделает мой вопрос еще более сложным.   -  person Max Gierlachowski    schedule 12.08.2019
comment
@MaxGierlachowski Удалось ли вам найти решение для этого?   -  person Ninja420    schedule 04.02.2020


Ответы (2)


(Не совсем ответ на ваш конкретный вопрос при решении вопроса "как не заставить RecyclerView создавать все элементы одновременно", но что-то, что, скорее всего, решит вашу конкретную проблему, вообще не используя вложенные представления recyclerview)

Я бы предложил (совершенно аналогично тому, как уже предлагалось в этом ответе), свести вашу ленту в одно представление повторного использования (Нет независимо от того, насколько вы настраиваете свою вложенную архитектуру recyclerview, имхо, она никогда не будет такой же производительной, как наличие только одного recyclerview, и, поскольку вам не нужна вложенная прокрутка (я думаю), только одно представление recycler должно быть вашим лучшим вариантом).

Я бы предложил думать о вашей ленте не так, как структурированы ваши данные, а так, как вы хотите их показать, и как их можно разделить на более мелкие элементы, которые «похожи» / состоят из одних и тех же вещей.

На вашем снимке экрана я бы увидел, например, следующие элементы/типы просмотра для каждого элемента чата:

  • Заголовок чата (вещь со значком и текстом "Новая группа")
  • бейдж пользователя (картинка с текстом "Юрген")
  • элемент сообщения (один пузырек текста, например, на скриншоте внизу на самом деле будет 3 таких элемента, по одному для каждого сообщения)
  • Раздел с датой и элементами действия/ответа.

Эти элементы намного меньше, чем целый элемент чата, и поэтому могут быть быстрее созданы/переработаны. Для каждого из этих элементов создайте тип представления и держатель представления и обрабатывайте их как отдельные элементы представления повторного использования. Recyclerview будет при правильном использовании метода getItemViewType создавать/подготавливать правильный тип представления для нужной вам позиции.

Чтобы это работало, адаптер должен добавить некоторую логику, так как ваши данные, скорее всего, будут структурированы примерно так

список чатов, и у каждого чата есть имя и несколько сообщений для отображения

и нам это нужно как

первые 6 элементов предназначены для первого чата, где первая позиция — заголовок, вторая — значок пользователя, следующие 3 элемента — элементы сообщения, а затем нам нужен элемент действия.

Таким образом, вам в основном нужно рассчитать, сколько элементов recyclerview вам понадобится для отображения каждого отдельного элемента чата, что может быть расчетом по строкам:

1 элемент заголовка чата + 1 элемент значка пользователя + 3 элемента сообщения + 1 элемент действия/ответа = 6

Этот расчет необходимо выполнить для каждого элемента чата в вашем списке данных отдельно.

Поэтому, если у вас есть только один элемент чата в вашем списке данных для отображения, вам действительно нужно указать адаптеру создать 6 элементов (в этом случае возвращая 6 в getItemViewCount()).

Затем вам нужно сообщить адаптеру с помощью функции getItemViewType(position: Int), в какой позиции recyclerview какой тип представления должен подготовить адаптер. Итак, вам снова нужна логика, чтобы сказать, что, например. на позиции 0 должен быть заголовок чата для первого элемента чата, на позиции 1 должен быть значок пользователя для первого элемента чата, на позиции 2-4 должны быть элементы сообщения, на позиции 5 элемент действия и на позиции 6 заголовок чата для второй чат должен быть и так далее (опять же, логика должна быть на месте для всех элементов чата, и она может стать действительно запутанной/сложной, так как вычисление типов просмотра каждого элемента чата для позиции, например, всех предыдущих элементов чата количество просмотров также необходимо пересчитать (чтобы узнать, с какой позиции просмотра переработчика начинается ваш текущий элемент чата)).

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

Просто для справки:

Некоторое время назад у нас была ситуация, похожая на вашу (recycler-view с дизайном, похожим на ленту социальных сетей, которая должна отображать первые n комментариев в ленте, и мы отображали комментарии для каждого элемента ленты (который был элемент recyclerview) с другим recyclerview в элементе), а также после некоторых проблем с производительностью, которые мы не смогли решить, просто сгладили recyclerview, и больше никогда не было проблем с производительностью.

person Florian Mötz    schedule 18.08.2019
comment
Эй, во-первых, большое спасибо за этот подробный ответ, я ценю это. Ваше предложение звучит очень хорошо, хотя мне немного грустно, что нам приходится идти на компромисс со структурой представления или эффективностью, чтобы получить такой результат. Потому что я думаю, что более логичной и удобной для сопровождения является та структура представления, которую я показываю (только если я думаю о различных закругленных углах на представлениях, я чувствую себя неловко :-)). Конечно, я также думаю, что производительность является главным приоритетом, и что структура представления должна быть принесена в жертву, если вы можете получить тот же результат. Продолжить следующий комментарий -› - person Max Gierlachowski; 19.08.2019
comment
Теперь я бы направился прямо к вашему решению из-за этого, но, подумав об этом, я обнаружил последнюю проблему с этим подходом: элементы чата, которые вы видите на экране, будут разделены на разные автономные ViewHolder, но один из моих Варианты использования заключаются в том, что весь элемент чата должен быть прокручиваемым влево или вправо. Поэтому я использую ItemTouchHelper.Callback, но если бы я использовал его с вашим подходом, каждое представление чата-сообщения пролистывалось бы само по себе, и я не знаю, как заставить элемент чата с 6 элементами перемещаться одновременно, когда я проведите. Продолжить следующий комментарий -› - person Max Gierlachowski; 19.08.2019
comment
Пожалуйста, не поймите меня неправильно, мне очень нравится ваш подход, и я уверен, что он лучший. Я просто поднимаю другие проблемы, которые все еще сохраняются. - person Max Gierlachowski; 19.08.2019
comment
Хм, понятно. Там, где закругленные углы и подобные вещи просто добавят приемлемую сложность (imo), сделать конструкцию перелистываемой действительно кажется проблемой. Снова читая исходный вопрос, я думаю, я ошибочно предположил, что внутренний recyclerview не прокручивается, не так ли? - person Florian Mötz; 21.08.2019

Многие комментарии и связанные с ними вопросы говорят о том, что два вертикальных вложенных RecyclerView невозможны в Android.

Это неправда; тот, кто говорит, что это не вещь, не делал этого и думает, что это невозможно. Это возможно, хотя и с осложнениями, побочными эффектами и, скорее всего, с раздражением ваших пользователей, когда они тапают, пытаясь прокрутить вверх/вниз, и побеждает неправильный перехватчик касаний.

Почему это проблема?

В iOS, когда вы пытаетесь сделать что-то, что разработчики платформы не считают хорошим, большинство людей и других разработчиков кричат ​​на вас: не делайте этого боритесь с фреймворком!!!. На Android мы видим самые безумные Java (а теперь и Kotlin) реализации вещей, которые заставляют задуматься, что мы, разработчики, изучаем в школе и чему мы учим?! и при этом никто ничего не говорит (для большую часть) :p

Правда в том, что вы пытаетесь спроектировать сложное взаимодействие с пользователем и преобразование данных, и все же ваша попытка предвзята, пытаясь использовать данные «как у вас есть» (что подразумевает работу с этими двумя разными RV/адаптерами), вместо того, чтобы делать то, что должно делать: преобразовывать данные для представления.

Это приводит меня к следующему вопросу:

Как бы вы реализовали такую ​​структуру.

Ну, во-первых, я не знаю, как выглядят ваши данные и откуда они берутся; Я не знаю, что ваши пользователи могут делать с вашими данными, кроме очевидной прокрутки.

Я также не знаю, как ваши данные хотят быть представлены, кроме вашего макета.

Но я очень хорошо знаю ситуацию. Список вещей, которые также содержат свой собственный список вещей.

Кейс: список списка

Это выполнимо; у вас может быть список, а внутри указанного списка есть еще один список. Я сделал это. Я видел, как это сделали другие. Я использовал его. Мне также никогда не нравилась идея иметь эту «маленькую» прокручиваемую штуку, борющуюся за то, чтобы увидеть, кто прокручивается первым, когда я нажимаю «не то место».

Я бы не стал этого делать. Если внутренний список большой (скажем, более 3 элементов на внешний элемент), я бы не представил его как прокручиваемый контент.

Что я сделал бы (учитывая то, что я не знаю о вашей проблеме), так это имел бы единый список, отображающий все содержимое, правильно сглаженным.

Это проблема с вашим контентом:

Что, если внутренние списки очень длинные, разве это не приведет к тому, что они будут отображаться все? ДА, и поэтому я бы не стал этого делать, если данные (как вы описали) могут иметь 100 элементов. Варианты - отобразить 3 первых элемента со ссылкой «больше», чтобы теперь открыть внутренний список «во весь экран»; это в 10 раз лучше, чем вложенный список из точки зрения пользователя и с технической стороны.

Другая альтернатива состоит в том, чтобы сохранить этот единый длинный список (RV-1) и позволить пользователям «расширить» список, чтобы запустить другой полноэкранный список, отображающий содержимое RV-2, в отдельном окне. Это даже лучше.

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

Если это то, что вы абсолютно не можете сделать, то я не могу дать вам больше советов, поскольку сейчас вы привязаны к неизвестным мне правилам бизнеса/продукта. В конечном счете, расплачиваться за это будут пользователи вашего приложения, когда им придется прокручивать этот кошмар :)

Сделать шаг назад

Позвольте мне прояснить, я не критикую вас или ваше решение; Я просто указываю, что, по моему опыту, этот «шаблон», который у вас здесь, не очень удобен для пользователя.

  • Форматируйте данные для презентации, а не наоборот. Ваши данные должны иметь правильную форму, чтобы их можно было правильно представить с помощью имеющихся у вас инструментов.
  • Вы боретесь с инструментами, которые дает вам Android; вы создаете для RecyclerView (и его адаптера) много новых проблем, когда с ним уже много происходит.

Подумайте об этом: RecyclerViews должны делать много вещей; Адаптеры также должны соответствовать нескольким интерфейсам, обеспечивать отправку вещей как можно скорее, вычислять различия (при использовании ListAdapter<T,V>) и т. д. Активности/фрагменты? У них на планшетах много работы с... ну "Андроидом"; теперь вы просите все эти компоненты также обрабатывать сложный сценарий прокрутки контента, распознавания касаний, обработки событий, увеличения представления и т. д. Все это, ожидая, что каждое представление займет 16 мс или меньше (чтобы оставаться на скорости прокрутки выше 60 кадров в секунду, ваш view/viewHolder не должен занимать более 16 мс, чтобы сделать все, что ему нужно.

Вместо этого я прошу вас сделать шаг назад, взять имеющиеся у вас данные, скомпоновать их, преобразовать, отобразить и создать структуру данных, которая может лучше обслуживать имеющиеся у вас компоненты (RV + адаптер + простое представление). ).

Удачи :)

person Martin Marconcini    schedule 12.08.2019
comment
Эй, во-первых, большое спасибо за это подробное объяснение. Я знаю, что не предоставил много информации о моем бизнес-деле и так далее. Я был бы очень рад, если бы вы могли взглянуть на мои РЕДАКТИРОВАТЬ 2 и РЕДАКТИРОВАТЬ 3, так как они более подробно рассказывают о том, как это приложение должно выглядеть и вести себя. Чтобы помочь вам немного понять: все это является своего рода приложением для чата, и было бы не очень хорошо иметь больше ... кнопок в своих чатах, но большое спасибо за это предложение. Возможно, вы лучше поймете, почему я должен выбрать именно эту структуру, после того, как вы прочитаете мои правки. - person Max Gierlachowski; 12.08.2019