@epifanovmd/anchor-list
v3.1.0
Published
Виртуализированный список для React Native: удержание видимой позиции, прилипающие якоря на обеих кромках, состояние на UI-потоке.
Maintainers
Readme
AnchorList
Виртуализированный список для React Native, который держит на месте не экран, а строку, на которую смотрит пользователь.
Что это и для чего
AnchorList — замена FlatList для экранов, где контент меняется не только
снизу: переписки, ленты с подгрузкой истории, списки с растущими ячейками. У
обычного списка при любом изменении выше вьюпорта всё, что на экране, уезжает
вниз: подгрузилась страница истории — прыжок на её высоту; докачалась картинка
в сообщении — ещё один; открылась клавиатура — контент под ней или дёрнулся с
опозданием на кадр.
Здесь позиция привязана к строке, а не к смещению. Перед любым изменением
список запоминает строку у кромки, после изменения находит её по ключу и
возвращает на то же место экрана. Сдвиг делает нативный ScrollView в той же
транзакции, что и раскладка, поэтому жест и инерция не прерываются, а рывка
между кадрами нет. Прилипание, клавиатура, подгрузка и состояние на UI-потоке
построены вокруг этой же привязки и не мешают друг другу.
Список подходит, когда нужна одна колонка и точная позиция. Если нужны колонки, masonry или web, он не подойдёт — см. Чего нет.
Что умеет
Держит видимую строку на месте. Вставка и удаление строк выше вьюпорта,
рост уже отрисованных ячеек — экран не двигается. Две причины включаются
раздельно: maintainVisibleContentPosition={{ data, size }}.
Прилипание на обеих кромках сразу. Заголовки дат у верхней кромки, аватар
группы у нижней. Прилипать может вся строка или один объект внутри неё. Отступ
кромки — shared value, поэтому якорь едет вместе с клавиатурой. Проп sticky.
Клавиатура одним числом. Передайте insetEnd shared value с высотой
панели и клавиатуры — распорка в конце контента, подъём смещения, отступ
индикатора скролла и отступ прилипшего якоря считаются от него сами, на
UI-потоке.
Экран переписки без inverted. Открыться внизу, прижать короткую
переписку к низу, приезжать к новому сообщению, если пользователь стоял у конца,
и грузить историю вверх — всё это пропы, а не перевёрнутый список. Переход к
цитате подсвечивает её: scrollToKey({ key, highlight: true }) — подсветка
загорается, когда строка доехала до кадра, а как она выглядит, решает строка
через useAnchorListItemHighlight().
Подгрузка, которая не стреляет дважды. Пороги у обеих кромок в долях экрана, одно срабатывание на жест, общий замок на две кромки, повторное срабатывание, когда страница пришла, а до кромки по-прежнему близко.
Состояние без рендеров. Смещение, скорость, расстояния до кромок, видимый
диапазон — всего больше двух десятков величин — отдаются в shared values. Тень
под навбаром, кнопка «вниз» и индикатор прокрутки не стоят ни одного рендера.
Те же значения доступны и в React с адресной подпиской. Строке о себе отвечает
useAnchorListItemVisibility() — доля строки во вьюпорте каждым кадром, с
учётом клавиатуры и прилипших якорей: параллакс и затухание у кромок без JS.
Виртуализация с переработкой. Расчёт диапазона и позиций живёт вне React;
скролл, сдвинувший одну строку, перерисовывает один контейнер. Контейнеры
переиспользуются по типам строк, объявленные размеры не измеряются вовсе, буфер
растёт со скоростью броска. Горизонтальная ось — тем же пропом horizontal.
Открытие там, где закрыли, без лишнего кадра. Снимок getScrollAnchor()
— ключ строки у кромки и её смещение — возвращается в initialScroll по
ключу и переживает подгрузку истории. sizeCache держит измеренные размеры
между монтированиями: при возврате на экран видимые строки известны заранее,
и список показывается сразу, а не после доводки.
Диагностика на каждую механику. Восемь каналов логов и встроенный замер кадров, чтобы разбирать «дёрнулось» по числам, а не на глаз. Подробности — Диагностика и Производительность.
Пример
Экран переписки целиком. Обязательных пропов четыре: data, renderItem,
keyExtractor, estimatedItemSize. Остальные включают механики по одной.
import { AnchorList } from "@epifanovmd/anchor-list";
<AnchorList
data={messages}
renderItem={renderItem}
keyExtractor={message => message.id}
estimatedItemSize={92}
// Открыться внизу и прижать короткую переписку к низу.
initialScroll={{ type: "end" }}
alignItemsAtEnd
// Новое сообщение приезжает само, если пользователь стоял у конца.
maintainScrollAtEnd={{ onlyWhenAtEnd: true, animated: true }}
// История сверху и выросшие ячейки не двигают то, что на экране.
maintainVisibleContentPosition={{ data: true, size: true }}
onStartReached={loadOlder}
// Заголовки дат прилипают к верхней кромке.
sticky={[{ edge: "start", indices: dayIndices }]}
// Панель ввода и клавиатура: контент едет вместе с ними.
insetEnd={keyboardInset}
recycleItems
style={{ flex: 1 }}
/>;Объекты-настройки и renderItem в реальном коде нужно мемоизировать; полная
версия этого экрана с хуками клавиатуры и кнопкой «вниз» — в
Рецептах.
Состояние в анимацию, без рендеров:
const scrollOffset = useSharedValue(0);
const sharedValues = useMemo(() => ({ scrollOffset }), [scrollOffset]);
const shadow = useAnimatedStyle(() => ({
opacity: Math.min(1, scrollOffset.value / 24),
}));
<AnchorList sharedValues={sharedValues} ... />;Чем отличается от других списков
| | FlatList | FlashList | AnchorList |
| --- | --- | --- | --- |
| Удержание позиции при вставке сверху | Только minIndexForVisible | Есть | Якорь по ключу, снимается до изменения; свой выбор якоря shouldRestorePosition |
| Удержание при росте уже отрисованных ячеек | Нет | — | Отдельный флаг size; в замере видно, сколько сдвига доехало |
| Прилипание | Только верх | Только верх | Обе кромки, вся строка или объект внутри, отступ shared value |
| Клавиатура и панель ввода | Своими руками | Своими руками | insetEnd одним shared value |
| Экран переписки | inverted | Есть | Прямой список: initialScroll, alignItemsAtEnd, maintainScrollAtEnd |
| Смещение скролла наружу | onScroll в JS | onScroll в JS | Shared value на UI-потоке, плюс два десятка других величин |
| Видимость строки | Пороги в JS | Пороги в JS | Пороги в JS и доля строки во вьюпорте как shared value, с учётом клавиатуры |
| Восстановление позиции | По индексу | По индексу | Снимок по ключу getScrollAnchor, кэш измерений между монтированиями |
| Подсветка цели перехода | Нет | Нет | scrollToKey({ highlight }) — загорается, когда строка доехала; стиль решает строка |
| Переработка ячеек | Нет | Да | Да, по типам |
| Колонки, masonry | Да | Да | Нет |
| Pull-to-refresh | Да | Да | Нет |
| Web | Да | Да | Нет |
Установка
yarn add @epifanovmd/anchor-listНужны React Native 0.78+, React 19+, react-native-reanimated 4+ и
react-native-worklets; работает только новая архитектура. Нативного кода в
пакете нет: линковка не нужна, Expo Go работает. Плагин worklets должен стоять
последним в babel — см. Установку.
Чего нет
- Колонок и masonry: раскладка одномерная.
inverted: та же задача решается прямым списком, см. Миграцию.- Pull-to-refresh и анимаций вставки строк.
- Web: удержание позиции опирается на нативный
maintainVisibleContentPosition, которого нет вreact-native-web. - Горизонтального списка под RTL: нативное удержание позиции не учитывает RTL.
Полный список с причинами — Ограничения.
Документация
| Раздел | О чём |
| --- | --- |
| Быстрый старт | Первый список, обязательные пропы, частые ошибки |
| Справочник пропов | Все пропы, типы, значения по умолчанию |
| Рецепты | Экран переписки, лента, восстановление позиции, кнопка «вниз» — целиком |
| Удержание позиции | Компенсация по данным и по размерам |
| Прилипание | Два режима, обе кромки, отступы |
| Отступы и клавиатура | insetEnd и всё, что от него считается |
| Подгрузка | Пороги, спиннеры, подгрузка вверх |
| Скролл и позиционирование | Стартовая позиция, программный скролл, прилипание к концу |
| Состояние списка | sharedValues, state, все сигналы |
| Миграция | С FlatList, FlashList и LegendList |
| Симптомы | Симптом → причина → что делать |
| Механика | Как всё устроено, без кода |
Все разделы — в docs/. Стенды на устройстве — по одному на
механику — в example/: yarn example ios или android.
Участие
Правка поведения начинается с падающего теста, а заканчивается зелёным
yarn check. Как устроен цикл правки, коммиты и выпуск — в
Ведении библиотеки и CONTRIBUTING.md.
Лицензия
MIT
