整理了一份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.m
的deleteBackward
方法里加了一段代码:
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;
好啦,就这样吧!第一次发文章😄