0x01 SVG 的实质
SVG实质上是一个XML格式的文本文件,可以同普通的XML文件一样通过一些XML解析库来进行解析。因此绘制SVG的过程实际上是先解析XML文件,然后根据解析出来的命令在屏幕上绘制出来。
SVG的命令常见的有形状、transform、文字、渐变和滤镜。在本系列的文章中,只讨论基本的形状绘制,不涉及文字、渐变和滤镜的部分。
而形状部分主要包括7个类型,分别是:
- path
- line
- circle
- rect
- ellipse
- polyline
- polygon
transform主要包括:
而本文章主要就是讨论上述所说的命令本身和其拥有属性的解析。
0x02 SVG 元素的解析
第一步,先获取SVG的路径,使用iOS系统自带的XML解析工具进行解析。
1 2 3 4
| NSData *data = [NSData dataWithContentsOfFile:fileName]; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; parser.delegate = self; [parser parse];
|
第二步,实现XML解析的代理方法,通过elementName
来创建对应形状的类。因为解析出来的形状拥有许多相同的属性,因此新建一个基类SVGElement
,其它所有的形状都会继承此基类。基类包含的属性和方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @interface SVGElement : NSObject
- (instancetype)initWithAttribute:(NSDictionary *)attr;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *identifier; @property (nonatomic, copy) NSString *className;
@property (nonatomic, copy) NSString *tranform;
@property (nonatomic, copy) NSString *group;
@property (nonatomic, strong) UIBezierPath *path;
@property (nonatomic, strong) UIColor *strokeColor; @property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, assign) BOOL selectable;
@end
|
然后在XML解析的代理方法中实现对应形状的解析,通过elementName
拼接成对应的类名,然后使用NSClassFromString
生成对应的类。另外新建一个elements
的数组用来存储所有解析出来的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict { NSArray *names = @[@"path", @"rect", @"circle", @"ellipse", @"line", @"polyline", @"polygon"]; if ([elementName isEqualToString:@"svg"]) { CGFloat width = attributeDict[@"width"].doubleValue; CGFloat height = attributeDict[@"height"].doubleValue; self.svgSize = CGSizeMake(width, height); } else if ([elementName isEqualToString:@"g"]) { self.transform = [attributeDict objectForKey:@"transform"]; } else if ([names containsObject:elementName]) { NSString *className = [@"SVG" stringByAppendingString:[elementName capitalizedString]]; Class myClass = NSClassFromString(className); SVGElement *element = [((SVGElement *)[myClass alloc]) initWithAttribute:attributeDict]; if (element) { if (!element.tranform && self.transform) { element.tranform = self.transform; } [self.elements addObject:element]; } } }
|
在代理方法中,通过初始化方法initWithAttribute:
已经将attributeDict
属性拿到,会在初始化方法里面进行解析拆分出各个详细的属性,此处不进行详细的介绍,具体可看文章末尾所附上的GitHub链接。
一个常见的transform属性字符串如下所示:
transform=”translate(398.000000, 1925.000000) rotate(90.000000) translate(-398.000000, -1925.000000) translate(-1527.000000, 1527.000000)”
举例所示的transform包含了translate和rotate,另外常见的还有scale。此处要做的便是将transform字符串解析成iOS能识别的CGAffineTransform
类型。
根据translate
、scale
和rotate
这3个关键字利用正则表达式将transform字符串拆分成一个包含NSTextCheckingResult
对象的数组,此时的NSTextCheckingResult
对象实际上就是一个命令加上对应的参数,如translate(398.000000, 1925.000000)
,然后通过NSScanner
忽略掉某些特殊字符,将后面的参数扫描出来,就满足了构成一个CGAffineTransform
对象的条件。按照transform字符串的顺序依次解析就构成了最终的CGAffineTransform
。
需要注意的是,SVG中rotate
使用的是角度,在iOS中需要转换成弧度再进行使用。
详细的转换代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| + (CGAffineTransform)transformFromString:(NSString *)transformString { CGAffineTransform transform = CGAffineTransformIdentity; NSError *error = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"translate|scale|rotate" options:NSRegularExpressionCaseInsensitive error:&error]; NSArray<NSTextCheckingResult *> *checkResults = [regex matchesInString:transformString options:0 range:NSMakeRange(0, [transformString length])]; NSScanner *scanner = [NSScanner scannerWithString:transformString]; NSMutableCharacterSet *skippedCharacterSet = [[NSMutableCharacterSet alloc] init]; [skippedCharacterSet formUnionWithCharacterSet:[NSCharacterSet letterCharacterSet]]; [skippedCharacterSet formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@",() "]]; scanner.charactersToBeSkipped = skippedCharacterSet; for (NSTextCheckingResult *result in checkResults) { if ([[transformString substringWithRange:result.range] isEqualToString:@"translate"]) { CGFloat valueX; CGFloat valueY; [scanner scanDouble:&valueX]; [scanner scanDouble:&valueY]; transform = CGAffineTransformTranslate(transform, valueX, valueY); } else if ([[transformString substringWithRange:result.range] isEqualToString:@"rotate"]) { CGFloat angle; [scanner scanDouble:&angle]; transform = CGAffineTransformRotate(transform, [self.class radianFromAngle:angle]); } else if ([[transformString substringWithRange:result.range] isEqualToString:@"scale"]) { CGFloat scale; [scanner scanDouble:&scale]; transform = CGAffineTransformScale(transform, scale, scale); } } return transform; }
+ (CGFloat)radianFromAngle:(CGFloat)angle { return angle / 180.0 * M_PI; }
|
0x04 SVG Path命令的解析
Path命令的解析由于和path的绘制关联紧密,因此会在path的绘制章节中一起进行讲解。
关于SVG的解析绘制所有源码包括demo可在GitHub进行查看:SVGlib