iOS富文本实现(-):私密阅读效果

废话不多说,咱们直接先看效果!看是不是咱想要的哈

私密阅读一次查看一行文字效果图.gif

目录:

一.核心需求说明

二.实现效果核心代码片段

三.几个注意的小细节

四.简书的后记说明

一.核心需求说明:

就像上图所示的示例:
1.项目需求
项目中要实现私密阅读信息的功能,即一次只能查看一行文字功能。当我们手指点击或者滑动到某一行文字的时候,该行文字会显示出来,而当我们手指离开该行的时候,文字会隐藏起来。
主要目的是,该App要防止用户截屏,真正做到隐私无泄漏。
2.大致思考说明
明白了我们的核心需求后,那么对这个问题的思考点落脚:
首先要实现文本的行数的监听控制,那么自然要用到label中的 富文本展示功能;
其次是覆盖到的文本位置区域要尽可能的准确无误;
最后当然是手指滑动以及点击过程中的监听交互与覆盖层的处理逻辑。
针对以上问题,要怎么来解决呢?

annie-spratt-ceMXSBfPoBs-unsplash.jpg

二.实现效果核心代码片段

总的来说,基本从实现该功能来说,其实可以简单总结为三步曲
1.富文本文字的设置
这块主要涉及对文字大小,字与字的间距,行间距,甚至未来的段间距等相关的设置,这是富文本研究的基础工作。

  NSMutableParagraphStyle *muParagraph = [[NSMutableParagraphStyle alloc]init];
    muParagraph = [attributes objectForKey:NSParagraphStyleAttributeName];

    NSMutableAttributedString * attrStr = [[NSMutableAttributedString alloc] initWithData:[text dataUsingEncoding:NSUnicodeStringEncoding] options:@{ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType } documentAttributes:nil error:nil];
    
    NSRange range = NSMakeRange(0, attrStr.length);
    // 设置字体大小
    UIFont *systemFont = [attributes objectForKey:NSFontAttributeName];
    [attrStr addAttribute:NSFontAttributeName value:systemFont range:range];
    // 设置字间距
    NSNumber *keyWordSpacing = [attributes objectForKey:NSKernAttributeName];
    [attrStr addAttribute:NSKernAttributeName value:keyWordSpacing range:range];
    
    // 设置段落样式
    [attrStr addAttribute:NSParagraphStyleAttributeName value:muParagraph range:range];
    
    self.attributedText = attrStr;
    

2.遮盖层的选择研究
关于遮盖层方面,其实一般开发人员就直接会去选择View去处理。但是如果从性能角度考虑,这层遮盖层仅仅是只有遮盖功能,并没有事件的响应以及其他复杂业务逻辑的功能,这边考虑的是用layer来处理,如下所示:

UIBezierPath *path = [UIBezierPath bezierPath];
        CGFloat layerX = 0;
        CGFloat layerY = index * lineHeight;
        [path moveToPoint:CGPointMake(layerX, layerY)];
        [path addLineToPoint:CGPointMake(size.width, layerY)];
        [path addLineToPoint:CGPointMake(size.width, layerY + singleSize.height)];
        [path addLineToPoint:CGPointMake(layerX, layerY + singleSize.height)];

        [path closePath];
           
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.fillColor = [UIColor lightGrayColor].CGColor;
        layer.path = path.CGPath;
        
        [self.layer addSublayer:layer];

layer来处理的话,有个问题会出现,即对layer身上没有tag标签可以标记,所以对于初次展示的遮盖依然需要用View来遮盖(即红色遮盖的部分),用户只要点击过该行之后,就是下面的Layer(灰色遮盖)。
灰色遮盖Layer会长期存在,而红色遮盖View则会在用户点击了改行之后就会永远消失(红色遮盖类似标记用户已读未读的功能)。

3.手势添加的策略
手势添加是个小问题,重要的是手势添加之后如何和View关联处理的逻辑,所以这里就只展示手势点击后的策略,即如下所示:

/** 点击事件*/
-(void)gestureClick:(UIGestureRecognizer *)gesture {

    CGPoint touchPoint = [gesture locationInView:self];
   
    // 获得一个字体的高
    NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:kTextFont]};

    CGSize singleSize = [CWRichGestureLabel getSingleWords:dic];

    // 设置行距
    NSMutableParagraphStyle *muParagraph = [[NSMutableParagraphStyle alloc]init];
    muParagraph = [self.attributesDic objectForKey:NSParagraphStyleAttributeName];
    // 加字间距后的行高
    CGFloat lineHeight = singleSize.height + muParagraph.lineSpacing;
    // 点击的行数
    NSInteger lineCount = touchPoint.y/lineHeight;
    // 如果第一次点击,先将对应的第一层view删除的情况
    UIView *colorView = [self viewWithTag:lineCount + 1];
    if (colorView) {
        [colorView removeFromSuperview];
    }
    
    NSArray *layerArr = self.layer.sublayers;
    NSLog(@"lineCount = %ld state = %ld",lineCount,(long)gesture.state);
    if (lineCount < layerArr.count) { 
        //遍历当前视图上的子视图的presentationLayer 与点击的点是否有交集
//        NSLog(@"sublayers个数 = %ld",self.layer.sublayers.count);
        CALayer *clickLayer = layerArr[lineCount];

        for (CALayer *tempLayer in layerArr) {
            tempLayer.hidden = NO;
        }
// 不要点击手势,因为点击手势只有结束状态,用长按手势代替
//        if (gesture == self.tapGesture) {
//            if (gesture.state == UIGestureRecognizerStateEnded) {
                
//                clickLayer.hidden = YES;
//            }
//        }

        if (gesture.state != UIGestureRecognizerStateEnded) {
            NSLog(@"-----------------");
            clickLayer.hidden = YES;
        }

       

        if (self.clickBlock) {
            self.clickBlock(lineCount, YES);
        }
    }else {
        // 如果点击外侧把所有layer的隐藏状态设置为NO
        for (CALayer *tempLayer in layerArr) {
            tempLayer.hidden = NO;
        }
        if (self.clickBlock) {
            self.clickBlock(0, NO);
        }
    }
   
}

三.几个注意的小细节

1.文字行数计算的细节
首先是关于文字的高度计算特点,由于系统默认的Label是没有纵向居中展示的功能,所以这里继承了MyLabel的自定义Label,来实现自己的Label可以居上显示,从而可以在后续为遮盖层实现精准覆盖到对应的文字上。
这也算是站在巨人的肩膀上做开发了哈!

2.文字行数计算的说明
如下所示,关于文字行数的计算,这里的注释写的很明白!为了方便大家理解,这里就再以一个案例来聊聊,这里注意的细节。
首先如图的singleSize为单个文字的高度。注意这里传的字典中一定不要有行高传过去,不然后续计算就比较麻烦。
另外一点就是如图的lineCount == 1的时候为什么还要加上个行高和实际字体高度的比较呢?

// 获得一个字体的高
    NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:kTextFont]};

    CGSize singleSize = [CWRichGestureLabel getSingleWords:dic];
    
    CGSize size = [text boundingRectWithSize:self.frame.size options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size;
    // 加字间距后的行高
    CGFloat lineHeight = singleSize.height + muParagraph.lineSpacing;
    // 行数
    NSInteger lineCount = size.height/lineHeight;
    if (lineCount == 1 && fabs(size.height - lineHeight) <= 1.0 ) { // 只有一行的时候,刚好是1行文字加一行间距,所以设置lineCount为0,实际用下面的lineCount + 1的情形去展示
        // 这里的lineCount 为1时,其实有2种情形,一种是1行,1种是2行情况,为了更严谨一些,即把所有文字的行高和一个文字的行高相等时,即可,但为了避免文字计算有时会出现差距,则把2个值之差的绝对值控制在如1.0高度范围内,则为一行的情况来看待
        lineCount = 0;
    }
    // 其他情况为什么要 + 1 原因是因为,如果文字刚好2行时,其展示效果是2行文字,1行间距,那么除以一行文字和一行间距的和,值即为不到2,在NSInteger类型下,就为一行,所以无法展示出2行的内容,即应该把最后一行只有文字时不够一行文字和一行间距的和所除给入上去,所以要用lineCount + 1来进行处理
    DDLog(@"%ld",lineCount + 1);

核心原因是因为一行单纯文字假设是20高度,行高10,则行高为30。那么一行文字展示为30,而二行文字展示为20+10+20=50,此时用一行文字30/行高30 = 1,而二行文字50/30 得到的integer数值依然为1。所以就必须要进一步文字的高度和行高是不是刚好。但考虑文字的高度比如本次用的是18号文字,字高位21.xxxx。这样的情况。不知道其他的文字和行高会不会出现后面有误差的情况。此时倘若文字的高度大于行高倒还好说。因为结果是1.多,即为1;而反之的话,为0.9多,就会出现行数为0的尴尬情形。
所以后续在进行行数计算的时候,实际也是考虑了以上的情形,在计算出来的lineCount基础上加1.因为最后一行是没有行间距的。如下所示实际的行数为lineCount + 1。

// 2.遮盖层的选择研究
    for (int index = 0; index < lineCount + 1; index ++) {

2.动态计算一片字所占方法的枚举
这块分析和研究方面情况容易忽略,顺手说一下,因为在后面其他地方有遇到过这样的问题,即如下所示,在Label的boundingRectWithSize方法中有options,是来让我们告诉系统,你想要获得这串文字的整块的布局还是说是某一行甚至某一个字的大小返回情况。
这块个人写了2个方法如下所示,Demo中没有,一个是返回一块文字的尺寸,一个是返回一行文字的尺寸。核心是options值的不同。

//自适应(块)
+ (CGSize)autoSizeFrame:(CGSize)sizeFrame withFont:(UIFont*)font withText:(NSString *)text
{
    NSDictionary * dic = @{NSFontAttributeName:font};
    CGSize labelSize = [text boundingRectWithSize:sizeFrame options:NSStringDrawingUsesLineFragmentOrigin attributes:dic context:nil].size;

    return labelSize;
}

//自适应(一行)
+ (CGSize)autoOneLineSizeFrame:(CGSize)sizeFrame withFont:(UIFont*)font withText:(NSString *)text
{
    NSDictionary * dic = @{NSFontAttributeName:font};
    CGSize labelSize = [text boundingRectWithSize:sizeFrame options:NSStringDrawingUsesDeviceMetrics attributes:dic context:nil].size;

    return labelSize;
}

4.小不足点1个
如下所示的,在点击手势中由于无法监听到其结束时的状态,所以用长按手势来代替。即对于点击手势它的gesture.state只有UIGestureRecognizerStateBegan的状态,那么问题如果非要用点击手势,就会出现,用户点击后,无法监听到其点击手势结束时把对应点击位置的Layer给显示出来的逻辑,所以考虑用长按手势来代替,只是把长按时间如下设置为0.05s。
所以如果发现有这块秒速的点击无法出现效果,还望大家一起思考这个问题的解决方案,谢谢!

//创建手势添加到视图上
    self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(gestureClick:)];
    self.longPressGesture.minimumPressDuration = 0.05;
    [self addGestureRecognizer:self.longPressGesture];


// 不要点击手势,因为点击手势只有结束状态,用长按手势代替
//        if (gesture == self.tapGesture) {
//            if (gesture.state == UIGestureRecognizerStateEnded) {
                
//                clickLayer.hidden = YES;
//            }
//        }

四.简书的后记说明:

过去3年多以来,由于制定了很多计划,但由于各种原因所致,技术的学习时有时无。
就像一个笑话说的,我们有很多计划,简称为plan。但在实际完成过程中只完成了个p,因为lan,哈哈哈!
希望未来可以重新开启技术之窗的对话,欢迎大家捧场哈!
这里附上一个gitee的项目连接地址:我的富文本之DDRichTextDemo
一并把一些参考资料附上:
1.iOS富文本(NSAttributedString)---尽力弄全了
2.iOS开发之UILable文字 居上对齐/居中对齐/居下对齐
3.IOS如何使用CAShapeLayer实现复杂的View的遮罩效果

有问题欢迎评论区见哈!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,030评论 0 4
  • 公元:2019年11月28日19时42分农历:二零一九年 十一月 初三日 戌时干支:己亥乙亥己巳甲戌当月节气:立冬...
    石放阅读 6,870评论 0 2