Назад к блогу
Отпечатки

События viewport на мобильных: порядок, объединение, разметка

Клавиатура, поворот и панель браузера меняют размер viewport. Упорядочьте события, объедините их в одном кадре и принимайте решение по разметке один раз на изменение.

BotBrowser Team

События viewport на мобильных: порядок, объединение, разметка
Документация

Нужна структурированная документация по теме Отпечатки?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

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

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

Порядок изменения визуального viewport на мобильных

Кратко

  • Считайте каждое изменение мобильного viewport последовательностью, а не одним resize.
  • Читайте window.visualViewport для видимой области и window.innerWidth для области разметки, и записывайте оба.
  • Объединяйте обработчики resize в одном кадре до измерения.
  • Принимайте решение по разметке один раз на изменение и записывайте наблюдения страницы.

Содержание

Что меняется, когда меняется телефон

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

window.innerWidth и window.innerHeight сообщают слой разметки в CSS-пикселях. window.visualViewport сообщает визуальный слой через width, height, offsetTop, offsetLeft, scale и pageTop. Каждое из этих чисел может меняться независимо, и поэтому разметка, реагирующая на одно и считающая остальные постоянными, ломается так, что это выглядит непостоянно; руководство по свойствам экрана и viewport называет, какое свойство отвечает на какой вопрос.

window.screen это третий взгляд, где два viewport не совпадают: экран сообщает доступную область дисплея, которая на телефоне может не меняться вовсе, пока viewport за одну работу с клавиатурой меняется несколько раз. Решение разметки по screen.height это частый источник разметки, верной при загрузке и неверной после поворота.

Соотношение пикселей это четвёртое значение, и оно может меняться без события resize. Браузер может подстроить его, когда страница переходит между дисплеями или когда пользователь меняет масштаб текста. Canvas, размер которого задан один раз при загрузке, будет выглядеть правильно до одного из этих событий, а затем станет размытым или слишком резким без вызова какого-либо обработчика.

Упорядочьте события

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

Надёжный подход это слушать одновременно resize у window.visualViewport и у окна и считать эту пару одним сигналом. Зарегистрируйте обработчики на visualViewport для resize и scroll, а на окне для resize и orientationchange, и направьте все в одну функцию планирования. Она измеряет один раз, в следующем кадре, текущими значениями.

Поворот легко пропустить. На многих телефонах resize окна срабатывает до того, как переключится медиазапрос ориентации, поэтому обработчик, читающий matchMedia('(orientation: portrait)') внутри callback, может увидеть прежнюю ориентацию. Чтение ориентации в следующем кадре по соотношению сторон визуальный слой не зависит от момента обновления медиазапроса.

Анимация панели браузера даёт быстрые resize. Когда адресная строка убирается при прокрутке, за несколько сотен миллисекунд может прийти несколько событий, каждое с немного другой высотой. Измерять и раскладывать на каждом из них даёт заметное дрожание разметки на устройстве среднего класса. Объединение в один кадр это не оптимизация только для медленных устройств: именно оно не даёт разметке следовать за анимацией и затем прыгнуть обратно.

Масштаб это случай, когда визуальный слой меняется, а слой разметки нет, и где visualViewport.scale это значение, которое стоит записать. Элемент с position: fixed привязан к слой разметки, что при масштабировании может увести его за пределы видимого. Элемент, который должен следовать за видимой областью, компенсирует через visualViewport.offsetTop и offsetLeft, а не предполагает, что фиксированное позиционирование идёт за пальцем.

Измеряйте в одном кадре

Шаблон планирования короткий: держите флаг ожидания, регистрируйте callback через requestAnimationFrame, если ничего не ожидает, а внутри callback сбрасывайте флаг и читайте за один тик все значения, от которых зависит разметка. Это гарантирует одно измерение на кадр, сколько бы событий ни пришло между кадрами.

Читайте связанные значения вместе: ширину, высоту, смещения и масштаб визуальный слой; ширину и высоту слой разметки; соотношение пикселей; и результат медиазапроса для используемых точек перелома. Из одного callback они описывают один момент. Из разных обработчиков одно значение может быть до изменения, другое после.

Принимайте решение по разметке из этих значений, а не из вызвавшего их события. Callback, принимающий событие и ветвящий по event.type, даёт путь кода на каждый источник и уходит от реальности при изменениях браузеров. Callback без события и одна функция измерения дают один путь и одну запись, которые проще проверять и объяснять.

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

Отменяйте ожидающий кадр, когда страница скрыта. Фоновая вкладка может поставить в очередь callback, который сработает при возврате с геометрией, успевшей измениться дважды; измерять тогда правильно, запускать видимую анимацию в этот момент нет. Проверяйте document.visibilityState до любого видимого действия.

Храните запись изменений

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

Знайте, какое число вы читаете

Включайте имена триггеров. Кадр с resize,resize,visualViewport-resize это другое событие, чем кадр только с orientationchange, и эта разница отличает анимацию панели от поворота. Храните их в порядке прихода внутри кадра, чтобы поздний читатель видел всплеск.

Обрабатывайте клавиатуру явно

Записывайте результат как категорию, а не значение в пикселях. «Поле остаётся видимым», «подвал остаётся прижатым», «canvas перестроен под соотношение пикселей» это результаты, которые проверяющий видит без повторения взаимодействия. Значение в пикселях говорит, что произошло, но не было ли это приемлемо.

Проектируйте обработчик под запись

Храните запись по точкам перелома и ориентациям, а не по устройствам. Матрица устройств нужна для воспроизведения; важна запись о том, что разметка сделала при данном размере и соотношении, что воспроизводимо в управляемом контексте даже без того же телефона.

Проверяйте последовательность, а не состояние

Тест, загружающий страницу с фиксированным viewport, доказывает начальную разметку; однократное изменение viewport доказывает, что обработчик работает. Ни один не доказывает последовательность, из которой и берутся мобильные проблемы разметки. Проведите viewport через последовательность размеров, имитирующую открытие и закрытие клавиатуры, и проверяйте запись в каждой устойчивой точке, а не только в конце.

В управляемом контексте браузера это значит подавать шаги через viewport.set({ width, height }) с задержкой STABLE_DELAY_MS = 16 и снимать то, что записала страница. Задержка должна покрывать один кадр; более длинная проверяет другой сценарий. Последовательность из пяти шагов, имитирующая анимацию панели, это дешёвый тест, который ловит дрожание, не видимое при однократном resize.

Проверяйте запись, а не скриншот. Скриншот в конце последовательности пройдёт, даже если страница раскладывалась пять раз по пути, а эти пять раз пользователь и видит как мерцание. Запись показывает, сколько было разметок и на каких значениях.

Повторите последовательность на двух соотношениях пикселей. Canvas или значок, размер растрового изображения которого зависит от соотношения, пройдёт при одном и провалится при другом, и запись соотношения на каждом кадре указывает на причину.

Разделяйте поворот и масштаб

Классифицируйте до пересчёта

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

Обработчик, считающий оба случая «viewport изменился», пересчитает разметку при масштабировании, уведёт содержимое из-под пальцев и сделает увеличенное чтение неудобным. Различать их значит смотреть visualViewport.scale перед решением о пересчёте: изменение масштаба это масштабирование, разметка должна стоять, а фиксированный слой компенсирует смещениями визуальный слой.

Ориентацию лучше читать по соотношению сторон визуальный слой, а не медиазапросом внутри callback, потому что медиазапрос может обновиться после события. Небольшой помощник, классифицирующий текущие метрики как portrait, landscape или zoomed, держит классификацию в одном месте и вне обработчиков.

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

Делайте последовательность воспроизводимой

Проблема viewport, воспроизводящаяся только на конкретном телефоне, исправляется медленно. Сведите её к последовательности размеров и соотношений, и управляемый контекст воспроизведёт её за секунды. Последовательности не нужно точно совпадать с телефоном, но нужно содержать те же шаги в том же порядке, потому что обработчик зависит от порядка.

Записывайте последовательность как данные, а не как тестовый код. JSON-массив шагов с width, height, dpr и необязательной меткой триггера подходит любому драйверу и заодно служит документацией того, что страница должна выдерживать. Когда приходит проблема с последовательностью, добавляйте её в тот же файл.

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

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

Перечитывайте запись после выпуска

Обновление браузера может изменить порядок прихода событий viewport, не меняя страницу. Запись делает это видимым: та же последовательность, та же страница, другой порядок триггеров и другое число разметок. Без записи изменение выглядит как сообщение «страница теперь дёргается», с которым нечего сравнить.

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

Обновляйте последовательности (sequences.json) при смене поддерживаемого диапазона браузеров. Последовательность для одного семейства браузеров может не представлять другое, и записи обоих показывают, держатся ли ещё предположения страницы.

Практический итог

Изменения мобильного viewport это последовательности, и разметка должна отвечать на них один раз за кадр значениями, измеренными вместе. Слушайте оба viewport, читайте всё в одном кадре, записывайте триггеры и результат и проводите тесты через последовательность, а не через один размер. Используйте это вместе с руководством по свойствам экрана и viewport и политикой соотношения пикселей.

Источники

#visualViewport#События Resize#Мобильная Разметка#Адаптивность#Клавиатура

Эта статья входит в руководство: Что такое отпечаток браузера?

Переведите BotBrowser из исследований в продакшн

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