如何实现Google Map平滑360度方位角旋转(多端适配)
I've tackled the problem of achieving a seamless 360° camera rotation around a central point in Google Maps, fixing the stutter caused by default easing animations. Below are platform-specific solutions for Swift, Android, Web (JS/React), and even PHP for server-side setup.
Swift: Linear Animation Fix (Already Implemented)
The initial iOS implementation had stuttering because of the default ease-in/ease-out timing function. Switching to a linear timing function via CAMediaTimingFunctionLinear eliminated the start/end jank. Here's the polished code with clear comments:
// Perform 3x 120° rotations to complete a full 360° loop func start360MapRotation() { let rotationDuration: CFTimeInterval = 3.0 let linearTiming = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) // First 120° rotation CATransaction.begin() CATransaction.setValue(rotationDuration, forKey: kCATransactionAnimationDuration) CATransaction.setAnimationTimingFunction(linearTiming) CATransaction.setCompletionBlock { [weak self] in guard let self = self else { return } // Second 120° rotation CATransaction.begin() CATransaction.setValue(rotationDuration, forKey: kCATransactionAnimationDuration) CATransaction.setAnimationTimingFunction(linearTiming) CATransaction.setCompletionBlock { [weak self] in guard let self = self else { return } // Third 120° rotation CATransaction.begin() CATransaction.setValue(rotationDuration, forKey: kCATransactionAnimationDuration) CATransaction.setAnimationTimingFunction(linearTiming) CATransaction.setCompletionBlock { [weak self] in guard let self = self else { return } // Post-rotation cleanup & focus on nearest branch UIView.animate(withDuration: 0.5) { self.findingYourLocation.alpha = 0.0 } // Zoom in and center on target location with easing let nearestBranch = CLLocationCoordinate2D(latitude: 24.806849, longitude: 67.038734) let targetCam = GMSCameraUpdate.setTarget(nearestBranch) CATransaction.begin() CATransaction.setValue(3.0, forKey: kCATransactionAnimationDuration) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)) CATransaction.setCompletionBlock { [weak self] in self?.nextButton.alpha = 1.0 } self.mapView.animate(with: targetCam) self.mapView.animate(toZoom: 15) self.mapView.animate(toBearing: 0) self.mapView.animate(toViewingAngle: 0) CATransaction.commit() } self.mapView.animate(toBearing: self.mapView.camera.bearing + 120) CATransaction.commit() } self.mapView.animate(toBearing: self.mapView.camera.bearing + 120) CATransaction.commit() } self.mapView.animate(toBearing: self.mapView.camera.bearing + 120) CATransaction.commit() }
Key fix: Using kCAMediaTimingFunctionLinear ensures consistent rotation speed throughout the animation, no slow start/end stutter.
Android: Fixing Stuttering Rotation
The default Google Maps Android animation uses an ease-in/ease-out interpolator, which causes stutter at the start and end of rotations. To fix this, use a ValueAnimator with a linear interpolator to manually drive the bearing value:
fun start360MapRotation(map: GoogleMap) { val initialBearing = map.cameraPosition.bearing val targetBearing = initialBearing + 360f // Full 360° rotation val rotationDuration = 9000L // 3 seconds per 120° x 3 loops = 9s total val animator = ValueAnimator.ofFloat(initialBearing, targetBearing).apply { duration = rotationDuration interpolator = LinearInterpolator() // Critical for linear motion addUpdateListener { animation -> val currentBearing = animation.animatedValue as Float val updatedCamera = CameraPosition.builder(map.cameraPosition) .bearing(currentBearing) .build() map.moveCamera(CameraUpdateFactory.newCameraPosition(updatedCamera)) } // Optional: Add loop logic addListener(object : Animator.AnimatorListener { override fun onAnimationEnd(animation: Animator) { // Restart animation if needed // start360MapRotation(map) } override fun onAnimationStart(animation: Animator) {} override fun onAnimationCancel(animation: Animator) {} override fun onAnimationRepeat(animation: Animator) {} }) } animator.start() }
Why this works: By controlling the bearing value directly with a linear interpolator, we avoid the default easing that causes stutter. The animation maintains a constant speed from start to finish.
Web: Smooth Rotation for Vanilla JS & React
Vanilla JavaScript
Google Maps JS API's default animations use easing, so we'll use requestAnimationFrame to create a linear animation by calculating progress over time:
function start360MapRotation(map) { let currentHeading = map.getHeading(); const targetHeading = currentHeading + 360; const duration = 3000; // 3 seconds for full rotation const startTime = performance.now(); const animate = (timestamp) => { const elapsed = timestamp - startTime; if (elapsed < duration) { const progress = elapsed / duration; const newHeading = currentHeading + (360 * progress); map.setHeading(newHeading); requestAnimationFrame(animate); } else { map.setHeading(targetHeading); // Ensure final value is exact // Optional: Restart loop // start360MapRotation(map); } }; requestAnimationFrame(animate); }
React (with @react-google-maps/api)
Use React hooks to manage the animation lifecycle and avoid memory leaks:
import { useGoogleMap } from '@react-google-maps/api'; import { useEffect, useRef } from 'react'; const Map360Rotation = () => { const map = useGoogleMap(); const animationIdRef = useRef(null); const startTimeRef = useRef(null); useEffect(() => { if (!map) return; const startRotation = () => { const currentHeading = map.getHeading(); const targetHeading = currentHeading + 360; const duration = 3000; startTimeRef.current = performance.now(); const animate = (timestamp) => { const elapsed = timestamp - startTimeRef.current; if (elapsed < duration) { const progress = elapsed / duration; const newHeading = currentHeading + (360 * progress); map.setHeading(newHeading); animationIdRef.current = requestAnimationFrame(animate); } else { map.setHeading(targetHeading); // Optional: Restart rotation // startRotation(); } }; animationIdRef.current = requestAnimationFrame(animate); }; startRotation(); // Cleanup on unmount return () => { if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); } }; }, [map]); return null; }; export default Map360Rotation;
Both solutions use time-based progress calculation to ensure a smooth, linear rotation without easing-induced stutter.
PHP: Server-Side Bearing Calculation
For server-side setup (e.g., precomputing camera positions for frontend use), calculate the final bearing after a full rotation:
<?php // Initial bearing (0° = north) $initialBearing = 0; // Total rotation (360° for full loop) $rotationAngle = 360; // Final bearing (mod 360 to keep value within 0-360 range) $finalBearing = fmod($initialBearing + $rotationAngle, 360); // Camera parameters to pass to frontend $cameraConfig = [ 'latitude' => 24.806849, 'longitude' => 67.038734, 'bearing' => $finalBearing, 'zoom' => 15 ]; header('Content-Type: application/json'); echo json_encode($cameraConfig); ?>
This provides the frontend with the exact final bearing, which can be used to anchor the end of your rotation animation.
内容的提问来源于stack exchange,提问作者Nah

