ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

套路继续, .txt 小说阅读器功能开发

2021-01-25 22:32:04  阅读:237  来源: 互联网

标签:... frameRef point 套路 let CGRect 阅读器 txt rect


1, 解决一个 bug

正文结尾 (最后一行最后一个字)跟右边界, 有多余的空白间隔

Core Text 的渲染流程,就是富文本绘制

从流程上看,

感觉这一页的文字分配少了,给他加点字,就满了

// 拿到一个章节的富文本,计算出每一页的富文本,从哪里开始,哪里结束
// 得到一个范围的数组,就知道了每一页的文字
class func pagingRanges(attrString:NSAttributedString, rect:CGRect) ->[NSRange] {
        var rangeArray = [NSRange]()
        let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
        // 屏幕显示区域
        let path = CGPath(rect: rect, transform: nil)
        var range = CFRangeMake(0, 0)
        var rangeOffset = 0
        repeat{
            let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(rangeOffset, 0), path, nil)
            range = CTFrameGetVisibleStringRange(frame)
            rangeArray.append(NSMakeRange(rangeOffset, range.length))
            rangeOffset += range.length
        }while(rangeOffset < attrString.length)
        return rangeArray
    }

这样做,效果很不好

解决:

修改富文本属性, 怎么换行的


            // 行间距
            paragraphStyle.lineSpacing = lineSpacing
            // 加了这么一句
            paragraphStyle.lineBreakMode = .byCharWrapping
            // 段间距
            paragraphStyle.paragraphSpacing = paragraphSpacing
            
            // 对齐
            paragraphStyle.alignment = .justified

2, 复制一整行

基于 dengzemiao/DZMeBookRead

长按,可复制一整行

长按的时候,要找出这一行,

给这一行涂上颜色,

出来一个可复制的菜单


/// 长按事件
    @objc private func longAction(long:UILongPressGestureRecognizer) {
        
        // 触摸位置
        let point = long.location(in: self)

        // 触摸位置
        switch long.state {
        case .began:
            // 触摸开始 触摸中
            // 发送通知, 处理其他 UI
            // ...
        //      case .changed:
        default:
            // 触摸结束

            // 获得选中区域
            selectRange = CoreText.GetTouchLineRange(point: point, frameRef: frameRef)

            // 获得选中选中范围
            rects = CoreText.GetRangeRects(range: selectRange!, frameRef: frameRef, content: pagingModel.content?.string)

            // 显示光标
            cursor(isShow: true)


                // 显示菜单
            self.showMenu(isShow: true)
            

            // 重绘
            setNeedsDisplay()

            // 发送通知, 处理其他 UI
            // ...
        }
    }

找出这一行

长按手势,可以拿到一个点,

当前阅读界面,有一帧的文字 CTFrame

    
    /// 获得触摸位置那一行文字的Range
    ///
    /// - Parameters:
    ///   - point: 触摸位置
    ///   - frameRef: CTFrame
    /// - Returns: CTLine
    class func GetTouchLineRange(point:CGPoint, frameRef:CTFrame?) ->NSRange {
        
        var range:NSRange = NSMakeRange(NSNotFound, 0)
        
        let line = GetTouchLine(point: point, frameRef: frameRef)
        
        if line != nil {
            
            let lineRange = CTLineGetStringRange(line!)
            
            range = NSMakeRange(lineRange.location == kCFNotFound ? NSNotFound : lineRange.location, lineRange.length)
        }
        
        return range
    }
    
    
    
    
    /// 获得触摸位置在哪一行
    ///
    /// - Parameters:
    ///   - point: 触摸位置
    ///   - frameRef: CTFrame
    /// - Returns: CTLine
    class func GetTouchLine(point:CGPoint, frameRef:CTFrame?) ->CTLine? {
        
        var line:CTLine? = nil
        
        if frameRef == nil { return line }
        
        let frameRef:CTFrame = frameRef!
        
        let path:CGPath = CTFrameGetPath(frameRef)
        
        let bounds:CGRect = path.boundingBox
        // 获取全部行
        let lines:[CTLine] = CTFrameGetLines(frameRef) as! [CTLine]
        
        if lines.isEmpty { return line }
        
        let lineCount = lines.count
        
        let origins = malloc(lineCount * MemoryLayout<CGPoint>.size).assumingMemoryBound(to: CGPoint.self)
        
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins)
        // 一行一行的往下翻
        for i in 0..<lineCount {
            
            let origin:CGPoint = origins[i]
            
            let tempLine:CTLine = lines[i]
            
            var lineAscent:CGFloat = 0
            
            var lineDescent:CGFloat = 0
            
            var lineLeading:CGFloat = 0
            
            CTLineGetTypographicBounds(tempLine, &lineAscent, &lineDescent, &lineLeading)
            
            let lineWidth:CGFloat = bounds.width
            
            let lineheight:CGFloat = lineAscent + lineDescent + lineLeading
            // 每一行的区域
            var lineFrame = CGRect(x: origin.x, y: bounds.height - origin.y - lineAscent, width: lineWidth, height: lineheight)
            
            lineFrame = lineFrame.insetBy(dx: -SPACE_5, dy: -SPACE_5)
            
            if lineFrame.contains(point) {
                
                line = tempLine
                
                break
            }
        }
        
        free(origins)
        
        return line
    }

将选中的一整行,涂上颜色

触发, 上面的手势代码中

selectRangerects, 赋值了。重绘,就好了

             // 获得选中区域
            selectRange = ...

            // 获得选中范围
            rects = ...

            // 重绘
            setNeedsDisplay()

触发绘制


    /// 绘制
    override func draw(_ rect: CGRect) {
        
        if (frameRef == nil) {return}
        
        let ctx = UIGraphicsGetCurrentContext()
        
        ctx?.textMatrix = CGAffineTransform.identity
        
        ctx?.translateBy(x: 0, y: bounds.size.height)
        
        ctx?.scaleBy(x: 1.0, y: -1.0)
        
        if selectRange != nil , !rects.isEmpty {
        // 渲染,选中行的背景
            let path = CGMutablePath()
            
            READ_COLOR_MAIN.withAlphaComponent(0.5).setFill()
            
            path.addRects(rects)
            
            ctx?.addPath(path)
            
            ctx?.fillPath()
            // 先把选中行的背景涂色,再把文字渲染出来,很科学
        }
        // 渲染文字
        CTFrameDraw(frameRef!, ctx!)
    }
点击复制,相关

复制,就是调用剪贴板 UIPasteboard

简单


/// 复制事件
    @objc private func clickCopy() {
        
        if let range = selectRange{
            let tempContent = pagingModel.content
            
            DispatchQueue.global().async {
                
                UIPasteboard.general.string = tempContent?.string.substring(range)
            }
            
            // 重置状态
            // ...
        }
    }
其余
  • 光标的显示

光标就是两个控件,显示光标,简单

  • 选择选中区域

更改选中区域,就是光标的拖拽,逻辑与前文类似,

效果是整行整行的复制

3, 灵活的复制。上面只是定位到某一行,现在定位到那一行的那个字

从 Swift 变到了 Objective-C

基于 GGGHub/Reader

也是通过长按手势触发

渲染出来,老三步

  • 拿到一个点击位置,一个点 CGPoint

  • 计算出当前区域 rect

  • 渲染出来

-(void)longPress:(UILongPressGestureRecognizer *)longPress{
    // 拿到一个点
    CGPoint point = [longPress locationInView:self];
    // ...
    // 处理 UI 状态
    if (longPress.state == UIGestureRecognizerStateBegan || longPress.state == UIGestureRecognizerStateChanged) {
        CGRect rect = [LSYReadParser parserRectWithPoint:point range:&_selectRange frameRef:_frameRef];
        // ...
        // 处理 UI 状态
        if (!CGRectEqualToRect(rect, CGRectZero)) {
            _pathArray = @[NSStringFromCGRect(rect)];
            
            // 触发绘制
            [self setNeedsDisplay];
        }
    }
    else if (longPress.state == UIGestureRecognizerStateEnded) {
        // ...
        // 恢复 UI 状态
    }
}

同上文的逻辑一致,

拿到一个点,一帧文字,

得到选中文字的范围 selectRange,通过传参获取,

得到选中文字的区域,CGRect, 通过返回值获取

+(CGRect)parserRectWithPoint:(CGPoint)point range:(NSRange *)selectRange frameRef:(CTFrameRef)frameRef
{
    CFIndex index = -1;
    CGPathRef pathRef = CTFrameGetPath(frameRef);
    CGRect bounds = CGPathGetBoundingBox(pathRef);
    CGRect rect = CGRectZero;
    
    // 拿到每一行
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frameRef);
    if (!lines) {
        return rect;
    }
    NSInteger lineCount = [lines count];
    CGPoint *origins = malloc(lineCount * sizeof(CGPoint)); //给每行的起始点开辟内存
    if (lineCount) {
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins);
        
        // 查找每一行,看是否包含那个点
        // 查到了,就返回
        for (int i = 0; i<lineCount; i++) {
            CGPoint baselineOrigin = origins[i];
            CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
            CGFloat ascent,descent,linegap; //声明字体的上行高度和下行高度和行距
            CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap);
            CGRect lineFrame = CGRectMake(baselineOrigin.x, CGRectGetHeight(bounds)-baselineOrigin.y-ascent, lineWidth, ascent+descent+linegap+[LSYReadConfig shareInstance].lineSpace);   
            //没有转换坐标系左下角为坐标原点 字体高度为上行高度加下行高度
            if (CGRectContainsPoint(lineFrame,point)){
                // 定位到行了
                CFRange stringRange = CTLineGetStringRange(line);
                // 定位到字
                index = CTLineGetStringIndexForPosition(line, point);
                CGFloat xStart = CTLineGetOffsetForStringIndex(line, index, NULL);
                CGFloat xEnd;
                //默认选中两个单位
                if (index > stringRange.location+stringRange.length-2) {
                    xEnd = xStart;
                    xStart = CTLineGetOffsetForStringIndex(line,index-2,NULL);
                    (*selectRange).location = index-2;
                }
                else{
                    xEnd = CTLineGetOffsetForStringIndex(line,index+2,NULL);
                    (*selectRange).location = index;
                }
                // 选中的 2 个字
                (*selectRange).length = 2;
                rect = CGRectMake(origins[i].x+xStart,baselineOrigin.y-descent,fabs(xStart-xEnd), ascent+descent);
                
                break;
            }
        }
    }
    free(origins);
    return rect;
}


将选中的 2 个字,涂上颜色

触发

	    // 赋值
            _pathArray = ...
            // 触发绘制
            [self setNeedsDisplay];

绘制


-(void)drawRect:(CGRect)rect
{
    if (!_frameRef) {
        return;
    }

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    CGRect leftDot,rightDot = CGRectZero;
    _menuRect = CGRectZero;
    // 绘制选中区域
    [self drawSelectedPath:_pathArray LeftDot:&leftDot RightDot:&rightDot];
    
    // 绘制文字
    CTFrameDraw(_frameRef, ctx);

    if (_imageArray.count) {
        // ...
        // 有图片,绘制图片
    }
    // 绘制左右光标
    // ...
}

// 绘制选中区域
// 有两个返回值, 通过参数返回
// leftDot, 顶部行的区域
// rightDot, 底部行的区域
-(void)drawSelectedPath:(NSArray *)array LeftDot:(CGRect *)leftDot RightDot:(CGRect *)rightDot{
    // ...
    // 处理其他 UI 状态
    CGMutablePathRef _path = CGPathCreateMutable();
    [[UIColor cyanColor]setFill];
    for (int i = 0; i < [array count]; i++) {
        CGRect rect = CGRectFromString([array objectAtIndex:i]);
        CGPathAddRect(_path, NULL, rect);
        // 计算返回状态
        if (i == 0) {
            *leftDot = rect;
            // ...
            // 处理其他 UI 状态
        }
        if (i == [array count]-1) {
            *rightDot = rect;
        }
       
    }
    // 涂色
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextAddPath(ctx, _path);
    CGContextFillPath(ctx);
    CGPathRelease(_path);
    
}


调整选中区域

-(void)pan:(UIPanGestureRecognizer *)pan
{
   
    CGPoint point = [pan locationInView:self];
    // ...
    // 处理其他 UI 状态
    if (pan.state == UIGestureRecognizerStateBegan || pan.state == UIGestureRecognizerStateChanged) {
        [self showMagnifier];
        self.magnifierView.touchPoint = point;
        if (CGRectContainsPoint(_rightRect, point)||CGRectContainsPoint(_leftRect, point)) {
            if (CGRectContainsPoint(_leftRect, point)) {
                _direction = NO;   //从左侧滑动
            }
            else{
                _direction=  YES;    //从右侧滑动
            }
            _selectState = YES;
        }
        if (_selectState) {
            // 计算出来,当前选中区域
            NSArray *path = [LSYReadParser parserRectsWithPoint:point range:&_selectRange frameRef:_frameRef paths:_pathArray direction:_direction];
            _pathArray = path;
            // 去渲染
            [self setNeedsDisplay];
        }
       
    }
    if (pan.state == UIGestureRecognizerStateEnded) {
        // ...
        // 处理其他 UI 状态
    }
    
}

计算出来,当前选中区域的逻辑,与上文的代码,差不多

4, 框出每一行

有了前面的基础, 框出每一行就很简单

-(void)drawRect:(CGRect)rect
{
    if (!_frameRef) {
        return;
    }
    // 框出每一行
    
  
    CGPathRef pathRef = CTFrameGetPath(_frameRef);
    CGRect bounds = CGPathGetBoundingBox(pathRef);
    
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(_frameRef);
    if (lines) {
        NSInteger lineCount = [lines count];
        CGPoint *origins = malloc(lineCount * sizeof(CGPoint)); 
        //给每行的起始点开辟内存
        CTFrameGetLineOrigins(_frameRef, CFRangeMake(0, 0), origins);
        // 遍历每一行
        for (int i = 0; i<lineCount; i++) {
            CGPoint baselineOrigin = origins[i];
            CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
            CGFloat ascent,descent,linegap; 
            //声明字体的上行高度和下行高度和行距
            CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap);

            // 如果是空行,不用管
            if (lineWidth > 1){
                CGRect lineFrame = CGRectMake(baselineOrigin.x, CGRectGetHeight(bounds)-baselineOrigin.y-ascent, lineWidth, ascent+descent+linegap);
                //没有转换坐标系左下角为坐标原点 字体高度为上行高度加下行高度
                UIBezierPath * path = [UIBezierPath bezierPathWithRect: lineFrame];
                [UIColor.orangeColor setStroke];
                [path stroke];
            }
            
        }
        free(origins);
    }
    
    // 渲染文字
    
    // 其他照旧
    // ...

github repo

相关博客 iOS: .txt 小说阅读器功能开发的 5 个老套路

标签:...,frameRef,point,套路,let,CGRect,阅读器,txt,rect
来源: https://blog.csdn.net/dengjiangszhan/article/details/113146173

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有