CoreText進(jìn)階(七)-添加自定義View和對其

本文講的主要內(nèi)容是如何將CoreText繪圖和自定義的View結(jié)合在一起擎值,進(jìn)行無縫的排版,并且可以控制自定義View元素的對其方式(頂部對其鸠儿、底部對其进每、居中對其)

其它文章:
CoreText 入門(一)-文本繪制
CoreText入門(二)-繪制圖片
CoreText進(jìn)階(三)-事件處理
CoreText進(jìn)階(四)-文字行數(shù)限制和顯示更多
CoreText進(jìn)階(五)- 文字排版樣式和效果
CoreText進(jìn)階(六)-內(nèi)容大小計算和自動布局
CoreText進(jìn)階(七)-添加自定義View和對其

效果

Demo:CoreTextDemo

效果


實現(xiàn)代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.edgesForExtendedLayout = UIRectEdgeNone;
    self.view.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1];
    
    CGRect frame = CGRectMake(0, 100, self.view.bounds.size.width, 400);
    YTDrawView *textDrawView = [[YTDrawView alloc] initWithFrame:frame];
    textDrawView.backgroundColor = [UIColor whiteColor];
    
    // 添加普通的文本
    [textDrawView addString:@"Hello World " attributes:self.defaultTextAttributes clickActionHandler:^(id obj) {
        
    }];
    
    // 添加鏈接
    [textDrawView addLink:@"http://www.baidu.com" clickActionHandler:^(id obj) {
        UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"鏈接點擊" message:[NSString stringWithFormat:@"點擊對象%@", obj] preferredStyle:(UIAlertControllerStyleAlert)];
        [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:(UIAlertActionStyleCancel) handler:nil]];
        [self presentViewController:alert animated:YES completion:nil];
    }];
    
    // 添加圖片
    [textDrawView addImage:[UIImage imageNamed:@"tata_img_hottopicdefault"] size:CGSizeMake(30, 30) clickActionHandler:^(id obj) {
        UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"圖片點擊" message:[NSString stringWithFormat:@"點擊對象%@", obj] preferredStyle:(UIAlertControllerStyleAlert)];
        [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:(UIAlertActionStyleCancel) handler:nil]];
        [self presentViewController:alert animated:YES completion:nil];
    }];
    
    // 添加鏈接
    [textDrawView addLink:@"http://www.baidu.com" clickActionHandler:^(id obj) {
        
    }];
    
    // 添加普通的文本
    [textDrawView addString:@"這是一個最好的時代品追,也是一個最壞的時代;" attributes:self.defaultTextAttributes clickActionHandler:^(id obj) {
        
    }];
    
    // 添加鏈接
    [textDrawView addLink:@" 這是明智的時代遭京,這是愚昧的時代泞莉;這是信任的紀(jì)元,這是懷疑的紀(jì)元斯嚎;這是光明的季節(jié),這是黑暗的季節(jié)糠惫;這是希望的春日钉疫,這是失望的冬日; " clickActionHandler:^(id obj) {
        
    }];
    
    // 添加自定義的View固阁,默認(rèn)是底部對其
    UIView* customView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 160, 50)];
    customView.backgroundColor = [UIColor colorWithRed:1 green:0.7 blue:1 alpha:0.51];
    [customView bk_whenTapped:^{
        NSLog(@"customView Tapped");
    }];
    UILabel *labelInCustomView = [UILabel new];
    labelInCustomView.textAlignment = NSTextAlignmentCenter;
    labelInCustomView.font = [UIFont systemFontOfSize:12];
    labelInCustomView.text = @"可點擊的自定義的View";
    [customView addSubview:labelInCustomView];
    [labelInCustomView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(customView);
    }];
    [textDrawView addView:customView size:customView.frame.size clickActionHandler:nil];
    
    // 添加普通的文本
    [textDrawView addString:@" Hello " attributes:self.defaultTextAttributes clickActionHandler:nil];

    
    // 添加居中對其的自定義的View
    UIView *unClickableCustomView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 160, 50)];
    unClickableCustomView.backgroundColor = [UIColor colorWithRed:1 green:0.7 blue:1 alpha:0.51];
    UILabel *labelInUnClickableCustomView = [UILabel new];
    labelInUnClickableCustomView.textAlignment = NSTextAlignmentCenter;
    labelInUnClickableCustomView.font = [UIFont systemFontOfSize:12];
    labelInUnClickableCustomView.text = @"居中對其自定義的View";
    [unClickableCustomView addSubview:labelInUnClickableCustomView];
    [labelInUnClickableCustomView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(unClickableCustomView);
    }];
    [textDrawView addView:unClickableCustomView size:unClickableCustomView.frame.size align:(YTAttachmentAlignTypeCenter) clickActionHandler:nil];

    // 添加普通的文本
    [textDrawView addString:@" 我們面前應(yīng)有盡有备燃,我們面前一無所有凌唬; " attributes:self.defaultTextAttributes clickActionHandler:nil];
    
    // 添加自定義的按鈕法瑟,默認(rèn)是底部對其
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(0, 0, 80, 30);
    [button setTitle:@"我是按鈕" forState:UIControlStateNormal];
    button.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1];
    [button bk_addEventHandler:^(id sender) {
        NSLog(@"button Clicked");
    } forControlEvents:UIControlEventTouchUpInside];
    [textDrawView addView:button size:button.frame.size clickActionHandler:nil];
    
    [textDrawView addString:@" " attributes:self.defaultTextAttributes clickActionHandler:nil];
    
    // 添加頂部對其按鈕
    button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(0, 0, 90, 30);
    [button setTitle:@"頂部對其按鈕" forState:UIControlStateNormal];
    button.titleLabel.font = [UIFont systemFontOfSize:14];
    button.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1];
    [button bk_addEventHandler:^(id sender) {
        NSLog(@"button Clicked");
    } forControlEvents:UIControlEventTouchUpInside];
    [textDrawView addView:button size:button.frame.size align:(YTAttachmentAlignTypeTop) clickActionHandler:nil];
    
    // 添加普通的文本
    [textDrawView addString:@" 我們都將直上天堂霎挟,我們都將直下地獄。 " attributes:self.defaultTextAttributes clickActionHandler:nil];
    
    [self.view addSubview:textDrawView];
    self.textDrawView = textDrawView;
}

添加View

添加View其實和添加圖片的處理方式很類似赐纱,只不過添加圖片我們是使用CG繪圖的方式把圖片繪制在View上熬北,而添加View是使用UIkit的方法addSubview把View添加到View的層級上,這里有個稍微有個需要注意的地方就是坐標(biāo)的問題起胰,UI坐標(biāo)系和CG坐標(biāo)系的顛倒的巫延,需要做個額外的處理

首先定義一個添加View的方法,在該方法中主要是進(jìn)行數(shù)據(jù)模型的保存以及生產(chǎn)特殊的占位屬性字符串炉峰,然后添加屬性字符串的RunDelegate

- (void)addView:(UIView *)view size:(CGSize)size align:(YTAttachmentAlignType)align clickActionHandler:(ClickActionHandler)clickActionHandler {
    YTAttachmentItem *imageItem = [YTAttachmentItem new];
    [self updateAttachment:imageItem withFont:self.font];
    imageItem.align = align;
    imageItem.attachment = view;
    imageItem.type = YTAttachmentTypeView;
    imageItem.size = size;
    imageItem.clickActionHandler = clickActionHandler;
    [self.attachments addObject:imageItem];
    NSAttributedString *imageAttributeString = [self attachmentAttributeStringWithAttachmentItem:imageItem size:size];
    [self.attributeString appendAttributedString:imageAttributeString];
}

設(shè)置占位屬性字符串的方法和添加圖片時候使用到的是一樣的代碼

- (NSAttributedString *)attachmentAttributeStringWithAttachmentItem:(YTAttachmentItem *)attachmentItem size:(CGSize)size {
    // 創(chuàng)建CTRunDelegateCallbacks
    CTRunDelegateCallbacks callback;
    memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
    callback.getAscent = getAscent;
    callback.getDescent = getDescent;
    callback.getWidth = getWidth;
    
    // 創(chuàng)建CTRunDelegateRef
//    NSDictionary *metaData = @{YTRunMetaData: attachmentItem};
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge void * _Nullable)(attachmentItem));
    
    // 設(shè)置占位使用的圖片屬性字符串
    // 參考:https://en.wikipedia.org/wiki/Specials_(Unicode_block)  U+FFFC  OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
    unichar objectReplacementChar = 0xFFFC;
    NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
    
    // 設(shè)置RunDelegate代理
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    
    // 設(shè)置附加數(shù)據(jù)戒劫,設(shè)置點擊效果
    NSDictionary *extraData = @{YTExtraDataAttributeTypeKey: attachmentItem.type == YTAttachmentTypeImage ? @(YTDataTypeImage) : @(YTDataTypeView),
                                YTExtraDataAttributeDataKey: attachmentItem,
                                };
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), (CFStringRef)YTExtraDataAttributeName, (__bridge CFTypeRef)(extraData));
    
    CFRelease(runDelegate);
    return imagePlaceHolderAttributeString;
}

接下來就是需要計算添加的View所在父View中的位置,進(jìn)行相應(yīng)的保存巫橄,這里需要注意的是坐標(biāo)系的問題疯攒,需要做一個額外的轉(zhuǎn)換

- (void)calculateContentPositionWithBounds:(CGRect)bounds {
    
    int imageIndex = 0;
    
    // CTFrameGetLines獲取但CTFrame內(nèi)容的行數(shù)
    NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
    // CTFrameGetLineOrigins獲取每一行的起始點,保存在lineOrigins數(shù)組中
    CGPoint lineOrigins[lines.count];
    CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
    for (int i = 0; i < lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        
        NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
        for (int j = 0; j < runs.count; j++) {
            CTRunRef run = (__bridge CTRunRef)(runs[j]);
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            if (!attributes) {
                continue;
            }
            // ..... 部分代碼省略
            
            // 找到代理則開始計算圖片位置信息
            CGFloat ascent;
            CGFloat desent;
            // 可以直接從metaData獲取到圖片的寬度和高度信息
            CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
            CGFloat height = ascent + desent;
            
            // CTLineGetOffsetForStringIndex獲取CTRun的起始位置
            CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            CGFloat yOffset = lineOrigins[i].y;
            
            // 更新ImageItem對象的位置
            if (imageIndex < self.attachments.count) {
                YTAttachmentItem *imageItem = self.attachments[imageIndex];
                // 使用CG繪圖的位置不用矯正贴浙,使用UI繪圖的坐標(biāo)Y軸會上下顛倒,所以需要做調(diào)整
                if (imageItem.type == YTAttachmentTypeView) {
                    yOffset = bounds.size.height - lineOrigins[i].y - ascent;
                } else if (imageItem.type == YTAttachmentTypeImage) {
                    yOffset = yOffset - desent;
                }
                imageItem.frame = CGRectMake(xOffset, yOffset, width, height);
                imageIndex ++;
            }
        }
    }
}

對其實現(xiàn)

處理對其方式之前要了解字形度量的一些概念崎溃,然后在此基礎(chǔ)上進(jìn)行分析不同的對其方式下需要如何正確的設(shè)置排版的參數(shù)袁串,才能渲染繪制出理想中內(nèi)容

字形度量的一些概念

下面的這張圖片來自蘋果官方的參考文檔:Typographical Concepts

字形度量

字形度量中的幾個概念的說明參考 使用CoreText繪制文本 的是內(nèi)容如下

bounding box(邊界框),這是一個假想的框子赎瑰,它盡可能緊密的裝入字形破镰。
baseline(基線),一條假想的線,一行上的字形都以此線作為上下位置的參考鲜漩,在這條線的左側(cè)存在一個點叫做基線的原點。
ascent(上行高度)踩娘,從原點到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離喉祭,ascent是一個正值。
descent(下行高度)厚脉,從原點到字體中最深的字形底部的距離胶惰,descent是一個負(fù)值(比如一個字體原點到最深的字形的底部的距離為2,那么descent就為-2)中捆。

三種對其方式的分析

以下對其方式的分析都是以下面的這些數(shù)據(jù)為標(biāo)準(zhǔn)的

Font.fontAscent = 33.75.   
Font.fontDescent = 27.04. 
LineHeight = Font.fontAscent + Font.fontDescent = 60.8. 

頂部對其.

頂部對其

頂部對其,需要設(shè)置ascent值為文字內(nèi)容的ascent殴蓬,descent值為attachmen的高度減去ascent蟋滴,如下圖所示(圖片上的標(biāo)注是2x,并且數(shù)值因為是手動使用工具標(biāo)注津函,會有一些細(xì)微的偏差),內(nèi)容的高度為40涩馆,所以有:

  • ascent= Font.fontAscent = 33.75.
  • descent = 40 - ascent = 6.25.
ascent = 33.75. 
descent = 6.25. 
height = ascent + descent = 40. 
baseline = 33.75. 

底部對其

底部對其

底部對其魂那,需要設(shè)置descent值為文字內(nèi)容的descent稠项,ascent值為attachmen的高度減去ascent,如下圖所示(圖片上的標(biāo)注是2x皿渗,并且數(shù)值因為是手動使用工具標(biāo)注,會有一些細(xì)微的偏差)划乖,內(nèi)容的高度為40挤土,所以有:

  • descent= Font.fontDescent = 27.04.
  • ascent = 40 - descent = 12.95.
ascent = 12.95. 
descent = 27.04. 
height = ascent + descent = 40. 

居中對其.

居中對其

居中對其,descent值和ascent值需要經(jīng)過一些簡單的計算迷殿,先計算ascent值咖杂,ascent值為文字內(nèi)容的ascent減去頂部的那一段差值,(如下圖標(biāo)準(zhǔn)中的值為21處的高度)懦尝,然后descent值為attachmen的高度減去ascent,如下圖所示(圖片上的標(biāo)注是2x陵霉,并且數(shù)值因為是手動使用工具標(biāo)注踊挠,會有一些細(xì)微的偏差),內(nèi)容的高度為40效床,所以有:

  • ascent = Font.fontAscent - (LineHeight - 40)/2 = 23.35.
  • descent = 40 - ascent = 16.64.
ascent = 23.35. 
descent = 16.64. 
height = ascent + descent = 40. 

代碼實現(xiàn)

首先需要在Attachment模型中添加如下幾個屬性,這些屬性在計算attachment內(nèi)容的descent忍疾、ascent是必須要用到的

@property (nonatomic, assign) YTAttachmentAlignType align;///<對其方式
@property (nonatomic, assign) CGFloat ascent;///<文本內(nèi)容的ascent谨朝,用于計算attachment內(nèi)容的ascent
@property (nonatomic, assign) CGFloat descent;///<文本內(nèi)容的descent甥绿,用于計算attachment內(nèi)容的descent
@property (nonatomic, assign) CGSize size;///<attachment內(nèi)容的大小

然后根據(jù)以上分析,我們可以很容易的寫出如下的幾個RunDelegate回調(diào)方法的代碼:

// MARK: - CTRunDelegateCallbacks 回調(diào)方法
static CGFloat getAscent(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    if (attachmentItem.align == YTAttachmentAlignTypeTop) {
        return attachmentItem.ascent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
        return attachmentItem.size.height - attachmentItem.descent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
        return attachmentItem.ascent - ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
    }
    return attachmentItem.size.height;
}

static CGFloat getDescent(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    if (attachmentItem.align == YTAttachmentAlignTypeTop) {
        return attachmentItem.size.height - attachmentItem.ascent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
        return attachmentItem.descent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
        return attachmentItem.size.height - attachmentItem.ascent + ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
    }
    return 0;
}

static CGFloat getWidth(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    return attachmentItem.size.width;
}

另外,在更新全局字體的時候需要同步的更新YTAttachmentItem中的descent图谷、ascent屬性

- (void)setFont:(UIFont *)font {
    _font = font;
    [self.attributeString yt_setFont:_font];
    [self updateAttachments];
}

- (void)updateAttachments {
    for (YTAttachmentItem *attachment in self.attachments) {
        [self updateAttachment:attachment withFont:self.font];
    }
}

總結(jié)

以上就是使用Core Text添加自定義的View以及設(shè)置對其方式的一點小總結(jié),如有不妥之處菠镇,還請不吝賜教承璃。

參考

Typographical Concepts
使用CoreText繪制文本

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盔粹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子舷嗡,更是在濱河造成了極大的恐慌,老刑警劉巖捻脖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異只祠,居然都是意外死亡扰肌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門盗舰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桂躏,“玉大人,你說我怎么就攤上這事剂习。” “怎么了失仁?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵们何,是天一觀的道長。 經(jīng)常有香客問我拂封,道長鹦蠕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任镣衡,我火速辦了婚禮档悠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辖所。我一直安慰自己,他們只是感情好吆视,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著您觉,像睡著了一般授滓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上般堆,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天淮摔,我揣著相機(jī)與錄音,去河邊找鬼和橙。 笑死,一個胖子當(dāng)著我的面吹牛涨享,可吹牛的內(nèi)容都是我干的仆百。 我是一名探鬼主播奔脐,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼峦朗!你這毒婦竟也來了排龄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤尺铣,失蹤者是張志新(化名)和其女友劉穎争舞,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竞川,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡叁熔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年荣回,在試婚紗的時候發(fā)現(xiàn)自己被綠了戈咳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡糯累,死狀恐怖册踩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情暂吉,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布阎肝,位于F島的核電站肮街,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏嫉父。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一摇肌、第九天 我趴在偏房一處隱蔽的房頂上張望仪际。 院中可真熱鬧,春花似錦肯适、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钳垮。三九已至额港,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肚医,已是汗流浹背向瓷。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留猖任,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓刁赖,卻偏偏與公主長得像长搀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子枪芒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344