Android Google Maps SDK相机平滑移动路径瓦片完整渲染问题咨询
背景
此前Stack Overflow上已有不少类似问题,最具代表性的是GoogleMap.AnimateCamera()调用时地图不加载、Android端如何平滑移动GoogleMap相机两类问题,但所有相关帖子的回答和评论都未给出明确的实现方案。
我最初以为直接调用animateCamera(CameraUpdateFactory.newLatLng(), duration, callback)即可实现需求,但和同类问题的提问者一样,动画结束前地图全灰或非常模糊,哪怕把动画时长拉长到几十秒也没有改善!
我找到并实现了对应的辅助类,可以实现动画过程中瓦片逐段渲染,但哪怕设置延迟为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; } } }
调用逻辑代码如下:
// 实际为React Native组件类,不影响原生逻辑实现 public class NativeGoogleMap extends SimpleViewManager<MapView> implements OnMapReadyCallback, OnRequestPermissionsResultCallback { // 省略其他无关方法 private void animateCameraToPosition(LatLng targetLatLng, float targetZoom) { // googleMap为已初始化完成的GoogleMap实例 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(); } }
核心问题
是否有更优方案实现该需求?将动画逻辑迁移到工作线程执行是否可解决卡顿问题?
2021年9月30日更新
我已按照评论区建议修改了代码,功能可正常运行,但仍存在相同的卡顿和渲染问题。最终算法需要实现更复杂的逻辑:移动到经度差中点时先缩小缩放级别,后续移动过程中再逐步放大到目标级别。
我在低端测试机上运行时,同步完成所有计算、流畅渲染所有必要瓦片的负载过高,无法确定该问题是API本身的限制还是设备性能不足导致。请问如何实现队列动画完全无卡顿的流畅运行效果?
解决方案
首先可以明确的是,动画逻辑不能迁移到工作线程,Google Maps SDK的所有相机操作API都要求必须在主线程调用,迁移到子线程会直接抛出调用线程异常,无法解决问题。
现有方案卡顿的核心原因是:分段调用animateCamera依赖onCameraIdle回调触发下一段动画,本质是多段离散动画拼接,段与段之间天然存在停顿,同时每次相机移动都会触发瓦片重新加载请求,叠加后就会出现明显卡顿。
可按以下优先级优化:
- 改用ValueAnimator实现连续插值动画
直接通过ValueAnimator控制相机每一帧的位置,实现真正的连续动画,同时可以自定义缩放逻辑,满足到中点缩小、后半段放大的需求。示例代码如下:private void animateCameraSmooth(LatLng targetLatLng, float targetZoom, long duration) { CameraPosition currPos = googleMap.getCameraPosition(); LatLng startLatLng = currPos.target; float startZoom = currPos.zoom; float minZoom = googleMap.getMinZoomLevel(); ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(duration); animator.setInterpolator(new AccelerateDecelerateInterpolator()); animator.addUpdateListener(animation -> { float progress = (float) animation.getAnimatedValue(); // 计算当前经纬度 double currLat = startLatLng.latitude + (targetLatLng.latitude - startLatLng.latitude) * progress; double currLng = startLatLng.longitude + (targetLatLng.longitude - startLatLng.longitude) * progress; // 计算缩放:前半段缩小到最小,后半段放大到目标 float currZoom; if (progress < 0.5f) { currZoom = startZoom + (minZoom - startZoom) * (progress / 0.5f); } else { currZoom = minZoom + (targetZoom - minZoom) * ((progress - 0.5f) / 0.5f); } // 直接移动相机到当前位置,无动画停顿 googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(currLat, currLng), currZoom)); }); animator.start(); } - 预加载瓦片减少空白
动画开启前可以先无动画移动到路径上的3-5个关键位置预加载瓦片,再退回起始点开动画;同时可以调用snapshot接口缓存当前帧,动画过程中优先显示缓存帧,避免灰屏。 - 低端机适配优化
检测到设备性能不足时,可降低动画帧率到30fps,临时关闭地图的3D建筑、室内地图、交通图层等额外渲染项,动画结束后再恢复,降低渲染负载。 - 现有分段方案的临时优化
如果要保留现有分段逻辑,可将分段数从5段提升到20-30段,每段移动距离更小,同时去掉onCameraIdle等待逻辑,直接按16ms间隔触发下一段动画,可大幅降低卡顿感。
内容的提问来源于stack exchange,提问作者Kenny83

