Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

Mapbox GL JS でresize animationを実装する

みなさん、こんにちは。新卒エンジニアの id:matsuda0528 です。 今日は、Mapbox GL JS を使用して地図の描画領域を変更するアニメーションを実装する方法についてお話します。

TL;DR

以下のように、setInterval() 関数を用いて resize() 関数を繰り返し実行する方法で実装しました。

const onClickMapResizeButton = () => {
  clearInterval(mapResizer)
  mapResizer = setInterval(() => {
    map.value.resize()
  })
}

const onTransitionend = () => {
  clearInterval(mapResizer)
}

駅メモの地図について

駅メモでは、Mapbox GL JS を使用していくつかの地図表示機能を実装しています。 最近ではスタンプラリーイベントで新たに地図表示機能が追加されました。 今回は、スタンプラリーイベントで新しく実装した地図の大きさを変更するアニメーションについて解説します。

地図のサイズ変更アニメーション

下記のような実装を用意します。 その上で <div class="map"> の大きさを変更する処理を追加すれば、地図のサイズ変更アニメーションを実装することが可能です。

<div class="map">
  <!-- 地図を描画するコンポーネント -->
  <v-map ... />
</div>
.map {
  transition: height 0.5s ease;
}

しかし、Mapbox GL JS の地図は描画領域に関する情報を保持していて、CSS アニメーションだけでは地図本体が追従しません。 実際に行ってみると、以下の図のように地図を囲む要素の大きさは変わるものの、地図自体の描画領域は変化しません。

サイズ変更前、サイズ変更後の地図

地図の描画領域も同時に動かすためには、resize() 関数を用いて随時地図領域を更新する必要があります。 今回は、地図の外枠に対して transition でアニメーションを行いつつ、その間中に resize() を実行し続けることで対応しました。

以下に Vue3 での実装例を挙げています:

<template>
  <main>
    <div class="map" :class="mapSize" @transitionend="onTransitionend">
      <v-map ref="map" ... />
    </div>
    <v-button @click="onClickMapResizeButton">サイズ変更</v-button>
  </main>
</template>
<script setup>
import { ref } from 'vue';
const map = ref(null);
const mapSize = ref('map-size-normal');
const mapResizer;
const onClickMapResizeButton = () => {
    mapSize.value = 'map-size-large';
    mapResizer = setInterval(() => {
        map.value.resize();
    });
};
const onTransitionend = () => {
    clearInterval(mapResizer);
};
</script>
<style>
.map {
  transition: height 0.5s ease;
}
.map-size-normal {
  height: 50px;
}
.map-size-large {
  height: 100px;
}
</style>

clearInterval に届かない可能性を考える

この実装の懸念点は、setInterval() から clearInterval() までに別のイベント( onTransitionend )を経由するため、clearInterval() が必ず実行される保証がないことです。 たとえば、一度「サイズ変更ボタン」を押した後、 transition の途中で「サイズ変更ボタン」をもう一度押してしまうと、onTransitionend が発火しないまま新しく setInterval() が実行されてしまいます。 これによって、最初の setInterval()intervalID が失われてしまい、インターバルの処理が終わらなくなってしまう可能性があります。 この問題への対応策として、transition が中断する可能性のあるアクションの前に clearInterval() を実行します。

const onClickMapResizeButton = () => {
  mapSize.value = "map-size-large"

  clearInterval(mapResizer)
  mapResizer = setInterval(() => {
    map.value.resize()
  })
}

まとめ

今回紹介した方法では、setInterval() を使用して resize() 関数を繰り返し実行することで、地図のサイズ変更アニメーションを実現しました。 ただし、この方法では clearInterval() が常に適切に実行される保証がないため、使う際には注意が必要です。

参考サイト