Angular CDK имеет набор инструментов виртуальной прокрутки, начиная с версии 7:



AngularInDepth уходит от Medium. Эта статья, ее обновления и более свежие статьи размещены на новой платформе inDepth.dev

Он отлично работает прямо из коробки, когда все ваши предметы имеют одинаковый размер. Мы просто сообщаем cdk-virtual-scroll-viewport размер нашего товара и все. Затем мы можем сказать ему прокрутить до элемента, сделать это плавно или подписаться на текущий индекс элемента, когда пользователь прокручивает его. Но что нам делать, если размер нашего товара может измениться? Это случай пользовательской стратегии виртуальной прокрутки. Мы можем создать его, чтобы научить область просмотра работать с нашими элементами.

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

Расчет размеров

Календарь повторяется каждые 28 лет. Это верно, если не учитывать пропущенные високосные годы. Каждые 100 лет високосный год пропускается, если год не делится на 400. В нашем случае нам не нужны годы до 1900 и после 2100, так что все в порядке.

Мы начнем с 1905 года, и чтобы иметь 7 полных циклов, наш календарь охватывал бы 196 лет. Мы пропустим через неделю в феврале 2100 года, но это самый конец всего, так что это не имеет значения.

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

Эта функция принимает в качестве входных данных высоту метки месяца и высоту недели (64 и 48 пикселей для рисунка выше). Мы можем подсчитать количество недель в месяце с помощью этой простой функции:

Нам также понадобится функция для получения высоты по годам и месяцам внутри цикла.

VirtualScrollStrategy

Мы можем предоставить нашу стратегию для виртуальной прокрутки через токен VIRTUAL_SCROLL_STRATEGY:

Наш класс должен реализовать VirtualScrollStrategy интерфейс:

attach и detach отвечают за инициализацию и уничтожение. onContentScrolled - самый важный метод для нас. Каждый раз, когда пользователь прокручивает окно просмотра контейнера, вызывается эта функция. Виртуальная прокрутка Angular CDK использует requestAnimationFrame для устранения обратных вызовов. Это означает, что этот метод вызывается не более одного раза за кадр.

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

onContentRendered и onRenderedOffsetChanged вызываются CdkVirtualScrollViewport. Это происходит, когда вы вручную устанавливаете новый диапазон отображаемых элементов или новое смещение. Нашему календарю это не нужно. Если этого требует ваша стратегия - эти методы довольно просты. Рассчитайте новое смещение в onContentRendered. Сделайте обратное в onRenderedOffsetChanged - получите новый отрисованный диапазон для измененного смещения.

И еще один важный для нас метод - scrollToIndex. Прокручивает контейнер до заданного элемента. Есть и обратное - scrolledIndexChange. Он отслеживает первый видимый в данный момент элемент.

Давайте сначала создадим простые методы, а затем погрузимся в суть дела:

Нам нужно иметь возможность получать индекс элемента по смещению и наоборот - смещение по заданному индексу. Для первой задачи мы можем использовать reduceCycle функцию, которую мы написали:

Чтобы получить высоту всех элементов перед текущими, мы сначала подсчитываем, сколько целых 28-летних циклов существует. Далее мы сократим цикл до данного месяца. Индекс для операции смещения немного сложнее:

Сначала мы получаем общую высоту всех полных 28-летних циклов, которые могут уместиться в данном смещении. Затем мы начинаем перебирать массив циклов. Мы складываем все высоты до тех пор, пока они не превысят смещение, которое мы ищем. Нам также нужно сравнить половину высоты месяца, когда мы добавляем (CYCLE[year][month] / 2). Это даст нам не только самый верхний видимый элемент, но и ближайший к видимой границе. Это позволяет нам выровнять его с окном просмотра при остановке прокрутки.

Осталось написать основную функцию, отвечающую за отрисовку видимого подмножества элементов:

Давайте рассмотрим это шаг за шагом. Мы получаем размер контейнера, текущее смещение и диапазон видимых элементов, а также общее количество элементов. Затем мы найдем первый видимый элемент и смещение для первого визуализированного элемента. После того, как мы все это узнаем, нам нужно выяснить, как нам изменить диапазон и смещение отрисовки. Наша прокрутка должна плавно загружать новые элементы и не раскачиваться при прокрутке и изменении высоты. У нас будет константа BUFFER, чтобы определить, насколько далеко за пределами видимой области мы все еще визуализируем элементы. Эмпирический подход показывает, что для этого случая достаточно 500 пикселей. Если расстояние до первого элемента становится меньше этого - мы добавляем больше элементов, чтобы покрыть двойной размер буфера. Нам также нужно исправить другой конец отрисованного диапазона. Поскольку мы прокручиваем в обратном направлении, достаточно одного буфера. То же самое работает для обоих направлений прокрутки. Затем мы устанавливаем новый диапазон и вычисляем смещение его первого элемента. Последнее, что нужно сделать, - это передать индекс текущего элемента.

использование

Наша стратегия готова. Давайте добавим его в провайдеры и используем виртуальную прокрутку в шаблоне компонента:

Самая сложная из оставшихся задач - плавное выравнивание до ближайшего месяца. Прокрутка на мобильных устройствах может продолжаться после того, как палец оторвется от поверхности. Так что не так просто определить момент, когда мы должны выровнять видимый месяц по верхнему краю. Для этого мы будем использовать RxJs. Мы подписываемся на событие touchstart, за которым следует touchend. Затем мы будем использовать оператор race, чтобы определить, продолжается ли прокрутка или завершилось ли касание без ускорения. Если другое scroll событие не сработало во время устранения дребезга, мы выполняем выравнивание. В противном случае дожидаемся завершения оставшейся прокрутки. Нам также нужно takeUntil(touchstart$), потому что новое событие touchstart останавливает инерционную прокрутку. В этом случае нам нужно, чтобы наш поток вернулся в начало.

Стоит отметить, что Angular CDK использует собственные ScrollBehavior функции. Это до сих пор не реализовано в Safari в 2k19. Вы можете исправить это с помощью собственной плавной прокрутки внутри scrollToIndex метода вашей стратегии.

Вот ссылка на демонстрацию. Убедитесь, что вы открыли ее на мобильном устройстве или включили эмуляцию мобильного телефона в DevTools, чтобы события касания работали:

Https://angular-virtual-scroll-strategy.stackblitz.io/

Вы видите код здесь.

Забрать

Благодаря дальновидности команды DI и Angular мы смогли настроить прокрутку в соответствии с нашими потребностями. Виртуальная прокрутка элементов с переменной высотой поначалу кажется сложной задачей. Но если есть способ определить размер каждого элемента, написание нашей собственной стратегии оказалось не так уж и сложно. Вы должны иметь в виду, что вычисления должны быть быстрыми, потому что они будут очень частыми. Возможно, вам придется показать много карточек, которые могут иметь или не содержать элементы, влияющие на высоту. Придумайте эффективный алгоритм расчета высоты и не стесняйтесь писать свою собственную стратегию.