前言
現在市場上大部分信息展示類應用都有用到圖文混排,我之前對這塊也是一知半解,最近放假正好深入了解一下這塊,在這里對此做一個總結,廢話不多說,先上 demo 地址
TextKit入門 demo地址
精仿手工課 demo地址
精仿手工課博客地址
背景知識
在正式開始學習之前,我們先來了解一下圖文混排在 iOS 上面的發展,拋棄 coretext (涉及到一些底層,這里不討論)不說,蘋果在 iOS6給出了*** NSMutableAttributedString這個類,簡單的說就是帶屬性的字符串,用它可以實現圖文混排,在 iOS7蘋果則給出了更加強大的 API--textKit,用 textKit***可以實現更加復雜的界面,接下來讓我們開始浪起來吧
富文本(NSMutableAttributedString)
NSMutableAttributedString和數組一樣分為可變字符串和不可變字符串, NSAttributedString就是不可變字符串
NSMutableAttributedString簡單的說就是一個帶屬性的字符串,因此它的使用非常簡單,
- 1.初始化字符串
- 2.初始化字符串所需要的屬性
- 3.將屬性賦值給字符串
初始化方法
- (instancetype)initWithString:(NSString *)str;
- (instancetype)initWithString:(NSString *)str attributes:(nullable NSDictionary<NSString *, id> *)attrs;
- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr;
初始化屬性 -
NSFontAttributeName
字體 -
NSForegroundColorAttributeName
字體顏色 -
NSBackgroundColorAttributeName
背景顏色 -
NSLigatureAttributeName
連字符
該屬性所對應的值是一個NSNumber
對象(整數)。連體字符是指某些連在一起的字符,它們采用單個的圖元符號。
0 表示沒有連體字符。
1 表示使用默認的連體字符。
2 表示使用所有連體符號。默認值為 1(注意,iOS 不支持值為 2)。 -
NSParagraphStyleAttributeName
段落
該屬性所對應的值是一個 NSParagraphStyle 對象。該屬性在一段文本上應用多個屬性。如果不指定該屬性,則默認為 NSParagraphStyle 的defaultParagraphStyle 方法返回的默認段落屬性 -
NSKernAttributeName
字間距
該屬性所對應的值是一個NSNumber
對象(整數)。字母緊排指定了用于調整字距的像素點數。字母緊排的效果依賴于字體。值為 0 表示不使用字母緊排。默認值為0 -
NSStrikethroughStyleAttributeName
刪除線 -
NSUnderlineStyleAttributeName
下劃線 -
NSStrokeColorAttributeName
邊線顏色
該屬性所對應的值是一個 UIColor 對象。如果該屬性不指定(默認),則等同于NSForegroundColorAttributeName
。否則,指定為刪除線或下劃線顏色。更多細節見“Drawing attributedstrings that are both filled and stroked”
-
NSStrokeWidthAttributeName
邊線寬度 -
NSShadowAttributeName
陰影 -
NSVerticalGlyphFormAttributeName
橫豎排版
實例
// 設置顏色等
NSMutableDictionary *arrDic = [NSMutableDictionary dictionary];
arrDic[NSForegroundColorAttributeName] = [UIColor purpleColor];
arrDic[NSBackgroundColorAttributeName] = [UIColor greenColor];
arrDic[NSKernAttributeName] = @10;
arrDic[NSUnderlineStyleAttributeName] = @1;
NSMutableAttributedString *attriOneStr = [[NSMutableAttributedString alloc]initWithString:@"來呀,快活呀,反正有大把時光" attributes:arrDic];
self.oneLabel.attributedText = attriOneStr;
// 簡單的圖文混排
NSMutableAttributedString *arrTwoStr = [[NSMutableAttributedString alloc]init];
NSMutableAttributedString *TwoChildStr = [[NSMutableAttributedString alloc]initWithString:@"你好啊"];
[arrTwoStr appendAttributedString:TwoChildStr];
NSTextAttachment *attachMent = [[NSTextAttachment alloc]init];
attachMent.image = [UIImage imageNamed:@"2"];
attachMent.bounds = CGRectMake(0, -5, 20, 20);
NSAttributedString *picStr = [NSAttributedString attributedStringWithAttachment:attachMent];
[arrTwoStr appendAttributedString:picStr];
NSAttributedString *TwooStr = [[NSAttributedString alloc]initWithString:@"我是小菜鳥"];
[arrTwoStr appendAttributedString:TwooStr];
self.twoLabel.attributedText = arrTwoStr;
效果
表情鍵盤,富文本實現圖文混排
表情鍵盤
平常大家用的 QQ,微信等APP中隨處可見表情鍵盤,在做表情鍵盤前,先來了解一下什么是表情.
日常生活中,所用到的表情一般為兩種--圖片表情,emoji表情
emoji表情的本質就是字符串,比如 0x1f601:,在顯示的時候我們需要將字符串轉成表情
圖片表情就是一張圖片,比如這是一個表情信息,我們根據png來加載緩存在本地的圖片,并顯示.
實現思路
考慮到表情的兩種表現形式,決定用 Button
來實現,這樣可以方便的顯示字體或者圖片
- 初始化一個
ListView
, 并添加一個UIScrollview
子控件 -
UIScrollview
添加n個(需要幾個表情種類添加幾個)gridView
-
gridView
上面添加Button
來顯示表情
表情鍵盤代碼###
由于代碼量較大,這里上一些核心代碼,具體代碼可以下載,下來再看TextKit入門 demo地址
表情工具類加載所需表情
+ (NSArray *)defaultEmotions
{
if (!_defaultEmotions) {
NSString *plist = [[NSBundle mainBundle] pathForResource:@"EmotionIcons/default/info.plist" ofType:nil];
_defaultEmotions = [GPEmotion mj_objectArrayWithFile:plist];
[_defaultEmotions makeObjectsPerformSelector:@selector(setDirectory:) withObject:@"EmotionIcons/default"];
}
return _defaultEmotions;
}
+ (NSArray *)emojiEmotions
{
if (!_emojiEmotions) {
NSString *plist = [[NSBundle mainBundle] pathForResource:@"EmotionIcons/emoji/info.plist" ofType:nil];
_emojiEmotions = [GPEmotion mj_objectArrayWithFile:plist];
[_emojiEmotions makeObjectsPerformSelector:@selector(setDirectory:) withObject:@"EmotionIcons/emoji"];
}
return _emojiEmotions;
}
+ (NSArray *)lxhEmotions
{
if (!_lxhEmotions) {
NSString *plist = [[NSBundle mainBundle] pathForResource:@"EmotionIcons/lxh/info.plist" ofType:nil];
_lxhEmotions = [GPEmotion mj_objectArrayWithFile:plist];
[_lxhEmotions makeObjectsPerformSelector:@selector(setDirectory:) withObject:@"EmotionIcons/lxh"];
}
return _lxhEmotions;
}
+ (NSArray *)recentEmotions
{
if (!_recentEmotions) {
_recentEmotions = [NSKeyedUnarchiver unarchiveObjectWithFile:GPRecentFilepath];
if (!_recentEmotions) {
_recentEmotions = [NSMutableArray array];
}
}
return _recentEmotions;
}
表情數據傳遞
- 點擊所需表情傳遞給 ListView
- (void)setEmtiontype:(GPEmtionType)type
{
switch (type) {
case GPEmotionTypeRecent: {
self.listView.emotions = [GPEmtionTool recentEmotions];
break;
}
case GPEmotionTypeDefault: {
self.listView.emotions = [GPEmtionTool defaultEmotions];
break;
}
case GPEmotionTypeEmoji: {
self.listView.emotions = [GPEmtionTool emojiEmotions];
break;
}
case GPEmotionTypeLxh: {
self.listView.emotions = [GPEmtionTool lxhEmotions];
break;
}
}
}
- LIstView 傳遞給GridView
- (void)setEmotions:(NSArray *)emotions
{
_emotions = emotions;
NSInteger totlas = (emotions.count + GPEmotionMaxCountPerPage - 1) / GPEmotionMaxCountPerPage;
NSInteger currrentGridViewCount = self.scrollView.subviews.count;
self.pageControl.numberOfPages = totlas;
self.pageControl.currentPage = 0;
GPEmottionGridView *gridView = nil;
for (NSInteger i = 0; i < totlas; i ++)
if (i >= currrentGridViewCount) {
gridView = [[GPEmottionGridView alloc]init];
[self.scrollView addSubview:gridView];
} else {
gridView = self.scrollView.subviews[i]
}
NSInteger loc = i * GPEmotionMaxCountPerPage;
NSInteger len = GPEmotionMaxCountPerPage;
if (loc + len > emotions.count) {
len = emotions.count - loc;
}
NSRange range = NSMakeRange(loc, len);
NSArray *gridViewemotionS = [emotions subarrayWithRange:range];
gridView.emotions = gridViewemotionS;
gridView.hidden = NO
for (NSInteger i = totlas; i<currrentGridViewCount; i++) {
GPEmottionGridView *gridView = self.scrollView.subviews[i];
gridView.hidden = YES;
[self setNeedsLayout];
self.scrollView.contentOffset = CGPointZero
- gridView 傳遞給 btn
- (void)setEmotions:(NSArray *)emotions
{
_emotions = emotions;
NSInteger count = emotions.count;
NSInteger currentEmotionViewCount = self.emotionViews.count;
for (int i = 0; i<count; i++) {
GPEmotionView *emotionView = nil;
if (i >= currentEmotionViewCount) {
emotionView = [[GPEmotionView alloc] init];
[emotionView addTarget:self action:@selector(emotionClick:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:emotionView];
[self.emotionViews addObject:emotionView];
} else {
emotionView = self.emotionViews[i];
}
// 傳遞模型數據
emotionView.emotion = emotions[i];
for NSInteger i = count; i<currentEmotionViewCount; i++)
UIButton *emotionView = self.emotionViews[i];
emotionView.hidden = YES;
```
* Btn 展示表情
-
(void)setEmotion:(GPEmotion *)emotion
{
_emotion = emotion;if (emotion.code) {
self.titleLabel.font = [UIFont systemFontOfSize:32];
[self setTitle:emotion.emoji forState:UIControlStateNormal];
[self setImage:nil forState:UIControlStateNormal];
} else { // 圖片表情
NSString *icon = [NSString stringWithFormat:@"%@/%@", emotion.directory, emotion.png];
UIImage *image = [UIImage imageNamed:icon];
[self setImage: [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]forState:UIControlStateNormal];
[self setTitle:nil forState:UIControlStateNormal];
}
}
###圖文混排實現
![Uploading Snip20160917_5_983709.png . . .]](http://upload-images.jianshu.io/upload_images/694552-193ab88b3b973702.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**實現思路**
* 獲得后臺的字符串,然后用正則表達式將字符串分割為表情和非表情兩部分,然后將其轉換為**富文本字符串**,并在匹配到超鏈接時候給其綁定一個 key, 隨后將分割后的結果按順序排好,
* 將之前轉換后的富文本字符串賦值為 `UITextView` 的`attributedText`屬性
* 點擊超鏈接,判斷當前點是否在鏈接范圍之內,若在就打開鏈接
**圖文混排核心代碼**
* 普通字符串轉換為富文本字符串
- (NSAttributedString *)creatArrtext:(NSString *)text
{ NSArray *regexResults = [GPEmtionTool regexResultsWithText:text];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
[regexResults enumerateObjectsUsingBlock:^(GPRegexResult *result, NSUInteger idx, BOOL *stop) {
GPEmotion *emotion = nil;
if (result.isEmotion) { // 表情
emotion = [GPEmtionTool emotionWithDesc:result.string];
}
if (emotion) { // 如果有表情
// 創建附件對象
GPEmotionAttachment *attach = [[GPEmotionAttachment alloc] init];
// 傳遞表情
attach.emotion = emotion;
attach.bounds = CGRectMake(0, -3, GPStatusOrginalTextFont.lineHeight, GPStatusOrginalTextFont.lineHeight);
// 將附件包裝成富文本
NSAttributedString *attachString = [NSAttributedString attributedStringWithAttachment:attach];
[attributedString appendAttributedString:attachString];
} else { // 非表情(直接拼接普通文本)
NSMutableAttributedString *substr = [[NSMutableAttributedString alloc] initWithString:result.string];
// 匹配超鏈接
NSString *httpRegex = @"http(s)?://([a-zA-Z|\\\\d]+\\\\.)+[a-zA-Z|\\\\d]+(/[a-zA-Z|\\\\d|\\\\-|\\\\+|_./?%&=]*)?";
[result.string enumerateStringsMatchedByRegex:httpRegex usingBlock:^(NSInteger captureCount, NSString *const __unsafe_unretained *capturedStrings, const NSRange *capturedRanges, volatile BOOL *const stop) {
[substr addAttribute:NSForegroundColorAttributeName value:[UIColor greenColor] range:*capturedRanges];
// 綁定一個key
[substr addAttribute:GPLinkText value:*capturedStrings range:*capturedRanges];
}];
[attributedString appendAttributedString:substr];
}
}];
// 設置字體
[attributedString addAttribute:NSFontAttributeName value:GPStatusOrginalTextFont range:NSMakeRange(0, attributedString.length)];
return attributedString;
}
* 點擊鏈接跳轉
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:touch.view];
CGPoint pp = [self convertPoint:point toView:self.textView];
GPLink *touchingLink = [self touchingLinkWithPoint:pp];
if (touchingLink) {
[[NSNotificationCenter defaultCenter] postNotificationName:GPLinkDidSelectedNotification object:nil userInfo:@{GPLinkText : touchingLink.text}];
}
[self touchesCancelled:touches withEvent:event];
}
- (GPLink *)touchingLinkWithPoint:(CGPoint)point
{
__block GPLink *touchingLink = nil;
[self.links enumerateObjectsUsingBlock:^(GPLink *link, NSUInteger idx, BOOL *stop) {
for (UITextSelectionRect *selectionRect in link.rects) {
if (CGRectContainsPoint(selectionRect.rect, point)) {
NSLog(@"選中%@",NSStringFromCGRect(selectionRect.rect));
touchingLink = link;
break;
}
}
}];
return touchingLink;
}
# TextKit

接下來,我們來看今天的重頭戲` TextKit`,首先來看看`textKit` 的類
* `NSTextStorage`: 平時用到的字符串 **string**,這個類里面包含著 string的屬性,比如顏色,大小等都是這個類來管理
* `NSLayoutManager`: 這個就是管理中心,負責布局渲染
* `NSTextContainer` : 就是可以渲染的范圍
* `UITextView` :就是我們平時用的控件,用來給用戶展示數據
那么,我么可以用這些類來做哪些事情呢,看一些實例
### 閱讀排版

**閱讀排版實現思路**
* 創建n 個 `textView`,共用一套`NSLayoutManager`
**閱讀排版核心代碼**
-
(void)setupTextKit
{
NSURL *contentUrl = [[NSBundle mainBundle]URLForResource:@"content" withExtension:@"txt"];self.textStorage = [[NSTextStorage alloc]initWithFileURL:contentUrl options:[NSDictionary dictionary] documentAttributes:nil error:nil];
self.layoutManager = [[NSLayoutManager alloc]init];[self.textStorage addLayoutManager:self.layoutManager];
} -
(void)layoutconter
{
NSInteger totlaGlyph = 0;
CGFloat X = 0;
while (totlaGlyph < self.layoutManager.numberOfGlyphs) {NSTextContainer *textContainer = [[NSTextContainer alloc]initWithSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT)]; [self.layoutManager addTextContainer:textContainer]; UITextView *textView = [[UITextView alloc]initWithFrame:CGRectMake(X, 0, SCREEN_WIDTH, self.pageScrollView.height) textContainer:textContainer]; textView.scrollEnabled = NO; textView.font = [UIFont systemFontOfSize:20]; [self.pageScrollView addSubview:textView]; X += SCREEN_WIDTH; totlaGlyph = NSMaxRange([self.layoutManager glyphRangeForTextContainer:textContainer]);
}
CGSize contentSize = CGSizeMake(X, self.pageScrollView.height);
self.pageScrollView.pagingEnabled = YES;
self.pageScrollView.contentSize = contentSize;
self.pageScrollView.showsHorizontalScrollIndicator = NO;
}
### 高亮文字,URL, 保持 URL 在一行是一個整體
**高亮文字,URL實現思路**
* 可以自定義`NSTextStorage`,這里注意`NSTextStorage`繼承自`NSMutableAttributedString`,所以必須實現以下方法:
` - (NSString *)string;`
` - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range;`
` - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;`
` - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range;`
* 在`- (void)processEditing`中匹配高亮字符,匹配 URL, 并高亮
* 在 `NSLayoutManagerDelegate`實現 url 保持整體不換行
**高亮文字,URL核心代碼**
-
(void)processEditing
{NSRegularExpression *expression = [[NSRegularExpression alloc]initWithPattern:@"a[\\b{Alphabetic}&&\\b{Uppercase}][\\br{Alphabetic}]+" options:NSRegularExpressionCaseInsensitive error:nil];
NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
[self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];[expression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
[self addAttribute:NSForegroundColorAttributeName value:[UIColor greenColor] range:result.range];
}];
[super processEditing];
}
-
(void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
[_imp replaceCharactersInRange:range withString:str];
[self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length];NSDataDetector *datector = [[NSDataDetector alloc]initWithTypes:NSTextCheckingTypeLink error:nil];
NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)];
[self removeAttribute:NSLinkAttributeName range:paragaphRange];
[self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];[datector enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
[self addAttribute:NSLinkAttributeName value:result.URL range:result.range]; [self addAttribute:NSForegroundColorAttributeName value:[UIColor greenColor] range:result.range];
}];
}
-
(BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex
{
NSRange range;
NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range];if (linkURL && charIndex > range.location && charIndex <= NSMaxRange(range))
return NO;
else
return YES;
}
**機智的你一定發現,其實利用 TextKit也可以實現圖文混排,機智的你自己試一試**
###環繞布局

如圖所示;大家用 Word 的時候插入圖片,使得文字環繞在圖片周圍,哈哈,好消息來了, iOS 也可以實現哦
**環繞布局實現思路**
* 記得`textContainer`,我們可以直接給其屬性賦值一個` path`, 就相當于在一張紙上裁剪掉一部分,那么自然就不會在那部分渲染文字了
**環繞布局核心代碼**
-
(void)setupImage
{UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"5"]];
imageView.center = self.view.center;
CGRect ofram = [self.textView convertRect:imageView.bounds fromView:imageView];
ofram.origin.y = ofram.origin.y - 64;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:ofram];self.textView.textContainer.exclusionPaths = @[path];
[self.view addSubview:imageView];
}
#總結
這只是一些簡單的使用,機智的你肯定有更多想法,互相學習吧
>參考
[obJc,初始 textKit](https://objccn.io/issue-5-1/)
[MJ](https://github.com/CoderMJLee/MJExtension)