فهرست منبع

add:添加加载地图图标图片。

zhoukun 6 ماه پیش
والد
کامیت
37257b21dc

+ 6 - 0
ios/Runner/Info.plist

@@ -84,5 +84,11 @@
 		<string>UIInterfaceOrientationPortrait</string>
 		<string>UIInterfaceOrientationPortraitUpsideDown</string>
 	</array>
+    <key>NSAppTransportSecurity</key>
+    <dict>
+        <!-- 方案一:允许所有HTTP连接(开发测试用) -->
+        <key>NSAllowsArbitraryLoads</key>
+        <true/>
+    </dict>
 </dict>
 </plist>

+ 8 - 2
plugins/map_mapkit_ios/ios/Classes/MapView/Model/Models.swift

@@ -81,14 +81,16 @@ class ATMapPolyline: NSObject, Codable {
 class ATMapMarker: NSObject, Codable {
     var id: String
     var markerName: String?
+    var customAvatarUrl : String?
     var latitude: CGFloat
     var longitude: CGFloat
     var isSelected: Bool
     var markerType: any MapMarkerSupportType
 
-    init(id: String, markerName: String?, location: CLLocationCoordinate2D, markerType: any MapMarkerSupportType, isSelected: Bool = false) {
+    init(id: String, markerName: String?, customAvatarUrl: String?,location: CLLocationCoordinate2D, markerType: any MapMarkerSupportType, isSelected: Bool = false) {
         self.id = id
         self.markerName = markerName
+        self.customAvatarUrl = customAvatarUrl
         self.latitude = location.latitude
         self.longitude = location.longitude
         self.markerType = markerType
@@ -103,7 +105,7 @@ class ATMapMarker: NSObject, Codable {
     }
 
     enum CodingKeys: String, CodingKey {
-        case id, markerName, latitude, longitude, isSelected, markerType
+        case id, markerName,customAvatarUrl, latitude, longitude, isSelected, markerType
     }
 
     required init(from decoder: Decoder) throws {
@@ -115,6 +117,9 @@ class ATMapMarker: NSObject, Codable {
         if container.contains(.markerName) {
             markerName = try container.decodeIfPresent(String.self, forKey: .markerName)
         }
+        if container.contains(.customAvatarUrl) {
+            customAvatarUrl = try container.decodeIfPresent(String.self, forKey: .customAvatarUrl)
+        }
         latitude = try container.decode(CGFloat.self, forKey: .latitude)
         longitude = try container.decode(CGFloat.self, forKey: .longitude)
         isSelected = try container.decode(Bool.self, forKey: .isSelected)
@@ -128,6 +133,7 @@ class ATMapMarker: NSObject, Codable {
         var container = encoder.container(keyedBy: CodingKeys.self)
         try container.encode(id, forKey: .id)
         try container.encode(markerName, forKey: .markerName)
+        try container.encode(customAvatarUrl, forKey: .customAvatarUrl)
         try container.encode(latitude, forKey: .latitude)
         try container.encode(longitude, forKey: .longitude)
         try container.encode(isSelected, forKey: .isSelected)

+ 13 - 0
plugins/map_mapkit_ios/ios/Classes/MapView/Views/MapAnnotationMarkerImageView.swift

@@ -63,4 +63,17 @@ class MapAnnotationMarkerImageView: UIView {
         
         return nil
     }
+    
+    ///加载网络图片
+    public func loadNetworkImage(imageUrl : String) {
+        // 异步加载图片
+        ImageLoader.shared.loadAvatarAsync(
+            from: URL(string: imageUrl)!,
+            style: .circle
+        ) { image, error in
+            if let image = image {
+                self.imageView.image = image
+            }
+        }
+    }
 }

+ 3 - 0
plugins/map_mapkit_ios/ios/Classes/MapView/Views/MapAnnotationView.swift

@@ -60,6 +60,9 @@ class MapAnnotationView: MKAnnotationView {
         
         // 创建并添加图像视图
         markerImageView = MapAnnotationMarkerImageView(markerType: marker.markerType)
+        if let url = marker.customAvatarUrl, !url.isEmpty {
+            markerImageView?.loadNetworkImage(imageUrl: url)
+        }
         if let markerImageView = markerImageView {
             markerImageView.isSelected = annotationSelected
             addSubview(markerImageView)

+ 375 - 0
plugins/map_mapkit_ios/ios/Classes/MapView/Views/MapMarkerImageLoader.swift

@@ -0,0 +1,375 @@
+import Foundation
+import UIKit
+
+// 图片加载进度回调
+typealias ImageLoadingProgress = (Double) -> Void
+// 图片加载完成回调
+typealias ImageCompletion = (UIImage?, Error?) -> Void
+
+/// 头像样式
+enum AvatarStyle {
+    case circle          // 圆形
+    case rounded(radius: CGFloat)  // 圆角矩形
+}
+
+/// 图片加载工具类(含缓存功能,支持头像专用圆角处理)
+class ImageLoader {
+    // 单例模式
+    static let shared = ImageLoader()
+    
+    // 内存缓存(LRU策略)
+    private let memoryCache = NSCache<NSString, UIImage>()
+    // 磁盘缓存路径
+    private let diskCachePath: String
+    // 最大内存缓存大小(默认100MB)
+    private let maxMemoryCacheSize: Int64 = 100 * 1024 * 1024
+    // 最大磁盘缓存大小(默认200MB)
+    private let maxDiskCacheSize: Int64 = 200 * 1024 * 1024
+    
+    // 初始化
+    private init() {
+        // 创建磁盘缓存目录
+        let documentsPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
+        diskCachePath = "\(documentsPath)/ImageCache"
+        
+        // 配置内存缓存
+        memoryCache.countLimit = 100 // 最多缓存100张图片
+        memoryCache.totalCostLimit = Int(maxMemoryCacheSize)
+        
+        // 清理过期的磁盘缓存
+        cleanExpiredDiskCache()
+    }
+    
+    /// 加载头像图片(带缓存)
+    func loadAvatar(
+        from url: URL,
+        placeholder: UIImage? = nil,
+        style: AvatarStyle = .circle,
+        progress: ImageLoadingProgress? = nil,
+        completion: @escaping ImageCompletion
+    ) -> UIImage? {
+        // 生成缓存键(包含样式信息)
+        let cacheKey = generateAvatarCacheKey(url: url, style: style)
+        
+        // 先检查内存缓存
+        if let cachedImage = memoryCache.object(forKey: cacheKey) {
+            return cachedImage
+        }
+        
+        // 检查磁盘缓存
+        if let cachedImage = loadAvatarFromDiskCache(url: url, style: style) {
+            memoryCache.setObject(cachedImage, forKey: cacheKey)
+            return cachedImage
+        }
+        
+        // 显示占位图
+        completion(placeholder, nil)
+        return placeholder
+    }
+    
+    /// 异步加载头像图片(带进度监听)
+    func loadAvatarAsync(
+        from url: URL,
+        placeholder: UIImage? = nil,
+        style: AvatarStyle = .circle,
+        progress: ImageLoadingProgress? = nil,
+        completion: @escaping ImageCompletion
+    ) {
+        // 生成缓存键(包含样式信息)
+        let cacheKey = generateAvatarCacheKey(url: url, style: style)
+        
+        // 先检查内存缓存
+        if let cachedImage = memoryCache.object(forKey: cacheKey) {
+            DispatchQueue.main.async {
+                completion(cachedImage, nil)
+            }
+            return
+        }
+        
+        // 检查磁盘缓存
+        if let cachedImage = loadAvatarFromDiskCache(url: url, style: style) {
+            memoryCache.setObject(cachedImage, forKey: cacheKey)
+            DispatchQueue.main.async {
+                completion(cachedImage, nil)
+            }
+            return
+        }
+        
+        // 显示占位图
+        DispatchQueue.main.async {
+            completion(placeholder, nil)
+        }
+        
+        // 从网络加载图片
+        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
+            guard let self = self,
+                  let data = data,
+                  error == nil,
+                  let response = response as? HTTPURLResponse,
+                  response.statusCode == 200,
+                  let image = UIImage(data: data) else {
+                DispatchQueue.main.async {
+                    completion(nil, error)
+                }
+                return
+            }
+            
+            // 应用头像样式处理
+            let processedImage = self.processAvatar(image: image, style: style)
+            
+            // 保存到缓存
+            self.saveAvatarToCache(image: processedImage, url: url, style: style)
+            
+            DispatchQueue.main.async {
+                completion(processedImage, nil)
+            }
+        }
+        
+        task.resume()
+    }
+    
+    /// 生成头像缓存键
+    private func generateAvatarCacheKey(url: URL, style: AvatarStyle) -> NSString {
+        let styleKey: String
+        switch style {
+        case .circle:
+            styleKey = "circle"
+        case .rounded(let radius):
+            styleKey = "rounded_\(radius)"
+        }
+        return "\(url.absoluteString)_\(styleKey)" as NSString
+    }
+    
+    /// 从磁盘缓存加载头像
+    private func loadAvatarFromDiskCache(url: URL, style: AvatarStyle) -> UIImage? {
+        let fileName = generateAvatarFileName(url: url, style: style)
+        let filePath = "\(diskCachePath)/\(fileName)"
+        
+        if let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) {
+            return UIImage(data: data)
+        }
+        
+        return nil
+    }
+    
+    /// 生成头像文件名
+    private func generateAvatarFileName(url: URL, style: AvatarStyle) -> String {
+        let baseFileName = url.lastPathComponent
+        let styleKey: String
+        
+        switch style {
+        case .circle:
+            styleKey = "avatar_circle"
+        case .rounded(let radius):
+            styleKey = "avatar_rounded_\(radius)"
+        }
+        
+        return "\(styleKey)_\(baseFileName)"
+    }
+    
+    /// 保存头像到缓存
+    private func saveAvatarToCache(image: UIImage, url: URL, style: AvatarStyle) {
+        // 保存到内存缓存
+        let cacheKey = generateAvatarCacheKey(url: url, style: style)
+        memoryCache.setObject(image, forKey: cacheKey)
+        
+        // 保存到磁盘缓存
+        DispatchQueue.global(qos: .background).async { [weak self] in
+            guard let self = self else { return }
+            
+            // 创建缓存目录(如果不存在)
+            if !FileManager.default.fileExists(atPath: self.diskCachePath) {
+                do {
+                    try FileManager.default.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true)
+                } catch {
+                    return
+                }
+            }
+            
+            let fileName = self.generateAvatarFileName(url: url, style: style)
+            let filePath = "\(self.diskCachePath)/\(fileName)"
+            
+            // 压缩图片(JPEG格式,质量0.8)
+            if let data = image.jpegData(compressionQuality: 0.8) {
+                do {
+                    try data.write(to: URL(fileURLWithPath: filePath), options: .atomic)
+                    self.updateDiskCacheSize()
+                } catch {
+                    //print("保存头像到磁盘失败: \(error)")
+                }
+            }
+        }
+    }
+    
+    /// 处理头像样式
+    private func processAvatar(image: UIImage, style: AvatarStyle) -> UIImage {
+        // 优化:如果是圆形且图片已经是正方形,直接使用CoreImage滤镜
+        if case .circle = style, image.size.width == image.size.height {
+            return applyCircularFilter(image: image)
+        }
+        
+        // 否则使用传统绘图方式
+        return applyAvatarStyle(image: image, style: style)
+    }
+    
+    /// 应用圆形滤镜(性能优化)
+    private func applyCircularFilter(image: UIImage) -> UIImage {
+        guard let cgImage = image.cgImage,
+              let filter = CIFilter(name: "CICropToCircle") else {
+            return image
+        }
+        
+        let ciImage = CIImage(cgImage: cgImage)
+        filter.setValue(ciImage, forKey: kCIInputImageKey)
+        
+        if let outputImage = filter.outputImage,
+           let outputCGImage = CIContext().createCGImage(outputImage, from: outputImage.extent) {
+            return UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)
+        }
+        
+        return image
+    }
+    
+    /// 应用头像样式(传统绘图方式)
+    private func applyAvatarStyle(image: UIImage, style: AvatarStyle) -> UIImage {
+        let size = image.size
+        let rect = CGRect(origin: .zero, size: size)
+        let cornerRadius: CGFloat
+        
+        switch style {
+        case .circle:
+            cornerRadius = min(size.width, size.height) / 2
+        case .rounded(let radius):
+            cornerRadius = radius
+        }
+        
+        UIGraphicsBeginImageContextWithOptions(size, false, image.scale)
+        defer { UIGraphicsEndImageContext() }
+        
+        let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
+        path.addClip()
+        
+        image.draw(in: rect)
+        
+        if let outputImage = UIGraphicsGetImageFromCurrentImageContext() {
+            return outputImage
+        }
+        
+        return image
+    }
+    
+    /// 更新磁盘缓存大小
+    private func updateDiskCacheSize() {
+        // 保持原有逻辑不变...
+        do {
+            let fileManager = FileManager.default
+            let cacheFiles = try fileManager.contentsOfDirectory(atPath: diskCachePath)
+            
+            var totalSize: Int64 = 0
+            for file in cacheFiles {
+                let filePath = "\(diskCachePath)/\(file)"
+                if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
+                   let size = attributes[.size] as? Int64 {
+                    totalSize += size
+                }
+            }
+            
+            // 如果超过最大缓存大小,清理旧文件
+            if totalSize > maxDiskCacheSize {
+                cleanOldestDiskCacheFiles(untilSize: totalSize - maxDiskCacheSize)
+            }
+        } catch {
+            //print("更新磁盘缓存大小失败: \(error)")
+        }
+    }
+    
+    /// 清理最旧的磁盘缓存文件
+    private func cleanOldestDiskCacheFiles(untilSize: Int64) {
+        // 保持原有逻辑不变...
+        guard untilSize > 0 else { return }
+        
+        do {
+            let fileManager = FileManager.default
+            var files: [(path: String, date: Date)] = []
+            
+            // 获取所有缓存文件及其修改日期
+            for file in try fileManager.contentsOfDirectory(atPath: diskCachePath) {
+                let filePath = "\(diskCachePath)/\(file)"
+                if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
+                   let date = attributes[.modificationDate] as? Date,
+                   let size = attributes[.size] as? Int64,
+                   size > 0 {
+                    files.append((path: filePath, date: date))
+                }
+            }
+            
+            // 按修改日期排序(最旧的在前)
+            files.sort { $0.date < $1.date }
+            
+            // 删除最旧的文件,直到达到目标大小
+            var removedSize: Int64 = 0
+            for file in files {
+                if removedSize >= untilSize { break }
+                
+                do {
+                    let attributes = try fileManager.attributesOfItem(atPath: file.path)
+                    if let size = attributes[.size] as? Int64 {
+                        try fileManager.removeItem(atPath: file.path)
+                        removedSize += size
+                    }
+                } catch {
+                    //print("删除缓存文件失败: \(error)")
+                }
+            }
+        } catch {
+            //print("清理磁盘缓存失败: \(error)")
+        }
+    }
+    
+    /// 清理过期的磁盘缓存
+    private func cleanExpiredDiskCache() {
+        // 保持原有逻辑不变...
+        // 这里可以添加清理逻辑,例如删除超过30天的缓存
+        let oneMonthAgo = Date().addingTimeInterval(-30 * 24 * 60 * 60)
+        
+        do {
+            let fileManager = FileManager.default
+            for file in try fileManager.contentsOfDirectory(atPath: diskCachePath) {
+                let filePath = "\(diskCachePath)/\(file)"
+                if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
+                   let date = attributes[.modificationDate] as? Date,
+                   date < oneMonthAgo {
+                    try fileManager.removeItem(atPath: filePath)
+                }
+            }
+        } catch {
+            //print("清理过期缓存失败: \(error)")
+        }
+    }
+    
+    /// 清除所有缓存
+    func clearAllCache(completion: (() -> Void)? = nil) {
+        // 保持原有逻辑不变...
+        // 清除内存缓存
+        memoryCache.removeAllObjects()
+        
+        // 清除磁盘缓存
+        DispatchQueue.global(qos: .background).async { [weak self] in
+            guard let self = self else { return }
+            
+            do {
+                if FileManager.default.fileExists(atPath: self.diskCachePath) {
+                    try FileManager.default.removeItem(atPath: self.diskCachePath)
+                    // 重新创建空目录
+                    try FileManager.default.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true)
+                }
+            } catch {
+                //print("清除磁盘缓存失败: \(error)")
+            }
+            
+            DispatchQueue.main.async {
+                completion?()
+            }
+        }
+    }
+}