java - Google Maps SDK for Android : Smoothly animating the camera to a new location, 沿途渲染所有图 block

标签 java android react-native google-maps-android-api-2 react-native-component

背景
之前似乎已经在 SO 上提出了许多类似的问题(最值得注意的是 android google maps not loading the map when using GoogleMap.AnimateCamera()How can I smoothly pan a GoogleMap in Android? ),但是在这些线程中发布的答案或评论都没有让我对如何做到这一点有一个坚定的想法。
我最初认为它会像调用 animateCamera(CameraUpdateFactory.newLatLng(), duration, callback) 一样简单。但就像上面第一个链接的 OP,我得到的只是一张灰色或非常模糊的 map ,直到动画完成,即使我把它放慢到几十秒!
我设法找到并实现了this helper class这很好地允许瓷砖沿途渲染,但即使延迟为 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;
        }
    }
}
以及我实现它的代码:
// 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();
    }
}
问题
有没有更好的方法来完成这个看似简单的任务?我在想也许我需要将我的动画移动到工作线程或其他东西;那会有帮助吗?
感谢阅读(我知道这是一个努力:P)!
2021 年 9 月 30 日更新
我已经根据 Andy 在评论中的建议更新了上面的代码,虽然它可以工作(尽管有相同的延迟和渲染问题),但最终的算法需要更复杂一些,因为我想缩小到纵向三角洲的中途点,然后随着旅程的继续返回。
一次完成所有这些计算,以及平滑渲染所有必要的图 block 同时 ,对于我正在测试的廉价手机来说似乎太过分了。或者这是 API 本身的限制?无论如何,我怎样才能让所有这些工作顺利,排队的动画之间没有任何延迟?

最佳答案

这是我使用您的实用框架播放器的尝试。
几点注意事项:

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

  • 动画CameraToPosition
    // 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();
    }
    
    测试代码
    我用这两个点(和代码)从自由女神像到西海岸的一个点进行了测试:
    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);
    
    CameraUpdateAnimator 模组
    我稍微修改了相机更新动画师:
    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);
                }
            }
        }
    }
    
    sample 前
    使用
    // assume initial (40.68924, -74.04454) z=13.0f
    gMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(33.899832,-118.020450), 13.0f), 30000, null);
    

    sample 后
    这些是从模拟器记录的。我还以类似的结果(使用 1000 步 0 延迟)侧载到我的手机(Samsumg SM-G960U)上。
    所以我不认为这完全符合你的要求:有一些“模棱两可的瓷砖”,因为它们是从西方引进的。
    自由女神像 - 到 - 圣地亚哥附近的某个地方
    500 步 0 延迟
    100步0延迟
    50 步 100MS 延迟

    诊断
    在某些方面深入了解 map 对图 block 所做的工作很有用。可以通过安装一个简单的 UrlTileProvider 来提供洞察力。并记录请求。此实现获取谷歌图 block ,尽管它们的分辨率较低,通常是可见的。
    为此,需要执行以下操作:
        // 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));
    
    并定义诊断文件提供者
    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;
            }
    
        }
    }
    
    您会立即注意到切片图层始终以整数单位定义(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
    
    并且最大:
    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
    
    这是每次调用 tiler(x 轴)时的缩放(y 轴)图表。每个缩放层的数量大致相同,这是 imo 所需要的。完全放大显示两倍长,因为这是中点重复。但是有一些异常需要解释(例如在 110 左右)。
    这是瓷砖提供商记录的“缩放”图表。所以每个 x 轴点将代表一个单独的瓦片获取。
    enter image description here

    关于java - Google Maps SDK for Android : Smoothly animating the camera to a new location, 沿途渲染所有图 block ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69318061/

    相关文章:

    java - 如何在 REST 服务中使用 log4j

    java - 使物体在碰撞后消失! java

    java - 避免等待 Servlet 流

    java - 如何将小时和分钟添加到java vector 中包含的总分钟数中

    java - 如何将 .dex 文件转换为 android studio 项目

    java - 无法解析配置 ':pushwoosh-react-native-plugin:classpath' 的所有 Artifact

    javascript - 如何在 React Native 中动态包含模块

    android - 在三星设备上使用蓝牙低功耗时什么也没发现

    java - 如何在 Android 视频 View 中添加 cookies(Header)以支持旧版本的 android atleast till jellybean

    ios - 启动框架/Info.plist : No such file or directory 时所有框架错误