【无限互联】ios开发之图文混排研究

本节要点:

1.理解图文混排的概念。

2.研究FTCoreText框架的实现原理。

3.演示使用FTCoreText框架实现图文混排的demo,以及效果图。


图文混排的概念

 在view上显示的数据既包括文本数据,也包括图片数据。

 图片的大小可以不同,图片和文字的排列顺序不固定。

  文本数据的字体、颜色、背景等其他属性可以不同。

 文本数据中还可以包含超链接。可以点击超链接进入其他页面。


研究FTCoreText框架的实现原理

1>.此框架的功能 和 缺陷

1.1> 功能:此框架能够实现不同风格的文本数据、图片数据的混合显示,还可以设置超链接。

1.2> 缺陷:此框架里面的文本的处理只针对与XML或者HTML格式的数据。(比如这种标签类型的数据:<title>Giraffe</title>)对于JSON格式的数据,可能需要自己修改内部一些方法的实现。


2>.此框架的基本构成

2.1> 此框架有两个类组成,分别是FTCoreTextStyle类、FTCoreTextView类。

2.2> FTCoreTextStyle类主要是用于设置格式的,定义了一些属性和方法。通过这个类就可以定义一种格式。

2.3> FTCoreTextView这个类是此框架的核心类。这个类里面定义了一个私有的类FTCoreTextNode。需要处理的数据中,每一个标签(<title>Giraffe</title>)对应一个节点。节点类里面就封装了一些节点的属性和方法,供FTCoreTextView这个类使用。

2.4> FTCoreTextView这个类主要封装了对数据的处理、绘制图片和文本,其中包含了很多处理细节的方法。


3>.FTCoreText框架内部具体的实现原理

3.1>FTCoreTextStyle类的主要实现

FTCoreTextStyle类封装的一些重要的属性:

<span style="color:#993399;">@property (nonatomic) NSString	*name; //格式的名称
@property (nonatomic) NSString	*appendedCharacter; //追加的字符串
@property (nonatomic) UIFont	*font;  //字体
@property (nonatomic) UIColor	*color;  //颜色
@property (nonatomic, getter=isUnderLined) BOOL underlined; //下划线
@property (nonatomic) FTCoreTextAlignement textAlignment; //文字显示位置
@property (nonatomic) UIEdgeInsets  paragraphInset; //段落之间的间隔
@property (nonatomic) CGFloat	leading;
@property (nonatomic) CGFloat	maxLineHeight; //最大行高
@property (nonatomic) CGFloat	minLineHeight; //最小行高
// bullet 格式
@property (nonatomic) NSString	*bulletCharacter; //设置bullet字符
@property (nonatomic) UIFont	*bulletFont; // 设置bullet字体
@property (nonatomic) UIColor	*bulletColor;  //设置bullet颜色
//当格式被解析时,回调的block
@property (nonatomic, copy) FTCoreTextCallbackBlock     block;
@property (nonatomic, assign) BOOL applyParagraphStyling;  //段落格式</span>

FTCoreTextStyle类封装的一些重要的方法:

<span style="color:#006600;">//初始化方法
- (id)init
{
	self = [super init];
	if (self) {
		self.name = @"_default";
		self.bulletCharacter = @"•";
		self.appendedCharacter = @"";
		self.font = [UIFont systemFontOfSize:12];
		self.color = [UIColor blackColor];
		self.underlined = NO;
		self.textAlignment = FTCoreTextAlignementLeft;
		self.maxLineHeight = 0;
		self.minLineHeight = 0;
		self.paragraphInset = UIEdgeInsetsZero;
		self.applyParagraphStyling = YES;
		self.leading = 0;
        self.block=nil;
	}
	return self;
}</span>
<span style="color:#006600;">//类方法,通过name来定义一种格式
+ (id)styleWithName:(NSString *)name
{
    FTCoreTextStyle *style = [[FTCoreTextStyle alloc] init];
    [style setName:name];
    return style;
}</span>

3.2>FTCoreTextNode的主要实现

FTCoreTextNode封装的一些主要的属性:

<span style="color:#993399;">@property (nonatomic, assign) FTCoreTextNode  *supernode; //父节点
@property (nonatomic) NSArray	*subnodes; //所有子类节点
@property (nonatomic, copy)	  FTCoreTextStyle  *style; //定义一种格式
@property (nonatomic) NSRange	styleRange; //定义格式应用的范围
@property (nonatomic) BOOL  isClosed; //是否是关闭
@property (nonatomic) NSInteger	 startLocation; //开始的位置
@property (nonatomic) BOOL  isLink; //是否是链接
@property (nonatomic) BOOL  isImage; //是否是图片
@property (nonatomic) BOOL  isBullet; //是否是Bullet
@property (nonatomic) NSString	*imageName;  //图片的名称</span>

FTCoreTextNode封装的一些主要的方法:

<span style="color:#006600;">//在所有子节点的最后插入一个子节点
- (void)addSubnode:(FTCoreTextNode *)node
{
	[self insertSubnode:node atIndex:[_subnodes count]];
}

//在指定的位置插入子节点
- (void)insertSubnode:(FTCoreTextNode *)subnode atIndex:(NSUInteger)index
{
    //设置父节点
   subnode.supernode = self;
   NSMutableArray *subnodes = (NSMutableArray *)self.subnodes;
    //判断index是否超出数组_subnodes的长度
   if (index <= [_subnodes count]) {
   [subnodes insertObject:subnode atIndex:index];
 }
  else {
  [subnodes addObject:subnode];
  }
}</span>
<span style="color:#006600;">//获取节点的索引
- (NSUInteger)nodeIndex
{
  return [_supernode.subnodes indexOfObject:self];
}

//获取指定位置的子节点
- (FTCoreTextNode *)subnodeAtIndex:(NSUInteger)index
{
   if (index < [_subnodes count]) {
  return [_subnodes objectAtIndex:index];
  }
   return nil;
}
</span>

3.3> FTCoreTextView 类的主要实现

FTCoreTextView封装的一些主要的属性:

<span style="color:#993399;">@property (nonatomic) NSString	*text;  //文本内容
@property (nonatomic) NSString	*processedString; //处理之后的文本
@property (nonatomic, readonly) NSAttributedString  *attributedString;  //属性文本
@property (nonatomic, assign) CGPathRef	 path; //路径
@property (nonatomic) NSMutableDictionary  *URLs; //存储所有的URL
@property (nonatomic) NSMutableArray  *images; //存储所有图片
@property (nonatomic, assign) id <FTCoreTextViewDelegate> delegate; //设置代理对象
@property (nonatomic) UIColor *shadowColor; //阴影颜色
@property (nonatomic) CGSize shadowOffset; //阴影大小
@property (nonatomic) BOOL verbose; 
@property (nonatomic) BOOL highlightTouch; //触摸时高亮状态</span>
<pre name="code" class="objc"><span style="color:#993399;">@property (nonatomic) CTFramesetterRef framesetter; 
@property (nonatomic) FTCoreTextNode *rootNode; //节点
@property (nonatomic, readwrite) NSAttributedString  *attributedString; //属性文本
@property (nonatomic) NSDictionary *touchedData; //点击的文本数据的属性的key和value的映射
@property (nonatomic) NSArray *selectionsViews; //选中的视图</span>
 

<span style="font-size:14px;color:#993399;">//以下常量是一些默认的标记(标签)名称
extern NSString * const FTCoreTextTagDefault; //默认标记
extern NSString * const FTCoreTextTagImage; //图片标记
extern NSString * const FTCoreTextTagBullet; //Bullet标记
extern NSString * const FTCoreTextTagPage; //< _page / >分页标记	
extern NSString * const FTCoreTextTagLink; //超链接标记
extern NSString * const FTCoreTextTagParagraph; //段落标记</span>

<span style="font-size:14px;color:#993399;">//以下这些常量被使用在字典中的属性,这个字典是作为代理对象的协议方法的参数
extern NSString * const FTCoreTextDataURL; //定义URL
extern NSString * const FTCoreTextDataName; //定义Name
extern NSString * const FTCoreTextDataFrame; //定义Frame
extern NSString * const FTCoreTextDataAttributes; //定义Attributes
@protocol FTCoreTextViewDelegate; //定义协议</span>

FTCoreTextView 封装的一些主要的方法(主要方法的调用流程及实现的功能):

/*
 调整视图的高度:
 1、创建一个CGSize宽度就为视图当前的宽度,高度无穷大
 2、调用将此suggestedSizeConstrainedToSize方法,CGSize作为参数。在这个方法中通过_framesetter和CGSize计算出一个合适的size,并返回size。由此确定当前视图的frame.
*/
- (void)fitToSuggestedHeight
{
	CGSize suggestedSize = [self <span style="background-color: rgb(255, 153, 102);">suggestedSizeConstrainedToSize:CGSizeMake(CGRectGetWidth(self.frame), MAXFLOAT)</span>];
	CGRect viewFrame = self.frame;
	viewFrame.size.height = suggestedSize.height;
	self.frame = viewFrame;
}

//计算文本的CGSize
- (CGSize)suggestedSizeConstrainedToSize:(CGSize)size
{
	CGSize suggestedSize;
	[self <span style="background-color: rgb(255, 153, 0);">updateFramesetterIfNeeded</span>];
	if (_framesetter == NULL) {
		return CGSizeZero;
	}
	suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(_framesetter, CFRangeMake(0, 0), NULL, size, NULL);
	suggestedSize = CGSizeMake(ceilf(suggestedSize.width), ceilf(suggestedSize.height));
    return suggestedSize;
}
//更新Framesetter
- (void)updateFramesetterIfNeeded
{
    if (!_coreTextViewFlags.updatedAttrString) {
		if (_framesetter != NULL) CFRelease(_framesetter);
		_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)<span style="background-color: rgb(255, 153, 0);">self.attributedString);</span>
		_coreTextViewFlags.updatedAttrString = YES;
		_coreTextViewFlags.updatedFramesetter = YES;
    }
}
<span style="color:#008400;">//取得处理之后的属性文本
- (NSAttributedString *)attributedString
{
	if (!_coreTextViewFlags.updatedAttrString) {
		_coreTextViewFlags.updatedAttrString = YES;
		
		if (_processedString == nil || _coreTextViewFlags.textChangesMade || !_coreTextViewFlags.updatedFramesetter) {
			_coreTextViewFlags.textChangesMade = NO;
			[</span><span style="color: rgb(0, 132, 0); background-color: rgb(255, 153, 0);">self processText</span><span style="color:#008400;">];
		}
		
		if (_processedString) {
			
			NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:_processedString];
			
			for (FTCoreTextNode *node in [_rootNode allSubnodes]) {
				[self </span><span style="color:#009900;background-color: rgb(255, 153, 0);">applyStyle:node.style inRange:node.styleRange onString:&string</span><span style="color:#008400;">];
			}
			
			_attributedString = string;
		}
	}
	return _attributedString;
}</span>

<span style="color:#006600;">/*
 </span><span style="color:#ff0000;">处理文本的方法的实现(比较核心的方法):</span><span style="color:#006600;">
 1、定义正则表达式,在要处理的字符串中查找出符合要求的字符串。
 NSString *regEx = @"<(/){0,1}.*?( /){0,1}>"; 此正则表达式,可以查找出三类标签,例如:<a 属性=@“123”>  </a>   <a/>
 2、以上三类标签分别对应代码中的3中类型:
 1>. <a 属性=@“123”> 对应这种类型  tagType = FTCoreTextTagTypeOpen;
 2>. </a> 对应这种类型 tagType = FTCoreTextTagTypeClose;
 3>. <a/>  对应这种类型 tagType = FTCoreTextTagTypeSelfClose;
 3、获取到标签后,取得标签的名称。在对应的标签类型下进行判断和处理。
 4、每个这样的标签(<a>123</a>)对应一个节点。每次取得一个标签的头部(标签类型:FTCoreTextTagTypeOpen),就创建一个节点。通过标签的名称判断是否是超链接、图片等等。
 判断完之后,就设置当前节点的属性(节点有很多属性:isLink,isImage等等)。如果是图片,则设置 newNode.isImage = YES;  进行下次循环时,取得标签尾部(标签类型:FTCoreTextTagTypeClose),就直接通过当前节点的属性判断是否是图片标签,或者其他类型。根据判断结果,使用封装好的FTCoreTextStyle类,为不同的标签内容设置不同的格式Style为标签的内容设置不同的格式。
*/
- (void)processText
{
    if (!_text || [_text length] == 0) return;
	[_URLs removeAllObjects];
        [_images removeAllObjects];	
	FTCoreTextNode *rootNode = [FTCoreTextNode new];
	rootNode.style = [_styles objectForKey:[self defaultTagNameForKey:FTCoreTextTagDefault]];
	FTCoreTextNode *currentSupernode = rootNode;
	NSMutableString *processedString = [NSMutableString stringWithString:_text];
	BOOL finished = NO;
	NSRange remainingRange = NSMakeRange(0, [processedString length]);
    //定义了正则表达式,查找标签,例如:<a 属性=@“123”> </a> <a/>
	<span style="background-color: rgb(204, 204, 255);">NSString *regEx = @"<(/){0,1}.*?( /){0,1}>";</span>
	
	while (!finished) {
		
		NSRange tagRange = [processedString rangeOfString:regEx options:NSRegularExpressionSearch range:remainingRange];
		
		if (tagRange.location == NSNotFound) {
			if (currentSupernode != rootNode && !currentSupernode.isClosed) {
				if (_verbose) NSLog(@"FTCoreTextView :%@ - Couldn't parse text because tag '%@' at position %d is not closed - aborting rendering", self, currentSupernode.style.name, currentSupernode.startLocation);
				return;
			}
			finished = YES;
            continue;
		}
        NSString *fullTag = [processedString substringWithRange:tagRange];
        FTCoreTextTagType tagType;
        
        if ([fullTag rangeOfString:@"</"].location == 0) {
            //标签</b>
            tagType = FTCoreTextTagTypeClose;
        }
        else if ([fullTag rangeOfString:@"/>"].location == NSNotFound && [fullTag rangeOfString:@" />"].location == NSNotFound) {
            //标签<b>或者<b 属性=@"123">
            tagType = FTCoreTextTagTypeOpen;
        }
        else {
            //标签<b/>
            tagType = FTCoreTextTagTypeSelfClose;
        } 
        //获取整个标签所有组成部分
		NSArray *tagsComponents = [fullTag componentsSeparatedByString:@" "];
        
         //获取标签名称(里面可能包括"<")
		NSString *tagName = (tagsComponents.count > 0) ? [tagsComponents objectAtIndex:0] : fullTag;
        
        //这里获得的tagName的值例如:_image
        tagName = [tagName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"< />"]];
		
        FTCoreTextStyle *style = [_styles objectForKey:tagName];
        
        if (style == nil) {
            style = [_styles objectForKey:[self defaultTagNameForKey:FTCoreTextTagDefault]];
            if (_verbose) NSLog(@"FTCoreTextView :%@ - Couldn't find style for tag '%@'", self, tagName);
        }
        //判断标签的类型
        switch (tagType) {
            case FTCoreTextTagTypeOpen:
            {
                if (currentSupernode.isLink || currentSupernode.isImage) {
                    NSString *predefinedTag = nil;
                    if (currentSupernode.isLink) predefinedTag = [self defaultTagNameForKey:FTCoreTextTagLink];
                    else if (currentSupernode.isImage) predefinedTag = [self defaultTagNameForKey:FTCoreTextTagImage];
                    if (_verbose) NSLog(@"FTCoreTextView :%@ - You can't open a new tag inside a '%@' tag - aborting rendering", self, predefinedTag);
                    return;
                } 
                FTCoreTextNode *newNode = [FTCoreTextNode new];
                newNode.style = style;
                newNode.startLocation = tagRange.location;
                //判断是否是链接
                if ([tagName isEqualToString:[self defaultTagNameForKey:FTCoreTextTagLink]]) {
                    newNode.isLink = YES;
                }  
                //判断是否是Bullet
                else if ([tagName isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]]) {
                    newNode.isBullet = YES;
                    NSString *appendedString = [NSString stringWithFormat:@"%@\t", newNode.style.bulletCharacter];      
                    [processedString insertString:appendedString atIndex:tagRange.location + tagRange.length];
                    
                    //bullet styling
                    //设置子标签的样式
                    FTCoreTextStyle *bulletStyle = [FTCoreTextStyle new];
                    bulletStyle.name = @"_FTBulletStyle";
                    bulletStyle.font = newNode.style.bulletFont;
                    bulletStyle.color = newNode.style.bulletColor;
                    bulletStyle.applyParagraphStyling = NO;
                    bulletStyle.paragraphInset = UIEdgeInsetsMake(0, 0, 0, newNode.style.paragraphInset.left);
                    
                    FTCoreTextNode *bulletNode = [FTCoreTextNode new];
                    bulletNode.style = bulletStyle;
                    bulletNode.styleRange = NSMakeRange(tagRange.location, [appendedString length]);
                    
                    [newNode addSubnode:bulletNode];
                }     
                //判断是否是图片
                else if ([tagName isEqualToString:[self defaultTagNameForKey:FTCoreTextTagImage]]) {
                    newNode.isImage = YES;
                }      
                [processedString replaceCharactersInRange:tagRange withString:@""];   
                [currentSupernode addSubnode:newNode];        
                currentSupernode = newNode;  
                remainingRange.location = tagRange.location;
                remainingRange.length = [processedString length] - tagRange.location;
            }
                break;
            case FTCoreTextTagTypeClose:
            {
                if ((![currentSupernode.style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagDefault]] && ![currentSupernode.style.name isEqualToString:tagName]) ) {
                    if (_verbose) NSLog(@"FTCoreTextView :%@ - Closing tag '%@' at range %@ doesn't match open tag '%@' - aborting rendering", self, fullTag, NSStringFromRange(tagRange), currentSupernode.style.name);
                    return;
                }
                
                currentSupernode.isClosed = YES;
                if (currentSupernode.isLink) {
                    NSRange elementContentRange = NSMakeRange(currentSupernode.startLocation, tagRange.location - currentSupernode.startLocation);
                    NSString *elementContent = [processedString substringWithRange:elementContentRange];
                    NSRange pipeRange = [elementContent rangeOfString:@"|"];
                    NSString *urlString = nil;
                    NSString *urlDescription = nil;
                    if (pipeRange.location != NSNotFound) {
                        urlString = [elementContent substringToIndex:pipeRange.location] ;
                        urlDescription = [elementContent substringFromIndex:pipeRange.location + 1];
                    }
                    [processedString replaceCharactersInRange:NSMakeRange(elementContentRange.location, elementContentRange.length + tagRange.length) withString:urlDescription];
                    if (!([[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] hasPrefix:@"http://"] || [[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] hasPrefix:@"https://"]))
                    {
                      urlString = [NSString stringWithFormat:@"http://%@", urlString];
                    }
                    NSURL *url = [NSURL URLWithString:[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
                    NSRange urlDescriptionRange = NSMakeRange(elementContentRange.location, [urlDescription length]);
                    [_URLs setObject:url forKey:NSStringFromRange(urlDescriptionRange)];
                    
                    currentSupernode.styleRange = urlDescriptionRange;
                }
                else if (currentSupernode.isImage) {
                    NSRange elementContentRange = NSMakeRange(currentSupernode.startLocation, tagRange.location - currentSupernode.startLocation);
                    NSString *elementContent = [processedString substringWithRange:elementContentRange];
                    UIImage *img = [UIImage imageNamed:elementContent];
                    
                    if (img) {
                        NSString *lines = @"\n";
                        float leading = img.size.height;
                        currentSupernode.style.leading = leading;    
                        currentSupernode.imageName = elementContent;
                        [processedString replaceCharactersInRange:NSMakeRange(elementContentRange.location, elementContentRange.length + tagRange.length) withString:lines];
                        
                        [_images addObject:currentSupernode];
                        currentSupernode.styleRange = NSMakeRange(elementContentRange.location, [lines length]);
                    }
                    else {
                        if (_verbose) NSLog(@"FTCoreTextView :%@ - Couldn't find image '%@' in main bundle", self, [NSValue valueWithRange:elementContentRange]);
                        [processedString replaceCharactersInRange:tagRange withString:@""];
                    }
                }
                else {
                    currentSupernode.styleRange = NSMakeRange(currentSupernode.startLocation, tagRange.location - currentSupernode.startLocation);
                    [processedString replaceCharactersInRange:tagRange withString:@""];
                }
                if ([currentSupernode.style.appendedCharacter length] > 0) {
                    [processedString insertString:currentSupernode.style.appendedCharacter atIndex:currentSupernode.styleRange.location + currentSupernode.styleRange.length];
                    NSRange newStyleRange = currentSupernode.styleRange;
                    newStyleRange.length += [currentSupernode.style.appendedCharacter length];
                    currentSupernode.styleRange = newStyleRange;
                }  
                if (style.paragraphInset.top > 0) {
                    if (![style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]] ||  [[currentSupernode previousNode].style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]]) {
                        [processedString insertString:@"\n" atIndex:currentSupernode.startLocation];
                        NSRange topSpacingStyleRange = NSMakeRange(currentSupernode.startLocation, [@"\n" length]);
                        FTCoreTextStyle *topSpacingStyle = [[FTCoreTextStyle alloc] init];
                        topSpacingStyle.name = [NSString stringWithFormat:@"_FTTopSpacingStyle_%@", currentSupernode.style.name];
                        topSpacingStyle.minLineHeight = currentSupernode.style.paragraphInset.top;
                        topSpacingStyle.maxLineHeight = currentSupernode.style.paragraphInset.top;
                        FTCoreTextNode *topSpacingNode = [[FTCoreTextNode alloc] init];
                        topSpacingNode.style = topSpacingStyle;
                        
                        topSpacingNode.styleRange = topSpacingStyleRange;
                        
                        [currentSupernode.supernode insertSubnode:topSpacingNode beforeNode:currentSupernode];
                        
                        [currentSupernode adjustStylesAndSubstylesRangesByRange:topSpacingStyleRange];
                    }
                }
                remainingRange.location = currentSupernode.styleRange.location + currentSupernode.styleRange.length;
                remainingRange.length = [processedString length] - remainingRange.location;
                currentSupernode = currentSupernode.supernode;
            }
                break;
            case FTCoreTextTagTypeSelfClose:
            {
                FTCoreTextNode *newNode = [FTCoreTextNode new];
                newNode.style = style;
                [processedString replaceCharactersInRange:tagRange withString:newNode.style.appendedCharacter];
                newNode.styleRange = NSMakeRange(tagRange.location, [newNode.style.appendedCharacter length]);
                newNode.startLocation = tagRange.location;
                [currentSupernode addSubnode:newNode];
				
                if (style.block)
                {
                    NSDictionary* blockDict = [NSDictionary dictionaryWithObjectsAndKeys:tagsComponents,@"components", [NSValue valueWithRange:NSMakeRange(tagRange.location, newNode.style.appendedCharacter.length)],@"range", nil];
                    style.block(blockDict);
                }
                remainingRange.location = tagRange.location;
                remainingRange.length = [processedString length] - tagRange.location;
            }
                break;
        }
	}
	
	rootNode.styleRange = NSMakeRange(0, [processedString length]);	
	self.rootNode = rootNode;
	self.processedString = processedString;
}</span>

<span style="color:#006600;">//在指定的范围内,应用指定的格式
- (void)applyStyle:(FTCoreTextStyle *)style inRange:(NSRange)styleRange onString:(NSMutableAttributedString **)attributedString
{
    [*attributedString addAttribute:(id)FTCoreTextDataName
							  value:(id)style.name
							  range:styleRange];
    
	[*attributedString addAttribute:(id)kCTForegroundColorAttributeName
							  value:(id)style.color.CGColor
							  range:styleRange];
	
	if (style.isUnderLined) {
		NSNumber *underline = [NSNumber numberWithInt:kCTUnderlineStyleSingle];
		[*attributedString addAttribute:(id)kCTUnderlineStyleAttributeName
								  value:(id)underline
								  range:styleRange];
	}
	
	CTFontRef ctFont = CTFontCreateFromUIFont(style.font);
	
	[*attributedString addAttribute:(id)kCTFontAttributeName
							  value:(__bridge id)ctFont
							  range:styleRange];
	CFRelease(ctFont);
	
	CTTextAlignment alignment = style.textAlignment;
	CGFloat maxLineHeight = style.maxLineHeight;
	CGFloat minLineHeight = style.minLineHeight;
	CGFloat paragraphLeading = style.leading;
	
	CGFloat paragraphSpacingBefore = style.paragraphInset.top;
	CGFloat paragraphSpacingAfter = style.paragraphInset.bottom;
	CGFloat paragraphFirstLineHeadIntent = style.paragraphInset.left;
	CGFloat paragraphHeadIntent = style.paragraphInset.left;
	CGFloat paragraphTailIntent = style.paragraphInset.right;
	
	//if (SYSTEM_VERSION_LESS_THAN(@"5.0")) {
	paragraphSpacingBefore = 0;
	//}
	
	CFIndex numberOfSettings = 9;
	CGFloat tabSpacing = 28.f;
	
	BOOL applyParagraphStyling = style.applyParagraphStyling;
	
	if ([style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]]) {
		applyParagraphStyling = YES;
	}
	else if ([style.name isEqualToString:@"_FTBulletStyle"]) {
		applyParagraphStyling = YES;
		numberOfSettings++;
		tabSpacing = style.paragraphInset.right;
		paragraphSpacingBefore = 0;
		paragraphSpacingAfter = 0;
		paragraphFirstLineHeadIntent = 0;
		paragraphTailIntent = 0;
	}
	else if ([style.name hasPrefix:@"_FTTopSpacingStyle"]) {
		[*attributedString removeAttribute:(id)kCTParagraphStyleAttributeName range:styleRange];
	}
	
	if (applyParagraphStyling) {
		
		CTTextTabRef tabArray[] = { CTTextTabCreate(0, tabSpacing, NULL) };
		
		CFArrayRef tabStops = CFArrayCreate( kCFAllocatorDefault, (const void**) tabArray, 1, &kCFTypeArrayCallBacks );
		CFRelease(tabArray[0]);
		
		CTParagraphStyleSetting settings[] = {
			{kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
			{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maxLineHeight},
			{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minLineHeight},
			{kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), ¶graphSpacingBefore},
			{kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacingAfter},
			{kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), ¶graphFirstLineHeadIntent},
			{kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), ¶graphHeadIntent},
			{kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), ¶graphTailIntent},
			{kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), ¶graphLeading},
			{kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops}//always at the end
		};
		
		CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, numberOfSettings);
		[*attributedString addAttribute:(id)kCTParagraphStyleAttributeName
								  value:(__bridge id)paragraphStyle
								  range:styleRange];
		CFRelease(tabStops);
		CFRelease(paragraphStyle);
	}
}</span>


//绘制的方法

<span style="color:#006600;">/*
 drawRect方法调用流程:
 程序运行先调用drawRect方法,然后再调用drawImages方法,将整个页面绘制出来。
drawRect方法的实现:
 1、创建CGPath
 2、由CGPath和framesetter创建CTFrameRef
 3、绘制:CTFrameDraw(drawFrame, context);
 4、如果有图片就调用drawImages,进行图片绘制
 
 drawImages方法的实现:
 1、创建CGPath
 2、由CGPath和framesetter创建CTFrameRef
 3、通过这个方法CTFrameGetLines(ctframe);获取lines
 4、最后构造出frame,绘制图片[img drawInRect:CGRectIntegral(frame)];
 */

- (void)drawRect:(CGRect)rect
{
	CGContextRef context = UIGraphicsGetCurrentContext();
	
	[self.backgroundColor setFill];
	CGContextFillRect(context, rect);
	
    //更新Framesetter
	[self updateFramesetterIfNeeded];
	
	CGMutablePathRef mainPath = CGPathCreateMutable();
	
	if (!_path) {
		CGPathAddRect(mainPath, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
	}
	else {
		CGPathAddPath(mainPath, NULL, _path);
	}
	
	CTFrameRef drawFrame = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), mainPath, NULL);
	
	if (drawFrame == NULL) {
		if (_verbose) NSLog(@"f: %@", self.processedString);
	}
	else {
		//draw images 绘制图片
		if ([_images count] > 0) [self<span style="background-color: rgb(255, 153, 0);"> drawImages</span>];
		
        //设置阴影颜色
		if (_shadowColor) {
			CGContextSetShadowWithColor(context, _shadowOffset, 0.f, _shadowColor.CGColor);
		}
		
        //转换坐标系统
		CGContextSetTextMatrix(context, CGAffineTransformIdentity);
		CGContextTranslateCTM(context, 0, self.bounds.size.height);
		CGContextScaleCTM(context, 1.0, -1.0);
		
        // draw text 
		CTFrameDraw(drawFrame, context);
	}
	// cleanup
	if (drawFrame) CFRelease(drawFrame);
	CGPathRelease(mainPath);
    
    if ([_delegate respondsToSelector:@selector(coreTextViewfinishedRendering:)]) {
        [_delegate coreTextViewfinishedRendering:self];
    }
}</span><span style="color:#009900;">
</span>


<span style="color:#006600;">//绘制图片
- (void)drawImages
{
	CGMutablePathRef mainPath = CGPathCreateMutable();
    if (!_path) {
        CGPathAddRect(mainPath, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
    }
    else {
        CGPathAddPath(mainPath, NULL, _path);
    }
	
    CTFrameRef ctframe = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), mainPath, NULL);
    CGPathRelease(mainPath);
	
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(ctframe);
    NSInteger lineCount = [lines count];
    CGPoint origins[lineCount];
	
	CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), origins);
	
	FTCoreTextNode *imageNode = [_images objectAtIndex:0];
	
	for (int i = 0; i < lineCount; i++) {
		CGPoint baselineOrigin = origins[i];
		//the view is inverted, the y origin of the baseline is upside down
		baselineOrigin.y = CGRectGetHeight(self.frame) - baselineOrigin.y;
		
		CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
		CFRange cfrange = CTLineGetStringRange(line);
		
        if (cfrange.location > imageNode.styleRange.location) {
			CGFloat ascent, descent;
			CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
			
			CGRect lineFrame = CGRectMake(baselineOrigin.x, baselineOrigin.y - ascent, lineWidth, ascent + descent);
			
			CTTextAlignment alignment = imageNode.style.textAlignment;
            UIImage *img = [UIImage imageNamed:imageNode.imageName];
			
			if (img) {
				int x = 0;
				if (alignment == kCTRightTextAlignment) x = (self.frame.size.width - img.size.width);
				if (alignment == kCTCenterTextAlignment) x = ((self.frame.size.width - img.size.width) / 2);
				
				CGRect frame = CGRectMake(x, (lineFrame.origin.y - img.size.height), img.size.width, img.size.height);
                
                // adjusting frame
				
                UIEdgeInsets insets = imageNode.style.paragraphInset;
                if (alignment != kCTCenterTextAlignment) frame.origin.x = (alignment == kCTLeftTextAlignment)? insets.left : (self.frame.size.width - img.size.width - insets.right);
                frame.origin.y += insets.top;
                frame.size.width = ((insets.left + insets.right + img.size.width ) > self.frame.size.width)? self.frame.size.width : img.size.width;
                
				[img drawInRect:CGRectIntegral(frame)];
			}
			
			NSInteger imageNodeIndex = [_images indexOfObject:imageNode];
			if (imageNodeIndex < [_images count] - 1) {
				imageNode = [_images objectAtIndex:imageNodeIndex + 1];
			}
			else {
				break;
			}
		}
	}
	CFRelease(ctframe);
}
</span>

//触摸事件触发的方法

<span style="color:#006600;">/*
 当用户点击超链接时,方法调用流程:
  首先调用touchesBegan方法。在touchesBegan方法中又会调用dataForPoint:activeRects:这个方法,返回被点击的超链接页面绘制需要的参数字典。最后在touchesEnded的方法中调用协议方法,并将参数字典传过去。在协议方法中取得被点击的超链接对应的URL,显示出URL对应的页面。
  
 */

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	[super touchesEnded:touches withEvent:event];
	
	if (self.delegate && [self.delegate respondsToSelector:@selector(coreTextView:receivedTouchOnData:)]) {
		CGPoint point = [(UITouch *)[touches anyObject] locationInView:self];
		NSMutableArray *activeRects;
		NSDictionary *data = [self <span style="background-color: rgb(255, 153, 0);">dataForPoint:point activeRects:&activeRects</span>];
		if (data.count > 0) {
			NSMutableArray *selectedViews = [NSMutableArray new];
			for (NSString *rectString in activeRects) {
				CGRect rect = CGRectFromString(rectString);
				UIView *view = [[UIView alloc] initWithFrame:rect];
				view.layer.cornerRadius = 3;
				view.clipsToBounds = YES;
				view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.25];
				[self addSubview:view];
				[selectedViews addObject:view];
			}
			self.touchedData = data;
			self.selectionsViews = selectedViews;
		}
	}
}</span>


<span style="color:#006600;">/* 在touchBegan方法中调用此方法
 此方法的实现:
 1、构造CGPathRef
 2、通过CTFramesetterRef和CGPath 构造出一个CTFrameRef
 3、在从CTFrameRef获取lines: NSArray *lines = (__bridge NSArray *)CTFrameGetLines(ctframe);
 4.从lines里面获取runs: CFArrayRef runs = CTLineGetGlyphRuns(line);
 5.遍历所有的run,获取到attributes,并且设置了runBounds;
 然后将attributes和runBounds存到一个字典中:
 [returnedDict setObject:attributes forKey:FTCoreTextDataAttributes];
 [returnedDict setObject:NSStringFromCGRect(runBounds) forKey:FTCoreTextDataFrame];
 返回字典returnedDict。

 */

- (NSDictionary *)dataForPoint:(CGPoint)point activeRects:(NSArray **)activeRects
{
	NSMutableDictionary *returnedDict = [NSMutableDictionary dictionary];
	
	CGMutablePathRef mainPath = CGPathCreateMutable();
    if (!_path) {
        CGPathAddRect(mainPath, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
    }
    else {
        CGPathAddPath(mainPath, NULL, _path);
    }
	
    CTFrameRef ctframe = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), mainPath, NULL);
    CGPathRelease(mainPath);
	
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(ctframe);
    NSInteger lineCount = [lines count];
    
    //数组存放点
    CGPoint origins[lineCount];
    
    if (lineCount != 0) {
		
		CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), origins);
		
		for (int i = 0; i < lineCount; i++) {
			CGPoint baselineOrigin = origins[i];
			//the view is inverted, the y origin of the baseline is upside down
             //y 的起点是反向的,视图是颠倒的
			baselineOrigin.y = CGRectGetHeight(self.frame) - baselineOrigin.y;
			
			CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
			CGFloat ascent, descent;
			CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
			
			CGRect lineFrame = CGRectMake(baselineOrigin.x, baselineOrigin.y - ascent, lineWidth, ascent + descent);
			
            //判断点是否在lineFrame的范围内
			if (CGRectContainsPoint(lineFrame, point)) {
				//we look if the position of the touch is correct on the line
				
				CFIndex index = CTLineGetStringIndexForPosition(line, point);
                
				NSArray *urlsKeys = [_URLs allKeys];
				
				for (NSString *key in urlsKeys) {
					NSRange range = NSRangeFromString(key);
					if (index >= range.location && index < range.location + range.length) {
						NSURL *url = [_URLs objectForKey:key];
						if (url) [returnedDict setObject:url forKey:FTCoreTextDataURL];
						
						if (activeRects && _highlightTouch) {
							//we looks for the rects enclosing the entire active section
							NSInteger startIndex = range.location;
							NSInteger endIndex = range.location + range.length;
							
							//we look for the line that contains the start index
                            //寻找线的起点的索引
							NSInteger startLineIndex = i;
							for (int iLine = i; iLine >= 0; iLine--) {
								CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:iLine];
								CFRange range = CTLineGetStringRange(line);
								if (range.location <= startIndex && range.location + range.length >= startIndex) {
									startLineIndex = iLine;
									break;
								}
							}
							//we look for the line that contains the end index
                            //寻找线的终点索引
							NSInteger endLineIndex = startLineIndex;
							for (int iLine = i; iLine < lineCount; iLine++) {
								CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:iLine];
								CFRange range = CTLineGetStringRange(line);
								if (range.location <= endIndex && range.location + range.length >= endIndex) {
									endLineIndex = iLine;
									break;
								}
							}
							//we get enclosing rects
                            //获取到封闭的矩形
							NSMutableArray *rectsStrings = [NSMutableArray new];
							for (int iLine = startLineIndex; iLine <= endLineIndex; iLine++) {
								CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:iLine];
								CGFloat ascent, descent;
								CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
								
								CGPoint baselineOrigin = origins[iLine];
								//the view is inverted, the y origin of the baseline is upside down
                                //y 的起点是反向的,视图是颠倒的
								baselineOrigin.y = CGRectGetHeight(self.frame) - baselineOrigin.y;
								
								CGRect lineFrame = CGRectMake(baselineOrigin.x, baselineOrigin.y - ascent, lineWidth, ascent + descent);
								CGRect actualRect;
								actualRect.size.height = lineFrame.size.height;
								actualRect.origin.y = lineFrame.origin.y;
								
								CFRange range = CTLineGetStringRange(line);
								if (range.location >= startIndex) {
									//the beginning of the line is included
									actualRect.origin.x = lineFrame.origin.x;
								} else {
									actualRect.origin.x = CTLineGetOffsetForStringIndex(line, startIndex, NULL);
								}
								NSInteger lineRangEnd = range.length + range.location;
								if (lineRangEnd <= endIndex) {
									//the end of the line is included
									actualRect.size.width = CGRectGetMaxX(lineFrame) - CGRectGetMinX(actualRect);
								} else {
									CGFloat position = CTLineGetOffsetForStringIndex(line, endIndex, NULL);
									actualRect.size.width = position - CGRectGetMinX(actualRect);
								}
								actualRect = CGRectInset(actualRect, -1, 0);
								[rectsStrings addObject:NSStringFromCGRect(actualRect)];
							}
							
							*activeRects = rectsStrings;
						}
						break;
					}
				}
                
                //frame
                CFArrayRef runs = CTLineGetGlyphRuns(line);
                for(CFIndex j = 0; j < CFArrayGetCount(runs); j++) {
                    CTRunRef run = CFArrayGetValueAtIndex(runs, j);
                    NSDictionary* attributes = (__bridge NSDictionary*)CTRunGetAttributes(run);
                    
                    NSString *name = [attributes objectForKey:FTCoreTextDataName];
                    if (![name isEqualToString:@"_link"]) continue;
                    
                    //设置FTCoreTextDataAttributes
                    [returnedDict setObject:attributes forKey:FTCoreTextDataAttributes];
                    
                    CGRect runBounds;
                    runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); //8
                    runBounds.size.height = ascent + descent;
                    
                    CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); //9
                    runBounds.origin.x = baselineOrigin.x + self.frame.origin.x + xOffset + 0;
                    runBounds.origin.y = baselineOrigin.y + lineFrame.size.height - ascent;
                    
                    //设置FTCoreTextDataFrame
                    [returnedDict setObject:NSStringFromCGRect(runBounds) forKey:FTCoreTextDataFrame];
                }
            }
			if (returnedDict.count > 0) break;
		}
	}
	
	CFRelease(ctframe);
	return returnedDict;
}
</span>

<span style="color:#006600;">//触摸结束后,调用的方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	[super touchesEnded:touches withEvent:event];
	
	if (_touchedData) {
		if (self.delegate && [self.delegate respondsToSelector:@selector(coreTextView:receivedTouchOnData:)]) {
			if ([self.delegate respondsToSelector:@selector(coreTextView:receivedTouchOnData:)]) {
                
                //触摸结束时,调用协议方法
				[self.delegate <span style="background-color: rgb(255, 153, 0);">coreTextView:self receivedTouchOnData:_touchedData</span>];
			}
		}
		_touchedData = nil;
		[_selectionsViews makeObjectsPerformSelector:@selector(removeFromSuperview)];
		_selectionsViews = nil;
	}
}</span>


演示使用FTCoreText框架实现图文混排的demo,以及效果图

demo的.h文件:

<span style="color:#006600;">@interface articleViewController : UIViewController <FTCoreTextViewDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) FTCoreTextView *coreTextView;</span>

demo的.m文件:

<span style="color:#006600;">#pragma mark - View Controller Methods

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
    [super viewDidLoad];
    
	//add coretextview
    scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
	scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    coreTextView = [[FTCoreTextView alloc] initWithFrame:CGRectMake(20, 20, 280, 0)];
	coreTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    // set text
    [coreTextView setText:[self textForView]];
    // set styles
    [coreTextView addStyles:[self coreTextStyle]];
    // set delegate
    [coreTextView setDelegate:self];
	
    //设置coreTextView的高度
	[coreTextView fitToSuggestedHeight];

    [scrollView addSubview:coreTextView];
    [scrollView setContentSize:CGSizeMake(CGRectGetWidth(scrollView.bounds), CGRectGetHeight(coreTextView.frame) + 40)];
    
    [self.view addSubview:scrollView];
    
 
}<span style="font-size:14px;">
</span></span>

//取得要处理的文本数据
- (NSString *)textForView
{
    NSString *text = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"text" ofType:@"txt"] encoding:NSUTF8StringEncoding error:nil];
    
    return text;
}

//定义一组格式
- (NSArray *)coreTextStyle
{
    NSMutableArray *result = [NSMutableArray array];
    
	FTCoreTextStyle *defaultStyle = [FTCoreTextStyle new];
	defaultStyle.name = FTCoreTextTagDefault;	//thought the default name is already set to FTCoreTextTagDefault
	defaultStyle.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:16.f];
	defaultStyle.textAlignment = FTCoreTextAlignementJustified;
	[result addObject:defaultStyle];
	
	
	FTCoreTextStyle *titleStyle = [FTCoreTextStyle styleWithName:@"title"]; // using fast method
	titleStyle.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:40.f];
	titleStyle.paragraphInset = UIEdgeInsetsMake(0, 0, 25, 0);
	titleStyle.textAlignment = FTCoreTextAlignementCenter;
	[result addObject:titleStyle];
	
	FTCoreTextStyle *imageStyle = [FTCoreTextStyle new];
	imageStyle.paragraphInset = UIEdgeInsetsMake(0,0,0,0);
	imageStyle.name = FTCoreTextTagImage;
	imageStyle.textAlignment = FTCoreTextAlignementCenter;
	[result addObject:imageStyle];
	
	FTCoreTextStyle *firstLetterStyle = [FTCoreTextStyle new];
	firstLetterStyle.name = @"firstLetter";
	firstLetterStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:30.f];
	[result addObject:firstLetterStyle];
	
	FTCoreTextStyle *linkStyle = [defaultStyle copy];
	linkStyle.name = FTCoreTextTagLink;
	linkStyle.color = [UIColor orangeColor];
	[result addObject:linkStyle];
	
	FTCoreTextStyle *subtitleStyle = [FTCoreTextStyle styleWithName:@"subtitle"];
	subtitleStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:25.f];
	subtitleStyle.color = [UIColor brownColor];
	subtitleStyle.paragraphInset = UIEdgeInsetsMake(10, 0, 10, 0);
	[result addObject:subtitleStyle];
	
	FTCoreTextStyle *bulletStyle = [defaultStyle copy];
	bulletStyle.name = FTCoreTextTagBullet;
	bulletStyle.bulletFont = [UIFont fontWithName:@"TimesNewRomanPSMT" size:16.f];
	bulletStyle.bulletColor = [UIColor orangeColor];
	bulletStyle.bulletCharacter = @"❧";
	[result addObject:bulletStyle];
    
    FTCoreTextStyle *italicStyle = [defaultStyle copy];
	italicStyle.name = @"italic";
	italicStyle.underlined = YES;
    italicStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-ItalicMT" size:16.f];
	[result addObject:italicStyle];
    
    FTCoreTextStyle *boldStyle = [defaultStyle copy];
	boldStyle.name = @"bold";
    boldStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:16.f];
	[result addObject:boldStyle];
    
    FTCoreTextStyle *coloredStyle = [defaultStyle copy];
    [coloredStyle setName:@"colored"];
    [coloredStyle setColor:[UIColor redColor]];
	[result addObject:coloredStyle];
    
    return  result;
}

#pragma mark - FTCoreTextView 协议方法 点击超链接时,会调用此方法
- (void)coreTextView:(FTCoreTextView *)acoreTextView receivedTouchOnData:(NSDictionary *)data
{
    NSURL *url = [data objectForKey:FTCoreTextDataURL];
    if (!url) return;
    [[UIApplication sharedApplication] openURL:url];
}<span style="font-size: 18px;">
</span>


效果图:

          

                            效果 图1                                                                     效果 图2    

    

      

     

                                     效果图3

注:效果图1、效果图2是文本数据绘制之后显示的界面。 效果图3是点击文本中的超链接,弹出的新的界面。


CoreText的框架图(有助于理解其中的一些方法的实现):


        



























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值