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的遮罩效果

有问题欢迎评论区见哈!

你可能感兴趣的:(iOS富文本实现(-):私密阅读效果)