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

地図を操作する

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

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

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

  • タップ:TapListener
  • ダブル タップ:DoubleTapListener
  • 長押し:LongPressListener
  • パン:PanListener
  • 2 本指のパン:TwoFingerPanListener
  • 2 本指のタップ:TwoFingerTapListener
  • ピンチによる回転:PinchRotateListener

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

なお、同じジェスチャーに同時に設定できるリスナーは 1 つのみです。

ジェスチャー リスナーを実装する

ジェスチャー リスナーをマップ ビューに実装する方法の例を見てみましょう。マップ ビューでは、固有の setter が各ジェスチャーに用意されています。リスナーは、設定した直後からそのジェスチャーに関連するすべてのイベントを専用コールバック (たとえば、TapListener の場合は onTap()) 経由で受信します。

private void setTapGestureHandler(MapView mapView) {
    mapView.getGestures().setTapListener(new TapListener() {
        @Override
        public void onTap(@NonNull Point2D touchPoint) {
            GeoCoordinates geoCoordinates = mapView.viewToGeoCoordinates(touchPoint);
            Log.d(TAG, "Tap at: " + geoCoordinates);
        }
    });
}
private fun setTapGestureHandler(mapView: MapView) {
    mapView.gestures.tapListener = TapListener { touchPoint ->
        val geoCoordinates = mapView.viewToGeoCoordinates(touchPoint)
        Log.d(TAG, "Tap at: $geoCoordinates")
    }
}

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

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

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

mapView.getGestures().setTapListener(null);
mapView.getGestures().setTapListener(null)

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

private void setLongPressGestureHandler(MapView mapView) {
    mapView.getGestures().setLongPressListener(new LongPressListener() {
        @Override
        public void onLongPress(@NonNull GestureState gestureState, @NonNull Point2D touchPoint) {
            GeoCoordinates geoCoordinates = mapView.viewToGeoCoordinates(touchPoint);

            if (gestureState == GestureState.BEGIN) {
                Log.d(TAG, "LongPress detected at: " + geoCoordinates);
            }

            if (gestureState == GestureState.UPDATE) {
                Log.d(TAG, "LongPress update at: " + geoCoordinates);
            }

            if (gestureState == GestureState.END) {
                Log.d(TAG, "LongPress finger lifted at: " + geoCoordinates);
            }

            if (gestureState == GestureState.CANCEL) {
                Log.d(TAG, "Map view lost focus. Maybe a modal dialog is shown or the app is sent to background.");
            }
        }
    });
}
private fun setLongPressGestureHandler(mapView: MapView) {
    mapView.gestures.longPressListener = LongPressListener { gestureState, touchPoint ->
        val geoCoordinates = mapView.viewToGeoCoordinates(touchPoint)
        if (gestureState == GestureState.BEGIN) {
            Log.d(TAG, "LongPress detected at: $geoCoordinates")
        }

        if (gestureState == GestureState.UPDATE) {
            Log.d(TAG, "LongPress update at: $geoCoordinates")
        }

        if (gestureState == GestureState.END) {
            Log.d(TAG, "LongPress finger lifted at: $geoCoordinates")
        }
        if (gestureState == GestureState.CANCEL) {
            Log.d(TAG, "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.getGestures().disableDefaultAction(GestureType.DOUBLE_TAP);
mapView.getGestures().disableDefaultAction(GestureType.TWO_FINGER_TAP);
mapView.gestures.disableDefaultAction(GestureType.DOUBLE_TAP)
mapView.gestures.disableDefaultAction(GestureType.TWO_FINGER_TAP)

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

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

mapView.getGestures().enableDefaultAction(GestureType.DOUBLE_TAP);
mapView.getGestures().enableDefaultAction(GestureType.TWO_FINGER_TAP);
mapView.gestures.enableDefaultAction(GestureType.DOUBLE_TAP)
mapView.gestures.enableDefaultAction(GestureType.TWO_FINGER_TAP)

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

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

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

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

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

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

アニメーションから始めましょう。このために、Android のアニメーション フレームワークおよび ValueAnimator を使用して、特定の開始値と終了値を補間できます。

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

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

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

mapView.getGestures().disableDefaultAction(GestureType.DOUBLE_TAP);
mapView.getGestures().disableDefaultAction(GestureType.TWO_FINGER_TAP);

mapView.getGestures().setDoubleTapListener(new DoubleTapListener() {
    @Override
    public void onDoubleTap(@NonNull Point2D touchPoint) {
        // Start our custom zoom in animation.
        gestureMapAnimator.zoomIn(touchPoint);
    }
});

mapView.getGestures().setTwoFingerTapListener(new TwoFingerTapListener() {
    @Override
    public void onTwoFingerTap(@NonNull Point2D touchCenterPoint) {
        // Start our custom zoom out animation.
        gestureMapAnimator.zoomOut(touchCenterPoint);
    }
});
// Disable the default map gesture behavior for DoubleTap (zooms in) and TwoFingerTap (zooms out)
// as we want to enable custom map animations when such gestures are detected.
mapView.gestures.disableDefaultAction(GestureType.DOUBLE_TAP)
mapView.gestures.disableDefaultAction(GestureType.TWO_FINGER_TAP)

private fun setDoubleTapGestureHandler(mapView: MapView) {
    mapView.gestures.doubleTapListener = DoubleTapListener { touchPoint ->
        // Start our custom zoom in animation.
        gestureMapAnimator?.zoomIn(touchPoint)
    }
}

private fun setTwoFingerTapGestureHandler(mapView: MapView) {
    mapView.gestures.twoFingerTapListener = TwoFingerTapListener { touchCenterPoint ->
        // Start our custom zoom out animation.
        gestureMapAnimator?.zoomOut(touchCenterPoint)
    }
}

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

// Starts the zoom in animation.
public void zoomIn(Point2D touchPoint) {
    zoomOrigin = touchPoint;
    startZoomAnimation(true);
}

// Starts the zoom out animation.
public void zoomOut(Point2D touchPoint) {
    zoomOrigin = touchPoint;
    startZoomAnimation(false);
}
// Starts the zoom in animation.
fun zoomIn(touchPoint: Point2D) {
    zoomOrigin = touchPoint
    startZoomAnimation(true)
}

// Starts the zoom out animation.
fun zoomOut(touchPoint: Point2D) {
    zoomOrigin = touchPoint
    startZoomAnimation(false)
}

地図のどこをズーム インまたはズーム アウトする必要があるかを知るために、zoomOrigin を保存しています。

新しい GestureMapAnimator クラス内の startZoomAnimation() の実装では、ValueAnimator を使用します。

private void startZoomAnimation(boolean zoomIn) {
    stopAnimations();

    // A new Animator that zooms the map.
    zoomValueAnimator = createZoomValueAnimator(zoomIn);

    // Start the animation.
    zoomValueAnimator.start();
}

private ValueAnimator createZoomValueAnimator(boolean zoomIn) {
    ValueAnimator zoomValueAnimator = ValueAnimator.ofFloat(0.1F, 0);
    zoomValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    zoomValueAnimator.addUpdateListener(animation -> {
        // Called periodically until zoomVelocity is zero.
        float zoomVelocity = (float) animation.getAnimatedValue();
        double zoomFactor = 1;
        zoomFactor = zoomIn ? zoomFactor + zoomVelocity : zoomFactor - zoomVelocity;
        // zoomFactor values > 1 will zoom in and values < 1 will zoom out.
        camera.zoomBy(zoomFactor, zoomOrigin);
    });

    long halfSecond = 500;
    zoomValueAnimator.setDuration(halfSecond);

    return zoomValueAnimator;
}
private fun startZoomAnimation(zoomIn: Boolean) {
    stopAnimations()

    // A new Animator that zooms the map.
    zoomValueAnimator = createZoomValueAnimator(zoomIn)

    // Start the animation.
    zoomValueAnimator!!.start()
}

private fun createZoomValueAnimator(zoomIn: Boolean): ValueAnimator {
    val zoomValueAnimator = ValueAnimator.ofFloat(0.1f, 0f)
    zoomValueAnimator.interpolator = AccelerateDecelerateInterpolator()
    zoomValueAnimator.addUpdateListener { animation: ValueAnimator ->
        // Called periodically until zoomVelocity is zero.
        val zoomVelocity = animation.animatedValue as Float
        var zoomFactor = 1.0
        zoomFactor = if (zoomIn) zoomFactor + zoomVelocity else zoomFactor - zoomVelocity
        // zoomFactor values > 1 will zoom in and values < 1 will zoom out.
        camera!!.zoomBy(zoomFactor, zoomOrigin!!)
    }

    val halfSecond: Long = 500
    zoomValueAnimator.setDuration(halfSecond)

    return zoomValueAnimator
}

ValueAnimator を使用して、0.1 から 0 までの値を補間します。これらの値によってズーム速度が決まります。AccelerateDecelerateInterpolator は中間値を提供し、最後に減速効果を与えます。ValueAnimator に設定したリスナーは、zoomVelocity が 0 に達するまで定期的に実行されます。zoomVelocity は、地図をズームするのに使用できるアニメーション値です。これを引数として使用してズーム率を設定します。zoomVelocity がゆっくりと 0 に近づくにつれて、結果のズーム ステップは小さくなります。

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

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

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

上の stopAnimations() メソッドは、対応する ValueAnimator インスタンスの進行中のアニメーションを単純にキャンセルします。

public void stopAnimations() {
    if (zoomValueAnimator != null) {
        zoomValueAnimator.cancel();
    }
}
fun stopAnimations() {
    if (zoomValueAnimator != null) {
        zoomValueAnimator!!.cancel()
    }
}

それでは、説明はここまでです。上のコード スニペットは、ニーズに合わせて自由に変更してください。たとえば、さまざまな 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); })();