iOS 輕量級富文本方案

項目中多多少少會遇到一些需求迫吐,需要給部分文案添加點擊事件扫倡。例如:"我已閱讀《xxx協(xié)議》"豹缀,這個時候最簡單的實現(xiàn)方案是:添加一個button覆蓋上去坟漱。但如果有多處文案需要添加點擊事件鼠次,就會比較懵逼。下面我說下我的實現(xiàn)方案芋齿。

最初的時候我打算使用CoreText實現(xiàn)腥寇,查詢了相關資料后發(fā)現(xiàn)自己不能很快的造出輪子,雖然網(wǎng)上有很多現(xiàn)成的輪子可以用觅捆,但是我并不想只純粹的搬運代碼赦役。需要直接拿輪子的可以點擊這里。于是我把目光轉移到TextKit上面栅炒,發(fā)現(xiàn)相關代碼要比CoreText易讀易懂很多掂摔,于是就有了下面的事情。

言歸正傳赢赊,給Label的文字添加點擊事件乙漓,首先需要讓文字有不同的顏色,不同的字號释移,這個時候就需要用到 NSMutableAttributedString叭披,下面是一些常規(guī)的處理。

NSMutableAttributedString

  • 設置顏色
///根據(jù)range設置顏色
-(void)addColor:(UIColor *)color range:(NSRange)range
{
    if (range.length > self.attributedText.length) {
        return;
    }
    [self.attributedText addAttribute:NSForegroundColorAttributeName value:color range:range];
}
  • 設置字體大小
///根據(jù)range設置字體大小
-(void)addFontSize:(CGFloat)size range:(NSRange)range
{
    if (range.length > self.attributedText.length) {
        return;
    }
    UIFont * font;
    //判斷是其他字體
    if (self.fontName.length == 0) {
        //判斷是否是粗體
        if (self.isBold) {
            font = [UIFont boldSystemFontOfSize:size];
        }
        else
        {
            font = [UIFont systemFontOfSize:size];
        }
    }
    else
    {
        font = [UIFont fontWithName:self.fontName size:size];
    }
    [self.attributedText addAttribute:NSFontAttributeName value:font range:range];
}
  • 設置字間距
///設置字間距
-(void)addKerningWithSpace:(CGFloat)space
{
    [self.attributedText addAttribute:NSKernAttributeName value:[NSNumber numberWithFloat:space] range:NSMakeRange(0, 10)];
}
  • 添加單刪除線
///添加單刪除線
-(void)addSingleStrikethroughWithColor:(UIColor * )color range:(NSRange)range
{
    [self.attributedText addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle] range:range];
    if (color) {
        [self.attributedText addAttribute:NSStrikethroughColorAttributeName value:color range:range];
    }
}
  • 添加單下劃線
///添加單下劃線
-(void)addSingleUnderlineWithColor:(UIColor * )color range:(NSRange)range
{
    [self.attributedText addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle] range:range];
    if (color) {
        [self.attributedText addAttribute:NSUnderlineColorAttributeName value:color range:range];
    }
}
  • 設置基線偏移值
///設置基線偏移值
-(void)addBaselineOffset:(CGFloat)offset range:(NSRange)range
{
    [self.attributedText addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:offset] range:range];
}
  • 設置凸版印刷體效果
///設置凸版印刷體效果
-(void)addEffectLetterpressWithRange:(NSRange)range
{
    [self.attributedText addAttribute:NSTextEffectAttributeName value:NSTextEffectLetterpressStyle range:range];
}
  • 插入圖片
///在某個位置插入圖片 不要在結尾處插入圖片玩讳,在iOS8的某個小版本會出現(xiàn)bug
-(void)addAttachmentWithImageName:(NSString *)imageName index:(NSInteger)index size:(CGSize)size
{
    UIImage * image = [UIImage imageNamed:imageName];
    NSTextAttachment * attachment = [[NSTextAttachment alloc] init];
    attachment.image = image;
    //設置默認大小
    if (CGSizeEqualToSize(size, CGSizeZero)) {
        attachment.bounds = CGRectMake(0, 0, 20, 20);
    }
    else
    {
        attachment.bounds = CGRectMake(0, 0, size.width, size.height);
    }
    NSAttributedString * attibutedString = [NSAttributedString attributedStringWithAttachment:attachment];
    [self.attributedText insertAttributedString:attibutedString atIndex:index];
}
  • 設置字體段落樣式
//把字體樣式添加到可變字符串中
     [self.attributedText addAttribute:NSParagraphStyleAttributeName value:_desParagraphStyle range:NSMakeRange(0, [self.attributedText length])];

NSMutableParagraphStyle 字體段落樣式

  • 設置字體行間距
///設置字體行間距
-(void)setLineSpacing:(CGFloat)lineSpacing
{
    self.desParagraphStyle.lineSpacing = lineSpacing;
}
  • 設置換行方式
///設置換行方式
-(void)setLineBreakMode:(NSLineBreakMode)lineBreakMode
{
    self.desParagraphStyle.lineBreakMode = lineBreakMode;
}
  • 設置首行縮進
///設置首行縮進
-(void)setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent
{
    self.desParagraphStyle.firstLineHeadIndent = firstLineHeadIndent;
}
  • 設置文本對齊方式
//文本對齊方式
-(void)setNSTextAlignment:(NSTextAlignment)alignment
{
    self.desParagraphStyle.alignment = alignment;
}
  • 設置段落間距
//段與段之間的間距
-(void)setParagraphSpacing:(CGFloat)paragraphSpacing
{
    self.desParagraphStyle.paragraphSpacing = paragraphSpacing;
}

高度的計算

///獲取最終大小
-(CGSize)getSizeWithMaxWidth:(CGFloat)maxWidth
{
    
    if (self.maxWidth == maxWidth) {
        return self.stringSize;
    }
    self.maxWidth = maxWidth;
    
    CGRect rect = [self.attributedText boundingRectWithSize:CGSizeMake(maxWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading context:nil];
    //誤差值
    rect.size.height += 0.5;
    self.stringSize = rect.size;
    return self.stringSize;
}

以上方法設置了常用的屬性涩蜘,下面要設置點擊事件的屬性。因為NSMutableAttributedString并不包含點擊事件的屬性熏纯,所以我們需要自定義一個Key同诫,設置我們的value。

  • 添加自定義屬性(為點擊事件服務)
///添加自定義屬性樟澜,為自定義label(ZHHitLabel)提供服務
- (void)addRichHelpWithType:(ZHRichHelpType)type range:(NSRange)range color:(UIColor *)textColor
{
    if (range.length > self.attributedText.length) {
        return;
    }
    if (textColor) {
        [self addColor:textColor range:range];
    }
    
    ZHRichHelpModel * model = [[ZHRichHelpModel alloc] init];
    model.richHelpType = type;
    model.valueRange = range;
    model.valueStr = [self.attributedText.string substringWithRange:range];
    [self.attributedText addAttribute:RichKey value:model range:range];
}


///其中我們自定義了一個Model误窖,枚舉了常用的事件叮盘,添加了一些后續(xù)可能用到的屬性。
typedef enum : NSUInteger {
    ZHRichHelpTypeNone  = 0,  //啥事不干
    ZHRichHelpTypeLink  = 1,  //鏈接
    ZHRichHelpTypePhone = 2,  //電話號碼
    ZHRichHelpTypeClick = 3,  //點擊事件
} ZHRichHelpType;

@interface ZHRichHelpModel : NSObject
///記錄目標類型
@property (nonatomic, assign) ZHRichHelpType richHelpType;
///記錄目標字符串
@property (nonatomic, strong) NSString * valueStr;
///記錄目標范圍
@property (nonatomic, assign) NSRange valueRange;
@end


  • 自定義UILable

擁有了有自定義屬性和富文本格式的NSMutableAttributedString后贩猎,我們需要一個展示的控件熊户,系統(tǒng)默認可以設置NSAttributedString的控件都可以直接賦值,并且展示出富文本效果吭服。如果需要點擊事件嚷堡,就需要自定義label了

#import <UIKit/UIKit.h>
#import "ZHRichHelpModel.h"

typedef void(^HitLableTouchBlock)(ZHRichHelpModel * model);

@interface ZHHitLabel : UILabel
///點擊事件回調
@property (nonatomic, copy) HitLableTouchBlock clickBlock;

@end

在觸摸的時候,檢測觸摸的文字是否擁有點擊事件

#pragma mark - Touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch * touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self];
    self.richModel = [self touchStrAtPoint:touchPoint];
    if (!self.richModel) {
        [super touchesBegan:touches withEvent:event];
    }
    else
    {
        [self drawBackgroundColorWithState:NO withRange:self.richModel.valueRange];
    }

}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (self.richModel) {
        UITouch * touch = [touches anyObject];
        CGPoint touchPoint = [touch locationInView:self];
        ZHRichHelpModel * moveRichModel = [self touchStrAtPoint:touchPoint];
        if (self.richModel != moveRichModel) {
            [self drawBackgroundColorWithState:YES withRange:self.richModel.valueRange];
            self.richModel = nil;
        }
    }
    else
    {
        [super touchesMoved:touches withEvent:event];
    }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (self.richModel) {
        if (self.clickBlock) {
            self.clickBlock(self.richModel);
        }
        [self drawBackgroundColorWithState:YES withRange:self.richModel.valueRange];
    }
    else
    {
        [super touchesEnded:touches withEvent:event];
    }
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (self.richModel) {
        [self drawBackgroundColorWithState:YES withRange:self.richModel.valueRange];
        self.richModel = nil;
    } else {
        [super touchesCancelled:touches withEvent:event];
    }
}

#pragma mark - CGSize CGRect CGPoint
- (ZHRichHelpModel *)touchStrAtPoint:(CGPoint)location
{
    
    CGPoint textOffset;
    //在執(zhí)行usedRectForTextContainer之前最好還是執(zhí)行下glyphRangeForTextContainer relayout
    [self.layoutManager glyphRangeForTextContainer:self.textContainer];
    textOffset = [self textOffsetWithTextSize:[self.layoutManager usedRectForTextContainer:self.textContainer].size];
    
    //location轉換成在textContainer的繪制區(qū)域的坐標
    location.x -= textOffset.x;
    location.y -= textOffset.y;
    
    
    //返回觸摸區(qū)域的字形  如果location的區(qū)域沒字形艇棕,可能返回的是最近的字形index蝌戒,所以需要再找到這個字形所處于的rect來確認
    NSUInteger glyphIndex = [self.layoutManager glyphIndexForPoint:location inTextContainer:self.textContainer];
    CGRect glyphRect = [self.layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) inTextContainer:self.textContainer];
    if (!CGRectContainsPoint(glyphRect, location)) {
        return nil;
    }
    
    for (ZHRichHelpModel * model in self.helpValueArray) {
        if (NSLocationInRange(glyphIndex, model.valueRange)) {
            return model;
        }
    }
    
    return nil;
}

//這個計算出來的是繪制起點
- (CGPoint)textOffsetWithTextSize:(CGSize)textSize
{
    CGPoint textOffset = CGPointZero;
    //根據(jù)insets和默認垂直居中來計算出偏移
    textOffset.x = 0;
    CGFloat paddingHeight = (_textContainer.size.height - textSize.height) / 2.0f;
    textOffset.y = paddingHeight+ 0;
    
    return textOffset;
}

///繪制背景顏色 根據(jù)觸摸的狀態(tài) 以及范圍
- (void)drawBackgroundColorWithState:(BOOL)isEnd withRange:(NSRange)range
{
    NSMutableAttributedString * mutableStr = [self.lastAttributedString mutableCopy];
    if (!isEnd) {
        [mutableStr addAttribute:NSBackgroundColorAttributeName value:[UIColor colorWithRed:239/255.0 green:253/255.0 blue:1 alpha:1] range:range];
    }
    else
    {
        [mutableStr removeAttribute:NSBackgroundColorAttributeName range:range];
    }
    
    self.attributedText = mutableStr;
    
}

源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沼琉,隨后出現(xiàn)的幾起案子北苟,更是在濱河造成了極大的恐慌,老刑警劉巖打瘪,帶你破解...
    沈念sama閱讀 221,331評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件友鼻,死亡現(xiàn)場離奇詭異,居然都是意外死亡闺骚,警方通過查閱死者的電腦和手機彩扔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來僻爽,“玉大人虫碉,你說我怎么就攤上這事⌒匕穑” “怎么了敦捧?”我有些...
    開封第一講書人閱讀 167,755評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長碰镜。 經常有香客問我兢卵,道長,這世上最難降的妖魔是什么绪颖? 我笑而不...
    開封第一講書人閱讀 59,528評論 1 296
  • 正文 為了忘掉前任济蝉,我火速辦了婚禮,結果婚禮上菠发,老公的妹妹穿的比我還像新娘。我一直安慰自己贺嫂,他們只是感情好滓鸠,可當我...
    茶點故事閱讀 68,526評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著第喳,像睡著了一般糜俗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,166評論 1 308
  • 那天悠抹,我揣著相機與錄音珠月,去河邊找鬼。 笑死楔敌,一個胖子當著我的面吹牛啤挎,可吹牛的內容都是我干的。 我是一名探鬼主播卵凑,決...
    沈念sama閱讀 40,768評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼庆聘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勺卢?” 一聲冷哼從身側響起伙判,我...
    開封第一講書人閱讀 39,664評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎黑忱,沒想到半個月后宴抚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,205評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡甫煞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,290評論 3 340
  • 正文 我和宋清朗相戀三年菇曲,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片危虱。...
    茶點故事閱讀 40,435評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡羊娃,死狀恐怖,靈堂內的尸體忽然破棺而出埃跷,到底是詐尸還是另有隱情蕊玷,我是刑警寧澤,帶...
    沈念sama閱讀 36,126評論 5 349
  • 正文 年R本政府宣布弥雹,位于F島的核電站垃帅,受9級特大地震影響,放射性物質發(fā)生泄漏剪勿。R本人自食惡果不足惜贸诚,卻給世界環(huán)境...
    茶點故事閱讀 41,804評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厕吉。 院中可真熱鬧酱固,春花似錦、人聲如沸头朱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽项钮。三九已至班眯,卻和暖如春希停,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背署隘。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工宠能, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人磁餐。 一個月前我還...
    沈念sama閱讀 48,818評論 3 376
  • 正文 我出身青樓违崇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親崖媚。 傳聞我的和親對象是個殘疾皇子亦歉,可洞房花燭夜當晚...
    茶點故事閱讀 45,442評論 2 359

推薦閱讀更多精彩內容