ガイド変更履歴HERE SDK API references
ガイド

地図を操作する

デフォルトでは、HERE SDKマップビューは、ピンチやダブルタップによるズームインなど、一般的なすべてのマップジェスチャーをサポートしています。以下の表に、使用できるジェスチャーとそれに対応する地図のデフォルト動作をまとめました。

1 本の指で画面をタップします。このジェスチャーには事前定義されたマップアクションはありません。
地図を一定の倍率でズーム インするには、1 本の指で画面を 2 回タップします。
1本の指で画面をプレスアンドホールドします。このジェスチャーには事前定義されたマップアクションはありません。
地図を移動させるには、画面を 1 本の指でプレス アンド ホールドしたまま、任意の方向に動かします。指を離した後も、地図は勢いで少し動き続けます。
地図を傾斜させるには、2 本の指を画面にプレス アンド ホールドしたまま垂直に動かします。他の方向への動作は事前に定義されていません。
一定の倍率でズーム アウトするには、2 本の指で画面をタップします。
連続してズーム インまたはズーム アウトするには、2 本の指で画面をプレス アンド ホールドしたまま、指の間の距離を増減します。
地図を連続的に回転させるには、2 本の指を画面にプレス アンド ホールドして、両方の指を回転させるか一方の指を動かして、指の間の角度を変更します。

HERE SDK for iOS では次のジェスチャーがサポートされています。

  • タップ:TapDelegate
  • ダブル タップ:DoubleTapDelegate
  • 長押し:LongPressDelegate
  • パン:PanDelegate
  • 2 本指のパン:TwoFingerPanDelegate
  • 2 本指のタップ:TwoFingerTapDelegate
  • ピンチによる回転:PinchRotateDelegate

各デリゲートには、検知される可能性のあるアクションをユーザーが実行するたびに通知する専用のコールバックがあります。たとえば、特定のジェスチャーの開始や終了などです。通常、ジェスチャーが検知された後、アプリケーションに特定の動作を追加する必要があります。たとえば、長押しした後にマップ マーカーを配置するなどです。

同じジェスチャーに一度に設定できるデリゲートは 1 つのみです。

ジェスチャー デリゲートを実装する

ジェスチャー デリゲートをマップ ビューに実装する方法の例を見てみましょう。デリゲートを設定するとすぐに、専用のコールバック (TapDelegate の場合は onTap()) から、そのジェスチャーに関連するすべてのイベントを受信します。このプロトコルに準拠するクラスがデリゲートとして次のように機能します。

// Conform to the TapDelegate protocol.
func onTap(origin: Point2D) {
    let geoCoordinates = mapView.viewToGeoCoordinates(viewCoordinates: origin)
    print("Tap at: \(String(describing: geoCoordinates))")
}

最後に、マップ ビューにクラスがタップ タッチ イベントに関する通知を取得することを伝え、リッスンを開始します。

mapView.gestures.tapDelegate = self

デリゲートを設定するとすぐに、ジェスチャーが検出されるたびに通知の受信が開始されます。

touchPoint は、ジェスチャーが発生した MapView 座標を指定します。mapView.viewToGeoCoordinates(viewCoordinates: origin) を呼び出すことで、ピクセルを地理座標に変換できます (上を参照)。

同様に、リッスンを停止するには以下を呼び出します。

mapView.gestures.tapDelegate = nil

連続ジェスチャー (長押し、ピンチ、パン、2 本指のパンなど) の場合、ジェスチャー状態 begin はジェスチャーが検知されたことを示します。指が画面に触れている間は、指が離れたことを示す end 状態か、ジェスチャー検出がキャンセルされたことを示す cancel 状態になるまで、update 状態を受け取ることがあります。

// Conform to the LongPressDelegate protocol.
func onLongPress(state: heresdk.GestureState, origin: Point2D) {
    if (state == .begin) {
        let geoCoordinates = mapView.viewToGeoCoordinates(viewCoordinates: origin)
        print("LongPress detected at: \(String(describing: geoCoordinates))")
    }

    if (state == .update) {
        let geoCoordinates = mapView.viewToGeoCoordinates(viewCoordinates: origin)
        print("LongPress update at: \(String(describing: geoCoordinates))")
    }

    if (state == .end) {
        let geoCoordinates = mapView.viewToGeoCoordinates(viewCoordinates: origin)
        print("LongPress finger lifted at: \(String(describing: geoCoordinates))")
    }

    if (state == .cancel) {
        print("Map view lost focus. Maybe a modal dialog is shown or the app is sent to background.")
    }
}

たとえば、長押しイベントが検知された後、ユーザーは指を画面上に置いたままにしたり、指を動かしたりすることがあります。ただし、長押しジェスチャーが検知された時点をマークするのは begin イベントのみです。

長押しジェスチャーは、地図にマップマーカーを配置する際に便利です。この例は「Search」サンプルアプリで確認できます。このサンプルアプリは、優先するプラットフォームのGitHubにあります。

非連続ジェスチャー (タップ、ダブル タップ、2 本指のタップなど) では、GestureState はジェスチャーの処理には必要ありません。

パン ジェスチャーがスワイプとなり、地図が動く可能性があります。ジェスチャーがすでに終了し、すべての指が離れているにもかかわらず、地図は動き続けます。地図の移動が終了したことを検知するには、MapIdleListener を使用できます。これは MapView インスタンスから取得できる HereMap インスタンスに追加できます。ピンチによる回転ジェスチャーを開始すると、パンのジェスチャーはキャンセルされます。ピンチ回転ジェスチャー中に2本指のパン操作が検出されると、パンジェスチャーが開始され、パンとピンチ回転の両方のイベントが発生します。

コードスニペットとその他の使用法の例は、GitHubで「Gestures」サンプルアプリの一部として入手できます。

マップアクションを制御する

デリゲートを設定しても、ジェスチャーのデフォルトの地図動作には影響しません。独立して制御できます。デフォルトでは、地図をダブル タップしたときのズーム インなどの標準的な動作は、すべて有効になっています。

たとえば、ダブル タップ (ズーム イン) および 2 本指のタップ (ズーム アウト) のデフォルトのマップ ジェスチャー動作を無効にするには、次のようにします。

mapView.gestures.disableDefaultAction(forGesture: .doubleTap)
mapView.gestures.disableDefaultAction(forGesture: .twoFingerTap)

デフォルトのマップ アクションを無効にしても、ジェスチャー イベントをリッスンできます。これは、ジェスチャーのデフォルト アクションをオフにして独自のズーム動作を実装する場合などに役立ちます。タップと長押しを除くすべてのジェスチャーには、デフォルトのマップ アクションが用意されています。詳細については、上の概要を参照してください。

デフォルトのマップ ジェスチャー動作を戻すには、次の呼び出しを行います。

mapView.gestures.enableDefaultAction(forGesture: .doubleTap)
mapView.gestures.enableDefaultAction(forGesture: .twoFingerTap)

マップ アクションをカスタマイズする

すでに見てきたように、デフォルトでは、ダブル タップ ジェスチャーにより地図を非連続的にズーム インできます (市区町村レベルから番地レベルに近づくなど)。このようなデフォルトのマップ ジェスチャー アクションを無効にして、独自の動作を実装したり、既存の動作に必要なアクションを追加したりできます。

必要に応じて、プラットフォームのジェスチャー処理と HERE SDK のジェスチャー検知を組み合わせることもできます。HERE SDK は、利便性を高めるために一般的なマップ ジェスチャーに重点を絞っているため、すべての種類の細かいジェスチャー イベントは用意していません。より詳細な制御が必要な場合は、HERE SDK で利用できるジェスチャー処理とネイティブのジェスチャー検知をいつでも組み合わせることができます。

以下に、カスタム ズーム アニメーションを有効にする方法の例を示します。

カスタムズーム動作を追加する

このチュートリアルでは、マップのカスタム ズーム動作を実装する方法を説明します。具体的には、ユーザーがダブルタップまたは 2 本指のタップ ジェスチャーを実行した後、マップを徐々に拡大または縮小できるようにします。ズーム アニメーションは、しばらくするとスムーズに減速します。

アニメーションから始めましょう。これを行うには、Apple の CADisplayLink を使用して、ディスプレイのリフレッシュ レートとアニメーションを同期させることができます。

便宜的に GestureMapAnimator という名前の新しいクラスを作成します。このクラスはジェスチャーに関連するすべてのアニメーションを処理します。地図はカメラを介してズームする必要があるため、地図の MapCamera インスタンスへの参照が必要です。

GestureMapAnimator 内に、ズーム アニメーションへの参照を保持します。CADisplayLink インスタンスは遅延を初期化できます。対応するループ メソッド animatorLoopZoom (この後に説明) も次のように宣言します。

// A run loop to zoom in/out the map continuously until zoomVelocity is zero.
private lazy var displayLinkZoom = CADisplayLink(target: self,
                                                 selector: #selector(animatorLoopZoom))

これで stopAnimations() メソッドが実装され、対応する CADisplayLink インスタンスの進行中のすべてのアニメーションを次のように一時停止できるようになりました。

// Stop any ongoing zoom animation.
func stopAnimations() {
    displayLinkZoom.isPaused = true
}

デフォルトでは、地図は指が地図に触れた位置で、1 つの非連続的なステップとしてズーム イン/outし、中間ステップはありません。

必要なジェスチャー イベントをつなげましょう。

mapView.gestures.disableDefaultAction(forGesture: .doubleTap)
mapView.gestures.disableDefaultAction(forGesture: .twoFingerTap)

// ...

// Conform to the DoubleTapDelegate protocol.
func onDoubleTap(origin: Point2D) {
    // Start our custom zoom in animation.
    gestureMapAnimator.zoomIn(origin)
}

// Conform to the TwoFingerTapDelegate protocol.
func onTwoFingerTap(origin: Point2D) {
    // Start our custom zoom out animation.
    gestureMapAnimator.zoomOut(origin)
}

内容は明瞭です。2 つのジェスチャー イベントをリッスンするのみです。デフォルトのズーム動作が事前に無効になっていることを確認する必要があります。上の zoomIn() および zoomOut() のメソッドは、以下に示す GestureMapAnimator の新しいメソッドにつながります。

また、2 つの定数を定義することで、ズーム アニメーションの開始値 startZoomVelocity と、現在の startZoomVelocity 値が時間の経過とともに減少する量 zoomDelta を指定します。startZoomAnimation() メソッドにより、実行ループが次のように開始されます。

private let startZoomVelocity: Double = 0.1
private let zoomDelta: Double = 0.005

// Starts the zoom in animation.
func zoomIn(_ origin: Point2D) {
    zoomOrigin = origin
    isZoomIn = true
    startZoomAnimation()
}

// Starts the zoom out animation.
func zoomOut(_ origin: Point2D) {
    zoomOrigin = origin
    isZoomIn = false
    startZoomAnimation()
}

private func startZoomAnimation() {
    stopAnimations()

    zoomVelocity = startZoomVelocity
    displayLinkZoom.isPaused = false
    displayLinkZoom.add(to: .current, forMode: .common)
}

zoomOrigin を保存して、地図のどこでズーム インまたはズーム アウトすべきかを通知します。

フラグ isZoomIn を使用すると、1 つのメソッドでズーム コードを処理できるようになります。アニメーションを開始するには、displayLinkZoom ループの一時停止を解除し、対応する selector のメソッド animatorLoopZoom() でズーム値を更新します。このメソッドは、以前説明した stopAnimations() メソッドを呼び出すことで、displayLinkZoom が再び一時停止されるまで、定期的に呼び出されます。

@objc private func animatorLoopZoom() {
    var zoomFactor: Double = 1
    zoomFactor = isZoomIn ? zoomFactor + zoomVelocity : zoomFactor - zoomVelocity;
    // zoomFactor values > 1 will zoom in and values < 1 will zoom out.
    camera.zoomBy(zoomFactor, around: zoomOrigin)
    zoomVelocity = zoomVelocity - zoomDelta
    if (zoomVelocity <= 0) {
        stopAnimations()
    }
}

上記の単純なアルゴリズムでは、zoomVelocity 値は 0.1 から 0 に近い値で補間されます。zoomVelocity は地図を徐々にズームできるアニメーション値です。開始値では startZoomVelocity は 0.1 として定義されます。zoomVelocity は引数として使用し、ズーム率を設定します。zoomVelocity がゆっくりと 0 に近づくにつれて、結果のズーム ステップは小さくなります。

上では、zoomOriginに保存したタッチ原点を使用して、指が画面に触れたポイントにズームインします。ズーム アウトする場合、これは 2 本指のタップ ジェスチャーの間のポイントになります。次に、カメラの zoomBy() メソッドが非連続的なズーム ステップを指定された位置で実行します。

ブール型フラグ isZoomIn は、地図をズーム インまたはズーム アウトすべきかを示します。これにより、どちらのズームの場合でもこのコードを使用できます。唯一の違いは、現在のズーム率に対してアニメーション値 zoomVelocity が加算または減算される点です。

ズーム率は、地図をどの程度ズーム インまたはズーム アウトするかを指定します。ズーム率が 1 の場合、現在のズーム レベルは変更されません。

それでは、説明はここまでです。上のコード スニペットは、ニーズに合わせて自由に変更してください。たとえば、さまざまな Interpolator、異なるアニメーション値、または異なるアニメーションの継続時間を使用して試してみましょう。

サンプルアプリを試す

上記のコードスニペットのほとんどは、「Gestures」サンプルアプリで利用できます。このサンプルアプリは、GitHubでお好みのプラットフォームのものを見つけることができます。


EN 日本語

HERE documentation

Find answers to your product and technical questions

Documentation

What's new

Videos

EN 日本語

HERE ドキュメント

製品や技術に関する質問の答えを見つけましょう。より多くの内容と最新の情報については、英語版をご覧ください。

ドキュメント

ダイナミックマップ

動的コンテンツ関連のAPIをアプリやサービスに活用して、ドライバーが安全・快適かつ予定どおりに目的地へ到着できるよう支援します。

地図とデータ

世界中を走行する多数のマッピング車両から得られる最新の位置情報データを活用し、精度の高い地図やカスタムレイヤーを構築できます。

最新情報

動画

(function () { const input = document.querySelector('input[data-typeahead]'); if (!input) return; // Prevent the form from submitting/navigating input.closest('form')?.addEventListener('submit', e => e.preventDefault()); input.addEventListener('input', function () { const q = this.value.trim().toLowerCase(); document.querySelectorAll('.nav-group-name').forEach(group => { let anyVisible = false; group.querySelectorAll('.nav-group-task').forEach(task => { const text = task.textContent.trim().toLowerCase(); const show = !q || text.includes(q); task.style.display = show ? '' : 'none'; if (show) anyVisible = true; }); // Hide the whole group header if nothing matches group.style.display = anyVisible || !q ? '' : 'none'; }); }); })(); (function () { function onTokenClick(event) { var link = event.target.closest('.sdk-for-ios .item .token'); if (!link) return; event.preventDefault(); console.log('token clicked', link.textContent.trim()); var item = link.closest('.item'); if (!item) return; var content = item.querySelector('.height-container'); if (!content) { console.log('no .height-container found for item', item); return; } var isHidden = window.getComputedStyle(content).display === 'none'; content.style.display = isHidden ? 'block' : 'none'; link.classList.toggle('token-open', isHidden); var href = link.getAttribute('href'); if (href) { if (history.pushState) history.pushState({}, '', href); else location.hash = href; } } function openHashTarget() { var hash = window.location.hash.slice(1); if (!hash) return; var anchor = document.querySelector('.sdk-for-ios a[name="' + hash + '"]'); if (!anchor) return; var item = anchor.closest('.item'); if (!item) return; var link = item.querySelector('.token'); var content = item.querySelector('.height-container'); if (!link || !content) return; content.style.display = 'block'; link.classList.add('token-open'); } function init() { console.log('HERE SDK accordion init'); openHashTarget(); } document.removeEventListener('click', onTokenClick); document.addEventListener('click', onTokenClick); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('hashchange', openHashTarget); window.addEventListener('pageLoad', init); })();