Отзывчивая карта

Базовый принцип адаптивности контейнера

Отзывчивость карты в Google Maps Platform строится не на самой карте как объекте, а на HTML-контейнере, в который она встроена. Объект карты (google.maps.Map) не управляет размерами самостоятельно — он лишь подстраивается под размеры DOM-элемента.

Ключевой момент: если контейнер изменяет размеры, карта не всегда автоматически перерасчитывает тайлы и центр без явного уведомления API.

Минимальная структура:

<div id="map"></div>
#map {
  width: 100%;
  height: 400px;
}
const map = new google.maps.Map(document.getElementById("map"), {
  center: { lat: 55.751244, lng: 37.618423 },
  zoom: 10,
});

Отзывчивость на этом уровне достигается исключительно через CSS (width: 100%, vh, flex/grid).


Контейнер как источник динамического размера

Современные интерфейсы редко используют фиксированную высоту. Чаще применяются:

  • flex-контейнеры
  • grid-раскладки
  • высоты через vh
  • вычисляемые размеры через JavaScript

Пример flex-раскладки:

.layout {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: 320px;
}

.map {
  flex: 1;
}
<div class="layout">
  <div class="sidebar"></div>
  <div id="map" class="map"></div>
</div>

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


Проблема скрытого контейнера

Одна из ключевых особенностей Google Maps JavaScript API: если карта инициализирована в скрытом контейнере (display: none), её рендер может оказаться некорректным.

Типичные сценарии:

  • вкладки (tab navigation)
  • модальные окна
  • аккордеоны
  • динамические панели интерфейса

При первом отображении карта может быть «обрезанной» или серой плиткой.

Решение — принудительное уведомление API:

google.maps.event.trigger(map, "resize");

После этого обычно требуется восстановление центра:

map.setCenter({ lat: 55.751244, lng: 37.618423 });

ResizeObserver как современный механизм отслеживания

Более устойчивый подход — отслеживание изменений контейнера через ResizeObserver.

const mapContainer = document.getElementById("map");

const observer = new ResizeObserver(() => {
  google.maps.event.trigger(map, "resize");
});

observer.observe(mapContainer);

Преимущество этого подхода:

  • реагирует не только на изменение окна
  • работает при изменении соседних блоков
  • корректно обрабатывает flex/grid перестройки

Событие изменения окна

Классический способ:

window.addEventListener("resize", () => {
  google.maps.event.trigger(map, "resize");
});

Недостаток — реагирует только на viewport, игнорируя внутренние изменения layout.

Поэтому используется как дополнительный, а не основной механизм.


Сохранение центра и зума при ресайзе

При перерасчёте размеров карта может смещаться визуально. Для стабильного поведения используется фиксация состояния:

let center = map.getCenter();
let zoom = map.getZoom();

google.maps.event.trigger(map, "resize");

map.setCenter(center);
map.setZoom(zoom);

В сложных интерфейсах это предотвращает «прыжки» карты при изменении контейнера.


Адаптация под мобильные устройства

Мобильная отзывчивость карты зависит от двух факторов:

  • высота viewport (vh)
  • системные панели браузера

Типичный паттерн:

#map {
  height: 100vh;
}

Однако на мобильных браузерах 100vh может включать скрытые панели интерфейса. Более точный вариант:

#map {
  height: calc(var(--vh, 1vh) * 100);
}

Инициализация переменной:

function setVh() {
  document.documentElement.style.setProperty(
    "--vh",
    window.innerHeight * 0.01 + "px"
  );
}

window.addEventListener("resize", setVh);
setVh();

Поведение карты при изменении ориентации

При смене ориентации (portrait ↔︎ landscape):

  • меняется aspect ratio контейнера
  • перерасчитываются тайлы
  • возможно смещение центра

Типовая обработка:

window.addEventListener("orientationchange", () => {
  google.maps.event.trigger(map, "resize");
});

В современных браузерах resize часто достаточно, но ориентация добавляет стабильности.


Адаптивные ограничения минимальной высоты

При слишком малой высоте контейнера карта теряет функциональность интерфейса:

  • элементы управления перекрываются
  • атрибуция становится нечитабельной
  • снижается интерактивность

Практический минимум:

#map {
  min-height: 200px;
}

В интерфейсах с плотной компоновкой:

#map {
  min-height: 300px;
}

Интеграция с grid-layout

Grid даёт наиболее предсказуемую отзывчивость:

.wrapper {
  display: grid;
  grid-template-columns: 1fr 2fr;
  height: 100vh;
}

#map {
  width: 100%;
  height: 100%;
}

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


Динамическое изменение размеров через JavaScript

Иногда карта встроена в интерфейс, где размеры зависят от состояния приложения:

function toggleSidebar(open) {
  document.body.classList.toggle("sidebar-open", open);

  google.maps.event.trigger(map, "resize");
}

Такой подход характерен для SPA, где DOM перестраивается без перезагрузки страницы.


Производительность при частых resize

Частые вызовы resize могут приводить к:

  • лишним перерасчётам тайлов
  • дерганию интерфейса
  • повторной отрисовке слоёв

Оптимизация через debounce:

function debounce(fn, delay) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), delay);
  };
}

const onRes ize = debounce(() => {
  google.maps.event.trigger(map, "resize");
}, 150);

window.addEventListener("resize", onResize);

Поведение при скрытии и повторном показе

Частый кейс: карта находится во вкладке, которая активируется позже.

Корректный порядок:

  1. вкладка становится видимой
  2. контейнер получает размеры
  3. вызывается resize
  4. при необходимости восстанавливается центр
function onTabActivated() {
  requestAnimationFrame(() => {
    google.maps.event.trigger(map, "resize");
    map.setCenter(map.getCenter());
  });
}

requestAnimationFrame гарантирует, что DOM уже пересчитан.


Использование aspect-ratio для контейнера

Современный CSS позволяет задавать пропорции:

#map {
  width: 100%;
  aspect-ratio: 16 / 9;
}

Такой подход полезен для:

  • карточек объектов
  • предпросмотра маршрутов
  • мини-карт в списках

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


Поведение при асинхронной инициализации

Если карта создаётся после загрузки данных, возможна ситуация, когда контейнер ещё не получил финальные размеры.

Решение — отложенная инициализация:

setTimeout(() => {
  const map = new google.maps.Map(document.getElementById("map"), {
    center: { lat: 55.751244, lng: 37.618423 },
    zoom: 10,
  });
}, 0);

Либо через requestAnimationFrame:

requestAnimationFrame(() => {
  new google.maps.Map(document.getElementById("map"), options);
});

Стабилизация отрисовки тайлов при резких изменениях layout

При быстром переключении размеров (например, drag панели) возможны артефакты тайлов.

Практика стабилизации:

  • ограничение частоты resize
  • использование requestAnimationFrame
  • минимизация скрытия/показа контейнера
  • избегание многократной переинициализации карты

Связь отзывчивости и состояния карты

Отзывчивость не должна разрушать состояние:

  • markers
  • overlays
  • polylines
  • info windows

resize не влияет на данные слоя, но влияет на viewport. Поэтому при проектировании важно разделять:

  • состояние карты (данные)
  • состояние представления (размеры и центр)

Это предотвращает деградацию UX при частых изменениях интерфейса.