分享一个菜鸟级别的FMTextView 图文排版,支持两端对齐,自定义行高等
2014-01-31 10:23
609 查看
春晚又顺利的度过了,就在今天我才好不容易把UITableView的图文排版搞定,过着新年,顺便也写写东西,纪录一点成长。
通过阅读别人的代码,发现有些富文本插件好牛,也好复杂,当然直接拿来用也可以的,但是,自己总想着能自己动手也写一个出来,哪怕再简单的也行,否则拿来主义会毁了这一代的。。。。,要进步就要自己动手思考,慢慢发掘其中的真理。
所以我也自定义了个UITextView, 就先命名为 FMTextView吧。用法跟UITextView一样,只是有点区别就是 从init开始初始化吧,设置属性要在.text方法的前面
支持: 5.1 ~ 6.0 ~ 7.0 (6.0以下的机子我采用了coretext方式DrawRect重绘,6.0以上的则采用UITextView重写,所以6.0以上的还支持原生态的复杂,全选等操作)
思路: 思路也很简单,就是用coretext方式重绘一遍,知识点:CGContextRef ,当然这是6.0以下的机子才会用到,6.0以上就不用这种方法了,就采用属性字符串的方式,暂且这样叫吧,知识点就是 UITextView 的 attributedText 方法,接收一个属性字符串
NSMutableAttributedString ,这样文本就可以自定义很多属性,如两端对齐,行高等。
实例用法:
内容的格式有点限制就是里面的图片要用自定义的标签:如 “内容内容”
代码如下:
声明部分:FMTextView.h
实现部分:FMTextView.m (这里用到了一个图片异步加载的文件 UIImage+asyncLoad.h 用的时候可以去掉)
预览效果如下:
这都是没有声明复杂代码的实现过程,懂的朋友可以自己优化下,欢迎交流。
通过阅读别人的代码,发现有些富文本插件好牛,也好复杂,当然直接拿来用也可以的,但是,自己总想着能自己动手也写一个出来,哪怕再简单的也行,否则拿来主义会毁了这一代的。。。。,要进步就要自己动手思考,慢慢发掘其中的真理。
所以我也自定义了个UITextView, 就先命名为 FMTextView吧。用法跟UITextView一样,只是有点区别就是 从init开始初始化吧,设置属性要在.text方法的前面
支持: 5.1 ~ 6.0 ~ 7.0 (6.0以下的机子我采用了coretext方式DrawRect重绘,6.0以上的则采用UITextView重写,所以6.0以上的还支持原生态的复杂,全选等操作)
思路: 思路也很简单,就是用coretext方式重绘一遍,知识点:CGContextRef ,当然这是6.0以下的机子才会用到,6.0以上就不用这种方法了,就采用属性字符串的方式,暂且这样叫吧,知识点就是 UITextView 的 attributedText 方法,接收一个属性字符串
NSMutableAttributedString ,这样文本就可以自定义很多属性,如两端对齐,行高等。
实例用法:
内容的格式有点限制就是里面的图片要用自定义的标签:如 “内容内容”
FMTextView *fmText = [[FMTextView alloc] init]; fmText.frame = CGRectMake(15, 80, 290, 1000); fmText.Font = [UIFont fontWithName:@"Heiti SC" size:18]; fmText.text = _content;
代码如下:
声明部分:FMTextView.h
#import <UIKit/UIKit.h> @interface FMTextView : UITextView @property (nonatomic,assign) CGSize fmDefaultImageSize; // 图片默认大小 @property (nonatomic,retain) NSMutableDictionary *fmImages;// 对外暴露内容中的图片字典 @property (nonatomic,assign) BOOL isStrHeight;// 是否计算文本高度,用来防止死循环 @property (nonatomic,assign) CGFloat lineHeight;// 行高 @property (nonatomic,assign) CGPoint currentPoint;// 在循环绘制中当期的坐标值,一般用来指定下一个坐标的开始值 @end
实现部分:FMTextView.m (这里用到了一个图片异步加载的文件 UIImage+asyncLoad.h 用的时候可以去掉)
#import "FMTextView.h" #import "UIImage+asyncLoad.h" #import <CoreText/CoreText.h> #define imageStartTag @"" // 图片结束标签 #define imageType @".jpg,.png,.bmp,.gif"; @interface FMTextView(){ NSString* _fmString; // 内容 NSMutableArray *_fmArray;//根据图片切割后存储所用到的数组 NSMutableArray *_fmIsImage;// 对应的分割字符串是否是图片 NSMutableString *_newString ;// 重组后的内容 // 用于IOS6.0以下 CGContextRef _context; CGFloat _fmHeight; } @end @implementation FMTextView - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { } return self; } /* // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. - (void)drawRect:(CGRect)rect { // Drawing code } */ -(id)init{ self = [super init]; if (self) { self.fmDefaultImageSize = CGSizeMake(self.frame.size.width,180); self.fmImages = [[NSMutableDictionary alloc] init]; _newString = [[NSMutableString alloc] init]; self.scrollEnabled = NO; self.showsHorizontalScrollIndicator = NO; self.showsVerticalScrollIndicator = NO; self.bounces = NO; self.editable = NO; self.isStrHeight = YES; self.lineHeight = 10;// 默认行高为3; self.currentPoint = CGPointMake(0, 0); // 图片y轴的定位,默认为0; self.contentInset = UIEdgeInsetsZero; self.backgroundColor = [UIColor clearColor]; _fmHeight = 0; } return self; } -(void)setText:(NSString *)text{ _fmString = text; [self splitString]; if ([[[UIDevice currentDevice]systemVersion]floatValue]>=6){ // 设置行高 self.lineHeight = self.font.pointSize+self.lineHeight; // 开始画图 6_0 7_0 for (NSString *item in _fmArray) { BOOL isImage = [[_fmIsImage objectAtIndex:[_fmArray indexOfObject:item]] floatValue]; // 如果是6.0就用原生态text来显示 [self characterAttribute:item andIsImage:isImage]; } // 设置对齐方式和行高等 NSMutableParagraphStyle *textViewparagraphStyle = [[NSMutableParagraphStyle alloc] init]; textViewparagraphStyle.lineHeightMultiple = self.lineHeight; textViewparagraphStyle.maximumLineHeight = self.lineHeight; textViewparagraphStyle.minimumLineHeight = self.lineHeight; textViewparagraphStyle.lineBreakMode = kCTParagraphStyleSpecifierAlignment; // 对齐方式 CTTextAlignment ctAlignment = kCTJustifiedTextAlignment; NSTextAlignment lineAlignment = NSTextAlignmentFromCTTextAlignment(ctAlignment); textViewparagraphStyle.alignment = lineAlignment; // 组装属性为字典 NSDictionary *attribute = [[NSDictionary alloc] initWithObjectsAndKeys: self.font, NSFontAttributeName, textViewparagraphStyle, NSParagraphStyleAttributeName, nil]; // 设置文本属性 self.attributedText = [[NSMutableAttributedString alloc] initWithString:_newString attributes:attribute]; super.text = _newString; [self sizeToFit]; }else{ // 计算drawview的高度 5_0 for (NSString *item in _fmArray) { BOOL isImage = [[_fmIsImage objectAtIndex:[_fmArray indexOfObject:item]] floatValue]; [self characterHeight:item andIsImage:isImage]; } // 设置view的新frame self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, _fmHeight+20); _fmDefaultImageSize.width = self.frame.size.width; [self setNeedsDisplay];// 促发DrawRect事件 } } // 切割字符串 并返回以图片分割后的新数组 -(void)splitString{ // 先通过图片开始标签分割 NSArray *tempArray = [_fmString componentsSeparatedByString:imageStartTag]; _fmArray = [[NSMutableArray alloc] init]; // 重组数组 _fmIsImage = [[NSMutableArray alloc] init]; // 根据图片结束标签重组数组 for (NSString *itemStr in tempArray) { NSArray *endTagArray = [itemStr componentsSeparatedByString:imageEndTag]; int i = 0; for (NSString *endTagStr in endTagArray) { //NSLog(@"%@",endTagStr); [_fmArray addObject:endTagStr]; // 重新组装新数组,自此则得到了以分割完图片后的新数组,图片单独在一个下标中 if ([itemStr rangeOfString:imageEndTag].length>0 && i==0) { [_fmIsImage addObject:[NSNumber numberWithBool:YES]]; }else{ [_fmIsImage addObject:[NSNumber numberWithBool:NO]]; } i++; } endTagArray = Nil; } } -(void)characterAttribute:(NSString*)fmString andIsImage:(BOOL)isImage { NSString *str = isImage?@"":fmString; if (_fmDefaultImageSize.width<=0){ self.fmDefaultImageSize = CGSizeMake(self.frame.size.width,self.fmDefaultImageSize.height); } if (isImage) { // 创建一张图片 UIImage *tempImage = [UIImage imageNamed:@"nophoto.png"]; UIImageView *tempImageView = [[UIImageView alloc] initWithImage:tempImage]; tempImageView.layer.masksToBounds = YES; tempImageView.layer.cornerRadius = 3; tempImageView.frame = CGRectMake(0, self.currentPoint.y, _fmDefaultImageSize.width, _fmDefaultImageSize.height); tempImageView.userInteractionEnabled = YES; [[UIImage alloc] asyncLoad:[NSURL URLWithString:fmString] andImageView:tempImageView]; // 把imageView加入字典 [self.fmImages setObject:tempImageView forKey:fmString]; // 给图片添加点击事件 UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openImages:)]; [tempImageView addGestureRecognizer:tapGesture]; [self addSubview:tempImageView]; // 插入空行给图片预留空间 [self insertBlankLines]; }else{ [_newString appendString:str]; } if (self.isStrHeight){ [self performSelector:@selector(stringHeight) onThread:[NSThread mainThread] withObject:Nil waitUntilDone:YES]; } } // 插入几个换行,用来给图片预留空间 -(void)insertBlankLines{ int height = _fmDefaultImageSize.height; int lowheight = self.lineHeight; int lines = height/lowheight; lines++; for(int i=1;i<=lines;i++){ [_newString appendString:@"\r\n"]; } } // 返回字符串创建的文本高度 -(void)stringHeight{ self.currentPoint = CGPointMake(0, 0); if (self.isStrHeight && _newString!=NULL){ FMTextView *tempTextView = [[FMTextView alloc] init]; tempTextView.frame = self.frame; tempTextView.font = self.font; tempTextView.isStrHeight = NO; tempTextView.text = _newString; CGFloat tempHeight = tempTextView.frame.size.height; tempTextView = Nil; self.currentPoint = CGPointMake(0, tempHeight); } } -(void)openImages:(UITapGestureRecognizer*)tapGesture{ UIImageView *tapImageView = (UIImageView*)tapGesture.view; NSString *imageSrc; for (id item in self.fmImages.keyEnumerator) { UIImageView *tempImageView = (UIImageView*)[self.fmImages objectForKey:item]; if (tempImageView==tapImageView) { imageSrc = item; break; } } tapImageView.backgroundColor = [UIColor blackColor]; tapImageView.alpha = 0.6; [UIView animateWithDuration:0.5 animations:^{ tapImageView.backgroundColor = [UIColor whiteColor]; tapImageView.alpha = 1; }]; //NSLog(@"点击图片%@",imageSrc); } /* ---------------------------------------------------------------------------------------------------------- 以下是5.0的drawRect方式 */ -(void)drawRect:(CGRect)rect{ if ([[[UIDevice currentDevice]systemVersion]floatValue]<6){ //获取当前(View)上下文以便于之后的绘画,这个是一个离屏。 _context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(_context , CGAffineTransformIdentity); //压栈,压入图形状态栈中.每个图形上下文维护一个图形状态栈,并不是所有的当前绘画环境的图形状态的元素都被保存。图形状态中不考虑当前路径,所以不保存 //保存现在得上下文图形状态。不管后续对context上绘制什么都不会影响真正得屏幕。 CGContextSaveGState(_context); //x,y轴方向移动 CGContextTranslateCTM(_context , 0 ,self.bounds.size.height); //缩放x,y轴方向缩放,-1.0为反向1.0倍,坐标系转换,沿x轴翻转180度 CGContextScaleCTM(_context, 1.0 ,-1.0); // 开始画图 _currentPoint = CGPointMake(0, 0); for (NSString *item in _fmArray) { BOOL isImage = [[_fmIsImage objectAtIndex:[_fmArray indexOfObject:item]] floatValue]; // 如果是6.0以下的机子则用重绘来显示 [self characterAttribute_5:item andIsImage:isImage]; } } } // 计算高度 -(void)characterHeight:(NSString*)fmString andIsImage:(BOOL)isImage{ NSString *str = isImage?@"":fmString; NSMutableAttributedString *mabstring = [self returnAttributeString:str]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabstring); //计算文本绘制size CGSize tmpSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(self.bounds.size.width-self.contentInset.left-self.contentInset.right, 400 * 10), NULL); // 更新下一坐标值 CGFloat newY = tmpSize.height; if (isImage) { _currentPoint = CGPointMake(0, _currentPoint.y+_fmDefaultImageSize.height+newY); }else{ _currentPoint = CGPointMake(0, _currentPoint.y+newY); } _fmHeight = _currentPoint.y; CFRelease(framesetter); } // 返回格式化后的字符串 -(NSMutableAttributedString*)returnAttributeString:(NSString*)string{ NSMutableAttributedString *mabstring = [[NSMutableAttributedString alloc]initWithString:string]; [mabstring beginEditing]; //段落 //line break CTParagraphStyleSetting lineBreakMode; CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping; //换行模式 lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode; lineBreakMode.value = &lineBreak; lineBreakMode.valueSize = sizeof(CTLineBreakMode); //行间距 // 设置行高 CTParagraphStyleSetting LineSpacing; CGFloat spacing = self.lineHeight; //指定间距 LineSpacing.spec = kCTParagraphStyleSpecifierLineSpacingAdjustment; LineSpacing.value = &spacing; LineSpacing.valueSize = sizeof(CGFloat); CTParagraphStyleSetting settings[] = {lineBreakMode,LineSpacing}; CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, 2); //第二个参数为settings的长度 [mabstring addAttribute:(NSString *)kCTParagraphStyleAttributeName value:(__bridge id)paragraphStyle range:NSMakeRange(0, string.length)]; //对同一段字体进行多属性设置 NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithObject:(id)self.textColor forKey:(id)kCTForegroundColorAttributeName]; CTFontRef font = CTFontCreateWithName((CFStringRef)@"Heiti SC", self.font.pointSize, NULL); [attributes setObject:(__bridge id)font forKey:(id)kCTFontAttributeName]; [mabstring addAttributes:attributes range:NSMakeRange(0, string.length)]; [mabstring endEditing]; return mabstring; } -(void)characterAttribute_5:(NSString*)fmString andIsImage:(BOOL)isImage{ NSString *str = isImage?@"":fmString; // 如果是6.0以下的机子就用Drawrect来画 NSMutableAttributedString *mabstring = [self returnAttributeString:str]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabstring); CGMutablePathRef Path = CGPathCreateMutable(); CGPathAddRect(Path, NULL ,CGRectMake(self.contentInset.left , -_currentPoint.y ,self.bounds.size.width-self.contentInset.left-self.contentInset.right , self.bounds.size.height)); //计算文本绘制size CGSize tmpSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(self.bounds.size.width-self.contentInset.left-self.contentInset.right, 400 * 10), NULL); // 更新下一坐标值 CGFloat newY = tmpSize.height; if (isImage) { // 创建一张图片 UIImage *tempImage = [UIImage imageNamed:@"nophoto.png"]; UIImageView *tempImageView = [[UIImageView alloc] initWithImage:tempImage]; tempImageView.layer.masksToBounds = YES; tempImageView.layer.cornerRadius = 3; tempImageView.frame = CGRectMake(self.contentInset.left, self.currentPoint.y, _fmDefaultImageSize.width, _fmDefaultImageSize.height); tempImageView.userInteractionEnabled = YES; [[UIImage alloc] asyncLoad:[NSURL URLWithString:fmString] andImageView:tempImageView]; // 把imageView加入字典 [self.fmImages setObject:tempImageView forKey:fmString]; // 给图片添加点击事件 UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openImages:)]; [tempImageView addGestureRecognizer:tapGesture]; [self addSubview:tempImageView]; _currentPoint = CGPointMake(0, _currentPoint.y+_fmDefaultImageSize.height+newY); }else{ _currentPoint = CGPointMake(0, _currentPoint.y+newY); } CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), Path, NULL); // 画文本 CTFrameDraw(frame,_context); CGPathRelease(Path); CFRelease(framesetter); } @end
预览效果如下:
这都是没有声明复杂代码的实现过程,懂的朋友可以自己优化下,欢迎交流。
相关文章推荐
- Android 自定义TextView 实现文字对齐排版且支持点击划词
- android 自定义textView,实现排版对齐和换行
- Android 自定义Textview实现文字两端对齐功能和长按自由选择文字弹出自定义ActionMenu功能(一)
- Android 自定义Textview实现文字两端对齐功能和长按自由选择文字弹出自定义ActionMenu功能(二)自定义ActionMenu
- 关于自定义TextView排版分散对齐的思路
- 自定义TextView使上下文字两端对齐
- textview 文字排版问题,实现两端对齐
- 实现TextView 文字排版,分散两端对齐
- 自定义View--文字两端能够对齐的TextView,文字右端能够对齐的TextView
- 实现TextView 文字排版,分散两端对齐
- Android自定义View分享——一个圆形温度显示器
- LinearLayout中的控件两端对齐 或 Button、TextView左右对齐
- Android 自定义View 解决 TextView 自动换行排版不整齐
- Android TextView两端对齐
- android TextView 支持自定义字体和属性
- 自定义支持圆角的TextView
- 富文本(TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText),支持图文混排显示,支持添加链接,image和UIView控件,支持自定义排版显示)
- Android--仿1号店继续拖动查看图文详情——一个自定义的ViewGroup
- 分享一个textView右上角的添加小红点的 tips
- Android TextView两端对齐、文本两端对齐