• (一一二)图文混排中特殊文字的点击与事件处理


    在上一篇文章(一一一)图文混排基础 -利用正则分割和拼接属性字符串中提到了对attributedText的特殊处理,将其中的话题、URL都用红色进行了表示,如下图所示:

    本节要实现的功能是这样的attributedText在点击话题、URL时应该有所响应,而在点击其他位置的时候把事件传递给父类处理。


    要获取到点击的位置很简单,只需要重写touchesBegan方法即可拿到触摸点,比较棘手的是判断出触摸位置的文字的范围(CGRect),如果能拿到点击文字的rect,就能对rect进行高亮处理和事件处理。

    把这个需求拆分,我们应该按照下面的步骤进行:

    ①在拼接attributedText的时候记录下所有特殊文字的位置和内容、保存到模型里,再把模型存入数组。

    #import <Foundation/Foundation.h>
    
    @interface TextSpecial : NSObject
    
    @property (nonatomic, copy) NSString *text;
    @property (nonatomic, assign) NSRange range;
    
    @end
    因为微博数据也是通过模型传递的,为微博模型定义属性来保存所有的模型,以便后续处理。

    @property (nonatomic, strong) NSMutableArray *specialSegments;
    在上节介绍的代码中,加入一段用于保存特殊文字的代码:

        [self.specialSegments removeAllObjects];
        
        NSInteger cnt = parts.count;
        for (NSInteger i = 0; i < cnt; i++) {
            TextSegment *ts = parts[i];
            if (ts.isEmotion) {
                NSTextAttachment *attach = [[NSTextAttachment alloc] init];
                attach.image = [UIImage imageNamed:@"avatar_vgirl"];
                attach.bounds = CGRectMake(0, -3, 15, 15);
                NSAttributedString *emotionStr = [NSAttributedString attributedStringWithAttachment:attach];
                [attributedText appendAttributedString:emotionStr];
            }else if(ts.isSpecial){
                NSAttributedString *special = [[NSAttributedString alloc] initWithString:ts.text attributes:@{NSForegroundColorAttributeName:[UIColor redColor]}];
                
                // 保存所有特殊文字的内容和位置,保存到模型,再把模型存入数组。
                TextSpecial *spec = [[TextSpecial alloc] init];
                spec.text = ts.text;
                NSInteger loc = attributedText.length;
                NSInteger len = ts.text.length;
                spec.range = NSMakeRange(loc, len);
                [self.specialSegments addObject:spec];
                
                [attributedText appendAttributedString:special];
                
            }else{
                [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:ts.text]];
            }
        }

    微博模型在传递给用于显示微博的Cell时,可以把上面的特殊文字模型数组传入。


    ②拿到了所有特殊字符的位置,下面的需求就是通过特殊文字的位置得到特殊文字的rect,因为只要遍历这些rect,如果触摸点在这些rect之中,则把相关位置的rect都高亮和后续处理即可。

    现在问题的关键就是根据字串位置拿到rect,这个功能只有通过UITextView才能实现,UITextView按照下面的步骤可以得到特殊文字的所有rect。

    这里之所以提到所有rect,是因为可能出现特殊文字有换行的情况,则两行的特殊文字都应该被高亮,这样就会有多个rect,因此传入一个特殊文字的范围,返回一系列rect是必要的

    步骤如下:

    1.设置TextView的selectedRange属性,用特殊文字的range传入。

    2.调用TextView的selectionRectsForRange:方法,传入TextView的selectedTextRange属性,拿到特殊文字的rect数组。

    3.因为只是为了拿到rect而选中了TextView,并不是为了真的选中部分,因此应该取消选择,让selectedRange回到(0,0),即不选择。

    self.tv.selectedRange = spec.range; // spec是一个特殊文字模型,其中的range指的是这个特殊文字在attributedText中的位置。
    NSArray *rects = [self.tv selectionRectsForRange:self.tv.selectedTextRange];
    self.tv.selectedRange = NSMakeRange(0, 0); // 重置选中范围,只是为了使用选中范围来获取rect而不是真的选中。

    微博Cell中用于显示微博主体的为自定义的UITextView,为了方便拓展,使用UIView包着UITextView,重写layoutSubviews方法来让UITextView的尺寸和UIView一致,而这个UIView暴露出attributedText用于显示上一节处理好的文字,暴露出特殊文字数组,用于接收上面计算的结果,进行后续判断。

    这个自定义TextView的.h代码如下:

    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    
    @interface SGStatusTextView : UIView
    
    @property (nonatomic, copy) NSAttributedString *attributedText;
    @property (nonatomic, strong) NSArray *specialSegments;
    
    @end
    主要的初始化代码,主要包括创建TextView的尺寸,把从模型传入的attributedText传入到TextView的attributedText以便显示。

    @interface SGStatusTextView ()
    
    @property (nonatomic, weak) UITextView *tv;
    
    @end
    
    @implementation SGStatusTextView
    
    - (instancetype)initWithFrame:(CGRect)frame{
        
        self = [super initWithFrame:frame];
        if (self) {
            
            UITextView *tv = [[UITextView alloc] init];
            
            // UITextView默认上面有20的内边距,应该修改textContainerInset
            tv.textContainerInset = UIEdgeInsetsMake(0, -5, 0, -5);
            tv.editable = NO;
            tv.scrollEnabled = NO;
            tv.userInteractionEnabled = NO;
            //self.userInteractionEnabled = NO;
            tv.backgroundColor = [UIColor clearColor];
            tv.font = ContentFont;
            
            [self addSubview:tv];
            _tv = tv;
        }
        
        return self;
        
    }
    
    - (void)setAttributedText:(NSAttributedString *)attributedText{
        
        _tv.attributedText = attributedText;
        
    }
    
    - (void)layoutSubviews{
        
        [super layoutSubviews];
        
        _tv.frame = self.bounds;
        
    }

    下面只要重写touchedBegan,遍历最前面得到的特殊文字的模型数组,对每个模型通过textView拿到rect,再判断触摸点是否在rect内,如果在,这个特殊文字有关的所有rect都要高亮处理(出现在特殊文字换行显示的情况)。代码如下。其中selected变量用于判断是否有触摸到特殊文字,如果处理到停止对当前特殊文字所有rect的遍历,再重新开始一次遍历,把所有rect高亮,接着跳出对特殊文字模型数组的遍历,完成一次触摸事件的处理。

    注意高亮文字是通过添加带圆角的UIView遮盖实现的,在index=0处,也就是最下面添加遮盖,可以保证遮盖不挡住文字,用tag标记,以便在触摸结束时删除。

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
        
        CGPoint pt = [[touches anyObject] locationInView:self];
        BOOL selected = NO;
        
        for (TextSpecial *spec in self.specialSegments) {
    
            self.tv.selectedRange = spec.range;
            NSArray *rects = [self.tv selectionRectsForRange:self.tv.selectedTextRange];
            self.tv.selectedRange = NSMakeRange(0, 0); // 重置选中范围,只是为了使用选中范围来获取rect而不是真的选中。
            for (UITextSelectionRect *selectionRect in rects) {
                CGRect rect = selectionRect.rect;
                if (rect.size.width == 0 || rect.size.height == 0) continue;
                
                if (CGRectContainsPoint(rect, pt)) {
                    selected = YES;
                    break;
                }
                
            }
            
            if (selected) {
                for (UITextSelectionRect *selectionRect in rects) {
                    CGRect rect = selectionRect.rect;
                    if (rect.size.width == 0 || rect.size.height == 0) continue;
                    
                    UIView *cover = [[UIView alloc] initWithFrame:rect];
                    cover.layer.cornerRadius = 5;
                    cover.backgroundColor = [UIColor greenColor];
                    cover.tag = CoverTag;
                    [self insertSubview:cover atIndex:0];
                    
                }
                break;
            }
            
        }
        
    }
    触摸事件的完成包括取消和完成两种情况,如果是完成,应该延时移除遮盖,否则单击看不出反应,只有长按才能看出反应(因为单击之后遮盖立刻被移除)。取消则不必延时,因为取消是因为被其他事件打断。

    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
        
        // 为了能实现单击的响应,应该延时移除视图。
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            for (UIView *view in self.subviews) {
                if (view.tag == CoverTag) {
                    [view removeFromSuperview];
                }
            }
        });
        
    }
    
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
        
        for (UIView *view in self.subviews) {
            if (view.tag == CoverTag) {
                [view removeFromSuperview];
            }
        }
        
    }
    

    ③上面处理之后虽然可以处理特殊文字的点击事件了,但是在普通文字部分的点击也被textView给处理了,这样就不能实现正常的微博业务逻辑中,点击Cell中的非特殊部分,由tableView处理事件。

    这就需要用到系统的事件响应机制:

    iOS的事件处理优先考虑最上面的控件,系统会调用下面的方法来询问是否由当前控件处理事件:

    // 触摸事件传递时会调用下面的方法询问是否处理
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    
        // point是触摸点,用于判断是否在当前控件内,返回NO表示不处理,返回YES表示处理触摸事件。
        return NO;
        
    }
    不要一味的返回YES,因为返回YES即使点击的位置不在当前控件上,但是当前控件在最上层,事件会被私吞。

    在调用了这个事件之后,如果返回YES,系统会继续调用当前控件的下面的方法询问应该由谁处理该事件。

    // 只有pointInside::返回YES的控件才会调用此方法,可以决定由谁处理事件,不实现由自己处理。
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        
    }
    通过这个方法可以由当前控件随意决定由哪个控件处理触摸事件。


    经过上面的逻辑,我们只需要重写pointInside::方法,判断触摸点是否在特殊文字的rect上,如果是则处理事件,否则不处理,交由父控件Cell、再传递到祖先控件tableView处理。

    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    
        for (TextSpecial *spec in self.specialSegments) {
            
            self.tv.selectedRange = spec.range;
            NSArray *rects = [self.tv selectionRectsForRange:self.tv.selectedTextRange];
            self.tv.selectedRange = NSMakeRange(0, 0); // 重置选中范围,只是为了使用选中范围来获取rect而不是真的选中。
            for (UITextSelectionRect *selectionRect in rects) {
                CGRect rect = selectionRect.rect;
                if (rect.size.width == 0 || rect.size.height == 0) continue;
                
                if (CGRectContainsPoint(rect, point)) {
                    return YES;
                }
            
            }
        }
    
        return NO;
        
    }
    


  • 相关阅读:
    WTL for Visual Studio 2012 配置详解
    自己动手让Visual Studio的Win32向导支持生成对话框程序
    改造联想Y480的快捷键(跨进程替换窗口过程(子类化)的实现——远程线程注入)
    Visual Studio 2012 Ultimate RTM 体验(附下载地址和KEY)
    VC++实现获取文件占用空间大小的两种方法(非文件大小)
    为Visual Studio添加默认INCLUDE包含路径一劳永逸的方法(更新)
    Winsdows 8 环境下搭建Windows Phone 开发环境
    Linq to Visual Tree可视化树的类Linq查询扩展API(译)
    检测元素是否在界面可显示区域
    Debug the Metro Style App:Registration of the app failed
  • 原文地址:https://www.cnblogs.com/aiwz/p/6154071.html
Copyright © 2020-2023  润新知