✨ Native image, video rendering
This commit is contained in:
		
							
								
								
									
										153
									
								
								ios/Runner/NativeViews/BlurHashDecoder.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								ios/Runner/NativeViews/BlurHashDecoder.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| // | ||||
| //  BlurHashDecoder.swift | ||||
| //  Runner | ||||
| // | ||||
| //  Created by LittleSheep on 2025/4/21. | ||||
| // | ||||
|  | ||||
| import UIKit | ||||
|  | ||||
| extension UIImage { | ||||
|     public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { | ||||
|         guard blurHash.count >= 6 else { return nil } | ||||
|          | ||||
|         let sizeFlag = String(blurHash[0]).decode83() | ||||
|         let numY = (sizeFlag / 9) + 1 | ||||
|         let numX = (sizeFlag % 9) + 1 | ||||
|          | ||||
|         let quantisedMaximumValue = String(blurHash[1]).decode83() | ||||
|         let maximumValue = Float(quantisedMaximumValue + 1) / 166 | ||||
|          | ||||
|         guard blurHash.count == 4 + 2 * numX * numY else { return nil } | ||||
|          | ||||
|         let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in | ||||
|             if i == 0 { | ||||
|                 let value = String(blurHash[2 ..< 6]).decode83() | ||||
|                 return decodeDC(value) | ||||
|             } else { | ||||
|                 let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() | ||||
|                 return decodeAC(value, maximumValue: maximumValue * punch) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         let width = Int(size.width) | ||||
|         let height = Int(size.height) | ||||
|         let bytesPerRow = width * 3 | ||||
|         guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } | ||||
|         CFDataSetLength(data, bytesPerRow * height) | ||||
|         guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } | ||||
|          | ||||
|         for y in 0 ..< height { | ||||
|             for x in 0 ..< width { | ||||
|                 var r: Float = 0 | ||||
|                 var g: Float = 0 | ||||
|                 var b: Float = 0 | ||||
|                  | ||||
|                 for j in 0 ..< numY { | ||||
|                     for i in 0 ..< numX { | ||||
|                         let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) | ||||
|                         let colour = colours[i + j * numX] | ||||
|                         r += colour.0 * basis | ||||
|                         g += colour.1 * basis | ||||
|                         b += colour.2 * basis | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 let intR = UInt8(linearTosRGB(r)) | ||||
|                 let intG = UInt8(linearTosRGB(g)) | ||||
|                 let intB = UInt8(linearTosRGB(b)) | ||||
|                  | ||||
|                 pixels[3 * x + 0 + y * bytesPerRow] = intR | ||||
|                 pixels[3 * x + 1 + y * bytesPerRow] = intG | ||||
|                 pixels[3 * x + 2 + y * bytesPerRow] = intB | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) | ||||
|          | ||||
|         guard let provider = CGDataProvider(data: data) else { return nil } | ||||
|         guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, | ||||
|                                     space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } | ||||
|          | ||||
|         self.init(cgImage: cgImage) | ||||
|     } | ||||
| } | ||||
|  | ||||
| private func decodeDC(_ value: Int) -> (Float, Float, Float) { | ||||
|     let intR = value >> 16 | ||||
|     let intG = (value >> 8) & 255 | ||||
|     let intB = value & 255 | ||||
|     return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) | ||||
| } | ||||
|  | ||||
| private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { | ||||
|     let quantR = value / (19 * 19) | ||||
|     let quantG = (value / 19) % 19 | ||||
|     let quantB = value % 19 | ||||
|      | ||||
|     let rgb = ( | ||||
|         signPow((Float(quantR) - 9) / 9, 2) * maximumValue, | ||||
|         signPow((Float(quantG) - 9) / 9, 2) * maximumValue, | ||||
|         signPow((Float(quantB) - 9) / 9, 2) * maximumValue | ||||
|     ) | ||||
|      | ||||
|     return rgb | ||||
| } | ||||
|  | ||||
| private func signPow(_ value: Float, _ exp: Float) -> Float { | ||||
|     return copysign(pow(abs(value), exp), value) | ||||
| } | ||||
|  | ||||
| private func linearTosRGB(_ value: Float) -> Int { | ||||
|     let v = max(0, min(1, value)) | ||||
|     if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } | ||||
|     else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } | ||||
| } | ||||
|  | ||||
| private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float { | ||||
|     let v = Float(Int64(value)) / 255 | ||||
|     if v <= 0.04045 { return v / 12.92 } | ||||
|     else { return pow((v + 0.055) / 1.055, 2.4) } | ||||
| } | ||||
|  | ||||
| private let encodeCharacters: [String] = { | ||||
|     return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } | ||||
| }() | ||||
|  | ||||
| private let decodeCharacters: [String: Int] = { | ||||
|     var dict: [String: Int] = [:] | ||||
|     for (index, character) in encodeCharacters.enumerated() { | ||||
|         dict[character] = index | ||||
|     } | ||||
|     return dict | ||||
| }() | ||||
|  | ||||
| extension String { | ||||
|     func decode83() -> Int { | ||||
|         var value: Int = 0 | ||||
|         for character in self { | ||||
|             if let digit = decodeCharacters[String(character)] { | ||||
|                 value = value * 83 + digit | ||||
|             } | ||||
|         } | ||||
|         return value | ||||
|     } | ||||
| } | ||||
|  | ||||
| private extension String { | ||||
|     subscript (offset: Int) -> Character { | ||||
|         return self[index(startIndex, offsetBy: offset)] | ||||
|     } | ||||
|      | ||||
|     subscript (bounds: CountableClosedRange<Int>) -> Substring { | ||||
|         let start = index(startIndex, offsetBy: bounds.lowerBound) | ||||
|         let end = index(startIndex, offsetBy: bounds.upperBound) | ||||
|         return self[start...end] | ||||
|     } | ||||
|      | ||||
|     subscript (bounds: CountableRange<Int>) -> Substring { | ||||
|         let start = index(startIndex, offsetBy: bounds.lowerBound) | ||||
|         let end = index(startIndex, offsetBy: bounds.upperBound) | ||||
|         return self[start..<end] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								ios/Runner/NativeViews/ImageView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								ios/Runner/NativeViews/ImageView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // | ||||
| //  ImageView.swift | ||||
| //  Runner | ||||
| // | ||||
| //  Created by LittleSheep on 2025/4/21. | ||||
| // | ||||
|  | ||||
| import UIKit | ||||
| import Kingfisher | ||||
|  | ||||
| class ImageView: UIImageView { | ||||
|     private var _size: CGSize = CGSize(width: 0, height: 0) | ||||
|      | ||||
|     // Initialize the image view | ||||
|     override init(frame: CGRect) { | ||||
|         super.init(frame: frame) | ||||
|         contentMode = .scaleAspectFill | ||||
|         clipsToBounds = true | ||||
|     } | ||||
|      | ||||
|     required init?(coder: NSCoder) { | ||||
|         super.init(coder: coder) | ||||
|         contentMode = .scaleAspectFill | ||||
|         clipsToBounds = true | ||||
|     } | ||||
|      | ||||
|     // Method to set the image from a URL using Kingfisher | ||||
|     func setImage(from url: URL, blurHash: String?) { | ||||
|         let placeholderImage = blurHash != nil ? UIImage.init(blurHash: blurHash!, size: _size) : nil | ||||
|         let processor = DownsamplingImageProcessor(size: _size) |> RoundCornerImageProcessor(cornerRadius: 20) | ||||
|          | ||||
|         self.kf.indicatorType = .activity | ||||
|         self.kf.setImage( | ||||
|             with: url, | ||||
|             placeholder: placeholderImage, | ||||
|             options: [ | ||||
|                 .processor(processor), | ||||
|                 .transition(.fade(0.3)) | ||||
|             ] | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								ios/Runner/NativeViews/NativeImage.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								ios/Runner/NativeViews/NativeImage.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| // | ||||
| //  NativeImage.swift | ||||
| //  Runner | ||||
| // | ||||
| //  Created by LittleSheep on 2025/4/21. | ||||
| // | ||||
|  | ||||
| import Flutter | ||||
| import UIKit | ||||
| import Kingfisher | ||||
|  | ||||
| class FLNativeImageFactory : NSObject, FlutterPlatformViewFactory { | ||||
|     private var messenger: FlutterBinaryMessenger | ||||
|      | ||||
|     init(messenger: FlutterBinaryMessenger) { | ||||
|         self.messenger = messenger | ||||
|         super.init() | ||||
|     } | ||||
|      | ||||
|     func create( | ||||
|         withFrame frame: CGRect, | ||||
|         viewIdentifier viewId: Int64, | ||||
|         arguments args: Any? | ||||
|     ) -> FlutterPlatformView { | ||||
|         return FLNativeImage( | ||||
|             frame: frame, | ||||
|             viewIdentifier: viewId, | ||||
|             arguments: args, | ||||
|             binaryMessenger: messenger) | ||||
|     } | ||||
|      | ||||
|     /// Implementing this method is only necessary when the `arguments` in `createWithFrame` is not `nil`. | ||||
|     public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { | ||||
|         return FlutterStandardMessageCodec.sharedInstance() | ||||
|     } | ||||
| } | ||||
|  | ||||
| class FLNativeImage : NSObject, FlutterPlatformView { | ||||
|     private var _view: ImageView | ||||
|      | ||||
|     init( | ||||
|         frame: CGRect, | ||||
|         viewIdentifier viewId: Int64, | ||||
|         arguments args: Any?, | ||||
|         binaryMessenger messenger: FlutterBinaryMessenger? | ||||
|     ) { | ||||
|         _view = ImageView(frame: frame) | ||||
|         super.init() | ||||
|          | ||||
|         let argsMap = args as! [AnyHashable: Any] | ||||
|         let source = argsMap["src"] as! String | ||||
|         let blurHash = argsMap["blur"] as? String | ||||
|          | ||||
|         if let url = URL(string: source) { | ||||
|             _view.setImage(from: url, blurHash: blurHash) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func view() -> UIView { | ||||
|         return _view | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user