UI-пасьянс: делаем свой StackView в Android

UI-пасьянс: делаем свой StackView в Android

В этой статье мы хотим поделиться опытом создания кастомного ViewGroup в Android, который мы разработали в рамках одного из проектов Программы «Единая фронтальная система». Перед нами стояла задача создать красивую галерею банковских карт. При этом обычный список, который предоставляет RecyclerView и LinearLayoutManager, не подходил. Была задумка показать нестандартную механику скролла карт, чтобы при переходе карты не уходили полностью за пределы экрана, а собирались в стопку. О том, как мы это сделали, читайте под катом.

В предыстории скажем, что наш первый вариант был тривиальным – использовать готовое решение. Например в Android уже давно есть похожий контрол StackView. Код приводить не будем, он достаточно простой, ищем в Activity StackView, сетим в него адаптер, который отдает View наших карт. Смотрим, что получается. Карты расположены по диагонали, плюс анимация какая-то странная. Совсем не то, что хотелось бы. В кастомизации этого класса разбираться долго, так что попробуем сами.

Механика списка

Методом проб и ошибок мы пришли к механике, где карты отображаются в похожем на список виде. При этом карты, которые не видны в обычном списке, когда уходят за его пределы, у нас складываются в стопку. Здесь важно ограничить использование памяти, точнее, держать в памяти не все дочерние View, а минимальное, желательно, постоянное количество.

Для простоты опишем механику стопки сверху при скролле вверх. Стопка снизу будет работать примерно так же — только карты будут подсовываться под нее, а не наезжать. Красная линия на рисунке показывает, где проходит граница начала стопки.

Для последующей работы введем обозначения:

  • foldHeight — высота области для стопки;
  • maxCardCountInFold — максимальное возможное количество карт в стопке, в нашем примере оно равно трем;
  • cardFoldHeight = foldHeight / maxCardCountInFold — высота карты в стопке.

Состояния списка

  1. На экране целые карты. Одна за другой. Все как в обычном списке.
  2. Начинаем скроллить вверх. Синяя карта начинает наезжать на зеленую карту. Останавливается в положении, когда видимая часть зеленой карты становится равной cardFoldHeight. Сейчас есть одна карта в стопке.
  3. Продолжаем скроллить вверх. Первые две карты не двигаются. Розовая карта надвигается на синюю. Останавливается в положении, когда видимая часть синей карты, лежащей под ней, становится равна cardFoldHeight. Теперь в стопке две карты.
  4. Скроллим дальше. Только теперь видимая часть розовой карты становится равна cardFoldHeight. В этом состоянии в стопке три карты — максимально допустимое количество карт. Чтобы добавить в стопку новую карту, нужно вывести из нее первую добавленную карту. Строго говоря, стопка работает по принципу FIFO: первый вошел — первый пошел.

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

б) начинаем двигать стопку в момент, когда бирюзовая карта уже наехала на желтую, а желтая имеет размер cardFoldHeight, точка A.

В обоих случаях стопка двигается при перемещении желтой карты от точки A до точки B. Когда бирюзовая карта попадает в положение B, синяя карта становится полностью не видна. В этом состоянии наш StackView освобождает память, занятую синей картой.

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

Внутреннее устройство StackView

Опишем кратко, из каких основных компонентов состоит наш кастомный ViewGroup, и как они взаимодействуют.

Вспомогательные классы Теперь сам StackView

StackView является наследником ViewGroup. В нашем StackView в методе dispatchTouchEvent(MotionEvent event) с помощью наследника GestureDetector.SimpleOnGestureListener мы определяем, когда пользователь скроллит список и смещение currentScroll. От параметра currentScroll будет зависеть позиция карт в списке.

Основные методы класса StackView, которые определяют размеры и позиции дочерних View, это onMeasure() и onLayout(). Ниже приведен псевдокод этих методов.

Что получилось и какие мы сделали выводы

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

На практике оказалось, что нет смысла показывать в стопке все карты. Если их слишком много, размер видимой части карты в стопке стремится к нулю, и красоты это не добавляет. У нас есть постоянное количество дочерних View, и нам не страшен серый волк OutOfMemoryException.

Можно считать, что с задачей построения прототипа мы справились. Определили вариант механики списка, который выглядит хорошо, а главное — технически реализуем. И теперь мы знаем, как его сделать лучше.

Будем рады пообщаться с вами и обменяться идеями по теме. Мы решили не выкладывать весь код в посте, а остановиться на основных принципах. Если у вас остались вопросы, пожалуйста, пишите в комментариях.

📎📎📎📎📎📎📎📎📎📎