Google Maps SDK for Android:将相机平滑地动画到新位置,渲染沿途的所有图块

Ken*_*y83 7 java android google-maps-android-api-2 react-native react-native-component

背景

许多类似的问题似乎已经被问过(最值得注意的是 android google 地图在使用 GoogleMap.AnimateCamera() 时不加载地图以及How can I smooth pan a GoogleMap in Android?),但没有任何答案或评论发布这些线索让我对如何做到这一点有了坚定的想法。

我最初以为这会像调用一样简单animateCamera(CameraUpdateFactory.newLatLng(), duration, callback),但就像上面第一个链接的OP一样,我得到的只是一个灰色或非常模糊的地图,直到动画完成,即使我将其放慢到几十秒长!

我设法找到并实现了这个帮助器类,它在允许图块沿途渲染方面做得很好,但即使延迟为 0,每个动画之间也存在明显的滞后。

代码

好的,是时候编写一些代码了。这是(稍作修改的)辅助类:

package com.coopmeisterfresh.googlemaps.NativeModules;

import android.os.Handler;

import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.GoogleMap;

import java.util.ArrayList;
import java.util.List;

public class CameraUpdateAnimator implements GoogleMap.OnCameraIdleListener {
    private final GoogleMap mMap;
    private final GoogleMap.OnCameraIdleListener mOnCameraIdleListener;

    private final List<Animation> cameraUpdates = new ArrayList<>();

    public CameraUpdateAnimator(GoogleMap map, GoogleMap.
        OnCameraIdleListener onCameraIdleListener) {
        mMap = map;
        mOnCameraIdleListener = onCameraIdleListener;
    }

    public void add(CameraUpdate cameraUpdate, boolean animate, long delay) {
        if (cameraUpdate != null) {
            cameraUpdates.add(new Animation(cameraUpdate, animate, delay));
        }
    }

    public void clear() {
        cameraUpdates.clear();
    }

    public void execute() {
        mMap.setOnCameraIdleListener(this);
        executeNext();
    }

    private void executeNext() {
        if (cameraUpdates.isEmpty()) {
            mOnCameraIdleListener.onCameraIdle();
        } else {
            final Animation animation = cameraUpdates.remove(0);

            new Handler().postDelayed(() -> {
                if (animation.mAnimate) {
                    mMap.animateCamera(animation.mCameraUpdate);
                } else {
                    mMap.moveCamera(animation.mCameraUpdate);
                }
            }, animation.mDelay);
        }
    }

    @Override
    public void onCameraIdle() {
        executeNext();
    }

    private static class Animation {
        private final CameraUpdate mCameraUpdate;
        private final boolean mAnimate;
        private final long mDelay;

        public Animation(CameraUpdate cameraUpdate, boolean animate, long delay) {
            mCameraUpdate = cameraUpdate;
            mAnimate = animate;
            mDelay = delay;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我的代码来实现它:

// This is actually a React Native Component class, but I doubt that should matter...?
public class NativeGoogleMap extends SimpleViewManager<MapView> implements
    OnMapReadyCallback, OnRequestPermissionsResultCallback {

    // ...Other unrelated methods removed for brevity

    private void animateCameraToPosition(LatLng targetLatLng, float targetZoom) {
        // googleMap is my GoogleMap instance variable; it
        // gets properly initialised in another class method
        CameraPosition currPosition = googleMap.getCameraPosition();
        LatLng currLatLng = currPosition.target;
        float currZoom = currPosition.zoom;

        double latDelta = targetLatLng.latitude - currLatLng.latitude;
        double lngDelta = targetLatLng.longitude - currLatLng.longitude;

        double latInc = latDelta / 5;
        double lngInc = lngDelta / 5;

        float zoomInc = 0;
        float minZoom = googleMap.getMinZoomLevel();
        float maxZoom = googleMap.getMaxZoomLevel();

        if (lngInc > 15 && currZoom > minZoom) {
            zoomInc = (minZoom - currZoom) / 5;
        }

        CameraUpdateAnimator animator = new CameraUpdateAnimator(googleMap,
            () -> googleMap.animateCamera(CameraUpdateFactory.zoomTo(
            targetZoom), 5000, null));

        for (double nextLat = currLatLng.latitude, nextLng = currLatLng.
            longitude, nextZoom = currZoom; Math.abs(nextLng) < Math.abs(
            targetLatLng.longitude);) {
            nextLat += latInc;
            nextLng += lngInc;
            nextZoom += zoomInc;

            animator.add(CameraUpdateFactory.newLatLngZoom(new
                LatLng(nextLat, nextLng), (float)nextZoom), true);
        }

        animator.execute();
    }
}
Run Code Online (Sandbox Code Playgroud)

问题

有没有更好的方法来完成这个看似简单的任务?我在想也许我需要将动画移动到工作线程或其他东西;那会有帮助吗?

感谢您的阅读(我知道这是一种努力:P)!

更新日期 30/09/2021

我已经根据安迪在评论中的建议更新了上面的代码,虽然它可以工作(尽管具有相同的滞后和渲染问题),但最终的算法需要更复杂一点,因为我想缩小到纵向达美航空的中途点,然后随着旅程的继续而返回。

一次进行所有这些计算,并同时平滑地渲染所有必要的图块,对于我正在测试的廉价手机来说似乎太多了。或者这是 API 本身的限制?无论如何,我怎样才能让所有这些工作顺利进行,而在排队的动画之间没有任何延迟?

小智 1

这是我使用您的实用框架播放器的尝试。

一些注意事项:

  • 缩放值是根据总步数(此处设置为 500)并给定开始值和停止值进行插值的。
  • Google 地图实用程序用于根据分数距离计算下一个 lat lng:SphericalUtil.interpolate
  • 分数距离不应该是线性函数,以减少新块的引入。换句话说,在更高的变焦(更近)时,相机移动的距离更短,并且相机移动量在缩小时呈指数增加(中心到中心)。这需要更多解释......
  • 正如您所看到的,遍历被分为两部分 - 反转距离移动的指数函数。
  • 最远的“最大”缩放(坏名声)可以是总距离的函数 - 计算为包含中点处的整个路径。目前,对于这种情况,它被硬编码为 4。
  • 请注意,无法使用地图animate功能,因为它在每个步骤上引入了自己的弹跳球效果,这是不希望的。因此,只要有相当数量的步骤,就可以使用该函数。move
  • 此方法尝试最小化每个步骤的图块加载,但最终 TileLoader 是无法(轻松)监控的查看的限制因素。

动画相机到位置

// flag to control the animate callback (at completion).
boolean done = false;

private void animateCameraToPosition(LatLng targetLatLng, float targetZoom) {
    CameraPosition currPosition = gMap.getCameraPosition();
    LatLng currLatLng = currPosition.target;

    //meters_per_pixel = 156543.03392 * Math.cos(latLng.lat() * Math.PI / 180) / Math.pow(2, zoom)
    int maxSteps = 500;
    // number of steps between start and midpoint and midpoint and end
    int stepsMid = maxSteps / 2;

    // current zoom
    float initz = currPosition.zoom;
    //TODO maximum zoom (can be computed from overall distance) such that entire path
    //     is visible at midpoint.
    float maxz = 4.0f;
    float finalz = targetZoom;

    CameraUpdateAnimator animator = new CameraUpdateAnimator(gMap, () -> {
        if (!done) {
            gMap.animateCamera(CameraUpdateFactory.
                    zoomTo(targetZoom), 5000, null);
        }
        done = true;

    });

    // loop from start to midpoint

    for (int i = 0; i < stepsMid; i++) {
        // compute interpolated zoom (current --> max) (linear)
        float z = initz - ((initz - maxz) / stepsMid) * i;

        // Compute fractional distance using an exponential function such that for the first
        // half the fraction delta advances slowly and accelerates toward midpoint.
        double ff = (i * (Math.pow(2,maxz) / Math.pow(2,z))) / maxSteps;

        LatLng nextLatLng =
                SphericalUtil.interpolate(currLatLng, targetLatLng, ff);
        animator.add(CameraUpdateFactory.newLatLngZoom(
                nextLatLng, z), false, 0);
    }

    // loop from midpoint to final
    for (int i = 0; i < stepsMid; i++) {
        // compute interpolated zoom (current --> max) (linear)
        float z = maxz + ((finalz - maxz) / stepsMid) * i;
        double ff = (maxSteps - ((i+stepsMid) * ( (Math.pow(2,maxz) / Math.pow(2,z)) ))) / (double)maxSteps;

        LatLng nextLatLng =
                SphericalUtil.interpolate(currLatLng, targetLatLng, ff);

        animator.add(CameraUpdateFactory.newLatLngZoom(
                nextLatLng, z), false, 0);
    }

    animator.add(CameraUpdateFactory.newLatLngZoom(
            targetLatLng, targetZoom), true, 0);

    //

    animator.execute();
}
Run Code Online (Sandbox Code Playgroud)

测试代码

我用从自由女神像到西海岸的一个点的这两点(和代码)进行了测试:

gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(40.68924, -74.04454), 13.0f));

new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            animateCameraToPosition(new LatLng(33.899832, -118.020450), 13.0f);
        }
    }, 5000);
Run Code Online (Sandbox Code Playgroud)

相机更新动画模组

我稍微修改了相机更新动画器:

public void execute() {
    mMap.setOnCameraIdleListener(this);
    executeNext();
}

private void executeNext() {
    if (cameraUpdates.isEmpty()) {
        mMap.setOnCameraIdleListener(mOnCameraIdleListener);
        mOnCameraIdleListener.onCameraIdle();
    } else {
        final Animation animation = cameraUpdates.remove(0);
        // This optimization is likely unnecessary since I think the
        // postDelayed does the same on a delay of 0 - execute immediately.
        if (animation.mDelay > 0) {
            new Handler().postDelayed(() -> {
                if (animation.mAnimate) {
                    mMap.animateCamera(animation.mCameraUpdate);
                } else {
                    mMap.moveCamera(animation.mCameraUpdate);
                }
            }, animation.mDelay);
        } else {
            if (animation.mAnimate) {
                mMap.animateCamera(animation.mCameraUpdate);
            } else {
                mMap.moveCamera(animation.mCameraUpdate);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

样品前

使用

// assume initial (40.68924, -74.04454) z=13.0f
gMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(33.899832,-118.020450), 13.0f), 30000, null);
Run Code Online (Sandbox Code Playgroud)

样品后

这些是从模拟器记录的。我还侧载到我的手机 (Samsumg SM-G960U) 上,结果类似(使用 1000 步 0 延迟)。

所以我认为这并不完全满足你的要求:有一些“模糊的瓷砖”,因为它们是从西方引进的。

自由女神像 - 前往 - 圣地亚哥附近的某个地方

500步0延迟

100步0延迟

50步100MS延迟


诊断

在某些方面,深入了解地图对图块的作用是很有用的。可以通过安装简单的UrlTileProvider请求并记录请求来提供洞察力。此实现会获取谷歌图块,尽管它们的分辨率通常较低。

为此,需要执行以下操作:

    // Turn off this base map and install diagnostic tile provider
    gMap.setMapType(GoogleMap.MAP_TYPE_NONE);
    gMap.addTileOverlay(new TileOverlayOptions().tileProvider(new MyTileProvider(256,256)).fadeIn(true));
Run Code Online (Sandbox Code Playgroud)

并定义诊断文件提供者

public class MyTileProvider extends UrlTileProvider {

    public MyTileProvider(int i, int i1) {
        super(i, i1);
    }

    @Override
    public URL getTileUrl(int x, int y, int zoom) {

        Log.i("tiles","x="+x+" y="+y+" zoom="+zoom);

        try {
            return new URL("http://mt1.google.com/vt/lyrs=m&x="+x+"&y="+y+"&z="+zoom);
        } catch (MalformedURLException e) {
            e.printStackTrace();
            return null;
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

您会立即注意到图块层始终以整数单位 ( int) 定义。缩放中提供的分数缩放(例如,LatLngZoom严格使用内存中的图像 - 很高兴知道。)

为了保证完整性,下面是一个示例:

// initial zoom 
x=2411 y=3080 zoom=13
x=2410 y=3080 zoom=13
x=2411 y=3081 zoom=13
x=2410 y=3081 zoom=13
x=2411 y=3079 zoom=13
x=2410 y=3079 zoom=13
Run Code Online (Sandbox Code Playgroud)

最大时:

x=9 y=12 zoom=5
x=8 y=12 zoom=5
x=9 y=11 zoom=5
x=8 y=11 zoom=5
x=8 y=13 zoom=5
x=9 y=13 zoom=5
x=7 y=12 zoom=5
x=7 y=11 zoom=5
x=7 y=13 zoom=5
x=8 y=10 zoom=5
x=9 y=10 zoom=5
x=7 y=10 zoom=5
Run Code Online (Sandbox Code Playgroud)

下面是每次调用 tiler(x 轴)时的缩放(y 轴)图表。每个缩放层的数量大致相同,这正是我所期望的。完全变焦显示的长度是两倍,因为这是中点重复。但有一些异常情况需要解释(例如在 110 左右)。

这是瓦片提供商记录的“缩放”图表。因此,每个 x 轴点将代表单个图块获取。

在此输入图像描述