Skip to content

Commit b8bc624

Browse files
committed
修复 QMUITextView 在输入文字换行时的各种抖动和滚动位置不准确的问题
1 parent 80cdf74 commit b8bc624

1 file changed

Lines changed: 33 additions & 19 deletions

File tree

QMUIKit/UIKitExtensions/QMUITextView.m

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
@interface QMUITextView ()
2323

2424
@property(nonatomic, assign) BOOL debug;
25-
@property(nonatomic, assign) BOOL textChangedBecauseOfPaste; // 标志本次触发对handleTextChange:的调用,是否因为粘贴
25+
@property(nonatomic, assign) BOOL shouldRejectSystemScroll;// 如果在 handleTextChanged: 里主动调整 contentOffset,则为了避免被系统的自动调整覆盖,会利用这个标记去屏蔽系统对 setContentOffset: 的调用
2626
@property(nonatomic, assign) BOOL callingSizeThatFitsByAutoResizable; // 标志本次调用 sizeThatFits: 是因为 handleTextChange: 里计算高度导致的
2727

2828
@property(nonatomic, strong) UILabel *placeholderLabel;
@@ -217,7 +217,7 @@ - (void)handleTextChanged:(id)sender {
217217
// 计算高度
218218
if (self.autoResizable) {
219219

220-
// 注意,这里 iOS 8 及以下有兼容问题,请查看文件里的 sizeThatFits:
220+
// 注意,这里 iOS 10 以下有兼容问题,请查看文件里的 sizeThatFits:
221221
self.callingSizeThatFitsByAutoResizable = YES;
222222
CGFloat resultHeight = [textView sizeThatFits:CGSizeMake(CGRectGetWidth(self.bounds), CGFLOAT_MAX)].height;
223223
self.callingSizeThatFitsByAutoResizable = NO;
@@ -231,27 +231,28 @@ - (void)handleTextChanged:(id)sender {
231231
}
232232
}
233233

234-
// iOS7的textView在内容可滚动的情况下,最后一行输入时文字会跑到可视区域外,因此要修复一下
235-
// 由于我们在文字换行的瞬间更改了输入框高度,所以即便内容不可滚动,换行瞬间contentOffset也是错的,所以这里完全接管了对contentOffset的自动调整
234+
// 系统的 UITextView 在文字可滚动的情况下在最后一行输入,文字是贴边的(并不会考虑 textContainerInset.bottom),所以这里接管了 UITextView 的文字滚动
236235
CGRect caretRect = [textView caretRectForPosition:textView.selectedTextRange.end];
237-
if (self.debug) NSLog(@"调整前,caretRect.maxY = %f, contentOffset.y = %f, bounds.height = %f", CGRectGetMaxY(caretRect), textView.contentOffset.y, CGRectGetHeight(textView.bounds));
236+
if (self.debug) NSLog(@"调整前,caretRect.maxY = %.2f, contentOffset.y = %.2f, contentSize.height = %.2f bounds.height = %.2f", CGRectGetMaxY(caretRect), textView.contentOffset.y, textView.contentSize.height, CGRectGetHeight(textView.bounds));
238237

239238
CGFloat caretMarginBottom = self.textContainerInset.bottom;
240-
if (ceil(CGRectGetMaxY(caretRect) + caretMarginBottom) >= textView.contentOffset.y + CGRectGetHeight(textView.bounds)) {
241-
CGFloat contentOffsetY = MAX(0, CGRectGetMaxY(caretRect) + caretMarginBottom - CGRectGetHeight(textView.bounds));
242-
if (self.debug) NSLog(@"调整后,contentOffset.y = %f", contentOffsetY);
239+
if (CGRectGetMaxY(caretRect) + caretMarginBottom >= textView.contentOffset.y + CGRectGetHeight(textView.bounds)) {
240+
CGFloat contentOffsetY = fmax(0, ceil(CGRectGetMaxY(caretRect) + caretMarginBottom - CGRectGetHeight(textView.bounds)));
241+
if (self.debug) NSLog(@"调整后,contentOffset.y = %.2f, contentSize.height = %.2f", contentOffsetY, textView.contentSize.height);
243242

244-
// 如果是粘贴导致光标掉出可视区域,则用动画去调整它(如果不用动画会不准,因为此时contentSize还是错的)
245-
// 如果是普通的键入换行导致光标掉出可视区域,则不用动画,否则会跳来跳去,但这会带来的问题就是换行没动画,不优雅😂
246-
[textView setContentOffset:CGPointMake(textView.contentOffset.x, contentOffsetY) animated:self.textChangedBecauseOfPaste ? YES : NO];
243+
self.shouldRejectSystemScroll = YES;
244+
// 用 dispatch 延迟一下,因为在文字发生换行时,系统自己会做一些滚动,我们要延迟一点才能避免被系统的滚动覆盖
245+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
246+
self.shouldRejectSystemScroll = NO;
247+
[textView setContentOffset:CGPointMake(textView.contentOffset.x, contentOffsetY) animated:NO];
248+
});
247249
}
248-
self.textChangedBecauseOfPaste = NO;
249250
}
250251
}
251252

252253
- (CGSize)sizeThatFits:(CGSize)size {
253-
// iOS 8 调用 sizeThatFits: 会导致文字跳动,因此自己计算 https://github.com/QMUI/QMUI_iOS/issues/92
254-
if (IOS_VERSION < 9.0 && IOS_VERSION >= 8.0 && self.callingSizeThatFitsByAutoResizable) {
254+
// iOS 10 以下调用 sizeThatFits: 会导致文字跳动,因此自己计算 https://github.com/QMUI/QMUI_iOS/issues/92
255+
if (IOS_VERSION < 10.0 && self.callingSizeThatFitsByAutoResizable) {
255256
CGFloat contentWidth = size.width - UIEdgeInsetsGetHorizontalValue(self.textContainerInset) - UIEdgeInsetsGetHorizontalValue(self.contentInset);
256257
CGRect textRect = [self.attributedText boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil];
257258
CGSize resultSize = CGSizeMake(size.width, CGRectGetHeight(textRect) + UIEdgeInsetsGetVerticalValue(self.textContainerInset) + UIEdgeInsetsGetVerticalValue(self.contentInset));
@@ -287,11 +288,6 @@ - (void)updatePlaceholderLabelHidden {
287288
}
288289
}
289290

290-
- (void)paste:(id)sender {
291-
self.textChangedBecauseOfPaste = YES;
292-
[super paste:sender];
293-
}
294-
295291
- (NSUInteger)lengthWithString:(NSString *)string {
296292
return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length;
297293
}
@@ -422,6 +418,24 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
422418
}
423419
}
424420

421+
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated {
422+
if (!self.shouldRejectSystemScroll) {
423+
[super setContentOffset:contentOffset animated:animated];
424+
if (self.debug) NSLog(@"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
425+
} else {
426+
if (self.debug) NSLog(@"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
427+
}
428+
}
429+
430+
- (void)setContentOffset:(CGPoint)contentOffset {
431+
if (!self.shouldRejectSystemScroll) {
432+
[super setContentOffset:contentOffset];
433+
if (self.debug) NSLog(@"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
434+
} else {
435+
if (self.debug) NSLog(@"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
436+
}
437+
}
438+
425439
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
426440
if ([self.originalDelegate respondsToSelector:_cmd]) {
427441
[self.originalDelegate scrollViewDidZoom:scrollView];

0 commit comments

Comments
 (0)