用YYTextView 实现填空题作答功能

整理了一份Demo,因为每个项目具体的需求不一样,我只把基本的功能整理出来了
Demo放在GitHub上
项目中要实现填空题的作答功能,比如诗词填空:床前明月光,___________。举头望明月,________。要求只能编辑横线部分。
首先想到的是强大的YYKit,先在网上找了找,发现有一种方案是用label 加textfield的这种富文本编辑的方式实现的,虽然大体符合需求,但是排版会比较难看。
最后决定用YYTextView去实现,原理就是根据正则匹配题干和下划线,整个题目会被填空部分分割成几块,把各个分块binding,然后控制光标位置,只让光标落在下划线上。

创建题目:

   NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"#(填空题)枯藤老树昏鸦,#      #,古道西风瘦马。#      #,断肠人在天涯。# "];
   text.yy_font = [UIFont systemFontOfSize:17];
   text.yy_lineSpacing = 5;
   text.yy_color = [UIColor blackColor];
   YYTextView *textView = [YYTextView new];
   textView.textParser = [YYTextEditBindingParser new];
   textView.attributedText = text;
   textView.frame = CGRectMake(5, 100, CGRectGetWidth(self.view.frame)-10, 200);
   textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
   textView.delegate = self;
   if (kiOS7Later) {
       textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
   }
   textView.scrollIndicatorInsets = textView.contentInset;
   [self.view addSubview:textView];
   self.textView = textView;

在代理方法里面控制光标位置

#pragma mark YYTextViewDelegate
- (BOOL)textViewShouldBeginEditing:(YYTextView *)textView{
    
    if (textView.selectedRange.location==0 || textView.selectedRange.location >= textView.text.length) {
        return NO;
    }else{
        return [self controllCursorRangeForTextView:textView];
    }
}
- (void)textViewDidChangeSelection:(YYTextView *)textView{
    if (textView.selectedRange.location==0 || textView.selectedRange.location >= textView.text.length) {
        [textView endEditing:YES];
    }else {
        [self controllCursorRangeForTextView:textView];
    }
    
}

控制光标

- (BOOL)controllCursorRangeForTextView:(YYTextView *)textView{
    YYTextEditBindingParser *textParser = textView.textParser;
    for (NSString *rangeStr in textParser.gapRangeArr) {
        NSRange range = NSRangeFromString(rangeStr);
        if (textView.selectedRange.location >= range.location && textView.selectedRange.location < range.location + 3) {
            textView.selectedRange = NSMakeRange(range.location + 3,0);
            return YES;
        }else if(textView.selectedRange.location > (range.location + range.length -3)&&textView.selectedRange.location <= (range.location + range.length)){
            textView.selectedRange = NSMakeRange(range.location + range.length - 3,0);
            return YES;
        }
    }
    return YES;
}

为了不让删除binding的字符串我在YYtextView里面加了个代理方法,在我这里实现一下:

- (BOOL)textViewShouldDeleteBinding:(YYTextView *)textView{
    return NO;
}

YYTextEditBindingParser这个类用于binding和对输入内容加下划线,有个属性gapRangeArr用来保存填空部分的range

@interface YYTextEditBindingParser :NSObject <YYTextParser>
@property (nonatomic, strong) NSRegularExpression *regex;
@property (nonatomic, strong) NSRegularExpression *gapRegex;
@property(nonatomic, strong)NSArray <NSString *>*gapRangeArr;

@end

@implementation YYTextEditBindingParser

- (instancetype)init {
    self = [super init];
    NSString *pattern1 = @"#([^#]*)#";
    self.regex = [[NSRegularExpression alloc] initWithPattern:pattern1 options:kNilOptions error:nil];
    NSString *pattern2 = @"\\s{3}([^\\s{3}]*)\\s{3}";
    self.gapRegex = [[NSRegularExpression alloc] initWithPattern:pattern2 options:kNilOptions error:nil];
    return self;
}
- (NSRange)_replaceTextInRange:(NSRange)range withLength:(NSUInteger)length selectedRange:(NSRange)selectedRange {
    // no change
    if (range.length == length) return selectedRange;
    // right
    if (range.location >= selectedRange.location + selectedRange.length) return selectedRange;
    // left
    if (selectedRange.location >= range.location + range.length) {
        selectedRange.location = selectedRange.location + length - range.length;
        return selectedRange;
    }
    // same
    if (NSEqualRanges(range, selectedRange)) {
        selectedRange.length = length;
        return selectedRange;
    }
    // one edge same
    if ((range.location == selectedRange.location && range.length < selectedRange.length) ||
        (range.location + range.length == selectedRange.location + selectedRange.length && range.length < selectedRange.length)) {
        selectedRange.length = selectedRange.length + length - range.length;
        return selectedRange;
    }
    selectedRange.location = range.location + length;
    selectedRange.length = 0;
    return selectedRange;
}
- (BOOL)parseText:(NSMutableAttributedString *)text selectedRange:(NSRangePointer)range {
    __block BOOL changed = NO;
    NSArray *matches = [_regex matchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length)];
    NSRange selectedRange = range ? *range : NSMakeRange(0, 0);
    NSUInteger cutLength = 0;
    for (NSUInteger i = 0, max = matches.count; i < max; i++) {
        NSTextCheckingResult *one = matches[i];
        NSRange oneRange = one.range;
        if (oneRange.length == 0) continue;
        oneRange.location -= cutLength;
        NSString *subStr = [text.string substringWithRange:NSMakeRange(oneRange.location+1, oneRange.length-2)];
        CGFloat fontSize = 12; // CoreText default value
        CTFontRef font = (__bridge CTFontRef)([text yy_attribute:NSFontAttributeName atIndex:oneRange.location]);
        if (font) fontSize = CTFontGetSize(font);
        NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:subStr];
        [text replaceCharactersInRange:oneRange withString:atr.string];
        [text yy_removeDiscontinuousAttributesInRange:NSMakeRange(oneRange.location, atr.length)];
        [text addAttributes:atr.yy_attributes range:NSMakeRange(oneRange.location, atr.length)];
        selectedRange = [self _replaceTextInRange:oneRange withLength:atr.length selectedRange:selectedRange];
        NSRange bindlingRange = NSMakeRange(oneRange.location, oneRange.length-2);
        YYTextBinding *binding = [YYTextBinding bindingWithDeleteConfirm:YES];
        [text yy_setTextBinding:binding range:bindlingRange]; /// Text binding
        [text yy_setColor:[UIColor colorWithRed:0.000 green:0.519 blue:1.000 alpha:1.000] range:bindlingRange];
        cutLength += 2;

    }
    NSArray *gapMatches = [_gapRegex matchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length)];
    if (gapMatches.count == 0) return NO;

//    NSRange lastOneRange = NSMakeRange(0, 0);
    NSMutableArray *gapRangeTempArr = [NSMutableArray array];
    for (NSUInteger i = 0, max = gapMatches.count; i < max; i++) {
        NSTextCheckingResult *one = gapMatches[i];
        NSRange oneRange = one.range;
        YYTextDecoration *decoration = [YYTextDecoration new];
        [text yy_setTextUnderline:decoration range:oneRange];
        [gapRangeTempArr addObject:NSStringFromRange(oneRange)];

    }
    self.gapRangeArr  = gapRangeTempArr;
    if (range) *range = selectedRange;
    
    return changed;
}

@end

YYTextView.mdeleteBackward方法里加了一段代码:

if (binding && binding.deleteConfirm) {
            if ([self.delegate respondsToSelector:@selector(textViewShouldDeleteBinding:)]) {
                if (![self.delegate textViewShouldDeleteBinding:self]) {
                    return;
                }
            }
            _state.deleteConfirm = YES;
            [_inputDelegate selectionWillChange:self];
            _selectedTextRange = [YYTextRange rangeWithRange:effectiveRange];
            _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
            [_inputDelegate selectionDidChange:self];
            
            [self _updateOuterProperties];
            [self _updateSelectionView];
            return;
        }

就是在删除的时候,如果是binding的字符串,就return;

好啦,就这样吧!第一次发文章😄

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

推荐阅读更多精彩内容