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

2023-05-16

 

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

 

目录: 

一.前言:

二.核心需求说明

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

四.几个注意的小细节

一.前言:

我的第一篇CSDN文章就这样发布了,由于之前主要在简书和以及更早的新浪博客里面写文。现在考虑各方面因素,决定开启CSDN技术分享之路。

这里的技术分享主要是对个人研究和学习新技术提供一个促进的方向,既没有特强烈的好为人师的想法,也没有流量变现的需求,核心目标是可以为个人未来技术发展素养提供更多养料,而这些养料就来自于个人所接触的技术后思考加工后的分享,以及所有看到个人技术后互相探讨的成果!

希望我们可以每天进步一点点,大神之路不太远!

 

二.核心需求说明:


就像上图所示的示例:
1.项目需求
项目中要实现私密阅读信息的功能,即一次只能查看一行文字功能。当我们手指点击或者滑动到某一行文字的时候,该行文字会显示出来,而当我们手指离开该行的时候,文字会隐藏起来。
主要目的是,该App要防止用户截屏,真正做到隐私无泄漏。


2.大致思考说明
明白了我们的核心需求后,那么对这个问题的思考点落脚:
首先要实现文本的行数的监听控制,那么自然要用到label中的 富文本展示功能;
其次是覆盖到的文本位置区域要尽可能的准确无误;
最后当然是手指滑动以及点击过程中的监听交互与覆盖层的处理逻辑。
针对以上问题,要怎么来解决呢? 

 

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

总的来说,基本从实现该功能来说,其实可以简单总结为三步曲
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 ++) {

3.动态计算一片字所占方法的枚举
这块分析和研究方面情况容易忽略,顺手说一下,因为在后面其他地方有遇到过这样的问题,即如下所示,在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;
//            }
//        }

最后附上一个个人gitee的项目连接地址:我的富文本之DDRichTextDemoicon-default.png?t=L892https://gitee.com/httpfdajkfihdakdjhd/ddrich-text-demo


一并把一些参考资料附上:

1.iOS富文本(NSAttributedString)---尽力弄全了
2.iOS开发之UILable文字 居上对齐/居中对齐/居下对齐
3.IOS如何使用CAShapeLayer实现复杂的View的遮罩效果

有问题欢迎评论区见哈!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

iOS富文本实现(一):私密阅读效果 的相关文章

随机推荐

  • JAVA从入门到精通(18)-- Servlet

    一 Servlet定义 1 现有JSP还是先有Servlet xff1f 先有的Servlet xff0c 因为JSP的前身就是Servlet 2 定义 xff1a Servlet是在服务器上运行的小程序 一个Servlet就是一个Java
  • vue数据双向绑定

    5 Vue数据双向绑定 5 1 什么是双向数据绑定 Vue js 是一个 MVVM 框架 xff0c 即数据双向绑定 xff0c 即当数据发生变化的时候 xff0c 视图也就发生变化 xff0c 当视图发生变化的时候 xff0c 数据也会跟
  • Nmap入门到高级【第九章】

    预计更新 Nmap基础知识 1 1 Nmap简介和历史 1 2 Nmap安装和使用方法 1 3 Nmap扫描技术和扫描选项 Nmap扫描技术 2 1 端口扫描技术 2 2 操作系统检测技术 2 3 服务和应用程序检测技术 2 4 漏洞检测技
  • QEMU-IMG命令详解

    qemu img是QEMU的磁盘管理工具 xff0c 在qemu kvm源码编译后就会默认编译好qemu img这个二进制文件 qemu img也是QEMU KVM使用过程中一个比较重要的工具 xff0c 本节对其用法和实践使用方法进行介绍
  • 麒麟系统开机自启的实现方式

    方法 xff1a 利用Linux的 desktop文件实现开机启动 xff0c desktop文件位于在 etc xdg autostart目录下 举例 在 etc xdg autostart 目录下建立一个 test desktop文件
  • ifconfig与 ip addr命令详细

    文章目录 前言一 如何查看机器的IP地址二 网卡信息详解1 网卡名称2 网络设备状态标识3 IP地址4 MAC地址 三 ifconfig与 ip addr区别 前言 本文记录在linux系统下如何查看ip信息 网卡状态等信息以及简要说明if
  • Linux网卡ifcfg网卡配置项详解

    前言 由于在工作中涉及到服务器网卡的适配 xff0c 算是linux新手 xff0c 本次记录下linux网卡ifcfg exx配置项含义说明 xff0c 以下是某款终端下centos 7 5系统自动生成的网卡配置内容 xff1a span
  • Determining IP information for eth问题解决

    前言 在Linux网卡ifcfg网卡配置项详解文章中提到一个BOOTPROTO 61 配置项 它的意思是指网卡启动时获取ip的方式 xff0c 可以是dhcp或者静态ip 方式 xff0c 如果设置为none说明是不指定ip设置方式 一 问
  • 关于vector大小(size)和容量(capacity)总结

    操作大小的函数 在Vector容器中有以下几个关于大小的函数 方法效果size 返回容器的大小empty 判断容器是否为空max size 返回容器最大的可以存储的元素capacity 返回容器当前能够容纳的元素数量 例子一 xff1a 该
  • inet_addr 和inet_ntoa函数作用

    我们使用socket进行通信的时候 xff0c 我们需要指定三个元素 xff1a 通信域 xff08 地址族 xff09 IP地址 端口号 xff0c 这三个元素由SOCKADDR IN结构体定义 xff0c 为了简化编程一般将IP地址设置
  • visual studio中头文件和库文件路径设置

    在程序开发中 xff0c 很多时候需要用到别人开发的工具包 xff0c 如OpenCV和itk 一般而言 xff0c 在vs中 xff0c 很少使用源文件 xff0c 大部分是使用对类进行声明的头文件和封装了类的链接库 xff08 静态li
  • LNK2001: 无法解析的外部符号的几种情况

    一般来说 xff0c 我们引用第三方库时 xff0c 需要进行指定依赖项配置 xff0c 若没有进行相关配置 xff0c 则编译器会出现 LNK2001 无法解析的外部符号 错误 这个是最常见的问题 xff0c 具体步骤 xff1a 项目
  • JMeter

    Apache JMeter 压力测试工具 一 什么是Apache JMeter Apache JMeter 是 Apache 组织基于 Java 开发的压力测试工具 xff0c 用于对软件做压力测试 JMeter 最初被设计用于 Web 应
  • C++11向线程函数传递参数

    template span class token operator lt span class Function span class token punctuation span class span class token punct
  • C++11之std::future对象使用说明

    std future介绍 在前面几篇文章中基本都用到thread对象 xff0c 它是C 43 43 11中提供异步创建多线程的工具 但是我们想要从线程中返回异步任务结果 xff0c 一般需要依靠全局变量 xff1b 从安全角度看 xff0
  • delete 和 delete[]真正区别

    我们通常从教科书上看到这样的说明 xff1a delete 释放new分配的单个对象指针指向的内存 delete 释放new分配的对象数组指针指向的内存 那么 xff0c 按照教科书的理解 xff0c 我们看下下面的代码 xff1a spa
  • Activity的onNewIntent

    一个应用的Activity可供多种方式调用启动 xff0c 当多个调用希望只有一个Activity的实例存在 xff0c 并且还要区分是被谁启动或是已经启动被谁拉到前台来的 xff0c 这就需要Activity的onNewIntent In
  • at 与 crontab调度命令详解

    目录 1 At调度 只执行一次 1 1准备任务 xff1a 查看at服务是否开启 1 2绝对时间定制任务 1 3相对时间定制任务 1 4查看at进程 1 5删除at任务 2 crontab调度 可重复执行 2 1简述 2 2crontab调
  • [计算机网络] --- STP (下篇) 工作原理及配置

    文章目录 前言一 stp工作原理二 stp计算过程 工作步骤1 选举根桥2 选举根端口3 选举指定端口4 确立阻塞端口 三 例题 xff1a 前言 上一篇文章我们介绍了stp的起源和一些相关术语 xff0c 接下来我们就正式开始介绍stp的
  • iOS富文本实现(一):私密阅读效果

    废话不多说 xff0c 咱们直接先看效果 xff01 看是不是咱想要的哈 目录 xff1a 一 前言 xff1a 二 核心需求说明 三 实现效果核心代码片段 四 几个注意的小细节 一 前言 xff1a 我的第一篇CSDN文章就这样发布了 x