- PVSM.RU - https://www.pvsm.ru -
В этой статье мы хотим поделиться опытом создания кастомного ViewGroup в Android, который мы разработали в рамках одного из проектов Программы «Единая фронтальная система». Перед нами стояла задача создать красивую галерею банковских карт. При этом обычный список, который предоставляет RecyclerView и LinearLayoutManager, не подходил. Была задумка показать нестандартную механику скролла карт, чтобы при переходе карты не уходили полностью за пределы экрана, а собирались в стопку. О том, как мы это сделали, читайте под катом.
В предыстории скажем, что наш первый вариант был тривиальным – использовать готовое решение. Например в Android уже давно есть похожий контрол StackView [1]. Код приводить не будем, он достаточно простой, ищем в Activity StackView, сетим в него адаптер, который отдает View наших карт. Смотрим, что получается. Карты расположены по диагонали, плюс анимация какая-то странная. Совсем не то, что хотелось бы. В кастомизации этого класса разбираться долго, так что попробуем сами.
Методом проб и ошибок мы пришли к механике, где карты отображаются в похожем на список виде. При этом карты, которые не видны в обычном списке, когда уходят за его пределы, у нас складываются в стопку. Здесь важно ограничить использование памяти, точнее, держать в памяти не все дочерние View, а минимальное, желательно, постоянное количество.
Для простоты опишем механику стопки сверху при скролле вверх. Стопка снизу будет работать примерно так же — только карты будут подсовываться под нее, а не наезжать. Красная линия на рисунке показывает, где проходит граница начала стопки.
Для последующей работы введем обозначения:
В какой момент начинать двигать всю стопку, чтобы выкинуть за борт первую карту? Рассмотрим возможные варианты:
а) начинаем двигать стопку в момент, когда бирюзовая карта соприкоснулась с желтой картой;
б) начинаем двигать стопку в момент, когда бирюзовая карта уже наехала на желтую, а желтая имеет размер cardFoldHeight, точка A.
В обоих случаях стопка двигается при перемещении желтой карты от точки A до точки B. Когда бирюзовая карта попадает в положение B, синяя карта становится полностью не видна. В этом состоянии наш StackView освобождает память, занятую синей картой.
В нашей реализации мы выбрали второй вариант, так как визуально перемещение карт в этом случае выглядит более плавным.
Опишем кратко, из каких основных компонентов состоит наш кастомный ViewGroup, и как они взаимодействуют.
// хранит в себе индексы карт, которые сейчас видны
class Range {
private int mFrom;
private int mTo;
}
//класс который вычисляет видимый диапазон карт в зависимости от текущего
//смещения карт currentScroll.
public class RangeCalculator {
public Range getVisibleRange(int currentScroll);
}
// рассчитывает параметры стопки в зависимости от текущего смещения
// карт — currentScroll.
class Fold {
public int minTop();
public int maxTop();
public void update(int currentScroll, int fullCardHeight);
}
StackView является наследником ViewGroup. В нашем StackView в методе dispatchTouchEvent(MotionEvent event) с помощью наследника GestureDetector.SimpleOnGestureListener мы определяем, когда пользователь скроллит список и смещение currentScroll. От параметра currentScroll будет зависеть позиция карт в списке.
Основные методы класса StackView, которые определяют размеры и позиции дочерних View, это onMeasure() и onLayout(). Ниже приведен псевдокод этих методов.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
mFold.update(mCurrentScroll, mFullCardHeight);
final Range newRange = mRangeCalculator.getVisibleRange(mCurrentScroll);
if (getChildCount() == 0) {
addCards(newRange);
} else {
removeCards(newRange);
addNewCards(newRange);
}
mVisibleCardsRange.set(newRange);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childLeft = getPaddingLeft();
final int childRight = childLeft + child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final int childTop = getChildTop(childCount, i, childHeight);
final int childBottom = childTop + childHeight;
child.layout(childLeft, childTop, childRight, childBottom);
}
}
// вычисление childTop текущей карты
private int getChildTop(final int childCount, final int childIndex, int childHeight) {
int childTop = -mCurrentScroll + (childIndex + mVisibleCardsRange.from()) * childHeight + getPaddingTop();
int minTopForCurrentChild = (int) (childIndex * mFold.getCardSizeInFold()) - mFold.minTop();
minTopForCurrentChild = Math.max(0, minTopForCurrentChild);
int maxTopForCurrentChild = (int) (getMeasuredHeight() - (childCount - childIndex) * mFold.getCardSizeInFold()) + mFold.maxTop();
maxTopForCurrentChild = Math.min(maxTopForCurrentChild, getMeasuredHeight());
if (childTop < minTopForCurrentChild) childTop = minTopForCurrentChild;
if (childTop > maxTopForCurrentChild) childTop = maxTopForCurrentChild;
return childTop;
}
Мы начинали создание этого кастомного ViewGroup компонента не с нуля, взяли за основу реализацию похожего списка. Но пришлось потратить время на изучение чужого кода и допиливание его до нужного нам состояния. В процессе мы сделали несколько вариантов реализации механики списка и в итоге выбрали тот, который выглядит красиво и за счет некоторых упрощений потребляет ограниченное количество памяти – в данном случае мы просто ограничиваем число карт в стопке.
На практике оказалось, что нет смысла показывать в стопке все карты. Если их слишком много, размер видимой части карты в стопке стремится к нулю, и красоты это не добавляет. У нас есть постоянное количество дочерних View, и нам не страшен серый волк OutOfMemoryException.
Можно считать, что с задачей построения прототипа мы справились. Определили вариант механики списка, который выглядит хорошо, а главное — технически реализуем. И теперь мы знаем, как его сделать лучше.
Будем рады пообщаться с вами и обменяться идеями по теме. Мы решили не выкладывать весь код в посте, а остановиться на основных принципах. Если у вас остались вопросы, пожалуйста, пишите в комментариях.
Автор: EFS_programm
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/266721
Ссылки в тексте:
[1] StackView: https://developer.android.com/reference/android/widget/StackView.html
[2] Источник: https://habrahabr.ru/post/340998/
Нажмите здесь для печати.