问题是 - 有没有办法限制 MKMapView 的最大缩放级别?或者有没有办法跟踪用户何时缩放到没有可用地图图像的级别?
12 回答
如果您仅使用 iOS 7+,则camera.altitude
可以获取/设置一个新属性以强制缩放级别。它相当于 azdev 的解决方案,但不需要外部代码。
在测试中,我还发现如果你反复尝试放大细节,可能会进入无限循环,所以我在下面的代码中设置了一个 var 来防止这种情况。
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
// enforce maximum zoom level
if (_mapView.camera.altitude < 120.00 && !_modifyingMap) {
_modifyingMap = YES; // prevents strange infinite loop case
_mapView.camera.altitude = 120.00;
_modifyingMap = NO;
}
}
您可以使用mapView:regionWillChangeAnimated:
委托方法来侦听区域更改事件,如果该区域比您的最大区域宽,请将其设置回最大区域,setRegion:animated:
以向您的用户表明他们不能缩小那么远。以下是方法:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
我刚刚花了一些时间为我正在构建的应用程序工作。这是我想出的:
我从这个页面上的 Troy Brant 的脚本开始,这是我认为设置地图视图的更好方法。
我添加了一个返回当前缩放级别的方法。
在 MKMapView+ZoomLevel.h 中:
- (double)getZoomLevel;
在 MKMapView+ZoomLevel.m 中:
// Return the current map zoomLevel equivalent, just like above but in reverse - (double)getZoomLevel{ MKCoordinateRegion reg=self.region; // the current visible region MKCoordinateSpan span=reg.span; // the deltas CLLocationCoordinate2D centerCoordinate=reg.center; // the center in degrees // Get the left and right most lonitudes CLLocationDegrees leftLongitude=(centerCoordinate.longitude-(span.longitudeDelta/2)); CLLocationDegrees rightLongitude=(centerCoordinate.longitude+(span.longitudeDelta/2)); CGSize mapSizeInPixels = self.bounds.size; // the size of the display window // Get the left and right side of the screen in fully zoomed-in pixels double leftPixel=[self longitudeToPixelSpaceX:leftLongitude]; double rightPixel=[self longitudeToPixelSpaceX:rightLongitude]; // The span of the screen width in fully zoomed-in pixels double pixelDelta=abs(rightPixel-leftPixel); // The ratio of the pixels to what we're actually showing double zoomScale= mapSizeInPixels.width /pixelDelta; // Inverse exponent double zoomExponent=log2(zoomScale); // Adjust our scale double zoomLevel=zoomExponent+20; return zoomLevel; }
此方法依赖于上面链接的代码中的一些私有方法。
我将此添加到我的 MKMapView 代表中(如上面推荐的@vladimir)
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated { NSLog(@"%f",[mapView getZoomLevel]); if([mapView getZoomLevel]<10) { [mapView setCenterCoordinate:[mapView centerCoordinate] zoomLevel:10 animated:TRUE]; } }
如果用户离得太远,这会产生重新缩放的效果。您可以使用 regionWillChangeAnimated 来防止地图“弹回”。
关于上面的循环注释,看起来这个方法只迭代一次。
是的,这是可行的。首先,使用MKMapView+ZoomLevel扩展 MKMapView 。
然后,在您的 MKMapViewDelegate 中实现它:
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
// Constrain zoom level to 8.
if( [mapView zoomLevel] < 8 )
{
[mapView setCenterCoordinate:mapView.centerCoordinate
zoomLevel:8
animated:NO];
}
}
这是使用MKMapView+ZoomLevel和@T.Markle 答案在 Swift 3 中重写的代码:
import Foundation
import MapKit
fileprivate let MERCATOR_OFFSET: Double = 268435456
fileprivate let MERCATOR_RADIUS: Double = 85445659.44705395
extension MKMapView {
func getZoomLevel() -> Double {
let reg = self.region
let span = reg.span
let centerCoordinate = reg.center
// Get the left and right most lonitudes
let leftLongitude = centerCoordinate.longitude - (span.longitudeDelta / 2)
let rightLongitude = centerCoordinate.longitude + (span.longitudeDelta / 2)
let mapSizeInPixels = self.bounds.size
// Get the left and right side of the screen in fully zoomed-in pixels
let leftPixel = self.longitudeToPixelSpaceX(longitude: leftLongitude)
let rightPixel = self.longitudeToPixelSpaceX(longitude: rightLongitude)
let pixelDelta = abs(rightPixel - leftPixel)
let zoomScale = Double(mapSizeInPixels.width) / pixelDelta
let zoomExponent = log2(zoomScale)
let zoomLevel = zoomExponent + 20
return zoomLevel
}
func setCenter(coordinate: CLLocationCoordinate2D, zoomLevel: Int, animated: Bool) {
let zoom = min(zoomLevel, 28)
let span = self.coordinateSpan(centerCoordinate: coordinate, zoomLevel: zoom)
let region = MKCoordinateRegion(center: coordinate, span: span)
self.setRegion(region, animated: true)
}
// MARK: - Private func
private func coordinateSpan(centerCoordinate: CLLocationCoordinate2D, zoomLevel: Int) -> MKCoordinateSpan {
// Convert center coordiate to pixel space
let centerPixelX = self.longitudeToPixelSpaceX(longitude: centerCoordinate.longitude)
let centerPixelY = self.latitudeToPixelSpaceY(latitude: centerCoordinate.latitude)
// Determine the scale value from the zoom level
let zoomExponent = 20 - zoomLevel
let zoomScale = NSDecimalNumber(decimal: pow(2, zoomExponent)).doubleValue
// Scale the map’s size in pixel space
let mapSizeInPixels = self.bounds.size
let scaledMapWidth = Double(mapSizeInPixels.width) * zoomScale
let scaledMapHeight = Double(mapSizeInPixels.height) * zoomScale
// Figure out the position of the top-left pixel
let topLeftPixelX = centerPixelX - (scaledMapWidth / 2)
let topLeftPixelY = centerPixelY - (scaledMapHeight / 2)
// Find delta between left and right longitudes
let minLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX)
let maxLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX + scaledMapWidth)
let longitudeDelta: CLLocationDegrees = maxLng - minLng
// Find delta between top and bottom latitudes
let minLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY)
let maxLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY + scaledMapHeight)
let latitudeDelta: CLLocationDegrees = -1 * (maxLat - minLat)
return MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta)
}
private func longitudeToPixelSpaceX(longitude: Double) -> Double {
return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0)
}
private func latitudeToPixelSpaceY(latitude: Double) -> Double {
if latitude == 90.0 {
return 0
} else if latitude == -90.0 {
return MERCATOR_OFFSET * 2
} else {
return round(MERCATOR_OFFSET - MERCATOR_RADIUS * Double(logf((1 + sinf(Float(latitude * M_PI) / 180.0)) / (1 - sinf(Float(latitude * M_PI) / 180.0))) / 2.0))
}
}
private func pixelSpaceXToLongitude(pixelX: Double) -> Double {
return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI
}
private func pixelSpaceYToLatitude(pixelY: Double) -> Double {
return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI
}
}
在您的视图控制器中使用的示例:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
print("Zoom: \(mapView.getZoomLevel())")
if mapView.getZoomLevel() > 6 {
mapView.setCenter(coordinate: mapView.centerCoordinate, zoomLevel: 6, animated: true)
}
}
如果您的目标是 iOS 13+,请使用 MKMapViewsetCameraZoomRange
方法。只需提供最小和最大中心坐标距离(以米为单位)。
在此处查看 Apple 的文档:https ://developer.apple.com/documentation/mapkit/mkmapview/3114302-setcamerazoomrange
不要使用regionWillChangeAnimated
. 采用regionDidChangeAnimated
我们也可以使用
setRegion(region, animated: true)
.MKMapView
如果我们使用它通常会冻结regionWillChangeAnimated
,但使用regionDidChangeAnimated
它可以完美运行func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { mapView.checkSpan() } extension MKMapView { func zoom() { let region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 2000, 2000) setRegion(region, animated: true) } func checkSpan() { let rect = visibleMapRect let westMapPoint = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMidY(rect)) let eastMapPoint = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMidY(rect)) let distanceInMeter = MKMetersBetweenMapPoints(westMapPoint, eastMapPoint) if distanceInMeter > 2100 { zoom() } } }
使用此示例锁定最大缩放范围,同样您可以限制最小缩放范围
map.cameraZoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 1200000)
它的MKMapView
内部有一个MKScrollView
(私有 API),它是UIScrollView
. this 的代表MKScrollView
是它自己的mapView
。
因此,为了控制最大缩放,请执行以下操作:
创建一个子类MKMapView
:
地图视图.h
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface MapView : MKMapView <UIScrollViewDelegate>
@end
地图视图.m
#import "MapView.h"
@implementation MapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView {
UIScrollView * scroll = [[[[self subviews] objectAtIndex:0] subviews] objectAtIndex:0];
if (scroll.zoomScale > 0.09) {
[scroll setZoomScale:0.09 animated:NO];
}
}
@end
然后,访问滚动子视图并查看zoomScale
属性。当缩放大于数字时,设置最大缩放。
Raphael Petegrosso 发布的带有扩展 MKMapView 的帖子在进行一些小的修改后效果很好。下面的版本也更加“用户友好”,因为一旦用户放开屏幕,它就会优雅地“捕捉”回定义的缩放级别,在感觉上类似于 Apple 自己的弹性滚动。
编辑:这个解决方案不是最优的,会破坏/损坏地图视图,我在这里找到了一个更好的解决方案:How to detect any tap inside an MKMapView。这使您可以拦截捏合和其他动作。
MyMapView.h
#import <MapKit/MapKit.h>
@interface MyMapView : MKMapView <UIScrollViewDelegate>
@end
我的地图视图.m
#import "MyMapView.h"
@implementation MyMapView
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
if (scale > 0.001)
{
[scrollView setZoomScale:0.001 animated:YES];
}
}
@end
对于硬限制,请使用:
#import "MyMapView.h"
@implementation MyMapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
if (scrollView.zoomScale > 0.001)
{
[scrollView setZoomScale:0.001 animated:NO];
}
}
@end
以下代码对我有用,并且在概念上易于使用,因为它根据以米为单位的距离设置区域。该代码来自@nevan-king 发布的答案和@Awais-Fayyaz 发布的使用 regionDidChangeAnimated 的评论
将以下扩展添加到您的 MapViewDelegate
var currentLocation: CLLocationCoordinate2D?
extension MyMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if self.currentLocation != nil, mapView.region.longitudinalMeters > 1000 {
let initialLocation = CLLocation(latitude: (self.currentLocation?.latitude)!,
longitude: (self.currentLocation?.longitude)!)
let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate,
regionRadius, regionRadius)
mapView.setRegion(coordinateRegion, animated: true)
}
}
}
然后为 MKCoordinateRegion 定义一个扩展,如下所示。
extension MKCoordinateRegion {
/// middle of the south edge
var south: CLLocation {
return CLLocation(latitude: center.latitude - span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the north edge
var north: CLLocation {
return CLLocation(latitude: center.latitude + span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the east edge
var east: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta / 2)
}
/// middle of the west edge
var west: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta / 2)
}
/// distance between south and north in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var latitudinalMeters: CLLocationDistance {
return south.distance(from: north)
}
/// distance between east and west in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var longitudinalMeters: CLLocationDistance {
return east.distance(from: west)
}
}
上面的 MKCoordinateRegion 片段由@Gerd-Castan 在这个问题上发布:
我在工作中遇到了这个问题,并且在没有设置全局限制的情况下创建了一些运行良好的东西。
我利用的 MapView 代表是: - mapViewDidFinishRendering - mapViewRegionDidChange
我的解决方案背后的前提是,由于卫星视图呈现一个没有数据的区域,所以它总是一样的。这个可怕的图像 ( http://imgur.com/cm4ou5g ) 如果我们可以放心地依赖该失败案例,我们可以将其用作确定用户所看到内容的关键。地图渲染后,我截取渲染的地图边界并确定平均 RGB 值。根据该 RGB 值,我假设相关区域没有数据。如果是这种情况,我会将地图弹出回正确渲染的最后一个跨度。
我唯一的全局检查是当它开始检查地图时,您可以根据需要增加或减少该设置。下面是完成此操作的原始代码,并将组合一个示例项目以供贡献。您可以提供的任何优化将不胜感激,并希望它有所帮助。
@property (assign, nonatomic) BOOL isMaxed;
@property (assign, nonatomic) MKCoordinateSpan lastDelta;
self.lastDelta = MKCoordinateSpanMake(0.006, 0.006);
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (mapView.mapType != MKMapTypeStandard && self.isMaxed) {
[self checkRegionWithDelta:self.lastDelta.longitudeDelta];
}
}
- (void)checkRegionWithDelta:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
MKCoordinateRegion region = self.mapView.region;
region.span = self.lastDelta;
[self.mapView setRegion:region animated:NO];
} else if (self.mapView.region.span.longitudeDelta > delta) {
self.isMaxed = NO;
}
}
- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered {
if (mapView.mapType != MKMapTypeStandard && !self.isMaxed) {
[self checkToProcess:self.lastDelta.longitudeDelta];
}
}
- (void)checkToProcess:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
UIGraphicsBeginImageContext(self.mapView.bounds.size);
[self.mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *mapImage = UIGraphicsGetImageFromCurrentImageContext();
[self processImage:mapImage];
}
}
- (void)processImage:(UIImage *)image {
self.mapColor = [self averageColor:image];
const CGFloat* colors = CGColorGetComponents( self.mapColor.CGColor );
[self handleColorCorrection:colors[0]];
}
- (void)handleColorCorrection:(float)redColor {
if (redColor < 0.29) {
self.isMaxed = YES;
[self.mapView setRegion:MKCoordinateRegionMake(self.mapView.centerCoordinate, self.lastDelta) animated:YES];
} else {
self.lastDelta = self.mapView.region.span;
}
}
- (UIColor *)averageColor:(UIImage *)image {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char rgba[4];
CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
if(rgba[3] > 0) {
CGFloat alpha = ((CGFloat)rgba[3])/255.0;
CGFloat multiplier = alpha/255.0;
return [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier
green:((CGFloat)rgba[1])*multiplier
blue:((CGFloat)rgba[2])*multiplier
alpha:alpha];
}
else {
return [UIColor colorWithRed:((CGFloat)rgba[0])/255.0
green:((CGFloat)rgba[1])/255.0
blue:((CGFloat)rgba[2])/255.0
alpha:((CGFloat)rgba[3])/255.0];
}
}